blob: dfd70f2e68360af1e5346952dc9c8f5fed9b6d1d [file] [log] [blame]
Tim Windelschmidt6d33a432025-02-04 14:34:25 +01001// Copyright The Monogon Project Authors.
2// SPDX-License-Identifier: Apache-2.0
3
Serge Bazanskif05e80a2021-10-12 11:53:34 +02004package consensus
5
6import (
7 "crypto/ed25519"
8 "crypto/x509"
9 "fmt"
10 "net"
11 "net/url"
12 "strconv"
13 "time"
14
Lorenz Brund13c1c62022-03-30 19:58:58 +020015 clientv3 "go.etcd.io/etcd/client/v3"
16 "go.etcd.io/etcd/server/v3/embed"
Serge Bazanskif05e80a2021-10-12 11:53:34 +020017
Jan Schär0f8ce4c2025-09-04 13:27:50 +020018 "source.monogon.dev/metropolis/node/allocs"
Serge Bazanskif05e80a2021-10-12 11:53:34 +020019 "source.monogon.dev/metropolis/node/core/localstorage"
Tim Windelschmidt9f21f532024-05-07 15:14:20 +020020 "source.monogon.dev/osbase/pki"
Serge Bazanskif05e80a2021-10-12 11:53:34 +020021)
22
23// Config describes the startup configuration of a consensus instance.
24type Config struct {
25 // Data directory (persistent, encrypted storage) for etcd.
26 Data *localstorage.DataEtcdDirectory
27 // Ephemeral directory for etcd.
28 Ephemeral *localstorage.EphemeralConsensusDirectory
29
30 // JoinCluster is set if this instance is to join an existing cluster for the
31 // first time. If not set, it's assumed this instance has ran before and has all
32 // the state on disk required to become part of whatever cluster it was before.
33 // If that data is not present, a new cluster will be bootstrapped.
34 JoinCluster *JoinCluster
35
Jan Schär39d9c242024-09-24 13:49:55 +020036 // NodeID is the node ID, which is also used to identify consensus nodes.
37 NodeID string
38
Serge Bazanskif05e80a2021-10-12 11:53:34 +020039 // NodePrivateKey is the node's main private key which is also used for
Jan Schär39d9c242024-09-24 13:49:55 +020040 // Metropolis PKI. The same key will be used for consensus nodes, but
Serge Bazanskif05e80a2021-10-12 11:53:34 +020041 // different certificates will be used.
42 NodePrivateKey ed25519.PrivateKey
43
44 testOverrides testOverrides
45}
46
47// JoinCluster is all the data required for a node to join (for the first time)
48// an already running cluster. This data is available from an already running
49// consensus member by performing AddNode, which is called by the Curator when
50// new etcd nodes are added to the cluster.
51type JoinCluster struct {
52 CACertificate *x509.Certificate
53 NodeCertificate *x509.Certificate
54 // ExistingNodes are an arbitrarily ordered list of other consensus members that
55 // the node should attempt to contact.
56 ExistingNodes []ExistingNode
57 // InitialCRL is a certificate revocation list for this cluster. After the node
58 // starts, a CRL on disk will be maintained reflecting the PKI state within etcd.
59 InitialCRL *pki.CRL
60}
61
62// ExistingNode is the peer URL and name of an already running consensus instance.
63type ExistingNode struct {
64 Name string
65 URL string
66}
67
68func (e *ExistingNode) connectionString() string {
69 return fmt.Sprintf("%s=%s", e.Name, e.URL)
70}
71
72func (c *Config) nodePublicKey() ed25519.PublicKey {
73 return c.NodePrivateKey.Public().(ed25519.PublicKey)
74}
75
76// testOverrides are available to test code to make some things easier in a test
77// environment.
78type testOverrides struct {
79 // externalPort overrides the default port used by the node.
80 externalPort int
81 // externalAddress overrides the address of the node, which is usually its ID.
82 externalAddress string
Tim Windelschmidtc37a8862023-07-19 16:33:21 +020083 // etcdMetricsPort overrides the default etcd metrics port used by the node.
84 etcdMetricsPort int
Serge Bazanskif05e80a2021-10-12 11:53:34 +020085}
86
87// build takes a Config and returns an etcd embed.Config.
88//
89// enablePeers selects whether the etcd instance will listen for peer traffic
90// over TLS. This requires TLS credentials to be present on disk, and will be
91// disabled for bootstrapping the instance.
92func (c *Config) build(enablePeers bool) *embed.Config {
Jan Schär0f8ce4c2025-09-04 13:27:50 +020093 port := int(allocs.PortConsensus)
Serge Bazanskif05e80a2021-10-12 11:53:34 +020094 if p := c.testOverrides.externalPort; p != 0 {
95 port = p
96 }
Jan Schär39d9c242024-09-24 13:49:55 +020097 host := c.NodeID
Serge Bazanskif05e80a2021-10-12 11:53:34 +020098 if c.testOverrides.externalAddress != "" {
99 host = c.testOverrides.externalAddress
Serge Bazanskif05e80a2021-10-12 11:53:34 +0200100 }
Jan Schär0f8ce4c2025-09-04 13:27:50 +0200101 etcdPort := int(allocs.PortMetricsEtcdListener)
Tim Windelschmidtc37a8862023-07-19 16:33:21 +0200102 if p := c.testOverrides.etcdMetricsPort; p != 0 {
103 etcdPort = p
104 }
Serge Bazanskif05e80a2021-10-12 11:53:34 +0200105
106 cfg := embed.NewConfig()
107
Jan Schär39d9c242024-09-24 13:49:55 +0200108 cfg.Name = c.NodeID
Serge Bazanskif05e80a2021-10-12 11:53:34 +0200109 cfg.ClusterState = "existing"
110 cfg.InitialClusterToken = "METROPOLIS"
111 cfg.Logger = "zap"
112 cfg.LogOutputs = []string{c.Ephemeral.ServerLogsFIFO.FullPath()}
Lorenz Brun62229cf2025-07-07 12:47:31 +0200113 cfg.EnableGRPCGateway = false
114 cfg.GRPCOnly = true
Tim Windelschmidtc37a8862023-07-19 16:33:21 +0200115 cfg.ListenMetricsUrls = []url.URL{
116 {Scheme: "http", Host: net.JoinHostPort("127.0.0.1", fmt.Sprintf("%d", etcdPort))},
117 }
Serge Bazanskif05e80a2021-10-12 11:53:34 +0200118
119 cfg.Dir = c.Data.Data.FullPath()
120
121 // Client URL, ie. local UNIX socket to listen on for trusted, unauthenticated
122 // traffic.
Lorenz Brun6211e4d2023-11-14 19:09:40 +0100123 cfg.ListenClientUrls = []url.URL{{
Serge Bazanskif05e80a2021-10-12 11:53:34 +0200124 Scheme: "unix",
125 Path: c.Ephemeral.ClientSocket.FullPath() + ":0",
126 }}
127
128 if enablePeers {
129 cfg.PeerTLSInfo.CertFile = c.Data.PeerPKI.Certificate.FullPath()
130 cfg.PeerTLSInfo.KeyFile = c.Data.PeerPKI.Key.FullPath()
131 cfg.PeerTLSInfo.TrustedCAFile = c.Data.PeerPKI.CACertificate.FullPath()
132 cfg.PeerTLSInfo.ClientCertAuth = true
133 cfg.PeerTLSInfo.CRLFile = c.Data.PeerCRL.FullPath()
134
Lorenz Brun6211e4d2023-11-14 19:09:40 +0100135 cfg.ListenPeerUrls = []url.URL{{
Serge Bazanskif05e80a2021-10-12 11:53:34 +0200136 Scheme: "https",
137 Host: fmt.Sprintf("[::]:%d", port),
138 }}
Lorenz Brun6211e4d2023-11-14 19:09:40 +0100139 cfg.AdvertisePeerUrls = []url.URL{{
Serge Bazanskif05e80a2021-10-12 11:53:34 +0200140 Scheme: "https",
141 Host: net.JoinHostPort(host, strconv.Itoa(port)),
142 }}
143 } else {
144 // When not enabling peer traffic, listen on loopback. We would not listen at
145 // all, but etcd seems to prevent us from doing that.
Lorenz Brun6211e4d2023-11-14 19:09:40 +0100146 cfg.ListenPeerUrls = []url.URL{{
Serge Bazanskif05e80a2021-10-12 11:53:34 +0200147 Scheme: "http",
148 Host: fmt.Sprintf("127.0.0.1:%d", port),
149 }}
Lorenz Brun6211e4d2023-11-14 19:09:40 +0100150 cfg.AdvertisePeerUrls = []url.URL{{
Serge Bazanskif05e80a2021-10-12 11:53:34 +0200151 Scheme: "http",
152 Host: fmt.Sprintf("127.0.0.1:%d", port),
153 }}
154 }
155
Jan Schär39d9c242024-09-24 13:49:55 +0200156 cfg.InitialCluster = cfg.InitialClusterFromName(c.NodeID)
Serge Bazanskif05e80a2021-10-12 11:53:34 +0200157 if c.JoinCluster != nil {
158 for _, n := range c.JoinCluster.ExistingNodes {
159 cfg.InitialCluster += "," + n.connectionString()
160 }
161 }
162 return cfg
163}
164
165// localClient returns an etcd client connected to the socket as configured in
166// Config.
167func (c *Config) localClient() (*clientv3.Client, error) {
168 socket := c.Ephemeral.ClientSocket.FullPath()
169 return clientv3.New(clientv3.Config{
170 Endpoints: []string{fmt.Sprintf("unix://%s:0", socket)},
Serge Bazanskib76b8d12023-03-16 00:46:56 +0100171 DialTimeout: 2 * time.Second,
Serge Bazanskif05e80a2021-10-12 11:53:34 +0200172 })
173}