snat_test.go 43 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310
  1. package nebula
  2. import (
  3. "encoding/binary"
  4. "net/netip"
  5. "slices"
  6. "testing"
  7. "time"
  8. "github.com/gaissmai/bart"
  9. "github.com/sirupsen/logrus"
  10. "github.com/slackhq/nebula/cert"
  11. "github.com/slackhq/nebula/firewall"
  12. "github.com/stretchr/testify/assert"
  13. "github.com/stretchr/testify/require"
  14. )
  15. // Canonical test packets with all checksums computed from scratch by
  16. // /tmp/gen_canonical.go. Tests feed these into the production rewrite
  17. // functions and compare byte-for-byte against expected outputs.
  18. // canonicalUDP: src=10.0.0.1:12345 dst=192.168.1.1:80 proto=UDP payload="hello world"
  19. var canonicalUDP = []byte{
  20. 0x45, 0x00, 0x00, 0x27, 0x12, 0x34, 0x40, 0x00, 0x40, 0x11, 0x5c, 0xe8, 0x0a, 0x00, 0x00, 0x01,
  21. 0xc0, 0xa8, 0x01, 0x01, 0x30, 0x39, 0x00, 0x50, 0x00, 0x13, 0x71, 0xc6, 0x68, 0x65, 0x6c, 0x6c,
  22. 0x6f, 0x20, 0x77, 0x6f, 0x72, 0x6c, 0x64,
  23. }
  24. // canonicalTCP: src=10.0.0.1:12345 dst=192.168.1.1:80 proto=TCP payload="GET / HTTP/1.1"
  25. var canonicalTCP = []byte{
  26. 0x45, 0x00, 0x00, 0x36, 0x12, 0x34, 0x40, 0x00, 0x40, 0x06, 0x5c, 0xe4, 0x0a, 0x00, 0x00, 0x01,
  27. 0xc0, 0xa8, 0x01, 0x01, 0x30, 0x39, 0x00, 0x50, 0x12, 0x34, 0x56, 0x78, 0x00, 0x00, 0x00, 0x00,
  28. 0x50, 0x02, 0xff, 0xff, 0x86, 0x68, 0x00, 0x00, 0x47, 0x45, 0x54, 0x20, 0x2f, 0x20, 0x48, 0x54,
  29. 0x54, 0x50, 0x2f, 0x31, 0x2e, 0x31,
  30. }
  31. // canonicalICMP: src=10.0.0.1 dst=192.168.1.1 proto=ICMP echo, id=0x1234 seq=1
  32. var canonicalICMP = []byte{
  33. 0x45, 0x00, 0x00, 0x1c, 0x12, 0x34, 0x40, 0x00, 0x40, 0x01, 0x5d, 0x03, 0x0a, 0x00, 0x00, 0x01,
  34. 0xc0, 0xa8, 0x01, 0x01, 0x08, 0x00, 0xe5, 0xca, 0x12, 0x34, 0x00, 0x01,
  35. }
  36. // canonicalUDPReply: src=192.168.1.1:80 dst=169.254.55.96:55555 proto=UDP payload="reply"
  37. var canonicalUDPReply = []byte{
  38. 0x45, 0x00, 0x00, 0x21, 0x12, 0x34, 0x40, 0x00, 0x40, 0x11, 0x85, 0x90, 0xc0, 0xa8, 0x01, 0x01,
  39. 0xa9, 0xfe, 0x37, 0x60, 0x00, 0x50, 0xd9, 0x03, 0x00, 0x0d, 0x27, 0xa6, 0x72, 0x65, 0x70, 0x6c,
  40. 0x79,
  41. }
  42. // canonicalUDPTest: src=10.0.0.1:12345 dst=192.168.1.1:80 proto=UDP payload="test"
  43. var canonicalUDPTest = []byte{
  44. 0x45, 0x00, 0x00, 0x20, 0x12, 0x34, 0x40, 0x00, 0x40, 0x11, 0x5c, 0xef, 0x0a, 0x00, 0x00, 0x01,
  45. 0xc0, 0xa8, 0x01, 0x01, 0x30, 0x39, 0x00, 0x50, 0x00, 0x0c, 0x1b, 0xc9, 0x74, 0x65, 0x73, 0x74,
  46. }
  47. // canonicalUDPHijack: src=10.0.0.1:12345 dst=192.168.1.1:80 proto=UDP payload="hijack"
  48. var canonicalUDPHijack = []byte{
  49. 0x45, 0x00, 0x00, 0x22, 0x12, 0x34, 0x40, 0x00, 0x40, 0x11, 0x5c, 0xed, 0x0a, 0x00, 0x00, 0x01,
  50. 0xc0, 0xa8, 0x01, 0x01, 0x30, 0x39, 0x00, 0x50, 0x00, 0x0e, 0xcd, 0x68, 0x68, 0x69, 0x6a, 0x61,
  51. 0x63, 0x6b,
  52. }
  53. // canonicalUDPBlocked: src=10.0.0.1:12345 dst=192.168.1.1:443 proto=UDP payload="blocked"
  54. var canonicalUDPBlocked = []byte{
  55. 0x45, 0x00, 0x00, 0x23, 0x12, 0x34, 0x40, 0x00, 0x40, 0x11, 0x5c, 0xec, 0x0a, 0x00, 0x00, 0x01,
  56. 0xc0, 0xa8, 0x01, 0x01, 0x30, 0x39, 0x01, 0xbb, 0x00, 0x0f, 0x60, 0xfc, 0x62, 0x6c, 0x6f, 0x63,
  57. 0x6b, 0x65, 0x64,
  58. }
  59. // canonicalUDPWrongDest: src=10.0.0.1:12345 dst=172.16.0.1:80 proto=UDP payload="wrong dest"
  60. var canonicalUDPWrongDest = []byte{
  61. 0x45, 0x00, 0x00, 0x26, 0x12, 0x34, 0x40, 0x00, 0x40, 0x11, 0x72, 0x81, 0x0a, 0x00, 0x00, 0x01,
  62. 0xac, 0x10, 0x00, 0x01, 0x30, 0x39, 0x00, 0x50, 0x00, 0x12, 0xf3, 0x53, 0x77, 0x72, 0x6f, 0x6e,
  63. 0x67, 0x20, 0x64, 0x65, 0x73, 0x74,
  64. }
  65. // canonicalUDPNoSnat: src=10.0.0.1:12345 dst=192.168.1.1:80 proto=UDP payload="no snat"
  66. var canonicalUDPNoSnat = []byte{
  67. 0x45, 0x00, 0x00, 0x23, 0x12, 0x34, 0x40, 0x00, 0x40, 0x11, 0x5c, 0xec, 0x0a, 0x00, 0x00, 0x01,
  68. 0xc0, 0xa8, 0x01, 0x01, 0x30, 0x39, 0x00, 0x50, 0x00, 0x0f, 0x92, 0x58, 0x6e, 0x6f, 0x20, 0x73,
  69. 0x6e, 0x61, 0x74,
  70. }
  71. // canonicalUDPV4Traffic: src=10.128.0.2:12345 dst=192.168.1.1:80 proto=UDP payload="v4 traffic"
  72. var canonicalUDPV4Traffic = []byte{
  73. 0x45, 0x00, 0x00, 0x26, 0x12, 0x34, 0x40, 0x00, 0x40, 0x11, 0x5c, 0x68, 0x0a, 0x80, 0x00, 0x02,
  74. 0xc0, 0xa8, 0x01, 0x01, 0x30, 0x39, 0x00, 0x50, 0x00, 0x12, 0x2a, 0x42, 0x76, 0x34, 0x20, 0x74,
  75. 0x72, 0x61, 0x66, 0x66, 0x69, 0x63,
  76. }
  77. // canonicalUDPRoundtrip: src=10.0.0.1:12345 dst=192.168.1.1:80 proto=UDP payload="roundtrip"
  78. var canonicalUDPRoundtrip = []byte{
  79. 0x45, 0x00, 0x00, 0x25, 0x12, 0x34, 0x40, 0x00, 0x40, 0x11, 0x5c, 0xea, 0x0a, 0x00, 0x00, 0x01,
  80. 0xc0, 0xa8, 0x01, 0x01, 0x30, 0x39, 0x00, 0x50, 0x00, 0x11, 0xd4, 0xdc, 0x72, 0x6f, 0x75, 0x6e,
  81. 0x64, 0x74, 0x72, 0x69, 0x70,
  82. }
  83. // canonicalUDPSnatMe: src=10.0.0.1:12345 dst=192.168.1.1:80 proto=UDP payload="snat me"
  84. var canonicalUDPSnatMe = []byte{
  85. 0x45, 0x00, 0x00, 0x23, 0x12, 0x34, 0x40, 0x00, 0x40, 0x11, 0x5c, 0xec, 0x0a, 0x00, 0x00, 0x01,
  86. 0xc0, 0xa8, 0x01, 0x01, 0x30, 0x39, 0x00, 0x50, 0x00, 0x0f, 0xa9, 0x4c, 0x73, 0x6e, 0x61, 0x74,
  87. 0x20, 0x6d, 0x65,
  88. }
  89. // Expected outputs after rewriting — built from scratch with the post-rewrite
  90. // addresses, so all checksums are independently correct.
  91. // canonicalUDPSnatted: canonicalUDP with src rewritten to 169.254.55.96:55555
  92. var canonicalUDPSnatted = []byte{
  93. 0x45, 0x00, 0x00, 0x27, 0x12, 0x34, 0x40, 0x00, 0x40, 0x11, 0x85, 0x8a, 0xa9, 0xfe, 0x37, 0x60,
  94. 0xc0, 0xa8, 0x01, 0x01, 0xd9, 0x03, 0x00, 0x50, 0x00, 0x13, 0xf1, 0x9d, 0x68, 0x65, 0x6c, 0x6c,
  95. 0x6f, 0x20, 0x77, 0x6f, 0x72, 0x6c, 0x64,
  96. }
  97. // canonicalUDPReplyUnSnatted: canonicalUDPReply with dst rewritten from 169.254.55.96:55555 to 10.0.0.1:12345
  98. var canonicalUDPReplyUnSnatted = []byte{
  99. 0x45, 0x00, 0x00, 0x21, 0x12, 0x34, 0x40, 0x00, 0x40, 0x11, 0x5c, 0xee, 0xc0, 0xa8, 0x01, 0x01,
  100. 0x0a, 0x00, 0x00, 0x01, 0x00, 0x50, 0x30, 0x39, 0x00, 0x0d, 0xa7, 0xce, 0x72, 0x65, 0x70, 0x6c,
  101. 0x79,
  102. }
  103. // canonicalTCPSnatted: canonicalTCP with src rewritten to 169.254.55.96:55555
  104. var canonicalTCPSnatted = []byte{
  105. 0x45, 0x00, 0x00, 0x36, 0x12, 0x34, 0x40, 0x00, 0x40, 0x06, 0x85, 0x86, 0xa9, 0xfe, 0x37, 0x60,
  106. 0xc0, 0xa8, 0x01, 0x01, 0xd9, 0x03, 0x00, 0x50, 0x12, 0x34, 0x56, 0x78, 0x00, 0x00, 0x00, 0x00,
  107. 0x50, 0x02, 0xff, 0xff, 0x06, 0x40, 0x00, 0x00, 0x47, 0x45, 0x54, 0x20, 0x2f, 0x20, 0x48, 0x54,
  108. 0x54, 0x50, 0x2f, 0x31, 0x2e, 0x31,
  109. }
  110. // canonicalICMPSnatted: canonicalICMP with src rewritten to 169.254.55.96, id changed from 0x1234 to 0x5678
  111. var canonicalICMPSnatted = []byte{
  112. 0x45, 0x00, 0x00, 0x1c, 0x12, 0x34, 0x40, 0x00, 0x40, 0x01, 0x85, 0xa5, 0xa9, 0xfe, 0x37, 0x60,
  113. 0xc0, 0xa8, 0x01, 0x01, 0x08, 0x00, 0xa1, 0x86, 0x56, 0x78, 0x00, 0x01,
  114. }
  115. func TestCalcNewTransportChecksum_Identity(t *testing.T) {
  116. // Rewriting to the same IP/port should return the same checksum
  117. ip := netip.MustParseAddr("10.0.0.1")
  118. result := calcNewTransportChecksum(0x1234, ip, 80, ip, 80)
  119. assert.Equal(t, uint16(0x1234), result)
  120. }
  121. func TestCalcNewTransportChecksum_VsCanonical(t *testing.T) {
  122. srcIP := netip.MustParseAddr("10.0.0.1")
  123. snatIP := netip.MustParseAddr("169.254.55.96")
  124. // Extract the original UDP checksum from canonicalUDP (bytes 26-27)
  125. origChecksum := binary.BigEndian.Uint16(canonicalUDP[26:28])
  126. // Compute incrementally
  127. incremental := calcNewTransportChecksum(origChecksum, srcIP, 12345, snatIP, 55555)
  128. // Verify it matches the checksum in the independently-computed canonicalUDPSnatted
  129. expectedChecksum := binary.BigEndian.Uint16(canonicalUDPSnatted[26:28])
  130. assert.Equal(t, expectedChecksum, incremental, "incremental checksum should match canonical expected output")
  131. }
  132. func TestCalcNewICMPChecksum_Identity(t *testing.T) {
  133. // Same values in and out should be identity
  134. result := calcNewICMPChecksum(0xABCD, 0, 0, 1234, 1234)
  135. assert.Equal(t, uint16(0xABCD), result)
  136. }
  137. func TestRewritePacket_UDP(t *testing.T) {
  138. srcIP := netip.MustParseAddr("10.0.0.1")
  139. dstIP := netip.MustParseAddr("192.168.1.1")
  140. snatIP := netip.MustParseAddr("169.254.55.96")
  141. pkt := slices.Clone(canonicalUDP)
  142. fp := firewall.Packet{
  143. LocalAddr: dstIP,
  144. RemoteAddr: srcIP,
  145. LocalPort: 80,
  146. RemotePort: 12345,
  147. Protocol: firewall.ProtoUDP,
  148. }
  149. // SNAT rewrites source: IP at offset 12, port at offset 0 inside transport
  150. oldIP := netip.AddrPortFrom(srcIP, 12345)
  151. newIP := netip.AddrPortFrom(snatIP, 55555)
  152. rewritePacket(pkt, &fp, oldIP, newIP, 12, 0)
  153. assert.Equal(t, canonicalUDPSnatted, pkt, "rewritten packet should match canonical expected output")
  154. }
  155. func TestRewritePacket_UDP_UnSNAT(t *testing.T) {
  156. snatIP := netip.MustParseAddr("169.254.55.96")
  157. dstIP := netip.MustParseAddr("192.168.1.1")
  158. origSrcIP := netip.MustParseAddr("10.0.0.1")
  159. pkt := slices.Clone(canonicalUDPReply)
  160. fp := firewall.Packet{
  161. LocalAddr: dstIP,
  162. RemoteAddr: snatIP,
  163. LocalPort: 80,
  164. RemotePort: 55555,
  165. Protocol: firewall.ProtoUDP,
  166. }
  167. // UnSNAT rewrites destination: IP at offset 16, port at offset 2 inside transport
  168. oldIP := netip.AddrPortFrom(snatIP, 55555)
  169. newIP := netip.AddrPortFrom(origSrcIP, 12345)
  170. rewritePacket(pkt, &fp, oldIP, newIP, 16, 2)
  171. assert.Equal(t, canonicalUDPReplyUnSnatted, pkt, "un-SNATted packet should match canonical expected output")
  172. }
  173. func TestRewritePacket_TCP(t *testing.T) {
  174. srcIP := netip.MustParseAddr("10.0.0.1")
  175. dstIP := netip.MustParseAddr("192.168.1.1")
  176. snatIP := netip.MustParseAddr("169.254.55.96")
  177. pkt := slices.Clone(canonicalTCP)
  178. fp := firewall.Packet{
  179. LocalAddr: dstIP,
  180. RemoteAddr: srcIP,
  181. LocalPort: 80,
  182. RemotePort: 12345,
  183. Protocol: firewall.ProtoTCP,
  184. }
  185. oldIP := netip.AddrPortFrom(srcIP, 12345)
  186. newIP := netip.AddrPortFrom(snatIP, 55555)
  187. rewritePacket(pkt, &fp, oldIP, newIP, 12, 0)
  188. assert.Equal(t, canonicalTCPSnatted, pkt, "rewritten TCP packet should match canonical expected output")
  189. }
  190. func TestRewritePacket_ICMP(t *testing.T) {
  191. srcIP := netip.MustParseAddr("10.0.0.1")
  192. dstIP := netip.MustParseAddr("192.168.1.1")
  193. snatIP := netip.MustParseAddr("169.254.55.96")
  194. pkt := slices.Clone(canonicalICMP)
  195. fp := firewall.Packet{
  196. LocalAddr: dstIP,
  197. RemoteAddr: srcIP,
  198. LocalPort: 0,
  199. RemotePort: 0x1234, // ICMP ID used as port
  200. Protocol: firewall.ProtoICMP,
  201. }
  202. oldIP := netip.AddrPortFrom(srcIP, 0x1234)
  203. newIP := netip.AddrPortFrom(snatIP, 0x5678)
  204. rewritePacket(pkt, &fp, oldIP, newIP, 12, 0)
  205. assert.Equal(t, canonicalICMPSnatted, pkt, "rewritten ICMP packet should match canonical expected output")
  206. }
  207. func TestRewritePacket_Roundtrip(t *testing.T) {
  208. // Test that SNAT followed by unSNAT produces the original packet
  209. srcIP := netip.MustParseAddr("10.0.0.1")
  210. dstIP := netip.MustParseAddr("192.168.1.1")
  211. snatIP := netip.MustParseAddr("169.254.55.96")
  212. pkt := slices.Clone(canonicalUDPRoundtrip)
  213. fp := firewall.Packet{
  214. LocalAddr: dstIP,
  215. RemoteAddr: srcIP,
  216. LocalPort: 80,
  217. RemotePort: 12345,
  218. Protocol: firewall.ProtoUDP,
  219. }
  220. // SNAT: rewrite source
  221. oldSrc := netip.AddrPortFrom(srcIP, 12345)
  222. newSrc := netip.AddrPortFrom(snatIP, 55555)
  223. rewritePacket(pkt, &fp, oldSrc, newSrc, 12, 0)
  224. // Verify intermediate state is not the original
  225. require.NotEqual(t, canonicalUDPRoundtrip, pkt)
  226. // UnSNAT: rewrite source back
  227. rewritePacket(pkt, &fp, newSrc, oldSrc, 12, 0)
  228. // Packet should be byte-for-byte identical to original
  229. assert.Equal(t, canonicalUDPRoundtrip, pkt, "packet should be identical after roundtrip SNAT/unSNAT")
  230. }
  231. func TestSnatInfo_Valid(t *testing.T) {
  232. t.Run("nil is invalid", func(t *testing.T) {
  233. var s *snatInfo
  234. assert.False(t, s.Valid())
  235. })
  236. t.Run("zero value is invalid", func(t *testing.T) {
  237. s := &snatInfo{}
  238. assert.False(t, s.Valid())
  239. })
  240. t.Run("with valid src is valid", func(t *testing.T) {
  241. s := &snatInfo{
  242. Src: netip.AddrPortFrom(netip.MustParseAddr("10.0.0.1"), 1234),
  243. SrcVpnIp: netip.MustParseAddr("fd00::1"),
  244. SnatPort: 55555,
  245. }
  246. assert.True(t, s.Valid())
  247. })
  248. }
  249. func TestFirewall_ShouldUnSNAT(t *testing.T) {
  250. snatAddr := netip.MustParseAddr("169.254.55.96")
  251. t.Run("no snat addr configured", func(t *testing.T) {
  252. fw := &Firewall{}
  253. fp := &firewall.Packet{RemoteAddr: snatAddr}
  254. assert.False(t, fw.ShouldUnSNAT(fp))
  255. })
  256. t.Run("packet to snat addr", func(t *testing.T) {
  257. fw := &Firewall{snatAddr: snatAddr}
  258. fp := &firewall.Packet{RemoteAddr: snatAddr}
  259. assert.True(t, fw.ShouldUnSNAT(fp))
  260. })
  261. t.Run("packet to different addr", func(t *testing.T) {
  262. fw := &Firewall{snatAddr: snatAddr}
  263. fp := &firewall.Packet{RemoteAddr: netip.MustParseAddr("10.0.0.1")}
  264. assert.False(t, fw.ShouldUnSNAT(fp))
  265. })
  266. }
  267. func TestFirewall_IdentifyNetworkType_SNATPeer(t *testing.T) {
  268. snatAddr := netip.MustParseAddr("169.254.55.96")
  269. t.Run("v4 packet from v6-only host without networks table", func(t *testing.T) {
  270. fw := &Firewall{snatAddr: snatAddr}
  271. h := &HostInfo{vpnAddrs: []netip.Addr{netip.MustParseAddr("fd00::1")}}
  272. fp := firewall.Packet{
  273. RemoteAddr: netip.MustParseAddr("10.0.0.1"),
  274. LocalAddr: netip.MustParseAddr("192.168.1.1"),
  275. }
  276. assert.Equal(t, NetworkTypeUnverifiedSNATPeer, fw.identifyRemoteNetworkType(h, fp))
  277. })
  278. t.Run("v4 packet from v4 host is not snat peer", func(t *testing.T) {
  279. fw := &Firewall{snatAddr: snatAddr}
  280. h := &HostInfo{vpnAddrs: []netip.Addr{netip.MustParseAddr("10.0.0.1")}}
  281. fp := firewall.Packet{
  282. RemoteAddr: netip.MustParseAddr("10.0.0.1"),
  283. LocalAddr: netip.MustParseAddr("192.168.1.1"),
  284. }
  285. assert.Equal(t, NetworkTypeVPN, fw.identifyRemoteNetworkType(h, fp))
  286. })
  287. t.Run("v6 packet from v6 host is VPN", func(t *testing.T) {
  288. fw := &Firewall{snatAddr: snatAddr}
  289. h := &HostInfo{vpnAddrs: []netip.Addr{netip.MustParseAddr("fd00::1")}}
  290. fp := firewall.Packet{
  291. RemoteAddr: netip.MustParseAddr("fd00::1"),
  292. LocalAddr: netip.MustParseAddr("fd00::2"),
  293. }
  294. assert.Equal(t, NetworkTypeVPN, fw.identifyRemoteNetworkType(h, fp))
  295. })
  296. t.Run("mismatched v4 from v4 host is invalid", func(t *testing.T) {
  297. fw := &Firewall{snatAddr: snatAddr}
  298. h := &HostInfo{vpnAddrs: []netip.Addr{netip.MustParseAddr("10.0.0.1")}}
  299. fp := firewall.Packet{
  300. RemoteAddr: netip.MustParseAddr("10.0.0.99"),
  301. LocalAddr: netip.MustParseAddr("192.168.1.1"),
  302. }
  303. assert.Equal(t, NetworkTypeInvalidPeer, fw.identifyRemoteNetworkType(h, fp))
  304. })
  305. }
  306. func TestFirewall_AllowNetworkType_SNAT(t *testing.T) {
  307. //todo fix!
  308. //t.Run("snat peer allowed with snat addr", func(t *testing.T) {
  309. // fw := &Firewall{snatAddr: netip.MustParseAddr("169.254.55.96")}
  310. // assert.NoError(t, fw.allowRemoteNetworkType(NetworkTypeUnverifiedSNATPeer, fp))
  311. //})
  312. //
  313. //t.Run("snat peer rejected without snat addr", func(t *testing.T) {
  314. // fw := &Firewall{}
  315. // assert.ErrorIs(t, fw.allowRemoteNetworkType(NetworkTypeUnverifiedSNATPeer, fp), ErrInvalidRemoteIP)
  316. //})
  317. t.Run("vpn always allowed", func(t *testing.T) {
  318. fw := &Firewall{}
  319. assert.NoError(t, fw.allowRemoteNetworkType(NetworkTypeVPN, firewall.Packet{}))
  320. })
  321. t.Run("unsafe always allowed", func(t *testing.T) {
  322. fw := &Firewall{}
  323. assert.NoError(t, fw.allowRemoteNetworkType(NetworkTypeUnsafe, firewall.Packet{}))
  324. })
  325. t.Run("invalid peer rejected", func(t *testing.T) {
  326. fw := &Firewall{}
  327. assert.ErrorIs(t, fw.allowRemoteNetworkType(NetworkTypeInvalidPeer, firewall.Packet{}), ErrInvalidRemoteIP)
  328. })
  329. t.Run("vpn peer rejected", func(t *testing.T) {
  330. fw := &Firewall{}
  331. assert.ErrorIs(t, fw.allowRemoteNetworkType(NetworkTypeVPNPeer, firewall.Packet{}), ErrPeerRejected)
  332. })
  333. }
  334. func TestFirewall_FindUsableSNATPort(t *testing.T) {
  335. l := logrus.New()
  336. l.SetLevel(logrus.PanicLevel)
  337. snatAddr := netip.MustParseAddr("169.254.55.96")
  338. t.Run("finds first available port", func(t *testing.T) {
  339. c := &dummyCert{
  340. networks: []netip.Prefix{netip.MustParsePrefix("fd00::1/128")},
  341. unsafeNetworks: []netip.Prefix{netip.MustParsePrefix("10.0.0.0/8")},
  342. }
  343. fw := NewFirewall(l, time.Second, time.Minute, time.Hour, c)
  344. fp := firewall.Packet{
  345. LocalAddr: netip.MustParseAddr("192.168.1.1"),
  346. RemoteAddr: snatAddr,
  347. LocalPort: 80,
  348. RemotePort: 12345,
  349. Protocol: firewall.ProtoUDP,
  350. }
  351. cn := &conn{snat: &snatInfo{}}
  352. err := fw.findUsableSNATPort(&fp, cn)
  353. require.NoError(t, err)
  354. // Port should have been assigned
  355. assert.Equal(t, uint16(12345), fp.RemotePort, "should use original port if available")
  356. })
  357. t.Run("skips occupied port", func(t *testing.T) {
  358. c := &dummyCert{
  359. networks: []netip.Prefix{netip.MustParsePrefix("fd00::1/128")},
  360. unsafeNetworks: []netip.Prefix{netip.MustParsePrefix("10.0.0.0/8")},
  361. }
  362. fw := NewFirewall(l, time.Second, time.Minute, time.Hour, c)
  363. fp := firewall.Packet{
  364. LocalAddr: netip.MustParseAddr("192.168.1.1"),
  365. RemoteAddr: snatAddr,
  366. LocalPort: 80,
  367. RemotePort: 12345,
  368. Protocol: firewall.ProtoUDP,
  369. }
  370. // Occupy the port
  371. fw.Conntrack.Lock()
  372. fw.Conntrack.Conns[fp] = &conn{}
  373. fw.Conntrack.Unlock()
  374. cn := &conn{snat: &snatInfo{}}
  375. err := fw.findUsableSNATPort(&fp, cn)
  376. require.NoError(t, err)
  377. assert.NotEqual(t, uint16(12345), fp.RemotePort, "should pick a different port")
  378. })
  379. t.Run("returns error on exhaustion", func(t *testing.T) {
  380. c := &dummyCert{
  381. networks: []netip.Prefix{netip.MustParsePrefix("fd00::1/128")},
  382. unsafeNetworks: []netip.Prefix{netip.MustParsePrefix("10.0.0.0/8")},
  383. }
  384. fw := NewFirewall(l, time.Second, time.Minute, time.Hour, c)
  385. // Fill all ports
  386. baseFP := firewall.Packet{
  387. LocalAddr: netip.MustParseAddr("192.168.1.1"),
  388. RemoteAddr: snatAddr,
  389. LocalPort: 80,
  390. Protocol: firewall.ProtoUDP,
  391. }
  392. fw.Conntrack.Lock()
  393. for i := 0; i < 65535; i++ {
  394. fp := baseFP
  395. fp.RemotePort = uint16(i)
  396. fw.Conntrack.Conns[fp] = &conn{}
  397. }
  398. fw.Conntrack.Unlock()
  399. // Try to find a port starting from 0x8000
  400. fp := baseFP
  401. fp.RemotePort = 0x8000
  402. cn := &conn{snat: &snatInfo{}}
  403. err := fw.findUsableSNATPort(&fp, cn)
  404. assert.ErrorIs(t, err, ErrCannotSNAT)
  405. })
  406. }
  407. func TestFirewall_ApplySnat(t *testing.T) {
  408. l := logrus.New()
  409. l.SetLevel(logrus.PanicLevel)
  410. snatAddr := netip.MustParseAddr("169.254.55.96")
  411. peerV6Addr := netip.MustParseAddr("fd00::1")
  412. dstIP := netip.MustParseAddr("192.168.1.1")
  413. t.Run("new flow from v6 host", func(t *testing.T) {
  414. c := &dummyCert{
  415. networks: []netip.Prefix{netip.MustParsePrefix("fd00::1/128")},
  416. unsafeNetworks: []netip.Prefix{netip.MustParsePrefix("192.168.0.0/16")},
  417. }
  418. fw := NewFirewall(l, time.Second, time.Minute, time.Hour, c)
  419. fw.snatAddr = snatAddr
  420. pkt := slices.Clone(canonicalUDPTest)
  421. fp := firewall.Packet{
  422. LocalAddr: dstIP,
  423. RemoteAddr: netip.MustParseAddr("10.0.0.1"),
  424. LocalPort: 80,
  425. RemotePort: 12345,
  426. Protocol: firewall.ProtoUDP,
  427. }
  428. cn := &conn{}
  429. h := &HostInfo{vpnAddrs: []netip.Addr{peerV6Addr}}
  430. err := fw.applySnat(pkt, &fp, cn, h)
  431. require.NoError(t, err)
  432. // Should have created snat info
  433. require.True(t, cn.snat.Valid())
  434. assert.Equal(t, peerV6Addr, cn.snat.SrcVpnIp)
  435. assert.Equal(t, netip.MustParseAddr("10.0.0.1"), cn.snat.Src.Addr())
  436. assert.Equal(t, uint16(12345), cn.snat.Src.Port())
  437. // Packet source should be rewritten to snatAddr
  438. gotSrcIP, _ := netip.AddrFromSlice(pkt[12:16])
  439. assert.Equal(t, snatAddr, gotSrcIP)
  440. })
  441. t.Run("existing flow with matching identity", func(t *testing.T) {
  442. c := &dummyCert{
  443. networks: []netip.Prefix{netip.MustParsePrefix("fd00::1/128")},
  444. unsafeNetworks: []netip.Prefix{netip.MustParsePrefix("192.168.0.0/16")},
  445. }
  446. fw := NewFirewall(l, time.Second, time.Minute, time.Hour, c)
  447. fw.snatAddr = snatAddr
  448. pkt := slices.Clone(canonicalUDPTest)
  449. fp := firewall.Packet{
  450. LocalAddr: dstIP,
  451. RemoteAddr: netip.MustParseAddr("10.0.0.1"),
  452. LocalPort: 80,
  453. RemotePort: 12345,
  454. Protocol: firewall.ProtoUDP,
  455. }
  456. cn := &conn{
  457. snat: &snatInfo{
  458. Src: netip.AddrPortFrom(netip.MustParseAddr("10.0.0.1"), 12345),
  459. SrcVpnIp: peerV6Addr,
  460. SnatPort: 55555,
  461. },
  462. }
  463. h := &HostInfo{vpnAddrs: []netip.Addr{peerV6Addr}}
  464. err := fw.applySnat(pkt, &fp, cn, h)
  465. require.NoError(t, err)
  466. // Source should be rewritten
  467. gotSrcIP, _ := netip.AddrFromSlice(pkt[12:16])
  468. assert.Equal(t, snatAddr, gotSrcIP)
  469. })
  470. t.Run("identity mismatch rejected", func(t *testing.T) {
  471. c := &dummyCert{
  472. networks: []netip.Prefix{netip.MustParsePrefix("fd00::1/128")},
  473. unsafeNetworks: []netip.Prefix{netip.MustParsePrefix("192.168.0.0/16")},
  474. }
  475. fw := NewFirewall(l, time.Second, time.Minute, time.Hour, c)
  476. fw.snatAddr = snatAddr
  477. pkt := slices.Clone(canonicalUDPTest)
  478. fp := firewall.Packet{
  479. LocalAddr: dstIP,
  480. RemoteAddr: netip.MustParseAddr("10.0.0.1"),
  481. LocalPort: 80,
  482. RemotePort: 12345,
  483. Protocol: firewall.ProtoUDP,
  484. }
  485. cn := &conn{
  486. snat: &snatInfo{
  487. Src: netip.AddrPortFrom(netip.MustParseAddr("10.0.0.1"), 12345),
  488. SrcVpnIp: netip.MustParseAddr("fd00::99"), // Different VPN IP
  489. SnatPort: 55555,
  490. },
  491. }
  492. // Attacker has a different VPN address
  493. h := &HostInfo{vpnAddrs: []netip.Addr{peerV6Addr}}
  494. err := fw.applySnat(pkt, &fp, cn, h)
  495. assert.ErrorIs(t, err, ErrSNATIdentityMismatch)
  496. })
  497. t.Run("no snat addr configured", func(t *testing.T) {
  498. c := &dummyCert{
  499. networks: []netip.Prefix{netip.MustParsePrefix("fd00::1/128")},
  500. }
  501. fw := NewFirewall(l, time.Second, time.Minute, time.Hour, c)
  502. pkt := slices.Clone(canonicalUDPTest)
  503. fp := firewall.Packet{
  504. LocalAddr: dstIP,
  505. RemoteAddr: netip.MustParseAddr("10.0.0.1"),
  506. LocalPort: 80,
  507. RemotePort: 12345,
  508. Protocol: firewall.ProtoUDP,
  509. }
  510. cn := &conn{}
  511. h := &HostInfo{vpnAddrs: []netip.Addr{peerV6Addr}}
  512. err := fw.applySnat(pkt, &fp, cn, h)
  513. assert.ErrorIs(t, err, ErrCannotSNAT)
  514. })
  515. t.Run("v4 host rejected for new flow", func(t *testing.T) {
  516. c := &dummyCert{
  517. networks: []netip.Prefix{netip.MustParsePrefix("fd00::1/128")},
  518. unsafeNetworks: []netip.Prefix{netip.MustParsePrefix("192.168.0.0/16")},
  519. }
  520. fw := NewFirewall(l, time.Second, time.Minute, time.Hour, c)
  521. fw.snatAddr = snatAddr
  522. pkt := slices.Clone(canonicalUDPTest)
  523. fp := firewall.Packet{
  524. LocalAddr: dstIP,
  525. RemoteAddr: netip.MustParseAddr("10.0.0.1"),
  526. LocalPort: 80,
  527. RemotePort: 12345,
  528. Protocol: firewall.ProtoUDP,
  529. }
  530. cn := &conn{}
  531. // This host has a v4 address - can't SNAT for it
  532. h := &HostInfo{vpnAddrs: []netip.Addr{netip.MustParseAddr("10.0.0.50")}}
  533. err := fw.applySnat(pkt, &fp, cn, h)
  534. assert.ErrorIs(t, err, ErrCannotSNAT)
  535. })
  536. }
  537. func TestFirewall_UnSnat(t *testing.T) {
  538. l := logrus.New()
  539. l.SetLevel(logrus.PanicLevel)
  540. snatAddr := netip.MustParseAddr("169.254.55.96")
  541. peerV6Addr := netip.MustParseAddr("fd00::1")
  542. origSrcIP := netip.MustParseAddr("10.0.0.1")
  543. t.Run("successful unsnat", func(t *testing.T) {
  544. c := &dummyCert{
  545. networks: []netip.Prefix{netip.MustParsePrefix("fd00::1/128")},
  546. unsafeNetworks: []netip.Prefix{netip.MustParsePrefix("192.168.0.0/16")},
  547. }
  548. fw := NewFirewall(l, time.Second, time.Minute, time.Hour, c)
  549. fw.snatAddr = snatAddr
  550. // Create a conntrack entry for the snatted flow
  551. snatFP := firewall.Packet{
  552. LocalAddr: netip.MustParseAddr("192.168.1.1"),
  553. RemoteAddr: snatAddr,
  554. LocalPort: 80,
  555. RemotePort: 55555,
  556. Protocol: firewall.ProtoUDP,
  557. }
  558. fw.Conntrack.Lock()
  559. fw.Conntrack.Conns[snatFP] = &conn{
  560. snat: &snatInfo{
  561. Src: netip.AddrPortFrom(origSrcIP, 12345),
  562. SrcVpnIp: peerV6Addr,
  563. SnatPort: 55555,
  564. },
  565. }
  566. fw.Conntrack.Unlock()
  567. pkt := slices.Clone(canonicalUDPReply)
  568. fp := firewall.Packet{
  569. LocalAddr: netip.MustParseAddr("192.168.1.1"),
  570. RemoteAddr: snatAddr,
  571. LocalPort: 80,
  572. RemotePort: 55555,
  573. Protocol: firewall.ProtoUDP,
  574. }
  575. result := fw.unSnat(pkt, &fp)
  576. assert.True(t, result.IsValid())
  577. assert.Equal(t, peerV6Addr, result)
  578. // Destination should be rewritten to the original source
  579. gotDstIP, _ := netip.AddrFromSlice(pkt[16:20])
  580. assert.Equal(t, origSrcIP, gotDstIP)
  581. })
  582. t.Run("no conntrack entry", func(t *testing.T) {
  583. c := &dummyCert{
  584. networks: []netip.Prefix{netip.MustParsePrefix("fd00::1/128")},
  585. unsafeNetworks: []netip.Prefix{netip.MustParsePrefix("192.168.0.0/16")},
  586. }
  587. fw := NewFirewall(l, time.Second, time.Minute, time.Hour, c)
  588. fw.snatAddr = snatAddr
  589. pkt := slices.Clone(canonicalUDPReply)
  590. fp := firewall.Packet{
  591. LocalAddr: netip.MustParseAddr("192.168.1.1"),
  592. RemoteAddr: snatAddr,
  593. LocalPort: 80,
  594. RemotePort: 55555,
  595. Protocol: firewall.ProtoUDP,
  596. }
  597. result := fw.unSnat(pkt, &fp)
  598. assert.False(t, result.IsValid())
  599. })
  600. }
  601. func TestFirewall_Drop_SNATFullFlow(t *testing.T) {
  602. // Integration test: a complete SNAT flow through Drop
  603. l := logrus.New()
  604. l.SetLevel(logrus.PanicLevel)
  605. snatAddr := netip.MustParseAddr("169.254.55.96")
  606. myV6Prefix := netip.MustParsePrefix("fd00::1/128")
  607. unsafeNet := netip.MustParsePrefix("192.168.0.0/16")
  608. myCert := &dummyCert{
  609. name: "me",
  610. networks: []netip.Prefix{myV6Prefix},
  611. unsafeNetworks: []netip.Prefix{unsafeNet},
  612. groups: []string{"default-group"},
  613. issuer: "signer-shasum",
  614. }
  615. fw := NewFirewall(l, time.Second, time.Minute, time.Hour, myCert)
  616. fw.snatAddr = snatAddr
  617. require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"any"}, "", "", "any", "", ""))
  618. // Set up the peer: an IPv6-only host sending IPv4 traffic
  619. peerV6Addr := netip.MustParseAddr("fd00::2")
  620. peerCert := &dummyCert{
  621. name: "peer",
  622. networks: []netip.Prefix{netip.MustParsePrefix("fd00::2/128")},
  623. groups: []string{"default-group"},
  624. issuer: "signer-shasum",
  625. }
  626. myVpnNetworksTable := new(bart.Lite)
  627. myVpnNetworksTable.Insert(myV6Prefix)
  628. h := &HostInfo{
  629. ConnectionState: &ConnectionState{
  630. peerCert: &cert.CachedCertificate{
  631. Certificate: peerCert,
  632. InvertedGroups: map[string]struct{}{"default-group": {}},
  633. },
  634. },
  635. vpnAddrs: []netip.Addr{peerV6Addr},
  636. }
  637. h.buildNetworks(myVpnNetworksTable, peerCert)
  638. pkt := slices.Clone(canonicalUDPSnatMe)
  639. fp := firewall.Packet{
  640. LocalAddr: netip.MustParseAddr("192.168.1.1"),
  641. RemoteAddr: netip.MustParseAddr("10.0.0.1"),
  642. LocalPort: 80,
  643. RemotePort: 12345,
  644. Protocol: firewall.ProtoUDP,
  645. }
  646. cp := cert.NewCAPool()
  647. // Drop should succeed and SNAT the packet
  648. err := fw.Drop(fp, pkt, true, h, cp, nil)
  649. require.NoError(t, err)
  650. // After Drop, the source should be rewritten to the snat addr
  651. gotSrcIP, _ := netip.AddrFromSlice(pkt[12:16])
  652. assert.Equal(t, snatAddr, gotSrcIP)
  653. }
  654. func TestHasOnlyV6Addresses(t *testing.T) {
  655. t.Run("v6 only", func(t *testing.T) {
  656. h := &HostInfo{vpnAddrs: []netip.Addr{
  657. netip.MustParseAddr("fd00::1"),
  658. netip.MustParseAddr("fd00::2"),
  659. }}
  660. assert.True(t, h.HasOnlyV6Addresses())
  661. })
  662. t.Run("v4 only", func(t *testing.T) {
  663. h := &HostInfo{vpnAddrs: []netip.Addr{
  664. netip.MustParseAddr("10.0.0.1"),
  665. }}
  666. assert.False(t, h.HasOnlyV6Addresses())
  667. })
  668. t.Run("mixed v4 and v6", func(t *testing.T) {
  669. h := &HostInfo{vpnAddrs: []netip.Addr{
  670. netip.MustParseAddr("fd00::1"),
  671. netip.MustParseAddr("10.0.0.1"),
  672. }}
  673. assert.False(t, h.HasOnlyV6Addresses())
  674. })
  675. }
  676. // --- Adversarial SNAT Tests ---
  677. func TestFirewall_ApplySnat_CrossHostHijack(t *testing.T) {
  678. // Host A (fd00::1) establishes SNAT flow. Host B (fd00::2) sends a packet
  679. // matching the same conntrack key but with a different identity.
  680. // applySnat must reject with ErrSNATIdentityMismatch and leave the packet unmodified.
  681. l := logrus.New()
  682. l.SetLevel(logrus.PanicLevel)
  683. snatAddr := netip.MustParseAddr("169.254.55.96")
  684. hostA := netip.MustParseAddr("fd00::1")
  685. hostB := netip.MustParseAddr("fd00::2")
  686. c := &dummyCert{
  687. networks: []netip.Prefix{netip.MustParsePrefix("fd00::1/128")},
  688. unsafeNetworks: []netip.Prefix{netip.MustParsePrefix("192.168.0.0/16")},
  689. }
  690. fw := NewFirewall(l, time.Second, time.Minute, time.Hour, c)
  691. fw.snatAddr = snatAddr
  692. // Simulate Host A having established a flow
  693. cn := &conn{
  694. snat: &snatInfo{
  695. Src: netip.AddrPortFrom(netip.MustParseAddr("10.0.0.1"), 12345),
  696. SrcVpnIp: hostA,
  697. SnatPort: 55555,
  698. },
  699. }
  700. // Host B tries to reuse the same conntrack entry
  701. pkt := slices.Clone(canonicalUDPHijack)
  702. fp := firewall.Packet{
  703. LocalAddr: netip.MustParseAddr("192.168.1.1"),
  704. RemoteAddr: netip.MustParseAddr("10.0.0.1"),
  705. LocalPort: 80,
  706. RemotePort: 12345,
  707. Protocol: firewall.ProtoUDP,
  708. }
  709. hB := &HostInfo{vpnAddrs: []netip.Addr{hostB}}
  710. err := fw.applySnat(pkt, &fp, cn, hB)
  711. require.ErrorIs(t, err, ErrSNATIdentityMismatch)
  712. assert.Equal(t, canonicalUDPHijack, pkt, "packet bytes must be unmodified after identity mismatch")
  713. }
  714. func TestFirewall_ApplySnat_MixedStackRejected(t *testing.T) {
  715. // A host with both v4 and v6 VPN addresses should never get SNAT treatment.
  716. // Test both orderings of vpnAddrs to verify behavior.
  717. l := logrus.New()
  718. l.SetLevel(logrus.PanicLevel)
  719. snatAddr := netip.MustParseAddr("169.254.55.96")
  720. dstIP := netip.MustParseAddr("192.168.1.1")
  721. c := &dummyCert{
  722. networks: []netip.Prefix{netip.MustParsePrefix("fd00::1/128")},
  723. unsafeNetworks: []netip.Prefix{netip.MustParsePrefix("192.168.0.0/16")},
  724. }
  725. t.Run("v6 first then v4", func(t *testing.T) {
  726. fw := NewFirewall(l, time.Second, time.Minute, time.Hour, c)
  727. fw.snatAddr = snatAddr
  728. pkt := slices.Clone(canonicalUDPTest)
  729. fp := firewall.Packet{
  730. LocalAddr: dstIP,
  731. RemoteAddr: netip.MustParseAddr("10.0.0.1"),
  732. LocalPort: 80,
  733. RemotePort: 12345,
  734. Protocol: firewall.ProtoUDP,
  735. }
  736. cn := &conn{}
  737. // Mixed-stack: v6 first. applySnat checks vpnAddrs[0].Is6() which is true,
  738. // so it would create a flow. But the caller (Drop) guards with HasOnlyV6Addresses().
  739. // This test documents that applySnat alone doesn't prevent mixed-stack SNAT.
  740. h := &HostInfo{vpnAddrs: []netip.Addr{
  741. netip.MustParseAddr("fd00::1"),
  742. netip.MustParseAddr("10.0.0.50"),
  743. }}
  744. err := fw.applySnat(pkt, &fp, cn, h)
  745. // applySnat only checks vpnAddrs[0].Is6(), so this succeeds.
  746. // The real guard is in Drop() via HasOnlyV6Addresses().
  747. assert.NoError(t, err, "applySnat alone allows v6-first mixed-stack (guarded by Drop)")
  748. })
  749. t.Run("v4 first then v6", func(t *testing.T) {
  750. fw := NewFirewall(l, time.Second, time.Minute, time.Hour, c)
  751. fw.snatAddr = snatAddr
  752. pkt := slices.Clone(canonicalUDPTest)
  753. fp := firewall.Packet{
  754. LocalAddr: dstIP,
  755. RemoteAddr: netip.MustParseAddr("10.0.0.1"),
  756. LocalPort: 80,
  757. RemotePort: 12345,
  758. Protocol: firewall.ProtoUDP,
  759. }
  760. cn := &conn{}
  761. // Mixed-stack: v4 first. vpnAddrs[0].Is6() is false -> ErrCannotSNAT.
  762. h := &HostInfo{vpnAddrs: []netip.Addr{
  763. netip.MustParseAddr("10.0.0.50"),
  764. netip.MustParseAddr("fd00::1"),
  765. }}
  766. err := fw.applySnat(pkt, &fp, cn, h)
  767. require.ErrorIs(t, err, ErrCannotSNAT)
  768. assert.Equal(t, canonicalUDPTest, pkt, "packet bytes must be unmodified on error")
  769. })
  770. }
  771. func TestFirewall_ApplySnat_PacketUnmodifiedOnError(t *testing.T) {
  772. // When applySnat returns an error, the packet must not be partially rewritten.
  773. l := logrus.New()
  774. l.SetLevel(logrus.PanicLevel)
  775. dstIP := netip.MustParseAddr("192.168.1.1")
  776. t.Run("no snatAddr configured", func(t *testing.T) {
  777. c := &dummyCert{
  778. networks: []netip.Prefix{netip.MustParsePrefix("fd00::1/128")},
  779. }
  780. fw := NewFirewall(l, time.Second, time.Minute, time.Hour, c)
  781. pkt := slices.Clone(canonicalUDPTest)
  782. fp := firewall.Packet{
  783. LocalAddr: dstIP,
  784. RemoteAddr: netip.MustParseAddr("10.0.0.1"),
  785. LocalPort: 80,
  786. RemotePort: 12345,
  787. Protocol: firewall.ProtoUDP,
  788. }
  789. cn := &conn{}
  790. h := &HostInfo{vpnAddrs: []netip.Addr{netip.MustParseAddr("fd00::1")}}
  791. err := fw.applySnat(pkt, &fp, cn, h)
  792. require.Error(t, err)
  793. assert.Equal(t, canonicalUDPTest, pkt, "packet must be byte-for-byte identical after error")
  794. })
  795. t.Run("identity mismatch", func(t *testing.T) {
  796. snatAddr := netip.MustParseAddr("169.254.55.96")
  797. c := &dummyCert{
  798. networks: []netip.Prefix{netip.MustParsePrefix("fd00::1/128")},
  799. unsafeNetworks: []netip.Prefix{netip.MustParsePrefix("192.168.0.0/16")},
  800. }
  801. fw := NewFirewall(l, time.Second, time.Minute, time.Hour, c)
  802. fw.snatAddr = snatAddr
  803. pkt := slices.Clone(canonicalUDPTest)
  804. fp := firewall.Packet{
  805. LocalAddr: dstIP,
  806. RemoteAddr: netip.MustParseAddr("10.0.0.1"),
  807. LocalPort: 80,
  808. RemotePort: 12345,
  809. Protocol: firewall.ProtoUDP,
  810. }
  811. cn := &conn{
  812. snat: &snatInfo{
  813. Src: netip.AddrPortFrom(netip.MustParseAddr("10.0.0.1"), 12345),
  814. SrcVpnIp: netip.MustParseAddr("fd00::99"),
  815. SnatPort: 55555,
  816. },
  817. }
  818. h := &HostInfo{vpnAddrs: []netip.Addr{netip.MustParseAddr("fd00::1")}}
  819. err := fw.applySnat(pkt, &fp, cn, h)
  820. require.ErrorIs(t, err, ErrSNATIdentityMismatch)
  821. assert.Equal(t, canonicalUDPTest, pkt, "packet must be byte-for-byte identical after identity mismatch")
  822. })
  823. t.Run("v4 host rejected", func(t *testing.T) {
  824. snatAddr := netip.MustParseAddr("169.254.55.96")
  825. c := &dummyCert{
  826. networks: []netip.Prefix{netip.MustParsePrefix("fd00::1/128")},
  827. unsafeNetworks: []netip.Prefix{netip.MustParsePrefix("192.168.0.0/16")},
  828. }
  829. fw := NewFirewall(l, time.Second, time.Minute, time.Hour, c)
  830. fw.snatAddr = snatAddr
  831. pkt := slices.Clone(canonicalUDPTest)
  832. fp := firewall.Packet{
  833. LocalAddr: dstIP,
  834. RemoteAddr: netip.MustParseAddr("10.0.0.1"),
  835. LocalPort: 80,
  836. RemotePort: 12345,
  837. Protocol: firewall.ProtoUDP,
  838. }
  839. cn := &conn{}
  840. h := &HostInfo{vpnAddrs: []netip.Addr{netip.MustParseAddr("10.0.0.50")}}
  841. err := fw.applySnat(pkt, &fp, cn, h)
  842. require.ErrorIs(t, err, ErrCannotSNAT)
  843. assert.Equal(t, canonicalUDPTest, pkt, "packet must be byte-for-byte identical after v4 host rejection")
  844. })
  845. }
  846. func TestFirewall_UnSnat_NonSNATConntrack(t *testing.T) {
  847. // A conntrack entry exists but has snat=nil. unSnat should return an invalid addr
  848. // and not rewrite the packet.
  849. l := logrus.New()
  850. l.SetLevel(logrus.PanicLevel)
  851. snatAddr := netip.MustParseAddr("169.254.55.96")
  852. c := &dummyCert{
  853. networks: []netip.Prefix{netip.MustParsePrefix("fd00::1/128")},
  854. unsafeNetworks: []netip.Prefix{netip.MustParsePrefix("192.168.0.0/16")},
  855. }
  856. fw := NewFirewall(l, time.Second, time.Minute, time.Hour, c)
  857. fw.snatAddr = snatAddr
  858. // Create a conntrack entry with snat=nil (a normal non-SNAT connection)
  859. snatFP := firewall.Packet{
  860. LocalAddr: netip.MustParseAddr("192.168.1.1"),
  861. RemoteAddr: snatAddr,
  862. LocalPort: 80,
  863. RemotePort: 55555,
  864. Protocol: firewall.ProtoUDP,
  865. }
  866. fw.Conntrack.Lock()
  867. fw.Conntrack.Conns[snatFP] = &conn{
  868. snat: nil, // deliberately nil
  869. }
  870. fw.Conntrack.Unlock()
  871. pkt := slices.Clone(canonicalUDPReply)
  872. fp := firewall.Packet{
  873. LocalAddr: netip.MustParseAddr("192.168.1.1"),
  874. RemoteAddr: snatAddr,
  875. LocalPort: 80,
  876. RemotePort: 55555,
  877. Protocol: firewall.ProtoUDP,
  878. }
  879. result := fw.unSnat(pkt, &fp)
  880. assert.False(t, result.IsValid(), "unSnat should return invalid addr for non-SNAT conntrack entry")
  881. assert.Equal(t, canonicalUDPReply, pkt, "packet must not be rewritten when conntrack has no snat info")
  882. }
  883. func TestFirewall_Drop_FirewallBlocksSNAT(t *testing.T) {
  884. // Firewall rules only allow port 80. An SNAT-eligible packet to port 443
  885. // must be rejected with ErrNoMatchingRule BEFORE any SNAT rewriting occurs.
  886. l := logrus.New()
  887. l.SetLevel(logrus.PanicLevel)
  888. snatAddr := netip.MustParseAddr("169.254.55.96")
  889. myCert := &dummyCert{
  890. name: "me",
  891. networks: []netip.Prefix{netip.MustParsePrefix("fd00::1/128")},
  892. unsafeNetworks: []netip.Prefix{netip.MustParsePrefix("192.168.0.0/16")},
  893. groups: []string{"default-group"},
  894. issuer: "signer-shasum",
  895. }
  896. fw := NewFirewall(l, time.Second, time.Minute, time.Hour, myCert)
  897. fw.snatAddr = snatAddr
  898. // Only allow port 80 inbound
  899. require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 80, 80, []string{"any"}, "", "", "any", "", ""))
  900. peerV6Addr := netip.MustParseAddr("fd00::2")
  901. peerCert := &dummyCert{
  902. name: "peer",
  903. networks: []netip.Prefix{netip.MustParsePrefix("fd00::2/128")},
  904. groups: []string{"default-group"},
  905. issuer: "signer-shasum",
  906. }
  907. myVpnNetworksTable := new(bart.Lite)
  908. myVpnNetworksTable.Insert(netip.MustParsePrefix("fd00::1/128"))
  909. h := &HostInfo{
  910. ConnectionState: &ConnectionState{
  911. peerCert: &cert.CachedCertificate{
  912. Certificate: peerCert,
  913. InvertedGroups: map[string]struct{}{"default-group": {}},
  914. },
  915. },
  916. vpnAddrs: []netip.Addr{peerV6Addr},
  917. }
  918. h.buildNetworks(myVpnNetworksTable, peerCert)
  919. // Send to port 443 (not allowed)
  920. pkt := slices.Clone(canonicalUDPBlocked)
  921. fp := firewall.Packet{
  922. LocalAddr: netip.MustParseAddr("192.168.1.1"),
  923. RemoteAddr: netip.MustParseAddr("10.0.0.1"),
  924. LocalPort: 443,
  925. RemotePort: 12345,
  926. Protocol: firewall.ProtoUDP,
  927. }
  928. cp := cert.NewCAPool()
  929. err := fw.Drop(fp, pkt, true, h, cp, nil)
  930. require.ErrorIs(t, err, ErrNoMatchingRule, "firewall should block SNAT-eligible traffic that doesn't match rules")
  931. assert.Equal(t, canonicalUDPBlocked, pkt, "packet must not be rewritten when firewall blocks it")
  932. }
  933. func TestFirewall_Drop_SNATLocalAddrNotRoutable(t *testing.T) {
  934. // An SNAT peer sends IPv4 traffic to an address NOT in routableNetworks.
  935. // willingToHandleLocalAddr should reject with ErrInvalidLocalIP.
  936. l := logrus.New()
  937. l.SetLevel(logrus.PanicLevel)
  938. snatAddr := netip.MustParseAddr("169.254.55.96")
  939. myCert := &dummyCert{
  940. name: "me",
  941. networks: []netip.Prefix{netip.MustParsePrefix("fd00::1/128")},
  942. unsafeNetworks: []netip.Prefix{netip.MustParsePrefix("192.168.0.0/16")},
  943. groups: []string{"default-group"},
  944. issuer: "signer-shasum",
  945. }
  946. fw := NewFirewall(l, time.Second, time.Minute, time.Hour, myCert)
  947. fw.snatAddr = snatAddr
  948. require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"any"}, "", "", "any", "", ""))
  949. peerV6Addr := netip.MustParseAddr("fd00::2")
  950. peerCert := &dummyCert{
  951. name: "peer",
  952. networks: []netip.Prefix{netip.MustParsePrefix("fd00::2/128")},
  953. groups: []string{"default-group"},
  954. issuer: "signer-shasum",
  955. }
  956. myVpnNetworksTable := new(bart.Lite)
  957. myVpnNetworksTable.Insert(netip.MustParsePrefix("fd00::1/128"))
  958. h := &HostInfo{
  959. ConnectionState: &ConnectionState{
  960. peerCert: &cert.CachedCertificate{
  961. Certificate: peerCert,
  962. InvertedGroups: map[string]struct{}{"default-group": {}},
  963. },
  964. },
  965. vpnAddrs: []netip.Addr{peerV6Addr},
  966. }
  967. h.buildNetworks(myVpnNetworksTable, peerCert)
  968. // Dest 172.16.0.1 is NOT in our routableNetworks (which only has fd00::1/128 and 192.168.0.0/16)
  969. pkt := slices.Clone(canonicalUDPWrongDest)
  970. fp := firewall.Packet{
  971. LocalAddr: netip.MustParseAddr("172.16.0.1"),
  972. RemoteAddr: netip.MustParseAddr("10.0.0.1"),
  973. LocalPort: 80,
  974. RemotePort: 12345,
  975. Protocol: firewall.ProtoUDP,
  976. }
  977. cp := cert.NewCAPool()
  978. err := fw.Drop(fp, pkt, true, h, cp, nil)
  979. assert.ErrorIs(t, err, ErrInvalidLocalIP, "traffic to non-routable local address should be rejected")
  980. }
  981. func TestFirewall_Drop_NoSnatAddrRejectsV6Peer(t *testing.T) {
  982. // Firewall has no snatAddr configured. An IPv6-only peer sends IPv4 traffic.
  983. // allowRemoteNetworkType(UncheckedSNATPeer) should reject with ErrInvalidRemoteIP.
  984. l := logrus.New()
  985. l.SetLevel(logrus.PanicLevel)
  986. myCert := &dummyCert{
  987. name: "me",
  988. networks: []netip.Prefix{netip.MustParsePrefix("fd00::1/128")},
  989. groups: []string{"default-group"},
  990. issuer: "signer-shasum",
  991. }
  992. fw := NewFirewall(l, time.Second, time.Minute, time.Hour, myCert)
  993. require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"any"}, "", "", "any", "", ""))
  994. peerV6Addr := netip.MustParseAddr("fd00::2")
  995. peerCert := &dummyCert{
  996. name: "peer",
  997. networks: []netip.Prefix{netip.MustParsePrefix("fd00::2/128")},
  998. groups: []string{"default-group"},
  999. issuer: "signer-shasum",
  1000. }
  1001. myVpnNetworksTable := new(bart.Lite)
  1002. myVpnNetworksTable.Insert(netip.MustParsePrefix("fd00::1/128"))
  1003. h := &HostInfo{
  1004. ConnectionState: &ConnectionState{
  1005. peerCert: &cert.CachedCertificate{
  1006. Certificate: peerCert,
  1007. InvertedGroups: map[string]struct{}{"default-group": {}},
  1008. },
  1009. },
  1010. vpnAddrs: []netip.Addr{peerV6Addr},
  1011. }
  1012. h.buildNetworks(myVpnNetworksTable, peerCert)
  1013. pkt := slices.Clone(canonicalUDPNoSnat)
  1014. fp := firewall.Packet{
  1015. LocalAddr: netip.MustParseAddr("192.168.1.1"),
  1016. RemoteAddr: netip.MustParseAddr("10.0.0.1"),
  1017. LocalPort: 80,
  1018. RemotePort: 12345,
  1019. Protocol: firewall.ProtoUDP,
  1020. }
  1021. cp := cert.NewCAPool()
  1022. err := fw.Drop(fp, pkt, true, h, cp, nil)
  1023. assert.ErrorIs(t, err, ErrInvalidRemoteIP, "v6 peer with no snatAddr should be rejected")
  1024. }
  1025. func TestFirewall_Drop_IPv4HostNotSNATted(t *testing.T) {
  1026. // An IPv4 VPN host sends IPv4 traffic. Even though the router has snatAddr
  1027. // configured and the traffic is IPv4, the firewall must NOT treat this as
  1028. // UncheckedSNATPeer. The packet must not be SNAT-rewritten.
  1029. l := logrus.New()
  1030. l.SetLevel(logrus.PanicLevel)
  1031. snatAddr := netip.MustParseAddr("169.254.55.96")
  1032. t.Run("v6-only router rejects v4 peer as VPNPeer", func(t *testing.T) {
  1033. // When the router is v6-only, the v4 peer's address is outside our VPN
  1034. // networks -> classified as NetworkTypeVPNPeer -> rejected (not SNATted).
  1035. myCert := &dummyCert{
  1036. name: "me",
  1037. networks: []netip.Prefix{netip.MustParsePrefix("fd00::1/128")},
  1038. unsafeNetworks: []netip.Prefix{netip.MustParsePrefix("192.168.0.0/16")},
  1039. groups: []string{"default-group"},
  1040. issuer: "signer-shasum",
  1041. }
  1042. fw := NewFirewall(l, time.Second, time.Minute, time.Hour, myCert)
  1043. fw.snatAddr = snatAddr
  1044. require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"any"}, "", "", "any", "", ""))
  1045. peerV4Addr := netip.MustParseAddr("10.128.0.2")
  1046. peerCert := &dummyCert{
  1047. name: "v4peer",
  1048. networks: []netip.Prefix{netip.MustParsePrefix("10.128.0.2/24")},
  1049. groups: []string{"default-group"},
  1050. issuer: "signer-shasum",
  1051. }
  1052. myVpnNetworksTable := new(bart.Lite)
  1053. myVpnNetworksTable.Insert(netip.MustParsePrefix("fd00::1/128"))
  1054. h := &HostInfo{
  1055. ConnectionState: &ConnectionState{
  1056. peerCert: &cert.CachedCertificate{
  1057. Certificate: peerCert,
  1058. InvertedGroups: map[string]struct{}{"default-group": {}},
  1059. },
  1060. },
  1061. vpnAddrs: []netip.Addr{peerV4Addr},
  1062. }
  1063. h.buildNetworks(myVpnNetworksTable, peerCert)
  1064. pkt := slices.Clone(canonicalUDPV4Traffic)
  1065. fp := firewall.Packet{
  1066. LocalAddr: netip.MustParseAddr("192.168.1.1"),
  1067. RemoteAddr: netip.MustParseAddr("10.128.0.2"),
  1068. LocalPort: 80,
  1069. RemotePort: 12345,
  1070. Protocol: firewall.ProtoUDP,
  1071. }
  1072. cp := cert.NewCAPool()
  1073. err := fw.Drop(fp, pkt, true, h, cp, nil)
  1074. require.ErrorIs(t, err, ErrPeerRejected, "IPv4 peer should be rejected as VPNPeer, not treated as SNAT")
  1075. assert.Equal(t, canonicalUDPV4Traffic, pkt, "packet must not be rewritten when peer is rejected")
  1076. })
  1077. t.Run("identifyRemoteNetworkType classifies v4 peer correctly", func(t *testing.T) {
  1078. // Directly verify that identifyRemoteNetworkType returns the right type for
  1079. // an IPv4 peer (not UncheckedSNATPeer).
  1080. fw := &Firewall{snatAddr: snatAddr}
  1081. // Simple case: v4 host, no networks table
  1082. h := &HostInfo{vpnAddrs: []netip.Addr{netip.MustParseAddr("10.128.0.2")}}
  1083. fp := firewall.Packet{
  1084. RemoteAddr: netip.MustParseAddr("10.128.0.2"),
  1085. LocalAddr: netip.MustParseAddr("192.168.1.1"),
  1086. }
  1087. nwType := fw.identifyRemoteNetworkType(h, fp)
  1088. assert.Equal(t, NetworkTypeVPN, nwType, "v4 peer using its own VPN addr should be NetworkTypeVPN")
  1089. assert.NotEqual(t, NetworkTypeUnverifiedSNATPeer, nwType, "must NOT be classified as SNAT peer")
  1090. })
  1091. t.Run("identifyRemoteNetworkType v4 peer with mismatched source", func(t *testing.T) {
  1092. // v4 host sends with a source IP that doesn't match its VPN addr
  1093. fw := &Firewall{snatAddr: snatAddr}
  1094. h := &HostInfo{vpnAddrs: []netip.Addr{netip.MustParseAddr("10.128.0.2")}}
  1095. fp := firewall.Packet{
  1096. RemoteAddr: netip.MustParseAddr("10.0.0.99"), // Not the peer's VPN addr
  1097. LocalAddr: netip.MustParseAddr("192.168.1.1"),
  1098. }
  1099. nwType := fw.identifyRemoteNetworkType(h, fp)
  1100. assert.Equal(t, NetworkTypeInvalidPeer, nwType, "v4 peer with mismatched source should be InvalidPeer")
  1101. assert.NotEqual(t, NetworkTypeUnverifiedSNATPeer, nwType, "must NOT be classified as SNAT peer")
  1102. })
  1103. }