m/n/core/clusternet: init

This implements the new cluster networking daemon. This is just the
daemon itself with some tests. It's not yet used.

Change-Id: Ida34b647db0d075fcaaf2d57c9a8a14701713552
Reviewed-on: https://review.monogon.dev/c/monogon/+/1416
Tested-by: Jenkins CI
Reviewed-by: Lorenz Brun <lorenz@monogon.tech>
diff --git a/metropolis/node/core/clusternet/wireguard.go b/metropolis/node/core/clusternet/wireguard.go
new file mode 100644
index 0000000..9ce6d49
--- /dev/null
+++ b/metropolis/node/core/clusternet/wireguard.go
@@ -0,0 +1,197 @@
+package clusternet
+
+import (
+	"fmt"
+	"net"
+	"os"
+
+	"github.com/vishvananda/netlink"
+	"golang.zx2c4.com/wireguard/wgctrl"
+	"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
+
+	common "source.monogon.dev/metropolis/node"
+	"source.monogon.dev/metropolis/node/core/localstorage"
+)
+
+const (
+	// clusterNetDevicename is the name of the WireGuard interface that will be
+	// created in the host network namespace.
+	clusterNetDeviceName = "clusternet"
+)
+
+// wireguard decouples the cluster networking service from actual mutations
+// performed in the local Linux networking namespace. This is mostly done to help
+// in testing the cluster networking service.
+//
+// Because it's effectively just a mockable interface, see the actual
+// localWireguard method implementations for documentation.
+type wireguard interface {
+	ensureOnDiskKey(dir *localstorage.DataKubernetesClusterNetworkingDirectory) error
+	setup(clusterNet *net.IPNet) error
+	configurePeers(n []*node) error
+	unconfigurePeer(n *node) error
+	key() wgtypes.Key
+	close()
+}
+
+type localWireguard struct {
+	wgClient *wgctrl.Client
+	privKey  wgtypes.Key
+}
+
+// ensureOnDiskKey loads the private key from disk or (if none exists) generates
+// one and persists it. The resulting key is then saved into the localWireguard
+// instance.
+func (s *localWireguard) ensureOnDiskKey(dir *localstorage.DataKubernetesClusterNetworkingDirectory) error {
+	keyRaw, err := dir.Key.Read()
+	if os.IsNotExist(err) {
+		key, err := wgtypes.GeneratePrivateKey()
+		if err != nil {
+			return fmt.Errorf("when generating key: %w", err)
+		}
+		if err := dir.Key.Write([]byte(key.String()), 0600); err != nil {
+			return fmt.Errorf("save failed: %w", err)
+		}
+		s.privKey = key
+		return nil
+	} else if err != nil {
+		return fmt.Errorf("load failed: %w", err)
+	}
+
+	key, err := wgtypes.ParseKey(string(keyRaw))
+	if err != nil {
+		return fmt.Errorf("invalid private key in file: %w", err)
+	}
+	s.privKey = key
+	return nil
+}
+
+// setup the local network namespace by creating a WireGuard interface and adding
+// a clusterNet route to it. If a matching WireGuard interface already exists in
+// the system, it is first deleted.
+//
+// ensureOnDiskKey must be called before calling this function.
+func (s *localWireguard) setup(clusterNet *net.IPNet) error {
+	links, err := netlink.LinkList()
+	if err != nil {
+		return fmt.Errorf("could not list links: %w", err)
+	}
+	for _, link := range links {
+		if link.Attrs().Name != clusterNetDeviceName {
+			continue
+		}
+		if err := netlink.LinkDel(link); err != nil {
+			return fmt.Errorf("could not remove existing clusternet link: %w", err)
+		}
+	}
+
+	wgInterface := &netlink.Wireguard{LinkAttrs: netlink.LinkAttrs{Name: clusterNetDeviceName, Flags: net.FlagUp}}
+	if err := netlink.LinkAdd(wgInterface); err != nil {
+		return fmt.Errorf("when adding network interface: %w", err)
+	}
+
+	wgClient, err := wgctrl.New()
+	if err != nil {
+		return fmt.Errorf("when creating wireguard client: %w", err)
+	}
+	s.wgClient = wgClient
+
+	listenPort := int(common.WireGuardPort)
+	if err := s.wgClient.ConfigureDevice(clusterNetDeviceName, wgtypes.Config{
+		PrivateKey: &s.privKey,
+		ListenPort: &listenPort,
+	}); err != nil {
+		return fmt.Errorf("when setting up device: %w", err)
+	}
+
+	if err := netlink.RouteAdd(&netlink.Route{
+		Dst:       clusterNet,
+		LinkIndex: wgInterface.Index,
+		Protocol:  common.ProtocolClusternet,
+	}); err != nil && !os.IsExist(err) {
+		return fmt.Errorf("when creating cluster route: %w", err)
+	}
+	return nil
+}
+
+// configurePeers creates or updates a peers on the local wireguard interface
+// based on the given nodes.
+//
+// If any node is somehow invalid and causes a parse/reconfiguration error, the
+// function will return an error. The caller should retry with a different set of
+// nodes, performing search/bisection on its own.
+func (s *localWireguard) configurePeers(nodes []*node) error {
+	var configs []wgtypes.PeerConfig
+
+	for i, n := range nodes {
+		if s.privKey.PublicKey().String() == n.pubkey {
+			// Node doesn't need to connect to itself
+			continue
+		}
+		pubkeyParsed, err := wgtypes.ParseKey(n.pubkey)
+		if err != nil {
+			return fmt.Errorf("node %d: failed to parse public-key %q: %w", i, n.pubkey, err)
+		}
+		addressParsed := net.ParseIP(n.address)
+		if addressParsed == nil {
+			return fmt.Errorf("node %d: failed to parse address %q: %w", i, n.address, err)
+		}
+		var allowedIPs []net.IPNet
+		for _, prefix := range n.prefixes {
+			_, podNet, err := net.ParseCIDR(prefix)
+			if err != nil {
+				// Just eat the parse error. Not much we can do here. We have enough validation
+				// in the rest of the system that we shouldn't ever reach this.
+				continue
+			}
+			allowedIPs = append(allowedIPs, *podNet)
+		}
+		endpoint := net.UDPAddr{Port: int(common.WireGuardPort), IP: addressParsed}
+		configs = append(configs, wgtypes.PeerConfig{
+			PublicKey:         pubkeyParsed,
+			Endpoint:          &endpoint,
+			ReplaceAllowedIPs: true,
+			AllowedIPs:        allowedIPs,
+		})
+	}
+
+	err := s.wgClient.ConfigureDevice(clusterNetDeviceName, wgtypes.Config{
+		Peers: configs,
+	})
+	if err != nil {
+		return fmt.Errorf("failed to configure WireGuard peers: %w", err)
+	}
+	return nil
+}
+
+// unconfigurePeer removes the peer from the local WireGuard interface based on
+// the given node. If no peer existed matching the given node, this operation is
+// a no-op.
+func (s *localWireguard) unconfigurePeer(n *node) error {
+	pubkeyParsed, err := wgtypes.ParseKey(n.pubkey)
+	if err != nil {
+		return fmt.Errorf("failed to parse public-key %q: %w", n.pubkey, err)
+	}
+
+	err = s.wgClient.ConfigureDevice(clusterNetDeviceName, wgtypes.Config{
+		Peers: []wgtypes.PeerConfig{{
+			PublicKey: pubkeyParsed,
+			Remove:    true,
+		}},
+	})
+	if err != nil {
+		return fmt.Errorf("failed to delete WireGuard peer: %w", err)
+	}
+	return nil
+}
+
+func (s *localWireguard) key() wgtypes.Key {
+	return s.privKey
+}
+
+// close cleans up after the wireguard client, but does _not_ remove the
+// interface or peers.
+func (s *localWireguard) close() {
+	s.wgClient.Close()
+	s.wgClient = nil
+}