This commit is contained in:
Gary Guo 2025-11-08 19:43:27 -08:00 committed by GitHub
commit d86d4010b3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 153 additions and 2 deletions

View file

@ -369,6 +369,13 @@ firewall:
# is explicitly defined. This is usually not the desired behavior and should be avoided! # is explicitly defined. This is usually not the desired behavior and should be avoided!
#default_local_cidr_any: false #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: conntrack:
tcp_timeout: 12m tcp_timeout: 12m
udp_timeout: 3m udp_timeout: 3m

View file

@ -63,6 +63,7 @@ type Firewall struct {
rulesVersion uint16 rulesVersion uint16
defaultLocalCIDRAny bool defaultLocalCIDRAny bool
unsafePeerRouting bool
incomingMetrics firewallMetrics incomingMetrics firewallMetrics
outgoingMetrics 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.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") inboundAction := c.GetString("firewall.inbound_action", "drop")
switch inboundAction { 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 // Make sure remote address matches nebula certificate
if h.networks != nil { 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) f.metrics(incoming).droppedRemoteAddr.Inc(1)
return ErrInvalidRemoteIP 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 // 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) f.metrics(incoming).droppedLocalAddr.Inc(1)
return ErrInvalidLocalIP return ErrInvalidLocalIP
} }

View file

@ -736,6 +736,141 @@ func TestFirewall_DropIPSpoofing(t *testing.T) {
assert.Equal(t, fw.Drop(p, true, &h1, cp, nil), ErrInvalidRemoteIP) 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) { func BenchmarkLookup(b *testing.B) {
ml := func(m map[string]struct{}, a [][]string) { ml := func(m map[string]struct{}, a [][]string) {
for n := 0; n < b.N; n++ { for n := 0; n < b.N; n++ {