Преглед изворни кода

Nodes without private IPs

Allow nodes to have no private IPs.
Nodes without private IPs will automatically be put into
their own location.
leonnicolas пре 5 година
родитељ
комит
3a201ba0fa

+ 59 - 0
pkg/encapsulation/noop.go

@@ -0,0 +1,59 @@
+// Copyright 2021 the Kilo authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package encapsulation
+
+import (
+	"net"
+
+	"github.com/squat/kilo/pkg/iptables"
+)
+
+// Noop is an encapsulation that does nothing.
+type Noop Strategy
+
+// CleanUp will also do nothing.
+func (n Noop) CleanUp() error {
+	return nil
+}
+
+// Gw will also do nothing.
+func (n Noop) Gw(_ net.IP, _ net.IP, _ *net.IPNet) net.IP {
+	return nil
+}
+
+// Index will also do nothing.
+func (n Noop) Index() int {
+	return 0
+}
+
+// Init will also do nothing.
+func (n Noop) Init(_ int) error {
+	return nil
+}
+
+// Rules will also do nothing.
+func (n Noop) Rules(_ []*net.IPNet) []iptables.Rule {
+	return nil
+}
+
+// Set will also do nothing.
+func (n Noop) Set(_ *net.IPNet) error {
+	return nil
+}
+
+// Strategy will finally do nothing.
+func (n Noop) Strategy() Strategy {
+	return Strategy(n)
+}

+ 7 - 1
pkg/k8s/backend.go

@@ -209,7 +209,11 @@ func (nb *nodeBackend) Set(name string, node *mesh.Node) error {
 	}
 	n := old.DeepCopy()
 	n.ObjectMeta.Annotations[endpointAnnotationKey] = node.Endpoint.String()
-	n.ObjectMeta.Annotations[internalIPAnnotationKey] = node.InternalIP.String()
+	if node.InternalIP == nil {
+		n.ObjectMeta.Annotations[internalIPAnnotationKey] = ""
+	} else {
+		n.ObjectMeta.Annotations[internalIPAnnotationKey] = node.InternalIP.String()
+	}
 	n.ObjectMeta.Annotations[keyAnnotationKey] = string(node.Key)
 	n.ObjectMeta.Annotations[lastSeenAnnotationKey] = strconv.FormatInt(node.LastSeen, 10)
 	if node.WireGuardIP == nil {
@@ -289,6 +293,8 @@ func translateNode(node *v1.Node, topologyLabel string) *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.
+		// It is valid for the InternalIP to be nil,
+		// if the given node only has public IP addresses.
 		Endpoint:            endpoint,
 		InternalIP:          internalIP,
 		Key:                 []byte(node.ObjectMeta.Annotations[keyAnnotationKey]),

+ 27 - 0
pkg/k8s/backend_test.go

@@ -187,6 +187,33 @@ func TestTranslateNode(t *testing.T) {
 			},
 			subnet: "10.2.1.0/24",
 		},
+		{
+			name: "no InternalIP",
+			annotations: map[string]string{
+				endpointAnnotationKey:    "10.0.0.1:51820",
+				internalIPAnnotationKey:  "",
+				keyAnnotationKey:         "foo",
+				lastSeenAnnotationKey:    "1000000000",
+				locationAnnotationKey:    "b",
+				persistentKeepaliveKey:   "25",
+				wireGuardIPAnnotationKey: "10.4.0.1/16",
+			},
+			labels: map[string]string{
+				RegionLabelKey: "a",
+			},
+			out: &mesh.Node{
+				Endpoint:            &wireguard.Endpoint{DNSOrIP: wireguard.DNSOrIP{IP: net.ParseIP("10.0.0.1")}, Port: 51820},
+				InternalIP:          nil,
+				Key:                 []byte("foo"),
+				LastSeen:            1000000000,
+				Leader:              false,
+				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",
+		},
 	} {
 		n := &v1.Node{}
 		n.ObjectMeta.Annotations = tc.annotations

+ 1 - 1
pkg/mesh/backend.go

@@ -70,7 +70,7 @@ type Node struct {
 // Ready indicates whether or not the node is ready.
 func (n *Node) Ready() bool {
 	// Nodes that are not leaders will not have WireGuardIPs, so it is not required.
-	return n != nil && n.Endpoint != nil && !(n.Endpoint.IP == nil && n.Endpoint.DNS == "") && n.Endpoint.Port != 0 && n.Key != nil && n.InternalIP != nil && n.Subnet != nil && time.Now().Unix()-n.LastSeen < int64(resyncPeriod)*2/int64(time.Second)
+	return n != nil && n.Endpoint != nil && !(n.Endpoint.IP == nil && n.Endpoint.DNS == "") && n.Endpoint.Port != 0 && n.Key != nil && n.Subnet != nil && time.Now().Unix()-n.LastSeen < int64(resyncPeriod)*2/int64(time.Second)
 }
 
 // Peer represents a peer in the network.

+ 3 - 4
pkg/mesh/discoverips.go

@@ -30,9 +30,7 @@ import (
 // - private IP to which hostname resolves
 // - private IP assigned to interface of default route
 // - private IP assigned to local interface
-// - public IP to which hostname resolves
-// - public IP assigned to interface of default route
-// - public IP assigned to local interface
+// - nil if no private IP was found
 // It selects the public IP address in the following order:
 // - public IP to which hostname resolves
 // - public IP assigned to interface of default route
@@ -153,7 +151,8 @@ func getIP(hostname string, ignoreIfaces ...int) (*net.IPNet, *net.IPNet, error)
 		return nil, nil, errors.New("no valid IP was found")
 	}
 	if len(priv) == 0 {
-		priv = pub
+		// If no private IPs were found, use nil.
+		priv = append(priv, nil)
 	}
 	if len(pub) == 0 {
 		pub = priv

+ 8 - 2
pkg/mesh/graph.go

@@ -70,7 +70,11 @@ func (t *Topology) Dot() (string, error) {
 					return "", fmt.Errorf("failed to add rank to node")
 				}
 			}
-			if err := g.Nodes.Lookup[graphEscape(s.hostnames[j])].Attrs.Add(string(gographviz.Label), nodeLabel(s.location, s.hostnames[j], s.cidrs[j], s.privateIPs[j], wg, endpoint)); err != nil {
+			var priv net.IP
+			if s.privateIPs != nil {
+				priv = s.privateIPs[j]
+			}
+			if err := g.Nodes.Lookup[graphEscape(s.hostnames[j])].Attrs.Add(string(gographviz.Label), nodeLabel(s.location, s.hostnames[j], s.cidrs[j], priv, wg, endpoint)); err != nil {
 				return "", fmt.Errorf("failed to add label to node")
 			}
 		}
@@ -155,7 +159,9 @@ func nodeLabel(location, name string, cidr *net.IPNet, priv, wgIP net.IP, endpoi
 		location,
 		name,
 		cidr.String(),
-		priv.String(),
+	}
+	if priv != nil {
+		label = append(label, priv.String())
 	}
 	if wgIP != nil {
 		label = append(label, wgIP.String())

+ 16 - 10
pkg/mesh/mesh.go

@@ -71,7 +71,7 @@ type Mesh struct {
 	wireGuardIP  *net.IPNet
 
 	// nodes and peers are mutable fields in the struct
-	// and needs to be guarded.
+	// and need to be guarded.
 	nodes map[string]*Node
 	peers map[string]*Peer
 	mu    sync.Mutex
@@ -125,17 +125,23 @@ func New(backend Backend, enc encapsulation.Encapsulator, granularity Granularit
 	if err != nil {
 		return nil, fmt.Errorf("failed to find public IP: %v", err)
 	}
-	ifaces, err := interfacesForIP(privateIP)
-	if err != nil {
-		return nil, fmt.Errorf("failed to find interface for private IP: %v", err)
-	}
-	privIface := ifaces[0].Index
-	if enc.Strategy() != encapsulation.Never {
-		if err := enc.Init(privIface); err != nil {
-			return nil, fmt.Errorf("failed to initialize encapsulator: %v", err)
+	var privIface int
+	if privateIP != nil {
+		ifaces, err := interfacesForIP(privateIP)
+		if err != nil {
+			return nil, fmt.Errorf("failed to find interface for private IP: %v", err)
 		}
+		privIface := ifaces[0].Index
+		if enc.Strategy() != encapsulation.Never {
+			if err := enc.Init(privIface); err != nil {
+				return nil, fmt.Errorf("failed to initialize encapsulator: %v", err)
+			}
+		}
+		level.Debug(logger).Log("msg", fmt.Sprintf("using %s as the private IP address", privateIP.String()))
+	} else {
+		enc = encapsulation.Noop(enc.Strategy())
+		level.Debug(logger).Log("msg", "running without a private IP address")
 	}
-	level.Debug(logger).Log("msg", fmt.Sprintf("using %s as the private IP address", privateIP.String()))
 	level.Debug(logger).Log("msg", fmt.Sprintf("using %s as the public IP address", publicIP.String()))
 	ipTables, err := iptables.New()
 	if err != nil {

+ 8 - 4
pkg/mesh/routes.go

@@ -97,9 +97,9 @@ func (t *Topology) Routes(kiloIfaceName string, kiloIface, privIface, tunlIface
 					LinkIndex: privIface,
 					Protocol:  unix.RTPROT_STATIC,
 				}, enc.Strategy(), t.privateIP, tunlIface))
+			}
+			for i := range segment.privateIPs {
 				// Add routes to the private IPs of nodes in other segments.
-				// Number of CIDRs and private IPs always match so
-				// we can reuse the loop.
 				routes = append(routes, encapsulateRoute(&netlink.Route{
 					Dst:       oneAddressCIDR(segment.privateIPs[i]),
 					Flags:     int(netlink.FLAG_ONLINK),
@@ -126,7 +126,9 @@ func (t *Topology) Routes(kiloIfaceName string, kiloIface, privIface, tunlIface
 	for _, segment := range t.segments {
 		// Add routes for the current segment if local is true.
 		if segment.location == t.location {
-			if local {
+			// If the local node does not have a private IP address,
+			// then skip adding routes, because the node is in its own location.
+			if local && t.privateIP != nil {
 				for i := range segment.cidrs {
 					// Don't add routes for the local node.
 					if segment.privateIPs[i].Equal(t.privateIP.IP) {
@@ -165,6 +167,8 @@ func (t *Topology) Routes(kiloIfaceName string, kiloIface, privIface, tunlIface
 					}
 				}
 			}
+			// Continuing here prevents leaders form adding routes via WireGuard to
+			// nodes in their own location.
 			continue
 		}
 		for i := range segment.cidrs {
@@ -180,7 +184,7 @@ func (t *Topology) Routes(kiloIfaceName string, kiloIface, privIface, tunlIface
 			// equals the external IP. This means that the node
 			// is only accessible through an external IP and we
 			// cannot encapsulate traffic to an IP through the IP.
-			if segment.privateIPs[i].Equal(segment.endpoint.IP) {
+			if segment.privateIPs == nil || segment.privateIPs[i].Equal(segment.endpoint.IP) {
 				continue
 			}
 			// Add routes to the private IPs of nodes in other segments.

+ 190 - 0
pkg/mesh/routes_test.go

@@ -74,6 +74,13 @@ func TestRoutes(t *testing.T) {
 					LinkIndex: kiloIface,
 					Protocol:  unix.RTPROT_STATIC,
 				},
+				{
+					Dst:       mustTopoForGranularityAndHost(LogicalGranularity, nodes["a"].Name).segments[2].cidrs[0],
+					Flags:     int(netlink.FLAG_ONLINK),
+					Gw:        mustTopoForGranularityAndHost(LogicalGranularity, nodes["a"].Name).segments[2].wireGuardIP,
+					LinkIndex: kiloIface,
+					Protocol:  unix.RTPROT_STATIC,
+				},
 				{
 					Dst:       peers["a"].AllowedIPs[0],
 					LinkIndex: kiloIface,
@@ -110,6 +117,13 @@ func TestRoutes(t *testing.T) {
 					LinkIndex: kiloIface,
 					Protocol:  unix.RTPROT_STATIC,
 				},
+				{
+					Dst:       mustTopoForGranularityAndHost(LogicalGranularity, nodes["b"].Name).segments[2].cidrs[0],
+					Flags:     int(netlink.FLAG_ONLINK),
+					Gw:        mustTopoForGranularityAndHost(LogicalGranularity, nodes["b"].Name).segments[2].wireGuardIP,
+					LinkIndex: kiloIface,
+					Protocol:  unix.RTPROT_STATIC,
+				},
 				{
 					Dst:       peers["a"].AllowedIPs[0],
 					LinkIndex: kiloIface,
@@ -160,6 +174,20 @@ func TestRoutes(t *testing.T) {
 					LinkIndex: privIface,
 					Protocol:  unix.RTPROT_STATIC,
 				},
+				{
+					Dst:       oneAddressCIDR(mustTopoForGranularityAndHost(LogicalGranularity, nodes["c"].Name).segments[2].wireGuardIP),
+					Flags:     int(netlink.FLAG_ONLINK),
+					Gw:        nodes["b"].InternalIP.IP,
+					LinkIndex: privIface,
+					Protocol:  unix.RTPROT_STATIC,
+				},
+				{
+					Dst:       mustTopoForGranularityAndHost(LogicalGranularity, nodes["c"].Name).segments[2].cidrs[0],
+					Flags:     int(netlink.FLAG_ONLINK),
+					Gw:        nodes["b"].InternalIP.IP,
+					LinkIndex: privIface,
+					Protocol:  unix.RTPROT_STATIC,
+				},
 				{
 					Dst:       peers["a"].AllowedIPs[0],
 					Flags:     int(netlink.FLAG_ONLINK),
@@ -183,6 +211,70 @@ func TestRoutes(t *testing.T) {
 				},
 			},
 		},
+		{
+			name:     "logical from d",
+			topology: mustTopoForGranularityAndHost(LogicalGranularity, nodes["d"].Name),
+			strategy: encapsulation.Never,
+			routes: []*netlink.Route{
+				{
+					Dst:       mustTopoForGranularityAndHost(LogicalGranularity, nodes["d"].Name).segments[0].cidrs[0],
+					Flags:     int(netlink.FLAG_ONLINK),
+					Gw:        mustTopoForGranularityAndHost(LogicalGranularity, nodes["d"].Name).segments[0].wireGuardIP,
+					LinkIndex: kiloIface,
+					Protocol:  unix.RTPROT_STATIC,
+				},
+				{
+					Dst:       oneAddressCIDR(nodes["a"].InternalIP.IP),
+					Flags:     int(netlink.FLAG_ONLINK),
+					Gw:        mustTopoForGranularityAndHost(LogicalGranularity, nodes["d"].Name).segments[0].wireGuardIP,
+					LinkIndex: kiloIface,
+					Protocol:  unix.RTPROT_STATIC,
+				},
+				{
+					Dst:       mustTopoForGranularityAndHost(LogicalGranularity, nodes["d"].Name).segments[1].cidrs[0],
+					Flags:     int(netlink.FLAG_ONLINK),
+					Gw:        mustTopoForGranularityAndHost(LogicalGranularity, nodes["d"].Name).segments[1].wireGuardIP,
+					LinkIndex: kiloIface,
+					Protocol:  unix.RTPROT_STATIC,
+				},
+				{
+					Dst:       oneAddressCIDR(nodes["b"].InternalIP.IP),
+					Flags:     int(netlink.FLAG_ONLINK),
+					Gw:        mustTopoForGranularityAndHost(LogicalGranularity, nodes["d"].Name).segments[1].wireGuardIP,
+					LinkIndex: kiloIface,
+					Protocol:  unix.RTPROT_STATIC,
+				},
+				{
+					Dst:       mustTopoForGranularityAndHost(LogicalGranularity, nodes["d"].Name).segments[1].cidrs[1],
+					Flags:     int(netlink.FLAG_ONLINK),
+					Gw:        mustTopoForGranularityAndHost(LogicalGranularity, nodes["d"].Name).segments[1].wireGuardIP,
+					LinkIndex: kiloIface,
+					Protocol:  unix.RTPROT_STATIC,
+				},
+				{
+					Dst:       oneAddressCIDR(nodes["c"].InternalIP.IP),
+					Flags:     int(netlink.FLAG_ONLINK),
+					Gw:        mustTopoForGranularityAndHost(LogicalGranularity, nodes["d"].Name).segments[1].wireGuardIP,
+					LinkIndex: kiloIface,
+					Protocol:  unix.RTPROT_STATIC,
+				},
+				{
+					Dst:       peers["a"].AllowedIPs[0],
+					LinkIndex: kiloIface,
+					Protocol:  unix.RTPROT_STATIC,
+				},
+				{
+					Dst:       peers["a"].AllowedIPs[1],
+					LinkIndex: kiloIface,
+					Protocol:  unix.RTPROT_STATIC,
+				},
+				{
+					Dst:       peers["b"].AllowedIPs[0],
+					LinkIndex: kiloIface,
+					Protocol:  unix.RTPROT_STATIC,
+				},
+			},
+		},
 		{
 			name:     "full from a",
 			topology: mustTopoForGranularityAndHost(FullGranularity, nodes["a"].Name),
@@ -216,6 +308,13 @@ func TestRoutes(t *testing.T) {
 					LinkIndex: kiloIface,
 					Protocol:  unix.RTPROT_STATIC,
 				},
+				{
+					Dst:       mustTopoForGranularityAndHost(FullGranularity, nodes["a"].Name).segments[3].cidrs[0],
+					Flags:     int(netlink.FLAG_ONLINK),
+					Gw:        mustTopoForGranularityAndHost(FullGranularity, nodes["a"].Name).segments[3].wireGuardIP,
+					LinkIndex: kiloIface,
+					Protocol:  unix.RTPROT_STATIC,
+				},
 				{
 					Dst:       peers["a"].AllowedIPs[0],
 					LinkIndex: kiloIface,
@@ -266,6 +365,13 @@ func TestRoutes(t *testing.T) {
 					LinkIndex: kiloIface,
 					Protocol:  unix.RTPROT_STATIC,
 				},
+				{
+					Dst:       mustTopoForGranularityAndHost(FullGranularity, nodes["b"].Name).segments[3].cidrs[0],
+					Flags:     int(netlink.FLAG_ONLINK),
+					Gw:        mustTopoForGranularityAndHost(FullGranularity, nodes["b"].Name).segments[3].wireGuardIP,
+					LinkIndex: kiloIface,
+					Protocol:  unix.RTPROT_STATIC,
+				},
 				{
 					Dst:       peers["a"].AllowedIPs[0],
 					LinkIndex: kiloIface,
@@ -316,6 +422,13 @@ func TestRoutes(t *testing.T) {
 					LinkIndex: kiloIface,
 					Protocol:  unix.RTPROT_STATIC,
 				},
+				{
+					Dst:       mustTopoForGranularityAndHost(FullGranularity, nodes["c"].Name).segments[3].cidrs[0],
+					Flags:     int(netlink.FLAG_ONLINK),
+					Gw:        mustTopoForGranularityAndHost(FullGranularity, nodes["c"].Name).segments[3].wireGuardIP,
+					LinkIndex: kiloIface,
+					Protocol:  unix.RTPROT_STATIC,
+				},
 				{
 					Dst:       peers["a"].AllowedIPs[0],
 					LinkIndex: kiloIface,
@@ -367,6 +480,13 @@ func TestRoutes(t *testing.T) {
 					LinkIndex: kiloIface,
 					Protocol:  unix.RTPROT_STATIC,
 				},
+				{
+					Dst:       nodes["d"].Subnet,
+					Flags:     int(netlink.FLAG_ONLINK),
+					Gw:        mustTopoForGranularityAndHost(LogicalGranularity, nodes["a"].Name).segments[2].wireGuardIP,
+					LinkIndex: kiloIface,
+					Protocol:  unix.RTPROT_STATIC,
+				},
 				{
 					Dst:       peers["a"].AllowedIPs[0],
 					LinkIndex: kiloIface,
@@ -418,6 +538,13 @@ func TestRoutes(t *testing.T) {
 					LinkIndex: kiloIface,
 					Protocol:  unix.RTPROT_STATIC,
 				},
+				{
+					Dst:       nodes["d"].Subnet,
+					Flags:     int(netlink.FLAG_ONLINK),
+					Gw:        mustTopoForGranularityAndHost(LogicalGranularity, nodes["a"].Name).segments[2].wireGuardIP,
+					LinkIndex: kiloIface,
+					Protocol:  unix.RTPROT_STATIC,
+				},
 				{
 					Dst:       peers["a"].AllowedIPs[0],
 					LinkIndex: kiloIface,
@@ -462,6 +589,13 @@ func TestRoutes(t *testing.T) {
 					LinkIndex: privIface,
 					Protocol:  unix.RTPROT_STATIC,
 				},
+				{
+					Dst:       nodes["d"].Subnet,
+					Flags:     int(netlink.FLAG_ONLINK),
+					Gw:        mustTopoForGranularityAndHost(LogicalGranularity, nodes["b"].Name).segments[2].wireGuardIP,
+					LinkIndex: kiloIface,
+					Protocol:  unix.RTPROT_STATIC,
+				},
 				{
 					Dst:       peers["a"].AllowedIPs[0],
 					LinkIndex: kiloIface,
@@ -514,6 +648,13 @@ func TestRoutes(t *testing.T) {
 					Protocol:  unix.RTPROT_STATIC,
 					Table:     kiloTableIndex,
 				},
+				{
+					Dst:       nodes["d"].Subnet,
+					Flags:     int(netlink.FLAG_ONLINK),
+					Gw:        mustTopoForGranularityAndHost(LogicalGranularity, nodes["b"].Name).segments[2].wireGuardIP,
+					LinkIndex: kiloIface,
+					Protocol:  unix.RTPROT_STATIC,
+				},
 				{
 					Dst:       peers["a"].AllowedIPs[0],
 					LinkIndex: kiloIface,
@@ -584,6 +725,20 @@ func TestRoutes(t *testing.T) {
 					LinkIndex: privIface,
 					Protocol:  unix.RTPROT_STATIC,
 				},
+				{
+					Dst:       oneAddressCIDR(mustTopoForGranularityAndHost(LogicalGranularity, nodes["c"].Name).segments[2].wireGuardIP),
+					Flags:     int(netlink.FLAG_ONLINK),
+					Gw:        nodes["b"].InternalIP.IP,
+					LinkIndex: privIface,
+					Protocol:  unix.RTPROT_STATIC,
+				},
+				{
+					Dst:       nodes["d"].Subnet,
+					Flags:     int(netlink.FLAG_ONLINK),
+					Gw:        nodes["b"].InternalIP.IP,
+					LinkIndex: privIface,
+					Protocol:  unix.RTPROT_STATIC,
+				},
 				{
 					Dst:       peers["a"].AllowedIPs[0],
 					Flags:     int(netlink.FLAG_ONLINK),
@@ -656,6 +811,20 @@ func TestRoutes(t *testing.T) {
 					Protocol:  unix.RTPROT_STATIC,
 					Table:     kiloTableIndex,
 				},
+				{
+					Dst:       oneAddressCIDR(mustTopoForGranularityAndHost(LogicalGranularity, nodes["c"].Name).segments[2].wireGuardIP),
+					Flags:     int(netlink.FLAG_ONLINK),
+					Gw:        nodes["b"].InternalIP.IP,
+					LinkIndex: tunlIface,
+					Protocol:  unix.RTPROT_STATIC,
+				},
+				{
+					Dst:       nodes["d"].Subnet,
+					Flags:     int(netlink.FLAG_ONLINK),
+					Gw:        nodes["b"].InternalIP.IP,
+					LinkIndex: tunlIface,
+					Protocol:  unix.RTPROT_STATIC,
+				},
 				{
 					Dst:       peers["a"].AllowedIPs[0],
 					Flags:     int(netlink.FLAG_ONLINK),
@@ -720,6 +889,13 @@ func TestRoutes(t *testing.T) {
 					LinkIndex: kiloIface,
 					Protocol:  unix.RTPROT_STATIC,
 				},
+				{
+					Dst:       nodes["d"].Subnet,
+					Flags:     int(netlink.FLAG_ONLINK),
+					Gw:        mustTopoForGranularityAndHost(FullGranularity, nodes["a"].Name).segments[3].wireGuardIP,
+					LinkIndex: kiloIface,
+					Protocol:  unix.RTPROT_STATIC,
+				},
 				{
 					Dst:       peers["a"].AllowedIPs[0],
 					LinkIndex: kiloIface,
@@ -771,6 +947,13 @@ func TestRoutes(t *testing.T) {
 					LinkIndex: kiloIface,
 					Protocol:  unix.RTPROT_STATIC,
 				},
+				{
+					Dst:       nodes["d"].Subnet,
+					Flags:     int(netlink.FLAG_ONLINK),
+					Gw:        mustTopoForGranularityAndHost(FullGranularity, nodes["b"].Name).segments[3].wireGuardIP,
+					LinkIndex: kiloIface,
+					Protocol:  unix.RTPROT_STATIC,
+				},
 				{
 					Dst:       peers["a"].AllowedIPs[0],
 					LinkIndex: kiloIface,
@@ -822,6 +1005,13 @@ func TestRoutes(t *testing.T) {
 					LinkIndex: kiloIface,
 					Protocol:  unix.RTPROT_STATIC,
 				},
+				{
+					Dst:       nodes["d"].Subnet,
+					Flags:     int(netlink.FLAG_ONLINK),
+					Gw:        mustTopoForGranularityAndHost(FullGranularity, nodes["c"].Name).segments[3].wireGuardIP,
+					LinkIndex: kiloIface,
+					Protocol:  unix.RTPROT_STATIC,
+				},
 				{
 					Dst:       peers["a"].AllowedIPs[0],
 					LinkIndex: kiloIface,

+ 20 - 6
pkg/mesh/topology.go

@@ -22,6 +22,11 @@ import (
 	"github.com/squat/kilo/pkg/wireguard"
 )
 
+const (
+	logicalLocationPrefix = "location:"
+	nodeLocationPrefix    = "node:"
+)
+
 // Topology represents the logical structure of the overlay network.
 type Topology struct {
 	// key is the private key of the node creating the topology.
@@ -77,18 +82,24 @@ func NewTopology(nodes map[string]*Node, peers map[string]*Peer, granularity Gra
 		var location string
 		switch granularity {
 		case LogicalGranularity:
-			location = node.Location
+			location = logicalLocationPrefix + node.Location
+			if node.InternalIP == nil {
+				location = nodeLocationPrefix + node.Name
+			}
 		case FullGranularity:
-			location = node.Name
+			location = nodeLocationPrefix + node.Name
 		}
 		topoMap[location] = append(topoMap[location], node)
 	}
 	var localLocation string
 	switch granularity {
 	case LogicalGranularity:
-		localLocation = nodes[hostname].Location
+		localLocation = logicalLocationPrefix + nodes[hostname].Location
+		if nodes[hostname].InternalIP == nil {
+			localLocation = nodeLocationPrefix + hostname
+		}
 	case FullGranularity:
-		localLocation = hostname
+		localLocation = nodeLocationPrefix + hostname
 	}
 
 	t := Topology{key: key, port: port, hostname: hostname, location: localLocation, persistentKeepalive: persistentKeepalive, privateIP: nodes[hostname].InternalIP, subnet: nodes[hostname].Subnet}
@@ -110,10 +121,13 @@ func NewTopology(nodes map[string]*Node, peers map[string]*Peer, granularity Gra
 			// - the node's allocated subnet
 			// - the node's WireGuard IP
 			// - the node's internal IP
-			allowedIPs = append(allowedIPs, node.Subnet, oneAddressCIDR(node.InternalIP.IP))
+			allowedIPs = append(allowedIPs, node.Subnet)
+			if node.InternalIP != nil {
+				allowedIPs = append(allowedIPs, oneAddressCIDR(node.InternalIP.IP))
+				privateIPs = append(privateIPs, node.InternalIP.IP)
+			}
 			cidrs = append(cidrs, node.Subnet)
 			hostnames = append(hostnames, node.Name)
-			privateIPs = append(privateIPs, node.InternalIP.IP)
 		}
 		t.segments = append(t.segments, &segment{
 			allowedIPs: allowedIPs,

+ 180 - 22
pkg/mesh/topology_test.go

@@ -33,6 +33,7 @@ func setup(t *testing.T) (map[string]*Node, map[string]*Peer, []byte, uint32) {
 	e1 := &net.IPNet{IP: net.ParseIP("10.1.0.1").To4(), Mask: net.CIDRMask(16, 32)}
 	e2 := &net.IPNet{IP: net.ParseIP("10.1.0.2").To4(), Mask: net.CIDRMask(16, 32)}
 	e3 := &net.IPNet{IP: net.ParseIP("10.1.0.3").To4(), Mask: net.CIDRMask(16, 32)}
+	e4 := &net.IPNet{IP: net.ParseIP("10.1.0.4").To4(), Mask: net.CIDRMask(16, 32)}
 	i1 := &net.IPNet{IP: net.ParseIP("192.168.0.1").To4(), Mask: net.CIDRMask(32, 32)}
 	i2 := &net.IPNet{IP: net.ParseIP("192.168.0.2").To4(), Mask: net.CIDRMask(32, 32)}
 	nodes := map[string]*Node{
@@ -57,11 +58,19 @@ func setup(t *testing.T) (map[string]*Node, map[string]*Peer, []byte, uint32) {
 			Name:       "c",
 			Endpoint:   &wireguard.Endpoint{DNSOrIP: wireguard.DNSOrIP{IP: e3.IP}, Port: DefaultKiloPort},
 			InternalIP: i2,
-			// Same location a node b.
+			// Same location as node b.
 			Location: "2",
 			Subnet:   &net.IPNet{IP: net.ParseIP("10.2.3.0"), Mask: net.CIDRMask(24, 32)},
 			Key:      []byte("key3"),
 		},
+		"d": {
+			Name:     "d",
+			Endpoint: &wireguard.Endpoint{DNSOrIP: wireguard.DNSOrIP{IP: e4.IP}, Port: DefaultKiloPort},
+			// Same location as node a, but without private IP
+			Location: "1",
+			Subnet:   &net.IPNet{IP: net.ParseIP("10.2.4.0"), Mask: net.CIDRMask(24, 32)},
+			Key:      []byte("key4"),
+		},
 	}
 	peers := map[string]*Peer{
 		"a": {
@@ -97,6 +106,7 @@ func TestNewTopology(t *testing.T) {
 	w1 := net.ParseIP("10.4.0.1").To4()
 	w2 := net.ParseIP("10.4.0.2").To4()
 	w3 := net.ParseIP("10.4.0.3").To4()
+	w4 := net.ParseIP("10.4.0.4").To4()
 	for _, tc := range []struct {
 		name        string
 		granularity Granularity
@@ -110,7 +120,7 @@ func TestNewTopology(t *testing.T) {
 			result: &Topology{
 				hostname:      nodes["a"].Name,
 				leader:        true,
-				location:      nodes["a"].Location,
+				location:      logicalLocationPrefix + nodes["a"].Location,
 				subnet:        nodes["a"].Subnet,
 				privateIP:     nodes["a"].InternalIP,
 				wireGuardCIDR: &net.IPNet{IP: w1, Mask: net.CIDRMask(16, 32)},
@@ -119,7 +129,7 @@ func TestNewTopology(t *testing.T) {
 						allowedIPs:  []*net.IPNet{nodes["a"].Subnet, nodes["a"].InternalIP, {IP: w1, Mask: net.CIDRMask(32, 32)}},
 						endpoint:    nodes["a"].Endpoint,
 						key:         nodes["a"].Key,
-						location:    nodes["a"].Location,
+						location:    logicalLocationPrefix + nodes["a"].Location,
 						cidrs:       []*net.IPNet{nodes["a"].Subnet},
 						hostnames:   []string{"a"},
 						privateIPs:  []net.IP{nodes["a"].InternalIP.IP},
@@ -129,12 +139,22 @@ func TestNewTopology(t *testing.T) {
 						allowedIPs:  []*net.IPNet{nodes["b"].Subnet, nodes["b"].InternalIP, nodes["c"].Subnet, nodes["c"].InternalIP, {IP: w2, Mask: net.CIDRMask(32, 32)}},
 						endpoint:    nodes["b"].Endpoint,
 						key:         nodes["b"].Key,
-						location:    nodes["b"].Location,
+						location:    logicalLocationPrefix + nodes["b"].Location,
 						cidrs:       []*net.IPNet{nodes["b"].Subnet, nodes["c"].Subnet},
 						hostnames:   []string{"b", "c"},
 						privateIPs:  []net.IP{nodes["b"].InternalIP.IP, nodes["c"].InternalIP.IP},
 						wireGuardIP: w2,
 					},
+					{
+						allowedIPs:  []*net.IPNet{nodes["d"].Subnet, {IP: w3, Mask: net.CIDRMask(32, 32)}},
+						endpoint:    nodes["d"].Endpoint,
+						key:         nodes["d"].Key,
+						location:    nodeLocationPrefix + nodes["d"].Name,
+						cidrs:       []*net.IPNet{nodes["d"].Subnet},
+						hostnames:   []string{"d"},
+						privateIPs:  nil,
+						wireGuardIP: w3,
+					},
 				},
 				peers: []*Peer{peers["a"], peers["b"]},
 			},
@@ -146,7 +166,7 @@ func TestNewTopology(t *testing.T) {
 			result: &Topology{
 				hostname:      nodes["b"].Name,
 				leader:        true,
-				location:      nodes["b"].Location,
+				location:      logicalLocationPrefix + nodes["b"].Location,
 				subnet:        nodes["b"].Subnet,
 				privateIP:     nodes["b"].InternalIP,
 				wireGuardCIDR: &net.IPNet{IP: w2, Mask: net.CIDRMask(16, 32)},
@@ -155,7 +175,7 @@ func TestNewTopology(t *testing.T) {
 						allowedIPs:  []*net.IPNet{nodes["a"].Subnet, nodes["a"].InternalIP, {IP: w1, Mask: net.CIDRMask(32, 32)}},
 						endpoint:    nodes["a"].Endpoint,
 						key:         nodes["a"].Key,
-						location:    nodes["a"].Location,
+						location:    logicalLocationPrefix + nodes["a"].Location,
 						cidrs:       []*net.IPNet{nodes["a"].Subnet},
 						hostnames:   []string{"a"},
 						privateIPs:  []net.IP{nodes["a"].InternalIP.IP},
@@ -165,12 +185,22 @@ func TestNewTopology(t *testing.T) {
 						allowedIPs:  []*net.IPNet{nodes["b"].Subnet, nodes["b"].InternalIP, nodes["c"].Subnet, nodes["c"].InternalIP, {IP: w2, Mask: net.CIDRMask(32, 32)}},
 						endpoint:    nodes["b"].Endpoint,
 						key:         nodes["b"].Key,
-						location:    nodes["b"].Location,
+						location:    logicalLocationPrefix + nodes["b"].Location,
 						cidrs:       []*net.IPNet{nodes["b"].Subnet, nodes["c"].Subnet},
 						hostnames:   []string{"b", "c"},
 						privateIPs:  []net.IP{nodes["b"].InternalIP.IP, nodes["c"].InternalIP.IP},
 						wireGuardIP: w2,
 					},
+					{
+						allowedIPs:  []*net.IPNet{nodes["d"].Subnet, {IP: w3, Mask: net.CIDRMask(32, 32)}},
+						endpoint:    nodes["d"].Endpoint,
+						key:         nodes["d"].Key,
+						location:    nodeLocationPrefix + nodes["d"].Name,
+						cidrs:       []*net.IPNet{nodes["d"].Subnet},
+						hostnames:   []string{"d"},
+						privateIPs:  nil,
+						wireGuardIP: w3,
+					},
 				},
 				peers: []*Peer{peers["a"], peers["b"]},
 			},
@@ -182,7 +212,7 @@ func TestNewTopology(t *testing.T) {
 			result: &Topology{
 				hostname:      nodes["c"].Name,
 				leader:        false,
-				location:      nodes["b"].Location,
+				location:      logicalLocationPrefix + nodes["b"].Location,
 				subnet:        nodes["c"].Subnet,
 				privateIP:     nodes["c"].InternalIP,
 				wireGuardCIDR: nil,
@@ -191,7 +221,7 @@ func TestNewTopology(t *testing.T) {
 						allowedIPs:  []*net.IPNet{nodes["a"].Subnet, nodes["a"].InternalIP, {IP: w1, Mask: net.CIDRMask(32, 32)}},
 						endpoint:    nodes["a"].Endpoint,
 						key:         nodes["a"].Key,
-						location:    nodes["a"].Location,
+						location:    logicalLocationPrefix + nodes["a"].Location,
 						cidrs:       []*net.IPNet{nodes["a"].Subnet},
 						hostnames:   []string{"a"},
 						privateIPs:  []net.IP{nodes["a"].InternalIP.IP},
@@ -201,12 +231,22 @@ func TestNewTopology(t *testing.T) {
 						allowedIPs:  []*net.IPNet{nodes["b"].Subnet, nodes["b"].InternalIP, nodes["c"].Subnet, nodes["c"].InternalIP, {IP: w2, Mask: net.CIDRMask(32, 32)}},
 						endpoint:    nodes["b"].Endpoint,
 						key:         nodes["b"].Key,
-						location:    nodes["b"].Location,
+						location:    logicalLocationPrefix + nodes["b"].Location,
 						cidrs:       []*net.IPNet{nodes["b"].Subnet, nodes["c"].Subnet},
 						hostnames:   []string{"b", "c"},
 						privateIPs:  []net.IP{nodes["b"].InternalIP.IP, nodes["c"].InternalIP.IP},
 						wireGuardIP: w2,
 					},
+					{
+						allowedIPs:  []*net.IPNet{nodes["d"].Subnet, {IP: w3, Mask: net.CIDRMask(32, 32)}},
+						endpoint:    nodes["d"].Endpoint,
+						key:         nodes["d"].Key,
+						location:    nodeLocationPrefix + nodes["d"].Name,
+						cidrs:       []*net.IPNet{nodes["d"].Subnet},
+						hostnames:   []string{"d"},
+						privateIPs:  nil,
+						wireGuardIP: w3,
+					},
 				},
 				peers: []*Peer{peers["a"], peers["b"]},
 			},
@@ -218,7 +258,7 @@ func TestNewTopology(t *testing.T) {
 			result: &Topology{
 				hostname:      nodes["a"].Name,
 				leader:        true,
-				location:      nodes["a"].Name,
+				location:      nodeLocationPrefix + nodes["a"].Name,
 				subnet:        nodes["a"].Subnet,
 				privateIP:     nodes["a"].InternalIP,
 				wireGuardCIDR: &net.IPNet{IP: w1, Mask: net.CIDRMask(16, 32)},
@@ -227,7 +267,7 @@ func TestNewTopology(t *testing.T) {
 						allowedIPs:  []*net.IPNet{nodes["a"].Subnet, nodes["a"].InternalIP, {IP: w1, Mask: net.CIDRMask(32, 32)}},
 						endpoint:    nodes["a"].Endpoint,
 						key:         nodes["a"].Key,
-						location:    nodes["a"].Name,
+						location:    nodeLocationPrefix + nodes["a"].Name,
 						cidrs:       []*net.IPNet{nodes["a"].Subnet},
 						hostnames:   []string{"a"},
 						privateIPs:  []net.IP{nodes["a"].InternalIP.IP},
@@ -237,7 +277,7 @@ func TestNewTopology(t *testing.T) {
 						allowedIPs:  []*net.IPNet{nodes["b"].Subnet, nodes["b"].InternalIP, {IP: w2, Mask: net.CIDRMask(32, 32)}},
 						endpoint:    nodes["b"].Endpoint,
 						key:         nodes["b"].Key,
-						location:    nodes["b"].Name,
+						location:    nodeLocationPrefix + nodes["b"].Name,
 						cidrs:       []*net.IPNet{nodes["b"].Subnet},
 						hostnames:   []string{"b"},
 						privateIPs:  []net.IP{nodes["b"].InternalIP.IP},
@@ -247,12 +287,22 @@ func TestNewTopology(t *testing.T) {
 						allowedIPs:  []*net.IPNet{nodes["c"].Subnet, nodes["c"].InternalIP, {IP: w3, Mask: net.CIDRMask(32, 32)}},
 						endpoint:    nodes["c"].Endpoint,
 						key:         nodes["c"].Key,
-						location:    nodes["c"].Name,
+						location:    nodeLocationPrefix + nodes["c"].Name,
 						cidrs:       []*net.IPNet{nodes["c"].Subnet},
 						hostnames:   []string{"c"},
 						privateIPs:  []net.IP{nodes["c"].InternalIP.IP},
 						wireGuardIP: w3,
 					},
+					{
+						allowedIPs:  []*net.IPNet{nodes["d"].Subnet, {IP: w4, Mask: net.CIDRMask(32, 32)}},
+						endpoint:    nodes["d"].Endpoint,
+						key:         nodes["d"].Key,
+						location:    nodeLocationPrefix + nodes["d"].Name,
+						cidrs:       []*net.IPNet{nodes["d"].Subnet},
+						hostnames:   []string{"d"},
+						privateIPs:  nil,
+						wireGuardIP: w4,
+					},
 				},
 				peers: []*Peer{peers["a"], peers["b"]},
 			},
@@ -264,7 +314,7 @@ func TestNewTopology(t *testing.T) {
 			result: &Topology{
 				hostname:      nodes["b"].Name,
 				leader:        true,
-				location:      nodes["b"].Name,
+				location:      nodeLocationPrefix + nodes["b"].Name,
 				subnet:        nodes["b"].Subnet,
 				privateIP:     nodes["b"].InternalIP,
 				wireGuardCIDR: &net.IPNet{IP: w2, Mask: net.CIDRMask(16, 32)},
@@ -273,7 +323,7 @@ func TestNewTopology(t *testing.T) {
 						allowedIPs:  []*net.IPNet{nodes["a"].Subnet, nodes["a"].InternalIP, {IP: w1, Mask: net.CIDRMask(32, 32)}},
 						endpoint:    nodes["a"].Endpoint,
 						key:         nodes["a"].Key,
-						location:    nodes["a"].Name,
+						location:    nodeLocationPrefix + nodes["a"].Name,
 						cidrs:       []*net.IPNet{nodes["a"].Subnet},
 						hostnames:   []string{"a"},
 						privateIPs:  []net.IP{nodes["a"].InternalIP.IP},
@@ -283,7 +333,7 @@ func TestNewTopology(t *testing.T) {
 						allowedIPs:  []*net.IPNet{nodes["b"].Subnet, nodes["b"].InternalIP, {IP: w2, Mask: net.CIDRMask(32, 32)}},
 						endpoint:    nodes["b"].Endpoint,
 						key:         nodes["b"].Key,
-						location:    nodes["b"].Name,
+						location:    nodeLocationPrefix + nodes["b"].Name,
 						cidrs:       []*net.IPNet{nodes["b"].Subnet},
 						hostnames:   []string{"b"},
 						privateIPs:  []net.IP{nodes["b"].InternalIP.IP},
@@ -293,12 +343,22 @@ func TestNewTopology(t *testing.T) {
 						allowedIPs:  []*net.IPNet{nodes["c"].Subnet, nodes["c"].InternalIP, {IP: w3, Mask: net.CIDRMask(32, 32)}},
 						endpoint:    nodes["c"].Endpoint,
 						key:         nodes["c"].Key,
-						location:    nodes["c"].Name,
+						location:    nodeLocationPrefix + nodes["c"].Name,
 						cidrs:       []*net.IPNet{nodes["c"].Subnet},
 						hostnames:   []string{"c"},
 						privateIPs:  []net.IP{nodes["c"].InternalIP.IP},
 						wireGuardIP: w3,
 					},
+					{
+						allowedIPs:  []*net.IPNet{nodes["d"].Subnet, {IP: w4, Mask: net.CIDRMask(32, 32)}},
+						endpoint:    nodes["d"].Endpoint,
+						key:         nodes["d"].Key,
+						location:    nodeLocationPrefix + nodes["d"].Name,
+						cidrs:       []*net.IPNet{nodes["d"].Subnet},
+						hostnames:   []string{"d"},
+						privateIPs:  nil,
+						wireGuardIP: w4,
+					},
 				},
 				peers: []*Peer{peers["a"], peers["b"]},
 			},
@@ -310,7 +370,7 @@ func TestNewTopology(t *testing.T) {
 			result: &Topology{
 				hostname:      nodes["c"].Name,
 				leader:        true,
-				location:      nodes["c"].Name,
+				location:      nodeLocationPrefix + nodes["c"].Name,
 				subnet:        nodes["c"].Subnet,
 				privateIP:     nodes["c"].InternalIP,
 				wireGuardCIDR: &net.IPNet{IP: w3, Mask: net.CIDRMask(16, 32)},
@@ -319,7 +379,7 @@ func TestNewTopology(t *testing.T) {
 						allowedIPs:  []*net.IPNet{nodes["a"].Subnet, nodes["a"].InternalIP, {IP: w1, Mask: net.CIDRMask(32, 32)}},
 						endpoint:    nodes["a"].Endpoint,
 						key:         nodes["a"].Key,
-						location:    nodes["a"].Name,
+						location:    nodeLocationPrefix + nodes["a"].Name,
 						cidrs:       []*net.IPNet{nodes["a"].Subnet},
 						hostnames:   []string{"a"},
 						privateIPs:  []net.IP{nodes["a"].InternalIP.IP},
@@ -329,7 +389,7 @@ func TestNewTopology(t *testing.T) {
 						allowedIPs:  []*net.IPNet{nodes["b"].Subnet, nodes["b"].InternalIP, {IP: w2, Mask: net.CIDRMask(32, 32)}},
 						endpoint:    nodes["b"].Endpoint,
 						key:         nodes["b"].Key,
-						location:    nodes["b"].Name,
+						location:    nodeLocationPrefix + nodes["b"].Name,
 						cidrs:       []*net.IPNet{nodes["b"].Subnet},
 						hostnames:   []string{"b"},
 						privateIPs:  []net.IP{nodes["b"].InternalIP.IP},
@@ -339,12 +399,78 @@ func TestNewTopology(t *testing.T) {
 						allowedIPs:  []*net.IPNet{nodes["c"].Subnet, nodes["c"].InternalIP, {IP: w3, Mask: net.CIDRMask(32, 32)}},
 						endpoint:    nodes["c"].Endpoint,
 						key:         nodes["c"].Key,
-						location:    nodes["c"].Name,
+						location:    nodeLocationPrefix + nodes["c"].Name,
 						cidrs:       []*net.IPNet{nodes["c"].Subnet},
 						hostnames:   []string{"c"},
 						privateIPs:  []net.IP{nodes["c"].InternalIP.IP},
 						wireGuardIP: w3,
 					},
+					{
+						allowedIPs:  []*net.IPNet{nodes["d"].Subnet, {IP: w4, Mask: net.CIDRMask(32, 32)}},
+						endpoint:    nodes["d"].Endpoint,
+						key:         nodes["d"].Key,
+						location:    nodeLocationPrefix + nodes["d"].Name,
+						cidrs:       []*net.IPNet{nodes["d"].Subnet},
+						hostnames:   []string{"d"},
+						privateIPs:  nil,
+						wireGuardIP: w4,
+					},
+				},
+				peers: []*Peer{peers["a"], peers["b"]},
+			},
+		},
+		{
+			name:        "full from d",
+			granularity: FullGranularity,
+			hostname:    nodes["d"].Name,
+			result: &Topology{
+				hostname:      nodes["d"].Name,
+				leader:        true,
+				location:      nodeLocationPrefix + nodes["d"].Name,
+				subnet:        nodes["d"].Subnet,
+				privateIP:     nil,
+				wireGuardCIDR: &net.IPNet{IP: w4, 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"].Endpoint,
+						key:         nodes["a"].Key,
+						location:    nodeLocationPrefix + 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["b"].Subnet, nodes["b"].InternalIP, {IP: w2, Mask: net.CIDRMask(32, 32)}},
+						endpoint:    nodes["b"].Endpoint,
+						key:         nodes["b"].Key,
+						location:    nodeLocationPrefix + nodes["b"].Name,
+						cidrs:       []*net.IPNet{nodes["b"].Subnet},
+						hostnames:   []string{"b"},
+						privateIPs:  []net.IP{nodes["b"].InternalIP.IP},
+						wireGuardIP: w2,
+					},
+					{
+						allowedIPs:  []*net.IPNet{nodes["c"].Subnet, nodes["c"].InternalIP, {IP: w3, Mask: net.CIDRMask(32, 32)}},
+						endpoint:    nodes["c"].Endpoint,
+						key:         nodes["c"].Key,
+						location:    nodeLocationPrefix + nodes["c"].Name,
+						cidrs:       []*net.IPNet{nodes["c"].Subnet},
+						hostnames:   []string{"c"},
+						privateIPs:  []net.IP{nodes["c"].InternalIP.IP},
+						wireGuardIP: w3,
+					},
+					{
+						allowedIPs:  []*net.IPNet{nodes["d"].Subnet, {IP: w4, Mask: net.CIDRMask(32, 32)}},
+						endpoint:    nodes["d"].Endpoint,
+						key:         nodes["d"].Key,
+						location:    nodeLocationPrefix + nodes["d"].Name,
+						cidrs:       []*net.IPNet{nodes["d"].Subnet},
+						hostnames:   []string{"d"},
+						privateIPs:  nil,
+						wireGuardIP: w4,
+					},
 				},
 				peers: []*Peer{peers["a"], peers["b"]},
 			},
@@ -390,6 +516,12 @@ Endpoint = 10.1.0.2:51820
 AllowedIPs = 10.2.2.0/24, 192.168.0.1/32, 10.2.3.0/24, 192.168.0.2/32, 10.4.0.2/32
 PersistentKeepalive = 25
 
+[Peer]
+PublicKey = key4
+Endpoint = 10.1.0.4:51820
+AllowedIPs = 10.2.4.0/24, 10.4.0.3/32
+PersistentKeepalive = 25
+
 [Peer]
 PublicKey = key4
 AllowedIPs = 10.5.0.1/24, 10.5.0.2/24
@@ -414,6 +546,11 @@ PersistentKeepalive = 25
 		Endpoint = 10.1.0.1:51820
 		AllowedIPs = 10.2.1.0/24, 192.168.0.1/32, 10.4.0.1/32
 
+		[Peer]
+		PublicKey = key4
+		Endpoint = 10.1.0.4:51820
+		AllowedIPs = 10.2.4.0/24, 10.4.0.3/32
+
 		[Peer]
 		PublicKey = key4
 		AllowedIPs = 10.5.0.1/24, 10.5.0.2/24
@@ -436,6 +573,11 @@ PersistentKeepalive = 25
 		Endpoint = 10.1.0.1:51820
 		AllowedIPs = 10.2.1.0/24, 192.168.0.1/32, 10.4.0.1/32
 
+		[Peer]
+		PublicKey = key4
+		Endpoint = 10.1.0.4:51820
+		AllowedIPs = 10.2.4.0/24, 10.4.0.3/32
+
 		[Peer]
 		PublicKey = key4
 		AllowedIPs = 10.5.0.1/24, 10.5.0.2/24
@@ -465,6 +607,12 @@ PersistentKeepalive = 25
 		AllowedIPs = 10.2.3.0/24, 192.168.0.2/32, 10.4.0.3/32
 		PersistentKeepalive = 25
 
+		[Peer]
+		PublicKey = key4
+		Endpoint = 10.1.0.4:51820
+		AllowedIPs = 10.2.4.0/24, 10.4.0.4/32
+		PersistentKeepalive = 25
+
 		[Peer]
 		PublicKey = key4
 		AllowedIPs = 10.5.0.1/24, 10.5.0.2/24
@@ -494,6 +642,11 @@ PersistentKeepalive = 25
 		Endpoint = 10.1.0.3:51820
 		AllowedIPs = 10.2.3.0/24, 192.168.0.2/32, 10.4.0.3/32
 
+		[Peer]
+		PublicKey = key4
+		Endpoint = 10.1.0.4:51820
+		AllowedIPs = 10.2.4.0/24, 10.4.0.4/32
+
 		[Peer]
 		PublicKey = key4
 		AllowedIPs = 10.5.0.1/24, 10.5.0.2/24
@@ -521,6 +674,11 @@ PersistentKeepalive = 25
 		Endpoint = 10.1.0.2:51820
 		AllowedIPs = 10.2.2.0/24, 192.168.0.1/32, 10.4.0.2/32
 
+		[Peer]
+		PublicKey = key4
+		Endpoint = 10.1.0.4:51820
+		AllowedIPs = 10.2.4.0/24, 10.4.0.4/32
+
 		[Peer]
 		PublicKey = key4
 		AllowedIPs = 10.5.0.1/24, 10.5.0.2/24