snat_test.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400
  1. //go:build e2e_testing
  2. // +build e2e_testing
  3. package e2e
  4. import (
  5. "encoding/binary"
  6. "net/netip"
  7. "testing"
  8. "time"
  9. "github.com/slackhq/nebula/cert"
  10. "github.com/slackhq/nebula/cert_test"
  11. "github.com/slackhq/nebula/e2e/router"
  12. "github.com/stretchr/testify/assert"
  13. "github.com/stretchr/testify/require"
  14. )
  15. // parseIPv4UDPPacket extracts source/dest IPs, ports, and payload from an IPv4 UDP packet.
  16. func parseIPv4UDPPacket(t testing.TB, pkt []byte) (srcIP, dstIP netip.Addr, srcPort, dstPort uint16, payload []byte) {
  17. t.Helper()
  18. require.True(t, len(pkt) >= 28, "packet too short for IPv4+UDP header")
  19. require.Equal(t, byte(0x45), pkt[0]&0xF0|pkt[0]&0x0F, "not a simple IPv4 packet (IHL!=5)")
  20. srcIP, _ = netip.AddrFromSlice(pkt[12:16])
  21. dstIP, _ = netip.AddrFromSlice(pkt[16:20])
  22. ihl := int(pkt[0]&0x0F) * 4
  23. require.True(t, len(pkt) >= ihl+8, "packet too short for UDP header")
  24. srcPort = binary.BigEndian.Uint16(pkt[ihl : ihl+2])
  25. dstPort = binary.BigEndian.Uint16(pkt[ihl+2 : ihl+4])
  26. udpLen := binary.BigEndian.Uint16(pkt[ihl+4 : ihl+6])
  27. payload = pkt[ihl+8 : ihl+int(udpLen)]
  28. return
  29. }
  30. func TestSNAT_IPv6OnlyPeer_IPv4UnsafeTraffic(t *testing.T) {
  31. // Scenario: Two IPv6-only VPN nodes. The "router" node has unsafe networks
  32. // (192.168.0.0/16) in its cert and a configured SNAT address. The "sender"
  33. // node has an unsafe route for 192.168.0.0/16 via the router.
  34. //
  35. // When sender injects an IPv4 packet destined for the unsafe network, it
  36. // gets tunneled to the router. The router's firewall detects this is IPv4
  37. // from an IPv6-only peer and applies SNAT, rewriting the source IP to the
  38. // SNAT address before delivering it to TUN.
  39. //
  40. // When a reply comes back from TUN addressed to the SNAT address, the
  41. // router un-SNATs it (restoring the original destination) and tunnels it
  42. // back to the sender.
  43. ca, _, caKey, _ := cert_test.NewTestCaCert(cert.Version2, cert.Curve_CURVE25519, time.Now(), time.Now().Add(10*time.Minute), nil, nil, []string{})
  44. unsafePrefix := "192.168.0.0/16"
  45. snatAddr := netip.MustParseAddr("169.254.42.42")
  46. // Router: IPv6-only with unsafe networks and a manual SNAT address.
  47. // Override inbound firewall with local_cidr: "any" so both IPv4 (unsafe)
  48. // and IPv6 (VPN) traffic is accepted.
  49. routerControl, routerVpnIpNet, routerUdpAddr, _ := newSimpleServerWithUdpAndUnsafeNetworks(
  50. cert.Version2, ca, caKey, "router", "ff::1/64",
  51. netip.MustParseAddrPort("[beef::1]:4242"),
  52. unsafePrefix,
  53. m{
  54. "firewall": m{
  55. "inbound": []m{{
  56. "proto": "any",
  57. "port": "any",
  58. "host": "any",
  59. "local_cidr": "any",
  60. }},
  61. },
  62. "tun": m{
  63. "snat_address_for_4over6": snatAddr.String(),
  64. },
  65. },
  66. )
  67. // Sender: IPv6-only with an unsafe route via the router
  68. senderControl, _, _, _ := newSimpleServerWithUdp(
  69. cert.Version2, ca, caKey, "sender", "ff::2/64",
  70. netip.MustParseAddrPort("[beef::2]:4242"),
  71. m{
  72. "tun": m{
  73. "unsafe_routes": []m{
  74. {"route": unsafePrefix, "via": routerVpnIpNet[0].Addr().String()},
  75. },
  76. },
  77. },
  78. )
  79. // Tell sender where the router lives
  80. senderControl.InjectLightHouseAddr(routerVpnIpNet[0].Addr(), routerUdpAddr)
  81. // Build the router and start both nodes
  82. r := router.NewR(t, routerControl, senderControl)
  83. defer r.RenderFlow()
  84. routerControl.Start()
  85. senderControl.Start()
  86. // --- Outbound: sender -> IPv4 unsafe dest (via router with SNAT) ---
  87. origSrcIP := netip.MustParseAddr("10.0.0.1")
  88. unsafeDest := netip.MustParseAddr("192.168.1.1")
  89. var origSrcPort uint16 = 12345
  90. var dstPort uint16 = 80
  91. t.Log("Sender injects an IPv4 packet to the unsafe network")
  92. senderControl.InjectTunUDPPacket(unsafeDest, dstPort, origSrcIP, origSrcPort, []byte("snat me"))
  93. t.Log("Route packets (handshake + data) until the router gets the packet on TUN")
  94. snatPkt := r.RouteForAllUntilTxTun(routerControl)
  95. t.Log("Verify the packet was SNATted")
  96. gotSrcIP, gotDstIP, gotSrcPort, gotDstPort, gotPayload := parseIPv4UDPPacket(t, snatPkt)
  97. assert.Equal(t, snatAddr, gotSrcIP, "source IP should be rewritten to the SNAT address")
  98. assert.Equal(t, unsafeDest, gotDstIP, "destination IP should be unchanged")
  99. assert.Equal(t, dstPort, gotDstPort, "destination port should be unchanged")
  100. assert.Equal(t, []byte("snat me"), gotPayload, "payload should be unchanged")
  101. // Capture the SNAT port (may differ from original if port was remapped)
  102. snatPort := gotSrcPort
  103. t.Logf("SNAT port: %d (original: %d)", snatPort, origSrcPort)
  104. // --- Return: reply from unsafe dest -> un-SNATted back to sender ---
  105. t.Log("Router injects a reply packet from the unsafe dest to the SNAT address")
  106. routerControl.InjectTunUDPPacket(snatAddr, snatPort, unsafeDest, dstPort, []byte("reply from unsafe"))
  107. t.Log("Route until sender gets the reply on TUN")
  108. replyPkt := r.RouteForAllUntilTxTun(senderControl)
  109. t.Log("Verify the reply was un-SNATted")
  110. replySrcIP, replyDstIP, replySrcPort, replyDstPort, replyPayload := parseIPv4UDPPacket(t, replyPkt)
  111. assert.Equal(t, unsafeDest, replySrcIP, "reply source should be the unsafe dest")
  112. assert.Equal(t, origSrcIP, replyDstIP, "reply dest should be the original source IP (un-SNATted)")
  113. assert.Equal(t, dstPort, replySrcPort, "reply source port should be the unsafe dest port")
  114. assert.Equal(t, origSrcPort, replyDstPort, "reply dest port should be the original source port (un-SNATted)")
  115. assert.Equal(t, []byte("reply from unsafe"), replyPayload, "payload should be unchanged")
  116. r.RenderHostmaps("Final hostmaps", routerControl, senderControl)
  117. // Also verify normal IPv6 VPN traffic still works between the nodes
  118. t.Log("Verify normal IPv6 VPN tunnel still works")
  119. assertTunnel(t, routerVpnIpNet[0].Addr(), senderControl.GetVpnAddrs()[0], routerControl, senderControl, r)
  120. routerControl.Stop()
  121. senderControl.Stop()
  122. }
  123. func TestSNAT_MultipleFlows(t *testing.T) {
  124. // Test that multiple distinct IPv4 flows from the same IPv6-only peer
  125. // are tracked independently through SNAT.
  126. ca, _, caKey, _ := cert_test.NewTestCaCert(cert.Version2, cert.Curve_CURVE25519, time.Now(), time.Now().Add(10*time.Minute), nil, nil, []string{})
  127. unsafePrefix := "192.168.0.0/16"
  128. snatAddr := netip.MustParseAddr("169.254.42.42")
  129. routerControl, routerVpnIpNet, routerUdpAddr, _ := newSimpleServerWithUdpAndUnsafeNetworks(
  130. cert.Version2, ca, caKey, "router", "ff::1/64",
  131. netip.MustParseAddrPort("[beef::1]:4242"),
  132. unsafePrefix,
  133. m{
  134. "firewall": m{
  135. "inbound": []m{{
  136. "proto": "any",
  137. "port": "any",
  138. "host": "any",
  139. "local_cidr": "any",
  140. }},
  141. },
  142. "tun": m{
  143. "snat_address_for_4over6": snatAddr.String(),
  144. },
  145. },
  146. )
  147. senderControl, _, _, _ := newSimpleServerWithUdp(
  148. cert.Version2, ca, caKey, "sender", "ff::2/64",
  149. netip.MustParseAddrPort("[beef::2]:4242"),
  150. m{
  151. "tun": m{
  152. "unsafe_routes": []m{
  153. {"route": unsafePrefix, "via": routerVpnIpNet[0].Addr().String()},
  154. },
  155. },
  156. },
  157. )
  158. senderControl.InjectLightHouseAddr(routerVpnIpNet[0].Addr(), routerUdpAddr)
  159. r := router.NewR(t, routerControl, senderControl)
  160. defer r.RenderFlow()
  161. r.CancelFlowLogs()
  162. routerControl.Start()
  163. senderControl.Start()
  164. unsafeDest := netip.MustParseAddr("192.168.1.1")
  165. // Send first flow
  166. senderControl.InjectTunUDPPacket(unsafeDest, 80, netip.MustParseAddr("10.0.0.1"), 1111, []byte("flow1"))
  167. pkt1 := r.RouteForAllUntilTxTun(routerControl)
  168. srcIP1, _, srcPort1, _, payload1 := parseIPv4UDPPacket(t, pkt1)
  169. assert.Equal(t, snatAddr, srcIP1)
  170. assert.Equal(t, []byte("flow1"), payload1)
  171. // Send second flow (different source port)
  172. senderControl.InjectTunUDPPacket(unsafeDest, 80, netip.MustParseAddr("10.0.0.1"), 2222, []byte("flow2"))
  173. pkt2 := r.RouteForAllUntilTxTun(routerControl)
  174. srcIP2, _, srcPort2, _, payload2 := parseIPv4UDPPacket(t, pkt2)
  175. assert.Equal(t, snatAddr, srcIP2)
  176. assert.Equal(t, []byte("flow2"), payload2)
  177. // The two flows should have different SNAT ports (since they're different conntracks)
  178. t.Logf("Flow 1 SNAT port: %d, Flow 2 SNAT port: %d", srcPort1, srcPort2)
  179. // Reply to flow 2 first (out of order)
  180. routerControl.InjectTunUDPPacket(snatAddr, srcPort2, unsafeDest, 80, []byte("reply2"))
  181. reply2 := r.RouteForAllUntilTxTun(senderControl)
  182. _, replyDst2, _, replyDstPort2, replyPayload2 := parseIPv4UDPPacket(t, reply2)
  183. assert.Equal(t, netip.MustParseAddr("10.0.0.1"), replyDst2)
  184. assert.Equal(t, uint16(2222), replyDstPort2, "reply to flow 2 should restore original port 2222")
  185. assert.Equal(t, []byte("reply2"), replyPayload2)
  186. // Reply to flow 1
  187. routerControl.InjectTunUDPPacket(snatAddr, srcPort1, unsafeDest, 80, []byte("reply1"))
  188. reply1 := r.RouteForAllUntilTxTun(senderControl)
  189. _, replyDst1, _, replyDstPort1, replyPayload1 := parseIPv4UDPPacket(t, reply1)
  190. assert.Equal(t, netip.MustParseAddr("10.0.0.1"), replyDst1)
  191. assert.Equal(t, uint16(1111), replyDstPort1, "reply to flow 1 should restore original port 1111")
  192. assert.Equal(t, []byte("reply1"), replyPayload1)
  193. routerControl.Stop()
  194. senderControl.Stop()
  195. }
  196. // --- Adversarial SNAT E2E Tests ---
  197. func TestSNAT_UnsolicitedReplyDropped(t *testing.T) {
  198. // Without any outbound SNAT traffic, inject a packet from the router's TUN
  199. // addressed to the SNAT address. The sender must never receive it because
  200. // there's no conntrack entry to un-SNAT through.
  201. ca, _, caKey, _ := cert_test.NewTestCaCert(cert.Version2, cert.Curve_CURVE25519, time.Now(), time.Now().Add(10*time.Minute), nil, nil, []string{})
  202. unsafePrefix := "192.168.0.0/16"
  203. snatAddr := netip.MustParseAddr("169.254.42.42")
  204. routerControl, routerVpnIpNet, routerUdpAddr, _ := newSimpleServerWithUdpAndUnsafeNetworks(
  205. cert.Version2, ca, caKey, "router", "ff::1/64",
  206. netip.MustParseAddrPort("[beef::1]:4242"),
  207. unsafePrefix,
  208. m{
  209. "firewall": m{
  210. "inbound": []m{{
  211. "proto": "any",
  212. "port": "any",
  213. "host": "any",
  214. "local_cidr": "any",
  215. }},
  216. },
  217. "tun": m{
  218. "snat_address_for_4over6": snatAddr.String(),
  219. },
  220. },
  221. )
  222. senderControl, _, _, _ := newSimpleServerWithUdp(
  223. cert.Version2, ca, caKey, "sender", "ff::2/64",
  224. netip.MustParseAddrPort("[beef::2]:4242"),
  225. m{
  226. "tun": m{
  227. "unsafe_routes": []m{
  228. {"route": unsafePrefix, "via": routerVpnIpNet[0].Addr().String()},
  229. },
  230. },
  231. },
  232. )
  233. senderControl.InjectLightHouseAddr(routerVpnIpNet[0].Addr(), routerUdpAddr)
  234. r := router.NewR(t, routerControl, senderControl)
  235. defer r.RenderFlow()
  236. r.CancelFlowLogs()
  237. routerControl.Start()
  238. senderControl.Start()
  239. // First establish the tunnel with normal IPv6 traffic so handshake completes
  240. assertTunnel(t, routerVpnIpNet[0].Addr(), senderControl.GetVpnAddrs()[0], routerControl, senderControl, r)
  241. // Inject the unsolicited reply from router's TUN to the SNAT address.
  242. // There is NO prior outbound SNAT flow, so no conntrack entry exists.
  243. // The router should silently drop this because unSnat finds no matching conntrack.
  244. routerControl.InjectTunUDPPacket(snatAddr, 55555, netip.MustParseAddr("192.168.1.1"), 80, []byte("unsolicited"))
  245. // Send a canary IPv6 VPN packet after the bad one. Since the router processes
  246. // TUN packets sequentially, the canary arriving proves the bad packet was processed first.
  247. senderVpnAddr := senderControl.GetVpnAddrs()[0]
  248. routerControl.InjectTunUDPPacket(senderVpnAddr, 90, routerVpnIpNet[0].Addr(), 80, []byte("canary"))
  249. canaryPkt := r.RouteForAllUntilTxTun(senderControl)
  250. assertUdpPacket(t, []byte("canary"), canaryPkt, routerVpnIpNet[0].Addr(), senderVpnAddr, 80, 90)
  251. // The unsolicited packet should have been dropped — nothing else on sender's TUN
  252. got := senderControl.GetFromTun(false)
  253. assert.Nil(t, got, "sender should not receive unsolicited packet to SNAT address with no conntrack entry")
  254. routerControl.Stop()
  255. senderControl.Stop()
  256. }
  257. func TestSNAT_NonUnsafeDestDropped(t *testing.T) {
  258. // An IPv6-only sender sends IPv4 traffic to a destination outside the router's
  259. // unsafe networks (172.16.0.1 when unsafe is 192.168.0.0/16). The router should
  260. // reject this because the local address is not routable. This verifies that
  261. // willingToHandleLocalAddr enforces boundaries on what SNAT traffic can reach.
  262. ca, _, caKey, _ := cert_test.NewTestCaCert(cert.Version2, cert.Curve_CURVE25519, time.Now(), time.Now().Add(10*time.Minute), nil, nil, []string{})
  263. unsafePrefix := "192.168.0.0/16"
  264. snatAddr := netip.MustParseAddr("169.254.42.42")
  265. routerControl, routerVpnIpNet, routerUdpAddr, _ := newSimpleServerWithUdpAndUnsafeNetworks(
  266. cert.Version2, ca, caKey, "router", "ff::1/64",
  267. netip.MustParseAddrPort("[beef::1]:4242"),
  268. unsafePrefix,
  269. m{
  270. "firewall": m{
  271. "inbound": []m{{
  272. "proto": "any",
  273. "port": "any",
  274. "host": "any",
  275. "local_cidr": "any",
  276. }},
  277. },
  278. "tun": m{
  279. "snat_address_for_4over6": snatAddr.String(),
  280. },
  281. },
  282. )
  283. // Sender has unsafe routes for BOTH 192.168.0.0/16 AND 172.16.0.0/12 via router.
  284. // This means the sender will route 172.16.0.1 through the tunnel to the router.
  285. // But the router should reject it because 172.16.0.0/12 is NOT in its unsafe networks.
  286. senderControl, _, _, _ := newSimpleServerWithUdp(
  287. cert.Version2, ca, caKey, "sender", "ff::2/64",
  288. netip.MustParseAddrPort("[beef::2]:4242"),
  289. m{
  290. "tun": m{
  291. "unsafe_routes": []m{
  292. {"route": unsafePrefix, "via": routerVpnIpNet[0].Addr().String()},
  293. {"route": "172.16.0.0/12", "via": routerVpnIpNet[0].Addr().String()},
  294. },
  295. },
  296. },
  297. )
  298. senderControl.InjectLightHouseAddr(routerVpnIpNet[0].Addr(), routerUdpAddr)
  299. r := router.NewR(t, routerControl, senderControl)
  300. defer r.RenderFlow()
  301. r.CancelFlowLogs()
  302. routerControl.Start()
  303. senderControl.Start()
  304. // Establish the tunnel first
  305. assertTunnel(t, routerVpnIpNet[0].Addr(), senderControl.GetVpnAddrs()[0], routerControl, senderControl, r)
  306. // Send to 172.16.0.1 (NOT in router's unsafe networks 192.168.0.0/16).
  307. // The router should reject this at willingToHandleLocalAddr.
  308. senderControl.InjectTunUDPPacket(
  309. netip.MustParseAddr("172.16.0.1"), 80,
  310. netip.MustParseAddr("10.0.0.1"), 12345,
  311. []byte("wrong dest"),
  312. )
  313. // Send a canary to a valid unsafe destination to prove the bad packet was processed
  314. senderControl.InjectTunUDPPacket(
  315. netip.MustParseAddr("192.168.1.1"), 80,
  316. netip.MustParseAddr("10.0.0.1"), 33333,
  317. []byte("canary"),
  318. )
  319. // Route until the canary arrives — the 172.16.0.1 packet should have been
  320. // processed and dropped before the canary gets through
  321. canaryPkt := r.RouteForAllUntilTxTun(routerControl)
  322. _, canaryDst, _, _, canaryPayload := parseIPv4UDPPacket(t, canaryPkt)
  323. assert.Equal(t, netip.MustParseAddr("192.168.1.1"), canaryDst, "canary should arrive at the valid unsafe dest")
  324. assert.Equal(t, []byte("canary"), canaryPayload)
  325. // No more packets — the 172.16.0.1 packet was dropped
  326. got := routerControl.GetFromTun(false)
  327. assert.Nil(t, got, "packet to non-unsafe destination 172.16.0.1 should be dropped by the router")
  328. routerControl.Stop()
  329. senderControl.Stop()
  330. }