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