blob: 6c07ee7ccb2a24bec87194c46650aff5f5441bd9 [file] [log] [blame]
package consensus
import (
"crypto/ed25519"
"crypto/x509"
"fmt"
"net"
"net/url"
"strconv"
"time"
clientv3 "go.etcd.io/etcd/client/v3"
"go.etcd.io/etcd/server/v3/embed"
"source.monogon.dev/metropolis/node"
"source.monogon.dev/metropolis/node/core/identity"
"source.monogon.dev/metropolis/node/core/localstorage"
"source.monogon.dev/metropolis/pkg/pki"
)
// Config describes the startup configuration of a consensus instance.
type Config struct {
// Data directory (persistent, encrypted storage) for etcd.
Data *localstorage.DataEtcdDirectory
// Ephemeral directory for etcd.
Ephemeral *localstorage.EphemeralConsensusDirectory
// JoinCluster is set if this instance is to join an existing cluster for the
// first time. If not set, it's assumed this instance has ran before and has all
// the state on disk required to become part of whatever cluster it was before.
// If that data is not present, a new cluster will be bootstrapped.
JoinCluster *JoinCluster
// NodePrivateKey is the node's main private key which is also used for
// Metropolis PKI. The same key will be used to identify consensus nodes, but
// different certificates will be used.
NodePrivateKey ed25519.PrivateKey
testOverrides testOverrides
}
// JoinCluster is all the data required for a node to join (for the first time)
// an already running cluster. This data is available from an already running
// consensus member by performing AddNode, which is called by the Curator when
// new etcd nodes are added to the cluster.
type JoinCluster struct {
CACertificate *x509.Certificate
NodeCertificate *x509.Certificate
// ExistingNodes are an arbitrarily ordered list of other consensus members that
// the node should attempt to contact.
ExistingNodes []ExistingNode
// InitialCRL is a certificate revocation list for this cluster. After the node
// starts, a CRL on disk will be maintained reflecting the PKI state within etcd.
InitialCRL *pki.CRL
}
// ExistingNode is the peer URL and name of an already running consensus instance.
type ExistingNode struct {
Name string
URL string
}
func (e *ExistingNode) connectionString() string {
return fmt.Sprintf("%s=%s", e.Name, e.URL)
}
func (c *Config) nodePublicKey() ed25519.PublicKey {
return c.NodePrivateKey.Public().(ed25519.PublicKey)
}
// testOverrides are available to test code to make some things easier in a test
// environment.
type testOverrides struct {
// externalPort overrides the default port used by the node.
externalPort int
// externalAddress overrides the address of the node, which is usually its ID.
externalAddress string
}
// build takes a Config and returns an etcd embed.Config.
//
// enablePeers selects whether the etcd instance will listen for peer traffic
// over TLS. This requires TLS credentials to be present on disk, and will be
// disabled for bootstrapping the instance.
func (c *Config) build(enablePeers bool) *embed.Config {
nodeID := identity.NodeID(c.nodePublicKey())
port := int(node.ConsensusPort)
if p := c.testOverrides.externalPort; p != 0 {
port = p
}
host := nodeID
var extraNames []string
if c.testOverrides.externalAddress != "" {
host = c.testOverrides.externalAddress
extraNames = append(extraNames, host)
}
cfg := embed.NewConfig()
cfg.Name = nodeID
cfg.ClusterState = "existing"
cfg.InitialClusterToken = "METROPOLIS"
cfg.Logger = "zap"
cfg.LogOutputs = []string{c.Ephemeral.ServerLogsFIFO.FullPath()}
cfg.Dir = c.Data.Data.FullPath()
// Client URL, ie. local UNIX socket to listen on for trusted, unauthenticated
// traffic.
cfg.LCUrls = []url.URL{{
Scheme: "unix",
Path: c.Ephemeral.ClientSocket.FullPath() + ":0",
}}
if enablePeers {
cfg.PeerTLSInfo.CertFile = c.Data.PeerPKI.Certificate.FullPath()
cfg.PeerTLSInfo.KeyFile = c.Data.PeerPKI.Key.FullPath()
cfg.PeerTLSInfo.TrustedCAFile = c.Data.PeerPKI.CACertificate.FullPath()
cfg.PeerTLSInfo.ClientCertAuth = true
cfg.PeerTLSInfo.CRLFile = c.Data.PeerCRL.FullPath()
cfg.LPUrls = []url.URL{{
Scheme: "https",
Host: fmt.Sprintf("[::]:%d", port),
}}
cfg.APUrls = []url.URL{{
Scheme: "https",
Host: net.JoinHostPort(host, strconv.Itoa(port)),
}}
} else {
// When not enabling peer traffic, listen on loopback. We would not listen at
// all, but etcd seems to prevent us from doing that.
cfg.LPUrls = []url.URL{{
Scheme: "http",
Host: fmt.Sprintf("127.0.0.1:%d", port),
}}
cfg.APUrls = []url.URL{{
Scheme: "http",
Host: fmt.Sprintf("127.0.0.1:%d", port),
}}
}
cfg.InitialCluster = cfg.InitialClusterFromName(nodeID)
if c.JoinCluster != nil {
for _, n := range c.JoinCluster.ExistingNodes {
cfg.InitialCluster += "," + n.connectionString()
}
}
return cfg
}
// localClient returns an etcd client connected to the socket as configured in
// Config.
func (c *Config) localClient() (*clientv3.Client, error) {
socket := c.Ephemeral.ClientSocket.FullPath()
return clientv3.New(clientv3.Config{
Endpoints: []string{fmt.Sprintf("unix://%s:0", socket)},
DialTimeout: 2 * time.Second,
})
}