blob: 071f5f5ad51a9824c61196cba487f3e8c548a6c6 [file] [log] [blame]
Serge Bazanskif05e80a2021-10-12 11:53:34 +02001package consensus
2
3import (
4 "crypto/ed25519"
5 "crypto/x509"
6 "fmt"
7 "net"
8 "net/url"
9 "strconv"
10 "time"
11
Lorenz Brund13c1c62022-03-30 19:58:58 +020012 clientv3 "go.etcd.io/etcd/client/v3"
13 "go.etcd.io/etcd/server/v3/embed"
Serge Bazanskif05e80a2021-10-12 11:53:34 +020014
15 "source.monogon.dev/metropolis/node"
Serge Bazanskif05e80a2021-10-12 11:53:34 +020016 "source.monogon.dev/metropolis/node/core/localstorage"
Tim Windelschmidt9f21f532024-05-07 15:14:20 +020017 "source.monogon.dev/osbase/pki"
Serge Bazanskif05e80a2021-10-12 11:53:34 +020018)
19
20// Config describes the startup configuration of a consensus instance.
21type Config struct {
22 // Data directory (persistent, encrypted storage) for etcd.
23 Data *localstorage.DataEtcdDirectory
24 // Ephemeral directory for etcd.
25 Ephemeral *localstorage.EphemeralConsensusDirectory
26
27 // JoinCluster is set if this instance is to join an existing cluster for the
28 // first time. If not set, it's assumed this instance has ran before and has all
29 // the state on disk required to become part of whatever cluster it was before.
30 // If that data is not present, a new cluster will be bootstrapped.
31 JoinCluster *JoinCluster
32
Jan Schär39d9c242024-09-24 13:49:55 +020033 // NodeID is the node ID, which is also used to identify consensus nodes.
34 NodeID string
35
Serge Bazanskif05e80a2021-10-12 11:53:34 +020036 // NodePrivateKey is the node's main private key which is also used for
Jan Schär39d9c242024-09-24 13:49:55 +020037 // Metropolis PKI. The same key will be used for consensus nodes, but
Serge Bazanskif05e80a2021-10-12 11:53:34 +020038 // different certificates will be used.
39 NodePrivateKey ed25519.PrivateKey
40
41 testOverrides testOverrides
42}
43
44// JoinCluster is all the data required for a node to join (for the first time)
45// an already running cluster. This data is available from an already running
46// consensus member by performing AddNode, which is called by the Curator when
47// new etcd nodes are added to the cluster.
48type JoinCluster struct {
49 CACertificate *x509.Certificate
50 NodeCertificate *x509.Certificate
51 // ExistingNodes are an arbitrarily ordered list of other consensus members that
52 // the node should attempt to contact.
53 ExistingNodes []ExistingNode
54 // InitialCRL is a certificate revocation list for this cluster. After the node
55 // starts, a CRL on disk will be maintained reflecting the PKI state within etcd.
56 InitialCRL *pki.CRL
57}
58
59// ExistingNode is the peer URL and name of an already running consensus instance.
60type ExistingNode struct {
61 Name string
62 URL string
63}
64
65func (e *ExistingNode) connectionString() string {
66 return fmt.Sprintf("%s=%s", e.Name, e.URL)
67}
68
69func (c *Config) nodePublicKey() ed25519.PublicKey {
70 return c.NodePrivateKey.Public().(ed25519.PublicKey)
71}
72
73// testOverrides are available to test code to make some things easier in a test
74// environment.
75type testOverrides struct {
76 // externalPort overrides the default port used by the node.
77 externalPort int
78 // externalAddress overrides the address of the node, which is usually its ID.
79 externalAddress string
Tim Windelschmidtc37a8862023-07-19 16:33:21 +020080 // etcdMetricsPort overrides the default etcd metrics port used by the node.
81 etcdMetricsPort int
Serge Bazanskif05e80a2021-10-12 11:53:34 +020082}
83
84// build takes a Config and returns an etcd embed.Config.
85//
86// enablePeers selects whether the etcd instance will listen for peer traffic
87// over TLS. This requires TLS credentials to be present on disk, and will be
88// disabled for bootstrapping the instance.
89func (c *Config) build(enablePeers bool) *embed.Config {
Serge Bazanskif05e80a2021-10-12 11:53:34 +020090 port := int(node.ConsensusPort)
91 if p := c.testOverrides.externalPort; p != 0 {
92 port = p
93 }
Jan Schär39d9c242024-09-24 13:49:55 +020094 host := c.NodeID
Serge Bazanskif05e80a2021-10-12 11:53:34 +020095 if c.testOverrides.externalAddress != "" {
96 host = c.testOverrides.externalAddress
Serge Bazanskif05e80a2021-10-12 11:53:34 +020097 }
Tim Windelschmidtc37a8862023-07-19 16:33:21 +020098 etcdPort := int(node.MetricsEtcdListenerPort)
99 if p := c.testOverrides.etcdMetricsPort; p != 0 {
100 etcdPort = p
101 }
Serge Bazanskif05e80a2021-10-12 11:53:34 +0200102
103 cfg := embed.NewConfig()
104
Jan Schär39d9c242024-09-24 13:49:55 +0200105 cfg.Name = c.NodeID
Serge Bazanskif05e80a2021-10-12 11:53:34 +0200106 cfg.ClusterState = "existing"
107 cfg.InitialClusterToken = "METROPOLIS"
108 cfg.Logger = "zap"
109 cfg.LogOutputs = []string{c.Ephemeral.ServerLogsFIFO.FullPath()}
Tim Windelschmidtc37a8862023-07-19 16:33:21 +0200110 cfg.ListenMetricsUrls = []url.URL{
111 {Scheme: "http", Host: net.JoinHostPort("127.0.0.1", fmt.Sprintf("%d", etcdPort))},
112 }
Serge Bazanskif05e80a2021-10-12 11:53:34 +0200113
114 cfg.Dir = c.Data.Data.FullPath()
115
116 // Client URL, ie. local UNIX socket to listen on for trusted, unauthenticated
117 // traffic.
Lorenz Brun6211e4d2023-11-14 19:09:40 +0100118 cfg.ListenClientUrls = []url.URL{{
Serge Bazanskif05e80a2021-10-12 11:53:34 +0200119 Scheme: "unix",
120 Path: c.Ephemeral.ClientSocket.FullPath() + ":0",
121 }}
122
123 if enablePeers {
124 cfg.PeerTLSInfo.CertFile = c.Data.PeerPKI.Certificate.FullPath()
125 cfg.PeerTLSInfo.KeyFile = c.Data.PeerPKI.Key.FullPath()
126 cfg.PeerTLSInfo.TrustedCAFile = c.Data.PeerPKI.CACertificate.FullPath()
127 cfg.PeerTLSInfo.ClientCertAuth = true
128 cfg.PeerTLSInfo.CRLFile = c.Data.PeerCRL.FullPath()
129
Lorenz Brun6211e4d2023-11-14 19:09:40 +0100130 cfg.ListenPeerUrls = []url.URL{{
Serge Bazanskif05e80a2021-10-12 11:53:34 +0200131 Scheme: "https",
132 Host: fmt.Sprintf("[::]:%d", port),
133 }}
Lorenz Brun6211e4d2023-11-14 19:09:40 +0100134 cfg.AdvertisePeerUrls = []url.URL{{
Serge Bazanskif05e80a2021-10-12 11:53:34 +0200135 Scheme: "https",
136 Host: net.JoinHostPort(host, strconv.Itoa(port)),
137 }}
138 } else {
139 // When not enabling peer traffic, listen on loopback. We would not listen at
140 // all, but etcd seems to prevent us from doing that.
Lorenz Brun6211e4d2023-11-14 19:09:40 +0100141 cfg.ListenPeerUrls = []url.URL{{
Serge Bazanskif05e80a2021-10-12 11:53:34 +0200142 Scheme: "http",
143 Host: fmt.Sprintf("127.0.0.1:%d", port),
144 }}
Lorenz Brun6211e4d2023-11-14 19:09:40 +0100145 cfg.AdvertisePeerUrls = []url.URL{{
Serge Bazanskif05e80a2021-10-12 11:53:34 +0200146 Scheme: "http",
147 Host: fmt.Sprintf("127.0.0.1:%d", port),
148 }}
149 }
150
Jan Schär39d9c242024-09-24 13:49:55 +0200151 cfg.InitialCluster = cfg.InitialClusterFromName(c.NodeID)
Serge Bazanskif05e80a2021-10-12 11:53:34 +0200152 if c.JoinCluster != nil {
153 for _, n := range c.JoinCluster.ExistingNodes {
154 cfg.InitialCluster += "," + n.connectionString()
155 }
156 }
157 return cfg
158}
159
160// localClient returns an etcd client connected to the socket as configured in
161// Config.
162func (c *Config) localClient() (*clientv3.Client, error) {
163 socket := c.Ephemeral.ClientSocket.FullPath()
164 return clientv3.New(clientv3.Config{
165 Endpoints: []string{fmt.Sprintf("unix://%s:0", socket)},
Serge Bazanskib76b8d12023-03-16 00:46:56 +0100166 DialTimeout: 2 * time.Second,
Serge Bazanskif05e80a2021-10-12 11:53:34 +0200167 })
168}