blob: 4c38c79994643210daa131b444762f128529fac4 [file] [log] [blame]
Serge Bazanski93d593b2023-03-28 16:43:47 +02001package clusternet
2
3import (
4 "fmt"
5 "net"
6 "os"
7
8 "github.com/vishvananda/netlink"
9 "golang.zx2c4.com/wireguard/wgctrl"
10 "golang.zx2c4.com/wireguard/wgctrl/wgtypes"
11
12 common "source.monogon.dev/metropolis/node"
Serge Bazanski60461b22023-10-26 19:16:59 +020013 ipb "source.monogon.dev/metropolis/node/core/curator/proto/api"
Serge Bazanski93d593b2023-03-28 16:43:47 +020014 "source.monogon.dev/metropolis/node/core/localstorage"
15)
16
17const (
18 // clusterNetDevicename is the name of the WireGuard interface that will be
19 // created in the host network namespace.
20 clusterNetDeviceName = "clusternet"
21)
22
23// wireguard decouples the cluster networking service from actual mutations
24// performed in the local Linux networking namespace. This is mostly done to help
25// in testing the cluster networking service.
26//
27// Because it's effectively just a mockable interface, see the actual
28// localWireguard method implementations for documentation.
29type wireguard interface {
30 ensureOnDiskKey(dir *localstorage.DataKubernetesClusterNetworkingDirectory) error
31 setup(clusterNet *net.IPNet) error
Serge Bazanski60461b22023-10-26 19:16:59 +020032 configurePeers(nodes []*ipb.Node) error
33 unconfigurePeer(n *ipb.Node) error
Serge Bazanski93d593b2023-03-28 16:43:47 +020034 key() wgtypes.Key
35 close()
36}
37
38type localWireguard struct {
39 wgClient *wgctrl.Client
40 privKey wgtypes.Key
41}
42
43// ensureOnDiskKey loads the private key from disk or (if none exists) generates
44// one and persists it. The resulting key is then saved into the localWireguard
45// instance.
46func (s *localWireguard) ensureOnDiskKey(dir *localstorage.DataKubernetesClusterNetworkingDirectory) error {
47 keyRaw, err := dir.Key.Read()
48 if os.IsNotExist(err) {
49 key, err := wgtypes.GeneratePrivateKey()
50 if err != nil {
51 return fmt.Errorf("when generating key: %w", err)
52 }
53 if err := dir.Key.Write([]byte(key.String()), 0600); err != nil {
54 return fmt.Errorf("save failed: %w", err)
55 }
56 s.privKey = key
57 return nil
58 } else if err != nil {
59 return fmt.Errorf("load failed: %w", err)
60 }
61
62 key, err := wgtypes.ParseKey(string(keyRaw))
63 if err != nil {
64 return fmt.Errorf("invalid private key in file: %w", err)
65 }
66 s.privKey = key
67 return nil
68}
69
70// setup the local network namespace by creating a WireGuard interface and adding
71// a clusterNet route to it. If a matching WireGuard interface already exists in
72// the system, it is first deleted.
73//
74// ensureOnDiskKey must be called before calling this function.
75func (s *localWireguard) setup(clusterNet *net.IPNet) error {
76 links, err := netlink.LinkList()
77 if err != nil {
78 return fmt.Errorf("could not list links: %w", err)
79 }
80 for _, link := range links {
81 if link.Attrs().Name != clusterNetDeviceName {
82 continue
83 }
84 if err := netlink.LinkDel(link); err != nil {
85 return fmt.Errorf("could not remove existing clusternet link: %w", err)
86 }
87 }
88
89 wgInterface := &netlink.Wireguard{LinkAttrs: netlink.LinkAttrs{Name: clusterNetDeviceName, Flags: net.FlagUp}}
90 if err := netlink.LinkAdd(wgInterface); err != nil {
91 return fmt.Errorf("when adding network interface: %w", err)
92 }
93
94 wgClient, err := wgctrl.New()
95 if err != nil {
96 return fmt.Errorf("when creating wireguard client: %w", err)
97 }
98 s.wgClient = wgClient
99
100 listenPort := int(common.WireGuardPort)
101 if err := s.wgClient.ConfigureDevice(clusterNetDeviceName, wgtypes.Config{
102 PrivateKey: &s.privKey,
103 ListenPort: &listenPort,
104 }); err != nil {
105 return fmt.Errorf("when setting up device: %w", err)
106 }
107
108 if err := netlink.RouteAdd(&netlink.Route{
109 Dst: clusterNet,
110 LinkIndex: wgInterface.Index,
111 Protocol: common.ProtocolClusternet,
112 }); err != nil && !os.IsExist(err) {
113 return fmt.Errorf("when creating cluster route: %w", err)
114 }
115 return nil
116}
117
Serge Bazanski60461b22023-10-26 19:16:59 +0200118// configurePeers creates or updates peers on the local wireguard interface
Serge Bazanski93d593b2023-03-28 16:43:47 +0200119// based on the given nodes.
Serge Bazanski60461b22023-10-26 19:16:59 +0200120func (s *localWireguard) configurePeers(nodes []*ipb.Node) error {
Serge Bazanski93d593b2023-03-28 16:43:47 +0200121 var configs []wgtypes.PeerConfig
Serge Bazanski93d593b2023-03-28 16:43:47 +0200122 for i, n := range nodes {
Serge Bazanski60461b22023-10-26 19:16:59 +0200123 if s.privKey.PublicKey().String() == n.Clusternet.WireguardPubkey {
Serge Bazanski93d593b2023-03-28 16:43:47 +0200124 // Node doesn't need to connect to itself
125 continue
126 }
Serge Bazanski60461b22023-10-26 19:16:59 +0200127 pubkeyParsed, err := wgtypes.ParseKey(n.Clusternet.WireguardPubkey)
Serge Bazanski93d593b2023-03-28 16:43:47 +0200128 if err != nil {
Serge Bazanski60461b22023-10-26 19:16:59 +0200129 return fmt.Errorf("node %d: failed to parse public-key %q: %w", i, n.Clusternet.WireguardPubkey, err)
Serge Bazanski93d593b2023-03-28 16:43:47 +0200130 }
Serge Bazanski60461b22023-10-26 19:16:59 +0200131 addressParsed := net.ParseIP(n.Status.ExternalAddress)
Serge Bazanski93d593b2023-03-28 16:43:47 +0200132 if addressParsed == nil {
Serge Bazanski60461b22023-10-26 19:16:59 +0200133 return fmt.Errorf("node %d: failed to parse address %q: %w", i, n.Status.ExternalAddress, err)
Serge Bazanski93d593b2023-03-28 16:43:47 +0200134 }
135 var allowedIPs []net.IPNet
Serge Bazanski60461b22023-10-26 19:16:59 +0200136 for _, prefix := range n.Clusternet.Prefixes {
137 _, podNet, err := net.ParseCIDR(prefix.Cidr)
Serge Bazanski93d593b2023-03-28 16:43:47 +0200138 if err != nil {
139 // Just eat the parse error. Not much we can do here. We have enough validation
140 // in the rest of the system that we shouldn't ever reach this.
141 continue
142 }
143 allowedIPs = append(allowedIPs, *podNet)
144 }
145 endpoint := net.UDPAddr{Port: int(common.WireGuardPort), IP: addressParsed}
146 configs = append(configs, wgtypes.PeerConfig{
147 PublicKey: pubkeyParsed,
148 Endpoint: &endpoint,
149 ReplaceAllowedIPs: true,
150 AllowedIPs: allowedIPs,
151 })
152 }
153
154 err := s.wgClient.ConfigureDevice(clusterNetDeviceName, wgtypes.Config{
155 Peers: configs,
156 })
157 if err != nil {
158 return fmt.Errorf("failed to configure WireGuard peers: %w", err)
159 }
160 return nil
161}
162
163// unconfigurePeer removes the peer from the local WireGuard interface based on
164// the given node. If no peer existed matching the given node, this operation is
165// a no-op.
Serge Bazanski60461b22023-10-26 19:16:59 +0200166func (s *localWireguard) unconfigurePeer(n *ipb.Node) error {
167 pubkeyParsed, err := wgtypes.ParseKey(n.Clusternet.WireguardPubkey)
Serge Bazanski93d593b2023-03-28 16:43:47 +0200168 if err != nil {
Serge Bazanski60461b22023-10-26 19:16:59 +0200169 return fmt.Errorf("failed to parse public-key %q: %w", n.Clusternet.WireguardPubkey, err)
Serge Bazanski93d593b2023-03-28 16:43:47 +0200170 }
171
172 err = s.wgClient.ConfigureDevice(clusterNetDeviceName, wgtypes.Config{
173 Peers: []wgtypes.PeerConfig{{
174 PublicKey: pubkeyParsed,
175 Remove: true,
176 }},
177 })
178 if err != nil {
179 return fmt.Errorf("failed to delete WireGuard peer: %w", err)
180 }
181 return nil
182}
183
184func (s *localWireguard) key() wgtypes.Key {
185 return s.privKey
186}
187
188// close cleans up after the wireguard client, but does _not_ remove the
189// interface or peers.
190func (s *localWireguard) close() {
191 s.wgClient.Close()
192 s.wgClient = nil
193}