blob: f7fe954d1b5002d1058324e71166770212621da8 [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"
16 "source.monogon.dev/metropolis/node/core/identity"
17 "source.monogon.dev/metropolis/node/core/localstorage"
18 "source.monogon.dev/metropolis/pkg/pki"
19)
20
21// Config describes the startup configuration of a consensus instance.
22type Config struct {
23 // Data directory (persistent, encrypted storage) for etcd.
24 Data *localstorage.DataEtcdDirectory
25 // Ephemeral directory for etcd.
26 Ephemeral *localstorage.EphemeralConsensusDirectory
27
28 // JoinCluster is set if this instance is to join an existing cluster for the
29 // first time. If not set, it's assumed this instance has ran before and has all
30 // the state on disk required to become part of whatever cluster it was before.
31 // If that data is not present, a new cluster will be bootstrapped.
32 JoinCluster *JoinCluster
33
34 // NodePrivateKey is the node's main private key which is also used for
35 // Metropolis PKI. The same key will be used to identify consensus nodes, but
36 // different certificates will be used.
37 NodePrivateKey ed25519.PrivateKey
38
39 testOverrides testOverrides
40}
41
42// JoinCluster is all the data required for a node to join (for the first time)
43// an already running cluster. This data is available from an already running
44// consensus member by performing AddNode, which is called by the Curator when
45// new etcd nodes are added to the cluster.
46type JoinCluster struct {
47 CACertificate *x509.Certificate
48 NodeCertificate *x509.Certificate
49 // ExistingNodes are an arbitrarily ordered list of other consensus members that
50 // the node should attempt to contact.
51 ExistingNodes []ExistingNode
52 // InitialCRL is a certificate revocation list for this cluster. After the node
53 // starts, a CRL on disk will be maintained reflecting the PKI state within etcd.
54 InitialCRL *pki.CRL
55}
56
57// ExistingNode is the peer URL and name of an already running consensus instance.
58type ExistingNode struct {
59 Name string
60 URL string
61}
62
63func (e *ExistingNode) connectionString() string {
64 return fmt.Sprintf("%s=%s", e.Name, e.URL)
65}
66
67func (c *Config) nodePublicKey() ed25519.PublicKey {
68 return c.NodePrivateKey.Public().(ed25519.PublicKey)
69}
70
71// testOverrides are available to test code to make some things easier in a test
72// environment.
73type testOverrides struct {
74 // externalPort overrides the default port used by the node.
75 externalPort int
76 // externalAddress overrides the address of the node, which is usually its ID.
77 externalAddress string
Tim Windelschmidtc37a8862023-07-19 16:33:21 +020078 // etcdMetricsPort overrides the default etcd metrics port used by the node.
79 etcdMetricsPort int
Serge Bazanskif05e80a2021-10-12 11:53:34 +020080}
81
82// build takes a Config and returns an etcd embed.Config.
83//
84// enablePeers selects whether the etcd instance will listen for peer traffic
85// over TLS. This requires TLS credentials to be present on disk, and will be
86// disabled for bootstrapping the instance.
87func (c *Config) build(enablePeers bool) *embed.Config {
88 nodeID := identity.NodeID(c.nodePublicKey())
89 port := int(node.ConsensusPort)
90 if p := c.testOverrides.externalPort; p != 0 {
91 port = p
92 }
93 host := nodeID
94 var extraNames []string
95 if c.testOverrides.externalAddress != "" {
96 host = c.testOverrides.externalAddress
97 extraNames = append(extraNames, host)
98 }
Tim Windelschmidtc37a8862023-07-19 16:33:21 +020099 etcdPort := int(node.MetricsEtcdListenerPort)
100 if p := c.testOverrides.etcdMetricsPort; p != 0 {
101 etcdPort = p
102 }
Serge Bazanskif05e80a2021-10-12 11:53:34 +0200103
104 cfg := embed.NewConfig()
105
106 cfg.Name = nodeID
107 cfg.ClusterState = "existing"
108 cfg.InitialClusterToken = "METROPOLIS"
109 cfg.Logger = "zap"
110 cfg.LogOutputs = []string{c.Ephemeral.ServerLogsFIFO.FullPath()}
Tim Windelschmidtc37a8862023-07-19 16:33:21 +0200111 cfg.ListenMetricsUrls = []url.URL{
112 {Scheme: "http", Host: net.JoinHostPort("127.0.0.1", fmt.Sprintf("%d", etcdPort))},
113 }
Serge Bazanskif05e80a2021-10-12 11:53:34 +0200114
115 cfg.Dir = c.Data.Data.FullPath()
116
117 // Client URL, ie. local UNIX socket to listen on for trusted, unauthenticated
118 // traffic.
119 cfg.LCUrls = []url.URL{{
120 Scheme: "unix",
121 Path: c.Ephemeral.ClientSocket.FullPath() + ":0",
122 }}
123
124 if enablePeers {
125 cfg.PeerTLSInfo.CertFile = c.Data.PeerPKI.Certificate.FullPath()
126 cfg.PeerTLSInfo.KeyFile = c.Data.PeerPKI.Key.FullPath()
127 cfg.PeerTLSInfo.TrustedCAFile = c.Data.PeerPKI.CACertificate.FullPath()
128 cfg.PeerTLSInfo.ClientCertAuth = true
129 cfg.PeerTLSInfo.CRLFile = c.Data.PeerCRL.FullPath()
130
131 cfg.LPUrls = []url.URL{{
132 Scheme: "https",
133 Host: fmt.Sprintf("[::]:%d", port),
134 }}
135 cfg.APUrls = []url.URL{{
136 Scheme: "https",
137 Host: net.JoinHostPort(host, strconv.Itoa(port)),
138 }}
139 } else {
140 // When not enabling peer traffic, listen on loopback. We would not listen at
141 // all, but etcd seems to prevent us from doing that.
142 cfg.LPUrls = []url.URL{{
143 Scheme: "http",
144 Host: fmt.Sprintf("127.0.0.1:%d", port),
145 }}
146 cfg.APUrls = []url.URL{{
147 Scheme: "http",
148 Host: fmt.Sprintf("127.0.0.1:%d", port),
149 }}
150 }
151
152 cfg.InitialCluster = cfg.InitialClusterFromName(nodeID)
153 if c.JoinCluster != nil {
154 for _, n := range c.JoinCluster.ExistingNodes {
155 cfg.InitialCluster += "," + n.connectionString()
156 }
157 }
158 return cfg
159}
160
161// localClient returns an etcd client connected to the socket as configured in
162// Config.
163func (c *Config) localClient() (*clientv3.Client, error) {
164 socket := c.Ephemeral.ClientSocket.FullPath()
165 return clientv3.New(clientv3.Config{
166 Endpoints: []string{fmt.Sprintf("unix://%s:0", socket)},
Serge Bazanskib76b8d12023-03-16 00:46:56 +0100167 DialTimeout: 2 * time.Second,
Serge Bazanskif05e80a2021-10-12 11:53:34 +0200168 })
169}