| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400 |
- //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()
- }
|