add management IP to machine and peer configs, update daemon to listen on it

This commit is contained in:
Pavel Sviderski
2024-08-30 18:33:37 +10:00
parent c93206e6d1
commit fd396dd91f
10 changed files with 124 additions and 39 deletions
+2
View File
@@ -12,6 +12,8 @@ import (
)
func main() {
slog.SetLogLoggerLevel(slog.LevelDebug)
var dataDir string
cmd := &cobra.Command{
Use: "uncloudd",
+1
View File
@@ -23,6 +23,7 @@ require (
github.com/spf13/cobra v1.8.1
github.com/vishvananda/netlink v1.1.1-0.20211118161826-650dca95af54
go.uber.org/zap v1.27.0
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
golang.org/x/crypto v0.26.0
golang.org/x/sync v0.8.0
golang.org/x/sys v0.24.0
+2
View File
@@ -597,6 +597,8 @@ go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN8
go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+1 -1
View File
@@ -147,7 +147,7 @@ func (c *Cluster) AddMachine(ctx context.Context, name, user, host string, port
return "", err
}
userPeerCfg := network.PeerConfig{
Subnet: netip.PrefixFrom(clusterUser.IPv6(), 128),
ManagementIP: clusterUser.ManagementIP(),
PublicKey: clusterUser.PublicKey(),
}
mcfg, err := machine.NewBootstrapConfig(name, netip.Prefix{}, userPeerCfg)
+1 -1
View File
@@ -44,6 +44,6 @@ func (u *User) PublicKey() secret.Secret {
return pubKey[:]
}
func (u *User) IPv6() netip.Addr {
func (u *User) ManagementIP() netip.Addr {
return network.PeerIPv6(u.PublicKey())
}
+1
View File
@@ -73,6 +73,7 @@ func NewBootstrapConfig(name string, subnet netip.Prefix, peers ...network.PeerC
Name: name,
Network: &network.Config{
Subnet: subnet,
ManagementIP: network.PeerIPv6(pubKey),
PrivateKey: privKey,
PublicKey: pubKey,
Peers: peers,
+4 -3
View File
@@ -7,6 +7,7 @@ import (
"google.golang.org/grpc"
"log/slog"
"net"
"strconv"
"uncloud/internal/machine"
"uncloud/internal/machine/api"
"uncloud/internal/machine/api/pb"
@@ -40,8 +41,8 @@ func Run(ctx context.Context, dataDir string) error {
//}
//fmt.Println("Addresses:", addrs)
addr := fmt.Sprintf("127.0.0.1:%d", MachineAPIPort)
listener, err := net.Listen("tcp", addr)
apiAddr := net.JoinHostPort(cfg.Network.ManagementIP.String(), strconv.Itoa(MachineAPIPort))
listener, err := net.Listen("tcp", apiAddr)
if err != nil {
return fmt.Errorf("listen API port: %w", err)
}
@@ -51,7 +52,7 @@ func Run(ctx context.Context, dataDir string) error {
// Use an errgroup to coordinate error handling and graceful shutdown of multiple daemon components.
errGroup, ctx := errgroup.WithContext(ctx)
errGroup.Go(func() error {
slog.Info("Starting API server.", "addr", addr)
slog.Info("Starting API server.", "addr", apiAddr)
if sErr := grpcServer.Serve(listener); sErr != nil {
return fmt.Errorf("API server failed: %w", sErr)
}
+17 -3
View File
@@ -17,13 +17,19 @@ type Config struct {
// Subnet is the IPv4 address range allocated to the machine. The machine's IP address is the first address
// in the subnet. Other IP addresses are allocated to containers running on the machine.
Subnet netip.Prefix
// ManagementIP is the ManagementIP address assigned to the machine within the WireGuard network. This address is used
// for cluster management traffic, such as gRPC communication with the machine API server and Serf gossip.
ManagementIP netip.Addr
PrivateKey secret.Secret
PublicKey secret.Secret
Peers []PeerConfig `json:",omitempty"`
}
type PeerConfig struct {
Subnet netip.Prefix
Subnet *netip.Prefix `json:",omitempty"`
// ManagementIP is the ManagementIP address assigned to the peer within the WireGuard network. This address is used
// for cluster management traffic, such as gRPC communication with the machine API server and Serf gossip.
ManagementIP netip.Addr
Endpoint *netip.AddrPort `json:",omitempty"`
AllEndpoints []netip.AddrPort `json:",omitempty"`
PublicKey secret.Secret
@@ -32,7 +38,7 @@ type PeerConfig struct {
func (c Config) toDeviceConfig() (wgtypes.Config, error) {
privateKey, err := wgtypes.NewKey(c.PrivateKey)
if err != nil {
panic(fmt.Errorf("parse private key: %w", err))
return wgtypes.Config{}, fmt.Errorf("parse private key: %w", err)
}
listenPort := WireGuardPort
@@ -43,10 +49,18 @@ func (c Config) toDeviceConfig() (wgtypes.Config, error) {
if kErr != nil {
return wgtypes.Config{}, fmt.Errorf("parse peer public key: %w", kErr)
}
manageIP, mErr := addrToSingleIPPrefix(peerConfig.ManagementIP)
if mErr != nil {
return wgtypes.Config{}, fmt.Errorf("parse management IP: %w", mErr)
}
allowedIPs := []net.IPNet{prefixToIPNet(manageIP)}
if peerConfig.Subnet != nil {
allowedIPs = append(allowedIPs, prefixToIPNet(*peerConfig.Subnet))
}
wgPeerConfigs[i] = wgtypes.PeerConfig{
PublicKey: peerPublicKey,
ReplaceAllowedIPs: true,
AllowedIPs: []net.IPNet{prefixToIPNet(peerConfig.Subnet)},
AllowedIPs: allowedIPs,
PersistentKeepaliveInterval: &persistentKeepalive,
}
if peerConfig.Endpoint != nil {
+33
View File
@@ -1,6 +1,7 @@
package network
import (
"fmt"
"net"
"net/netip"
"uncloud/internal/secret"
@@ -24,3 +25,35 @@ func prefixToIPNet(prefix netip.Prefix) net.IPNet {
Mask: net.CIDRMask(prefix.Bits(), prefix.Addr().BitLen()),
}
}
// ipNetToPrefix returns a netip.Prefix from the net.IPNet type. If ipNet is invalid, ok is false.
// Based on https://github.com/tailscale/tailscale/blob/main/net/netaddr/netaddr.go
func ipNetToPrefix(ipNet net.IPNet) (netip.Prefix, error) {
ip, ok := netip.AddrFromSlice(ipNet.IP)
if !ok {
return netip.Prefix{}, fmt.Errorf("invalid IP network")
}
ip = ip.Unmap()
if l := len(ipNet.Mask); l != net.IPv4len && l != net.IPv6len {
return netip.Prefix{}, fmt.Errorf("invalid IP network mask length: %d", l)
}
ones, bits := ipNet.Mask.Size()
if ones == 0 && bits == 0 {
return netip.Prefix{}, fmt.Errorf("non-contiguous IP network mask")
}
return netip.PrefixFrom(ip, ones), nil
}
func addrToSingleIPPrefix(addr netip.Addr) (netip.Prefix, error) {
if !addr.IsValid() {
return netip.Prefix{}, fmt.Errorf("invalid IP address")
}
bits := 32
if addr.Is6() {
bits = 128
}
return addr.Prefix(bits)
}
+50 -19
View File
@@ -7,6 +7,7 @@ import (
"errors"
"fmt"
"github.com/vishvananda/netlink"
"go4.org/netipx"
"golang.org/x/sys/unix"
"golang.zx2c4.com/wireguard/wgctrl"
"log/slog"
@@ -102,10 +103,12 @@ func (n *WireGuardNetwork) Configure(config Config) error {
}
slog.Info("Configured WireGuard interface.", "name", n.link.Attrs().Name)
machineIP := MachineIP(config.Subnet)
machineIPSubnet := netip.PrefixFrom(machineIP, config.Subnet.Bits())
machineIPv6 := netip.PrefixFrom(PeerIPv6(config.PublicKey), 128)
addrs := []netip.Prefix{machineIPSubnet, machineIPv6}
machinePrefix := netip.PrefixFrom(MachineIP(config.Subnet), config.Subnet.Bits())
managementPrefix, err := addrToSingleIPPrefix(config.ManagementIP)
if err != nil {
return fmt.Errorf("parse management IP: %w", err)
}
addrs := []netip.Prefix{managementPrefix, machinePrefix}
if err = n.updateAddresses(addrs); err != nil {
return err
}
@@ -160,39 +163,55 @@ func (n *WireGuardNetwork) updateAddresses(addrs []netip.Prefix) error {
// updatePeerRoutes adds routes to the peers via the WireGuard interface and removes old routes to peers
// that are no longer in the configuration.
func (n *WireGuardNetwork) updatePeerRoutes() error {
// Add routes to the peers via the WireGuard link.
// Build a set of compacted IP ranges for all peers.
var ipsetBuilder netipx.IPSetBuilder
for _, p := range n.peers {
dst := prefixToIPNet(p.config.Subnet)
if err := netlink.RouteAdd(&netlink.Route{
prefixes, err := p.prefixes()
if err != nil {
return fmt.Errorf("get peer addresses: %w", err)
}
for _, pref := range prefixes {
ipsetBuilder.AddPrefix(pref)
}
}
ipset, err := ipsetBuilder.IPSet()
if err != nil {
return fmt.Errorf("build list of IP ranges for peers: %w", err)
}
// Add routes to the computed IP ranges via the WireGuard link.
for _, prefix := range ipset.Prefixes() {
dst := prefixToIPNet(prefix)
if err = netlink.RouteAdd(&netlink.Route{
LinkIndex: n.link.Attrs().Index,
Scope: netlink.SCOPE_LINK,
Dst: &dst,
}); err != nil && !errors.Is(err, unix.EEXIST) {
return fmt.Errorf("add route to WireGuard link %q: %w", n.link.Attrs().Name, err)
}
slog.Debug("Added route to peer via WireGuard interface.",
"name", n.link.Attrs().Name, "peer", dst)
slog.Debug("Added route to peer(s) via WireGuard interface.",
"name", n.link.Attrs().Name, "dst", prefix)
}
// Remove old routes to peers that are no longer in the configuration.
// Remove old routes to IP ranges that are no longer in the configuration.
addedRoutes := ipset.Prefixes()
routes, err := netlink.RouteList(n.link, netlink.FAMILY_ALL)
if err != nil {
return fmt.Errorf("list routes on WireGuard link %q: %w", n.link.Attrs().Name, err)
}
for _, route := range routes {
old := true
for _, p := range n.peers {
if route.Dst.String() == p.config.Subnet.String() {
old = false
break
routePrefix, pErr := ipNetToPrefix(*route.Dst)
if pErr != nil {
return fmt.Errorf("parse route destination: %w", pErr)
}
if slices.Contains(addedRoutes, routePrefix) {
continue
}
if old {
if err = netlink.RouteDel(&route); err != nil {
return fmt.Errorf("remove route %q from WireGuard link %q: %w", route.Dst, n.link.Attrs().Name, err)
}
slog.Debug("Removed route to peer via WireGuard interface.",
"name", n.link.Attrs().Name, "peer", route.Dst)
}
slog.Debug("Removed route to peer(s) via WireGuard interface.",
"name", n.link.Attrs().Name, "dst", routePrefix)
}
return nil
}
@@ -201,3 +220,15 @@ func (n *WireGuardNetwork) Run(ctx context.Context) error {
<-ctx.Done()
return nil
}
func (p peer) prefixes() ([]netip.Prefix, error) {
managePrefix, err := addrToSingleIPPrefix(p.config.ManagementIP)
if err != nil {
return nil, fmt.Errorf("parse management IP: %w", err)
}
prefixes := []netip.Prefix{managePrefix}
if p.config.Subnet != nil {
prefixes = append(prefixes, *p.config.Subnet)
}
return prefixes, nil
}