diff --git a/pkg/k8s/backend.go b/pkg/k8s/backend.go index 6af65c4..6620735 100644 --- a/pkg/k8s/backend.go +++ b/pkg/k8s/backend.go @@ -56,6 +56,7 @@ const ( lastSeenAnnotationKey = "kilo.squat.ai/last-seen" leaderAnnotationKey = "kilo.squat.ai/leader" locationAnnotationKey = "kilo.squat.ai/location" + persistentKeepaliveKey = "kilo.squat.ai/persistent-keepalive" wireGuardIPAnnotationKey = "kilo.squat.ai/wireguard-ip" regionLabelKey = "topology.kubernetes.io/region" @@ -262,6 +263,15 @@ func translateNode(node *v1.Node) *mesh.Node { if !ok { internalIP = node.ObjectMeta.Annotations[internalIPAnnotationKey] } + // Set Wireguard PersistentKeepalive setting for the node. + var persistentKeepalive int64 + if keepAlive, ok := node.ObjectMeta.Annotations[persistentKeepaliveKey]; !ok { + persistentKeepalive = 0 + } else { + if persistentKeepalive, err = strconv.ParseInt(keepAlive, 10, 64); err != nil { + persistentKeepalive = 0 + } + } var lastSeen int64 if ls, ok := node.ObjectMeta.Annotations[lastSeenAnnotationKey]; !ok { lastSeen = 0 @@ -275,14 +285,15 @@ func translateNode(node *v1.Node) *mesh.Node { // remote node's agent has not yet set its IP address; // in this case the IP will be nil and // the mesh can wait for the node to be updated. - ExternalIP: normalizeIP(externalIP), - InternalIP: normalizeIP(internalIP), - Key: []byte(node.ObjectMeta.Annotations[keyAnnotationKey]), - LastSeen: lastSeen, - Leader: leader, - Location: location, - Name: node.Name, - Subnet: subnet, + ExternalIP: normalizeIP(externalIP), + InternalIP: normalizeIP(internalIP), + Key: []byte(node.ObjectMeta.Annotations[keyAnnotationKey]), + LastSeen: lastSeen, + Leader: leader, + Location: location, + Name: node.Name, + PersistentKeepalive: int(persistentKeepalive), + Subnet: subnet, // WireGuardIP can fail to parse if the node is not a leader or if // the node's agent has not yet reconciled. In either case, the IP // will parse as nil. diff --git a/pkg/k8s/backend_test.go b/pkg/k8s/backend_test.go index 532f177..a0f8afe 100644 --- a/pkg/k8s/backend_test.go +++ b/pkg/k8s/backend_test.go @@ -111,6 +111,15 @@ func TestTranslateNode(t *testing.T) { ExternalIP: &net.IPNet{IP: net.ParseIP("10.0.0.2"), Mask: net.CIDRMask(24, 32)}, }, }, + { + name: "wireguard persistent keepalive override", + annotations: map[string]string{ + persistentKeepaliveKey: "25", + }, + out: &mesh.Node{ + PersistentKeepalive: 25, + }, + }, { name: "internal IP override", annotations: map[string]string{ @@ -139,20 +148,22 @@ func TestTranslateNode(t *testing.T) { lastSeenAnnotationKey: "1000000000", leaderAnnotationKey: "", locationAnnotationKey: "b", + persistentKeepaliveKey: "25", wireGuardIPAnnotationKey: "10.4.0.1/16", }, labels: map[string]string{ regionLabelKey: "a", }, out: &mesh.Node{ - ExternalIP: &net.IPNet{IP: net.ParseIP("10.0.0.2"), Mask: net.CIDRMask(24, 32)}, - InternalIP: &net.IPNet{IP: net.ParseIP("10.1.0.2"), Mask: net.CIDRMask(32, 32)}, - Key: []byte("foo"), - LastSeen: 1000000000, - Leader: true, - Location: "b", - Subnet: &net.IPNet{IP: net.ParseIP("10.2.1.0"), Mask: net.CIDRMask(24, 32)}, - WireGuardIP: &net.IPNet{IP: net.ParseIP("10.4.0.1"), Mask: net.CIDRMask(16, 32)}, + ExternalIP: &net.IPNet{IP: net.ParseIP("10.0.0.2"), Mask: net.CIDRMask(24, 32)}, + InternalIP: &net.IPNet{IP: net.ParseIP("10.1.0.2"), Mask: net.CIDRMask(32, 32)}, + Key: []byte("foo"), + LastSeen: 1000000000, + Leader: true, + Location: "b", + PersistentKeepalive: 25, + Subnet: &net.IPNet{IP: net.ParseIP("10.2.1.0"), Mask: net.CIDRMask(24, 32)}, + WireGuardIP: &net.IPNet{IP: net.ParseIP("10.4.0.1"), Mask: net.CIDRMask(16, 32)}, }, subnet: "10.2.1.0/24", }, diff --git a/pkg/mesh/mesh.go b/pkg/mesh/mesh.go index eed3a86..b47a0ea 100644 --- a/pkg/mesh/mesh.go +++ b/pkg/mesh/mesh.go @@ -79,11 +79,12 @@ type Node struct { LastSeen int64 // Leader is a suggestion to Kilo that // the node wants to lead its segment. - Leader bool - Location string - Name string - Subnet *net.IPNet - WireGuardIP *net.IPNet + Leader bool + Location string + Name string + PersistentKeepalive int + Subnet *net.IPNet + WireGuardIP *net.IPNet } // Ready indicates whether or not the node is ready. diff --git a/pkg/mesh/topology.go b/pkg/mesh/topology.go index c27ed3e..d3ca4c2 100644 --- a/pkg/mesh/topology.go +++ b/pkg/mesh/topology.go @@ -64,6 +64,9 @@ type segment struct { hostnames []string // leader is the index of the leader of the segment. leader int + // persistentKeepalive is the interval in seconds of the emission + // of keepalive packets to the peer. + persistentKeepalive int // privateIPs is a slice of private IPs of all peers in the segment. privateIPs []net.IP // wireGuardIP is the allocated IP address of the WireGuard @@ -117,14 +120,15 @@ func NewTopology(nodes map[string]*Node, peers map[string]*Peer, granularity Gra privateIPs = append(privateIPs, node.InternalIP.IP) } t.segments = append(t.segments, &segment{ - allowedIPs: allowedIPs, - endpoint: topoMap[location][leader].ExternalIP.IP, - key: topoMap[location][leader].Key, - location: location, - cidrs: cidrs, - hostnames: hostnames, - leader: leader, - privateIPs: privateIPs, + allowedIPs: allowedIPs, + endpoint: topoMap[location][leader].ExternalIP.IP, + key: topoMap[location][leader].Key, + location: location, + cidrs: cidrs, + hostnames: hostnames, + leader: leader, + privateIPs: privateIPs, + persistentKeepalive: topoMap[location][leader].PersistentKeepalive, }) } // Sort the Topology segments so the result is stable. @@ -334,7 +338,8 @@ func (t *Topology) Conf() *wireguard.Conf { IP: s.endpoint, Port: uint32(t.port), }, - PublicKey: s.key, + PublicKey: s.key, + PersistentKeepalive: s.persistentKeepalive, } c.Peers = append(c.Peers, peer) } @@ -363,7 +368,8 @@ func (t *Topology) AsPeer() *wireguard.Peer { IP: s.endpoint, Port: uint32(t.port), }, - PublicKey: s.key, + PersistentKeepalive: s.persistentKeepalive, + PublicKey: s.key, } } return nil @@ -379,7 +385,8 @@ func (t *Topology) PeerConf(name string) *wireguard.Conf { IP: s.endpoint, Port: uint32(t.port), }, - PublicKey: s.key, + PersistentKeepalive: s.persistentKeepalive, + PublicKey: s.key, } c.Peers = append(c.Peers, peer) } diff --git a/pkg/mesh/topology_test.go b/pkg/mesh/topology_test.go index 661caa9..f6402de 100644 --- a/pkg/mesh/topology_test.go +++ b/pkg/mesh/topology_test.go @@ -39,12 +39,13 @@ func setup(t *testing.T) (map[string]*Node, map[string]*Peer, []byte, uint32) { i2 := &net.IPNet{IP: net.ParseIP("192.168.0.2").To4(), Mask: net.CIDRMask(32, 32)} nodes := map[string]*Node{ "a": { - Name: "a", - ExternalIP: e1, - InternalIP: i1, - Location: "1", - Subnet: &net.IPNet{IP: net.ParseIP("10.2.1.0"), Mask: net.CIDRMask(24, 32)}, - Key: []byte("key1"), + Name: "a", + ExternalIP: e1, + InternalIP: i1, + Location: "1", + Subnet: &net.IPNet{IP: net.ParseIP("10.2.1.0"), Mask: net.CIDRMask(24, 32)}, + Key: []byte("key1"), + PersistentKeepalive: 25, }, "b": { Name: "b", @@ -117,14 +118,15 @@ func TestNewTopology(t *testing.T) { wireGuardCIDR: &net.IPNet{IP: w1, Mask: net.CIDRMask(16, 32)}, segments: []*segment{ { - allowedIPs: []*net.IPNet{nodes["a"].Subnet, nodes["a"].InternalIP, {IP: w1, Mask: net.CIDRMask(32, 32)}}, - endpoint: nodes["a"].ExternalIP.IP, - key: nodes["a"].Key, - location: nodes["a"].Location, - cidrs: []*net.IPNet{nodes["a"].Subnet}, - hostnames: []string{"a"}, - privateIPs: []net.IP{nodes["a"].InternalIP.IP}, - wireGuardIP: w1, + allowedIPs: []*net.IPNet{nodes["a"].Subnet, nodes["a"].InternalIP, {IP: w1, Mask: net.CIDRMask(32, 32)}}, + endpoint: nodes["a"].ExternalIP.IP, + key: nodes["a"].Key, + location: nodes["a"].Location, + cidrs: []*net.IPNet{nodes["a"].Subnet}, + hostnames: []string{"a"}, + privateIPs: []net.IP{nodes["a"].InternalIP.IP}, + persistentKeepalive: nodes["a"].PersistentKeepalive, + wireGuardIP: w1, }, { allowedIPs: []*net.IPNet{nodes["b"].Subnet, nodes["b"].InternalIP, nodes["c"].Subnet, nodes["c"].InternalIP, {IP: w2, Mask: net.CIDRMask(32, 32)}}, @@ -153,14 +155,15 @@ func TestNewTopology(t *testing.T) { wireGuardCIDR: &net.IPNet{IP: w2, Mask: net.CIDRMask(16, 32)}, segments: []*segment{ { - allowedIPs: []*net.IPNet{nodes["a"].Subnet, nodes["a"].InternalIP, {IP: w1, Mask: net.CIDRMask(32, 32)}}, - endpoint: nodes["a"].ExternalIP.IP, - key: nodes["a"].Key, - location: nodes["a"].Location, - cidrs: []*net.IPNet{nodes["a"].Subnet}, - hostnames: []string{"a"}, - privateIPs: []net.IP{nodes["a"].InternalIP.IP}, - wireGuardIP: w1, + allowedIPs: []*net.IPNet{nodes["a"].Subnet, nodes["a"].InternalIP, {IP: w1, Mask: net.CIDRMask(32, 32)}}, + endpoint: nodes["a"].ExternalIP.IP, + key: nodes["a"].Key, + location: nodes["a"].Location, + cidrs: []*net.IPNet{nodes["a"].Subnet}, + hostnames: []string{"a"}, + privateIPs: []net.IP{nodes["a"].InternalIP.IP}, + persistentKeepalive: nodes["a"].PersistentKeepalive, + wireGuardIP: w1, }, { allowedIPs: []*net.IPNet{nodes["b"].Subnet, nodes["b"].InternalIP, nodes["c"].Subnet, nodes["c"].InternalIP, {IP: w2, Mask: net.CIDRMask(32, 32)}}, @@ -189,14 +192,15 @@ func TestNewTopology(t *testing.T) { wireGuardCIDR: nil, segments: []*segment{ { - allowedIPs: []*net.IPNet{nodes["a"].Subnet, nodes["a"].InternalIP, {IP: w1, Mask: net.CIDRMask(32, 32)}}, - endpoint: nodes["a"].ExternalIP.IP, - key: nodes["a"].Key, - location: nodes["a"].Location, - cidrs: []*net.IPNet{nodes["a"].Subnet}, - hostnames: []string{"a"}, - privateIPs: []net.IP{nodes["a"].InternalIP.IP}, - wireGuardIP: w1, + allowedIPs: []*net.IPNet{nodes["a"].Subnet, nodes["a"].InternalIP, {IP: w1, Mask: net.CIDRMask(32, 32)}}, + endpoint: nodes["a"].ExternalIP.IP, + key: nodes["a"].Key, + location: nodes["a"].Location, + cidrs: []*net.IPNet{nodes["a"].Subnet}, + hostnames: []string{"a"}, + privateIPs: []net.IP{nodes["a"].InternalIP.IP}, + persistentKeepalive: nodes["a"].PersistentKeepalive, + wireGuardIP: w1, }, { allowedIPs: []*net.IPNet{nodes["b"].Subnet, nodes["b"].InternalIP, nodes["c"].Subnet, nodes["c"].InternalIP, {IP: w2, Mask: net.CIDRMask(32, 32)}}, @@ -225,14 +229,15 @@ func TestNewTopology(t *testing.T) { wireGuardCIDR: &net.IPNet{IP: w1, Mask: net.CIDRMask(16, 32)}, segments: []*segment{ { - allowedIPs: []*net.IPNet{nodes["a"].Subnet, nodes["a"].InternalIP, {IP: w1, Mask: net.CIDRMask(32, 32)}}, - endpoint: nodes["a"].ExternalIP.IP, - key: nodes["a"].Key, - location: nodes["a"].Name, - cidrs: []*net.IPNet{nodes["a"].Subnet}, - hostnames: []string{"a"}, - privateIPs: []net.IP{nodes["a"].InternalIP.IP}, - wireGuardIP: w1, + allowedIPs: []*net.IPNet{nodes["a"].Subnet, nodes["a"].InternalIP, {IP: w1, Mask: net.CIDRMask(32, 32)}}, + endpoint: nodes["a"].ExternalIP.IP, + key: nodes["a"].Key, + location: nodes["a"].Name, + cidrs: []*net.IPNet{nodes["a"].Subnet}, + hostnames: []string{"a"}, + privateIPs: []net.IP{nodes["a"].InternalIP.IP}, + persistentKeepalive: nodes["a"].PersistentKeepalive, + wireGuardIP: w1, }, { allowedIPs: []*net.IPNet{nodes["b"].Subnet, nodes["b"].InternalIP, {IP: w2, Mask: net.CIDRMask(32, 32)}}, @@ -271,14 +276,15 @@ func TestNewTopology(t *testing.T) { wireGuardCIDR: &net.IPNet{IP: w2, Mask: net.CIDRMask(16, 32)}, segments: []*segment{ { - allowedIPs: []*net.IPNet{nodes["a"].Subnet, nodes["a"].InternalIP, {IP: w1, Mask: net.CIDRMask(32, 32)}}, - endpoint: nodes["a"].ExternalIP.IP, - key: nodes["a"].Key, - location: nodes["a"].Name, - cidrs: []*net.IPNet{nodes["a"].Subnet}, - hostnames: []string{"a"}, - privateIPs: []net.IP{nodes["a"].InternalIP.IP}, - wireGuardIP: w1, + allowedIPs: []*net.IPNet{nodes["a"].Subnet, nodes["a"].InternalIP, {IP: w1, Mask: net.CIDRMask(32, 32)}}, + endpoint: nodes["a"].ExternalIP.IP, + key: nodes["a"].Key, + location: nodes["a"].Name, + cidrs: []*net.IPNet{nodes["a"].Subnet}, + hostnames: []string{"a"}, + privateIPs: []net.IP{nodes["a"].InternalIP.IP}, + persistentKeepalive: nodes["a"].PersistentKeepalive, + wireGuardIP: w1, }, { allowedIPs: []*net.IPNet{nodes["b"].Subnet, nodes["b"].InternalIP, {IP: w2, Mask: net.CIDRMask(32, 32)}}, @@ -317,14 +323,15 @@ func TestNewTopology(t *testing.T) { wireGuardCIDR: &net.IPNet{IP: w3, Mask: net.CIDRMask(16, 32)}, segments: []*segment{ { - allowedIPs: []*net.IPNet{nodes["a"].Subnet, nodes["a"].InternalIP, {IP: w1, Mask: net.CIDRMask(32, 32)}}, - endpoint: nodes["a"].ExternalIP.IP, - key: nodes["a"].Key, - location: nodes["a"].Name, - cidrs: []*net.IPNet{nodes["a"].Subnet}, - hostnames: []string{"a"}, - privateIPs: []net.IP{nodes["a"].InternalIP.IP}, - wireGuardIP: w1, + allowedIPs: []*net.IPNet{nodes["a"].Subnet, nodes["a"].InternalIP, {IP: w1, Mask: net.CIDRMask(32, 32)}}, + endpoint: nodes["a"].ExternalIP.IP, + key: nodes["a"].Key, + location: nodes["a"].Name, + cidrs: []*net.IPNet{nodes["a"].Subnet}, + hostnames: []string{"a"}, + privateIPs: []net.IP{nodes["a"].InternalIP.IP}, + persistentKeepalive: nodes["a"].PersistentKeepalive, + wireGuardIP: w1, }, { allowedIPs: []*net.IPNet{nodes["b"].Subnet, nodes["b"].InternalIP, {IP: w2, Mask: net.CIDRMask(32, 32)}}, @@ -1027,6 +1034,7 @@ AllowedIPs = 10.5.0.3/24 [Peer] PublicKey = key1 Endpoint = 10.1.0.1:51820 + PersistentKeepalive = 25 AllowedIPs = 10.2.1.0/24, 192.168.0.1/32, 10.4.0.1/32 [Peer] @@ -1051,6 +1059,7 @@ AllowedIPs = 10.5.0.3/24 [Peer] PublicKey = key1 Endpoint = 10.1.0.1:51820 + PersistentKeepalive = 25 AllowedIPs = 10.2.1.0/24, 192.168.0.1/32, 10.4.0.1/32 [Peer] @@ -1104,6 +1113,7 @@ AllowedIPs = 10.5.0.3/24 [Peer] PublicKey = key1 Endpoint = 10.1.0.1:51820 + PersistentKeepalive = 25 AllowedIPs = 10.2.1.0/24, 192.168.0.1/32, 10.4.0.1/32 [Peer] @@ -1133,6 +1143,7 @@ AllowedIPs = 10.5.0.3/24 [Peer] PublicKey = key1 Endpoint = 10.1.0.1:51820 + PersistentKeepalive = 25 AllowedIPs = 10.2.1.0/24, 192.168.0.1/32, 10.4.0.1/32 [Peer]