Compare commits

..

2 Commits

Author SHA1 Message Date
JackDoan be58a866d9 skeleton to handle other tunless packets 2026-02-11 10:54:46 -06:00
JackDoan ed960c7fb8 refactor 2026-02-11 10:54:34 -06:00
45 changed files with 583 additions and 3467 deletions
-3
View File
@@ -30,9 +30,6 @@ jobs:
- name: add hashicorp source - name: add hashicorp source
run: wget -O- https://apt.releases.hashicorp.com/gpg | gpg --dearmor | sudo tee /usr/share/keyrings/hashicorp-archive-keyring.gpg && echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list run: wget -O- https://apt.releases.hashicorp.com/gpg | gpg --dearmor | sudo tee /usr/share/keyrings/hashicorp-archive-keyring.gpg && echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
- name: workaround AMD-V issue # https://github.com/cri-o/packaging/pull/306
run: sudo rmmod kvm_amd
- name: install vagrant - name: install vagrant
run: sudo apt-get update && sudo apt-get install -y vagrant virtualbox run: sudo apt-get update && sudo apt-get install -y vagrant virtualbox
+9 -10
View File
@@ -37,18 +37,17 @@ docker run --name host4 --device /dev/net/tun:/dev/net/tun --cap-add NET_ADMIN -
sleep 1 sleep 1
# grab tcpdump pcaps for debugging # grab tcpdump pcaps for debugging
docker exec lighthouse1 tcpdump -i tun0 -q -w - -U 2>logs/lighthouse1.inside.log >logs/lighthouse1.inside.pcap & docker exec lighthouse1 tcpdump -i nebula1 -q -w - -U 2>logs/lighthouse1.inside.log >logs/lighthouse1.inside.pcap &
docker exec lighthouse1 tcpdump -i eth0 -q -w - -U 2>logs/lighthouse1.outside.log >logs/lighthouse1.outside.pcap & docker exec lighthouse1 tcpdump -i eth0 -q -w - -U 2>logs/lighthouse1.outside.log >logs/lighthouse1.outside.pcap &
docker exec host2 tcpdump -i tun0 -q -w - -U 2>logs/host2.inside.log >logs/host2.inside.pcap & docker exec host2 tcpdump -i nebula1 -q -w - -U 2>logs/host2.inside.log >logs/host2.inside.pcap &
docker exec host2 tcpdump -i eth0 -q -w - -U 2>logs/host2.outside.log >logs/host2.outside.pcap & docker exec host2 tcpdump -i eth0 -q -w - -U 2>logs/host2.outside.log >logs/host2.outside.pcap &
docker exec host3 tcpdump -i tun0 -q -w - -U 2>logs/host3.inside.log >logs/host3.inside.pcap & docker exec host3 tcpdump -i nebula1 -q -w - -U 2>logs/host3.inside.log >logs/host3.inside.pcap &
docker exec host3 tcpdump -i eth0 -q -w - -U 2>logs/host3.outside.log >logs/host3.outside.pcap & docker exec host3 tcpdump -i eth0 -q -w - -U 2>logs/host3.outside.log >logs/host3.outside.pcap &
docker exec host4 tcpdump -i tun0 -q -w - -U 2>logs/host4.inside.log >logs/host4.inside.pcap & docker exec host4 tcpdump -i nebula1 -q -w - -U 2>logs/host4.inside.log >logs/host4.inside.pcap &
docker exec host4 tcpdump -i eth0 -q -w - -U 2>logs/host4.outside.log >logs/host4.outside.pcap & docker exec host4 tcpdump -i eth0 -q -w - -U 2>logs/host4.outside.log >logs/host4.outside.pcap &
docker exec host2 ncat -nklv 0.0.0.0 2000 & docker exec host2 ncat -nklv 0.0.0.0 2000 &
docker exec host3 ncat -nklv 0.0.0.0 2000 & docker exec host3 ncat -nklv 0.0.0.0 2000 &
docker exec host4 ncat -nkluv 0.0.0.0 4000 &
docker exec host2 ncat -e '/usr/bin/echo host2' -nkluv 0.0.0.0 3000 & docker exec host2 ncat -e '/usr/bin/echo host2' -nkluv 0.0.0.0 3000 &
docker exec host3 ncat -e '/usr/bin/echo host3' -nkluv 0.0.0.0 3000 & docker exec host3 ncat -e '/usr/bin/echo host3' -nkluv 0.0.0.0 3000 &
@@ -120,11 +119,11 @@ echo
echo " *** Testing conntrack" echo " *** Testing conntrack"
echo echo
set -x set -x
# host2 can ping host3 now that host3 pinged it first
# host2 speaking to host4 on UDP 4000 should allow it to reply, when firewall rules would normally not permit this docker exec host2 ping -c1 192.168.100.3
docker exec host2 sh -c "/usr/bin/echo host2 | ncat -nuv 192.168.100.4 4000" # host4 can ping host2 once conntrack established
docker exec host2 ncat -e '/usr/bin/echo helloagainfromhost2' -nkluv 0.0.0.0 4000 & docker exec host2 ping -c1 192.168.100.4
docker exec host4 sh -c "/usr/bin/echo host4 | ncat -nuv 192.168.100.2 4000" docker exec host4 ping -c1 192.168.100.2
docker exec host4 sh -c 'kill 1' docker exec host4 sh -c 'kill 1'
docker exec host3 sh -c 'kill 1' docker exec host3 sh -c 'kill 1'
+1
View File
@@ -1,4 +1,5 @@
//go:build boringcrypto //go:build boringcrypto
// +build boringcrypto
package nebula package nebula
+5 -1
View File
@@ -439,7 +439,11 @@ func (c *certificateV2) validate() error {
if !hasV6Networks { if !hasV6Networks {
return NewErrInvalidCertificateProperties("IPv6 unsafe networks require an IPv6 address assignment: %s", network) return NewErrInvalidCertificateProperties("IPv6 unsafe networks require an IPv6 address assignment: %s", network)
} }
} // as long as we have any IP address, IPv4 UnsafeNetworks are allowed } else if network.Addr().Is4() {
if !hasV4Networks {
return NewErrInvalidCertificateProperties("IPv4 unsafe networks require an IPv4 address assignment: %s", network)
}
}
} }
} }
+1
View File
@@ -1,4 +1,5 @@
//go:build e2e_testing //go:build e2e_testing
// +build e2e_testing
package nebula package nebula
-400
View File
@@ -1,400 +0,0 @@
//go:build e2e_testing
// +build e2e_testing
package e2e
import (
"encoding/binary"
"net/netip"
"testing"
"time"
"github.com/slackhq/nebula/cert"
"github.com/slackhq/nebula/cert_test"
"github.com/slackhq/nebula/e2e/router"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// parseIPv4UDPPacket extracts source/dest IPs, ports, and payload from an IPv4 UDP packet.
func parseIPv4UDPPacket(t testing.TB, pkt []byte) (srcIP, dstIP netip.Addr, srcPort, dstPort uint16, payload []byte) {
t.Helper()
require.True(t, len(pkt) >= 28, "packet too short for IPv4+UDP header")
require.Equal(t, byte(0x45), pkt[0]&0xF0|pkt[0]&0x0F, "not a simple IPv4 packet (IHL!=5)")
srcIP, _ = netip.AddrFromSlice(pkt[12:16])
dstIP, _ = netip.AddrFromSlice(pkt[16:20])
ihl := int(pkt[0]&0x0F) * 4
require.True(t, len(pkt) >= ihl+8, "packet too short for UDP header")
srcPort = binary.BigEndian.Uint16(pkt[ihl : ihl+2])
dstPort = binary.BigEndian.Uint16(pkt[ihl+2 : ihl+4])
udpLen := binary.BigEndian.Uint16(pkt[ihl+4 : ihl+6])
payload = pkt[ihl+8 : ihl+int(udpLen)]
return
}
func TestSNAT_IPv6OnlyPeer_IPv4UnsafeTraffic(t *testing.T) {
// Scenario: Two IPv6-only VPN nodes. The "router" node has unsafe networks
// (192.168.0.0/16) in its cert and a configured SNAT address. The "sender"
// node has an unsafe route for 192.168.0.0/16 via the router.
//
// When sender injects an IPv4 packet destined for the unsafe network, it
// gets tunneled to the router. The router's firewall detects this is IPv4
// from an IPv6-only peer and applies SNAT, rewriting the source IP to the
// SNAT address before delivering it to TUN.
//
// When a reply comes back from TUN addressed to the SNAT address, the
// router un-SNATs it (restoring the original destination) and tunnels it
// back to the sender.
ca, _, caKey, _ := cert_test.NewTestCaCert(cert.Version2, cert.Curve_CURVE25519, time.Now(), time.Now().Add(10*time.Minute), nil, nil, []string{})
unsafePrefix := "192.168.0.0/16"
snatAddr := netip.MustParseAddr("169.254.42.42")
// Router: IPv6-only with unsafe networks and a manual SNAT address.
// Override inbound firewall with local_cidr: "any" so both IPv4 (unsafe)
// and IPv6 (VPN) traffic is accepted.
routerControl, routerVpnIpNet, routerUdpAddr, _ := newSimpleServerWithUdpAndUnsafeNetworks(
cert.Version2, ca, caKey, "router", "ff::1/64",
netip.MustParseAddrPort("[beef::1]:4242"),
unsafePrefix,
m{
"firewall": m{
"inbound": []m{{
"proto": "any",
"port": "any",
"host": "any",
"local_cidr": "any",
}},
},
"tun": m{
"snat_address_for_4over6": snatAddr.String(),
},
},
)
// Sender: IPv6-only with an unsafe route via the router
senderControl, _, _, _ := newSimpleServerWithUdp(
cert.Version2, ca, caKey, "sender", "ff::2/64",
netip.MustParseAddrPort("[beef::2]:4242"),
m{
"tun": m{
"unsafe_routes": []m{
{"route": unsafePrefix, "via": routerVpnIpNet[0].Addr().String()},
},
},
},
)
// Tell sender where the router lives
senderControl.InjectLightHouseAddr(routerVpnIpNet[0].Addr(), routerUdpAddr)
// Build the router and start both nodes
r := router.NewR(t, routerControl, senderControl)
defer r.RenderFlow()
routerControl.Start()
senderControl.Start()
// --- Outbound: sender -> IPv4 unsafe dest (via router with SNAT) ---
origSrcIP := netip.MustParseAddr("10.0.0.1")
unsafeDest := netip.MustParseAddr("192.168.1.1")
var origSrcPort uint16 = 12345
var dstPort uint16 = 80
t.Log("Sender injects an IPv4 packet to the unsafe network")
senderControl.InjectTunUDPPacket(unsafeDest, dstPort, origSrcIP, origSrcPort, []byte("snat me"))
t.Log("Route packets (handshake + data) until the router gets the packet on TUN")
snatPkt := r.RouteForAllUntilTxTun(routerControl)
t.Log("Verify the packet was SNATted")
gotSrcIP, gotDstIP, gotSrcPort, gotDstPort, gotPayload := parseIPv4UDPPacket(t, snatPkt)
assert.Equal(t, snatAddr, gotSrcIP, "source IP should be rewritten to the SNAT address")
assert.Equal(t, unsafeDest, gotDstIP, "destination IP should be unchanged")
assert.Equal(t, dstPort, gotDstPort, "destination port should be unchanged")
assert.Equal(t, []byte("snat me"), gotPayload, "payload should be unchanged")
// Capture the SNAT port (may differ from original if port was remapped)
snatPort := gotSrcPort
t.Logf("SNAT port: %d (original: %d)", snatPort, origSrcPort)
// --- Return: reply from unsafe dest -> un-SNATted back to sender ---
t.Log("Router injects a reply packet from the unsafe dest to the SNAT address")
routerControl.InjectTunUDPPacket(snatAddr, snatPort, unsafeDest, dstPort, []byte("reply from unsafe"))
t.Log("Route until sender gets the reply on TUN")
replyPkt := r.RouteForAllUntilTxTun(senderControl)
t.Log("Verify the reply was un-SNATted")
replySrcIP, replyDstIP, replySrcPort, replyDstPort, replyPayload := parseIPv4UDPPacket(t, replyPkt)
assert.Equal(t, unsafeDest, replySrcIP, "reply source should be the unsafe dest")
assert.Equal(t, origSrcIP, replyDstIP, "reply dest should be the original source IP (un-SNATted)")
assert.Equal(t, dstPort, replySrcPort, "reply source port should be the unsafe dest port")
assert.Equal(t, origSrcPort, replyDstPort, "reply dest port should be the original source port (un-SNATted)")
assert.Equal(t, []byte("reply from unsafe"), replyPayload, "payload should be unchanged")
r.RenderHostmaps("Final hostmaps", routerControl, senderControl)
// Also verify normal IPv6 VPN traffic still works between the nodes
t.Log("Verify normal IPv6 VPN tunnel still works")
assertTunnel(t, routerVpnIpNet[0].Addr(), senderControl.GetVpnAddrs()[0], routerControl, senderControl, r)
routerControl.Stop()
senderControl.Stop()
}
func TestSNAT_MultipleFlows(t *testing.T) {
// Test that multiple distinct IPv4 flows from the same IPv6-only peer
// are tracked independently through SNAT.
ca, _, caKey, _ := cert_test.NewTestCaCert(cert.Version2, cert.Curve_CURVE25519, time.Now(), time.Now().Add(10*time.Minute), nil, nil, []string{})
unsafePrefix := "192.168.0.0/16"
snatAddr := netip.MustParseAddr("169.254.42.42")
routerControl, routerVpnIpNet, routerUdpAddr, _ := newSimpleServerWithUdpAndUnsafeNetworks(
cert.Version2, ca, caKey, "router", "ff::1/64",
netip.MustParseAddrPort("[beef::1]:4242"),
unsafePrefix,
m{
"firewall": m{
"inbound": []m{{
"proto": "any",
"port": "any",
"host": "any",
"local_cidr": "any",
}},
},
"tun": m{
"snat_address_for_4over6": snatAddr.String(),
},
},
)
senderControl, _, _, _ := newSimpleServerWithUdp(
cert.Version2, ca, caKey, "sender", "ff::2/64",
netip.MustParseAddrPort("[beef::2]:4242"),
m{
"tun": m{
"unsafe_routes": []m{
{"route": unsafePrefix, "via": routerVpnIpNet[0].Addr().String()},
},
},
},
)
senderControl.InjectLightHouseAddr(routerVpnIpNet[0].Addr(), routerUdpAddr)
r := router.NewR(t, routerControl, senderControl)
defer r.RenderFlow()
r.CancelFlowLogs()
routerControl.Start()
senderControl.Start()
unsafeDest := netip.MustParseAddr("192.168.1.1")
// Send first flow
senderControl.InjectTunUDPPacket(unsafeDest, 80, netip.MustParseAddr("10.0.0.1"), 1111, []byte("flow1"))
pkt1 := r.RouteForAllUntilTxTun(routerControl)
srcIP1, _, srcPort1, _, payload1 := parseIPv4UDPPacket(t, pkt1)
assert.Equal(t, snatAddr, srcIP1)
assert.Equal(t, []byte("flow1"), payload1)
// Send second flow (different source port)
senderControl.InjectTunUDPPacket(unsafeDest, 80, netip.MustParseAddr("10.0.0.1"), 2222, []byte("flow2"))
pkt2 := r.RouteForAllUntilTxTun(routerControl)
srcIP2, _, srcPort2, _, payload2 := parseIPv4UDPPacket(t, pkt2)
assert.Equal(t, snatAddr, srcIP2)
assert.Equal(t, []byte("flow2"), payload2)
// The two flows should have different SNAT ports (since they're different conntracks)
t.Logf("Flow 1 SNAT port: %d, Flow 2 SNAT port: %d", srcPort1, srcPort2)
// Reply to flow 2 first (out of order)
routerControl.InjectTunUDPPacket(snatAddr, srcPort2, unsafeDest, 80, []byte("reply2"))
reply2 := r.RouteForAllUntilTxTun(senderControl)
_, replyDst2, _, replyDstPort2, replyPayload2 := parseIPv4UDPPacket(t, reply2)
assert.Equal(t, netip.MustParseAddr("10.0.0.1"), replyDst2)
assert.Equal(t, uint16(2222), replyDstPort2, "reply to flow 2 should restore original port 2222")
assert.Equal(t, []byte("reply2"), replyPayload2)
// Reply to flow 1
routerControl.InjectTunUDPPacket(snatAddr, srcPort1, unsafeDest, 80, []byte("reply1"))
reply1 := r.RouteForAllUntilTxTun(senderControl)
_, replyDst1, _, replyDstPort1, replyPayload1 := parseIPv4UDPPacket(t, reply1)
assert.Equal(t, netip.MustParseAddr("10.0.0.1"), replyDst1)
assert.Equal(t, uint16(1111), replyDstPort1, "reply to flow 1 should restore original port 1111")
assert.Equal(t, []byte("reply1"), replyPayload1)
routerControl.Stop()
senderControl.Stop()
}
// --- Adversarial SNAT E2E Tests ---
func TestSNAT_UnsolicitedReplyDropped(t *testing.T) {
// Without any outbound SNAT traffic, inject a packet from the router's TUN
// addressed to the SNAT address. The sender must never receive it because
// there's no conntrack entry to un-SNAT through.
ca, _, caKey, _ := cert_test.NewTestCaCert(cert.Version2, cert.Curve_CURVE25519, time.Now(), time.Now().Add(10*time.Minute), nil, nil, []string{})
unsafePrefix := "192.168.0.0/16"
snatAddr := netip.MustParseAddr("169.254.42.42")
routerControl, routerVpnIpNet, routerUdpAddr, _ := newSimpleServerWithUdpAndUnsafeNetworks(
cert.Version2, ca, caKey, "router", "ff::1/64",
netip.MustParseAddrPort("[beef::1]:4242"),
unsafePrefix,
m{
"firewall": m{
"inbound": []m{{
"proto": "any",
"port": "any",
"host": "any",
"local_cidr": "any",
}},
},
"tun": m{
"snat_address_for_4over6": snatAddr.String(),
},
},
)
senderControl, _, _, _ := newSimpleServerWithUdp(
cert.Version2, ca, caKey, "sender", "ff::2/64",
netip.MustParseAddrPort("[beef::2]:4242"),
m{
"tun": m{
"unsafe_routes": []m{
{"route": unsafePrefix, "via": routerVpnIpNet[0].Addr().String()},
},
},
},
)
senderControl.InjectLightHouseAddr(routerVpnIpNet[0].Addr(), routerUdpAddr)
r := router.NewR(t, routerControl, senderControl)
defer r.RenderFlow()
r.CancelFlowLogs()
routerControl.Start()
senderControl.Start()
// First establish the tunnel with normal IPv6 traffic so handshake completes
assertTunnel(t, routerVpnIpNet[0].Addr(), senderControl.GetVpnAddrs()[0], routerControl, senderControl, r)
// Inject the unsolicited reply from router's TUN to the SNAT address.
// There is NO prior outbound SNAT flow, so no conntrack entry exists.
// The router should silently drop this because unSnat finds no matching conntrack.
routerControl.InjectTunUDPPacket(snatAddr, 55555, netip.MustParseAddr("192.168.1.1"), 80, []byte("unsolicited"))
// Send a canary IPv6 VPN packet after the bad one. Since the router processes
// TUN packets sequentially, the canary arriving proves the bad packet was processed first.
senderVpnAddr := senderControl.GetVpnAddrs()[0]
routerControl.InjectTunUDPPacket(senderVpnAddr, 90, routerVpnIpNet[0].Addr(), 80, []byte("canary"))
canaryPkt := r.RouteForAllUntilTxTun(senderControl)
assertUdpPacket(t, []byte("canary"), canaryPkt, routerVpnIpNet[0].Addr(), senderVpnAddr, 80, 90)
// The unsolicited packet should have been dropped — nothing else on sender's TUN
got := senderControl.GetFromTun(false)
assert.Nil(t, got, "sender should not receive unsolicited packet to SNAT address with no conntrack entry")
routerControl.Stop()
senderControl.Stop()
}
func TestSNAT_NonUnsafeDestDropped(t *testing.T) {
// An IPv6-only sender sends IPv4 traffic to a destination outside the router's
// unsafe networks (172.16.0.1 when unsafe is 192.168.0.0/16). The router should
// reject this because the local address is not routable. This verifies that
// willingToHandleLocalAddr enforces boundaries on what SNAT traffic can reach.
ca, _, caKey, _ := cert_test.NewTestCaCert(cert.Version2, cert.Curve_CURVE25519, time.Now(), time.Now().Add(10*time.Minute), nil, nil, []string{})
unsafePrefix := "192.168.0.0/16"
snatAddr := netip.MustParseAddr("169.254.42.42")
routerControl, routerVpnIpNet, routerUdpAddr, _ := newSimpleServerWithUdpAndUnsafeNetworks(
cert.Version2, ca, caKey, "router", "ff::1/64",
netip.MustParseAddrPort("[beef::1]:4242"),
unsafePrefix,
m{
"firewall": m{
"inbound": []m{{
"proto": "any",
"port": "any",
"host": "any",
"local_cidr": "any",
}},
},
"tun": m{
"snat_address_for_4over6": snatAddr.String(),
},
},
)
// Sender has unsafe routes for BOTH 192.168.0.0/16 AND 172.16.0.0/12 via router.
// This means the sender will route 172.16.0.1 through the tunnel to the router.
// But the router should reject it because 172.16.0.0/12 is NOT in its unsafe networks.
senderControl, _, _, _ := newSimpleServerWithUdp(
cert.Version2, ca, caKey, "sender", "ff::2/64",
netip.MustParseAddrPort("[beef::2]:4242"),
m{
"tun": m{
"unsafe_routes": []m{
{"route": unsafePrefix, "via": routerVpnIpNet[0].Addr().String()},
{"route": "172.16.0.0/12", "via": routerVpnIpNet[0].Addr().String()},
},
},
},
)
senderControl.InjectLightHouseAddr(routerVpnIpNet[0].Addr(), routerUdpAddr)
r := router.NewR(t, routerControl, senderControl)
defer r.RenderFlow()
r.CancelFlowLogs()
routerControl.Start()
senderControl.Start()
// Establish the tunnel first
assertTunnel(t, routerVpnIpNet[0].Addr(), senderControl.GetVpnAddrs()[0], routerControl, senderControl, r)
// Send to 172.16.0.1 (NOT in router's unsafe networks 192.168.0.0/16).
// The router should reject this at willingToHandleLocalAddr.
senderControl.InjectTunUDPPacket(
netip.MustParseAddr("172.16.0.1"), 80,
netip.MustParseAddr("10.0.0.1"), 12345,
[]byte("wrong dest"),
)
// Send a canary to a valid unsafe destination to prove the bad packet was processed
senderControl.InjectTunUDPPacket(
netip.MustParseAddr("192.168.1.1"), 80,
netip.MustParseAddr("10.0.0.1"), 33333,
[]byte("canary"),
)
// Route until the canary arrives — the 172.16.0.1 packet should have been
// processed and dropped before the canary gets through
canaryPkt := r.RouteForAllUntilTxTun(routerControl)
_, canaryDst, _, _, canaryPayload := parseIPv4UDPPacket(t, canaryPkt)
assert.Equal(t, netip.MustParseAddr("192.168.1.1"), canaryDst, "canary should arrive at the valid unsafe dest")
assert.Equal(t, []byte("canary"), canaryPayload)
// No more packets — the 172.16.0.1 packet was dropped
got := routerControl.GetFromTun(false)
assert.Nil(t, got, "packet to non-unsafe destination 172.16.0.1 should be dropped by the router")
routerControl.Stop()
senderControl.Stop()
}
+1 -10
View File
@@ -283,15 +283,6 @@ tun:
# If using massive routes updates, for example BGP, you may need to increase this value to avoid packet loss. # If using massive routes updates, for example BGP, you may need to increase this value to avoid packet loss.
# SO_RCVBUFFORCE is used to avoid having to raise the system wide max # SO_RCVBUFFORCE is used to avoid having to raise the system wide max
#use_system_route_table_buffer_size: 0 #use_system_route_table_buffer_size: 0
#
# When using the feature to carry IPv4 unsafe_routes through a IPv6-only overlay network (4over6),
# the system still needs to use IPv4 addresses to pass that traffic.
# By default, they are randomly chosen out of 169.254.0.0/16 at startup, but if you need them to be static, you can set them here.
# Setting these options is not required for use of the 4over6 feature.
# this is the address that a client will use as a source IP for packets sent to the router
#unsafe_origin_address_for_4over6: 169.254.55.96
# this is the IP the router will use internally to hairpin-NAT 4over6 traffic before it hits the operating system.
#snat_address_for_4over6: 169.254.12.34
# Configure logging level # Configure logging level
logging: logging:
@@ -391,8 +382,8 @@ firewall:
# Rules are comprised of a protocol, port, and one or more of host, group, or CIDR # Rules are comprised of a protocol, port, and one or more of host, group, or CIDR
# Logical evaluation is roughly: port AND proto AND (ca_sha OR ca_name) AND (host OR group OR groups OR cidr) AND (local cidr) # Logical evaluation is roughly: port AND proto AND (ca_sha OR ca_name) AND (host OR group OR groups OR cidr) AND (local cidr)
# - port: Takes `0` or `any` as any, a single number `80`, a range `200-901`, or `fragment` to match second and further fragments of fragmented packets (since there is no port available). # - port: Takes `0` or `any` as any, a single number `80`, a range `200-901`, or `fragment` to match second and further fragments of fragmented packets (since there is no port available).
# code: same as port but makes more sense when talking about ICMP, TODO: this is not currently implemented in a way that works, use `any`
# proto: `any`, `tcp`, `udp`, or `icmp` # proto: `any`, `tcp`, `udp`, or `icmp`
# a port specification is ignored if proto is `icmp`
# host: `any` or a literal hostname, ie `test-host` # host: `any` or a literal hostname, ie `test-host`
# group: `any` or a literal group name, ie `default-group` # group: `any` or a literal group name, ie `default-group`
# groups: Same as group but accepts a list of values. Multiple values are AND'd together and a certificate would have to contain all groups to pass # groups: Same as group but accepts a list of values. Multiple values are AND'd together and a certificate would have to contain all groups to pass
+112 -345
View File
@@ -2,7 +2,6 @@ package nebula
import ( import (
"crypto/sha256" "crypto/sha256"
"encoding/binary"
"encoding/hex" "encoding/hex"
"errors" "errors"
"fmt" "fmt"
@@ -23,35 +22,10 @@ import (
"github.com/slackhq/nebula/firewall" "github.com/slackhq/nebula/firewall"
) )
var ErrCannotSNAT = errors.New("cannot SNAT this packet")
var ErrSNATIdentityMismatch = errors.New("refusing to SNAT for mismatched host")
var ErrSNATAddressCollision = errors.New("refusing to accept an incoming packet with my SNAT address")
const ipv4SourcePosition = 12
const ipv4DestinationPosition = 16
const sourcePortOffset = 0
const destinationPortOffset = 2
type FirewallInterface interface { type FirewallInterface interface {
AddRule(incoming bool, proto uint8, startPort int32, endPort int32, groups []string, host string, cidr, localCidr string, caName string, caSha string) error AddRule(incoming bool, proto uint8, startPort int32, endPort int32, groups []string, host string, cidr, localCidr string, caName string, caSha string) error
} }
type snatInfo struct {
//Src is the source IP+port to write into unsafe-route-bound packet
Src netip.AddrPort
//SrcVpnIp is the overlay IP associated with this flow. It's needed to associate reply traffic so we can get it back to the right host.
SrcVpnIp netip.Addr
//SnatPort is the port to rewrite into an overlay-bound packet
SnatPort uint16
}
func (s *snatInfo) Valid() bool {
if s == nil {
return false
}
return s.Src.IsValid()
}
type conn struct { type conn struct {
Expires time.Time // Time when this conntrack entry will expire Expires time.Time // Time when this conntrack entry will expire
@@ -60,9 +34,6 @@ type conn struct {
// fields pack for free after the uint32 above // fields pack for free after the uint32 above
incoming bool incoming bool
rulesVersion uint16 rulesVersion uint16
//for SNAT support
snat *snatInfo
} }
// TODO: need conntrack max tracked connections handling // TODO: need conntrack max tracked connections handling
@@ -95,8 +66,6 @@ type Firewall struct {
defaultLocalCIDRAny bool defaultLocalCIDRAny bool
incomingMetrics firewallMetrics incomingMetrics firewallMetrics
outgoingMetrics firewallMetrics outgoingMetrics firewallMetrics
unsafeIPv4Origin netip.Addr
snatAddr netip.Addr
l *logrus.Logger l *logrus.Logger
} }
@@ -224,12 +193,12 @@ func NewFirewall(l *logrus.Logger, tcpTimeout, UDPTimeout, defaultTimeout time.D
func NewFirewallFromConfig(l *logrus.Logger, cs *CertState, c *config.C) (*Firewall, error) { func NewFirewallFromConfig(l *logrus.Logger, cs *CertState, c *config.C) (*Firewall, error) {
certificate := cs.getCertificate(cert.Version2) certificate := cs.getCertificate(cert.Version2)
if certificate == nil { //todo if config.initiating_version is set to 1, and unsafe_networks differ, things will suck if certificate == nil {
certificate = cs.getCertificate(cert.Version1) certificate = cs.getCertificate(cert.Version1)
} }
if certificate == nil { if certificate == nil {
return nil, errors.New("no certificate available to reconfigure the firewall") panic("No certificate available to reconfigure the firewall")
} }
fw := NewFirewall( fw := NewFirewall(
@@ -238,6 +207,7 @@ func NewFirewallFromConfig(l *logrus.Logger, cs *CertState, c *config.C) (*Firew
c.GetDuration("firewall.conntrack.udp_timeout", time.Minute*3), c.GetDuration("firewall.conntrack.udp_timeout", time.Minute*3),
c.GetDuration("firewall.conntrack.default_timeout", time.Minute*10), c.GetDuration("firewall.conntrack.default_timeout", time.Minute*10),
certificate, certificate,
//TODO: max_connections
) )
fw.defaultLocalCIDRAny = c.GetBool("firewall.default_local_cidr_any", false) fw.defaultLocalCIDRAny = c.GetBool("firewall.default_local_cidr_any", false)
@@ -279,6 +249,20 @@ func NewFirewallFromConfig(l *logrus.Logger, cs *CertState, c *config.C) (*Firew
// AddRule properly creates the in memory rule structure for a firewall table. // AddRule properly creates the in memory rule structure for a firewall table.
func (f *Firewall) AddRule(incoming bool, proto uint8, startPort int32, endPort int32, groups []string, host string, cidr, localCidr, caName string, caSha string) error { func (f *Firewall) AddRule(incoming bool, proto uint8, startPort int32, endPort int32, groups []string, host string, cidr, localCidr, caName string, caSha string) error {
// We need this rule string because we generate a hash. Removing this will break firewall reload.
ruleString := fmt.Sprintf(
"incoming: %v, proto: %v, startPort: %v, endPort: %v, groups: %v, host: %v, ip: %v, localIp: %v, caName: %v, caSha: %s",
incoming, proto, startPort, endPort, groups, host, cidr, localCidr, caName, caSha,
)
f.rules += ruleString + "\n"
direction := "incoming"
if !incoming {
direction = "outgoing"
}
f.l.WithField("firewallRule", m{"direction": direction, "proto": proto, "startPort": startPort, "endPort": endPort, "groups": groups, "host": host, "cidr": cidr, "localCidr": localCidr, "caName": caName, "caSha": caSha}).
Info("Firewall rule added")
var ( var (
ft *FirewallTable ft *FirewallTable
fp firewallPort fp firewallPort
@@ -296,12 +280,6 @@ func (f *Firewall) AddRule(incoming bool, proto uint8, startPort int32, endPort
case firewall.ProtoUDP: case firewall.ProtoUDP:
fp = ft.UDP fp = ft.UDP
case firewall.ProtoICMP, firewall.ProtoICMPv6: case firewall.ProtoICMP, firewall.ProtoICMPv6:
//ICMP traffic doesn't have ports, so we always coerce to "any", even if a value is provided
if startPort != firewall.PortAny {
f.l.WithField("startPort", startPort).Warn("ignoring port specification for ICMP firewall rule")
}
startPort = firewall.PortAny
endPort = firewall.PortAny
fp = ft.ICMP fp = ft.ICMP
case firewall.ProtoAny: case firewall.ProtoAny:
fp = ft.AnyProto fp = ft.AnyProto
@@ -309,20 +287,6 @@ func (f *Firewall) AddRule(incoming bool, proto uint8, startPort int32, endPort
return fmt.Errorf("unknown protocol %v", proto) return fmt.Errorf("unknown protocol %v", proto)
} }
// We need this rule string because we generate a hash. Removing this will break firewall reload.
ruleString := fmt.Sprintf(
"incoming: %v, proto: %v, startPort: %v, endPort: %v, groups: %v, host: %v, ip: %v, localIp: %v, caName: %v, caSha: %s",
incoming, proto, startPort, endPort, groups, host, cidr, localCidr, caName, caSha,
)
f.rules += ruleString + "\n"
direction := "incoming"
if !incoming {
direction = "outgoing"
}
f.l.WithField("firewallRule", m{"direction": direction, "proto": proto, "startPort": startPort, "endPort": endPort, "groups": groups, "host": host, "cidr": cidr, "localCidr": localCidr, "caName": caName, "caSha": caSha}).
Info("Firewall rule added")
return fp.addRule(f, startPort, endPort, groups, host, cidr, localCidr, caName, caSha) return fp.addRule(f, startPort, endPort, groups, host, cidr, localCidr, caName, caSha)
} }
@@ -344,18 +308,6 @@ func (f *Firewall) GetRuleHashes() string {
return "SHA:" + f.GetRuleHash() + ",FNV:" + strconv.FormatUint(uint64(f.GetRuleHashFNV()), 10) return "SHA:" + f.GetRuleHash() + ",FNV:" + strconv.FormatUint(uint64(f.GetRuleHashFNV()), 10)
} }
func (f *Firewall) SetSNATAddressFromInterface(i *Interface) {
//address-mutation-avoidance is done inside Interface, the firewall doesn't need to care
//todo should snatted conntracks get expired out? Probably not needed until if/when we allow reload
f.snatAddr = i.inside.SNATAddress().Addr()
f.unsafeIPv4Origin = i.inside.UnsafeIPv4OriginAddress().Addr()
}
func (f *Firewall) ShouldUnSNAT(fp *firewall.Packet) bool {
// f.snatAddr is only valid if we're a snat-capable router
return f.snatAddr.IsValid() && fp.RemoteAddr == f.snatAddr
}
func AddFirewallRulesFromConfig(l *logrus.Logger, inbound bool, c *config.C, fw FirewallInterface) error { func AddFirewallRulesFromConfig(l *logrus.Logger, inbound bool, c *config.C, fw FirewallInterface) error {
var table string var table string
if inbound { if inbound {
@@ -397,31 +349,24 @@ func AddFirewallRulesFromConfig(l *logrus.Logger, inbound bool, c *config.C, fw
sPort = r.Port sPort = r.Port
} }
startPort, endPort, err := parsePort(sPort)
if err != nil {
return fmt.Errorf("%s rule #%v; %s %s", table, i, errPort, err)
}
var proto uint8 var proto uint8
var startPort, endPort int32
switch r.Proto { switch r.Proto {
case "any": case "any":
proto = firewall.ProtoAny proto = firewall.ProtoAny
startPort, endPort, err = parsePort(sPort)
case "tcp": case "tcp":
proto = firewall.ProtoTCP proto = firewall.ProtoTCP
startPort, endPort, err = parsePort(sPort)
case "udp": case "udp":
proto = firewall.ProtoUDP proto = firewall.ProtoUDP
startPort, endPort, err = parsePort(sPort)
case "icmp": case "icmp":
proto = firewall.ProtoICMP proto = firewall.ProtoICMP
startPort = firewall.PortAny
endPort = firewall.PortAny
if sPort != "" {
l.WithField("port", sPort).Warn("ignoring port specification for ICMP firewall rule")
}
default: default:
return fmt.Errorf("%s rule #%v; proto was not understood; `%s`", table, i, r.Proto) return fmt.Errorf("%s rule #%v; proto was not understood; `%s`", table, i, r.Proto)
} }
if err != nil {
return fmt.Errorf("%s rule #%v; %s %s", table, i, errPort, err)
}
if r.Cidr != "" && r.Cidr != "any" { if r.Cidr != "" && r.Cidr != "any" {
_, err = netip.ParsePrefix(r.Cidr) _, err = netip.ParsePrefix(r.Cidr)
@@ -456,207 +401,50 @@ var ErrInvalidRemoteIP = errors.New("remote address is not in remote certificate
var ErrInvalidLocalIP = errors.New("local address is not in list of handled local addresses") var ErrInvalidLocalIP = errors.New("local address is not in list of handled local addresses")
var ErrNoMatchingRule = errors.New("no matching rule in firewall table") var ErrNoMatchingRule = errors.New("no matching rule in firewall table")
func (f *Firewall) unSnat(data []byte, fp *firewall.Packet) netip.Addr {
c := f.peek(*fp) //unfortunately this needs to lock. Surely there's a better way.
if c == nil {
return netip.Addr{}
}
if !c.snat.Valid() {
return netip.Addr{}
}
oldIP := netip.AddrPortFrom(f.snatAddr, fp.RemotePort)
rewritePacket(data, fp, oldIP, c.snat.Src, ipv4DestinationPosition, destinationPortOffset)
return c.snat.SrcVpnIp
}
func rewritePacket(data []byte, fp *firewall.Packet, oldIP netip.AddrPort, newIP netip.AddrPort, ipOffset int, portOffset int) {
//change address
copy(data[ipOffset:], newIP.Addr().AsSlice())
recalcIPv4Checksum(data, oldIP.Addr(), newIP.Addr())
ipHeaderLen := int(data[0]&0x0F) * 4
switch fp.Protocol {
case firewall.ProtoICMP:
binary.BigEndian.PutUint16(data[ipHeaderLen+4:ipHeaderLen+6], newIP.Port()) //we use the ID field as a "port" for ICMP
icmpCode := uint16(data[ipHeaderLen+1]) //todo not snatting on code yet (but Linux would)
recalcICMPv4Checksum(data, icmpCode, icmpCode, oldIP.Port(), newIP.Port())
case firewall.ProtoUDP:
dstport := ipHeaderLen + portOffset
binary.BigEndian.PutUint16(data[dstport:dstport+2], newIP.Port())
recalcUDPv4Checksum(data, oldIP, newIP)
case firewall.ProtoTCP:
dstport := ipHeaderLen + portOffset
binary.BigEndian.PutUint16(data[dstport:dstport+2], newIP.Port())
recalcTCPv4Checksum(data, oldIP, newIP)
}
}
func (f *Firewall) findUsableSNATPort(fp *firewall.Packet, c *conn) error {
const halfThePorts = 0x7fff
oldPort := fp.RemotePort
conntrack := f.Conntrack
conntrack.Lock()
defer conntrack.Unlock()
for numPortsChecked := 0; numPortsChecked < halfThePorts; numPortsChecked++ {
_, ok := conntrack.Conns[*fp]
if !ok {
//yay, we can use this port
//track the snatted flow with the same expiration as the unsnatted version
c.snat.SnatPort = fp.RemotePort
conntrack.Conns[*fp] = c
return nil
}
//increment and retry. There's probably better strategies out there
fp.RemotePort++
if fp.RemotePort < halfThePorts {
fp.RemotePort += halfThePorts // keep it ephemeral for now
}
}
//if we made it here, we failed
fp.RemotePort = oldPort
return ErrCannotSNAT
}
func (f *Firewall) applySnat(data []byte, fp *firewall.Packet, c *conn, hostinfo *HostInfo) error {
if !f.snatAddr.IsValid() {
return ErrCannotSNAT
}
if f.snatAddr == fp.LocalAddr { //a packet that came from UDP (incoming) should never ever have our snat address on it
return ErrSNATAddressCollision
}
if c.snat.Valid() {
//old flow: make sure it came from the right place
if !slices.Contains(hostinfo.vpnAddrs, c.snat.SrcVpnIp) {
return ErrSNATIdentityMismatch
}
fp.RemoteAddr = f.snatAddr
fp.RemotePort = c.snat.SnatPort
} else if hostinfo.vpnAddrs[0].Is6() {
//we got a new flow
c.snat = &snatInfo{
Src: netip.AddrPortFrom(fp.RemoteAddr, fp.RemotePort),
SrcVpnIp: hostinfo.vpnAddrs[0],
}
fp.RemoteAddr = f.snatAddr
//find a new port to use, if needed
err := f.findUsableSNATPort(fp, c)
if err != nil {
c.snat = nil
return err
}
} else {
return ErrCannotSNAT
}
newIP := netip.AddrPortFrom(f.snatAddr, c.snat.SnatPort)
rewritePacket(data, fp, c.snat.Src, newIP, ipv4SourcePosition, sourcePortOffset)
return nil
}
func (f *Firewall) identifyRemoteNetworkType(h *HostInfo, fp firewall.Packet) NetworkType {
if h.networks == nil {
// Simple case: Certificate has one address and no unsafe networks
if h.vpnAddrs[0] == fp.RemoteAddr {
return NetworkTypeVPN
} //else, fallthrough
} else if nwType, ok := h.networks.Lookup(fp.RemoteAddr); ok {
return nwType //will return NetworkTypeVPN or NetworkTypeUnsafe
}
//RemoteAddr not in our networks table
if f.snatAddr.IsValid() && fp.IsIPv4() && h.HasOnlyV6Addresses() {
return NetworkTypeUnverifiedSNATPeer
} else {
return NetworkTypeInvalidPeer
}
}
func (f *Firewall) allowRemoteNetworkType(nwType NetworkType, fp firewall.Packet) error {
switch nwType {
case NetworkTypeVPN:
return nil
case NetworkTypeInvalidPeer:
return ErrInvalidRemoteIP
case NetworkTypeVPNPeer:
//one day we might need a specialSnatMode case in here to handle routers with v4 addresses when we don't also have a v4 address?
return ErrPeerRejected // reject for now, one day this may have different FW rules
case NetworkTypeUnsafe:
return nil // nothing special, one day this may have different FW rules
case NetworkTypeUnverifiedSNATPeer:
if f.unsafeIPv4Origin.IsValid() && fp.LocalAddr == f.unsafeIPv4Origin {
return nil //the client case
}
if f.snatAddr.IsValid() {
if fp.RemoteAddr == f.snatAddr {
return ErrInvalidRemoteIP //we should never get a packet with our SNAT addr as the destination, or "from" our SNAT addr
}
return nil
} else {
return ErrInvalidRemoteIP
}
default:
return ErrUnknownNetworkType //should never happen
}
}
func (f *Firewall) willingToHandleLocalAddr(incoming bool, fp firewall.Packet, remoteNwType NetworkType) error {
if f.routableNetworks.Contains(fp.LocalAddr) {
return nil //easy, this should handle NetworkTypeVPN in all cases, and NetworkTypeUnsafe on the router side
}
if incoming { //at least for now, reject all traffic other than what we've already decided is locally routable
return ErrInvalidLocalIP
}
//below this line, all traffic is outgoing. Outgoing traffic to NetworkTypeUnsafe is not required to be considered inbound-routable
if remoteNwType == NetworkTypeUnsafe {
return nil
}
return ErrInvalidLocalIP
}
// Drop returns an error if the packet should be dropped, explaining why. It // Drop returns an error if the packet should be dropped, explaining why. It
// returns nil if the packet should not be dropped. // returns nil if the packet should not be dropped.
func (f *Firewall) Drop(fp firewall.Packet, pkt []byte, incoming bool, h *HostInfo, caPool *cert.CAPool, localCache firewall.ConntrackCache) error { func (f *Firewall) Drop(fp firewall.Packet, incoming bool, h *HostInfo, caPool *cert.CAPool, localCache firewall.ConntrackCache) error {
table := f.OutRules
if incoming {
table = f.InRules
}
snatmode := fp.IsIPv4() && h.HasOnlyV6Addresses() && f.snatAddr.IsValid()
if snatmode {
//if this is an IPv4 packet from a V6 only host, and we're configured to snat that kind of traffic, it must be snatted,
//so it can never be in the localcache, which lacks SNAT data
//nil out the pointer to avoid ever using it
localCache = nil
}
// Check if we spoke to this tuple, if we did then allow this packet // Check if we spoke to this tuple, if we did then allow this packet
if localCache != nil { if f.inConns(fp, h, caPool, localCache) {
if _, ok := localCache[fp]; ok {
return nil //packet matched the cache, we're not snatting, we can return early
}
}
c := f.inConns(fp, h, caPool, localCache)
if c != nil {
if incoming && snatmode {
return f.applySnat(pkt, &fp, c, h)
}
return nil return nil
} }
// Make sure remote address matches nebula certificate, and determine how to treat it // Make sure remote address matches nebula certificate, and determine how to treat it
remoteNetworkType := f.identifyRemoteNetworkType(h, fp) if h.networks == nil {
if err := f.allowRemoteNetworkType(remoteNetworkType, fp); err != nil { // Simple case: Certificate has one address and no unsafe networks
f.metrics(incoming).droppedRemoteAddr.Inc(1) if h.vpnAddrs[0] != fp.RemoteAddr {
return err f.metrics(incoming).droppedRemoteAddr.Inc(1)
return ErrInvalidRemoteIP
}
} else {
nwType, ok := h.networks.Lookup(fp.RemoteAddr)
if !ok {
f.metrics(incoming).droppedRemoteAddr.Inc(1)
return ErrInvalidRemoteIP
}
switch nwType {
case NetworkTypeVPN:
break // nothing special
case NetworkTypeVPNPeer:
f.metrics(incoming).droppedRemoteAddr.Inc(1)
return ErrPeerRejected // reject for now, one day this may have different FW rules
case NetworkTypeUnsafe:
break // nothing special, one day this may have different FW rules
default:
f.metrics(incoming).droppedRemoteAddr.Inc(1)
return ErrUnknownNetworkType //should never happen
}
} }
// 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 err := f.willingToHandleLocalAddr(incoming, fp, remoteNetworkType); err != nil { if !f.routableNetworks.Contains(fp.LocalAddr) {
f.metrics(incoming).droppedLocalAddr.Inc(1) f.metrics(incoming).droppedLocalAddr.Inc(1)
return err return ErrInvalidLocalIP
}
table := f.OutRules
if incoming {
table = f.InRules
} }
// We now know which firewall table to check against // We now know which firewall table to check against
@@ -666,14 +454,9 @@ func (f *Firewall) Drop(fp firewall.Packet, pkt []byte, incoming bool, h *HostIn
} }
// We always want to conntrack since it is a faster operation // We always want to conntrack since it is a faster operation
c = f.addConn(fp, incoming) f.addConn(fp, incoming)
if incoming && remoteNetworkType == NetworkTypeUnverifiedSNATPeer { return nil
return f.applySnat(pkt, &fp, c, h)
} else {
//outgoing snat is handled before this function is called
return nil
}
} }
func (f *Firewall) metrics(incoming bool) firewallMetrics { func (f *Firewall) metrics(incoming bool) firewallMetrics {
@@ -684,7 +467,7 @@ func (f *Firewall) metrics(incoming bool) firewallMetrics {
} }
} }
// Destroy cleans up any known cyclical references so the object can be freed by GC. This should be called if a new // Destroy cleans up any known cyclical references so the object can be free'd my GC. This should be called if a new
// firewall object is created // firewall object is created
func (f *Firewall) Destroy() { func (f *Firewall) Destroy() {
//TODO: clean references if/when needed //TODO: clean references if/when needed
@@ -700,14 +483,12 @@ func (f *Firewall) EmitStats() {
metrics.GetOrRegisterGauge("firewall.rules.hash", nil).Update(int64(f.GetRuleHashFNV())) metrics.GetOrRegisterGauge("firewall.rules.hash", nil).Update(int64(f.GetRuleHashFNV()))
} }
func (f *Firewall) peek(fp firewall.Packet) *conn { func (f *Firewall) inConns(fp firewall.Packet, h *HostInfo, caPool *cert.CAPool, localCache firewall.ConntrackCache) bool {
f.Conntrack.Lock() if localCache != nil {
c := f.Conntrack.Conns[fp] if _, ok := localCache[fp]; ok {
f.Conntrack.Unlock() return true
return c }
} }
func (f *Firewall) inConns(fp firewall.Packet, h *HostInfo, caPool *cert.CAPool, localCache firewall.ConntrackCache) *conn {
conntrack := f.Conntrack conntrack := f.Conntrack
conntrack.Lock() conntrack.Lock()
@@ -721,7 +502,7 @@ func (f *Firewall) inConns(fp firewall.Packet, h *HostInfo, caPool *cert.CAPool,
if !ok { if !ok {
conntrack.Unlock() conntrack.Unlock()
return nil return false
} }
if c.rulesVersion != f.rulesVersion { if c.rulesVersion != f.rulesVersion {
@@ -744,7 +525,7 @@ func (f *Firewall) inConns(fp firewall.Packet, h *HostInfo, caPool *cert.CAPool,
} }
delete(conntrack.Conns, fp) delete(conntrack.Conns, fp)
conntrack.Unlock() conntrack.Unlock()
return nil return false
} }
if f.l.Level >= logrus.DebugLevel { if f.l.Level >= logrus.DebugLevel {
@@ -774,11 +555,12 @@ func (f *Firewall) inConns(fp firewall.Packet, h *HostInfo, caPool *cert.CAPool,
localCache[fp] = struct{}{} localCache[fp] = struct{}{}
} }
return c return true
} }
func (f *Firewall) packetTimeout(fp firewall.Packet) time.Duration { func (f *Firewall) addConn(fp firewall.Packet, incoming bool) {
var timeout time.Duration var timeout time.Duration
c := &conn{}
switch fp.Protocol { switch fp.Protocol {
case firewall.ProtoTCP: case firewall.ProtoTCP:
@@ -788,13 +570,7 @@ func (f *Firewall) packetTimeout(fp firewall.Packet) time.Duration {
default: default:
timeout = f.DefaultTimeout timeout = f.DefaultTimeout
} }
return timeout
}
func (f *Firewall) addConn(fp firewall.Packet, incoming bool) *conn {
c := &conn{}
timeout := f.packetTimeout(fp)
conntrack := f.Conntrack conntrack := f.Conntrack
conntrack.Lock() conntrack.Lock()
if _, ok := conntrack.Conns[fp]; !ok { if _, ok := conntrack.Conns[fp]; !ok {
@@ -808,9 +584,7 @@ func (f *Firewall) addConn(fp firewall.Packet, incoming bool) *conn {
c.rulesVersion = f.rulesVersion c.rulesVersion = f.rulesVersion
c.Expires = time.Now().Add(timeout) c.Expires = time.Now().Add(timeout)
conntrack.Conns[fp] = c conntrack.Conns[fp] = c
conntrack.Unlock() conntrack.Unlock()
return c
} }
// Evict checks if a conntrack entry has expired, if so it is removed, if not it is re-added to the wheel // Evict checks if a conntrack entry has expired, if so it is removed, if not it is re-added to the wheel
@@ -886,13 +660,6 @@ func (fp firewallPort) match(p firewall.Packet, incoming bool, c *cert.CachedCer
return false return false
} }
// this branch is here to catch traffic from FirewallTable.Any.match and FirewallTable.ICMP.match
if p.Protocol == firewall.ProtoICMP || p.Protocol == firewall.ProtoICMPv6 {
// port numbers are re-used for connection tracking of ICMP,
// but we don't want to actually filter on them.
return fp[firewall.PortAny].match(p, c, caPool)
}
var port int32 var port int32
if p.Fragment { if p.Fragment {
@@ -1037,8 +804,10 @@ func (fr *FirewallRule) isAny(groups []string, host string, cidr string) bool {
return true return true
} }
if slices.Contains(groups, "any") { for _, group := range groups {
return true if group == "any" {
return true
}
} }
if host == "any" { if host == "any" {
@@ -1249,56 +1018,54 @@ func (r *rule) sanity() error {
} }
} }
if r.Code != "" {
return fmt.Errorf("code specified as [%s]. Support for 'code' will be dropped in a future release, as it has never been functional", r.Code)
}
//todo alert on cidr-any //todo alert on cidr-any
return nil return nil
} }
func parsePort(s string) (int32, int32, error) { func parsePort(s string) (startPort, endPort int32, err error) {
var err error
const notAPort int32 = -2
if s == "any" { if s == "any" {
return firewall.PortAny, firewall.PortAny, nil startPort = firewall.PortAny
} endPort = firewall.PortAny
if s == "fragment" {
return firewall.PortFragment, firewall.PortFragment, nil } else if s == "fragment" {
} startPort = firewall.PortFragment
if !strings.Contains(s, `-`) { endPort = firewall.PortFragment
} else if strings.Contains(s, `-`) {
sPorts := strings.SplitN(s, `-`, 2)
sPorts[0] = strings.Trim(sPorts[0], " ")
sPorts[1] = strings.Trim(sPorts[1], " ")
if len(sPorts) != 2 || sPorts[0] == "" || sPorts[1] == "" {
return 0, 0, fmt.Errorf("appears to be a range but could not be parsed; `%s`", s)
}
rStartPort, err := strconv.Atoi(sPorts[0])
if err != nil {
return 0, 0, fmt.Errorf("beginning range was not a number; `%s`", sPorts[0])
}
rEndPort, err := strconv.Atoi(sPorts[1])
if err != nil {
return 0, 0, fmt.Errorf("ending range was not a number; `%s`", sPorts[1])
}
startPort = int32(rStartPort)
endPort = int32(rEndPort)
if startPort == firewall.PortAny {
endPort = firewall.PortAny
}
} else {
rPort, err := strconv.Atoi(s) rPort, err := strconv.Atoi(s)
if err != nil { if err != nil {
return notAPort, notAPort, fmt.Errorf("was not a number; `%s`", s) return 0, 0, fmt.Errorf("was not a number; `%s`", s)
} }
return int32(rPort), int32(rPort), nil startPort = int32(rPort)
endPort = startPort
} }
sPorts := strings.SplitN(s, `-`, 2) return
for i := range sPorts {
sPorts[i] = strings.Trim(sPorts[i], " ")
}
if len(sPorts) != 2 || sPorts[0] == "" || sPorts[1] == "" {
return notAPort, notAPort, fmt.Errorf("appears to be a range but could not be parsed; `%s`", s)
}
rStartPort, err := strconv.Atoi(sPorts[0])
if err != nil {
return notAPort, notAPort, fmt.Errorf("beginning range was not a number; `%s`", sPorts[0])
}
rEndPort, err := strconv.Atoi(sPorts[1])
if err != nil {
return notAPort, notAPort, fmt.Errorf("ending range was not a number; `%s`", sPorts[1])
}
startPort := int32(rStartPort)
endPort := int32(rEndPort)
if startPort == firewall.PortAny {
endPort = firewall.PortAny
}
return startPort, endPort, nil
} }
+187 -10
View File
@@ -1,9 +1,15 @@
package firewall package firewall
import ( import (
"encoding/binary"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"net/netip" "net/netip"
"github.com/google/gopacket/layers"
"golang.org/x/net/ipv4"
"golang.org/x/net/ipv6"
) )
type m = map[string]any type m = map[string]any
@@ -17,24 +23,28 @@ const (
PortAny = 0 // Special value for matching `port: any` PortAny = 0 // Special value for matching `port: any`
PortFragment = -1 // Special value for matching `port: fragment` PortFragment = -1 // Special value for matching `port: fragment`
minFwPacketLen = 4
)
var (
ErrPacketTooShort = errors.New("packet is too short")
ErrUnknownIPVersion = errors.New("packet is an unknown ip version")
ErrIPv4InvalidHeaderLength = errors.New("invalid ipv4 header length")
ErrIPv4PacketTooShort = errors.New("ipv4 packet is too short")
ErrIPv6PacketTooShort = errors.New("ipv6 packet is too short")
ErrIPv6CouldNotFindPayload = errors.New("could not find payload in ipv6 packet")
) )
type Packet struct { type Packet struct {
LocalAddr netip.Addr LocalAddr netip.Addr
RemoteAddr netip.Addr RemoteAddr netip.Addr
// LocalPort is the destination port for incoming traffic, or the source port for outgoing. Zero for ICMP. LocalPort uint16
LocalPort uint16
// RemotePort is the source port for incoming traffic, or the destination port for outgoing.
// For ICMP, it's the "identifier". This is only used for connection tracking, actual firewall rules will not filter on ICMP identifier
RemotePort uint16 RemotePort uint16
Protocol uint8 Protocol uint8
Fragment bool Fragment bool
} }
func (fp *Packet) IsIPv4() bool {
return fp.LocalAddr.Is4() && fp.RemoteAddr.Is4()
}
func (fp *Packet) Copy() *Packet { func (fp *Packet) Copy() *Packet {
return &Packet{ return &Packet{
LocalAddr: fp.LocalAddr, LocalAddr: fp.LocalAddr,
@@ -53,8 +63,6 @@ func (fp Packet) MarshalJSON() ([]byte, error) {
proto = "tcp" proto = "tcp"
case ProtoICMP: case ProtoICMP:
proto = "icmp" proto = "icmp"
case ProtoICMPv6:
proto = "icmpv6"
case ProtoUDP: case ProtoUDP:
proto = "udp" proto = "udp"
default: default:
@@ -69,3 +77,172 @@ func (fp Packet) MarshalJSON() ([]byte, error) {
"Fragment": fp.Fragment, "Fragment": fp.Fragment,
}) })
} }
func parseV6(data []byte, incoming bool, fp *Packet) error {
dataLen := len(data)
if dataLen < ipv6.HeaderLen {
return ErrIPv6PacketTooShort
}
if incoming {
fp.RemoteAddr, _ = netip.AddrFromSlice(data[8:24])
fp.LocalAddr, _ = netip.AddrFromSlice(data[24:40])
} else {
fp.LocalAddr, _ = netip.AddrFromSlice(data[8:24])
fp.RemoteAddr, _ = netip.AddrFromSlice(data[24:40])
}
protoAt := 6 // NextHeader is at 6 bytes into the ipv6 header
offset := ipv6.HeaderLen // Start at the end of the ipv6 header
next := 0
for {
if protoAt >= dataLen {
break
}
proto := layers.IPProtocol(data[protoAt])
switch proto {
case layers.IPProtocolICMPv6, layers.IPProtocolESP, layers.IPProtocolNoNextHeader:
fp.Protocol = uint8(proto)
fp.RemotePort = 0
fp.LocalPort = 0
fp.Fragment = false
return nil
case layers.IPProtocolTCP, layers.IPProtocolUDP:
if dataLen < offset+4 {
return ErrIPv6PacketTooShort
}
fp.Protocol = uint8(proto)
if incoming {
fp.RemotePort = binary.BigEndian.Uint16(data[offset : offset+2])
fp.LocalPort = binary.BigEndian.Uint16(data[offset+2 : offset+4])
} else {
fp.LocalPort = binary.BigEndian.Uint16(data[offset : offset+2])
fp.RemotePort = binary.BigEndian.Uint16(data[offset+2 : offset+4])
}
fp.Fragment = false
return nil
case layers.IPProtocolIPv6Fragment:
// Fragment header is 8 bytes, need at least offset+4 to read the offset field
if dataLen < offset+8 {
return ErrIPv6PacketTooShort
}
// Check if this is the first fragment
fragmentOffset := binary.BigEndian.Uint16(data[offset+2:offset+4]) &^ uint16(0x7) // Remove the reserved and M flag bits
if fragmentOffset != 0 {
// Non-first fragment, use what we have now and stop processing
fp.Protocol = data[offset]
fp.Fragment = true
fp.RemotePort = 0
fp.LocalPort = 0
return nil
}
// The next loop should be the transport layer since we are the first fragment
next = 8 // Fragment headers are always 8 bytes
case layers.IPProtocolAH:
// Auth headers, used by IPSec, have a different meaning for header length
if dataLen <= offset+1 {
break
}
next = int(data[offset+1]+2) << 2
default:
// Normal ipv6 header length processing
if dataLen <= offset+1 {
break
}
next = int(data[offset+1]+1) << 3
}
if next <= 0 {
// Safety check, each ipv6 header has to be at least 8 bytes
next = 8
}
protoAt = offset
offset = offset + next
}
return ErrIPv6CouldNotFindPayload
}
func parseV4(data []byte, incoming bool, fp *Packet) error {
// Do we at least have an ipv4 header worth of data?
if len(data) < ipv4.HeaderLen {
return ErrIPv4PacketTooShort
}
// Adjust our start position based on the advertised ip header length
ihl := int(data[0]&0x0f) << 2
// Well-formed ip header length?
if ihl < ipv4.HeaderLen {
return ErrIPv4InvalidHeaderLength
}
// Check if this is the second or further fragment of a fragmented packet.
flagsfrags := binary.BigEndian.Uint16(data[6:8])
fp.Fragment = (flagsfrags & 0x1FFF) != 0
// Firewall handles protocol checks
fp.Protocol = data[9]
// Accounting for a variable header length, do we have enough data for our src/dst tuples?
minLen := ihl
if !fp.Fragment && fp.Protocol != ProtoICMP {
minLen += minFwPacketLen
}
if len(data) < minLen {
return ErrIPv4InvalidHeaderLength
}
// Firewall packets are locally oriented
if incoming {
fp.RemoteAddr, _ = netip.AddrFromSlice(data[12:16])
fp.LocalAddr, _ = netip.AddrFromSlice(data[16:20])
if fp.Fragment || fp.Protocol == ProtoICMP {
fp.RemotePort = 0
fp.LocalPort = 0
} else {
fp.RemotePort = binary.BigEndian.Uint16(data[ihl : ihl+2])
fp.LocalPort = binary.BigEndian.Uint16(data[ihl+2 : ihl+4])
}
} else {
fp.LocalAddr, _ = netip.AddrFromSlice(data[12:16])
fp.RemoteAddr, _ = netip.AddrFromSlice(data[16:20])
if fp.Fragment || fp.Protocol == ProtoICMP {
fp.RemotePort = 0
fp.LocalPort = 0
} else {
fp.LocalPort = binary.BigEndian.Uint16(data[ihl : ihl+2])
fp.RemotePort = binary.BigEndian.Uint16(data[ihl+2 : ihl+4])
}
}
return nil
}
// NewPacket validates and parses the interesting bits for the firewall out of the ip and sub protocol headers
func NewPacket(data []byte, incoming bool, fp *Packet) error {
if len(data) < 1 {
return ErrPacketTooShort
}
version := int((data[0] >> 4) & 0x0f)
switch version {
case ipv4.Version:
return parseV4(data, incoming, fp)
case ipv6.Version:
return parseV6(data, incoming, fp)
}
return ErrUnknownIPVersion
}
+40 -266
View File
@@ -87,10 +87,9 @@ func TestFirewall_AddRule(t *testing.T) {
fw = NewFirewall(l, time.Second, time.Minute, time.Hour, c) fw = NewFirewall(l, time.Second, time.Minute, time.Hour, c)
require.NoError(t, fw.AddRule(true, firewall.ProtoICMP, 1, 1, []string{}, "h1", "", "", "", "")) require.NoError(t, fw.AddRule(true, firewall.ProtoICMP, 1, 1, []string{}, "h1", "", "", "", ""))
//no matter what port is given for icmp, it should end up as "any" assert.Nil(t, fw.InRules.ICMP[1].Any.Any)
assert.Nil(t, fw.InRules.ICMP[firewall.PortAny].Any.Any) assert.Empty(t, fw.InRules.ICMP[1].Any.Groups)
assert.Empty(t, fw.InRules.ICMP[firewall.PortAny].Any.Groups) assert.Contains(t, fw.InRules.ICMP[1].Any.Hosts, "h1")
assert.Contains(t, fw.InRules.ICMP[firewall.PortAny].Any.Hosts, "h1")
fw = NewFirewall(l, time.Second, time.Minute, time.Hour, c) fw = NewFirewall(l, time.Second, time.Minute, time.Hour, c)
require.NoError(t, fw.AddRule(false, firewall.ProtoAny, 1, 1, []string{}, "", ti.String(), "", "", "")) require.NoError(t, fw.AddRule(false, firewall.ProtoAny, 1, 1, []string{}, "", ti.String(), "", "", ""))
@@ -213,44 +212,44 @@ func TestFirewall_Drop(t *testing.T) {
cp := cert.NewCAPool() cp := cert.NewCAPool()
// Drop outbound // Drop outbound
assert.Equal(t, ErrNoMatchingRule, fw.Drop(p, nil, false, &h, cp, nil)) assert.Equal(t, ErrNoMatchingRule, fw.Drop(p, false, &h, cp, nil))
// Allow inbound // Allow inbound
resetConntrack(fw) resetConntrack(fw)
require.NoError(t, fw.Drop(p, nil, true, &h, cp, nil)) require.NoError(t, fw.Drop(p, true, &h, cp, nil))
// Allow outbound because conntrack // Allow outbound because conntrack
require.NoError(t, fw.Drop(p, nil, false, &h, cp, nil)) require.NoError(t, fw.Drop(p, false, &h, cp, nil))
// test remote mismatch // test remote mismatch
oldRemote := p.RemoteAddr oldRemote := p.RemoteAddr
p.RemoteAddr = netip.MustParseAddr("1.2.3.10") p.RemoteAddr = netip.MustParseAddr("1.2.3.10")
assert.Equal(t, fw.Drop(p, nil, false, &h, cp, nil), ErrInvalidRemoteIP) assert.Equal(t, fw.Drop(p, false, &h, cp, nil), ErrInvalidRemoteIP)
p.RemoteAddr = oldRemote p.RemoteAddr = oldRemote
// ensure signer doesn't get in the way of group checks // ensure signer doesn't get in the way of group checks
fw = NewFirewall(l, time.Second, time.Minute, time.Hour, &c) fw = NewFirewall(l, time.Second, time.Minute, time.Hour, &c)
require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"nope"}, "", "", "", "", "signer-shasum")) require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"nope"}, "", "", "", "", "signer-shasum"))
require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"default-group"}, "", "", "", "", "signer-shasum-bad")) require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"default-group"}, "", "", "", "", "signer-shasum-bad"))
assert.Equal(t, fw.Drop(p, nil, true, &h, cp, nil), ErrNoMatchingRule) assert.Equal(t, fw.Drop(p, true, &h, cp, nil), ErrNoMatchingRule)
// test caSha doesn't drop on match // test caSha doesn't drop on match
fw = NewFirewall(l, time.Second, time.Minute, time.Hour, &c) fw = NewFirewall(l, time.Second, time.Minute, time.Hour, &c)
require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"nope"}, "", "", "", "", "signer-shasum-bad")) require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"nope"}, "", "", "", "", "signer-shasum-bad"))
require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"default-group"}, "", "", "", "", "signer-shasum")) require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"default-group"}, "", "", "", "", "signer-shasum"))
require.NoError(t, fw.Drop(p, nil, true, &h, cp, nil)) require.NoError(t, fw.Drop(p, true, &h, cp, nil))
// ensure ca name doesn't get in the way of group checks // ensure ca name doesn't get in the way of group checks
cp.CAs["signer-shasum"] = &cert.CachedCertificate{Certificate: &dummyCert{name: "ca-good"}} cp.CAs["signer-shasum"] = &cert.CachedCertificate{Certificate: &dummyCert{name: "ca-good"}}
fw = NewFirewall(l, time.Second, time.Minute, time.Hour, &c) fw = NewFirewall(l, time.Second, time.Minute, time.Hour, &c)
require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"nope"}, "", "", "", "ca-good", "")) require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"nope"}, "", "", "", "ca-good", ""))
require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"default-group"}, "", "", "", "ca-good-bad", "")) require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"default-group"}, "", "", "", "ca-good-bad", ""))
assert.Equal(t, fw.Drop(p, nil, true, &h, cp, nil), ErrNoMatchingRule) assert.Equal(t, fw.Drop(p, true, &h, cp, nil), ErrNoMatchingRule)
// test caName doesn't drop on match // test caName doesn't drop on match
cp.CAs["signer-shasum"] = &cert.CachedCertificate{Certificate: &dummyCert{name: "ca-good"}} cp.CAs["signer-shasum"] = &cert.CachedCertificate{Certificate: &dummyCert{name: "ca-good"}}
fw = NewFirewall(l, time.Second, time.Minute, time.Hour, &c) fw = NewFirewall(l, time.Second, time.Minute, time.Hour, &c)
require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"nope"}, "", "", "", "ca-good-bad", "")) require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"nope"}, "", "", "", "ca-good-bad", ""))
require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"default-group"}, "", "", "", "ca-good", "")) require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"default-group"}, "", "", "", "ca-good", ""))
require.NoError(t, fw.Drop(p, nil, true, &h, cp, nil)) require.NoError(t, fw.Drop(p, true, &h, cp, nil))
} }
func TestFirewall_DropV6(t *testing.T) { func TestFirewall_DropV6(t *testing.T) {
@@ -292,44 +291,44 @@ func TestFirewall_DropV6(t *testing.T) {
cp := cert.NewCAPool() cp := cert.NewCAPool()
// Drop outbound // Drop outbound
assert.Equal(t, ErrNoMatchingRule, fw.Drop(p, nil, false, &h, cp, nil)) assert.Equal(t, ErrNoMatchingRule, fw.Drop(p, false, &h, cp, nil))
// Allow inbound // Allow inbound
resetConntrack(fw) resetConntrack(fw)
require.NoError(t, fw.Drop(p, nil, true, &h, cp, nil)) require.NoError(t, fw.Drop(p, true, &h, cp, nil))
// Allow outbound because conntrack // Allow outbound because conntrack
require.NoError(t, fw.Drop(p, nil, false, &h, cp, nil)) require.NoError(t, fw.Drop(p, false, &h, cp, nil))
// test remote mismatch // test remote mismatch
oldRemote := p.RemoteAddr oldRemote := p.RemoteAddr
p.RemoteAddr = netip.MustParseAddr("fd12::56") p.RemoteAddr = netip.MustParseAddr("fd12::56")
assert.Equal(t, fw.Drop(p, nil, false, &h, cp, nil), ErrInvalidRemoteIP) assert.Equal(t, fw.Drop(p, false, &h, cp, nil), ErrInvalidRemoteIP)
p.RemoteAddr = oldRemote p.RemoteAddr = oldRemote
// ensure signer doesn't get in the way of group checks // ensure signer doesn't get in the way of group checks
fw = NewFirewall(l, time.Second, time.Minute, time.Hour, &c) fw = NewFirewall(l, time.Second, time.Minute, time.Hour, &c)
require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"nope"}, "", "", "", "", "signer-shasum")) require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"nope"}, "", "", "", "", "signer-shasum"))
require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"default-group"}, "", "", "", "", "signer-shasum-bad")) require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"default-group"}, "", "", "", "", "signer-shasum-bad"))
assert.Equal(t, fw.Drop(p, nil, true, &h, cp, nil), ErrNoMatchingRule) assert.Equal(t, fw.Drop(p, true, &h, cp, nil), ErrNoMatchingRule)
// test caSha doesn't drop on match // test caSha doesn't drop on match
fw = NewFirewall(l, time.Second, time.Minute, time.Hour, &c) fw = NewFirewall(l, time.Second, time.Minute, time.Hour, &c)
require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"nope"}, "", "", "", "", "signer-shasum-bad")) require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"nope"}, "", "", "", "", "signer-shasum-bad"))
require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"default-group"}, "", "", "", "", "signer-shasum")) require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"default-group"}, "", "", "", "", "signer-shasum"))
require.NoError(t, fw.Drop(p, nil, true, &h, cp, nil)) require.NoError(t, fw.Drop(p, true, &h, cp, nil))
// ensure ca name doesn't get in the way of group checks // ensure ca name doesn't get in the way of group checks
cp.CAs["signer-shasum"] = &cert.CachedCertificate{Certificate: &dummyCert{name: "ca-good"}} cp.CAs["signer-shasum"] = &cert.CachedCertificate{Certificate: &dummyCert{name: "ca-good"}}
fw = NewFirewall(l, time.Second, time.Minute, time.Hour, &c) fw = NewFirewall(l, time.Second, time.Minute, time.Hour, &c)
require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"nope"}, "", "", "", "ca-good", "")) require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"nope"}, "", "", "", "ca-good", ""))
require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"default-group"}, "", "", "", "ca-good-bad", "")) require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"default-group"}, "", "", "", "ca-good-bad", ""))
assert.Equal(t, fw.Drop(p, nil, true, &h, cp, nil), ErrNoMatchingRule) assert.Equal(t, fw.Drop(p, true, &h, cp, nil), ErrNoMatchingRule)
// test caName doesn't drop on match // test caName doesn't drop on match
cp.CAs["signer-shasum"] = &cert.CachedCertificate{Certificate: &dummyCert{name: "ca-good"}} cp.CAs["signer-shasum"] = &cert.CachedCertificate{Certificate: &dummyCert{name: "ca-good"}}
fw = NewFirewall(l, time.Second, time.Minute, time.Hour, &c) fw = NewFirewall(l, time.Second, time.Minute, time.Hour, &c)
require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"nope"}, "", "", "", "ca-good-bad", "")) require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"nope"}, "", "", "", "ca-good-bad", ""))
require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"default-group"}, "", "", "", "ca-good", "")) require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"default-group"}, "", "", "", "ca-good", ""))
require.NoError(t, fw.Drop(p, nil, true, &h, cp, nil)) require.NoError(t, fw.Drop(p, true, &h, cp, nil))
} }
func BenchmarkFirewallTable_match(b *testing.B) { func BenchmarkFirewallTable_match(b *testing.B) {
@@ -537,10 +536,10 @@ func TestFirewall_Drop2(t *testing.T) {
cp := cert.NewCAPool() cp := cert.NewCAPool()
// h1/c1 lacks the proper groups // h1/c1 lacks the proper groups
require.ErrorIs(t, fw.Drop(p, nil, true, &h1, cp, nil), ErrNoMatchingRule) require.ErrorIs(t, fw.Drop(p, true, &h1, cp, nil), ErrNoMatchingRule)
// c has the proper groups // c has the proper groups
resetConntrack(fw) resetConntrack(fw)
require.NoError(t, fw.Drop(p, nil, true, &h, cp, nil)) require.NoError(t, fw.Drop(p, true, &h, cp, nil))
} }
func TestFirewall_Drop3(t *testing.T) { func TestFirewall_Drop3(t *testing.T) {
@@ -618,18 +617,18 @@ func TestFirewall_Drop3(t *testing.T) {
cp := cert.NewCAPool() cp := cert.NewCAPool()
// c1 should pass because host match // c1 should pass because host match
require.NoError(t, fw.Drop(p, nil, true, &h1, cp, nil)) require.NoError(t, fw.Drop(p, true, &h1, cp, nil))
// c2 should pass because ca sha match // c2 should pass because ca sha match
resetConntrack(fw) resetConntrack(fw)
require.NoError(t, fw.Drop(p, nil, true, &h2, cp, nil)) require.NoError(t, fw.Drop(p, true, &h2, cp, nil))
// c3 should fail because no match // c3 should fail because no match
resetConntrack(fw) resetConntrack(fw)
assert.Equal(t, fw.Drop(p, nil, true, &h3, cp, nil), ErrNoMatchingRule) assert.Equal(t, fw.Drop(p, true, &h3, cp, nil), ErrNoMatchingRule)
// Test a remote address match // Test a remote address match
fw = NewFirewall(l, time.Second, time.Minute, time.Hour, c.Certificate) fw = NewFirewall(l, time.Second, time.Minute, time.Hour, c.Certificate)
require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 1, 1, []string{}, "", "1.2.3.4/24", "", "", "")) require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 1, 1, []string{}, "", "1.2.3.4/24", "", "", ""))
require.NoError(t, fw.Drop(p, nil, true, &h1, cp, nil)) require.NoError(t, fw.Drop(p, true, &h1, cp, nil))
} }
func TestFirewall_Drop3V6(t *testing.T) { func TestFirewall_Drop3V6(t *testing.T) {
@@ -667,7 +666,7 @@ func TestFirewall_Drop3V6(t *testing.T) {
fw := NewFirewall(l, time.Second, time.Minute, time.Hour, c.Certificate) fw := NewFirewall(l, time.Second, time.Minute, time.Hour, c.Certificate)
cp := cert.NewCAPool() cp := cert.NewCAPool()
require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 1, 1, []string{}, "", "fd12::34/120", "", "", "")) require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 1, 1, []string{}, "", "fd12::34/120", "", "", ""))
require.NoError(t, fw.Drop(p, nil, true, &h, cp, nil)) require.NoError(t, fw.Drop(p, true, &h, cp, nil))
} }
func TestFirewall_DropConntrackReload(t *testing.T) { func TestFirewall_DropConntrackReload(t *testing.T) {
@@ -709,12 +708,12 @@ func TestFirewall_DropConntrackReload(t *testing.T) {
cp := cert.NewCAPool() cp := cert.NewCAPool()
// Drop outbound // Drop outbound
assert.Equal(t, fw.Drop(p, nil, false, &h, cp, nil), ErrNoMatchingRule) assert.Equal(t, fw.Drop(p, false, &h, cp, nil), ErrNoMatchingRule)
// Allow inbound // Allow inbound
resetConntrack(fw) resetConntrack(fw)
require.NoError(t, fw.Drop(p, nil, true, &h, cp, nil)) require.NoError(t, fw.Drop(p, true, &h, cp, nil))
// Allow outbound because conntrack // Allow outbound because conntrack
require.NoError(t, fw.Drop(p, nil, false, &h, cp, nil)) require.NoError(t, fw.Drop(p, false, &h, cp, nil))
oldFw := fw oldFw := fw
fw = NewFirewall(l, time.Second, time.Minute, time.Hour, c.Certificate) fw = NewFirewall(l, time.Second, time.Minute, time.Hour, c.Certificate)
@@ -723,7 +722,7 @@ func TestFirewall_DropConntrackReload(t *testing.T) {
fw.rulesVersion = oldFw.rulesVersion + 1 fw.rulesVersion = oldFw.rulesVersion + 1
// Allow outbound because conntrack and new rules allow port 10 // Allow outbound because conntrack and new rules allow port 10
require.NoError(t, fw.Drop(p, nil, false, &h, cp, nil)) require.NoError(t, fw.Drop(p, false, &h, cp, nil))
oldFw = fw oldFw = fw
fw = NewFirewall(l, time.Second, time.Minute, time.Hour, c.Certificate) fw = NewFirewall(l, time.Second, time.Minute, time.Hour, c.Certificate)
@@ -732,151 +731,7 @@ func TestFirewall_DropConntrackReload(t *testing.T) {
fw.rulesVersion = oldFw.rulesVersion + 1 fw.rulesVersion = oldFw.rulesVersion + 1
// Drop outbound because conntrack doesn't match new ruleset // Drop outbound because conntrack doesn't match new ruleset
assert.Equal(t, fw.Drop(p, nil, false, &h, cp, nil), ErrNoMatchingRule) assert.Equal(t, fw.Drop(p, false, &h, cp, nil), ErrNoMatchingRule)
}
func TestFirewall_ICMPPortBehavior(t *testing.T) {
l := test.NewLogger()
ob := &bytes.Buffer{}
l.SetOutput(ob)
myVpnNetworksTable := new(bart.Lite)
myVpnNetworksTable.Insert(netip.MustParsePrefix("1.1.1.1/8"))
network := netip.MustParsePrefix("1.2.3.4/24")
c := cert.CachedCertificate{
Certificate: &dummyCert{
name: "host1",
networks: []netip.Prefix{network},
groups: []string{"default-group"},
issuer: "signer-shasum",
},
InvertedGroups: map[string]struct{}{"default-group": {}},
}
h := HostInfo{
ConnectionState: &ConnectionState{
peerCert: &c,
},
vpnAddrs: []netip.Addr{network.Addr()},
}
h.buildNetworks(myVpnNetworksTable, c.Certificate)
cp := cert.NewCAPool()
templ := firewall.Packet{
LocalAddr: netip.MustParseAddr("1.2.3.4"),
RemoteAddr: netip.MustParseAddr("1.2.3.4"),
Protocol: firewall.ProtoICMP,
Fragment: false,
}
t.Run("ICMP allowed", func(t *testing.T) {
fw := NewFirewall(l, time.Second, time.Minute, time.Hour, c.Certificate)
require.NoError(t, fw.AddRule(true, firewall.ProtoICMP, 0, 0, []string{"any"}, "", "", "", "", ""))
t.Run("zero ports", func(t *testing.T) {
p := templ.Copy()
p.LocalPort = 0
p.RemotePort = 0
// Drop outbound
assert.Equal(t, fw.Drop(*p, nil, false, &h, cp, nil), ErrNoMatchingRule)
// Allow inbound
resetConntrack(fw)
require.NoError(t, fw.Drop(*p, nil, true, &h, cp, nil))
//now also allow outbound
require.NoError(t, fw.Drop(*p, nil, false, &h, cp, nil))
})
t.Run("nonzero ports", func(t *testing.T) {
p := templ.Copy()
p.LocalPort = 0xabcd
p.RemotePort = 0x1234
// Drop outbound
assert.Equal(t, fw.Drop(*p, nil, false, &h, cp, nil), ErrNoMatchingRule)
// Allow inbound
resetConntrack(fw)
require.NoError(t, fw.Drop(*p, nil, true, &h, cp, nil))
//now also allow outbound
require.NoError(t, fw.Drop(*p, nil, false, &h, cp, nil))
})
})
t.Run("Any proto, some ports allowed", func(t *testing.T) {
fw := NewFirewall(l, time.Second, time.Minute, time.Hour, c.Certificate)
require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 80, 444, []string{"any"}, "", "", "", "", ""))
t.Run("zero ports, still blocked", func(t *testing.T) {
p := templ.Copy()
p.LocalPort = 0
p.RemotePort = 0
// Drop outbound
assert.Equal(t, fw.Drop(*p, nil, false, &h, cp, nil), ErrNoMatchingRule)
// Allow inbound
resetConntrack(fw)
assert.Equal(t, fw.Drop(*p, nil, true, &h, cp, nil), ErrNoMatchingRule)
//now also allow outbound
assert.Equal(t, fw.Drop(*p, nil, false, &h, cp, nil), ErrNoMatchingRule)
})
t.Run("nonzero ports, still blocked", func(t *testing.T) {
p := templ.Copy()
p.LocalPort = 0xabcd
p.RemotePort = 0x1234
// Drop outbound
assert.Equal(t, fw.Drop(*p, nil, false, &h, cp, nil), ErrNoMatchingRule)
// Allow inbound
resetConntrack(fw)
assert.Equal(t, fw.Drop(*p, nil, true, &h, cp, nil), ErrNoMatchingRule)
//now also allow outbound
assert.Equal(t, fw.Drop(*p, nil, false, &h, cp, nil), ErrNoMatchingRule)
})
t.Run("nonzero, matching ports, still blocked", func(t *testing.T) {
p := templ.Copy()
p.LocalPort = 80
p.RemotePort = 80
// Drop outbound
assert.Equal(t, fw.Drop(*p, nil, false, &h, cp, nil), ErrNoMatchingRule)
// Allow inbound
resetConntrack(fw)
assert.Equal(t, fw.Drop(*p, nil, true, &h, cp, nil), ErrNoMatchingRule)
//now also allow outbound
assert.Equal(t, fw.Drop(*p, nil, false, &h, cp, nil), ErrNoMatchingRule)
})
})
t.Run("Any proto, any port", func(t *testing.T) {
fw := NewFirewall(l, time.Second, time.Minute, time.Hour, c.Certificate)
require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"any"}, "", "", "", "", ""))
t.Run("zero ports, allowed", func(t *testing.T) {
resetConntrack(fw)
p := templ.Copy()
p.LocalPort = 0
p.RemotePort = 0
// Drop outbound
assert.Equal(t, fw.Drop(*p, nil, false, &h, cp, nil), ErrNoMatchingRule)
// Allow inbound
resetConntrack(fw)
require.NoError(t, fw.Drop(*p, nil, true, &h, cp, nil))
//now also allow outbound
require.NoError(t, fw.Drop(*p, nil, false, &h, cp, nil))
})
t.Run("nonzero ports, allowed", func(t *testing.T) {
resetConntrack(fw)
p := templ.Copy()
p.LocalPort = 0xabcd
p.RemotePort = 0x1234
// Drop outbound
assert.Equal(t, fw.Drop(*p, nil, false, &h, cp, nil), ErrNoMatchingRule)
// Allow inbound
resetConntrack(fw)
require.NoError(t, fw.Drop(*p, nil, true, &h, cp, nil))
//now also allow outbound
require.NoError(t, fw.Drop(*p, nil, false, &h, cp, nil))
//different ID is blocked
p.RemotePort++
require.Equal(t, fw.Drop(*p, nil, false, &h, cp, nil), ErrNoMatchingRule)
})
})
} }
func TestFirewall_DropIPSpoofing(t *testing.T) { func TestFirewall_DropIPSpoofing(t *testing.T) {
@@ -922,7 +777,7 @@ func TestFirewall_DropIPSpoofing(t *testing.T) {
Protocol: firewall.ProtoUDP, Protocol: firewall.ProtoUDP,
Fragment: false, Fragment: false,
} }
assert.Equal(t, fw.Drop(p, nil, true, &h1, cp, nil), ErrInvalidRemoteIP) assert.Equal(t, fw.Drop(p, true, &h1, cp, nil), ErrInvalidRemoteIP)
} }
func BenchmarkLookup(b *testing.B) { func BenchmarkLookup(b *testing.B) {
@@ -1042,7 +897,7 @@ func TestNewFirewallFromConfig(t *testing.T) {
l := test.NewLogger() l := test.NewLogger()
// Test a bad rule definition // Test a bad rule definition
c := &dummyCert{} c := &dummyCert{}
cs, err := newCertState(l, cert.Version2, nil, c, false, cert.Curve_CURVE25519, nil) cs, err := newCertState(cert.Version2, nil, c, false, cert.Curve_CURVE25519, nil)
require.NoError(t, err) require.NoError(t, err)
conf := config.NewC(l) conf := config.NewC(l)
@@ -1064,11 +919,11 @@ func TestNewFirewallFromConfig(t *testing.T) {
// Test code/port error // Test code/port error
conf = config.NewC(l) conf = config.NewC(l)
conf.Settings["firewall"] = map[string]any{"outbound": []any{map[string]any{"code": "a", "host": "testh", "proto": "any"}}} conf.Settings["firewall"] = map[string]any{"outbound": []any{map[string]any{"code": "a", "host": "testh"}}}
_, err = NewFirewallFromConfig(l, cs, conf) _, err = NewFirewallFromConfig(l, cs, conf)
require.EqualError(t, err, "firewall.outbound rule #0; code was not a number; `a`") require.EqualError(t, err, "firewall.outbound rule #0; code was not a number; `a`")
conf.Settings["firewall"] = map[string]any{"outbound": []any{map[string]any{"port": "a", "host": "testh", "proto": "any"}}} conf.Settings["firewall"] = map[string]any{"outbound": []any{map[string]any{"port": "a", "host": "testh"}}}
_, err = NewFirewallFromConfig(l, cs, conf) _, err = NewFirewallFromConfig(l, cs, conf)
require.EqualError(t, err, "firewall.outbound rule #0; port was not a number; `a`") require.EqualError(t, err, "firewall.outbound rule #0; port was not a number; `a`")
@@ -1118,14 +973,7 @@ func TestAddFirewallRulesFromConfig(t *testing.T) {
mf = &mockFirewall{} mf = &mockFirewall{}
conf.Settings["firewall"] = map[string]any{"outbound": []any{map[string]any{"port": "1", "proto": "icmp", "host": "a"}}} conf.Settings["firewall"] = map[string]any{"outbound": []any{map[string]any{"port": "1", "proto": "icmp", "host": "a"}}}
require.NoError(t, AddFirewallRulesFromConfig(l, false, conf, mf)) require.NoError(t, AddFirewallRulesFromConfig(l, false, conf, mf))
assert.Equal(t, addRuleCall{incoming: false, proto: firewall.ProtoICMP, startPort: firewall.PortAny, endPort: firewall.PortAny, groups: nil, host: "a", ip: "", localIp: ""}, mf.lastCall) assert.Equal(t, addRuleCall{incoming: false, proto: firewall.ProtoICMP, startPort: 1, endPort: 1, groups: nil, host: "a", ip: "", localIp: ""}, mf.lastCall)
// Test adding icmp rule no port
conf = config.NewC(l)
mf = &mockFirewall{}
conf.Settings["firewall"] = map[string]any{"outbound": []any{map[string]any{"proto": "icmp", "host": "a"}}}
require.NoError(t, AddFirewallRulesFromConfig(l, false, conf, mf))
assert.Equal(t, addRuleCall{incoming: false, proto: firewall.ProtoICMP, startPort: firewall.PortAny, endPort: firewall.PortAny, groups: nil, host: "a", ip: "", localIp: ""}, mf.lastCall)
// Test adding any rule // Test adding any rule
conf = config.NewC(l) conf = config.NewC(l)
@@ -1336,7 +1184,7 @@ func (c *testcase) Test(t *testing.T, fw *Firewall) {
t.Helper() t.Helper()
cp := cert.NewCAPool() cp := cert.NewCAPool()
resetConntrack(fw) resetConntrack(fw)
err := fw.Drop(c.p, nil, true, c.h, cp, nil) err := fw.Drop(c.p, true, c.h, cp, nil)
if c.err == nil { if c.err == nil {
require.NoError(t, err, "failed to not drop remote address %s", c.p.RemoteAddr) require.NoError(t, err, "failed to not drop remote address %s", c.p.RemoteAddr)
} else { } else {
@@ -1344,7 +1192,7 @@ func (c *testcase) Test(t *testing.T, fw *Firewall) {
} }
} }
func buildHostinfo(setup testsetup, theirPrefixes ...netip.Prefix) *HostInfo { func buildTestCase(setup testsetup, err error, theirPrefixes ...netip.Prefix) testcase {
c1 := dummyCert{ c1 := dummyCert{
name: "host1", name: "host1",
networks: theirPrefixes, networks: theirPrefixes,
@@ -1364,11 +1212,6 @@ func buildHostinfo(setup testsetup, theirPrefixes ...netip.Prefix) *HostInfo {
h.vpnAddrs[i] = theirPrefixes[i].Addr() h.vpnAddrs[i] = theirPrefixes[i].Addr()
} }
h.buildNetworks(setup.myVpnNetworksTable, &c1) h.buildNetworks(setup.myVpnNetworksTable, &c1)
return &h
}
func buildTestCase(setup testsetup, err error, theirPrefixes ...netip.Prefix) testcase {
h := buildHostinfo(setup, theirPrefixes...)
p := firewall.Packet{ p := firewall.Packet{
LocalAddr: setup.c.Networks()[0].Addr(), //todo? LocalAddr: setup.c.Networks()[0].Addr(), //todo?
RemoteAddr: theirPrefixes[0].Addr(), RemoteAddr: theirPrefixes[0].Addr(),
@@ -1378,9 +1221,9 @@ func buildTestCase(setup testsetup, err error, theirPrefixes ...netip.Prefix) te
Fragment: false, Fragment: false,
} }
return testcase{ return testcase{
h: h, h: &h,
p: p, p: p,
c: h.ConnectionState.peerCert.Certificate, c: &c1,
err: err, err: err,
} }
} }
@@ -1402,19 +1245,6 @@ func newSetup(t *testing.T, l *logrus.Logger, myPrefixes ...netip.Prefix) testse
return newSetupFromCert(t, l, c) return newSetupFromCert(t, l, c)
} }
func newSnatSetup(t *testing.T, l *logrus.Logger, myPrefix netip.Prefix, snatAddr netip.Addr) testsetup {
c := dummyCert{
name: "me",
networks: []netip.Prefix{myPrefix},
groups: []string{"default-group"},
issuer: "signer-shasum",
}
out := newSetupFromCert(t, l, c)
out.fw.snatAddr = snatAddr
return out
}
func newSetupFromCert(t *testing.T, l *logrus.Logger, c dummyCert) testsetup { func newSetupFromCert(t *testing.T, l *logrus.Logger, c dummyCert) testsetup {
myVpnNetworksTable := new(bart.Lite) myVpnNetworksTable := new(bart.Lite)
for _, prefix := range c.Networks() { for _, prefix := range c.Networks() {
@@ -1550,59 +1380,3 @@ func resetConntrack(fw *Firewall) {
fw.Conntrack.Conns = map[firewall.Packet]*conn{} fw.Conntrack.Conns = map[firewall.Packet]*conn{}
fw.Conntrack.Unlock() fw.Conntrack.Unlock()
} }
func TestFirewall_SNAT(t *testing.T) {
t.Parallel()
l := test.NewLogger()
ob := &bytes.Buffer{}
l.SetOutput(ob)
cp := cert.NewCAPool()
myPrefix := netip.MustParsePrefix("1.1.1.1/8")
MyCert := dummyCert{
name: "me",
networks: []netip.Prefix{myPrefix},
groups: []string{"default-group"},
issuer: "signer-shasum",
}
theirPrefix := netip.MustParsePrefix("1.2.2.2/8")
snatAddr := netip.MustParseAddr("169.254.55.96")
t.Run("allow inbound all matching", func(t *testing.T) {
t.Parallel()
myCert := MyCert.Copy()
setup := newSnatSetup(t, l, myPrefix, snatAddr)
fw := NewFirewall(l, time.Second, time.Minute, time.Hour, myCert)
require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"any"}, "", "", "", "", ""))
resetConntrack(setup.fw)
h := buildHostinfo(setup, theirPrefix)
p := firewall.Packet{
LocalAddr: setup.c.Networks()[0].Addr(), //todo?
RemoteAddr: h.vpnAddrs[0],
LocalPort: 10,
RemotePort: 90,
Protocol: firewall.ProtoUDP,
Fragment: false,
}
require.NoError(t, setup.fw.Drop(p, nil, true, h, cp, nil))
})
//t.Run("allow inbound unsafe route", func(t *testing.T) {
// t.Parallel()
// unsafePrefix := netip.MustParsePrefix("192.168.0.0/24")
// c := dummyCert{
// name: "me",
// networks: []netip.Prefix{myPrefix},
// unsafeNetworks: []netip.Prefix{unsafePrefix},
// groups: []string{"default-group"},
// issuer: "signer-shasum",
// }
// unsafeSetup := newSetupFromCert(t, l, c)
// tc := buildTestCase(unsafeSetup, nil, twoPrefixes...)
// tc.p.LocalAddr = netip.MustParseAddr("192.168.0.3")
// tc.err = ErrNoMatchingRule
// tc.Test(t, unsafeSetup.fw) //should hit firewall and bounce off
// require.NoError(t, unsafeSetup.fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"any"}, "", "", unsafePrefix.String(), "", ""))
// tc.err = nil
// tc.Test(t, unsafeSetup.fw) //should pass
//})
}
+1 -1
View File
@@ -590,7 +590,7 @@ func (hm *HandshakeManager) allocateIndex(hh *HandshakeHostInfo) error {
hm.Lock() hm.Lock()
defer hm.Unlock() defer hm.Unlock()
for range 32 { for i := 0; i < 32; i++ {
index, err := generateIndex(hm.l) index, err := generateIndex(hm.l)
if err != nil { if err != nil {
return err return err
-12
View File
@@ -224,9 +224,6 @@ const (
NetworkTypeVPNPeer NetworkTypeVPNPeer
// NetworkTypeUnsafe is a network from Certificate.UnsafeNetworks() // NetworkTypeUnsafe is a network from Certificate.UnsafeNetworks()
NetworkTypeUnsafe NetworkTypeUnsafe
// NetworkTypeUnverifiedSNATPeer is used to indicate traffic we're willing to route, but never deliver to a NetworkTypeVPN
NetworkTypeUnverifiedSNATPeer
NetworkTypeInvalidPeer
) )
type HostInfo struct { type HostInfo struct {
@@ -280,15 +277,6 @@ type HostInfo struct {
lastUsed time.Time lastUsed time.Time
} }
func (i *HostInfo) HasOnlyV6Addresses() bool {
for _, vpnIp := range i.vpnAddrs {
if !vpnIp.Is6() {
return false
}
}
return true
}
type ViaSender struct { type ViaSender struct {
UdpAddr netip.AddrPort UdpAddr netip.AddrPort
relayHI *HostInfo // relayHI is the host info object of the relay relayHI *HostInfo // relayHI is the host info object of the relay
+1
View File
@@ -1,4 +1,5 @@
//go:build e2e_testing //go:build e2e_testing
// +build e2e_testing
package nebula package nebula
+8 -25
View File
@@ -12,7 +12,7 @@ import (
) )
func (f *Interface) consumeInsidePacket(packet []byte, fwPacket *firewall.Packet, nb, out []byte, q int, localCache firewall.ConntrackCache) { func (f *Interface) consumeInsidePacket(packet []byte, fwPacket *firewall.Packet, nb, out []byte, q int, localCache firewall.ConntrackCache) {
err := newPacket(packet, false, fwPacket) err := firewall.NewPacket(packet, false, fwPacket)
if err != nil { if err != nil {
if f.l.Level >= logrus.DebugLevel { if f.l.Level >= logrus.DebugLevel {
f.l.WithField("packet", packet).Debugf("Error while validating outbound packet: %s", err) f.l.WithField("packet", packet).Debugf("Error while validating outbound packet: %s", err)
@@ -48,7 +48,9 @@ func (f *Interface) consumeInsidePacket(packet []byte, fwPacket *firewall.Packet
return return
} }
hostinfo, ready := f.getHostinfo(packet, fwPacket) hostinfo, ready := f.getOrHandshakeConsiderRouting(fwPacket, func(hh *HandshakeHostInfo) {
hh.cachePacket(f.l, header.Message, 0, packet, f.sendMessageNow, f.cachedPacketMetrics)
})
if hostinfo == nil { if hostinfo == nil {
f.rejectInside(packet, out, q) f.rejectInside(packet, out, q)
@@ -64,9 +66,10 @@ func (f *Interface) consumeInsidePacket(packet []byte, fwPacket *firewall.Packet
return return
} }
dropReason := f.firewall.Drop(*fwPacket, packet, false, hostinfo, f.pki.GetCAPool(), localCache) dropReason := f.firewall.Drop(*fwPacket, false, hostinfo, f.pki.GetCAPool(), localCache)
if dropReason == nil { if dropReason == nil {
f.sendNoMetrics(header.Message, 0, hostinfo.ConnectionState, hostinfo, netip.AddrPort{}, packet, nb, out, q) f.sendNoMetrics(header.Message, 0, hostinfo.ConnectionState, hostinfo, netip.AddrPort{}, packet, nb, out, q)
} else { } else {
f.rejectInside(packet, out, q) f.rejectInside(packet, out, q)
if f.l.Level >= logrus.DebugLevel { if f.l.Level >= logrus.DebugLevel {
@@ -78,26 +81,6 @@ func (f *Interface) consumeInsidePacket(packet []byte, fwPacket *firewall.Packet
} }
} }
func (f *Interface) getHostinfo(packet []byte, fwPacket *firewall.Packet) (*HostInfo, bool) {
if f.firewall.ShouldUnSNAT(fwPacket) {
//unsnat packet re-writing also happens here, would be nice to not,
//but we need to do the unsnat lookup to find the hostinfo so we can run the firewall checks
destVpnAddr := f.firewall.unSnat(packet, fwPacket)
if destVpnAddr.IsValid() {
//because this was a snatted packet, we know it has an on-overlay destination, so no routing should be required.
return f.getOrHandshakeNoRouting(destVpnAddr, func(hh *HandshakeHostInfo) {
hh.cachePacket(f.l, header.Message, 0, packet, f.sendMessageNow, f.cachedPacketMetrics)
})
} else {
return nil, false
}
} else { //if we didn't need to unsnat
return f.getOrHandshakeConsiderRouting(fwPacket, func(hh *HandshakeHostInfo) {
hh.cachePacket(f.l, header.Message, 0, packet, f.sendMessageNow, f.cachedPacketMetrics)
})
}
}
func (f *Interface) rejectInside(packet []byte, out []byte, q int) { func (f *Interface) rejectInside(packet []byte, out []byte, q int) {
if !f.firewall.InSendReject { if !f.firewall.InSendReject {
return return
@@ -228,14 +211,14 @@ func (f *Interface) getOrHandshakeConsiderRouting(fwPacket *firewall.Packet, cac
func (f *Interface) sendMessageNow(t header.MessageType, st header.MessageSubType, hostinfo *HostInfo, p, nb, out []byte) { func (f *Interface) sendMessageNow(t header.MessageType, st header.MessageSubType, hostinfo *HostInfo, p, nb, out []byte) {
fp := &firewall.Packet{} fp := &firewall.Packet{}
err := newPacket(p, false, fp) err := firewall.NewPacket(p, false, fp)
if err != nil { if err != nil {
f.l.Warnf("error while parsing outgoing packet for firewall check; %v", err) f.l.Warnf("error while parsing outgoing packet for firewall check; %v", err)
return return
} }
// check if packet is in outbound fw rules // check if packet is in outbound fw rules
dropReason := f.firewall.Drop(*fp, p, false, hostinfo, f.pki.GetCAPool(), nil) dropReason := f.firewall.Drop(*fp, false, hostinfo, f.pki.GetCAPool(), nil)
if dropReason != nil { if dropReason != nil {
if f.l.Level >= logrus.DebugLevel { if f.l.Level >= logrus.DebugLevel {
f.l.WithField("fwPacket", fp). f.l.WithField("fwPacket", fp).
+1
View File
@@ -1,4 +1,5 @@
//go:build darwin || dragonfly || freebsd || netbsd || openbsd //go:build darwin || dragonfly || freebsd || netbsd || openbsd
// +build darwin dragonfly freebsd netbsd openbsd
package nebula package nebula
+1
View File
@@ -1,4 +1,5 @@
//go:build !darwin && !dragonfly && !freebsd && !netbsd && !openbsd //go:build !darwin && !dragonfly && !freebsd && !netbsd && !openbsd
// +build !darwin,!dragonfly,!freebsd,!netbsd,!openbsd
package nebula package nebula
-2
View File
@@ -248,7 +248,6 @@ func (f *Interface) activate() {
f.inside.Close() f.inside.Close()
f.l.Fatal(err) f.l.Fatal(err)
} }
f.firewall.SetSNATAddressFromInterface(f)
} }
func (f *Interface) run() { func (f *Interface) run() {
@@ -345,7 +344,6 @@ func (f *Interface) reloadFirewall(c *config.C) {
f.l.WithError(err).Error("Error while creating firewall during reload") f.l.WithError(err).Error("Error while creating firewall during reload")
return return
} }
fw.SetSNATAddressFromInterface(f)
oldFw := f.firewall oldFw := f.firewall
conntrack := oldFw.Conntrack conntrack := oldFw.Conntrack
+10 -3
View File
@@ -713,14 +713,21 @@ func (lh *LightHouse) unlockedShouldAddV6(vpnAddr netip.Addr, to *V6AddrPort) bo
func (lh *LightHouse) IsLighthouseAddr(vpnAddr netip.Addr) bool { func (lh *LightHouse) IsLighthouseAddr(vpnAddr netip.Addr) bool {
l := lh.GetLighthouses() l := lh.GetLighthouses()
return slices.Contains(l, vpnAddr) for i := range l {
if l[i] == vpnAddr {
return true
}
}
return false
} }
func (lh *LightHouse) IsAnyLighthouseAddr(vpnAddrs []netip.Addr) bool { func (lh *LightHouse) IsAnyLighthouseAddr(vpnAddrs []netip.Addr) bool {
l := lh.GetLighthouses() l := lh.GetLighthouses()
for i := range vpnAddrs { for i := range vpnAddrs {
if slices.Contains(l, vpnAddrs[i]) { for j := range l {
return true if l[j] == vpnAddrs[i] {
return true
}
} }
} }
return false return false
+6 -3
View File
@@ -105,7 +105,11 @@ func Main(c *config.C, configTest bool, buildVersion string, logger *logrus.Logg
// deprecated and undocumented // deprecated and undocumented
tunQueues := c.GetInt("tun.routines", 1) tunQueues := c.GetInt("tun.routines", 1)
udpQueues := c.GetInt("listen.routines", 1) udpQueues := c.GetInt("listen.routines", 1)
routines = max(tunQueues, udpQueues) if tunQueues > udpQueues {
routines = tunQueues
} else {
routines = udpQueues
}
if routines != 1 { if routines != 1 {
l.WithField("routines", routines).Warn("Setting tun.routines and listen.routines is deprecated. Use `routines` instead") l.WithField("routines", routines).Warn("Setting tun.routines and listen.routines is deprecated. Use `routines` instead")
} }
@@ -131,8 +135,7 @@ func Main(c *config.C, configTest bool, buildVersion string, logger *logrus.Logg
deviceFactory = overlay.NewDeviceFromConfig deviceFactory = overlay.NewDeviceFromConfig
} }
cs := pki.getCertState() tun, err = deviceFactory(c, l, pki.getCertState().myVpnNetworks, routines)
tun, err = deviceFactory(c, l, cs.myVpnNetworks, cs.GetDefaultCertificate().UnsafeNetworks(), routines)
if err != nil { if err != nil {
return nil, util.ContextualizeIfNeeded("Failed to get a tun/tap device", err) return nil, util.ContextualizeIfNeeded("Failed to get a tun/tap device", err)
} }
+1
View File
@@ -1,4 +1,5 @@
//go:build !boringcrypto //go:build !boringcrypto
// +build !boringcrypto
package nebula package nebula
+2 -209
View File
@@ -1,22 +1,13 @@
package nebula package nebula
import ( import (
"encoding/binary"
"errors" "errors"
"net/netip" "net/netip"
"time" "time"
"github.com/google/gopacket/layers"
"golang.org/x/net/ipv6"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/slackhq/nebula/firewall" "github.com/slackhq/nebula/firewall"
"github.com/slackhq/nebula/header" "github.com/slackhq/nebula/header"
"golang.org/x/net/ipv4"
)
const (
minFwPacketLen = 4
) )
func (f *Interface) readOutsidePackets(via ViaSender, out []byte, packet []byte, h *header.H, fwPacket *firewall.Packet, lhf *LightHouseHandler, nb []byte, q int, localCache firewall.ConntrackCache) { func (f *Interface) readOutsidePackets(via ViaSender, out []byte, packet []byte, h *header.H, fwPacket *firewall.Packet, lhf *LightHouseHandler, nb []byte, q int, localCache firewall.ConntrackCache) {
@@ -278,204 +269,6 @@ func (f *Interface) handleEncrypted(ci *ConnectionState, via ViaSender, h *heade
return true return true
} }
var (
ErrPacketTooShort = errors.New("packet is too short")
ErrUnknownIPVersion = errors.New("packet is an unknown ip version")
ErrIPv4InvalidHeaderLength = errors.New("invalid ipv4 header length")
ErrIPv4PacketTooShort = errors.New("ipv4 packet is too short")
ErrIPv6PacketTooShort = errors.New("ipv6 packet is too short")
ErrIPv6CouldNotFindPayload = errors.New("could not find payload in ipv6 packet")
)
// newPacket validates and parses the interesting bits for the firewall out of the ip and sub protocol headers
func newPacket(data []byte, incoming bool, fp *firewall.Packet) error {
if len(data) < 1 {
return ErrPacketTooShort
}
version := int((data[0] >> 4) & 0x0f)
switch version {
case ipv4.Version:
return parseV4(data, incoming, fp)
case ipv6.Version:
return parseV6(data, incoming, fp)
}
return ErrUnknownIPVersion
}
func parseV6(data []byte, incoming bool, fp *firewall.Packet) error {
dataLen := len(data)
if dataLen < ipv6.HeaderLen {
return ErrIPv6PacketTooShort
}
if incoming {
fp.RemoteAddr, _ = netip.AddrFromSlice(data[8:24])
fp.LocalAddr, _ = netip.AddrFromSlice(data[24:40])
} else {
fp.LocalAddr, _ = netip.AddrFromSlice(data[8:24])
fp.RemoteAddr, _ = netip.AddrFromSlice(data[24:40])
}
protoAt := 6 // NextHeader is at 6 bytes into the ipv6 header
offset := ipv6.HeaderLen // Start at the end of the ipv6 header
next := 0
for {
if protoAt >= dataLen {
break
}
proto := layers.IPProtocol(data[protoAt])
switch proto {
case layers.IPProtocolESP, layers.IPProtocolNoNextHeader:
fp.Protocol = uint8(proto)
fp.RemotePort = 0
fp.LocalPort = 0
fp.Fragment = false
return nil
case layers.IPProtocolICMPv6:
if dataLen < offset+6 {
return ErrIPv6PacketTooShort
}
fp.Protocol = uint8(proto)
fp.LocalPort = 0 //incoming vs outgoing doesn't matter for icmpv6
icmptype := data[offset+1]
switch icmptype {
case layers.ICMPv6TypeEchoRequest, layers.ICMPv6TypeEchoReply:
fp.RemotePort = binary.BigEndian.Uint16(data[offset+4 : offset+6]) //identifier
default:
fp.RemotePort = 0
}
fp.Fragment = false
return nil
case layers.IPProtocolTCP, layers.IPProtocolUDP:
if dataLen < offset+4 {
return ErrIPv6PacketTooShort
}
fp.Protocol = uint8(proto)
if incoming {
fp.RemotePort = binary.BigEndian.Uint16(data[offset : offset+2])
fp.LocalPort = binary.BigEndian.Uint16(data[offset+2 : offset+4])
} else {
fp.LocalPort = binary.BigEndian.Uint16(data[offset : offset+2])
fp.RemotePort = binary.BigEndian.Uint16(data[offset+2 : offset+4])
}
fp.Fragment = false
return nil
case layers.IPProtocolIPv6Fragment:
// Fragment header is 8 bytes, need at least offset+4 to read the offset field
if dataLen < offset+8 {
return ErrIPv6PacketTooShort
}
// Check if this is the first fragment
fragmentOffset := binary.BigEndian.Uint16(data[offset+2:offset+4]) &^ uint16(0x7) // Remove the reserved and M flag bits
if fragmentOffset != 0 {
// Non-first fragment, use what we have now and stop processing
fp.Protocol = data[offset]
fp.Fragment = true
fp.RemotePort = 0
fp.LocalPort = 0
return nil
}
// The next loop should be the transport layer since we are the first fragment
next = 8 // Fragment headers are always 8 bytes
case layers.IPProtocolAH:
// Auth headers, used by IPSec, have a different meaning for header length
if dataLen <= offset+1 {
break
}
next = int(data[offset+1]+2) << 2
default:
// Normal ipv6 header length processing
if dataLen <= offset+1 {
break
}
next = int(data[offset+1]+1) << 3
}
if next <= 0 {
// Safety check, each ipv6 header has to be at least 8 bytes
next = 8
}
protoAt = offset
offset = offset + next
}
return ErrIPv6CouldNotFindPayload
}
func parseV4(data []byte, incoming bool, fp *firewall.Packet) error {
// Do we at least have an ipv4 header worth of data?
if len(data) < ipv4.HeaderLen {
return ErrIPv4PacketTooShort
}
// Adjust our start position based on the advertised ip header length
ihl := int(data[0]&0x0f) << 2
// Well-formed ip header length?
if ihl < ipv4.HeaderLen {
return ErrIPv4InvalidHeaderLength
}
// Check if this is the second or further fragment of a fragmented packet.
flagsfrags := binary.BigEndian.Uint16(data[6:8])
fp.Fragment = (flagsfrags & 0x1FFF) != 0
// Firewall handles protocol checks
fp.Protocol = data[9]
// Accounting for a variable header length, do we have enough data for our src/dst tuples?
minLen := ihl
if !fp.Fragment {
if fp.Protocol == firewall.ProtoICMP {
minLen += minFwPacketLen + 2
} else {
minLen += minFwPacketLen
}
}
if len(data) < minLen {
return ErrIPv4InvalidHeaderLength
}
if incoming { // Firewall packets are locally oriented
fp.RemoteAddr, _ = netip.AddrFromSlice(data[12:16])
fp.LocalAddr, _ = netip.AddrFromSlice(data[16:20])
} else {
fp.LocalAddr, _ = netip.AddrFromSlice(data[12:16])
fp.RemoteAddr, _ = netip.AddrFromSlice(data[16:20])
}
if fp.Fragment {
fp.RemotePort = 0
fp.LocalPort = 0
} else if fp.Protocol == firewall.ProtoICMP { //note that orientation doesn't matter on ICMP
fp.RemotePort = binary.BigEndian.Uint16(data[ihl+4 : ihl+6]) //identifier
fp.LocalPort = 0 //code would be uint16(data[ihl+1])
} else if incoming {
fp.RemotePort = binary.BigEndian.Uint16(data[ihl : ihl+2]) //src port
fp.LocalPort = binary.BigEndian.Uint16(data[ihl+2 : ihl+4]) //dst port
} else {
fp.LocalPort = binary.BigEndian.Uint16(data[ihl : ihl+2]) //src port
fp.RemotePort = binary.BigEndian.Uint16(data[ihl+2 : ihl+4]) //dst port
}
return nil
}
func (f *Interface) decrypt(hostinfo *HostInfo, mc uint64, out []byte, packet []byte, h *header.H, nb []byte) ([]byte, error) { func (f *Interface) decrypt(hostinfo *HostInfo, mc uint64, out []byte, packet []byte, h *header.H, nb []byte) ([]byte, error) {
var err error var err error
out, err = hostinfo.ConnectionState.dKey.DecryptDanger(out, packet[:header.Len], packet[header.Len:], mc, nb) out, err = hostinfo.ConnectionState.dKey.DecryptDanger(out, packet[:header.Len], packet[header.Len:], mc, nb)
@@ -501,7 +294,7 @@ func (f *Interface) decryptToTun(hostinfo *HostInfo, messageCounter uint64, out
return false return false
} }
err = newPacket(out, true, fwPacket) err = firewall.NewPacket(out, true, fwPacket)
if err != nil { if err != nil {
hostinfo.logger(f.l).WithError(err).WithField("packet", out). hostinfo.logger(f.l).WithError(err).WithField("packet", out).
Warnf("Error while validating inbound packet") Warnf("Error while validating inbound packet")
@@ -514,7 +307,7 @@ func (f *Interface) decryptToTun(hostinfo *HostInfo, messageCounter uint64, out
return false return false
} }
dropReason := f.firewall.Drop(*fwPacket, out, true, hostinfo, f.pki.GetCAPool(), localCache) dropReason := f.firewall.Drop(*fwPacket, true, hostinfo, f.pki.GetCAPool(), localCache)
if dropReason != nil { if dropReason != nil {
// NOTE: We give `packet` as the `out` here since we already decrypted from it and we don't need it anymore // NOTE: We give `packet` as the `out` here since we already decrypted from it and we don't need it anymore
// This gives us a buffer to build the reject packet in // This gives us a buffer to build the reject packet in
+37 -44
View File
@@ -20,13 +20,13 @@ func Test_newPacket(t *testing.T) {
p := &firewall.Packet{} p := &firewall.Packet{}
// length fails // length fails
err := newPacket([]byte{}, true, p) err := firewall.NewPacket([]byte{}, true, p)
require.ErrorIs(t, err, ErrPacketTooShort) require.ErrorIs(t, err, ErrPacketTooShort)
err = newPacket([]byte{0x40}, true, p) err = firewall.NewPacket([]byte{0x40}, true, p)
require.ErrorIs(t, err, ErrIPv4PacketTooShort) require.ErrorIs(t, err, ErrIPv4PacketTooShort)
err = newPacket([]byte{0x60}, true, p) err = firewall.NewPacket([]byte{0x60}, true, p)
require.ErrorIs(t, err, ErrIPv6PacketTooShort) require.ErrorIs(t, err, ErrIPv6PacketTooShort)
// length fail with ip options // length fail with ip options
@@ -39,15 +39,15 @@ func Test_newPacket(t *testing.T) {
} }
b, _ := h.Marshal() b, _ := h.Marshal()
err = newPacket(b, true, p) err = firewall.NewPacket(b, true, p)
require.ErrorIs(t, err, ErrIPv4InvalidHeaderLength) require.ErrorIs(t, err, ErrIPv4InvalidHeaderLength)
// not an ipv4 packet // not an ipv4 packet
err = newPacket([]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, true, p) err = firewall.NewPacket([]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, true, p)
require.ErrorIs(t, err, ErrUnknownIPVersion) require.ErrorIs(t, err, ErrUnknownIPVersion)
// invalid ihl // invalid ihl
err = newPacket([]byte{4<<4 | (8 >> 2 & 0x0f), 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, true, p) err = firewall.NewPacket([]byte{4<<4 | (8 >> 2 & 0x0f), 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, true, p)
require.ErrorIs(t, err, ErrIPv4InvalidHeaderLength) require.ErrorIs(t, err, ErrIPv4InvalidHeaderLength)
// account for variable ip header length - incoming // account for variable ip header length - incoming
@@ -62,7 +62,7 @@ func Test_newPacket(t *testing.T) {
b, _ = h.Marshal() b, _ = h.Marshal()
b = append(b, []byte{0, 3, 0, 4}...) b = append(b, []byte{0, 3, 0, 4}...)
err = newPacket(b, true, p) err = firewall.NewPacket(b, true, p)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, uint8(firewall.ProtoTCP), p.Protocol) assert.Equal(t, uint8(firewall.ProtoTCP), p.Protocol)
@@ -84,7 +84,7 @@ func Test_newPacket(t *testing.T) {
b, _ = h.Marshal() b, _ = h.Marshal()
b = append(b, []byte{0, 5, 0, 6}...) b = append(b, []byte{0, 5, 0, 6}...)
err = newPacket(b, false, p) err = firewall.NewPacket(b, false, p)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, uint8(2), p.Protocol) assert.Equal(t, uint8(2), p.Protocol)
@@ -114,7 +114,7 @@ func Test_newPacket_v6(t *testing.T) {
err := gopacket.SerializeLayers(buffer, opt, &ip) err := gopacket.SerializeLayers(buffer, opt, &ip)
require.NoError(t, err) require.NoError(t, err)
err = newPacket(buffer.Bytes(), true, p) err = firewall.NewPacket(buffer.Bytes(), true, p)
require.ErrorIs(t, err, ErrIPv6CouldNotFindPayload) require.ErrorIs(t, err, ErrIPv6CouldNotFindPayload)
// A v6 packet with a hop-by-hop extension // A v6 packet with a hop-by-hop extension
@@ -148,14 +148,13 @@ func Test_newPacket_v6(t *testing.T) {
// A full IPv6 header and 1 byte in the first extension, but missing // A full IPv6 header and 1 byte in the first extension, but missing
// the length byte. // the length byte.
err = newPacket(buffer.Bytes()[:41], true, p) err = firewall.NewPacket(buffer.Bytes()[:41], true, p)
require.ErrorIs(t, err, ErrIPv6CouldNotFindPayload) require.ErrorIs(t, err, ErrIPv6CouldNotFindPayload)
// A full IPv6 header plus 1 full extension, but only 1 byte of the // A full IPv6 header plus 1 full extension, but only 1 byte of the
// next layer, missing length byte // next layer, missing length byte
err = newPacket(buffer.Bytes()[:49], true, p) err = firewall.NewPacket(buffer.Bytes()[:49], true, p)
require.ErrorIs(t, err, ErrIPv6CouldNotFindPayload) require.ErrorIs(t, err, ErrIPv6CouldNotFindPayload)
err = nil
// A good ICMP packet // A good ICMP packet
ip = layers.IPv6{ ip = layers.IPv6{
@@ -166,33 +165,27 @@ func Test_newPacket_v6(t *testing.T) {
DstIP: net.IPv6linklocalallnodes, DstIP: net.IPv6linklocalallnodes,
} }
icmp := layers.ICMPv6{ icmp := layers.ICMPv6{}
TypeCode: layers.ICMPv6TypeEchoRequest,
Checksum: 0x1234,
}
buffer.Clear() buffer.Clear()
require.NoError(t, gopacket.SerializeLayers(buffer, opt, &ip, &icmp)) err = gopacket.SerializeLayers(buffer, opt, &ip, &icmp)
require.Error(t, newPacket(buffer.Bytes(), true, p)) if err != nil {
panic(err)
buffer.Clear()
echo := layers.ICMPv6Echo{
Identifier: 0xabcd,
SeqNumber: 1234,
} }
require.NoError(t, gopacket.SerializeLayers(buffer, opt, &ip, &icmp, &echo))
require.NoError(t, newPacket(buffer.Bytes(), true, p)) err = firewall.NewPacket(buffer.Bytes(), true, p)
require.NoError(t, err)
assert.Equal(t, uint8(layers.IPProtocolICMPv6), p.Protocol) assert.Equal(t, uint8(layers.IPProtocolICMPv6), p.Protocol)
assert.Equal(t, netip.MustParseAddr("ff02::2"), p.RemoteAddr) assert.Equal(t, netip.MustParseAddr("ff02::2"), p.RemoteAddr)
assert.Equal(t, netip.MustParseAddr("ff02::1"), p.LocalAddr) assert.Equal(t, netip.MustParseAddr("ff02::1"), p.LocalAddr)
assert.Equal(t, uint16(0xabcd), p.RemotePort) assert.Equal(t, uint16(0), p.RemotePort)
assert.Equal(t, uint16(0), p.LocalPort) assert.Equal(t, uint16(0), p.LocalPort)
assert.False(t, p.Fragment) assert.False(t, p.Fragment)
// A good ESP packet // A good ESP packet
b := buffer.Bytes() b := buffer.Bytes()
b[6] = byte(layers.IPProtocolESP) b[6] = byte(layers.IPProtocolESP)
err = newPacket(b, true, p) err = firewall.NewPacket(b, true, p)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, uint8(layers.IPProtocolESP), p.Protocol) assert.Equal(t, uint8(layers.IPProtocolESP), p.Protocol)
assert.Equal(t, netip.MustParseAddr("ff02::2"), p.RemoteAddr) assert.Equal(t, netip.MustParseAddr("ff02::2"), p.RemoteAddr)
@@ -204,7 +197,7 @@ func Test_newPacket_v6(t *testing.T) {
// A good None packet // A good None packet
b = buffer.Bytes() b = buffer.Bytes()
b[6] = byte(layers.IPProtocolNoNextHeader) b[6] = byte(layers.IPProtocolNoNextHeader)
err = newPacket(b, true, p) err = firewall.NewPacket(b, true, p)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, uint8(layers.IPProtocolNoNextHeader), p.Protocol) assert.Equal(t, uint8(layers.IPProtocolNoNextHeader), p.Protocol)
assert.Equal(t, netip.MustParseAddr("ff02::2"), p.RemoteAddr) assert.Equal(t, netip.MustParseAddr("ff02::2"), p.RemoteAddr)
@@ -216,7 +209,7 @@ func Test_newPacket_v6(t *testing.T) {
// An unknown protocol packet // An unknown protocol packet
b = buffer.Bytes() b = buffer.Bytes()
b[6] = 255 // 255 is a reserved protocol number b[6] = 255 // 255 is a reserved protocol number
err = newPacket(b, true, p) err = firewall.NewPacket(b, true, p)
require.ErrorIs(t, err, ErrIPv6CouldNotFindPayload) require.ErrorIs(t, err, ErrIPv6CouldNotFindPayload)
// A good UDP packet // A good UDP packet
@@ -243,7 +236,7 @@ func Test_newPacket_v6(t *testing.T) {
b = buffer.Bytes() b = buffer.Bytes()
// incoming // incoming
err = newPacket(b, true, p) err = firewall.NewPacket(b, true, p)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, uint8(firewall.ProtoUDP), p.Protocol) assert.Equal(t, uint8(firewall.ProtoUDP), p.Protocol)
assert.Equal(t, netip.MustParseAddr("ff02::2"), p.RemoteAddr) assert.Equal(t, netip.MustParseAddr("ff02::2"), p.RemoteAddr)
@@ -253,7 +246,7 @@ func Test_newPacket_v6(t *testing.T) {
assert.False(t, p.Fragment) assert.False(t, p.Fragment)
// outgoing // outgoing
err = newPacket(b, false, p) err = firewall.NewPacket(b, false, p)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, uint8(firewall.ProtoUDP), p.Protocol) assert.Equal(t, uint8(firewall.ProtoUDP), p.Protocol)
assert.Equal(t, netip.MustParseAddr("ff02::2"), p.LocalAddr) assert.Equal(t, netip.MustParseAddr("ff02::2"), p.LocalAddr)
@@ -263,14 +256,14 @@ func Test_newPacket_v6(t *testing.T) {
assert.False(t, p.Fragment) assert.False(t, p.Fragment)
// Too short UDP packet // Too short UDP packet
err = newPacket(b[:len(b)-10], false, p) // pull off the last 10 bytes err = firewall.NewPacket(b[:len(b)-10], false, p) // pull off the last 10 bytes
require.ErrorIs(t, err, ErrIPv6PacketTooShort) require.ErrorIs(t, err, ErrIPv6PacketTooShort)
// A good TCP packet // A good TCP packet
b[6] = byte(layers.IPProtocolTCP) b[6] = byte(layers.IPProtocolTCP)
// incoming // incoming
err = newPacket(b, true, p) err = firewall.NewPacket(b, true, p)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, uint8(firewall.ProtoTCP), p.Protocol) assert.Equal(t, uint8(firewall.ProtoTCP), p.Protocol)
assert.Equal(t, netip.MustParseAddr("ff02::2"), p.RemoteAddr) assert.Equal(t, netip.MustParseAddr("ff02::2"), p.RemoteAddr)
@@ -280,7 +273,7 @@ func Test_newPacket_v6(t *testing.T) {
assert.False(t, p.Fragment) assert.False(t, p.Fragment)
// outgoing // outgoing
err = newPacket(b, false, p) err = firewall.NewPacket(b, false, p)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, uint8(firewall.ProtoTCP), p.Protocol) assert.Equal(t, uint8(firewall.ProtoTCP), p.Protocol)
assert.Equal(t, netip.MustParseAddr("ff02::2"), p.LocalAddr) assert.Equal(t, netip.MustParseAddr("ff02::2"), p.LocalAddr)
@@ -290,7 +283,7 @@ func Test_newPacket_v6(t *testing.T) {
assert.False(t, p.Fragment) assert.False(t, p.Fragment)
// Too short TCP packet // Too short TCP packet
err = newPacket(b[:len(b)-10], false, p) // pull off the last 10 bytes err = firewall.NewPacket(b[:len(b)-10], false, p) // pull off the last 10 bytes
require.ErrorIs(t, err, ErrIPv6PacketTooShort) require.ErrorIs(t, err, ErrIPv6PacketTooShort)
// A good UDP packet with an AH header // A good UDP packet with an AH header
@@ -325,7 +318,7 @@ func Test_newPacket_v6(t *testing.T) {
b = append(b, ahb...) b = append(b, ahb...)
b = append(b, udpHeader...) b = append(b, udpHeader...)
err = newPacket(b, true, p) err = firewall.NewPacket(b, true, p)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, uint8(firewall.ProtoUDP), p.Protocol) assert.Equal(t, uint8(firewall.ProtoUDP), p.Protocol)
assert.Equal(t, netip.MustParseAddr("ff02::2"), p.RemoteAddr) assert.Equal(t, netip.MustParseAddr("ff02::2"), p.RemoteAddr)
@@ -335,12 +328,12 @@ func Test_newPacket_v6(t *testing.T) {
assert.False(t, p.Fragment) assert.False(t, p.Fragment)
// Ensure buffer bounds checking during processing // Ensure buffer bounds checking during processing
err = newPacket(b[:41], true, p) err = firewall.NewPacket(b[:41], true, p)
require.ErrorIs(t, err, ErrIPv6PacketTooShort) require.ErrorIs(t, err, ErrIPv6PacketTooShort)
// Invalid AH header // Invalid AH header
b = buffer.Bytes() b = buffer.Bytes()
err = newPacket(b, true, p) err = firewall.NewPacket(b, true, p)
require.ErrorIs(t, err, ErrIPv6CouldNotFindPayload) require.ErrorIs(t, err, ErrIPv6CouldNotFindPayload)
} }
@@ -388,7 +381,7 @@ func Test_newPacket_ipv6Fragment(t *testing.T) {
firstFrag = append(firstFrag, []byte{0xde, 0xad, 0xbe, 0xef}...) firstFrag = append(firstFrag, []byte{0xde, 0xad, 0xbe, 0xef}...)
// Test first fragment incoming // Test first fragment incoming
err = newPacket(firstFrag, true, p) err = firewall.NewPacket(firstFrag, true, p)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, netip.MustParseAddr("ff02::2"), p.RemoteAddr) assert.Equal(t, netip.MustParseAddr("ff02::2"), p.RemoteAddr)
assert.Equal(t, netip.MustParseAddr("ff02::1"), p.LocalAddr) assert.Equal(t, netip.MustParseAddr("ff02::1"), p.LocalAddr)
@@ -398,7 +391,7 @@ func Test_newPacket_ipv6Fragment(t *testing.T) {
assert.False(t, p.Fragment) assert.False(t, p.Fragment)
// Test first fragment outgoing // Test first fragment outgoing
err = newPacket(firstFrag, false, p) err = firewall.NewPacket(firstFrag, false, p)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, netip.MustParseAddr("ff02::2"), p.LocalAddr) assert.Equal(t, netip.MustParseAddr("ff02::2"), p.LocalAddr)
assert.Equal(t, netip.MustParseAddr("ff02::1"), p.RemoteAddr) assert.Equal(t, netip.MustParseAddr("ff02::1"), p.RemoteAddr)
@@ -427,7 +420,7 @@ func Test_newPacket_ipv6Fragment(t *testing.T) {
secondFrag = append(secondFrag, []byte{0xde, 0xad, 0xbe, 0xef}...) secondFrag = append(secondFrag, []byte{0xde, 0xad, 0xbe, 0xef}...)
// Test second fragment incoming // Test second fragment incoming
err = newPacket(secondFrag, true, p) err = firewall.NewPacket(secondFrag, true, p)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, netip.MustParseAddr("ff02::2"), p.RemoteAddr) assert.Equal(t, netip.MustParseAddr("ff02::2"), p.RemoteAddr)
assert.Equal(t, netip.MustParseAddr("ff02::1"), p.LocalAddr) assert.Equal(t, netip.MustParseAddr("ff02::1"), p.LocalAddr)
@@ -437,7 +430,7 @@ func Test_newPacket_ipv6Fragment(t *testing.T) {
assert.True(t, p.Fragment) assert.True(t, p.Fragment)
// Test second fragment outgoing // Test second fragment outgoing
err = newPacket(secondFrag, false, p) err = firewall.NewPacket(secondFrag, false, p)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, netip.MustParseAddr("ff02::2"), p.LocalAddr) assert.Equal(t, netip.MustParseAddr("ff02::2"), p.LocalAddr)
assert.Equal(t, netip.MustParseAddr("ff02::1"), p.RemoteAddr) assert.Equal(t, netip.MustParseAddr("ff02::1"), p.RemoteAddr)
@@ -447,7 +440,7 @@ func Test_newPacket_ipv6Fragment(t *testing.T) {
assert.True(t, p.Fragment) assert.True(t, p.Fragment)
// Too short of a fragment packet // Too short of a fragment packet
err = newPacket(secondFrag[:len(secondFrag)-10], false, p) err = firewall.NewPacket(secondFrag[:len(secondFrag)-10], false, p)
require.ErrorIs(t, err, ErrIPv6PacketTooShort) require.ErrorIs(t, err, ErrIPv6PacketTooShort)
} }
@@ -581,7 +574,7 @@ func BenchmarkParseV6(b *testing.B) {
} }
evilBytes := buffer.Bytes() evilBytes := buffer.Bytes()
for range 200 { for i := 0; i < 200; i++ {
evilBytes = append(evilBytes, hopHeader...) evilBytes = append(evilBytes, hopHeader...)
} }
evilBytes = append(evilBytes, lastHopHeader...) evilBytes = append(evilBytes, lastHopHeader...)
-3
View File
@@ -11,9 +11,6 @@ type Device interface {
io.ReadWriteCloser io.ReadWriteCloser
Activate() error Activate() error
Networks() []netip.Prefix Networks() []netip.Prefix
UnsafeNetworks() []netip.Prefix
UnsafeIPv4OriginAddress() netip.Prefix
SNATAddress() netip.Prefix
Name() string Name() string
RoutesFor(netip.Addr) routing.Gateways RoutesFor(netip.Addr) routing.Gateways
SupportsMultiqueue() bool SupportsMultiqueue() bool
+5 -89
View File
@@ -1,9 +1,7 @@
package overlay package overlay
import ( import (
"crypto/rand"
"fmt" "fmt"
"io"
"net" "net"
"net/netip" "net/netip"
@@ -24,22 +22,22 @@ func (e *NameError) Error() string {
} }
// TODO: We may be able to remove routines // TODO: We may be able to remove routines
type DeviceFactory func(c *config.C, l *logrus.Logger, vpnNetworks []netip.Prefix, unsafeNetworks []netip.Prefix, routines int) (Device, error) type DeviceFactory func(c *config.C, l *logrus.Logger, vpnNetworks []netip.Prefix, routines int) (Device, error)
func NewDeviceFromConfig(c *config.C, l *logrus.Logger, vpnNetworks []netip.Prefix, unsafeNetworks []netip.Prefix, routines int) (Device, error) { func NewDeviceFromConfig(c *config.C, l *logrus.Logger, vpnNetworks []netip.Prefix, routines int) (Device, error) {
switch { switch {
case c.GetBool("tun.disabled", false): case c.GetBool("tun.disabled", false):
tun := newDisabledTun(vpnNetworks, c.GetInt("tun.tx_queue", 500), c.GetBool("stats.message_metrics", false), l) tun := newDisabledTun(vpnNetworks, c.GetInt("tun.tx_queue", 500), c.GetBool("stats.message_metrics", false), l)
return tun, nil return tun, nil
default: default:
return newTun(c, l, vpnNetworks, unsafeNetworks, routines > 1) return newTun(c, l, vpnNetworks, routines > 1)
} }
} }
func NewFdDeviceFromConfig(fd *int) DeviceFactory { func NewFdDeviceFromConfig(fd *int) DeviceFactory {
return func(c *config.C, l *logrus.Logger, vpnNetworks []netip.Prefix, unsafeNetworks []netip.Prefix, routines int) (Device, error) { return func(c *config.C, l *logrus.Logger, vpnNetworks []netip.Prefix, routines int) (Device, error) {
return newTunFromFd(c, l, *fd, vpnNetworks, unsafeNetworks) return newTunFromFd(c, l, *fd, vpnNetworks)
} }
} }
@@ -131,85 +129,3 @@ func selectGateway(dest netip.Prefix, gateways []netip.Prefix) (netip.Prefix, er
return netip.Prefix{}, fmt.Errorf("no gateway found for %v in the list of vpn networks", dest) return netip.Prefix{}, fmt.Errorf("no gateway found for %v in the list of vpn networks", dest)
} }
// genLinkLocal generates a random IPv4 link-local address.
// If randomizer is nil, it uses rand.Reader to find two random bytes
func genLinkLocal(randomizer io.Reader) netip.Prefix {
if randomizer == nil {
randomizer = rand.Reader
}
octets := []byte{169, 254, 0, 0}
_, _ = randomizer.Read(octets[2:4])
return coerceLinkLocal(octets)
}
func coerceLinkLocal(octets []byte) netip.Prefix {
if octets[3] == 0 {
octets[3] = 1 //please no .0 addresses
} else if octets[2] == 255 && octets[3] == 255 {
octets[3] = 254 //please no broadcast addresses
}
out, _ := netip.AddrFromSlice(octets)
return netip.PrefixFrom(out, 32)
}
// prepareUnsafeOriginAddr provides the IPv4 address used on IPv6-only clients that need to access IPv4 unsafe routes
func prepareUnsafeOriginAddr(d Device, l *logrus.Logger, c *config.C, routes []Route) netip.Prefix {
if !d.Networks()[0].Addr().Is6() {
return netip.Prefix{} //if we have an IPv4 assignment within the overlay, we don't need an unsafe origin address
}
needed := false
for _, route := range routes { //or if we have a route defined into an IPv4 range
if route.Cidr.Addr().Is4() {
needed = true //todo should this only apply to unsafe routes? almost certainly
break
}
}
if !needed {
return netip.Prefix{}
}
//todo better config name for sure
if a := c.GetString("tun.unsafe_origin_address_for_4over6", ""); a != "" {
out, err := netip.ParseAddr(a)
if err != nil {
l.WithField("value", a).WithError(err).Warn("failed to parse tun.unsafe_origin_address_for_4over6, will use a random value")
} else if !out.Is4() || !out.IsLinkLocalUnicast() {
l.WithField("value", out).Warn("tun.unsafe_origin_address_for_4over6 must be an IPv4 address")
} else if out.IsValid() {
return netip.PrefixFrom(out, 32)
}
}
return genLinkLocal(nil)
}
// prepareSnatAddr provides the address that an IPv6-only unsafe router should use to SNAT traffic before handing it to the operating system
func prepareSnatAddr(d Device, l *logrus.Logger, c *config.C) netip.Prefix {
if !d.Networks()[0].Addr().Is6() {
return netip.Prefix{} //if we have an IPv4 assignment within the overlay, we don't need a snat address
}
needed := false
for _, un := range d.UnsafeNetworks() { //if we are an unsafe router for an IPv4 range
if un.Addr().Is4() {
needed = true
break
}
}
if !needed {
return netip.Prefix{}
}
if a := c.GetString("tun.snat_address_for_4over6", ""); a != "" {
out, err := netip.ParseAddr(a)
if err != nil {
l.WithField("value", a).WithError(err).Warn("failed to parse tun.snat_address_for_4over6, will use a random value")
} else if !out.Is4() || !out.IsLinkLocalUnicast() {
l.WithField("value", out).Warn("tun.snat_address_for_4over6 must be an IPv4 address")
} else if out.IsValid() {
return netip.PrefixFrom(out, 32)
}
}
return genLinkLocal(nil)
}
+7 -24
View File
@@ -19,16 +19,14 @@ import (
type tun struct { type tun struct {
io.ReadWriteCloser io.ReadWriteCloser
fd int fd int
vpnNetworks []netip.Prefix vpnNetworks []netip.Prefix
unsafeNetworks []netip.Prefix Routes atomic.Pointer[[]Route]
unsafeIPv4Origin netip.Prefix routeTree atomic.Pointer[bart.Table[routing.Gateways]]
Routes atomic.Pointer[[]Route] l *logrus.Logger
routeTree atomic.Pointer[bart.Table[routing.Gateways]]
l *logrus.Logger
} }
func newTunFromFd(c *config.C, l *logrus.Logger, deviceFd int, vpnNetworks []netip.Prefix, unsafeNetworks []netip.Prefix) (*tun, error) { func newTunFromFd(c *config.C, l *logrus.Logger, deviceFd int, vpnNetworks []netip.Prefix) (*tun, error) {
// XXX Android returns an fd in non-blocking mode which is necessary for shutdown to work properly. // XXX Android returns an fd in non-blocking mode which is necessary for shutdown to work properly.
// Be sure not to call file.Fd() as it will set the fd to blocking mode. // Be sure not to call file.Fd() as it will set the fd to blocking mode.
file := os.NewFile(uintptr(deviceFd), "/dev/net/tun") file := os.NewFile(uintptr(deviceFd), "/dev/net/tun")
@@ -37,7 +35,6 @@ func newTunFromFd(c *config.C, l *logrus.Logger, deviceFd int, vpnNetworks []net
ReadWriteCloser: file, ReadWriteCloser: file,
fd: deviceFd, fd: deviceFd,
vpnNetworks: vpnNetworks, vpnNetworks: vpnNetworks,
unsafeNetworks: unsafeNetworks,
l: l, l: l,
} }
@@ -56,7 +53,7 @@ func newTunFromFd(c *config.C, l *logrus.Logger, deviceFd int, vpnNetworks []net
return t, nil return t, nil
} }
func newTun(_ *config.C, _ *logrus.Logger, _ []netip.Prefix, _ []netip.Prefix, _ bool) (*tun, error) { func newTun(_ *config.C, _ *logrus.Logger, _ []netip.Prefix, _ bool) (*tun, error) {
return nil, fmt.Errorf("newTun not supported in Android") return nil, fmt.Errorf("newTun not supported in Android")
} }
@@ -79,8 +76,6 @@ func (t *tun) reload(c *config.C, initial bool) error {
return nil return nil
} }
t.unsafeIPv4Origin = prepareUnsafeOriginAddr(t, t.l, c, routes)
routeTree, err := makeRouteTree(t.l, routes, false) routeTree, err := makeRouteTree(t.l, routes, false)
if err != nil { if err != nil {
return err return err
@@ -96,18 +91,6 @@ func (t *tun) Networks() []netip.Prefix {
return t.vpnNetworks return t.vpnNetworks
} }
func (t *tun) UnsafeNetworks() []netip.Prefix {
return t.unsafeNetworks
}
func (t *tun) UnsafeIPv4OriginAddress() netip.Prefix {
return t.unsafeIPv4Origin
}
func (t *tun) SNATAddress() netip.Prefix {
return netip.Prefix{}
}
func (t *tun) Name() string { func (t *tun) Name() string {
return "android" return "android"
} }
+9 -33
View File
@@ -24,15 +24,13 @@ import (
type tun struct { type tun struct {
io.ReadWriteCloser io.ReadWriteCloser
Device string Device string
vpnNetworks []netip.Prefix vpnNetworks []netip.Prefix
unsafeNetworks []netip.Prefix DefaultMTU int
unsafeIPv4Origin netip.Prefix Routes atomic.Pointer[[]Route]
DefaultMTU int routeTree atomic.Pointer[bart.Table[routing.Gateways]]
Routes atomic.Pointer[[]Route] linkAddr *netroute.LinkAddr
routeTree atomic.Pointer[bart.Table[routing.Gateways]] l *logrus.Logger
linkAddr *netroute.LinkAddr
l *logrus.Logger
// cache out buffer since we need to prepend 4 bytes for tun metadata // cache out buffer since we need to prepend 4 bytes for tun metadata
out []byte out []byte
@@ -81,7 +79,7 @@ type ifreqAlias6 struct {
Lifetime addrLifetime Lifetime addrLifetime
} }
func newTun(c *config.C, l *logrus.Logger, vpnNetworks []netip.Prefix, unsafeNetworks []netip.Prefix, _ bool) (*tun, error) { func newTun(c *config.C, l *logrus.Logger, vpnNetworks []netip.Prefix, _ bool) (*tun, error) {
name := c.GetString("tun.dev", "") name := c.GetString("tun.dev", "")
ifIndex := -1 ifIndex := -1
if name != "" && name != "utun" { if name != "" && name != "utun" {
@@ -129,7 +127,6 @@ func newTun(c *config.C, l *logrus.Logger, vpnNetworks []netip.Prefix, unsafeNet
ReadWriteCloser: os.NewFile(uintptr(fd), ""), ReadWriteCloser: os.NewFile(uintptr(fd), ""),
Device: name, Device: name,
vpnNetworks: vpnNetworks, vpnNetworks: vpnNetworks,
unsafeNetworks: unsafeNetworks,
DefaultMTU: c.GetInt("tun.mtu", DefaultMTU), DefaultMTU: c.GetInt("tun.mtu", DefaultMTU),
l: l, l: l,
} }
@@ -156,7 +153,7 @@ func (t *tun) deviceBytes() (o [16]byte) {
return return
} }
func newTunFromFd(_ *config.C, _ *logrus.Logger, _ int, _ []netip.Prefix, _ []netip.Prefix) (*tun, error) { func newTunFromFd(_ *config.C, _ *logrus.Logger, _ int, _ []netip.Prefix) (*tun, error) {
return nil, fmt.Errorf("newTunFromFd not supported in Darwin") return nil, fmt.Errorf("newTunFromFd not supported in Darwin")
} }
@@ -216,11 +213,6 @@ func (t *tun) Activate() error {
} }
} }
} }
if t.unsafeIPv4Origin.IsValid() && t.unsafeIPv4Origin.Addr().Is4() {
if err = t.activate4(t.unsafeIPv4Origin); err != nil {
return err
}
}
// Run the interface // Run the interface
ifrf.Flags = ifrf.Flags | unix.IFF_UP | unix.IFF_RUNNING ifrf.Flags = ifrf.Flags | unix.IFF_UP | unix.IFF_RUNNING
@@ -322,10 +314,6 @@ func (t *tun) reload(c *config.C, initial bool) error {
return nil return nil
} }
if initial {
t.unsafeIPv4Origin = prepareUnsafeOriginAddr(t, t.l, c, routes)
}
routeTree, err := makeRouteTree(t.l, routes, false) routeTree, err := makeRouteTree(t.l, routes, false)
if err != nil { if err != nil {
return err return err
@@ -557,18 +545,6 @@ func (t *tun) Networks() []netip.Prefix {
return t.vpnNetworks return t.vpnNetworks
} }
func (t *tun) UnsafeNetworks() []netip.Prefix {
return t.unsafeNetworks
}
func (t *tun) UnsafeIPv4OriginAddress() netip.Prefix {
return t.unsafeIPv4Origin
}
func (t *tun) SNATAddress() netip.Prefix {
return netip.Prefix{}
}
func (t *tun) Name() string { func (t *tun) Name() string {
return t.Device return t.Device
} }
+30 -16
View File
@@ -8,6 +8,7 @@ import (
"github.com/rcrowley/go-metrics" "github.com/rcrowley/go-metrics"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/slackhq/nebula/firewall"
"github.com/slackhq/nebula/iputil" "github.com/slackhq/nebula/iputil"
"github.com/slackhq/nebula/routing" "github.com/slackhq/nebula/routing"
) )
@@ -52,17 +53,6 @@ func (t *disabledTun) Networks() []netip.Prefix {
return t.vpnNetworks return t.vpnNetworks
} }
func (*disabledTun) UnsafeNetworks() []netip.Prefix {
return nil
}
func (*disabledTun) SNATAddress() netip.Prefix {
return netip.Prefix{}
}
func (*disabledTun) UnsafeIPv4OriginAddress() netip.Prefix {
return netip.Prefix{}
}
func (*disabledTun) Name() string { func (*disabledTun) Name() string {
return "disabled" return "disabled"
} }
@@ -98,21 +88,45 @@ func (t *disabledTun) handleICMPEchoRequest(b []byte) bool {
default: default:
t.l.Debugf("tun_disabled: dropped ICMP Echo Reply response") t.l.Debugf("tun_disabled: dropped ICMP Echo Reply response")
} }
if t.l.Level >= logrus.DebugLevel {
t.l.WithField("raw", prettyPacket(b)).Debugf("Disabled tun responded to ICMP Echo Request")
}
return true return true
} }
func (t *disabledTun) handleOtherPackets(b []byte) error {
fp := &firewall.Packet{}
err := firewall.NewPacket(b, true, fp)
if err != nil {
return err
}
out := make([]byte, len(b)) //todo do something!
// attempt to write it, but don't block
select {
case t.read <- out:
default:
t.l.Debugf("tun_disabled: dropped reply")
}
return err
}
func (t *disabledTun) Write(b []byte) (int, error) { func (t *disabledTun) Write(b []byte) (int, error) {
t.rx.Inc(1) t.rx.Inc(1)
// Check for ICMP Echo Request before spending time doing the full parsing
if t.handleICMPEchoRequest(b) { if t.handleICMPEchoRequest(b) {
return len(b), nil
}
if err := t.handleOtherPackets(b); err != nil {
if t.l.Level >= logrus.DebugLevel { if t.l.Level >= logrus.DebugLevel {
t.l.WithField("raw", prettyPacket(b)).Debugf("Disabled tun responded to ICMP Echo Request") t.l.WithField("raw", prettyPacket(b)).WithError(err).Debugf("Disabled tun failed to respond")
} }
} else if t.l.Level >= logrus.DebugLevel { return len(b), nil
}
if t.l.Level >= logrus.DebugLevel {
t.l.WithField("raw", prettyPacket(b)).Debugf("Disabled tun received unexpected payload") t.l.WithField("raw", prettyPacket(b)).Debugf("Disabled tun received unexpected payload")
} }
return len(b), nil return len(b), nil
} }
+15 -34
View File
@@ -86,16 +86,14 @@ type ifreqAlias6 struct {
} }
type tun struct { type tun struct {
Device string Device string
vpnNetworks []netip.Prefix vpnNetworks []netip.Prefix
unsafeNetworks []netip.Prefix MTU int
unsafeIPv4Origin netip.Prefix Routes atomic.Pointer[[]Route]
MTU int routeTree atomic.Pointer[bart.Table[routing.Gateways]]
Routes atomic.Pointer[[]Route] linkAddr *netroute.LinkAddr
routeTree atomic.Pointer[bart.Table[routing.Gateways]] l *logrus.Logger
linkAddr *netroute.LinkAddr devFd int
l *logrus.Logger
devFd int
} }
func (t *tun) Read(to []byte) (int, error) { func (t *tun) Read(to []byte) (int, error) {
@@ -201,11 +199,11 @@ func (t *tun) Close() error {
return nil return nil
} }
func newTunFromFd(_ *config.C, _ *logrus.Logger, _ int, _ []netip.Prefix, _ []netip.Prefix) (*tun, error) { func newTunFromFd(_ *config.C, _ *logrus.Logger, _ int, _ []netip.Prefix) (*tun, error) {
return nil, fmt.Errorf("newTunFromFd not supported in FreeBSD") return nil, fmt.Errorf("newTunFromFd not supported in FreeBSD")
} }
func newTun(c *config.C, l *logrus.Logger, vpnNetworks []netip.Prefix, unsafeNetworks []netip.Prefix, _ bool) (*tun, error) { func newTun(c *config.C, l *logrus.Logger, vpnNetworks []netip.Prefix, _ bool) (*tun, error) {
// Try to open existing tun device // Try to open existing tun device
var fd int var fd int
var err error var err error
@@ -272,12 +270,11 @@ func newTun(c *config.C, l *logrus.Logger, vpnNetworks []netip.Prefix, unsafeNet
} }
t := &tun{ t := &tun{
Device: deviceName, Device: deviceName,
vpnNetworks: vpnNetworks, vpnNetworks: vpnNetworks,
unsafeNetworks: unsafeNetworks, MTU: c.GetInt("tun.mtu", DefaultMTU),
MTU: c.GetInt("tun.mtu", DefaultMTU), l: l,
l: l, devFd: fd,
devFd: fd,
} }
err = t.reload(c, true) err = t.reload(c, true)
@@ -413,10 +410,6 @@ func (t *tun) reload(c *config.C, initial bool) error {
return nil return nil
} }
if initial {
t.unsafeIPv4Origin = prepareUnsafeOriginAddr(t, t.l, c, routes)
}
routeTree, err := makeRouteTree(t.l, routes, false) routeTree, err := makeRouteTree(t.l, routes, false)
if err != nil { if err != nil {
return err return err
@@ -453,18 +446,6 @@ func (t *tun) Networks() []netip.Prefix {
return t.vpnNetworks return t.vpnNetworks
} }
func (t *tun) UnsafeNetworks() []netip.Prefix {
return t.unsafeNetworks
}
func (t *tun) UnsafeIPv4OriginAddress() netip.Prefix {
return t.unsafeIPv4Origin
}
func (t *tun) SNATAddress() netip.Prefix {
return netip.Prefix{}
}
func (t *tun) Name() string { func (t *tun) Name() string {
return t.Device return t.Device
} }
+6 -23
View File
@@ -22,23 +22,20 @@ import (
type tun struct { type tun struct {
io.ReadWriteCloser io.ReadWriteCloser
vpnNetworks []netip.Prefix vpnNetworks []netip.Prefix
unsafeNetworks []netip.Prefix Routes atomic.Pointer[[]Route]
unsafeIPv4Origin netip.Prefix routeTree atomic.Pointer[bart.Table[routing.Gateways]]
Routes atomic.Pointer[[]Route] l *logrus.Logger
routeTree atomic.Pointer[bart.Table[routing.Gateways]]
l *logrus.Logger
} }
func newTun(_ *config.C, _ *logrus.Logger, _ []netip.Prefix, _ []netip.Prefix, _ bool) (*tun, error) { func newTun(_ *config.C, _ *logrus.Logger, _ []netip.Prefix, _ bool) (*tun, error) {
return nil, fmt.Errorf("newTun not supported in iOS") return nil, fmt.Errorf("newTun not supported in iOS")
} }
func newTunFromFd(c *config.C, l *logrus.Logger, deviceFd int, vpnNetworks []netip.Prefix, unsafeNetworks []netip.Prefix) (*tun, error) { func newTunFromFd(c *config.C, l *logrus.Logger, deviceFd int, vpnNetworks []netip.Prefix) (*tun, error) {
file := os.NewFile(uintptr(deviceFd), "/dev/tun") file := os.NewFile(uintptr(deviceFd), "/dev/tun")
t := &tun{ t := &tun{
vpnNetworks: vpnNetworks, vpnNetworks: vpnNetworks,
unsafeNetworks: unsafeNetworks,
ReadWriteCloser: &tunReadCloser{f: file}, ReadWriteCloser: &tunReadCloser{f: file},
l: l, l: l,
} }
@@ -72,8 +69,6 @@ func (t *tun) reload(c *config.C, initial bool) error {
return nil return nil
} }
t.unsafeIPv4Origin = prepareUnsafeOriginAddr(t, t.l, c, routes)
routeTree, err := makeRouteTree(t.l, routes, false) routeTree, err := makeRouteTree(t.l, routes, false)
if err != nil { if err != nil {
return err return err
@@ -152,18 +147,6 @@ func (t *tun) Networks() []netip.Prefix {
return t.vpnNetworks return t.vpnNetworks
} }
func (t *tun) UnsafeNetworks() []netip.Prefix {
return t.unsafeNetworks
}
func (t *tun) UnsafeIPv4OriginAddress() netip.Prefix {
return t.unsafeIPv4Origin
}
func (t *tun) SNATAddress() netip.Prefix {
return netip.Prefix{}
}
func (t *tun) Name() string { func (t *tun) Name() string {
return "iOS" return "iOS"
} }
+14 -77
View File
@@ -26,15 +26,14 @@ import (
type tun struct { type tun struct {
io.ReadWriteCloser io.ReadWriteCloser
fd int fd int
Device string Device string
vpnNetworks []netip.Prefix vpnNetworks []netip.Prefix
unsafeNetworks []netip.Prefix MaxMTU int
MaxMTU int DefaultMTU int
DefaultMTU int TXQueueLen int
TXQueueLen int deviceIndex int
deviceIndex int ioctlFd uintptr
ioctlFd uintptr
Routes atomic.Pointer[[]Route] Routes atomic.Pointer[[]Route]
routeTree atomic.Pointer[bart.Table[routing.Gateways]] routeTree atomic.Pointer[bart.Table[routing.Gateways]]
@@ -47,9 +46,6 @@ type tun struct {
routesFromSystem map[netip.Prefix]routing.Gateways routesFromSystem map[netip.Prefix]routing.Gateways
routesFromSystemLock sync.Mutex routesFromSystemLock sync.Mutex
snatAddr netip.Prefix
unsafeIPv4Origin netip.Prefix
l *logrus.Logger l *logrus.Logger
} }
@@ -57,18 +53,6 @@ func (t *tun) Networks() []netip.Prefix {
return t.vpnNetworks return t.vpnNetworks
} }
func (t *tun) UnsafeNetworks() []netip.Prefix {
return t.unsafeNetworks
}
func (t *tun) UnsafeIPv4OriginAddress() netip.Prefix {
return t.unsafeIPv4Origin
}
func (t *tun) SNATAddress() netip.Prefix {
return t.snatAddr
}
type ifReq struct { type ifReq struct {
Name [16]byte Name [16]byte
Flags uint16 Flags uint16
@@ -87,10 +71,10 @@ type ifreqQLEN struct {
pad [8]byte pad [8]byte
} }
func newTunFromFd(c *config.C, l *logrus.Logger, deviceFd int, vpnNetworks []netip.Prefix, unsafeNetworks []netip.Prefix) (*tun, error) { func newTunFromFd(c *config.C, l *logrus.Logger, deviceFd int, vpnNetworks []netip.Prefix) (*tun, error) {
file := os.NewFile(uintptr(deviceFd), "/dev/net/tun") file := os.NewFile(uintptr(deviceFd), "/dev/net/tun")
t, err := newTunGeneric(c, l, file, vpnNetworks, unsafeNetworks) t, err := newTunGeneric(c, l, file, vpnNetworks)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -100,7 +84,7 @@ func newTunFromFd(c *config.C, l *logrus.Logger, deviceFd int, vpnNetworks []net
return t, nil return t, nil
} }
func newTun(c *config.C, l *logrus.Logger, vpnNetworks []netip.Prefix, unsafeNetworks []netip.Prefix, multiqueue bool) (*tun, error) { func newTun(c *config.C, l *logrus.Logger, vpnNetworks []netip.Prefix, multiqueue bool) (*tun, error) {
fd, err := unix.Open("/dev/net/tun", os.O_RDWR, 0) fd, err := unix.Open("/dev/net/tun", os.O_RDWR, 0)
if err != nil { if err != nil {
// If /dev/net/tun doesn't exist, try to create it (will happen in docker) // If /dev/net/tun doesn't exist, try to create it (will happen in docker)
@@ -139,7 +123,7 @@ func newTun(c *config.C, l *logrus.Logger, vpnNetworks []netip.Prefix, unsafeNet
name := strings.Trim(string(req.Name[:]), "\x00") name := strings.Trim(string(req.Name[:]), "\x00")
file := os.NewFile(uintptr(fd), "/dev/net/tun") file := os.NewFile(uintptr(fd), "/dev/net/tun")
t, err := newTunGeneric(c, l, file, vpnNetworks, unsafeNetworks) t, err := newTunGeneric(c, l, file, vpnNetworks)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -149,12 +133,11 @@ func newTun(c *config.C, l *logrus.Logger, vpnNetworks []netip.Prefix, unsafeNet
return t, nil return t, nil
} }
func newTunGeneric(c *config.C, l *logrus.Logger, file *os.File, vpnNetworks []netip.Prefix, unsafeNetworks []netip.Prefix) (*tun, error) { func newTunGeneric(c *config.C, l *logrus.Logger, file *os.File, vpnNetworks []netip.Prefix) (*tun, error) {
t := &tun{ t := &tun{
ReadWriteCloser: file, ReadWriteCloser: file,
fd: int(file.Fd()), fd: int(file.Fd()),
vpnNetworks: vpnNetworks, vpnNetworks: vpnNetworks,
unsafeNetworks: unsafeNetworks,
TXQueueLen: c.GetInt("tun.tx_queue", 500), TXQueueLen: c.GetInt("tun.tx_queue", 500),
useSystemRoutes: c.GetBool("tun.use_system_route_table", false), useSystemRoutes: c.GetBool("tun.use_system_route_table", false),
useSystemRoutesBufferSize: c.GetInt("tun.use_system_route_table_buffer_size", 0), useSystemRoutesBufferSize: c.GetInt("tun.use_system_route_table_buffer_size", 0),
@@ -187,11 +170,6 @@ func (t *tun) reload(c *config.C, initial bool) error {
return nil return nil
} }
if initial {
t.unsafeIPv4Origin = prepareUnsafeOriginAddr(t, t.l, c, routes) //todo MUST be different from t.snatAddr!
t.snatAddr = prepareSnatAddr(t, t.l, c)
}
routeTree, err := makeRouteTree(t.l, routes, true) routeTree, err := makeRouteTree(t.l, routes, true)
if err != nil { if err != nil {
return err return err
@@ -335,17 +313,6 @@ func (t *tun) addIPs(link netlink.Link) error {
} }
} }
if t.unsafeIPv4Origin.IsValid() {
newAddrs = append(newAddrs, &netlink.Addr{
IPNet: &net.IPNet{
IP: t.unsafeIPv4Origin.Addr().AsSlice(),
Mask: net.CIDRMask(t.unsafeIPv4Origin.Bits(), t.unsafeIPv4Origin.Addr().BitLen()),
},
Label: t.unsafeIPv4Origin.Addr().Zone(),
})
t.l.WithField("address", t.unsafeIPv4Origin).Info("Adding origin address for IPv4 unsafe_routes")
}
//add all new addresses //add all new addresses
for i := range newAddrs { for i := range newAddrs {
//AddrReplace still adds new IPs, but if their properties change it will change them as well //AddrReplace still adds new IPs, but if their properties change it will change them as well
@@ -433,13 +400,7 @@ func (t *tun) Activate() error {
//set route MTU //set route MTU
for i := range t.vpnNetworks { for i := range t.vpnNetworks {
if err = t.setDefaultRoute(t.vpnNetworks[i]); err != nil { if err = t.setDefaultRoute(t.vpnNetworks[i]); err != nil {
return fmt.Errorf("failed to set default route MTU for %s: %w", t.vpnNetworks[i], err) return fmt.Errorf("failed to set default route MTU: %w", err)
}
}
if t.unsafeIPv4Origin.IsValid() {
if err = t.setDefaultRoute(t.unsafeIPv4Origin); err != nil {
return fmt.Errorf("failed to set default route MTU for %s: %w", t.unsafeIPv4Origin, err)
} }
} }
@@ -466,23 +427,6 @@ func (t *tun) setMTU() {
} }
} }
func (t *tun) setSnatRoute() error {
dr := &net.IPNet{
IP: t.snatAddr.Masked().Addr().AsSlice(),
Mask: net.CIDRMask(t.snatAddr.Bits(), t.snatAddr.Addr().BitLen()),
}
nr := netlink.Route{
LinkIndex: t.deviceIndex,
Dst: dr,
Scope: unix.RT_SCOPE_LINK,
//Protocol: unix.RTPROT_KERNEL,
Table: unix.RT_TABLE_MAIN,
Type: unix.RTN_UNICAST,
}
return netlink.RouteReplace(&nr)
}
func (t *tun) setDefaultRoute(cidr netip.Prefix) error { func (t *tun) setDefaultRoute(cidr netip.Prefix) error {
dr := &net.IPNet{ dr := &net.IPNet{
IP: cidr.Masked().Addr().AsSlice(), IP: cidr.Masked().Addr().AsSlice(),
@@ -559,13 +503,6 @@ func (t *tun) addRoutes(logErrors bool) error {
} }
} }
if t.snatAddr.IsValid() {
//at least for Linux, we need to set a return route for the SNATted traffic in order to satisfy the reverse-path filter,
//and to help the kernel deliver our reply traffic to the tun device.
//however, it is important that we do not actually /assign/ the SNAT address,
//since link-local addresses will not be routed between interfaces without significant trickery.
return t.setSnatRoute()
}
return nil return nil
} }
+10 -28
View File
@@ -58,25 +58,23 @@ type addrLifetime struct {
} }
type tun struct { type tun struct {
Device string Device string
vpnNetworks []netip.Prefix vpnNetworks []netip.Prefix
unsafeNetworks []netip.Prefix MTU int
unsafeIPv4Origin netip.Prefix Routes atomic.Pointer[[]Route]
MTU int routeTree atomic.Pointer[bart.Table[routing.Gateways]]
Routes atomic.Pointer[[]Route] l *logrus.Logger
routeTree atomic.Pointer[bart.Table[routing.Gateways]] f *os.File
l *logrus.Logger fd int
f *os.File
fd int
} }
var deviceNameRE = regexp.MustCompile(`^tun[0-9]+$`) var deviceNameRE = regexp.MustCompile(`^tun[0-9]+$`)
func newTunFromFd(_ *config.C, _ *logrus.Logger, _ int, _ []netip.Prefix, _ []netip.Prefix) (*tun, error) { func newTunFromFd(_ *config.C, _ *logrus.Logger, _ int, _ []netip.Prefix) (*tun, error) {
return nil, fmt.Errorf("newTunFromFd not supported in NetBSD") return nil, fmt.Errorf("newTunFromFd not supported in NetBSD")
} }
func newTun(c *config.C, l *logrus.Logger, vpnNetworks []netip.Prefix, unsafeNetworks []netip.Prefix, _ bool) (*tun, error) { func newTun(c *config.C, l *logrus.Logger, vpnNetworks []netip.Prefix, _ bool) (*tun, error) {
// Try to open tun device // Try to open tun device
var err error var err error
deviceName := c.GetString("tun.dev", "") deviceName := c.GetString("tun.dev", "")
@@ -352,10 +350,6 @@ func (t *tun) reload(c *config.C, initial bool) error {
return nil return nil
} }
if initial {
t.unsafeIPv4Origin = prepareUnsafeOriginAddr(t, t.l, c, routes)
}
routeTree, err := makeRouteTree(t.l, routes, false) routeTree, err := makeRouteTree(t.l, routes, false)
if err != nil { if err != nil {
return err return err
@@ -392,18 +386,6 @@ func (t *tun) Networks() []netip.Prefix {
return t.vpnNetworks return t.vpnNetworks
} }
func (t *tun) UnsafeNetworks() []netip.Prefix {
return t.unsafeNetworks
}
func (t *tun) UnsafeIPv4OriginAddress() netip.Prefix {
return t.unsafeIPv4Origin
}
func (t *tun) SNATAddress() netip.Prefix {
return netip.Prefix{}
}
func (t *tun) Name() string { func (t *tun) Name() string {
return t.Device return t.Device
} }
+16 -35
View File
@@ -49,27 +49,25 @@ type ifreq struct {
} }
type tun struct { type tun struct {
Device string Device string
vpnNetworks []netip.Prefix vpnNetworks []netip.Prefix
unsafeNetworks []netip.Prefix MTU int
unsafeIPv4Origin netip.Prefix Routes atomic.Pointer[[]Route]
MTU int routeTree atomic.Pointer[bart.Table[routing.Gateways]]
Routes atomic.Pointer[[]Route] l *logrus.Logger
routeTree atomic.Pointer[bart.Table[routing.Gateways]] f *os.File
l *logrus.Logger fd int
f *os.File
fd int
// cache out buffer since we need to prepend 4 bytes for tun metadata // cache out buffer since we need to prepend 4 bytes for tun metadata
out []byte out []byte
} }
var deviceNameRE = regexp.MustCompile(`^tun[0-9]+$`) var deviceNameRE = regexp.MustCompile(`^tun[0-9]+$`)
func newTunFromFd(_ *config.C, _ *logrus.Logger, _ int, _ []netip.Prefix, _ []netip.Prefix) (*tun, error) { func newTunFromFd(_ *config.C, _ *logrus.Logger, _ int, _ []netip.Prefix) (*tun, error) {
return nil, fmt.Errorf("newTunFromFd not supported in openbsd") return nil, fmt.Errorf("newTunFromFd not supported in openbsd")
} }
func newTun(c *config.C, l *logrus.Logger, vpnNetworks []netip.Prefix, unsafeNetworks []netip.Prefix, _ bool) (*tun, error) { func newTun(c *config.C, l *logrus.Logger, vpnNetworks []netip.Prefix, _ bool) (*tun, error) {
// Try to open tun device // Try to open tun device
var err error var err error
deviceName := c.GetString("tun.dev", "") deviceName := c.GetString("tun.dev", "")
@@ -91,13 +89,12 @@ func newTun(c *config.C, l *logrus.Logger, vpnNetworks []netip.Prefix, unsafeNet
} }
t := &tun{ t := &tun{
f: os.NewFile(uintptr(fd), ""), f: os.NewFile(uintptr(fd), ""),
fd: fd, fd: fd,
Device: deviceName, Device: deviceName,
vpnNetworks: vpnNetworks, vpnNetworks: vpnNetworks,
unsafeNetworks: unsafeNetworks, MTU: c.GetInt("tun.mtu", DefaultMTU),
MTU: c.GetInt("tun.mtu", DefaultMTU), l: l,
l: l,
} }
err = t.reload(c, true) err = t.reload(c, true)
@@ -273,10 +270,6 @@ func (t *tun) reload(c *config.C, initial bool) error {
return nil return nil
} }
if initial {
t.unsafeIPv4Origin = prepareUnsafeOriginAddr(t, t.l, c, routes)
}
routeTree, err := makeRouteTree(t.l, routes, false) routeTree, err := makeRouteTree(t.l, routes, false)
if err != nil { if err != nil {
return err return err
@@ -313,18 +306,6 @@ func (t *tun) Networks() []netip.Prefix {
return t.vpnNetworks return t.vpnNetworks
} }
func (t *tun) UnsafeNetworks() []netip.Prefix {
return t.unsafeNetworks
}
func (t *tun) UnsafeIPv4OriginAddress() netip.Prefix {
return t.unsafeIPv4Origin
}
func (t *tun) SNATAddress() netip.Prefix {
return netip.Prefix{}
}
func (t *tun) Name() string { func (t *tun) Name() string {
return t.Device return t.Device
} }
-179
View File
@@ -1,179 +0,0 @@
package overlay
import (
"io"
"net/netip"
"testing"
"github.com/sirupsen/logrus"
"github.com/slackhq/nebula/config"
"github.com/slackhq/nebula/routing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// mockDevice is a minimal Device implementation for testing prepareUnsafeOriginAddr.
type mockDevice struct {
networks []netip.Prefix
unsafeNetworks []netip.Prefix
snatAddr netip.Prefix
unsafeSnatAddr netip.Prefix
}
func (d *mockDevice) Read([]byte) (int, error) { return 0, nil }
func (d *mockDevice) Write([]byte) (int, error) { return 0, nil }
func (d *mockDevice) Close() error { return nil }
func (d *mockDevice) Activate() error { return nil }
func (d *mockDevice) Networks() []netip.Prefix { return d.networks }
func (d *mockDevice) UnsafeNetworks() []netip.Prefix { return d.unsafeNetworks }
func (d *mockDevice) SNATAddress() netip.Prefix { return d.snatAddr }
func (d *mockDevice) UnsafeIPv4OriginAddress() netip.Prefix { return d.unsafeSnatAddr }
func (d *mockDevice) Name() string { return "mock" }
func (d *mockDevice) RoutesFor(netip.Addr) routing.Gateways { return routing.Gateways{} }
func (d *mockDevice) SupportsMultiqueue() bool { return false }
func (d *mockDevice) NewMultiQueueReader() (io.ReadWriteCloser, error) { return nil, nil }
func TestPrepareSnatAddr_V4Primary_NoSnat(t *testing.T) {
l := logrus.New()
l.SetLevel(logrus.PanicLevel)
c := config.NewC(l)
// If the device has an IPv4 primary address, no SNAT needed
d := &mockDevice{
networks: []netip.Prefix{netip.MustParsePrefix("10.0.0.1/24")},
}
result := prepareUnsafeOriginAddr(d, l, c, nil)
assert.Equal(t, netip.Prefix{}, result, "should not assign SNAT addr when device has IPv4 primary")
}
func TestPrepareSnatAddr_V6Primary_NoUnsafeOrRoutes(t *testing.T) {
l := logrus.New()
l.SetLevel(logrus.PanicLevel)
c := config.NewC(l)
// IPv6 primary but no unsafe networks or IPv4 routes
d := &mockDevice{
networks: []netip.Prefix{netip.MustParsePrefix("fd00::1/128")},
}
result := prepareUnsafeOriginAddr(d, l, c, nil)
assert.Equal(t, netip.Prefix{}, result, "should not assign SNAT addr without IPv4 unsafe networks or routes")
}
func TestPrepareSnatAddr_V6Primary_WithV4Unsafe(t *testing.T) {
l := logrus.New()
l.SetLevel(logrus.PanicLevel)
c := config.NewC(l)
// IPv6 primary with IPv4 unsafe network -> should get SNAT addr
d := &mockDevice{
networks: []netip.Prefix{netip.MustParsePrefix("fd00::1/128")},
unsafeNetworks: []netip.Prefix{netip.MustParsePrefix("192.168.0.0/16")},
}
result := prepareSnatAddr(d, l, c)
require.True(t, result.IsValid(), "should assign SNAT addr")
assert.True(t, result.Addr().Is4(), "SNAT addr should be IPv4")
assert.True(t, result.Addr().IsLinkLocalUnicast(), "SNAT addr should be link-local")
assert.Equal(t, 32, result.Bits(), "SNAT addr should be /32")
result = prepareUnsafeOriginAddr(d, l, c, nil)
require.False(t, result.IsValid(), "no routes = no origin addr needed")
}
func TestPrepareUnsafeOriginAddr_V6Primary_WithV4Route(t *testing.T) {
l := logrus.New()
l.SetLevel(logrus.PanicLevel)
c := config.NewC(l)
// IPv6 primary with IPv4 route -> should get SNAT addr
d := &mockDevice{
networks: []netip.Prefix{netip.MustParsePrefix("fd00::1/128")},
}
routes := []Route{
{Cidr: netip.MustParsePrefix("10.0.0.0/8")},
}
result := prepareUnsafeOriginAddr(d, l, c, routes)
require.True(t, result.IsValid(), "should assign SNAT addr when IPv4 route exists")
assert.True(t, result.Addr().Is4())
assert.True(t, result.Addr().IsLinkLocalUnicast())
result = prepareSnatAddr(d, l, c)
require.False(t, result.IsValid(), "no UnsafeNetworks = no snat addr needed")
}
func TestPrepareSnatAddr_V6Primary_V6UnsafeOnly(t *testing.T) {
l := logrus.New()
l.SetLevel(logrus.PanicLevel)
c := config.NewC(l)
// IPv6 primary with only IPv6 unsafe network -> no SNAT needed
d := &mockDevice{
networks: []netip.Prefix{netip.MustParsePrefix("fd00::1/128")},
unsafeNetworks: []netip.Prefix{netip.MustParsePrefix("fd01::/64")},
}
result := prepareUnsafeOriginAddr(d, l, c, nil)
assert.Equal(t, netip.Prefix{}, result, "should not assign SNAT addr for IPv6-only unsafe networks")
}
func TestPrepareSnatAddr_ManualAddress(t *testing.T) {
l := logrus.New()
l.SetLevel(logrus.PanicLevel)
c := config.NewC(l)
c.Settings["tun"] = map[string]any{
"snat_address_for_4over6": "169.254.42.42",
}
d := &mockDevice{
networks: []netip.Prefix{netip.MustParsePrefix("fd00::1/128")},
unsafeNetworks: []netip.Prefix{netip.MustParsePrefix("192.168.0.0/16")},
}
result := prepareSnatAddr(d, l, c)
require.True(t, result.IsValid())
assert.Equal(t, netip.MustParseAddr("169.254.42.42"), result.Addr())
assert.Equal(t, 32, result.Bits())
}
func TestPrepareSnatAddr_InvalidManualAddress_Fallback(t *testing.T) {
l := logrus.New()
l.SetLevel(logrus.PanicLevel)
c := config.NewC(l)
c.Settings["tun"] = map[string]any{
"snat_address_for_4over6": "not-an-ip",
}
d := &mockDevice{
networks: []netip.Prefix{netip.MustParsePrefix("fd00::1/128")},
unsafeNetworks: []netip.Prefix{netip.MustParsePrefix("192.168.0.0/16")},
}
result := prepareSnatAddr(d, l, c)
// Should fall back to auto-assignment
require.True(t, result.IsValid(), "should fall back to auto-assigned address")
assert.True(t, result.Addr().Is4())
assert.True(t, result.Addr().IsLinkLocalUnicast())
}
func TestPrepareSnatAddr_AutoGenerated_Range(t *testing.T) {
l := logrus.New()
l.SetLevel(logrus.PanicLevel)
c := config.NewC(l)
d := &mockDevice{
networks: []netip.Prefix{netip.MustParsePrefix("fd00::1/128")},
unsafeNetworks: []netip.Prefix{netip.MustParsePrefix("192.168.0.0/16")},
}
// Generate several addresses and verify they're all in the expected range
for i := 0; i < 100; i++ {
result := prepareSnatAddr(d, l, c)
require.True(t, result.IsValid())
addr := result.Addr()
octets := addr.As4()
assert.Equal(t, byte(169), octets[0], "first octet should be 169")
assert.Equal(t, byte(254), octets[1], "second octet should be 254")
// Should not have .0 in the last octet
assert.NotEqual(t, byte(0), octets[3], "last octet should not be 0")
// Should not be 169.254.255.255 (broadcast)
if octets[2] == 255 {
assert.NotEqual(t, byte(255), octets[3], "should not be broadcast address")
}
}
}
-37
View File
@@ -1,37 +0,0 @@
package overlay
import (
"bytes"
"net/netip"
"testing"
"github.com/stretchr/testify/assert"
)
func TestLinkLocal(t *testing.T) {
r := bytes.NewReader([]byte{42, 99})
result := genLinkLocal(r)
assert.Equal(t, netip.MustParsePrefix("169.254.42.99/32"), result, "genLinkLocal with a deterministic randomizer")
result = genLinkLocal(nil)
assert.True(t, result.IsValid(), "genLinkLocal with nil randomizer should be valid")
assert.True(t, result.Addr().IsLinkLocalUnicast(), "genLinkLocal with nil randomizer should be link-local")
result = coerceLinkLocal([]byte{169, 254, 100, 50})
assert.Equal(t, netip.MustParsePrefix("169.254.100.50/32"), result, "coerceLinkLocal should pass through normal values")
result = coerceLinkLocal([]byte{169, 254, 0, 0})
assert.Equal(t, netip.MustParsePrefix("169.254.0.1/32"), result, "coerceLinkLocal should bump .0 last octet to .1")
result = coerceLinkLocal([]byte{169, 254, 255, 255})
assert.Equal(t, netip.MustParsePrefix("169.254.255.254/32"), result, "coerceLinkLocal should bump broadcast 255.255 to 255.254")
result = coerceLinkLocal([]byte{169, 254, 0, 1})
assert.Equal(t, netip.MustParsePrefix("169.254.0.1/32"), result, "coerceLinkLocal should leave .1 last octet unchanged")
result = coerceLinkLocal([]byte{169, 254, 255, 254})
assert.Equal(t, netip.MustParsePrefix("169.254.255.254/32"), result, "coerceLinkLocal should leave 255.254 unchanged")
result = coerceLinkLocal([]byte{169, 254, 255, 100})
assert.Equal(t, netip.MustParsePrefix("169.254.255.100/32"), result, "coerceLinkLocal should leave 255.100 unchanged")
}
+16 -35
View File
@@ -17,21 +17,18 @@ import (
) )
type TestTun struct { type TestTun struct {
Device string Device string
vpnNetworks []netip.Prefix vpnNetworks []netip.Prefix
unsafeNetworks []netip.Prefix Routes []Route
snatAddr netip.Prefix routeTree *bart.Table[routing.Gateways]
unsafeIPv4Origin netip.Prefix l *logrus.Logger
Routes []Route
routeTree *bart.Table[routing.Gateways]
l *logrus.Logger
closed atomic.Bool closed atomic.Bool
rxPackets chan []byte // Packets to receive into nebula rxPackets chan []byte // Packets to receive into nebula
TxPackets chan []byte // Packets transmitted outside by nebula TxPackets chan []byte // Packets transmitted outside by nebula
} }
func newTun(c *config.C, l *logrus.Logger, vpnNetworks []netip.Prefix, unsafeNetworks []netip.Prefix, _ bool) (*TestTun, error) { func newTun(c *config.C, l *logrus.Logger, vpnNetworks []netip.Prefix, _ bool) (*TestTun, error) {
_, routes, err := getAllRoutesFromConfig(c, vpnNetworks, true) _, routes, err := getAllRoutesFromConfig(c, vpnNetworks, true)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -41,22 +38,18 @@ func newTun(c *config.C, l *logrus.Logger, vpnNetworks []netip.Prefix, unsafeNet
return nil, err return nil, err
} }
tt := &TestTun{ return &TestTun{
Device: c.GetString("tun.dev", ""), Device: c.GetString("tun.dev", ""),
vpnNetworks: vpnNetworks, vpnNetworks: vpnNetworks,
unsafeNetworks: unsafeNetworks, Routes: routes,
Routes: routes, routeTree: routeTree,
routeTree: routeTree, l: l,
l: l, rxPackets: make(chan []byte, 10),
rxPackets: make(chan []byte, 10), TxPackets: make(chan []byte, 10),
TxPackets: make(chan []byte, 10), }, nil
}
tt.unsafeIPv4Origin = prepareUnsafeOriginAddr(tt, l, c, routes)
tt.snatAddr = prepareSnatAddr(tt, tt.l, c)
return tt, nil
} }
func newTunFromFd(_ *config.C, _ *logrus.Logger, _ int, _ []netip.Prefix, _ []netip.Prefix) (*TestTun, error) { func newTunFromFd(_ *config.C, _ *logrus.Logger, _ int, _ []netip.Prefix) (*TestTun, error) {
return nil, fmt.Errorf("newTunFromFd not supported") return nil, fmt.Errorf("newTunFromFd not supported")
} }
@@ -146,15 +139,3 @@ func (t *TestTun) SupportsMultiqueue() bool {
func (t *TestTun) NewMultiQueueReader() (io.ReadWriteCloser, error) { func (t *TestTun) NewMultiQueueReader() (io.ReadWriteCloser, error) {
return nil, fmt.Errorf("TODO: multiqueue not implemented") return nil, fmt.Errorf("TODO: multiqueue not implemented")
} }
func (t *TestTun) UnsafeNetworks() []netip.Prefix {
return t.unsafeNetworks
}
func (t *TestTun) UnsafeIPv4OriginAddress() netip.Prefix {
return t.unsafeIPv4Origin
}
func (t *TestTun) SNATAddress() netip.Prefix {
return t.snatAddr
}
+13 -37
View File
@@ -28,23 +28,21 @@ import (
const tunGUIDLabel = "Fixed Nebula Windows GUID v1" const tunGUIDLabel = "Fixed Nebula Windows GUID v1"
type winTun struct { type winTun struct {
Device string Device string
vpnNetworks []netip.Prefix vpnNetworks []netip.Prefix
unsafeNetworks []netip.Prefix MTU int
unsafeIPv4Origin netip.Prefix Routes atomic.Pointer[[]Route]
MTU int routeTree atomic.Pointer[bart.Table[routing.Gateways]]
Routes atomic.Pointer[[]Route] l *logrus.Logger
routeTree atomic.Pointer[bart.Table[routing.Gateways]]
l *logrus.Logger
tun *wintun.NativeTun tun *wintun.NativeTun
} }
func newTunFromFd(_ *config.C, _ *logrus.Logger, _ int, _ []netip.Prefix, _ []netip.Prefix) (Device, error) { func newTunFromFd(_ *config.C, _ *logrus.Logger, _ int, _ []netip.Prefix) (Device, error) {
return nil, fmt.Errorf("newTunFromFd not supported in Windows") return nil, fmt.Errorf("newTunFromFd not supported in Windows")
} }
func newTun(c *config.C, l *logrus.Logger, vpnNetworks []netip.Prefix, unsafeNetworks []netip.Prefix, _ bool) (*winTun, error) { func newTun(c *config.C, l *logrus.Logger, vpnNetworks []netip.Prefix, _ bool) (*winTun, error) {
err := checkWinTunExists() err := checkWinTunExists()
if err != nil { if err != nil {
return nil, fmt.Errorf("can not load the wintun driver: %w", err) return nil, fmt.Errorf("can not load the wintun driver: %w", err)
@@ -57,11 +55,10 @@ func newTun(c *config.C, l *logrus.Logger, vpnNetworks []netip.Prefix, unsafeNet
} }
t := &winTun{ t := &winTun{
Device: deviceName, Device: deviceName,
vpnNetworks: vpnNetworks, vpnNetworks: vpnNetworks,
unsafeNetworks: unsafeNetworks, MTU: c.GetInt("tun.mtu", DefaultMTU),
MTU: c.GetInt("tun.mtu", DefaultMTU), l: l,
l: l,
} }
err = t.reload(c, true) err = t.reload(c, true)
@@ -105,10 +102,6 @@ func (t *winTun) reload(c *config.C, initial bool) error {
return nil return nil
} }
if initial {
t.unsafeIPv4Origin = prepareUnsafeOriginAddr(t, t.l, c, routes)
}
routeTree, err := makeRouteTree(t.l, routes, false) routeTree, err := makeRouteTree(t.l, routes, false)
if err != nil { if err != nil {
return err return err
@@ -139,12 +132,7 @@ func (t *winTun) reload(c *config.C, initial bool) error {
func (t *winTun) Activate() error { func (t *winTun) Activate() error {
luid := winipcfg.LUID(t.tun.LUID()) luid := winipcfg.LUID(t.tun.LUID())
prefixes := t.vpnNetworks err := luid.SetIPAddresses(t.vpnNetworks)
if t.unsafeIPv4Origin.IsValid() {
prefixes = append(prefixes, t.unsafeIPv4Origin)
}
err := luid.SetIPAddresses(prefixes)
if err != nil { if err != nil {
return fmt.Errorf("failed to set address: %w", err) return fmt.Errorf("failed to set address: %w", err)
} }
@@ -237,18 +225,6 @@ func (t *winTun) Networks() []netip.Prefix {
return t.vpnNetworks return t.vpnNetworks
} }
func (t *winTun) UnsafeNetworks() []netip.Prefix {
return t.unsafeNetworks
}
func (t *winTun) UnsafeIPv4OriginAddress() netip.Prefix {
return t.unsafeIPv4Origin
}
func (t *winTun) SNATAddress() netip.Prefix {
return netip.Prefix{}
}
func (t *winTun) Name() string { func (t *winTun) Name() string {
return t.Device return t.Device
} }
+1 -12
View File
@@ -9,7 +9,7 @@ import (
"github.com/slackhq/nebula/routing" "github.com/slackhq/nebula/routing"
) )
func NewUserDeviceFromConfig(c *config.C, l *logrus.Logger, vpnNetworks []netip.Prefix, unsafeNetworks []netip.Prefix, routines int) (Device, error) { func NewUserDeviceFromConfig(c *config.C, l *logrus.Logger, vpnNetworks []netip.Prefix, routines int) (Device, error) {
return NewUserDevice(vpnNetworks) return NewUserDevice(vpnNetworks)
} }
@@ -36,17 +36,6 @@ type UserDevice struct {
inboundWriter *io.PipeWriter inboundWriter *io.PipeWriter
} }
func (d *UserDevice) UnsafeNetworks() []netip.Prefix {
return nil
}
func (d *UserDevice) SNATAddress() netip.Prefix {
return netip.Prefix{}
}
func (d *UserDevice) UnsafeIPv4OriginAddress() netip.Prefix {
return netip.Prefix{}
}
func (d *UserDevice) Activate() error { func (d *UserDevice) Activate() error {
return nil return nil
} }
+5 -42
View File
@@ -91,7 +91,7 @@ func (p *PKI) reload(c *config.C, initial bool) error {
} }
func (p *PKI) reloadCerts(c *config.C, initial bool) *util.ContextualError { func (p *PKI) reloadCerts(c *config.C, initial bool) *util.ContextualError {
newState, err := newCertStateFromConfig(c, p.l) newState, err := newCertStateFromConfig(c)
if err != nil { if err != nil {
return util.NewContextualError("Could not load client cert", nil, err) return util.NewContextualError("Could not load client cert", nil, err)
} }
@@ -102,7 +102,7 @@ func (p *PKI) reloadCerts(c *config.C, initial bool) *util.ContextualError {
if currentState.v1Cert == nil { if currentState.v1Cert == nil {
//adding certs is fine, actually. Networks-in-common confirmed in newCertState(). //adding certs is fine, actually. Networks-in-common confirmed in newCertState().
} else { } else {
// did IP in cert change? if so, don't set. If we ever allow this, need to set p.firewallReloadNeeded // did IP in cert change? if so, don't set
if !slices.Equal(currentState.v1Cert.Networks(), newState.v1Cert.Networks()) { if !slices.Equal(currentState.v1Cert.Networks(), newState.v1Cert.Networks()) {
return util.NewContextualError( return util.NewContextualError(
"Networks in new cert was different from old", "Networks in new cert was different from old",
@@ -158,14 +158,6 @@ func (p *PKI) reloadCerts(c *config.C, initial bool) *util.ContextualError {
} }
} }
newUN := newState.GetDefaultCertificate().UnsafeNetworks()
oldUN := currentState.GetDefaultCertificate().UnsafeNetworks()
if !slices.Equal(newUN, oldUN) {
//todo I don't love this, because other clients will see the new assignments and act on them, but we will not be able to.
//I think we need to wire this into the firewall reload.
p.l.WithFields(m{"previous": oldUN, "new": newUN}).Warning("UnsafeNetworks assignments differ. A restart is required in order for this to take effect.")
}
// Cipher cant be hot swapped so just leave it at what it was before // Cipher cant be hot swapped so just leave it at what it was before
newState.cipher = currentState.cipher newState.cipher = currentState.cipher
@@ -268,7 +260,7 @@ func (cs *CertState) MarshalJSON() ([]byte, error) {
return json.Marshal(msg) return json.Marshal(msg)
} }
func newCertStateFromConfig(c *config.C, l *logrus.Logger) (*CertState, error) { func newCertStateFromConfig(c *config.C) (*CertState, error) {
var err error var err error
privPathOrPEM := c.GetString("pki.key", "") privPathOrPEM := c.GetString("pki.key", "")
@@ -352,33 +344,10 @@ func newCertStateFromConfig(c *config.C, l *logrus.Logger) (*CertState, error) {
return nil, fmt.Errorf("unknown pki.initiating_version: %v", rawInitiatingVersion) return nil, fmt.Errorf("unknown pki.initiating_version: %v", rawInitiatingVersion)
} }
return newCertState(l, initiatingVersion, v1, v2, isPkcs11, curve, rawKey) return newCertState(initiatingVersion, v1, v2, isPkcs11, curve, rawKey)
} }
func compareUnsafeNetworksAcrossCertVersions(v1, v2 cert.Certificate) error { func newCertState(dv cert.Version, v1, v2 cert.Certificate, pkcs11backed bool, privateKeyCurve cert.Curve, privateKey []byte) (*CertState, error) {
if v1 == nil || v2 == nil {
return nil //can't be a problem if we don't have one of the kinds of cert
}
v4UnsafeNets := 0
for _, n := range v2.UnsafeNetworks() {
if n.Addr().Is6() {
continue // V1 certs can't have IPv6 unsafe networks
} else {
v4UnsafeNets++
}
if !slices.Contains(v1.UnsafeNetworks(), n) {
return errors.New("UnsafeNetworks mismatch")
}
}
if len(v1.UnsafeNetworks()) != v4UnsafeNets {
return errors.New("UnsafeNetworks mismatch")
}
return nil
}
func newCertState(l *logrus.Logger, dv cert.Version, v1, v2 cert.Certificate, pkcs11backed bool, privateKeyCurve cert.Curve, privateKey []byte) (*CertState, error) {
cs := CertState{ cs := CertState{
privateKey: privateKey, privateKey: privateKey,
pkcs11Backed: pkcs11backed, pkcs11Backed: pkcs11backed,
@@ -401,12 +370,6 @@ func newCertState(l *logrus.Logger, dv cert.Version, v1, v2 cert.Certificate, pk
} }
cs.initiatingVersion = dv cs.initiatingVersion = dv
warn := compareUnsafeNetworksAcrossCertVersions(v1, v2)
if warn != nil {
l.WithFields(m{"UnsafeNetworksV1": v1.UnsafeNetworks(), "UnsafeNetworksV2": v2.UnsafeNetworks()}).
Warning("the IPv4 UnsafeNetworks assigned in the V1 certificate do not match the ones in V2")
}
} }
if v1 != nil { if v1 != nil {
+1 -1
View File
@@ -55,7 +55,7 @@ func (rm *relayManager) setAmRelay(v bool) {
func AddRelay(l *logrus.Logger, relayHostInfo *HostInfo, hm *HostMap, vpnIp netip.Addr, remoteIdx *uint32, relayType int, state int) (uint32, error) { func AddRelay(l *logrus.Logger, relayHostInfo *HostInfo, hm *HostMap, vpnIp netip.Addr, remoteIdx *uint32, relayType int, state int) (uint32, error) {
hm.Lock() hm.Lock()
defer hm.Unlock() defer hm.Unlock()
for range 32 { for i := 0; i < 32; i++ {
index, err := generateIndex(l) index, err := generateIndex(l)
if err != nil { if err != nil {
return 0, err return 0, err
+6 -1
View File
@@ -404,7 +404,12 @@ func (r *RemoteList) Rebuild(preferredRanges []netip.Prefix) {
// unlockedIsBad assumes you have the write lock and checks if the remote matches any entry in the blocked address list // unlockedIsBad assumes you have the write lock and checks if the remote matches any entry in the blocked address list
func (r *RemoteList) unlockedIsBad(remote netip.AddrPort) bool { func (r *RemoteList) unlockedIsBad(remote netip.AddrPort) bool {
return slices.Contains(r.badRemotes, remote) for _, v := range r.badRemotes {
if v == remote {
return true
}
}
return false
} }
// unlockedSetLearnedV4 assumes you have the write lock and sets the current learned address for this owner and marks the // unlockedSetLearnedV4 assumes you have the write lock and sets the current learned address for this owner and marks the
-91
View File
@@ -1,91 +0,0 @@
package nebula
import (
"encoding/binary"
"net/netip"
)
func recalcIPv4Checksum(data []byte, oldSrcIP netip.Addr, newSrcIP netip.Addr) {
oldChecksum := binary.BigEndian.Uint16(data[10:12])
//because of how checksums work, we can re-use this function
checksum := calcNewTransportChecksum(oldChecksum, oldSrcIP, 0, newSrcIP, 0)
binary.BigEndian.PutUint16(data[10:12], checksum)
}
func calcNewTransportChecksum(oldChecksum uint16, oldSrcIP netip.Addr, oldSrcPort uint16, newSrcIP netip.Addr, newSrcPort uint16) uint16 {
oldIP := binary.BigEndian.Uint32(oldSrcIP.AsSlice())
newIP := binary.BigEndian.Uint32(newSrcIP.AsSlice())
// Start with inverted checksum
checksum := uint32(^oldChecksum)
// Subtract old IP (as two 16-bit words)
checksum += uint32(^uint16(oldIP >> 16))
checksum += uint32(^uint16(oldIP & 0xFFFF))
// Subtract old port
checksum += uint32(^oldSrcPort)
// Add new IP (as two 16-bit words)
checksum += uint32(newIP >> 16)
checksum += uint32(newIP & 0xFFFF)
// Add new port
checksum += uint32(newSrcPort)
// Fold carries
for checksum > 0xFFFF {
checksum = (checksum & 0xFFFF) + (checksum >> 16)
}
// Return ones' complement
return ^uint16(checksum)
}
func recalcV4TransportChecksum(offsetInsideHeader int, data []byte, oldSrcIP netip.AddrPort, newSrcIP netip.AddrPort) {
ipHeaderOffset := int(data[0]&0x0F) * 4
offset := ipHeaderOffset + offsetInsideHeader
oldcsum := binary.BigEndian.Uint16(data[offset : offset+2])
checksum := calcNewTransportChecksum(oldcsum, oldSrcIP.Addr(), oldSrcIP.Port(), newSrcIP.Addr(), newSrcIP.Port())
binary.BigEndian.PutUint16(data[offset:offset+2], checksum)
}
func recalcUDPv4Checksum(data []byte, oldSrcIP netip.AddrPort, newSrcIP netip.AddrPort) {
const offsetInsideHeader = 6
recalcV4TransportChecksum(offsetInsideHeader, data, oldSrcIP, newSrcIP)
}
func recalcTCPv4Checksum(data []byte, oldSrcIP netip.AddrPort, newSrcIP netip.AddrPort) {
const offsetInsideHeader = 16
recalcV4TransportChecksum(offsetInsideHeader, data, oldSrcIP, newSrcIP)
}
func calcNewICMPChecksum(oldChecksum uint16, oldCode uint16, newCode uint16, oldID uint16, newID uint16) uint16 {
// Start with inverted checksum
checksum := uint32(^oldChecksum)
// Subtract old stuff
checksum += uint32(^oldCode)
checksum += uint32(^oldID)
// Add new stuff
checksum += uint32(newCode)
checksum += uint32(newID)
// Fold carries
for checksum > 0xFFFF {
checksum = (checksum & 0xFFFF) + (checksum >> 16)
}
// Return ones' complement
return ^uint16(checksum)
}
func recalcICMPv4Checksum(data []byte, oldCode uint16, newCode uint16, oldID uint16, newID uint16) {
const offsetInsideHeader = 2
ipHeaderOffset := int(data[0]&0x0F) * 4
offset := ipHeaderOffset + offsetInsideHeader
oldChecksum := binary.BigEndian.Uint16(data[offset : offset+2])
checksum := calcNewICMPChecksum(oldChecksum, oldCode, newCode, oldID, newID)
binary.BigEndian.PutUint16(data[offset:offset+2], checksum)
}
-1310
View File
File diff suppressed because it is too large Load Diff
+3 -2
View File
@@ -6,7 +6,6 @@ import (
"errors" "errors"
"flag" "flag"
"fmt" "fmt"
"maps"
"net" "net"
"net/netip" "net/netip"
"os" "os"
@@ -832,7 +831,9 @@ func sshPrintRelays(ifce *Interface, fs any, a []string, w sshd.StringWriter) er
relays := map[uint32]*HostInfo{} relays := map[uint32]*HostInfo{}
ifce.hostMap.Lock() ifce.hostMap.Lock()
maps.Copy(relays, ifce.hostMap.Relays) for k, v := range ifce.hostMap.Relays {
relays[k] = v
}
ifce.hostMap.Unlock() ifce.hostMap.Unlock()
type RelayFor struct { type RelayFor struct {
-12
View File
@@ -10,18 +10,6 @@ import (
type NoopTun struct{} type NoopTun struct{}
func (NoopTun) UnsafeNetworks() []netip.Prefix {
return nil
}
func (NoopTun) SNATAddress() netip.Prefix {
return netip.Prefix{}
}
func (NoopTun) UnsafeIPv4OriginAddress() netip.Prefix {
return netip.Prefix{}
}
func (NoopTun) RoutesFor(addr netip.Addr) routing.Gateways { func (NoopTun) RoutesFor(addr netip.Addr) routing.Gateways {
return routing.Gateways{} return routing.Gateways{}
} }
+2 -2
View File
@@ -134,7 +134,7 @@ func TestTimerWheel_Purge(t *testing.T) {
assert.True(t, tw.lastTick.After(lastTick)) assert.True(t, tw.lastTick.After(lastTick))
// Make sure we get all 4 packets back // Make sure we get all 4 packets back
for i := range 4 { for i := 0; i < 4; i++ {
p, has := tw.Purge() p, has := tw.Purge()
assert.True(t, has) assert.True(t, has)
assert.Equal(t, fps[i], p) assert.Equal(t, fps[i], p)
@@ -149,7 +149,7 @@ func TestTimerWheel_Purge(t *testing.T) {
// Make sure we cached the free'd items // Make sure we cached the free'd items
assert.Equal(t, 4, tw.itemsCached) assert.Equal(t, 4, tw.itemsCached)
ci := tw.itemCache ci := tw.itemCache
for range 4 { for i := 0; i < 4; i++ {
assert.NotNil(t, ci) assert.NotNil(t, ci)
ci = ci.Next ci = ci.Next
} }