blob: a3379ca768abda3a7faada9449b56ac496503fc9 [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
18 "source.monogon.dev/metropolis/node"
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 {
Serge Bazanskif05e80a2021-10-12 11:53:34 +020093 port := int(node.ConsensusPort)
94 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 }
Tim Windelschmidtc37a8862023-07-19 16:33:21 +0200101 etcdPort := int(node.MetricsEtcdListenerPort)
102 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()}
Tim Windelschmidtc37a8862023-07-19 16:33:21 +0200113 cfg.ListenMetricsUrls = []url.URL{
114 {Scheme: "http", Host: net.JoinHostPort("127.0.0.1", fmt.Sprintf("%d", etcdPort))},
115 }
Serge Bazanskif05e80a2021-10-12 11:53:34 +0200116
117 cfg.Dir = c.Data.Data.FullPath()
118
119 // Client URL, ie. local UNIX socket to listen on for trusted, unauthenticated
120 // traffic.
Lorenz Brun6211e4d2023-11-14 19:09:40 +0100121 cfg.ListenClientUrls = []url.URL{{
Serge Bazanskif05e80a2021-10-12 11:53:34 +0200122 Scheme: "unix",
123 Path: c.Ephemeral.ClientSocket.FullPath() + ":0",
124 }}
125
126 if enablePeers {
127 cfg.PeerTLSInfo.CertFile = c.Data.PeerPKI.Certificate.FullPath()
128 cfg.PeerTLSInfo.KeyFile = c.Data.PeerPKI.Key.FullPath()
129 cfg.PeerTLSInfo.TrustedCAFile = c.Data.PeerPKI.CACertificate.FullPath()
130 cfg.PeerTLSInfo.ClientCertAuth = true
131 cfg.PeerTLSInfo.CRLFile = c.Data.PeerCRL.FullPath()
132
Lorenz Brun6211e4d2023-11-14 19:09:40 +0100133 cfg.ListenPeerUrls = []url.URL{{
Serge Bazanskif05e80a2021-10-12 11:53:34 +0200134 Scheme: "https",
135 Host: fmt.Sprintf("[::]:%d", port),
136 }}
Lorenz Brun6211e4d2023-11-14 19:09:40 +0100137 cfg.AdvertisePeerUrls = []url.URL{{
Serge Bazanskif05e80a2021-10-12 11:53:34 +0200138 Scheme: "https",
139 Host: net.JoinHostPort(host, strconv.Itoa(port)),
140 }}
141 } else {
142 // When not enabling peer traffic, listen on loopback. We would not listen at
143 // all, but etcd seems to prevent us from doing that.
Lorenz Brun6211e4d2023-11-14 19:09:40 +0100144 cfg.ListenPeerUrls = []url.URL{{
Serge Bazanskif05e80a2021-10-12 11:53:34 +0200145 Scheme: "http",
146 Host: fmt.Sprintf("127.0.0.1:%d", port),
147 }}
Lorenz Brun6211e4d2023-11-14 19:09:40 +0100148 cfg.AdvertisePeerUrls = []url.URL{{
Serge Bazanskif05e80a2021-10-12 11:53:34 +0200149 Scheme: "http",
150 Host: fmt.Sprintf("127.0.0.1:%d", port),
151 }}
152 }
153
Jan Schär39d9c242024-09-24 13:49:55 +0200154 cfg.InitialCluster = cfg.InitialClusterFromName(c.NodeID)
Serge Bazanskif05e80a2021-10-12 11:53:34 +0200155 if c.JoinCluster != nil {
156 for _, n := range c.JoinCluster.ExistingNodes {
157 cfg.InitialCluster += "," + n.connectionString()
158 }
159 }
160 return cfg
161}
162
163// localClient returns an etcd client connected to the socket as configured in
164// Config.
165func (c *Config) localClient() (*clientv3.Client, error) {
166 socket := c.Ephemeral.ClientSocket.FullPath()
167 return clientv3.New(clientv3.Config{
168 Endpoints: []string{fmt.Sprintf("unix://%s:0", socket)},
Serge Bazanskib76b8d12023-03-16 00:46:56 +0100169 DialTimeout: 2 * time.Second,
Serge Bazanskif05e80a2021-10-12 11:53:34 +0200170 })
171}