blob: aa2d54cca41eca9eff2a573884163833e0816696 [file] [log] [blame]
Serge Bazanski6bd41592021-08-23 13:18:37 +02001package curator
2
3import (
Serge Bazanskibc671d02021-10-05 17:53:32 +02004 "bytes"
Serge Bazanski6bd41592021-08-23 13:18:37 +02005 "context"
6 "crypto/rand"
Serge Bazanskibc671d02021-10-05 17:53:32 +02007 "sort"
Serge Bazanski6bd41592021-08-23 13:18:37 +02008
9 "go.etcd.io/etcd/clientv3"
10 "google.golang.org/grpc/codes"
11 "google.golang.org/grpc/status"
12 "google.golang.org/protobuf/proto"
13
Serge Bazanski2893e982021-09-09 13:06:16 +020014 ppb "source.monogon.dev/metropolis/node/core/curator/proto/private"
Serge Bazanski6bd41592021-08-23 13:18:37 +020015 apb "source.monogon.dev/metropolis/proto/api"
Serge Bazanskibc671d02021-10-05 17:53:32 +020016 cpb "source.monogon.dev/metropolis/proto/common"
Serge Bazanski6bd41592021-08-23 13:18:37 +020017)
18
19type leaderManagement struct {
Serge Bazanski3be48322021-10-05 17:24:26 +020020 *leadership
Serge Bazanski6bd41592021-08-23 13:18:37 +020021}
22
23const (
24 // registerTicketSize is the size, in bytes, of the RegisterTicket used to
25 // perform early perimeter checks for nodes which wish to register into the
26 // cluster.
27 //
28 // The size was picked to offer resistance against on-line bruteforcing attacks
29 // in even the worst case scenario (no ratelimiting, no monitoring, zero latency
30 // between attacker and cluster). 256 bits of entropy require 3.6e68 requests
31 // per second to bruteforce the ticket within 10 years. The ticket doesn't need
32 // to be manually copied by humans, so the relatively overkill size also doesn't
33 // impact usability.
34 registerTicketSize = 32
35)
36
37const (
38 // registerTicketEtcdPath is the etcd key under which private.RegisterTicket is
39 // stored.
40 registerTicketEtcdPath = "/global/register_ticket"
41)
42
43func (l *leaderManagement) GetRegisterTicket(ctx context.Context, req *apb.GetRegisterTicketRequest) (*apb.GetRegisterTicketResponse, error) {
Serge Bazanski6bd41592021-08-23 13:18:37 +020044 // Retrieve existing ticket, if any.
45 res, err := l.txnAsLeader(ctx, clientv3.OpGet(registerTicketEtcdPath))
46 if err != nil {
47 return nil, status.Errorf(codes.Unavailable, "could not retrieve register ticket: %v", err)
48 }
49 kvs := res.Responses[0].GetResponseRange().Kvs
50 if len(kvs) > 0 {
51 // Ticket already generated, return.
52 return &apb.GetRegisterTicketResponse{
53 Ticket: kvs[0].Value,
54 }, nil
55 }
56
57 // No ticket, generate one.
Serge Bazanski2893e982021-09-09 13:06:16 +020058 ticket := &ppb.RegisterTicket{
Serge Bazanski6bd41592021-08-23 13:18:37 +020059 Opaque: make([]byte, registerTicketSize),
60 }
61 _, err = rand.Read(ticket.Opaque)
62 if err != nil {
63 return nil, status.Errorf(codes.Unavailable, "could not generate new ticket: %v", err)
64 }
65 ticketBytes, err := proto.Marshal(ticket)
66 if err != nil {
67 return nil, status.Errorf(codes.Unavailable, "could not marshal new ticket: %v", err)
68 }
69
70 // Commit new ticket to etcd.
71 _, err = l.txnAsLeader(ctx, clientv3.OpPut(registerTicketEtcdPath, string(ticketBytes)))
72 if err != nil {
73 return nil, status.Errorf(codes.Unavailable, "could not save new ticket: %v", err)
74 }
75
76 return &apb.GetRegisterTicketResponse{
77 Ticket: ticketBytes,
78 }, nil
79}
Serge Bazanskibc671d02021-10-05 17:53:32 +020080
81// GetClusterInfo implements Curator.GetClusterInfo, which returns summary
82// information about the Metropolis cluster.
83func (l *leaderManagement) GetClusterInfo(ctx context.Context, req *apb.GetClusterInfoRequest) (*apb.GetClusterInfoResponse, error) {
84 res, err := l.txnAsLeader(ctx, nodeEtcdPrefix.Range())
85 if err != nil {
86 return nil, status.Errorf(codes.Unavailable, "could not retrieve list of nodes: %v", err)
87 }
88
89 // Sort nodes by public key, filter out Up, use top 15 in cluster directory
90 // (limited to an arbitrary amount that doesn't overload callers with
91 // unnecesssary information).
92 //
93 // MVP: this should be formalized and possibly re-designed/engineered.
94 kvs := res.Responses[0].GetResponseRange().Kvs
95 var nodes []*Node
96 for _, kv := range kvs {
97 node, err := nodeUnmarshal(kv.Value)
98 if err != nil {
99 // TODO(q3k): log this
100 continue
101 }
102 if node.state != cpb.NodeState_NODE_STATE_UP {
103 continue
104 }
105 nodes = append(nodes, node)
106 }
107 sort.Slice(nodes, func(i, j int) bool {
108 return bytes.Compare(nodes[i].pubkey, nodes[j].pubkey) < 0
109 })
110 if len(nodes) > 15 {
111 nodes = nodes[:15]
112 }
113
114 // Build cluster directory.
115 directory := &cpb.ClusterDirectory{
116 Nodes: make([]*cpb.ClusterDirectory_Node, len(nodes)),
117 }
118 for i, node := range nodes {
119 var addresses []*cpb.ClusterDirectory_Node_Address
120 if node.status != nil && node.status.ExternalAddress != "" {
121 addresses = append(addresses, &cpb.ClusterDirectory_Node_Address{
122 Host: node.status.ExternalAddress,
123 })
124 }
125 directory.Nodes[i] = &cpb.ClusterDirectory_Node{
126 PublicKey: node.pubkey,
127 Addresses: addresses,
128 }
129 }
130
131 return &apb.GetClusterInfoResponse{
132 ClusterDirectory: directory,
133 }, nil
134}