mirror of
https://github.com/slackhq/nebula.git
synced 2026-04-01 23:25:18 +02:00
401 lines
15 KiB
Go
401 lines
15 KiB
Go
//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()
|
|
}
|