Merge pull request #179 from squat/location_allowed_ips

pkg/: FEATURE: support allowed IPs outside a cluster
This commit is contained in:
Lucas Servén Marín 2021-06-15 23:07:22 +02:00 committed by GitHub
commit 311414e63a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 376 additions and 26 deletions

View File

@ -60,7 +60,7 @@ func runGraph(_ *cobra.Command, _ []string) error {
peers[p.Name] = p
}
}
t, err := mesh.NewTopology(nodes, peers, opts.granularity, hostname, 0, []byte{}, subnet, nodes[hostname].PersistentKeepalive)
t, err := mesh.NewTopology(nodes, peers, opts.granularity, hostname, 0, []byte{}, subnet, nodes[hostname].PersistentKeepalive, nil)
if err != nil {
return fmt.Errorf("failed to create topology: %v", err)
}

View File

@ -147,7 +147,7 @@ func runShowConfNode(_ *cobra.Command, args []string) error {
}
}
t, err := mesh.NewTopology(nodes, peers, opts.granularity, hostname, opts.port, []byte{}, subnet, nodes[hostname].PersistentKeepalive)
t, err := mesh.NewTopology(nodes, peers, opts.granularity, hostname, opts.port, []byte{}, subnet, nodes[hostname].PersistentKeepalive, nil)
if err != nil {
return fmt.Errorf("failed to create topology: %v", err)
}
@ -236,7 +236,7 @@ func runShowConfPeer(_ *cobra.Command, args []string) error {
return fmt.Errorf("did not find any peer named %q in the cluster", peer)
}
t, err := mesh.NewTopology(nodes, peers, opts.granularity, hostname, mesh.DefaultKiloPort, []byte{}, subnet, peers[peer].PersistentKeepalive)
t, err := mesh.NewTopology(nodes, peers, opts.granularity, hostname, mesh.DefaultKiloPort, []byte{}, subnet, peers[peer].PersistentKeepalive, nil)
if err != nil {
return fmt.Errorf("failed to create topology: %v", err)
}

View File

@ -9,6 +9,7 @@ The following annotations can be added to any Kubernetes Node object to configur
|[kilo.squat.ai/leader](#leader)|string|`""`, `true`|
|[kilo.squat.ai/location](#location)|string|`gcp-east`, `lab`|
|[kilo.squat.ai/persistent-keepalive](#persistent-keepalive)|uint|`10`|
|[kilo.squat.ai/allowed-location-ips](#allowed-location-ips)|CIDR|`66.66.66.66/32`|
### force-endpoint
In order to create links between locations, Kilo requires at least one node in each location to have an endpoint, ie a `host:port` combination, that is routable from the other locations.
@ -52,3 +53,10 @@ In order for a node behind NAT to receive packets from nodes outside of the NATe
The frequency of emission of these keepalive packets can be controlled by setting the persistent-keepalive annotation on the node behind NAT.
The annotated node will use the specified value will as the persistent-keepalive interval for all of its peers.
For more background, [see the WireGuard documentation on NAT and firewall traversal](https://www.wireguard.com/quickstart/#nat-and-firewall-traversal-persistence).
### allowed-location-ips
It is possible to add allowed-location-ips to a location by annotating any node within that location.
Adding allowed-location-ips to a location makes these IPs routable from other locations as well.
In an example deployment of Kilo with two locations A and B, a printer in location A can be accessible from nodes and pods in location B.
Additionally, Kilo Peers can use the printer in location A.

View File

@ -59,6 +59,7 @@ const (
persistentKeepaliveKey = "kilo.squat.ai/persistent-keepalive"
wireGuardIPAnnotationKey = "kilo.squat.ai/wireguard-ip"
discoveredEndpointsKey = "kilo.squat.ai/discovered-endpoints"
allowedLocationIPsKey = "kilo.squat.ai/allowed-location-ips"
// RegionLabelKey is the key for the well-known Kubernetes topology region label.
RegionLabelKey = "topology.kubernetes.io/region"
jsonPatchSlash = "~1"
@ -311,6 +312,15 @@ func translateNode(node *v1.Node, topologyLabel string) *mesh.Node {
discoveredEndpoints = nil
}
}
// Set allowed IPs for a location.
var allowedLocationIPs []*net.IPNet
if str, ok := node.ObjectMeta.Annotations[allowedLocationIPsKey]; ok {
for _, ip := range strings.Split(str, ",") {
if ipnet := normalizeIP(ip); ipnet != nil {
allowedLocationIPs = append(allowedLocationIPs, ipnet)
}
}
}
return &mesh.Node{
// Endpoint and InternalIP should only ever fail to parse if the
@ -334,6 +344,7 @@ func translateNode(node *v1.Node, topologyLabel string) *mesh.Node {
// will parse as nil.
WireGuardIP: normalizeIP(node.ObjectMeta.Annotations[wireGuardIPAnnotationKey]),
DiscoveredEndpoints: discoveredEndpoints,
AllowedLocationIPs: allowedLocationIPs,
}
}

View File

@ -67,6 +67,7 @@ type Node struct {
Subnet *net.IPNet
WireGuardIP *net.IPNet
DiscoveredEndpoints map[string]*wireguard.Endpoint
AllowedLocationIPs []*net.IPNet
}
// Ready indicates whether or not the node is ready.

View File

@ -380,6 +380,7 @@ func (m *Mesh) handleLocal(n *Node) {
Subnet: n.Subnet,
WireGuardIP: m.wireGuardIP,
DiscoveredEndpoints: n.DiscoveredEndpoints,
AllowedLocationIPs: n.AllowedLocationIPs,
}
if !nodesAreEqual(n, local) {
level.Debug(m.logger).Log("msg", "local node differs from backend")
@ -460,7 +461,7 @@ func (m *Mesh) applyTopology() {
oldConf := wireguard.Parse(oldConfRaw)
natEndpoints := discoverNATEndpoints(nodes, peers, oldConf, m.logger)
nodes[m.hostname].DiscoveredEndpoints = natEndpoints
t, err := NewTopology(nodes, peers, m.granularity, m.hostname, nodes[m.hostname].Endpoint.Port, m.priv, m.subnet, nodes[m.hostname].PersistentKeepalive)
t, err := NewTopology(nodes, peers, m.granularity, m.hostname, nodes[m.hostname].Endpoint.Port, m.priv, m.subnet, nodes[m.hostname].PersistentKeepalive, m.logger)
if err != nil {
level.Error(m.logger).Log("error", err)
m.errorCounter.WithLabelValues("apply").Inc()
@ -674,7 +675,7 @@ func nodesAreEqual(a, b *Node) bool {
// Ignore LastSeen when comparing equality we want to check if the nodes are
// equivalent. However, we do want to check if LastSeen has transitioned
// between valid and invalid.
return string(a.Key) == string(b.Key) && ipNetsEqual(a.WireGuardIP, b.WireGuardIP) && ipNetsEqual(a.InternalIP, b.InternalIP) && a.Leader == b.Leader && a.Location == b.Location && a.Name == b.Name && subnetsEqual(a.Subnet, b.Subnet) && a.Ready() == b.Ready() && a.PersistentKeepalive == b.PersistentKeepalive && discoveredEndpointsAreEqual(a.DiscoveredEndpoints, b.DiscoveredEndpoints)
return string(a.Key) == string(b.Key) && ipNetsEqual(a.WireGuardIP, b.WireGuardIP) && ipNetsEqual(a.InternalIP, b.InternalIP) && a.Leader == b.Leader && a.Location == b.Location && a.Name == b.Name && subnetsEqual(a.Subnet, b.Subnet) && a.Ready() == b.Ready() && a.PersistentKeepalive == b.PersistentKeepalive && discoveredEndpointsAreEqual(a.DiscoveredEndpoints, b.DiscoveredEndpoints) && ipNetSlicesEqual(a.AllowedLocationIPs, b.AllowedLocationIPs)
}
func peersAreEqual(a, b *Peer) bool {
@ -713,6 +714,18 @@ func ipNetsEqual(a, b *net.IPNet) bool {
return a.IP.Equal(b.IP)
}
func ipNetSlicesEqual(a, b []*net.IPNet) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if !ipNetsEqual(a[i], b[i]) {
return false
}
}
return true
}
func subnetsEqual(a, b *net.IPNet) bool {
if a == nil && b == nil {
return true

View File

@ -108,6 +108,17 @@ func (t *Topology) Routes(kiloIfaceName string, kiloIface, privIface, tunlIface
Protocol: unix.RTPROT_STATIC,
}, enc.Strategy(), t.privateIP, tunlIface))
}
// For segments / locations other than the location of this instance of kg,
// we need to set routes for allowed location IPs over the leader in the current location.
for i := range segment.allowedLocationIPs {
routes = append(routes, encapsulateRoute(&netlink.Route{
Dst: segment.allowedLocationIPs[i],
Flags: int(netlink.FLAG_ONLINK),
Gw: gw,
LinkIndex: privIface,
Protocol: unix.RTPROT_STATIC,
}, enc.Strategy(), t.privateIP, tunlIface))
}
}
// Add routes for the allowed IPs of peers.
for _, peer := range t.peers {
@ -198,6 +209,17 @@ func (t *Topology) Routes(kiloIfaceName string, kiloIface, privIface, tunlIface
Protocol: unix.RTPROT_STATIC,
})
}
// For segments / locations other than the location of this instance of kg,
// we need to set routes for allowed location IPs over the wg interface.
for i := range segment.allowedLocationIPs {
routes = append(routes, &netlink.Route{
Dst: segment.allowedLocationIPs[i],
Flags: int(netlink.FLAG_ONLINK),
Gw: segment.wireGuardIP,
LinkIndex: kiloIface,
Protocol: unix.RTPROT_STATIC,
})
}
}
// Add routes for the allowed IPs of peers.
for _, peer := range t.peers {
@ -232,6 +254,16 @@ func (t *Topology) Rules(cni bool) []iptables.Rule {
for _, aip := range s.allowedIPs {
rules = append(rules, iptables.NewRule(iptables.GetProtocol(len(aip.IP)), "nat", "KILO-NAT", "-d", aip.String(), "-m", "comment", "--comment", "Kilo: do not NAT packets destined for known IPs", "-j", "RETURN"))
}
// Make sure packets to allowed location IPs go through the KILO-NAT chain, so they can be MASQUERADEd,
// Otherwise packets to these destinations will reach the destination, but never find their way back.
// We only want to NAT in locations of the corresponding allowed location IPs.
if t.location == s.location {
for _, alip := range s.allowedLocationIPs {
rules = append(rules,
iptables.NewRule(iptables.GetProtocol(len(alip.IP)), "nat", "POSTROUTING", "-d", alip.String(), "-m", "comment", "--comment", "Kilo: jump to NAT chain", "-j", "KILO-NAT"),
)
}
}
}
for _, p := range t.peers {
for _, aip := range p.AllowedIPs {

View File

@ -74,6 +74,13 @@ func TestRoutes(t *testing.T) {
LinkIndex: kiloIface,
Protocol: unix.RTPROT_STATIC,
},
{
Dst: nodes["b"].AllowedLocationIPs[0],
Flags: int(netlink.FLAG_ONLINK),
Gw: mustTopoForGranularityAndHost(LogicalGranularity, nodes["a"].Name).segments[1].wireGuardIP,
LinkIndex: kiloIface,
Protocol: unix.RTPROT_STATIC,
},
{
Dst: mustTopoForGranularityAndHost(LogicalGranularity, nodes["a"].Name).segments[2].cidrs[0],
Flags: int(netlink.FLAG_ONLINK),
@ -258,6 +265,13 @@ func TestRoutes(t *testing.T) {
LinkIndex: kiloIface,
Protocol: unix.RTPROT_STATIC,
},
{
Dst: nodes["b"].AllowedLocationIPs[0],
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,
@ -294,6 +308,13 @@ func TestRoutes(t *testing.T) {
LinkIndex: kiloIface,
Protocol: unix.RTPROT_STATIC,
},
{
Dst: nodes["b"].AllowedLocationIPs[0],
Flags: int(netlink.FLAG_ONLINK),
Gw: mustTopoForGranularityAndHost(FullGranularity, nodes["a"].Name).segments[1].wireGuardIP,
LinkIndex: kiloIface,
Protocol: unix.RTPROT_STATIC,
},
{
Dst: mustTopoForGranularityAndHost(FullGranularity, nodes["a"].Name).segments[2].cidrs[0],
Flags: int(netlink.FLAG_ONLINK),
@ -422,6 +443,13 @@ func TestRoutes(t *testing.T) {
LinkIndex: kiloIface,
Protocol: unix.RTPROT_STATIC,
},
{
Dst: nodes["b"].AllowedLocationIPs[0],
Flags: int(netlink.FLAG_ONLINK),
Gw: mustTopoForGranularityAndHost(FullGranularity, nodes["c"].Name).segments[1].wireGuardIP,
LinkIndex: kiloIface,
Protocol: unix.RTPROT_STATIC,
},
{
Dst: mustTopoForGranularityAndHost(FullGranularity, nodes["c"].Name).segments[3].cidrs[0],
Flags: int(netlink.FLAG_ONLINK),
@ -480,6 +508,13 @@ func TestRoutes(t *testing.T) {
LinkIndex: kiloIface,
Protocol: unix.RTPROT_STATIC,
},
{
Dst: nodes["b"].AllowedLocationIPs[0],
Flags: int(netlink.FLAG_ONLINK),
Gw: mustTopoForGranularityAndHost(LogicalGranularity, nodes["a"].Name).segments[1].wireGuardIP,
LinkIndex: kiloIface,
Protocol: unix.RTPROT_STATIC,
},
{
Dst: nodes["d"].Subnet,
Flags: int(netlink.FLAG_ONLINK),
@ -538,6 +573,13 @@ func TestRoutes(t *testing.T) {
LinkIndex: kiloIface,
Protocol: unix.RTPROT_STATIC,
},
{
Dst: nodes["b"].AllowedLocationIPs[0],
Flags: int(netlink.FLAG_ONLINK),
Gw: mustTopoForGranularityAndHost(LogicalGranularity, nodes["a"].Name).segments[1].wireGuardIP,
LinkIndex: kiloIface,
Protocol: unix.RTPROT_STATIC,
},
{
Dst: nodes["d"].Subnet,
Flags: int(netlink.FLAG_ONLINK),
@ -875,6 +917,13 @@ func TestRoutes(t *testing.T) {
LinkIndex: kiloIface,
Protocol: unix.RTPROT_STATIC,
},
{
Dst: nodes["b"].AllowedLocationIPs[0],
Flags: int(netlink.FLAG_ONLINK),
Gw: mustTopoForGranularityAndHost(FullGranularity, nodes["a"].Name).segments[1].wireGuardIP,
LinkIndex: kiloIface,
Protocol: unix.RTPROT_STATIC,
},
{
Dst: nodes["c"].Subnet,
Flags: int(netlink.FLAG_ONLINK),
@ -1005,6 +1054,13 @@ func TestRoutes(t *testing.T) {
LinkIndex: kiloIface,
Protocol: unix.RTPROT_STATIC,
},
{
Dst: nodes["b"].AllowedLocationIPs[0],
Flags: int(netlink.FLAG_ONLINK),
Gw: mustTopoForGranularityAndHost(FullGranularity, nodes["c"].Name).segments[1].wireGuardIP,
LinkIndex: kiloIface,
Protocol: unix.RTPROT_STATIC,
},
{
Dst: nodes["d"].Subnet,
Flags: int(netlink.FLAG_ONLINK),

View File

@ -19,6 +19,9 @@ import (
"net"
"sort"
"github.com/go-kit/kit/log"
"github.com/go-kit/kit/log/level"
"github.com/squat/kilo/pkg/wireguard"
)
@ -57,6 +60,7 @@ type Topology struct {
wireGuardCIDR *net.IPNet
// discoveredEndpoints is the updated map of valid discovered Endpoints
discoveredEndpoints map[string]*wireguard.Endpoint
logger log.Logger
}
type segment struct {
@ -78,10 +82,17 @@ type segment struct {
// wireGuardIP is the allocated IP address of the WireGuard
// interface on the leader of the segment.
wireGuardIP net.IP
// allowedLocationIPs are not part of the cluster and are not peers.
// They are directly routable from nodes within the segment.
// A classic example is a printer that ought to be routable from other locations.
allowedLocationIPs []*net.IPNet
}
// NewTopology creates a new Topology struct from a given set of nodes and peers.
func NewTopology(nodes map[string]*Node, peers map[string]*Peer, granularity Granularity, hostname string, port uint32, key []byte, subnet *net.IPNet, persistentKeepalive int) (*Topology, error) {
func NewTopology(nodes map[string]*Node, peers map[string]*Peer, granularity Granularity, hostname string, port uint32, key []byte, subnet *net.IPNet, persistentKeepalive int, logger log.Logger) (*Topology, error) {
if logger == nil {
logger = log.NewNopLogger()
}
topoMap := make(map[string][]*Node)
for _, node := range nodes {
var location string
@ -109,7 +120,7 @@ func NewTopology(nodes map[string]*Node, peers map[string]*Peer, granularity Gra
localLocation = nodeLocationPrefix + hostname
}
t := Topology{key: key, port: port, hostname: hostname, location: localLocation, persistentKeepalive: persistentKeepalive, privateIP: nodes[hostname].InternalIP, subnet: nodes[hostname].Subnet, wireGuardCIDR: subnet, discoveredEndpoints: make(map[string]*wireguard.Endpoint)}
t := Topology{key: key, port: port, hostname: hostname, location: localLocation, persistentKeepalive: persistentKeepalive, privateIP: nodes[hostname].InternalIP, subnet: nodes[hostname].Subnet, wireGuardCIDR: subnet, discoveredEndpoints: make(map[string]*wireguard.Endpoint), logger: logger}
for location := range topoMap {
// Sort the location so the result is stable.
sort.Slice(topoMap[location], func(i, j int) bool {
@ -120,6 +131,8 @@ func NewTopology(nodes map[string]*Node, peers map[string]*Peer, granularity Gra
t.leader = true
}
var allowedIPs []*net.IPNet
allowedLocationIPsMap := make(map[string]struct{})
var allowedLocationIPs []*net.IPNet
var cidrs []*net.IPNet
var hostnames []string
var privateIPs []net.IP
@ -128,7 +141,14 @@ 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
// - IPs that were specified by the allowed-location-ips annotation
allowedIPs = append(allowedIPs, node.Subnet)
for _, ip := range node.AllowedLocationIPs {
if _, ok := allowedLocationIPsMap[ip.String()]; !ok {
allowedLocationIPs = append(allowedLocationIPs, ip)
allowedLocationIPsMap[ip.String()] = struct{}{}
}
}
if node.InternalIP != nil {
allowedIPs = append(allowedIPs, oneAddressCIDR(node.InternalIP.IP))
privateIPs = append(privateIPs, node.InternalIP.IP)
@ -136,6 +156,10 @@ func NewTopology(nodes map[string]*Node, peers map[string]*Peer, granularity Gra
cidrs = append(cidrs, node.Subnet)
hostnames = append(hostnames, node.Name)
}
// The sorting has no function, but makes testing easier.
sort.Slice(allowedLocationIPs, func(i, j int) bool {
return allowedLocationIPs[i].String() < allowedLocationIPs[j].String()
})
t.segments = append(t.segments, &segment{
allowedIPs: allowedIPs,
endpoint: topoMap[location][leader].Endpoint,
@ -146,6 +170,7 @@ func NewTopology(nodes map[string]*Node, peers map[string]*Peer, granularity Gra
hostnames: hostnames,
leader: leader,
privateIPs: privateIPs,
allowedLocationIPs: allowedLocationIPs,
})
}
// Sort the Topology segments so the result is stable.
@ -189,11 +214,59 @@ func NewTopology(nodes map[string]*Node, peers map[string]*Peer, granularity Gra
}
}
}
// Check for intersecting IPs in allowed location IPs
segment.allowedLocationIPs = t.filterAllowedLocationIPs(segment.allowedLocationIPs, segment.location)
}
return &t, nil
}
func intersect(n1, n2 *net.IPNet) bool {
return n1.Contains(n2.IP) || n2.Contains(n1.IP)
}
func (t *Topology) filterAllowedLocationIPs(ips []*net.IPNet, location string) (ret []*net.IPNet) {
CheckIPs:
for _, ip := range ips {
for _, s := range t.segments {
// Check if allowed location IPs are also allowed in other locations.
if location != s.location {
for _, i := range s.allowedLocationIPs {
if intersect(ip, i) {
level.Warn(t.logger).Log("msg", "overlapping allowed location IPnets", "IP", ip.String(), "IP2", i.String(), "segment-location", s.location)
continue CheckIPs
}
}
}
// Check if allowed location IPs intersect with the allowed IPs.
for _, i := range s.allowedIPs {
if intersect(ip, i) {
level.Warn(t.logger).Log("msg", "overlapping allowed location IPnet with allowed IPnets", "IP", ip.String(), "IP2", i.String(), "segment-location", s.location)
continue CheckIPs
}
}
// Check if allowed location IPs intersect with the private IPs of the segment.
for _, i := range s.privateIPs {
if ip.Contains(i) {
level.Warn(t.logger).Log("msg", "overlapping allowed location IPnet with privateIP", "IP", ip.String(), "IP2", i.String(), "segment-location", s.location)
continue CheckIPs
}
}
}
// Check if allowed location IPs intersect with allowed IPs of peers.
for _, p := range t.peers {
for _, i := range p.AllowedIPs {
if intersect(ip, i) {
level.Warn(t.logger).Log("msg", "overlapping allowed location IPnet with peer IPnet", "IP", ip.String(), "IP2", i.String(), "peer", p.Name)
continue CheckIPs
}
}
}
ret = append(ret, ip)
}
return
}
func (t *Topology) updateEndpoint(endpoint *wireguard.Endpoint, key []byte, persistentKeepalive int) *wireguard.Endpoint {
// Do not update non-nat peers
if persistentKeepalive == 0 {
@ -219,7 +292,7 @@ func (t *Topology) Conf() *wireguard.Conf {
continue
}
peer := &wireguard.Peer{
AllowedIPs: s.allowedIPs,
AllowedIPs: append(s.allowedIPs, s.allowedLocationIPs...),
Endpoint: t.updateEndpoint(s.endpoint, s.key, s.persistentKeepalive),
PersistentKeepalive: t.persistentKeepalive,
PublicKey: s.key,

View File

@ -19,6 +19,7 @@ import (
"strings"
"testing"
"github.com/go-kit/kit/log"
"github.com/kylelemons/godebug/pretty"
"github.com/squat/kilo/pkg/wireguard"
@ -28,6 +29,15 @@ func allowedIPs(ips ...string) string {
return strings.Join(ips, ", ")
}
func mustParseCIDR(s string) (r *net.IPNet) {
if _, ip, err := net.ParseCIDR(s); err != nil {
panic("failed to parse CIDR")
} else {
r = ip
}
return
}
func setup(t *testing.T) (map[string]*Node, map[string]*Peer, []byte, uint32) {
key := []byte("private")
e1 := &net.IPNet{IP: net.ParseIP("10.1.0.1").To4(), Mask: net.CIDRMask(16, 32)}
@ -36,6 +46,7 @@ func setup(t *testing.T) (map[string]*Node, map[string]*Peer, []byte, uint32) {
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)}
i3 := &net.IPNet{IP: net.ParseIP("192.168.178.3").To4(), Mask: net.CIDRMask(32, 32)}
nodes := map[string]*Node{
"a": {
Name: "a",
@ -47,12 +58,13 @@ func setup(t *testing.T) (map[string]*Node, map[string]*Peer, []byte, uint32) {
PersistentKeepalive: 25,
},
"b": {
Name: "b",
Endpoint: &wireguard.Endpoint{DNSOrIP: wireguard.DNSOrIP{IP: e2.IP}, Port: DefaultKiloPort},
InternalIP: i1,
Location: "2",
Subnet: &net.IPNet{IP: net.ParseIP("10.2.2.0"), Mask: net.CIDRMask(24, 32)},
Key: []byte("key2"),
Name: "b",
Endpoint: &wireguard.Endpoint{DNSOrIP: wireguard.DNSOrIP{IP: e2.IP}, Port: DefaultKiloPort},
InternalIP: i1,
Location: "2",
Subnet: &net.IPNet{IP: net.ParseIP("10.2.2.0"), Mask: net.CIDRMask(24, 32)},
Key: []byte("key2"),
AllowedLocationIPs: []*net.IPNet{i3},
},
"c": {
Name: "c",
@ -146,6 +158,7 @@ func TestNewTopology(t *testing.T) {
hostnames: []string{"b", "c"},
privateIPs: []net.IP{nodes["b"].InternalIP.IP, nodes["c"].InternalIP.IP},
wireGuardIP: w2,
allowedLocationIPs: nodes["b"].AllowedLocationIPs,
},
{
allowedIPs: []*net.IPNet{nodes["d"].Subnet, {IP: w3, Mask: net.CIDRMask(32, 32)}},
@ -159,7 +172,8 @@ func TestNewTopology(t *testing.T) {
wireGuardIP: w3,
},
},
peers: []*Peer{peers["a"], peers["b"]},
peers: []*Peer{peers["a"], peers["b"]},
logger: log.NewNopLogger(),
},
},
{
@ -195,6 +209,7 @@ func TestNewTopology(t *testing.T) {
hostnames: []string{"b", "c"},
privateIPs: []net.IP{nodes["b"].InternalIP.IP, nodes["c"].InternalIP.IP},
wireGuardIP: w2,
allowedLocationIPs: nodes["b"].AllowedLocationIPs,
},
{
allowedIPs: []*net.IPNet{nodes["d"].Subnet, {IP: w3, Mask: net.CIDRMask(32, 32)}},
@ -208,7 +223,8 @@ func TestNewTopology(t *testing.T) {
wireGuardIP: w3,
},
},
peers: []*Peer{peers["a"], peers["b"]},
peers: []*Peer{peers["a"], peers["b"]},
logger: log.NewNopLogger(),
},
},
{
@ -244,6 +260,7 @@ func TestNewTopology(t *testing.T) {
hostnames: []string{"b", "c"},
privateIPs: []net.IP{nodes["b"].InternalIP.IP, nodes["c"].InternalIP.IP},
wireGuardIP: w2,
allowedLocationIPs: nodes["b"].AllowedLocationIPs,
},
{
allowedIPs: []*net.IPNet{nodes["d"].Subnet, {IP: w3, Mask: net.CIDRMask(32, 32)}},
@ -257,7 +274,8 @@ func TestNewTopology(t *testing.T) {
wireGuardIP: w3,
},
},
peers: []*Peer{peers["a"], peers["b"]},
peers: []*Peer{peers["a"], peers["b"]},
logger: log.NewNopLogger(),
},
},
{
@ -293,6 +311,7 @@ func TestNewTopology(t *testing.T) {
hostnames: []string{"b"},
privateIPs: []net.IP{nodes["b"].InternalIP.IP},
wireGuardIP: w2,
allowedLocationIPs: nodes["b"].AllowedLocationIPs,
},
{
allowedIPs: []*net.IPNet{nodes["c"].Subnet, nodes["c"].InternalIP, {IP: w3, Mask: net.CIDRMask(32, 32)}},
@ -317,7 +336,8 @@ func TestNewTopology(t *testing.T) {
wireGuardIP: w4,
},
},
peers: []*Peer{peers["a"], peers["b"]},
peers: []*Peer{peers["a"], peers["b"]},
logger: log.NewNopLogger(),
},
},
{
@ -353,6 +373,7 @@ func TestNewTopology(t *testing.T) {
hostnames: []string{"b"},
privateIPs: []net.IP{nodes["b"].InternalIP.IP},
wireGuardIP: w2,
allowedLocationIPs: nodes["b"].AllowedLocationIPs,
},
{
allowedIPs: []*net.IPNet{nodes["c"].Subnet, nodes["c"].InternalIP, {IP: w3, Mask: net.CIDRMask(32, 32)}},
@ -377,7 +398,8 @@ func TestNewTopology(t *testing.T) {
wireGuardIP: w4,
},
},
peers: []*Peer{peers["a"], peers["b"]},
peers: []*Peer{peers["a"], peers["b"]},
logger: log.NewNopLogger(),
},
},
{
@ -413,6 +435,7 @@ func TestNewTopology(t *testing.T) {
hostnames: []string{"b"},
privateIPs: []net.IP{nodes["b"].InternalIP.IP},
wireGuardIP: w2,
allowedLocationIPs: nodes["b"].AllowedLocationIPs,
},
{
allowedIPs: []*net.IPNet{nodes["c"].Subnet, nodes["c"].InternalIP, {IP: w3, Mask: net.CIDRMask(32, 32)}},
@ -437,7 +460,8 @@ func TestNewTopology(t *testing.T) {
wireGuardIP: w4,
},
},
peers: []*Peer{peers["a"], peers["b"]},
peers: []*Peer{peers["a"], peers["b"]},
logger: log.NewNopLogger(),
},
},
{
@ -473,6 +497,7 @@ func TestNewTopology(t *testing.T) {
hostnames: []string{"b"},
privateIPs: []net.IP{nodes["b"].InternalIP.IP},
wireGuardIP: w2,
allowedLocationIPs: nodes["b"].AllowedLocationIPs,
},
{
allowedIPs: []*net.IPNet{nodes["c"].Subnet, nodes["c"].InternalIP, {IP: w3, Mask: net.CIDRMask(32, 32)}},
@ -497,13 +522,14 @@ func TestNewTopology(t *testing.T) {
wireGuardIP: w4,
},
},
peers: []*Peer{peers["a"], peers["b"]},
peers: []*Peer{peers["a"], peers["b"]},
logger: log.NewNopLogger(),
},
},
} {
tc.result.key = key
tc.result.port = port
topo, err := NewTopology(nodes, peers, tc.granularity, tc.hostname, port, key, DefaultKiloSubnet, 0)
topo, err := NewTopology(nodes, peers, tc.granularity, tc.hostname, port, key, DefaultKiloSubnet, 0, nil)
if err != nil {
t.Errorf("test case %q: failed to generate Topology: %v", tc.name, err)
}
@ -514,7 +540,7 @@ func TestNewTopology(t *testing.T) {
}
func mustTopo(t *testing.T, nodes map[string]*Node, peers map[string]*Peer, granularity Granularity, hostname string, port uint32, key []byte, subnet *net.IPNet, persistentKeepalive int) *Topology {
topo, err := NewTopology(nodes, peers, granularity, hostname, port, key, subnet, persistentKeepalive)
topo, err := NewTopology(nodes, peers, granularity, hostname, port, key, subnet, persistentKeepalive, nil)
if err != nil {
t.Errorf("failed to generate Topology: %v", err)
}
@ -538,7 +564,7 @@ ListenPort = 51820
[Peer]
PublicKey = key2
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
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, 192.168.178.3/32
PersistentKeepalive = 25
[Peer]
@ -623,7 +649,7 @@ PersistentKeepalive = 25
[Peer]
PublicKey = key2
Endpoint = 10.1.0.2:51820
AllowedIPs = 10.2.2.0/24, 192.168.0.1/32, 10.4.0.2/32
AllowedIPs = 10.2.2.0/24, 192.168.0.1/32, 10.4.0.2/32, 192.168.178.3/32
PersistentKeepalive = 25
[Peer]
@ -697,7 +723,7 @@ PersistentKeepalive = 25
[Peer]
PublicKey = key2
Endpoint = 10.1.0.2:51820
AllowedIPs = 10.2.2.0/24, 192.168.0.1/32, 10.4.0.2/32
AllowedIPs = 10.2.2.0/24, 192.168.0.1/32, 10.4.0.2/32, 192.168.178.3/32
[Peer]
PublicKey = key4
@ -953,3 +979,133 @@ func TestDeduplicatePeerIPs(t *testing.T) {
}
}
}
func TestFilterAllowedIPs(t *testing.T) {
nodes, peers, key, port := setup(t)
topo := mustTopo(t, nodes, peers, LogicalGranularity, nodes["a"].Name, port, key, DefaultKiloSubnet, nodes["a"].PersistentKeepalive)
for _, tc := range []struct {
name string
allowedLocationIPs map[int][]*net.IPNet
result map[int][]*net.IPNet
}{
{
name: "nothing to filter",
allowedLocationIPs: map[int][]*net.IPNet{
0: {
mustParseCIDR("192.168.178.4/32"),
},
1: {
mustParseCIDR("192.168.178.5/32"),
},
2: {
mustParseCIDR("192.168.178.6/32"),
mustParseCIDR("192.168.178.7/32"),
},
},
result: map[int][]*net.IPNet{
0: {
mustParseCIDR("192.168.178.4/32"),
},
1: {
mustParseCIDR("192.168.178.5/32"),
},
2: {
mustParseCIDR("192.168.178.6/32"),
mustParseCIDR("192.168.178.7/32"),
},
},
},
{
name: "intersections between segments",
allowedLocationIPs: map[int][]*net.IPNet{
0: {
mustParseCIDR("192.168.178.4/32"),
mustParseCIDR("192.168.178.8/32"),
},
1: {
mustParseCIDR("192.168.178.5/32"),
},
2: {
mustParseCIDR("192.168.178.6/32"),
mustParseCIDR("192.168.178.7/32"),
mustParseCIDR("192.168.178.4/32"),
},
},
result: map[int][]*net.IPNet{
0: {
mustParseCIDR("192.168.178.8/32"),
},
1: {
mustParseCIDR("192.168.178.5/32"),
},
2: {
mustParseCIDR("192.168.178.6/32"),
mustParseCIDR("192.168.178.7/32"),
mustParseCIDR("192.168.178.4/32"),
},
},
},
{
name: "intersections with wireGuardCIDR",
allowedLocationIPs: map[int][]*net.IPNet{
0: {
mustParseCIDR("10.4.0.1/32"),
mustParseCIDR("192.168.178.8/32"),
},
1: {
mustParseCIDR("192.168.178.5/32"),
},
2: {
mustParseCIDR("192.168.178.6/32"),
mustParseCIDR("192.168.178.7/32"),
},
},
result: map[int][]*net.IPNet{
0: {
mustParseCIDR("192.168.178.8/32"),
},
1: {
mustParseCIDR("192.168.178.5/32"),
},
2: {
mustParseCIDR("192.168.178.6/32"),
mustParseCIDR("192.168.178.7/32"),
},
},
},
{
name: "intersections with more than one allowedLocationIPs",
allowedLocationIPs: map[int][]*net.IPNet{
0: {
mustParseCIDR("192.168.178.8/32"),
},
1: {
mustParseCIDR("192.168.178.5/32"),
},
2: {
mustParseCIDR("192.168.178.7/24"),
},
},
result: map[int][]*net.IPNet{
0: {},
1: {},
2: {
mustParseCIDR("192.168.178.7/24"),
},
},
},
} {
for k, v := range tc.allowedLocationIPs {
topo.segments[k].allowedLocationIPs = v
}
for k, v := range topo.segments {
f := topo.filterAllowedLocationIPs(v.allowedLocationIPs, v.location)
// Overwrite the allowedLocationIPs to mimic the actual usage of the filterAllowedLocationIPs function.
topo.segments[k].allowedLocationIPs = f
if !ipNetSlicesEqual(f, tc.result[k]) {
t.Errorf("test case %q:\n\texpected:\n\t%q\n\tgot:\n\t%q\n", tc.name, tc.result[k], f)
}
}
}
}