diff --git a/examples/config.yml b/examples/config.yml index 42c32c8a..4423f90a 100644 --- a/examples/config.yml +++ b/examples/config.yml @@ -369,6 +369,13 @@ firewall: # is explicitly defined. This is usually not the desired behavior and should be avoided! #default_local_cidr_any: false + # Allow one Nebula peer to route packets to another peer if they can both handle the destination address or the + # source address. By default, Nebula only allows a packet through a firewall if the sending peer can handle the + # source address and the receiving peer can handle the destination address, but in case where you'd want multi-hop + # routing, you can enable this option to relax the rule slightly. + # This option is needed for each peer that wants to route traffic in this way, but it's not needed for other hosts. + #unsafe_peer_routing: false + conntrack: tcp_timeout: 12m udp_timeout: 3m diff --git a/firewall.go b/firewall.go index 971c156d..f8720c95 100644 --- a/firewall.go +++ b/firewall.go @@ -63,6 +63,7 @@ type Firewall struct { rulesVersion uint16 defaultLocalCIDRAny bool + unsafePeerRouting bool incomingMetrics firewallMetrics outgoingMetrics firewallMetrics @@ -210,6 +211,7 @@ func NewFirewallFromConfig(l *logrus.Logger, cs *CertState, c *config.C) (*Firew ) fw.defaultLocalCIDRAny = c.GetBool("firewall.default_local_cidr_any", false) + fw.unsafePeerRouting = c.GetBool("firewall.unsafe_peer_routing", false) inboundAction := c.GetString("firewall.inbound_action", "drop") switch inboundAction { @@ -431,7 +433,10 @@ func (f *Firewall) Drop(fp firewall.Packet, incoming bool, h *HostInfo, caPool * // Make sure remote address matches nebula certificate if h.networks != nil { - if !h.networks.Contains(fp.RemoteAddr) { + // In case where `unsafe-peer-routing` is enabled, we also accept the packet if we only can handle LocalAddr. + // This is to support multi-hop routing via Nebula, e.g. a peer is fowarding an ingress traffic to us. + if !(h.networks.Contains(fp.RemoteAddr) || + f.unsafePeerRouting && h.networks.Contains(fp.LocalAddr)) { f.metrics(incoming).droppedRemoteAddr.Inc(1) return ErrInvalidRemoteIP } @@ -444,7 +449,11 @@ func (f *Firewall) Drop(fp firewall.Packet, incoming bool, h *HostInfo, caPool * } // Make sure we are supposed to be handling this local ip address - if !f.routableNetworks.Contains(fp.LocalAddr) { + // + // In case where `unsafe-peer-routing` is enabled, we also accept the packet if we can only handle RemoteAddr. + // This is to support multi-hop routing via Nebula, e.g. we are forwarding an ingress traffic to a peer. + if !(f.routableNetworks.Contains(fp.LocalAddr) || + f.unsafePeerRouting && f.routableNetworks.Contains(fp.RemoteAddr)) { f.metrics(incoming).droppedLocalAddr.Inc(1) return ErrInvalidLocalIP } diff --git a/firewall_test.go b/firewall_test.go index a0cb3c88..e660d2d3 100644 --- a/firewall_test.go +++ b/firewall_test.go @@ -736,6 +736,141 @@ func TestFirewall_DropIPSpoofing(t *testing.T) { assert.Equal(t, fw.Drop(p, true, &h1, cp, nil), ErrInvalidRemoteIP) } +func TestFirewall_DropUnsafePeerRouting(t *testing.T) { + l := test.NewLogger() + ob := &bytes.Buffer{} + l.SetOutput(ob) + + anyNetwork := netip.MustParsePrefix("0.0.0.0/0") + unsafeNetwork := netip.MustParsePrefix("198.51.100.0/24") + + c := cert.CachedCertificate{ + Certificate: &dummyCert{ + name: "host-owner", + networks: []netip.Prefix{netip.MustParsePrefix("192.0.2.1/24")}, + unsafeNetworks: []netip.Prefix{unsafeNetwork}, + }, + } + + // This is a peer that we want to route to/from. + c1 := cert.CachedCertificate{ + Certificate: &dummyCert{ + name: "peer", + networks: []netip.Prefix{netip.MustParsePrefix("192.0.2.2/24")}, + unsafeNetworks: []netip.Prefix{unsafeNetwork}, + issuer: "signer-sha", + }, + } + h1 := HostInfo{ + ConnectionState: &ConnectionState{ + peerCert: &c1, + }, + vpnAddrs: []netip.Addr{c1.Certificate.Networks()[0].Addr()}, + } + h1.buildNetworks(c1.Certificate.Networks(), c1.Certificate.UnsafeNetworks()) + + // This is another host in the Nebula network. + c2 := cert.CachedCertificate{ + Certificate: &dummyCert{ + name: "host", + networks: []netip.Prefix{netip.MustParsePrefix("192.0.2.3/24")}, + unsafeNetworks: []netip.Prefix{}, + issuer: "signer-sha", + }, + } + h2 := HostInfo{ + ConnectionState: &ConnectionState{ + peerCert: &c2, + }, + vpnAddrs: []netip.Addr{c2.Certificate.Networks()[0].Addr()}, + } + h2.buildNetworks(c2.Certificate.Networks(), c2.Certificate.UnsafeNetworks()) + + fw := NewFirewall(l, time.Second, time.Minute, time.Hour, c.Certificate) + fw2 := NewFirewall(l, time.Second, time.Minute, time.Hour, c.Certificate) + fw2.unsafePeerRouting = true + + // Add firewall rules. Due to `default_local_cidr_any` we need to explicitly add CIDR for unsafe network. + require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 1, 1, []string{}, "", netip.Prefix{}, anyNetwork, "", "")) + require.NoError(t, fw.AddRule(false, firewall.ProtoAny, 1, 1, []string{}, "", netip.Prefix{}, anyNetwork, "", "")) + require.NoError(t, fw2.AddRule(true, firewall.ProtoAny, 1, 1, []string{}, "", netip.Prefix{}, anyNetwork, "", "")) + require.NoError(t, fw2.AddRule(false, firewall.ProtoAny, 1, 1, []string{}, "", netip.Prefix{}, anyNetwork, "", "")) + cp := cert.NewCAPool() + + // Packet initially send from `host` to us should pass both config. + p := firewall.Packet{ + LocalAddr: netip.MustParseAddr("198.51.100.42"), + RemoteAddr: netip.MustParseAddr("192.0.2.3"), + LocalPort: 1, + RemotePort: 1, + Protocol: firewall.ProtoUDP, + Fragment: false, + } + + require.NoError(t, fw.Drop(p, true, &h2, cp, nil)) + require.NoError(t, fw2.Drop(p, true, &h2, cp, nil)) + + pRev := firewall.Packet{ + LocalAddr: netip.MustParseAddr("192.0.2.3"), + RemoteAddr: netip.MustParseAddr("198.51.100.42"), + LocalPort: 1, + RemotePort: 1, + Protocol: firewall.ProtoUDP, + Fragment: false, + } + + // Forward to `peer`, should only pass `fw2`. + assert.Equal(t, fw.Drop(pRev, false, &h1, cp, nil), ErrInvalidLocalIP) + require.NoError(t, fw2.Drop(pRev, false, &h1, cp, nil)) + + // Now let's test receiving end, check when the packet is received via `peer`. + resetConntrack(fw) + resetConntrack(fw2) + assert.Equal(t, fw.Drop(p, true, &h1, cp, nil), ErrInvalidRemoteIP) + require.NoError(t, fw2.Drop(p, true, &h1, cp, nil)) + + // The reverse direction, forward traffic to `peer` for it to reach `host`. + assert.Equal(t, fw.Drop(p, false, &h1, cp, nil), ErrInvalidRemoteIP) + require.NoError(t, fw2.Drop(p, false, &h1, cp, nil)) + + // Now let's test the receving end of the reverse direction, check when he packet is received via `peer`. + resetConntrack(fw) + resetConntrack(fw2) + assert.Equal(t, fw.Drop(pRev, true, &h1, cp, nil), ErrInvalidLocalIP) + require.NoError(t, fw2.Drop(pRev, true, &h1, cp, nil)) + + // Final reply to `host`. This time it's allowed regardless the config. + require.NoError(t, fw.Drop(p, false, &h2, cp, nil)) + require.NoError(t, fw2.Drop(p, false, &h2, cp, nil)) + + // Try some cases where the address is *not* covered by certificate and ensure they're rejected. + p = firewall.Packet{ + LocalAddr: netip.MustParseAddr("203.0.113.42"), + RemoteAddr: netip.MustParseAddr("192.0.2.3"), + LocalPort: 1, + RemotePort: 1, + Protocol: firewall.ProtoUDP, + Fragment: false, + } + pRev = firewall.Packet{ + LocalAddr: netip.MustParseAddr("192.0.2.3"), + RemoteAddr: netip.MustParseAddr("203.0.113.42"), + LocalPort: 1, + RemotePort: 1, + Protocol: firewall.ProtoUDP, + Fragment: false, + } + resetConntrack(fw2) + assert.Equal(t, fw2.Drop(p, true, &h2, cp, nil), ErrInvalidLocalIP) + assert.Equal(t, fw2.Drop(pRev, false, &h1, cp, nil), ErrInvalidRemoteIP) + resetConntrack(fw2) + assert.Equal(t, fw2.Drop(p, true, &h1, cp, nil), ErrInvalidRemoteIP) + assert.Equal(t, fw2.Drop(p, false, &h1, cp, nil), ErrInvalidRemoteIP) + resetConntrack(fw2) + assert.Equal(t, fw.Drop(pRev, true, &h1, cp, nil), ErrInvalidRemoteIP) + assert.Equal(t, fw.Drop(p, false, &h2, cp, nil), ErrInvalidLocalIP) +} + func BenchmarkLookup(b *testing.B) { ml := func(m map[string]struct{}, a [][]string) { for n := 0; n < b.N; n++ {