firewall_test.go 71 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994
  1. package nebula
  2. import (
  3. "bytes"
  4. "errors"
  5. "fmt"
  6. "log/slog"
  7. "math"
  8. "net/netip"
  9. "testing"
  10. "time"
  11. "github.com/gaissmai/bart"
  12. "github.com/slackhq/nebula/cert"
  13. "github.com/slackhq/nebula/config"
  14. "github.com/slackhq/nebula/firewall"
  15. "github.com/slackhq/nebula/iputil"
  16. "github.com/slackhq/nebula/test"
  17. "github.com/stretchr/testify/assert"
  18. "github.com/stretchr/testify/require"
  19. )
  20. func TestNewFirewall(t *testing.T) {
  21. l := test.NewLogger()
  22. c := &dummyCert{}
  23. fw := NewFirewall(l, time.Second, time.Minute, time.Hour, c)
  24. conntrack := fw.Conntrack
  25. assert.NotNil(t, conntrack)
  26. assert.NotNil(t, conntrack.Conns)
  27. assert.NotNil(t, conntrack.TimerWheel)
  28. assert.NotNil(t, fw.InRules)
  29. assert.NotNil(t, fw.OutRules)
  30. assert.Equal(t, time.Second, fw.TCPTimeout)
  31. assert.Equal(t, time.Minute, fw.UDPTimeout)
  32. assert.Equal(t, time.Hour, fw.DefaultTimeout)
  33. assert.Equal(t, time.Hour, conntrack.TimerWheel.wheelDuration)
  34. assert.Equal(t, time.Hour, conntrack.TimerWheel.wheelDuration)
  35. assert.Equal(t, 3602, conntrack.TimerWheel.wheelLen)
  36. fw = NewFirewall(l, time.Second, time.Hour, time.Minute, c)
  37. assert.Equal(t, time.Hour, conntrack.TimerWheel.wheelDuration)
  38. assert.Equal(t, 3602, conntrack.TimerWheel.wheelLen)
  39. fw = NewFirewall(l, time.Hour, time.Second, time.Minute, c)
  40. assert.Equal(t, time.Hour, conntrack.TimerWheel.wheelDuration)
  41. assert.Equal(t, 3602, conntrack.TimerWheel.wheelLen)
  42. fw = NewFirewall(l, time.Hour, time.Minute, time.Second, c)
  43. assert.Equal(t, time.Hour, conntrack.TimerWheel.wheelDuration)
  44. assert.Equal(t, 3602, conntrack.TimerWheel.wheelLen)
  45. fw = NewFirewall(l, time.Minute, time.Hour, time.Second, c)
  46. assert.Equal(t, time.Hour, conntrack.TimerWheel.wheelDuration)
  47. assert.Equal(t, 3602, conntrack.TimerWheel.wheelLen)
  48. fw = NewFirewall(l, time.Minute, time.Second, time.Hour, c)
  49. assert.Equal(t, time.Hour, conntrack.TimerWheel.wheelDuration)
  50. assert.Equal(t, 3602, conntrack.TimerWheel.wheelLen)
  51. }
  52. func TestFirewall_AddRule(t *testing.T) {
  53. ob := &bytes.Buffer{}
  54. l := test.NewLoggerWithOutput(ob)
  55. c := &dummyCert{}
  56. fw := NewFirewall(l, time.Second, time.Minute, time.Hour, c)
  57. assert.NotNil(t, fw.InRules)
  58. assert.NotNil(t, fw.OutRules)
  59. ti, err := netip.ParsePrefix("1.2.3.4/32")
  60. require.NoError(t, err)
  61. ti6, err := netip.ParsePrefix("fd12::34/128")
  62. require.NoError(t, err)
  63. require.NoError(t, fw.AddRule(true, iputil.IPProtocolTCP, 1, 1, []string{}, "", "", "", "", ""))
  64. // An empty rule is any
  65. assert.True(t, fw.InRules.TCP[1].Any.Any.Any)
  66. assert.Empty(t, fw.InRules.TCP[1].Any.Groups)
  67. assert.Empty(t, fw.InRules.TCP[1].Any.Hosts)
  68. fw = NewFirewall(l, time.Second, time.Minute, time.Hour, c)
  69. require.NoError(t, fw.AddRule(true, iputil.IPProtocolUDP, 1, 1, []string{"g1"}, "", "", "", "", ""))
  70. assert.Nil(t, fw.InRules.UDP[1].Any.Any)
  71. assert.Contains(t, fw.InRules.UDP[1].Any.Groups[0].Groups, "g1")
  72. assert.Empty(t, fw.InRules.UDP[1].Any.Hosts)
  73. fw = NewFirewall(l, time.Second, time.Minute, time.Hour, c)
  74. require.NoError(t, fw.AddRule(true, iputil.IPProtocolICMP, 1, 1, []string{}, "h1", "", "", "", ""))
  75. //no matter what port is given for icmp, it should end up as "any"
  76. assert.Nil(t, fw.InRules.ICMP[firewall.PortAny].Any.Any)
  77. assert.Empty(t, fw.InRules.ICMP[firewall.PortAny].Any.Groups)
  78. assert.Contains(t, fw.InRules.ICMP[firewall.PortAny].Any.Hosts, "h1")
  79. fw = NewFirewall(l, time.Second, time.Minute, time.Hour, c)
  80. require.NoError(t, fw.AddRule(false, firewall.ProtoAny, 1, 1, []string{}, "", ti.String(), "", "", ""))
  81. assert.Nil(t, fw.OutRules.AnyProto[1].Any.Any)
  82. _, ok := fw.OutRules.AnyProto[1].Any.CIDR.Get(ti)
  83. assert.True(t, ok)
  84. fw = NewFirewall(l, time.Second, time.Minute, time.Hour, c)
  85. require.NoError(t, fw.AddRule(false, firewall.ProtoAny, 1, 1, []string{}, "", ti6.String(), "", "", ""))
  86. assert.Nil(t, fw.OutRules.AnyProto[1].Any.Any)
  87. _, ok = fw.OutRules.AnyProto[1].Any.CIDR.Get(ti6)
  88. assert.True(t, ok)
  89. fw = NewFirewall(l, time.Second, time.Minute, time.Hour, c)
  90. require.NoError(t, fw.AddRule(false, firewall.ProtoAny, 1, 1, []string{}, "", "", ti.String(), "", ""))
  91. assert.NotNil(t, fw.OutRules.AnyProto[1].Any.Any)
  92. ok = fw.OutRules.AnyProto[1].Any.Any.LocalCIDR.Get(ti)
  93. assert.True(t, ok)
  94. fw = NewFirewall(l, time.Second, time.Minute, time.Hour, c)
  95. require.NoError(t, fw.AddRule(false, firewall.ProtoAny, 1, 1, []string{}, "", "", ti6.String(), "", ""))
  96. assert.NotNil(t, fw.OutRules.AnyProto[1].Any.Any)
  97. ok = fw.OutRules.AnyProto[1].Any.Any.LocalCIDR.Get(ti6)
  98. assert.True(t, ok)
  99. fw = NewFirewall(l, time.Second, time.Minute, time.Hour, c)
  100. require.NoError(t, fw.AddRule(true, iputil.IPProtocolUDP, 1, 1, []string{"g1"}, "", "", "", "ca-name", ""))
  101. assert.Contains(t, fw.InRules.UDP[1].CANames, "ca-name")
  102. fw = NewFirewall(l, time.Second, time.Minute, time.Hour, c)
  103. require.NoError(t, fw.AddRule(true, iputil.IPProtocolUDP, 1, 1, []string{"g1"}, "", "", "", "", "ca-sha"))
  104. assert.Contains(t, fw.InRules.UDP[1].CAShas, "ca-sha")
  105. fw = NewFirewall(l, time.Second, time.Minute, time.Hour, c)
  106. require.NoError(t, fw.AddRule(false, firewall.ProtoAny, 0, 0, []string{}, "any", "", "", "", ""))
  107. assert.True(t, fw.OutRules.AnyProto[0].Any.Any.Any)
  108. fw = NewFirewall(l, time.Second, time.Minute, time.Hour, c)
  109. anyIp, err := netip.ParsePrefix("0.0.0.0/0")
  110. require.NoError(t, err)
  111. require.NoError(t, fw.AddRule(false, firewall.ProtoAny, 0, 0, []string{}, "", anyIp.String(), "", "", ""))
  112. assert.Nil(t, fw.OutRules.AnyProto[0].Any.Any)
  113. table, ok := fw.OutRules.AnyProto[0].Any.CIDR.Lookup(netip.MustParseAddr("1.1.1.1"))
  114. assert.True(t, table.Any)
  115. table, ok = fw.OutRules.AnyProto[0].Any.CIDR.Lookup(netip.MustParseAddr("9::9"))
  116. assert.False(t, ok)
  117. fw = NewFirewall(l, time.Second, time.Minute, time.Hour, c)
  118. anyIp6, err := netip.ParsePrefix("::/0")
  119. require.NoError(t, err)
  120. require.NoError(t, fw.AddRule(false, firewall.ProtoAny, 0, 0, []string{}, "", anyIp6.String(), "", "", ""))
  121. assert.Nil(t, fw.OutRules.AnyProto[0].Any.Any)
  122. table, ok = fw.OutRules.AnyProto[0].Any.CIDR.Lookup(netip.MustParseAddr("9::9"))
  123. assert.True(t, table.Any)
  124. table, ok = fw.OutRules.AnyProto[0].Any.CIDR.Lookup(netip.MustParseAddr("1.1.1.1"))
  125. assert.False(t, ok)
  126. fw = NewFirewall(l, time.Second, time.Minute, time.Hour, c)
  127. require.NoError(t, fw.AddRule(false, firewall.ProtoAny, 0, 0, []string{}, "", "any", "", "", ""))
  128. assert.True(t, fw.OutRules.AnyProto[0].Any.Any.Any)
  129. fw = NewFirewall(l, time.Second, time.Minute, time.Hour, c)
  130. require.NoError(t, fw.AddRule(false, firewall.ProtoAny, 0, 0, []string{}, "", "", anyIp.String(), "", ""))
  131. assert.False(t, fw.OutRules.AnyProto[0].Any.Any.Any)
  132. assert.True(t, fw.OutRules.AnyProto[0].Any.Any.LocalCIDR.Lookup(netip.MustParseAddr("1.1.1.1")))
  133. assert.False(t, fw.OutRules.AnyProto[0].Any.Any.LocalCIDR.Lookup(netip.MustParseAddr("9::9")))
  134. fw = NewFirewall(l, time.Second, time.Minute, time.Hour, c)
  135. require.NoError(t, fw.AddRule(false, firewall.ProtoAny, 0, 0, []string{}, "", "", anyIp6.String(), "", ""))
  136. assert.False(t, fw.OutRules.AnyProto[0].Any.Any.Any)
  137. assert.True(t, fw.OutRules.AnyProto[0].Any.Any.LocalCIDR.Lookup(netip.MustParseAddr("9::9")))
  138. assert.False(t, fw.OutRules.AnyProto[0].Any.Any.LocalCIDR.Lookup(netip.MustParseAddr("1.1.1.1")))
  139. fw = NewFirewall(l, time.Second, time.Minute, time.Hour, c)
  140. require.NoError(t, fw.AddRule(false, firewall.ProtoAny, 0, 0, []string{}, "", "", "any", "", ""))
  141. assert.True(t, fw.OutRules.AnyProto[0].Any.Any.Any)
  142. // Test error conditions
  143. fw = NewFirewall(l, time.Second, time.Minute, time.Hour, c)
  144. require.Error(t, fw.AddRule(true, math.MaxUint8, 0, 0, []string{}, "", "", "", "", ""))
  145. require.Error(t, fw.AddRule(true, firewall.ProtoAny, 10, 0, []string{}, "", "", "", "", ""))
  146. }
  147. func TestFirewall_Drop(t *testing.T) {
  148. ob := &bytes.Buffer{}
  149. l := test.NewLoggerWithOutput(ob)
  150. myVpnNetworksTable := new(bart.Lite)
  151. myVpnNetworksTable.Insert(netip.MustParsePrefix("1.1.1.1/8"))
  152. p := firewall.Packet{
  153. LocalAddr: netip.MustParseAddr("1.2.3.4"),
  154. RemoteAddr: netip.MustParseAddr("1.2.3.4"),
  155. LocalPort: 10,
  156. RemotePort: 90,
  157. Protocol: iputil.IPProtocolUDP,
  158. Fragment: false,
  159. }
  160. c := dummyCert{
  161. name: "host1",
  162. networks: []netip.Prefix{netip.MustParsePrefix("1.2.3.4/24")},
  163. groups: []string{"default-group"},
  164. issuer: "signer-shasum",
  165. }
  166. h := HostInfo{
  167. ConnectionState: &ConnectionState{
  168. peerCert: &cert.CachedCertificate{
  169. Certificate: &c,
  170. InvertedGroups: map[string]struct{}{"default-group": {}},
  171. },
  172. },
  173. vpnAddrs: []netip.Addr{netip.MustParseAddr("1.2.3.4")},
  174. }
  175. h.buildNetworks(myVpnNetworksTable, &c)
  176. fw := NewFirewall(l, time.Second, time.Minute, time.Hour, &c)
  177. require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"any"}, "", "", "", "", ""))
  178. cp := cert.NewCAPool()
  179. // Drop outbound
  180. assert.Equal(t, ErrNoMatchingRule, fw.Drop(p, false, &h, cp, nil))
  181. // Allow inbound
  182. resetConntrack(fw)
  183. require.NoError(t, fw.Drop(p, true, &h, cp, nil))
  184. // Allow outbound because conntrack
  185. require.NoError(t, fw.Drop(p, false, &h, cp, nil))
  186. // test remote mismatch
  187. oldRemote := p.RemoteAddr
  188. p.RemoteAddr = netip.MustParseAddr("1.2.3.10")
  189. assert.Equal(t, fw.Drop(p, false, &h, cp, nil), ErrInvalidRemoteIP)
  190. p.RemoteAddr = oldRemote
  191. // ensure signer doesn't get in the way of group checks
  192. fw = NewFirewall(l, time.Second, time.Minute, time.Hour, &c)
  193. require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"nope"}, "", "", "", "", "signer-shasum"))
  194. require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"default-group"}, "", "", "", "", "signer-shasum-bad"))
  195. assert.Equal(t, fw.Drop(p, true, &h, cp, nil), ErrNoMatchingRule)
  196. // test caSha doesn't drop on match
  197. fw = NewFirewall(l, time.Second, time.Minute, time.Hour, &c)
  198. require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"nope"}, "", "", "", "", "signer-shasum-bad"))
  199. require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"default-group"}, "", "", "", "", "signer-shasum"))
  200. require.NoError(t, fw.Drop(p, true, &h, cp, nil))
  201. // ensure ca name doesn't get in the way of group checks
  202. cp.CAs["signer-shasum"] = &cert.CachedCertificate{Certificate: &dummyCert{name: "ca-good"}}
  203. fw = NewFirewall(l, time.Second, time.Minute, time.Hour, &c)
  204. require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"nope"}, "", "", "", "ca-good", ""))
  205. require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"default-group"}, "", "", "", "ca-good-bad", ""))
  206. assert.Equal(t, fw.Drop(p, true, &h, cp, nil), ErrNoMatchingRule)
  207. // test caName doesn't drop on match
  208. cp.CAs["signer-shasum"] = &cert.CachedCertificate{Certificate: &dummyCert{name: "ca-good"}}
  209. fw = NewFirewall(l, time.Second, time.Minute, time.Hour, &c)
  210. require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"nope"}, "", "", "", "ca-good-bad", ""))
  211. require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"default-group"}, "", "", "", "ca-good", ""))
  212. require.NoError(t, fw.Drop(p, true, &h, cp, nil))
  213. }
  214. func TestFirewall_DropV6(t *testing.T) {
  215. ob := &bytes.Buffer{}
  216. l := test.NewLoggerWithOutput(ob)
  217. myVpnNetworksTable := new(bart.Lite)
  218. myVpnNetworksTable.Insert(netip.MustParsePrefix("fd00::/7"))
  219. p := firewall.Packet{
  220. LocalAddr: netip.MustParseAddr("fd12::34"),
  221. RemoteAddr: netip.MustParseAddr("fd12::34"),
  222. LocalPort: 10,
  223. RemotePort: 90,
  224. Protocol: iputil.IPProtocolUDP,
  225. Fragment: false,
  226. }
  227. c := dummyCert{
  228. name: "host1",
  229. networks: []netip.Prefix{netip.MustParsePrefix("fd12::34/120")},
  230. groups: []string{"default-group"},
  231. issuer: "signer-shasum",
  232. }
  233. h := HostInfo{
  234. ConnectionState: &ConnectionState{
  235. peerCert: &cert.CachedCertificate{
  236. Certificate: &c,
  237. InvertedGroups: map[string]struct{}{"default-group": {}},
  238. },
  239. },
  240. vpnAddrs: []netip.Addr{netip.MustParseAddr("fd12::34")},
  241. }
  242. h.buildNetworks(myVpnNetworksTable, &c)
  243. fw := NewFirewall(l, time.Second, time.Minute, time.Hour, &c)
  244. require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"any"}, "", "", "", "", ""))
  245. cp := cert.NewCAPool()
  246. // Drop outbound
  247. assert.Equal(t, ErrNoMatchingRule, fw.Drop(p, false, &h, cp, nil))
  248. // Allow inbound
  249. resetConntrack(fw)
  250. require.NoError(t, fw.Drop(p, true, &h, cp, nil))
  251. // Allow outbound because conntrack
  252. require.NoError(t, fw.Drop(p, false, &h, cp, nil))
  253. // test remote mismatch
  254. oldRemote := p.RemoteAddr
  255. p.RemoteAddr = netip.MustParseAddr("fd12::56")
  256. assert.Equal(t, fw.Drop(p, false, &h, cp, nil), ErrInvalidRemoteIP)
  257. p.RemoteAddr = oldRemote
  258. // ensure signer doesn't get in the way of group checks
  259. fw = NewFirewall(l, time.Second, time.Minute, time.Hour, &c)
  260. require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"nope"}, "", "", "", "", "signer-shasum"))
  261. require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"default-group"}, "", "", "", "", "signer-shasum-bad"))
  262. assert.Equal(t, fw.Drop(p, true, &h, cp, nil), ErrNoMatchingRule)
  263. // test caSha doesn't drop on match
  264. fw = NewFirewall(l, time.Second, time.Minute, time.Hour, &c)
  265. require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"nope"}, "", "", "", "", "signer-shasum-bad"))
  266. require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"default-group"}, "", "", "", "", "signer-shasum"))
  267. require.NoError(t, fw.Drop(p, true, &h, cp, nil))
  268. // ensure ca name doesn't get in the way of group checks
  269. cp.CAs["signer-shasum"] = &cert.CachedCertificate{Certificate: &dummyCert{name: "ca-good"}}
  270. fw = NewFirewall(l, time.Second, time.Minute, time.Hour, &c)
  271. require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"nope"}, "", "", "", "ca-good", ""))
  272. require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"default-group"}, "", "", "", "ca-good-bad", ""))
  273. assert.Equal(t, fw.Drop(p, true, &h, cp, nil), ErrNoMatchingRule)
  274. // test caName doesn't drop on match
  275. cp.CAs["signer-shasum"] = &cert.CachedCertificate{Certificate: &dummyCert{name: "ca-good"}}
  276. fw = NewFirewall(l, time.Second, time.Minute, time.Hour, &c)
  277. require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"nope"}, "", "", "", "ca-good-bad", ""))
  278. require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"default-group"}, "", "", "", "ca-good", ""))
  279. require.NoError(t, fw.Drop(p, true, &h, cp, nil))
  280. }
  281. func BenchmarkFirewallTable_match(b *testing.B) {
  282. f := &Firewall{}
  283. ft := FirewallTable{
  284. TCP: firewallPort{},
  285. }
  286. pfix := netip.MustParsePrefix("172.1.1.1/32")
  287. _ = ft.TCP.addRule(f, 10, 10, []string{"good-group"}, "good-host", pfix.String(), "", "", "")
  288. _ = ft.TCP.addRule(f, 100, 100, []string{"good-group"}, "good-host", "", pfix.String(), "", "")
  289. pfix6 := netip.MustParsePrefix("fd11::11/128")
  290. _ = ft.TCP.addRule(f, 10, 10, []string{"good-group"}, "good-host", pfix6.String(), "", "", "")
  291. _ = ft.TCP.addRule(f, 100, 100, []string{"good-group"}, "good-host", "", pfix6.String(), "", "")
  292. cp := cert.NewCAPool()
  293. b.Run("fail on proto", func(b *testing.B) {
  294. // This benchmark is showing us the cost of failing to match the protocol
  295. c := &cert.CachedCertificate{
  296. Certificate: &dummyCert{},
  297. }
  298. for n := 0; n < b.N; n++ {
  299. assert.False(b, ft.match(firewall.Packet{Protocol: iputil.IPProtocolUDP}, true, c, cp))
  300. }
  301. })
  302. b.Run("pass proto, fail on port", func(b *testing.B) {
  303. // This benchmark is showing us the cost of matching a specific protocol but failing to match the port
  304. c := &cert.CachedCertificate{
  305. Certificate: &dummyCert{},
  306. }
  307. for n := 0; n < b.N; n++ {
  308. assert.False(b, ft.match(firewall.Packet{Protocol: iputil.IPProtocolTCP, LocalPort: 1}, true, c, cp))
  309. }
  310. })
  311. b.Run("pass proto, port, fail on local CIDR", func(b *testing.B) {
  312. c := &cert.CachedCertificate{
  313. Certificate: &dummyCert{},
  314. }
  315. ip := netip.MustParsePrefix("9.254.254.254/32")
  316. for n := 0; n < b.N; n++ {
  317. assert.False(b, ft.match(firewall.Packet{Protocol: iputil.IPProtocolTCP, LocalPort: 100, LocalAddr: ip.Addr()}, true, c, cp))
  318. }
  319. })
  320. b.Run("pass proto, port, fail on local CIDRv6", func(b *testing.B) {
  321. c := &cert.CachedCertificate{
  322. Certificate: &dummyCert{},
  323. }
  324. ip := netip.MustParsePrefix("fd99::99/128")
  325. for n := 0; n < b.N; n++ {
  326. assert.False(b, ft.match(firewall.Packet{Protocol: iputil.IPProtocolTCP, LocalPort: 100, LocalAddr: ip.Addr()}, true, c, cp))
  327. }
  328. })
  329. b.Run("pass proto, port, any local CIDR, fail all group, name, and cidr", func(b *testing.B) {
  330. c := &cert.CachedCertificate{
  331. Certificate: &dummyCert{
  332. name: "nope",
  333. networks: []netip.Prefix{netip.MustParsePrefix("9.254.254.245/32")},
  334. },
  335. InvertedGroups: map[string]struct{}{"nope": {}},
  336. }
  337. for n := 0; n < b.N; n++ {
  338. assert.False(b, ft.match(firewall.Packet{Protocol: iputil.IPProtocolTCP, LocalPort: 10}, true, c, cp))
  339. }
  340. })
  341. b.Run("pass proto, port, any local CIDRv6, fail all group, name, and cidr", func(b *testing.B) {
  342. c := &cert.CachedCertificate{
  343. Certificate: &dummyCert{
  344. name: "nope",
  345. networks: []netip.Prefix{netip.MustParsePrefix("fd99::99/128")},
  346. },
  347. InvertedGroups: map[string]struct{}{"nope": {}},
  348. }
  349. for n := 0; n < b.N; n++ {
  350. assert.False(b, ft.match(firewall.Packet{Protocol: iputil.IPProtocolTCP, LocalPort: 10}, true, c, cp))
  351. }
  352. })
  353. b.Run("pass proto, port, specific local CIDR, fail all group, name, and cidr", func(b *testing.B) {
  354. c := &cert.CachedCertificate{
  355. Certificate: &dummyCert{
  356. name: "nope",
  357. networks: []netip.Prefix{netip.MustParsePrefix("9.254.254.245/32")},
  358. },
  359. InvertedGroups: map[string]struct{}{"nope": {}},
  360. }
  361. for n := 0; n < b.N; n++ {
  362. assert.False(b, ft.match(firewall.Packet{Protocol: iputil.IPProtocolTCP, LocalPort: 100, LocalAddr: pfix.Addr()}, true, c, cp))
  363. }
  364. })
  365. b.Run("pass proto, port, specific local CIDRv6, fail all group, name, and cidr", func(b *testing.B) {
  366. c := &cert.CachedCertificate{
  367. Certificate: &dummyCert{
  368. name: "nope",
  369. networks: []netip.Prefix{netip.MustParsePrefix("fd99::99/128")},
  370. },
  371. InvertedGroups: map[string]struct{}{"nope": {}},
  372. }
  373. for n := 0; n < b.N; n++ {
  374. assert.False(b, ft.match(firewall.Packet{Protocol: iputil.IPProtocolTCP, LocalPort: 100, LocalAddr: pfix6.Addr()}, true, c, cp))
  375. }
  376. })
  377. b.Run("pass on group on any local cidr", func(b *testing.B) {
  378. c := &cert.CachedCertificate{
  379. Certificate: &dummyCert{
  380. name: "nope",
  381. },
  382. InvertedGroups: map[string]struct{}{"good-group": {}},
  383. }
  384. for n := 0; n < b.N; n++ {
  385. assert.True(b, ft.match(firewall.Packet{Protocol: iputil.IPProtocolTCP, LocalPort: 10}, true, c, cp))
  386. }
  387. })
  388. b.Run("pass on group on specific local cidr", func(b *testing.B) {
  389. c := &cert.CachedCertificate{
  390. Certificate: &dummyCert{
  391. name: "nope",
  392. },
  393. InvertedGroups: map[string]struct{}{"good-group": {}},
  394. }
  395. for n := 0; n < b.N; n++ {
  396. assert.True(b, ft.match(firewall.Packet{Protocol: iputil.IPProtocolTCP, LocalPort: 100, LocalAddr: pfix.Addr()}, true, c, cp))
  397. }
  398. })
  399. b.Run("pass on group on specific local cidr6", func(b *testing.B) {
  400. c := &cert.CachedCertificate{
  401. Certificate: &dummyCert{
  402. name: "nope",
  403. },
  404. InvertedGroups: map[string]struct{}{"good-group": {}},
  405. }
  406. for n := 0; n < b.N; n++ {
  407. assert.True(b, ft.match(firewall.Packet{Protocol: iputil.IPProtocolTCP, LocalPort: 100, LocalAddr: pfix6.Addr()}, true, c, cp))
  408. }
  409. })
  410. b.Run("pass on name", func(b *testing.B) {
  411. c := &cert.CachedCertificate{
  412. Certificate: &dummyCert{
  413. name: "good-host",
  414. },
  415. InvertedGroups: map[string]struct{}{"nope": {}},
  416. }
  417. for n := 0; n < b.N; n++ {
  418. ft.match(firewall.Packet{Protocol: iputil.IPProtocolTCP, LocalPort: 10}, true, c, cp)
  419. }
  420. })
  421. }
  422. func TestFirewall_Drop2(t *testing.T) {
  423. ob := &bytes.Buffer{}
  424. l := test.NewLoggerWithOutput(ob)
  425. myVpnNetworksTable := new(bart.Lite)
  426. myVpnNetworksTable.Insert(netip.MustParsePrefix("1.1.1.1/8"))
  427. p := firewall.Packet{
  428. LocalAddr: netip.MustParseAddr("1.2.3.4"),
  429. RemoteAddr: netip.MustParseAddr("1.2.3.4"),
  430. LocalPort: 10,
  431. RemotePort: 90,
  432. Protocol: iputil.IPProtocolUDP,
  433. Fragment: false,
  434. }
  435. network := netip.MustParsePrefix("1.2.3.4/24")
  436. c := cert.CachedCertificate{
  437. Certificate: &dummyCert{
  438. name: "host1",
  439. networks: []netip.Prefix{network},
  440. },
  441. InvertedGroups: map[string]struct{}{"default-group": {}, "test-group": {}},
  442. }
  443. h := HostInfo{
  444. ConnectionState: &ConnectionState{
  445. peerCert: &c,
  446. },
  447. vpnAddrs: []netip.Addr{network.Addr()},
  448. }
  449. h.buildNetworks(myVpnNetworksTable, c.Certificate)
  450. c1 := cert.CachedCertificate{
  451. Certificate: &dummyCert{
  452. name: "host1",
  453. networks: []netip.Prefix{network},
  454. },
  455. InvertedGroups: map[string]struct{}{"default-group": {}, "test-group-not": {}},
  456. }
  457. h1 := HostInfo{
  458. vpnAddrs: []netip.Addr{network.Addr()},
  459. ConnectionState: &ConnectionState{
  460. peerCert: &c1,
  461. },
  462. }
  463. h1.buildNetworks(myVpnNetworksTable, c1.Certificate)
  464. fw := NewFirewall(l, time.Second, time.Minute, time.Hour, c.Certificate)
  465. require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"default-group", "test-group"}, "", "", "", "", ""))
  466. cp := cert.NewCAPool()
  467. // h1/c1 lacks the proper groups
  468. require.ErrorIs(t, fw.Drop(p, true, &h1, cp, nil), ErrNoMatchingRule)
  469. // c has the proper groups
  470. resetConntrack(fw)
  471. require.NoError(t, fw.Drop(p, true, &h, cp, nil))
  472. }
  473. func TestFirewall_Drop3(t *testing.T) {
  474. ob := &bytes.Buffer{}
  475. l := test.NewLoggerWithOutput(ob)
  476. myVpnNetworksTable := new(bart.Lite)
  477. myVpnNetworksTable.Insert(netip.MustParsePrefix("1.1.1.1/8"))
  478. p := firewall.Packet{
  479. LocalAddr: netip.MustParseAddr("1.2.3.4"),
  480. RemoteAddr: netip.MustParseAddr("1.2.3.4"),
  481. LocalPort: 1,
  482. RemotePort: 1,
  483. Protocol: iputil.IPProtocolUDP,
  484. Fragment: false,
  485. }
  486. network := netip.MustParsePrefix("1.2.3.4/24")
  487. c := cert.CachedCertificate{
  488. Certificate: &dummyCert{
  489. name: "host-owner",
  490. networks: []netip.Prefix{network},
  491. },
  492. }
  493. c1 := cert.CachedCertificate{
  494. Certificate: &dummyCert{
  495. name: "host1",
  496. networks: []netip.Prefix{network},
  497. issuer: "signer-sha-bad",
  498. },
  499. }
  500. h1 := HostInfo{
  501. ConnectionState: &ConnectionState{
  502. peerCert: &c1,
  503. },
  504. vpnAddrs: []netip.Addr{network.Addr()},
  505. }
  506. h1.buildNetworks(myVpnNetworksTable, c1.Certificate)
  507. c2 := cert.CachedCertificate{
  508. Certificate: &dummyCert{
  509. name: "host2",
  510. networks: []netip.Prefix{network},
  511. issuer: "signer-sha",
  512. },
  513. }
  514. h2 := HostInfo{
  515. ConnectionState: &ConnectionState{
  516. peerCert: &c2,
  517. },
  518. vpnAddrs: []netip.Addr{network.Addr()},
  519. }
  520. h2.buildNetworks(myVpnNetworksTable, c2.Certificate)
  521. c3 := cert.CachedCertificate{
  522. Certificate: &dummyCert{
  523. name: "host3",
  524. networks: []netip.Prefix{network},
  525. issuer: "signer-sha-bad",
  526. },
  527. }
  528. h3 := HostInfo{
  529. ConnectionState: &ConnectionState{
  530. peerCert: &c3,
  531. },
  532. vpnAddrs: []netip.Addr{network.Addr()},
  533. }
  534. h3.buildNetworks(myVpnNetworksTable, c3.Certificate)
  535. fw := NewFirewall(l, time.Second, time.Minute, time.Hour, c.Certificate)
  536. require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 1, 1, []string{}, "host1", "", "", "", ""))
  537. require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 1, 1, []string{}, "", "", "", "", "signer-sha"))
  538. cp := cert.NewCAPool()
  539. // c1 should pass because host match
  540. require.NoError(t, fw.Drop(p, true, &h1, cp, nil))
  541. // c2 should pass because ca sha match
  542. resetConntrack(fw)
  543. require.NoError(t, fw.Drop(p, true, &h2, cp, nil))
  544. // c3 should fail because no match
  545. resetConntrack(fw)
  546. assert.Equal(t, fw.Drop(p, true, &h3, cp, nil), ErrNoMatchingRule)
  547. // Test a remote address match
  548. fw = NewFirewall(l, time.Second, time.Minute, time.Hour, c.Certificate)
  549. require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 1, 1, []string{}, "", "1.2.3.4/24", "", "", ""))
  550. require.NoError(t, fw.Drop(p, true, &h1, cp, nil))
  551. }
  552. func TestFirewall_Drop3V6(t *testing.T) {
  553. ob := &bytes.Buffer{}
  554. l := test.NewLoggerWithOutput(ob)
  555. myVpnNetworksTable := new(bart.Lite)
  556. myVpnNetworksTable.Insert(netip.MustParsePrefix("fd00::/7"))
  557. p := firewall.Packet{
  558. LocalAddr: netip.MustParseAddr("fd12::34"),
  559. RemoteAddr: netip.MustParseAddr("fd12::34"),
  560. LocalPort: 1,
  561. RemotePort: 1,
  562. Protocol: iputil.IPProtocolUDP,
  563. Fragment: false,
  564. }
  565. network := netip.MustParsePrefix("fd12::34/120")
  566. c := cert.CachedCertificate{
  567. Certificate: &dummyCert{
  568. name: "host-owner",
  569. networks: []netip.Prefix{network},
  570. },
  571. }
  572. h := HostInfo{
  573. ConnectionState: &ConnectionState{
  574. peerCert: &c,
  575. },
  576. vpnAddrs: []netip.Addr{network.Addr()},
  577. }
  578. h.buildNetworks(myVpnNetworksTable, c.Certificate)
  579. // Test a remote address match
  580. fw := NewFirewall(l, time.Second, time.Minute, time.Hour, c.Certificate)
  581. cp := cert.NewCAPool()
  582. require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 1, 1, []string{}, "", "fd12::34/120", "", "", ""))
  583. require.NoError(t, fw.Drop(p, true, &h, cp, nil))
  584. }
  585. func TestFirewall_DropConntrackReload(t *testing.T) {
  586. ob := &bytes.Buffer{}
  587. l := test.NewLoggerWithOutput(ob)
  588. myVpnNetworksTable := new(bart.Lite)
  589. myVpnNetworksTable.Insert(netip.MustParsePrefix("1.1.1.1/8"))
  590. p := firewall.Packet{
  591. LocalAddr: netip.MustParseAddr("1.2.3.4"),
  592. RemoteAddr: netip.MustParseAddr("1.2.3.4"),
  593. LocalPort: 10,
  594. RemotePort: 90,
  595. Protocol: iputil.IPProtocolUDP,
  596. Fragment: false,
  597. }
  598. network := netip.MustParsePrefix("1.2.3.4/24")
  599. c := cert.CachedCertificate{
  600. Certificate: &dummyCert{
  601. name: "host1",
  602. networks: []netip.Prefix{network},
  603. groups: []string{"default-group"},
  604. issuer: "signer-shasum",
  605. },
  606. InvertedGroups: map[string]struct{}{"default-group": {}},
  607. }
  608. h := HostInfo{
  609. ConnectionState: &ConnectionState{
  610. peerCert: &c,
  611. },
  612. vpnAddrs: []netip.Addr{network.Addr()},
  613. }
  614. h.buildNetworks(myVpnNetworksTable, c.Certificate)
  615. fw := NewFirewall(l, time.Second, time.Minute, time.Hour, c.Certificate)
  616. require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"any"}, "", "", "", "", ""))
  617. cp := cert.NewCAPool()
  618. // Drop outbound
  619. assert.Equal(t, fw.Drop(p, false, &h, cp, nil), ErrNoMatchingRule)
  620. // Allow inbound
  621. resetConntrack(fw)
  622. require.NoError(t, fw.Drop(p, true, &h, cp, nil))
  623. // Allow outbound because conntrack
  624. require.NoError(t, fw.Drop(p, false, &h, cp, nil))
  625. oldFw := fw
  626. fw = NewFirewall(l, time.Second, time.Minute, time.Hour, c.Certificate)
  627. require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 10, 10, []string{"any"}, "", "", "", "", ""))
  628. fw.Conntrack = oldFw.Conntrack
  629. fw.rulesVersion = oldFw.rulesVersion + 1
  630. // Allow outbound because conntrack and new rules allow port 10
  631. require.NoError(t, fw.Drop(p, false, &h, cp, nil))
  632. oldFw = fw
  633. fw = NewFirewall(l, time.Second, time.Minute, time.Hour, c.Certificate)
  634. require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 11, 11, []string{"any"}, "", "", "", "", ""))
  635. fw.Conntrack = oldFw.Conntrack
  636. fw.rulesVersion = oldFw.rulesVersion + 1
  637. // Drop outbound because conntrack doesn't match new ruleset
  638. assert.Equal(t, fw.Drop(p, false, &h, cp, nil), ErrNoMatchingRule)
  639. }
  640. func TestFirewall_ICMPPortBehavior(t *testing.T) {
  641. ob := &bytes.Buffer{}
  642. l := test.NewLoggerWithOutput(ob)
  643. myVpnNetworksTable := new(bart.Lite)
  644. myVpnNetworksTable.Insert(netip.MustParsePrefix("1.1.1.1/8"))
  645. network := netip.MustParsePrefix("1.2.3.4/24")
  646. c := cert.CachedCertificate{
  647. Certificate: &dummyCert{
  648. name: "host1",
  649. networks: []netip.Prefix{network},
  650. groups: []string{"default-group"},
  651. issuer: "signer-shasum",
  652. },
  653. InvertedGroups: map[string]struct{}{"default-group": {}},
  654. }
  655. h := HostInfo{
  656. ConnectionState: &ConnectionState{
  657. peerCert: &c,
  658. },
  659. vpnAddrs: []netip.Addr{network.Addr()},
  660. }
  661. h.buildNetworks(myVpnNetworksTable, c.Certificate)
  662. cp := cert.NewCAPool()
  663. templ := firewall.Packet{
  664. LocalAddr: netip.MustParseAddr("1.2.3.4"),
  665. RemoteAddr: netip.MustParseAddr("1.2.3.4"),
  666. Protocol: iputil.IPProtocolICMP,
  667. Fragment: false,
  668. }
  669. t.Run("ICMP allowed", func(t *testing.T) {
  670. fw := NewFirewall(l, time.Second, time.Minute, time.Hour, c.Certificate)
  671. require.NoError(t, fw.AddRule(true, iputil.IPProtocolICMP, 0, 0, []string{"any"}, "", "", "", "", ""))
  672. t.Run("zero ports", func(t *testing.T) {
  673. p := templ.Copy()
  674. p.LocalPort = 0
  675. p.RemotePort = 0
  676. // Drop outbound
  677. assert.Equal(t, fw.Drop(*p, false, &h, cp, nil), ErrNoMatchingRule)
  678. // Allow inbound
  679. resetConntrack(fw)
  680. require.NoError(t, fw.Drop(*p, true, &h, cp, nil))
  681. //now also allow outbound
  682. require.NoError(t, fw.Drop(*p, false, &h, cp, nil))
  683. })
  684. t.Run("nonzero ports", func(t *testing.T) {
  685. p := templ.Copy()
  686. p.LocalPort = 0xabcd
  687. p.RemotePort = 0x1234
  688. // Drop outbound
  689. assert.Equal(t, fw.Drop(*p, false, &h, cp, nil), ErrNoMatchingRule)
  690. // Allow inbound
  691. resetConntrack(fw)
  692. require.NoError(t, fw.Drop(*p, true, &h, cp, nil))
  693. //now also allow outbound
  694. require.NoError(t, fw.Drop(*p, false, &h, cp, nil))
  695. })
  696. })
  697. t.Run("Any proto, some ports allowed", func(t *testing.T) {
  698. fw := NewFirewall(l, time.Second, time.Minute, time.Hour, c.Certificate)
  699. require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 80, 444, []string{"any"}, "", "", "", "", ""))
  700. t.Run("zero ports, still blocked", func(t *testing.T) {
  701. p := templ.Copy()
  702. p.LocalPort = 0
  703. p.RemotePort = 0
  704. // Drop outbound
  705. assert.Equal(t, fw.Drop(*p, false, &h, cp, nil), ErrNoMatchingRule)
  706. // Allow inbound
  707. resetConntrack(fw)
  708. assert.Equal(t, fw.Drop(*p, true, &h, cp, nil), ErrNoMatchingRule)
  709. //now also allow outbound
  710. assert.Equal(t, fw.Drop(*p, false, &h, cp, nil), ErrNoMatchingRule)
  711. })
  712. t.Run("nonzero ports, still blocked", func(t *testing.T) {
  713. p := templ.Copy()
  714. p.LocalPort = 0xabcd
  715. p.RemotePort = 0x1234
  716. // Drop outbound
  717. assert.Equal(t, fw.Drop(*p, false, &h, cp, nil), ErrNoMatchingRule)
  718. // Allow inbound
  719. resetConntrack(fw)
  720. assert.Equal(t, fw.Drop(*p, true, &h, cp, nil), ErrNoMatchingRule)
  721. //now also allow outbound
  722. assert.Equal(t, fw.Drop(*p, false, &h, cp, nil), ErrNoMatchingRule)
  723. })
  724. t.Run("nonzero, matching ports, still blocked", func(t *testing.T) {
  725. p := templ.Copy()
  726. p.LocalPort = 80
  727. p.RemotePort = 80
  728. // Drop outbound
  729. assert.Equal(t, fw.Drop(*p, false, &h, cp, nil), ErrNoMatchingRule)
  730. // Allow inbound
  731. resetConntrack(fw)
  732. assert.Equal(t, fw.Drop(*p, true, &h, cp, nil), ErrNoMatchingRule)
  733. //now also allow outbound
  734. assert.Equal(t, fw.Drop(*p, false, &h, cp, nil), ErrNoMatchingRule)
  735. })
  736. })
  737. t.Run("Any proto, any port", func(t *testing.T) {
  738. fw := NewFirewall(l, time.Second, time.Minute, time.Hour, c.Certificate)
  739. require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"any"}, "", "", "", "", ""))
  740. t.Run("zero ports, allowed", func(t *testing.T) {
  741. resetConntrack(fw)
  742. p := templ.Copy()
  743. p.LocalPort = 0
  744. p.RemotePort = 0
  745. // Drop outbound
  746. assert.Equal(t, fw.Drop(*p, false, &h, cp, nil), ErrNoMatchingRule)
  747. // Allow inbound
  748. resetConntrack(fw)
  749. require.NoError(t, fw.Drop(*p, true, &h, cp, nil))
  750. //now also allow outbound
  751. require.NoError(t, fw.Drop(*p, false, &h, cp, nil))
  752. })
  753. t.Run("nonzero ports, allowed", func(t *testing.T) {
  754. resetConntrack(fw)
  755. p := templ.Copy()
  756. p.LocalPort = 0xabcd
  757. p.RemotePort = 0x1234
  758. // Drop outbound
  759. assert.Equal(t, fw.Drop(*p, false, &h, cp, nil), ErrNoMatchingRule)
  760. // Allow inbound
  761. resetConntrack(fw)
  762. require.NoError(t, fw.Drop(*p, true, &h, cp, nil))
  763. //now also allow outbound
  764. require.NoError(t, fw.Drop(*p, false, &h, cp, nil))
  765. //different ID is blocked
  766. p.RemotePort++
  767. require.Equal(t, fw.Drop(*p, false, &h, cp, nil), ErrNoMatchingRule)
  768. })
  769. })
  770. }
  771. func TestFirewall_DropIPSpoofing(t *testing.T) {
  772. ob := &bytes.Buffer{}
  773. l := test.NewLoggerWithOutput(ob)
  774. myVpnNetworksTable := new(bart.Lite)
  775. myVpnNetworksTable.Insert(netip.MustParsePrefix("192.0.2.1/24"))
  776. c := cert.CachedCertificate{
  777. Certificate: &dummyCert{
  778. name: "host-owner",
  779. networks: []netip.Prefix{netip.MustParsePrefix("192.0.2.1/24")},
  780. },
  781. }
  782. c1 := cert.CachedCertificate{
  783. Certificate: &dummyCert{
  784. name: "host",
  785. networks: []netip.Prefix{netip.MustParsePrefix("192.0.2.2/24")},
  786. unsafeNetworks: []netip.Prefix{netip.MustParsePrefix("198.51.100.0/24")},
  787. },
  788. }
  789. h1 := HostInfo{
  790. ConnectionState: &ConnectionState{
  791. peerCert: &c1,
  792. },
  793. vpnAddrs: []netip.Addr{c1.Certificate.Networks()[0].Addr()},
  794. }
  795. h1.buildNetworks(myVpnNetworksTable, c1.Certificate)
  796. fw := NewFirewall(l, time.Second, time.Minute, time.Hour, c.Certificate)
  797. require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 1, 1, []string{}, "", "", "", "", ""))
  798. cp := cert.NewCAPool()
  799. // Packet spoofed by `c1`. Note that the remote addr is not a valid one.
  800. p := firewall.Packet{
  801. LocalAddr: netip.MustParseAddr("192.0.2.1"),
  802. RemoteAddr: netip.MustParseAddr("192.0.2.3"),
  803. LocalPort: 1,
  804. RemotePort: 1,
  805. Protocol: iputil.IPProtocolUDP,
  806. Fragment: false,
  807. }
  808. assert.Equal(t, fw.Drop(p, true, &h1, cp, nil), ErrInvalidRemoteIP)
  809. }
  810. func TestFirewall_ConntrackSourceSpoofingAcrossPeers(t *testing.T) {
  811. l := test.NewLoggerWithOutput(&bytes.Buffer{})
  812. myVpnNetworksTable := new(bart.Lite)
  813. myVpnNetworksTable.Insert(netip.MustParsePrefix("192.0.2.1/24"))
  814. owner := &dummyCert{
  815. name: "owner",
  816. networks: []netip.Prefix{netip.MustParsePrefix("192.0.2.1/24")},
  817. }
  818. victim := &cert.CachedCertificate{
  819. Certificate: &dummyCert{
  820. name: "victim",
  821. networks: []netip.Prefix{netip.MustParsePrefix("192.0.2.2/24")},
  822. },
  823. }
  824. victimHI := HostInfo{
  825. ConnectionState: &ConnectionState{peerCert: victim},
  826. vpnAddrs: []netip.Addr{netip.MustParseAddr("192.0.2.2")},
  827. }
  828. victimHI.buildNetworks(myVpnNetworksTable, victim.Certificate)
  829. attacker := &cert.CachedCertificate{
  830. Certificate: &dummyCert{
  831. name: "attacker",
  832. networks: []netip.Prefix{netip.MustParsePrefix("192.0.2.3/24")},
  833. },
  834. }
  835. attackerHI := HostInfo{
  836. ConnectionState: &ConnectionState{peerCert: attacker},
  837. vpnAddrs: []netip.Addr{netip.MustParseAddr("192.0.2.3")},
  838. }
  839. attackerHI.buildNetworks(myVpnNetworksTable, attacker.Certificate)
  840. fw := NewFirewall(l, time.Second, time.Minute, time.Hour, owner)
  841. // Allow any inbound traffic that passes the cert / source-IP checks.
  842. require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"any"}, "", "", "", "", ""))
  843. cp := cert.NewCAPool()
  844. flow := firewall.Packet{
  845. LocalAddr: netip.MustParseAddr("192.0.2.1"),
  846. RemoteAddr: netip.MustParseAddr("192.0.2.2"),
  847. LocalPort: 443,
  848. RemotePort: 55000,
  849. Protocol: iputil.IPProtocolUDP,
  850. }
  851. require.NoError(t, fw.Drop(flow, true, &victimHI, cp, nil),
  852. "victim's own traffic from its own overlay IP must be allowed")
  853. unseen := flow
  854. unseen.RemotePort = 55001
  855. assert.Equal(t, ErrInvalidRemoteIP, fw.Drop(unseen, true, &attackerHI, cp, nil),
  856. "sanity: attacker forging victim's source IP must be rejected when no conntrack entry exists")
  857. got := fw.Drop(flow, true, &attackerHI, cp, nil)
  858. t.Logf("attacker replaying victim's 4-tuple: Drop returned %v (nil == packet ALLOWED == spoof succeeded)", got)
  859. assert.Equal(t, ErrInvalidRemoteIP, got,
  860. "SECURITY: attacker spoofed victim's overlay source IP (192.0.2.2) by reusing an existing conntrack 4-tuple; Drop returned %v instead of rejecting", got)
  861. }
  862. func TestFirewall_ConntrackUnsafeNetworkPeers(t *testing.T) {
  863. l := test.NewLoggerWithOutput(&bytes.Buffer{})
  864. myVpnNetworksTable := new(bart.Lite)
  865. myVpnNetworksTable.Insert(netip.MustParsePrefix("192.0.2.1/24"))
  866. owner := &dummyCert{
  867. name: "owner",
  868. networks: []netip.Prefix{netip.MustParsePrefix("192.0.2.1/24")},
  869. }
  870. // Two routers for the same unsafe network, only one of them is trusted inbound
  871. router := func(name string, addr string, groups ...string) *HostInfo {
  872. inverted := map[string]struct{}{}
  873. for _, g := range groups {
  874. inverted[g] = struct{}{}
  875. }
  876. c := &cert.CachedCertificate{
  877. Certificate: &dummyCert{
  878. name: name,
  879. groups: groups,
  880. networks: []netip.Prefix{netip.MustParsePrefix(addr + "/24")},
  881. unsafeNetworks: []netip.Prefix{netip.MustParsePrefix("198.51.100.0/24")},
  882. },
  883. InvertedGroups: inverted,
  884. }
  885. h := &HostInfo{
  886. ConnectionState: &ConnectionState{peerCert: c},
  887. vpnAddrs: []netip.Addr{netip.MustParseAddr(addr)},
  888. }
  889. h.buildNetworks(myVpnNetworksTable, c.Certificate)
  890. return h
  891. }
  892. trusted := router("trusted", "192.0.2.2", "trusted")
  893. other := router("other", "192.0.2.3")
  894. cp := cert.NewCAPool()
  895. flow := firewall.Packet{
  896. LocalAddr: netip.MustParseAddr("192.0.2.1"),
  897. RemoteAddr: netip.MustParseAddr("198.51.100.5"),
  898. LocalPort: 443,
  899. RemotePort: 55000,
  900. Protocol: iputil.IPProtocolTCP,
  901. }
  902. for _, useCache := range []bool{false, true} {
  903. var cache firewall.ConntrackCache
  904. if useCache {
  905. cache = firewall.ConntrackCache{}
  906. }
  907. t.Run(fmt.Sprintf("inbound flow allowed for one peer/cache=%v", useCache), func(t *testing.T) {
  908. fw := NewFirewall(l, time.Second, time.Minute, time.Hour, owner)
  909. require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"trusted"}, "", "", "", "", ""))
  910. clear(cache)
  911. require.NoError(t, fw.Drop(flow, true, trusted, cp, cache))
  912. assert.Equal(t, ErrNoMatchingRule, fw.Drop(flow, true, other, cp, cache))
  913. require.NoError(t, fw.Drop(flow, true, trusted, cp, cache))
  914. })
  915. t.Run(fmt.Sprintf("reply to a flow opened via one peer/cache=%v", useCache), func(t *testing.T) {
  916. fw := NewFirewall(l, time.Second, time.Minute, time.Hour, owner)
  917. require.NoError(t, fw.AddRule(false, firewall.ProtoAny, 0, 0, []string{"any"}, "", "", "", "", ""))
  918. clear(cache)
  919. require.NoError(t, fw.Drop(flow, false, trusted, cp, cache))
  920. require.NoError(t, fw.Drop(flow, true, trusted, cp, cache))
  921. assert.Equal(t, ErrNoMatchingRule, fw.Drop(flow, true, other, cp, cache))
  922. })
  923. }
  924. }
  925. // fakeSiblings maps each gateway to the conntrack peers of the gateways it shares a route with
  926. type fakeSiblings map[*HostInfo][]netip.Addr
  927. func (s fakeSiblings) siblingPeers(_ netip.Addr, h *HostInfo, buf []netip.Addr) []netip.Addr {
  928. return append(buf, s[h]...)
  929. }
  930. func TestFirewall_ConntrackGatewaySiblings(t *testing.T) {
  931. l := test.NewLoggerWithOutput(&bytes.Buffer{})
  932. myVpnNetworksTable := new(bart.Lite)
  933. myVpnNetworksTable.Insert(netip.MustParsePrefix("192.0.2.1/24"))
  934. owner := &dummyCert{
  935. name: "owner",
  936. networks: []netip.Prefix{netip.MustParsePrefix("192.0.2.1/24")},
  937. }
  938. // Routers for the same unsafe network. a and b are both our gateways for it, c only claims it in its cert
  939. router := func(name string, addr string, groups ...string) *HostInfo {
  940. inverted := map[string]struct{}{}
  941. for _, g := range groups {
  942. inverted[g] = struct{}{}
  943. }
  944. c := &cert.CachedCertificate{
  945. Certificate: &dummyCert{
  946. name: name,
  947. groups: groups,
  948. networks: []netip.Prefix{netip.MustParsePrefix(addr + "/24")},
  949. unsafeNetworks: []netip.Prefix{netip.MustParsePrefix("198.51.100.0/24")},
  950. },
  951. InvertedGroups: inverted,
  952. }
  953. h := &HostInfo{
  954. ConnectionState: &ConnectionState{peerCert: c},
  955. vpnAddrs: []netip.Addr{netip.MustParseAddr(addr)},
  956. }
  957. h.buildNetworks(myVpnNetworksTable, c.Certificate)
  958. return h
  959. }
  960. a := router("a", "192.0.2.2", "trusted")
  961. b := router("b", "192.0.2.3", "trusted")
  962. c := router("c", "192.0.2.4", "trusted")
  963. siblings := fakeSiblings{a: {b.vpnAddrs[0]}, b: {a.vpnAddrs[0]}}
  964. cp := cert.NewCAPool()
  965. flow := firewall.Packet{
  966. LocalAddr: netip.MustParseAddr("192.0.2.1"),
  967. RemoteAddr: netip.MustParseAddr("198.51.100.5"),
  968. LocalPort: 55000,
  969. RemotePort: 443,
  970. Protocol: iputil.IPProtocolTCP,
  971. }
  972. // We open flows through our gateways, nothing is allowed in on its own
  973. newFirewall := func(outGroup string) *Firewall {
  974. fw := NewFirewall(l, time.Second, time.Minute, time.Hour, owner)
  975. require.NoError(t, fw.AddRule(false, firewall.ProtoAny, 0, 0, []string{outGroup}, "", "", "", "", ""))
  976. fw.siblings = siblings
  977. return fw
  978. }
  979. t.Run("reply through the other gateway", func(t *testing.T) {
  980. fw := newFirewall("trusted")
  981. require.NoError(t, fw.Drop(flow, false, b, cp, nil))
  982. require.NoError(t, fw.Drop(flow, true, a, cp, nil))
  983. // a has its own entry now, and keeps the flow when b's is gone
  984. delete(fw.Conntrack.Conns, firewall.ConntrackKey{Packet: flow, Peer: b.vpnAddrs[0]})
  985. require.NoError(t, fw.Drop(flow, true, a, cp, nil))
  986. })
  987. t.Run("not our gateway", func(t *testing.T) {
  988. fw := newFirewall("trusted")
  989. require.NoError(t, fw.Drop(flow, false, b, cp, nil))
  990. assert.Equal(t, ErrNoMatchingRule, fw.Drop(flow, true, c, cp, nil))
  991. })
  992. t.Run("off without siblings", func(t *testing.T) {
  993. fw := newFirewall("trusted")
  994. fw.siblings = nil
  995. require.NoError(t, fw.Drop(flow, false, b, cp, nil))
  996. assert.Equal(t, ErrNoMatchingRule, fw.Drop(flow, true, a, cp, nil))
  997. })
  998. t.Run("expired sibling entry", func(t *testing.T) {
  999. fw := newFirewall("trusted")
  1000. require.NoError(t, fw.Drop(flow, false, b, cp, nil))
  1001. fw.Conntrack.Conns[firewall.ConntrackKey{Packet: flow, Peer: b.vpnAddrs[0]}].Expires = time.Now().Add(-time.Second)
  1002. assert.Equal(t, ErrNoMatchingRule, fw.Drop(flow, true, a, cp, nil))
  1003. })
  1004. t.Run("the original rules must allow the new gateway", func(t *testing.T) {
  1005. fw := newFirewall("b-only")
  1006. b.ConnectionState.peerCert.InvertedGroups["b-only"] = struct{}{}
  1007. defer delete(b.ConnectionState.peerCert.InvertedGroups, "b-only")
  1008. require.NoError(t, fw.Drop(flow, false, b, cp, nil))
  1009. assert.Equal(t, ErrNoMatchingRule, fw.Drop(flow, true, a, cp, nil))
  1010. })
  1011. }
  1012. func TestFirewall_ConntrackPeerRehandshake(t *testing.T) {
  1013. l := test.NewLoggerWithOutput(&bytes.Buffer{})
  1014. myVpnNetworksTable := new(bart.Lite)
  1015. myVpnNetworksTable.Insert(netip.MustParsePrefix("192.0.2.1/24"))
  1016. owner := &dummyCert{
  1017. name: "owner",
  1018. networks: []netip.Prefix{netip.MustParsePrefix("192.0.2.1/24")},
  1019. }
  1020. // Each call is a new tunnel to the same peer, presenting a cert with the given groups
  1021. tunnel := func(localIndex uint32, groups ...string) *HostInfo {
  1022. inverted := map[string]struct{}{}
  1023. for _, g := range groups {
  1024. inverted[g] = struct{}{}
  1025. }
  1026. c := &cert.CachedCertificate{
  1027. Certificate: &dummyCert{
  1028. name: "peer",
  1029. groups: groups,
  1030. networks: []netip.Prefix{netip.MustParsePrefix("192.0.2.2/24"), netip.MustParsePrefix("2001:db8::2/64")},
  1031. },
  1032. InvertedGroups: inverted,
  1033. }
  1034. h := &HostInfo{
  1035. ConnectionState: &ConnectionState{peerCert: c},
  1036. vpnAddrs: []netip.Addr{netip.MustParseAddr("192.0.2.2"), netip.MustParseAddr("2001:db8::2")},
  1037. localIndexId: localIndex,
  1038. }
  1039. h.buildNetworks(myVpnNetworksTable, c.Certificate)
  1040. return h
  1041. }
  1042. cp := cert.NewCAPool()
  1043. flow := firewall.Packet{
  1044. LocalAddr: netip.MustParseAddr("192.0.2.1"),
  1045. RemoteAddr: netip.MustParseAddr("192.0.2.2"),
  1046. LocalPort: 443,
  1047. RemotePort: 55000,
  1048. Protocol: iputil.IPProtocolTCP,
  1049. }
  1050. key := firewall.ConntrackKey{Packet: flow, Peer: netip.MustParseAddr("192.0.2.2")}
  1051. t.Run("inbound flow follows the peer's groups", func(t *testing.T) {
  1052. fw := NewFirewall(l, time.Second, time.Minute, time.Hour, owner)
  1053. require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"trusted"}, "", "", "", "", ""))
  1054. require.NoError(t, fw.Drop(flow, true, tunnel(1, "trusted"), cp, nil))
  1055. // Same groups on a new tunnel keeps the entry and moves it to the new tunnel
  1056. require.NoError(t, fw.Drop(flow, true, tunnel(2, "trusted"), cp, nil))
  1057. require.Contains(t, fw.Conntrack.Conns, key)
  1058. assert.Equal(t, uint32(2), fw.Conntrack.Conns[key].localIndex)
  1059. // Losing the group on a new tunnel removes the entry
  1060. assert.Equal(t, ErrNoMatchingRule, fw.Drop(flow, true, tunnel(3), cp, nil))
  1061. assert.NotContains(t, fw.Conntrack.Conns, key)
  1062. })
  1063. t.Run("reply to a flow we opened survives a new tunnel", func(t *testing.T) {
  1064. fw := NewFirewall(l, time.Second, time.Minute, time.Hour, owner)
  1065. require.NoError(t, fw.AddRule(false, firewall.ProtoAny, 0, 0, []string{"any"}, "", "", "", "", ""))
  1066. require.NoError(t, fw.Drop(flow, false, tunnel(1, "trusted"), cp, nil))
  1067. require.NoError(t, fw.Drop(flow, true, tunnel(2), cp, nil))
  1068. assert.Equal(t, uint32(2), fw.Conntrack.Conns[key].localIndex)
  1069. })
  1070. }
  1071. // BenchmarkFirewallDropConntrackHit measures Drop on an already-established flow
  1072. // (a conntrack hit). This is the fast path that the source-IP<->cert binding
  1073. // reordering adds work to, so it quantifies the cost of moving the address checks
  1074. // ahead of the conntrack lookup. Cases:
  1075. // - simple: peer cert has one address, no unsafe networks (h.networks == nil),
  1076. // so the remote-address check is a single netip.Addr compare.
  1077. // - complex: peer cert has unsafe networks (h.networks populated), so the
  1078. // remote-address check is a BART lookup.
  1079. // - noCache/localCache: whether a per-batch ConntrackCache is supplied, which in
  1080. // the original code let the fast path skip straight past the address checks.
  1081. func BenchmarkFirewallDropConntrackHit(b *testing.B) {
  1082. l := test.NewLoggerWithOutput(&bytes.Buffer{})
  1083. myVpnNetworksTable := new(bart.Lite)
  1084. myVpnNetworksTable.Insert(netip.MustParsePrefix("192.0.2.1/24"))
  1085. owner := &dummyCert{
  1086. name: "owner",
  1087. networks: []netip.Prefix{netip.MustParsePrefix("192.0.2.1/24")},
  1088. }
  1089. simpleCert := &cert.CachedCertificate{
  1090. Certificate: &dummyCert{
  1091. name: "simple",
  1092. networks: []netip.Prefix{netip.MustParsePrefix("192.0.2.2/24")},
  1093. },
  1094. }
  1095. simpleHost := &HostInfo{
  1096. ConnectionState: &ConnectionState{peerCert: simpleCert},
  1097. vpnAddrs: []netip.Addr{netip.MustParseAddr("192.0.2.2")},
  1098. }
  1099. simpleHost.buildNetworks(myVpnNetworksTable, simpleCert.Certificate)
  1100. complexCert := &cert.CachedCertificate{
  1101. Certificate: &dummyCert{
  1102. name: "complex",
  1103. networks: []netip.Prefix{netip.MustParsePrefix("192.0.2.2/24")},
  1104. unsafeNetworks: []netip.Prefix{netip.MustParsePrefix("198.51.100.0/24")},
  1105. },
  1106. }
  1107. complexHost := &HostInfo{
  1108. ConnectionState: &ConnectionState{peerCert: complexCert},
  1109. vpnAddrs: []netip.Addr{netip.MustParseAddr("192.0.2.2")},
  1110. }
  1111. complexHost.buildNetworks(myVpnNetworksTable, complexCert.Certificate)
  1112. cp := cert.NewCAPool()
  1113. flow := firewall.Packet{
  1114. LocalAddr: netip.MustParseAddr("192.0.2.1"),
  1115. RemoteAddr: netip.MustParseAddr("192.0.2.2"),
  1116. LocalPort: 443,
  1117. RemotePort: 55000,
  1118. Protocol: iputil.IPProtocolUDP,
  1119. }
  1120. cases := []struct {
  1121. name string
  1122. host *HostInfo
  1123. useCache bool
  1124. }{
  1125. {"simple/noCache", simpleHost, false},
  1126. {"simple/localCache", simpleHost, true},
  1127. {"complex/noCache", complexHost, false},
  1128. {"complex/localCache", complexHost, true},
  1129. }
  1130. for _, tc := range cases {
  1131. b.Run(tc.name, func(b *testing.B) {
  1132. fw := NewFirewall(l, time.Second, time.Minute, time.Hour, owner)
  1133. require.NoError(b, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"any"}, "", "", "", "", ""))
  1134. // Establish the conntrack entry so every benchmarked Drop is a hit.
  1135. require.NoError(b, fw.Drop(flow, true, tc.host, cp, nil))
  1136. var cache firewall.ConntrackCache
  1137. if tc.useCache {
  1138. cache = firewall.ConntrackCache{}
  1139. }
  1140. b.ReportAllocs()
  1141. b.ResetTimer()
  1142. for i := 0; i < b.N; i++ {
  1143. if err := fw.Drop(flow, true, tc.host, cp, cache); err != nil {
  1144. b.Fatal(err)
  1145. }
  1146. }
  1147. })
  1148. }
  1149. }
  1150. func BenchmarkLookup(b *testing.B) {
  1151. ml := func(m map[string]struct{}, a [][]string) {
  1152. for n := 0; n < b.N; n++ {
  1153. for _, sg := range a {
  1154. found := false
  1155. for _, g := range sg {
  1156. if _, ok := m[g]; !ok {
  1157. found = false
  1158. break
  1159. }
  1160. found = true
  1161. }
  1162. if found {
  1163. return
  1164. }
  1165. }
  1166. }
  1167. }
  1168. b.Run("array to map best", func(b *testing.B) {
  1169. m := map[string]struct{}{
  1170. "1ne": {},
  1171. "2wo": {},
  1172. "3hr": {},
  1173. "4ou": {},
  1174. "5iv": {},
  1175. "6ix": {},
  1176. }
  1177. a := [][]string{
  1178. {"1ne", "2wo", "3hr", "4ou", "5iv", "6ix"},
  1179. {"one", "2wo", "3hr", "4ou", "5iv", "6ix"},
  1180. {"one", "two", "3hr", "4ou", "5iv", "6ix"},
  1181. {"one", "two", "thr", "4ou", "5iv", "6ix"},
  1182. {"one", "two", "thr", "fou", "5iv", "6ix"},
  1183. {"one", "two", "thr", "fou", "fiv", "6ix"},
  1184. {"one", "two", "thr", "fou", "fiv", "six"},
  1185. }
  1186. for n := 0; n < b.N; n++ {
  1187. ml(m, a)
  1188. }
  1189. })
  1190. b.Run("array to map worst", func(b *testing.B) {
  1191. m := map[string]struct{}{
  1192. "one": {},
  1193. "two": {},
  1194. "thr": {},
  1195. "fou": {},
  1196. "fiv": {},
  1197. "six": {},
  1198. }
  1199. a := [][]string{
  1200. {"1ne", "2wo", "3hr", "4ou", "5iv", "6ix"},
  1201. {"one", "2wo", "3hr", "4ou", "5iv", "6ix"},
  1202. {"one", "two", "3hr", "4ou", "5iv", "6ix"},
  1203. {"one", "two", "thr", "4ou", "5iv", "6ix"},
  1204. {"one", "two", "thr", "fou", "5iv", "6ix"},
  1205. {"one", "two", "thr", "fou", "fiv", "6ix"},
  1206. {"one", "two", "thr", "fou", "fiv", "six"},
  1207. }
  1208. for n := 0; n < b.N; n++ {
  1209. ml(m, a)
  1210. }
  1211. })
  1212. }
  1213. func Test_parsePort(t *testing.T) {
  1214. _, _, err := parsePort("")
  1215. require.EqualError(t, err, "was not a number; ``")
  1216. _, _, err = parsePort(" ")
  1217. require.EqualError(t, err, "was not a number; ` `")
  1218. _, _, err = parsePort("-")
  1219. require.EqualError(t, err, "appears to be a range but could not be parsed; `-`")
  1220. _, _, err = parsePort(" - ")
  1221. require.EqualError(t, err, "appears to be a range but could not be parsed; ` - `")
  1222. _, _, err = parsePort("a-b")
  1223. require.EqualError(t, err, "beginning range was not a number; `a`")
  1224. _, _, err = parsePort("1-b")
  1225. require.EqualError(t, err, "ending range was not a number; `b`")
  1226. s, e, err := parsePort(" 1 - 2 ")
  1227. assert.Equal(t, int32(1), s)
  1228. assert.Equal(t, int32(2), e)
  1229. require.NoError(t, err)
  1230. s, e, err = parsePort("0-1")
  1231. assert.Equal(t, int32(0), s)
  1232. assert.Equal(t, int32(0), e)
  1233. require.NoError(t, err)
  1234. s, e, err = parsePort("9919")
  1235. assert.Equal(t, int32(9919), s)
  1236. assert.Equal(t, int32(9919), e)
  1237. require.NoError(t, err)
  1238. s, e, err = parsePort("any")
  1239. assert.Equal(t, int32(0), s)
  1240. assert.Equal(t, int32(0), e)
  1241. require.NoError(t, err)
  1242. }
  1243. // Test_parsePort_invalid covers inputs that must error. The named bug is
  1244. // that int32(strconv.Atoi("4294967296")) truncates to 0 == firewall.PortAny,
  1245. // silently turning a typo into a match-all-ports rule; the rest are
  1246. // representative syntax/range probes.
  1247. func Test_parsePort_invalid(t *testing.T) {
  1248. tests := []struct {
  1249. name string
  1250. input string
  1251. wantErrContains string
  1252. }{
  1253. // Numeric overflow (the named bug + boundary).
  1254. {"named bug: 2^32 truncates to PortAny", "4294967296", "out of range"},
  1255. {"just above max real port", "65536", "out of range"},
  1256. // Negatives route through the range branch and hit the empty-half
  1257. // guard; included as defense in depth so a future refactor cannot
  1258. // accidentally reach the int32 cast.
  1259. {"negative", "-1", "could not be parsed"},
  1260. // Syntax probes.
  1261. {"NUL between digits", "4\x002", "was not a number"},
  1262. {"hex notation", "0x10", "was not a number"},
  1263. {"scientific notation", "1e3", "was not a number"},
  1264. {"leading whitespace", " 42", "was not a number"},
  1265. {"fullwidth digits", "42", "was not a number"},
  1266. // Range branch.
  1267. {"range upper out of range", "1-65536", "ending range out of range"},
  1268. {"range lower out of range", "65536-65537", "beginning range out of range"},
  1269. {"range with negative upper", "1--1", "ending range was not a number"},
  1270. }
  1271. for _, tc := range tests {
  1272. t.Run(tc.name, func(t *testing.T) {
  1273. _, _, err := parsePort(tc.input)
  1274. require.Error(t, err, "input %q must error", tc.input)
  1275. require.ErrorContains(t, err, tc.wantErrContains)
  1276. })
  1277. }
  1278. }
  1279. // Test_parsePort_valid_boundaries locks in success cases at 0, 1, and 65535
  1280. // so a future refactor cannot regress the boundaries.
  1281. func Test_parsePort_valid_boundaries(t *testing.T) {
  1282. tests := []struct {
  1283. name string
  1284. input string
  1285. wantStart int32
  1286. wantEnd int32
  1287. }{
  1288. {"zero is PortAny", "0", 0, 0},
  1289. {"min real port", "1", 1, 1},
  1290. {"max real port", "65535", 65535, 65535},
  1291. {"range zero to max forces end to zero", "0-65535", 0, 0},
  1292. {"range max to max", "65535-65535", 65535, 65535},
  1293. {"range one to max", "1-65535", 1, 65535},
  1294. {"range with whitespace inside", " 1 - 2 ", 1, 2},
  1295. }
  1296. for _, tc := range tests {
  1297. t.Run(tc.name, func(t *testing.T) {
  1298. s, e, err := parsePort(tc.input)
  1299. require.NoError(t, err)
  1300. assert.Equal(t, tc.wantStart, s, "start port")
  1301. assert.Equal(t, tc.wantEnd, e, "end port")
  1302. })
  1303. }
  1304. }
  1305. func TestNewFirewallFromConfig(t *testing.T) {
  1306. l := test.NewLogger()
  1307. // Test a bad rule definition
  1308. c := &dummyCert{}
  1309. cs, err := newCertState(cert.Version2, nil, c, false, cert.Curve_CURVE25519, nil, "aes")
  1310. require.NoError(t, err)
  1311. conf := config.NewC(test.NewLogger())
  1312. conf.Settings["firewall"] = map[string]any{"outbound": "asdf"}
  1313. _, err = NewFirewallFromConfig(l, cs, conf)
  1314. require.EqualError(t, err, "firewall.outbound failed to parse, should be an array of rules")
  1315. // Test both port and code
  1316. conf = config.NewC(test.NewLogger())
  1317. conf.Settings["firewall"] = map[string]any{"outbound": []any{map[string]any{"port": "1", "code": "2"}}}
  1318. _, err = NewFirewallFromConfig(l, cs, conf)
  1319. require.EqualError(t, err, "firewall.outbound rule #0; only one of port or code should be provided")
  1320. // Test missing host, group, cidr, ca_name and ca_sha
  1321. conf = config.NewC(test.NewLogger())
  1322. conf.Settings["firewall"] = map[string]any{"outbound": []any{map[string]any{}}}
  1323. _, err = NewFirewallFromConfig(l, cs, conf)
  1324. require.EqualError(t, err, "firewall.outbound rule #0; at least one of host, group, cidr, local_cidr, ca_name, or ca_sha must be provided")
  1325. // Test code/port error
  1326. conf = config.NewC(test.NewLogger())
  1327. conf.Settings["firewall"] = map[string]any{"outbound": []any{map[string]any{"code": "a", "host": "testh", "proto": "any"}}}
  1328. _, err = NewFirewallFromConfig(l, cs, conf)
  1329. require.EqualError(t, err, "firewall.outbound rule #0; code was not a number; `a`")
  1330. conf.Settings["firewall"] = map[string]any{"outbound": []any{map[string]any{"port": "a", "host": "testh", "proto": "any"}}}
  1331. _, err = NewFirewallFromConfig(l, cs, conf)
  1332. require.EqualError(t, err, "firewall.outbound rule #0; port was not a number; `a`")
  1333. // Test proto error
  1334. conf = config.NewC(test.NewLogger())
  1335. conf.Settings["firewall"] = map[string]any{"outbound": []any{map[string]any{"code": "1", "host": "testh"}}}
  1336. _, err = NewFirewallFromConfig(l, cs, conf)
  1337. require.EqualError(t, err, "firewall.outbound rule #0; proto was not understood; ``")
  1338. // Test cidr parse error
  1339. conf = config.NewC(test.NewLogger())
  1340. conf.Settings["firewall"] = map[string]any{"outbound": []any{map[string]any{"code": "1", "cidr": "testh", "proto": "any"}}}
  1341. _, err = NewFirewallFromConfig(l, cs, conf)
  1342. require.EqualError(t, err, "firewall.outbound rule #0; cidr did not parse; netip.ParsePrefix(\"testh\"): no '/'")
  1343. // Test local_cidr parse error
  1344. conf = config.NewC(test.NewLogger())
  1345. conf.Settings["firewall"] = map[string]any{"outbound": []any{map[string]any{"code": "1", "local_cidr": "testh", "proto": "any"}}}
  1346. _, err = NewFirewallFromConfig(l, cs, conf)
  1347. require.EqualError(t, err, "firewall.outbound rule #0; local_cidr did not parse; netip.ParsePrefix(\"testh\"): no '/'")
  1348. // Test both group and groups
  1349. conf = config.NewC(test.NewLogger())
  1350. conf.Settings["firewall"] = map[string]any{"inbound": []any{map[string]any{"port": "1", "proto": "any", "group": "a", "groups": []string{"b", "c"}}}}
  1351. _, err = NewFirewallFromConfig(l, cs, conf)
  1352. require.EqualError(t, err, "firewall.inbound rule #0; only one of group or groups should be defined, both provided")
  1353. }
  1354. func TestAddFirewallRulesFromConfig(t *testing.T) {
  1355. l := test.NewLogger()
  1356. // Test adding tcp rule
  1357. conf := config.NewC(test.NewLogger())
  1358. mf := &mockFirewall{}
  1359. conf.Settings["firewall"] = map[string]any{"outbound": []any{map[string]any{"port": "1", "proto": "tcp", "host": "a"}}}
  1360. require.NoError(t, AddFirewallRulesFromConfig(l, false, conf, mf))
  1361. assert.Equal(t, addRuleCall{incoming: false, proto: iputil.IPProtocolTCP, startPort: 1, endPort: 1, groups: nil, host: "a", ip: "", localIp: ""}, mf.lastCall)
  1362. // Test adding udp rule
  1363. conf = config.NewC(test.NewLogger())
  1364. mf = &mockFirewall{}
  1365. conf.Settings["firewall"] = map[string]any{"outbound": []any{map[string]any{"port": "1", "proto": "udp", "host": "a"}}}
  1366. require.NoError(t, AddFirewallRulesFromConfig(l, false, conf, mf))
  1367. assert.Equal(t, addRuleCall{incoming: false, proto: iputil.IPProtocolUDP, startPort: 1, endPort: 1, groups: nil, host: "a", ip: "", localIp: ""}, mf.lastCall)
  1368. // Test adding icmp rule
  1369. conf = config.NewC(test.NewLogger())
  1370. mf = &mockFirewall{}
  1371. conf.Settings["firewall"] = map[string]any{"outbound": []any{map[string]any{"port": "1", "proto": "icmp", "host": "a"}}}
  1372. require.NoError(t, AddFirewallRulesFromConfig(l, false, conf, mf))
  1373. assert.Equal(t, addRuleCall{incoming: false, proto: iputil.IPProtocolICMP, startPort: firewall.PortAny, endPort: firewall.PortAny, groups: nil, host: "a", ip: "", localIp: ""}, mf.lastCall)
  1374. // Test adding icmp rule no port
  1375. conf = config.NewC(test.NewLogger())
  1376. mf = &mockFirewall{}
  1377. conf.Settings["firewall"] = map[string]any{"outbound": []any{map[string]any{"proto": "icmp", "host": "a"}}}
  1378. require.NoError(t, AddFirewallRulesFromConfig(l, false, conf, mf))
  1379. assert.Equal(t, addRuleCall{incoming: false, proto: iputil.IPProtocolICMP, startPort: firewall.PortAny, endPort: firewall.PortAny, groups: nil, host: "a", ip: "", localIp: ""}, mf.lastCall)
  1380. // Test adding any rule
  1381. conf = config.NewC(test.NewLogger())
  1382. mf = &mockFirewall{}
  1383. conf.Settings["firewall"] = map[string]any{"inbound": []any{map[string]any{"port": "1", "proto": "any", "host": "a"}}}
  1384. require.NoError(t, AddFirewallRulesFromConfig(l, true, conf, mf))
  1385. assert.Equal(t, addRuleCall{incoming: true, proto: firewall.ProtoAny, startPort: 1, endPort: 1, groups: nil, host: "a", ip: "", localIp: ""}, mf.lastCall)
  1386. // Test adding rule with cidr
  1387. cidr := netip.MustParsePrefix("10.0.0.0/8")
  1388. conf = config.NewC(test.NewLogger())
  1389. mf = &mockFirewall{}
  1390. conf.Settings["firewall"] = map[string]any{"inbound": []any{map[string]any{"port": "1", "proto": "any", "cidr": cidr.String()}}}
  1391. require.NoError(t, AddFirewallRulesFromConfig(l, true, conf, mf))
  1392. assert.Equal(t, addRuleCall{incoming: true, proto: firewall.ProtoAny, startPort: 1, endPort: 1, groups: nil, ip: cidr.String(), localIp: ""}, mf.lastCall)
  1393. // Test adding rule with local_cidr
  1394. conf = config.NewC(test.NewLogger())
  1395. mf = &mockFirewall{}
  1396. conf.Settings["firewall"] = map[string]any{"inbound": []any{map[string]any{"port": "1", "proto": "any", "local_cidr": cidr.String()}}}
  1397. require.NoError(t, AddFirewallRulesFromConfig(l, true, conf, mf))
  1398. assert.Equal(t, addRuleCall{incoming: true, proto: firewall.ProtoAny, startPort: 1, endPort: 1, groups: nil, ip: "", localIp: cidr.String()}, mf.lastCall)
  1399. // Test adding rule with cidr ipv6
  1400. cidr6 := netip.MustParsePrefix("fd00::/8")
  1401. conf = config.NewC(test.NewLogger())
  1402. mf = &mockFirewall{}
  1403. conf.Settings["firewall"] = map[string]any{"inbound": []any{map[string]any{"port": "1", "proto": "any", "cidr": cidr6.String()}}}
  1404. require.NoError(t, AddFirewallRulesFromConfig(l, true, conf, mf))
  1405. assert.Equal(t, addRuleCall{incoming: true, proto: firewall.ProtoAny, startPort: 1, endPort: 1, groups: nil, ip: cidr6.String(), localIp: ""}, mf.lastCall)
  1406. // Test adding rule with any cidr
  1407. conf = config.NewC(test.NewLogger())
  1408. mf = &mockFirewall{}
  1409. conf.Settings["firewall"] = map[string]any{"inbound": []any{map[string]any{"port": "1", "proto": "any", "cidr": "any"}}}
  1410. require.NoError(t, AddFirewallRulesFromConfig(l, true, conf, mf))
  1411. assert.Equal(t, addRuleCall{incoming: true, proto: firewall.ProtoAny, startPort: 1, endPort: 1, groups: nil, ip: "any", localIp: ""}, mf.lastCall)
  1412. // Test adding rule with junk cidr
  1413. conf = config.NewC(test.NewLogger())
  1414. mf = &mockFirewall{}
  1415. conf.Settings["firewall"] = map[string]any{"inbound": []any{map[string]any{"port": "1", "proto": "any", "cidr": "junk/junk"}}}
  1416. require.EqualError(t, AddFirewallRulesFromConfig(l, true, conf, mf), "firewall.inbound rule #0; cidr did not parse; netip.ParsePrefix(\"junk/junk\"): ParseAddr(\"junk\"): unable to parse IP")
  1417. // Test adding rule with local_cidr ipv6
  1418. conf = config.NewC(test.NewLogger())
  1419. mf = &mockFirewall{}
  1420. conf.Settings["firewall"] = map[string]any{"inbound": []any{map[string]any{"port": "1", "proto": "any", "local_cidr": cidr6.String()}}}
  1421. require.NoError(t, AddFirewallRulesFromConfig(l, true, conf, mf))
  1422. assert.Equal(t, addRuleCall{incoming: true, proto: firewall.ProtoAny, startPort: 1, endPort: 1, groups: nil, ip: "", localIp: cidr6.String()}, mf.lastCall)
  1423. // Test adding rule with any local_cidr
  1424. conf = config.NewC(test.NewLogger())
  1425. mf = &mockFirewall{}
  1426. conf.Settings["firewall"] = map[string]any{"inbound": []any{map[string]any{"port": "1", "proto": "any", "local_cidr": "any"}}}
  1427. require.NoError(t, AddFirewallRulesFromConfig(l, true, conf, mf))
  1428. assert.Equal(t, addRuleCall{incoming: true, proto: firewall.ProtoAny, startPort: 1, endPort: 1, groups: nil, localIp: "any"}, mf.lastCall)
  1429. // Test adding rule with junk local_cidr
  1430. conf = config.NewC(test.NewLogger())
  1431. mf = &mockFirewall{}
  1432. conf.Settings["firewall"] = map[string]any{"inbound": []any{map[string]any{"port": "1", "proto": "any", "local_cidr": "junk/junk"}}}
  1433. require.EqualError(t, AddFirewallRulesFromConfig(l, true, conf, mf), "firewall.inbound rule #0; local_cidr did not parse; netip.ParsePrefix(\"junk/junk\"): ParseAddr(\"junk\"): unable to parse IP")
  1434. // Test adding rule with ca_sha
  1435. conf = config.NewC(test.NewLogger())
  1436. mf = &mockFirewall{}
  1437. conf.Settings["firewall"] = map[string]any{"inbound": []any{map[string]any{"port": "1", "proto": "any", "ca_sha": "12312313123"}}}
  1438. require.NoError(t, AddFirewallRulesFromConfig(l, true, conf, mf))
  1439. assert.Equal(t, addRuleCall{incoming: true, proto: firewall.ProtoAny, startPort: 1, endPort: 1, groups: nil, ip: "", localIp: "", caSha: "12312313123"}, mf.lastCall)
  1440. // Test adding rule with ca_name
  1441. conf = config.NewC(test.NewLogger())
  1442. mf = &mockFirewall{}
  1443. conf.Settings["firewall"] = map[string]any{"inbound": []any{map[string]any{"port": "1", "proto": "any", "ca_name": "root01"}}}
  1444. require.NoError(t, AddFirewallRulesFromConfig(l, true, conf, mf))
  1445. assert.Equal(t, addRuleCall{incoming: true, proto: firewall.ProtoAny, startPort: 1, endPort: 1, groups: nil, ip: "", localIp: "", caName: "root01"}, mf.lastCall)
  1446. // Test single group
  1447. conf = config.NewC(test.NewLogger())
  1448. mf = &mockFirewall{}
  1449. conf.Settings["firewall"] = map[string]any{"inbound": []any{map[string]any{"port": "1", "proto": "any", "group": "a"}}}
  1450. require.NoError(t, AddFirewallRulesFromConfig(l, true, conf, mf))
  1451. assert.Equal(t, addRuleCall{incoming: true, proto: firewall.ProtoAny, startPort: 1, endPort: 1, groups: []string{"a"}, ip: "", localIp: ""}, mf.lastCall)
  1452. // Test single groups
  1453. conf = config.NewC(test.NewLogger())
  1454. mf = &mockFirewall{}
  1455. conf.Settings["firewall"] = map[string]any{"inbound": []any{map[string]any{"port": "1", "proto": "any", "groups": "a"}}}
  1456. require.NoError(t, AddFirewallRulesFromConfig(l, true, conf, mf))
  1457. assert.Equal(t, addRuleCall{incoming: true, proto: firewall.ProtoAny, startPort: 1, endPort: 1, groups: []string{"a"}, ip: "", localIp: ""}, mf.lastCall)
  1458. // Test multiple AND groups
  1459. conf = config.NewC(test.NewLogger())
  1460. mf = &mockFirewall{}
  1461. conf.Settings["firewall"] = map[string]any{"inbound": []any{map[string]any{"port": "1", "proto": "any", "groups": []string{"a", "b"}}}}
  1462. require.NoError(t, AddFirewallRulesFromConfig(l, true, conf, mf))
  1463. assert.Equal(t, addRuleCall{incoming: true, proto: firewall.ProtoAny, startPort: 1, endPort: 1, groups: []string{"a", "b"}, ip: "", localIp: ""}, mf.lastCall)
  1464. // Test Add error
  1465. conf = config.NewC(test.NewLogger())
  1466. mf = &mockFirewall{}
  1467. mf.nextCallReturn = errors.New("test error")
  1468. conf.Settings["firewall"] = map[string]any{"inbound": []any{map[string]any{"port": "1", "proto": "any", "host": "a"}}}
  1469. require.EqualError(t, AddFirewallRulesFromConfig(l, true, conf, mf), "firewall.inbound rule #0; `test error`")
  1470. }
  1471. func TestFirewall_convertRule(t *testing.T) {
  1472. ob := &bytes.Buffer{}
  1473. l := test.NewLoggerWithOutput(ob)
  1474. // Ensure group array of 1 is converted and a warning is printed
  1475. c := map[string]any{
  1476. "group": []any{"group1"},
  1477. }
  1478. r, err := convertRule(l, c, "test", 1)
  1479. assert.Contains(t, ob.String(), "group was an array with a single value, converting to simple value")
  1480. assert.Contains(t, ob.String(), "table=test")
  1481. assert.Contains(t, ob.String(), "rule=1")
  1482. require.NoError(t, err)
  1483. assert.Equal(t, []string{"group1"}, r.Groups)
  1484. // Ensure group array of > 1 is errord
  1485. ob.Reset()
  1486. c = map[string]any{
  1487. "group": []any{"group1", "group2"},
  1488. }
  1489. r, err = convertRule(l, c, "test", 1)
  1490. assert.Empty(t, ob.String())
  1491. require.Error(t, err, "group should contain a single value, an array with more than one entry was provided")
  1492. // Make sure a well formed group is alright
  1493. ob.Reset()
  1494. c = map[string]any{
  1495. "group": "group1",
  1496. }
  1497. r, err = convertRule(l, c, "test", 1)
  1498. require.NoError(t, err)
  1499. assert.Equal(t, []string{"group1"}, r.Groups)
  1500. }
  1501. func TestFirewall_convertRuleSanity(t *testing.T) {
  1502. ob := &bytes.Buffer{}
  1503. l := test.NewLoggerWithOutput(ob)
  1504. noWarningPlease := []map[string]any{
  1505. {"group": "group1"},
  1506. {"groups": []any{"group2"}},
  1507. {"host": "bob"},
  1508. {"cidr": "1.1.1.1/1"},
  1509. {"groups": []any{"group2"}, "host": "bob"},
  1510. {"cidr": "1.1.1.1/1", "host": "bob"},
  1511. {"groups": []any{"group2"}, "cidr": "1.1.1.1/1"},
  1512. {"groups": []any{"group2"}, "cidr": "1.1.1.1/1", "host": "bob"},
  1513. }
  1514. for _, c := range noWarningPlease {
  1515. r, err := convertRule(l, c, "test", 1)
  1516. require.NoError(t, err)
  1517. require.NoError(t, r.sanity(), "should not generate a sanity warning, %+v", c)
  1518. }
  1519. yesWarningPlease := []map[string]any{
  1520. {"group": "group1"},
  1521. {"groups": []any{"group2"}},
  1522. {"cidr": "1.1.1.1/1"},
  1523. {"groups": []any{"group2"}, "host": "bob"},
  1524. {"cidr": "1.1.1.1/1", "host": "bob"},
  1525. {"groups": []any{"group2"}, "cidr": "1.1.1.1/1"},
  1526. {"groups": []any{"group2"}, "cidr": "1.1.1.1/1", "host": "bob"},
  1527. }
  1528. for _, c := range yesWarningPlease {
  1529. c["host"] = "any"
  1530. r, err := convertRule(l, c, "test", 1)
  1531. require.NoError(t, err)
  1532. err = r.sanity()
  1533. require.Error(t, err, "I wanted a warning: %+v", c)
  1534. }
  1535. //reset the list
  1536. yesWarningPlease = []map[string]any{
  1537. {"group": "group1"},
  1538. {"groups": []any{"group2"}},
  1539. {"cidr": "1.1.1.1/1"},
  1540. {"groups": []any{"group2"}, "host": "bob"},
  1541. {"cidr": "1.1.1.1/1", "host": "bob"},
  1542. {"groups": []any{"group2"}, "cidr": "1.1.1.1/1"},
  1543. {"groups": []any{"group2"}, "cidr": "1.1.1.1/1", "host": "bob"},
  1544. }
  1545. for _, c := range yesWarningPlease {
  1546. r, err := convertRule(l, c, "test", 1)
  1547. require.NoError(t, err)
  1548. r.Groups = append(r.Groups, "any")
  1549. err = r.sanity()
  1550. require.Error(t, err, "I wanted a warning: %+v", c)
  1551. }
  1552. }
  1553. type testcase struct {
  1554. h *HostInfo
  1555. p firewall.Packet
  1556. c cert.Certificate
  1557. err error
  1558. }
  1559. func (c *testcase) Test(t *testing.T, fw *Firewall) {
  1560. t.Helper()
  1561. cp := cert.NewCAPool()
  1562. resetConntrack(fw)
  1563. err := fw.Drop(c.p, true, c.h, cp, nil)
  1564. if c.err == nil {
  1565. require.NoError(t, err, "failed to not drop remote address %s", c.p.RemoteAddr)
  1566. } else {
  1567. require.ErrorIs(t, c.err, err, "failed to drop remote address %s", c.p.RemoteAddr)
  1568. }
  1569. }
  1570. func buildTestCase(setup testsetup, err error, theirPrefixes ...netip.Prefix) testcase {
  1571. c1 := dummyCert{
  1572. name: "host1",
  1573. networks: theirPrefixes,
  1574. groups: []string{"default-group"},
  1575. issuer: "signer-shasum",
  1576. }
  1577. h := HostInfo{
  1578. ConnectionState: &ConnectionState{
  1579. peerCert: &cert.CachedCertificate{
  1580. Certificate: &c1,
  1581. InvertedGroups: map[string]struct{}{"default-group": {}},
  1582. },
  1583. },
  1584. vpnAddrs: make([]netip.Addr, len(theirPrefixes)),
  1585. }
  1586. for i := range theirPrefixes {
  1587. h.vpnAddrs[i] = theirPrefixes[i].Addr()
  1588. }
  1589. h.buildNetworks(setup.myVpnNetworksTable, &c1)
  1590. p := firewall.Packet{
  1591. LocalAddr: setup.c.Networks()[0].Addr(), //todo?
  1592. RemoteAddr: theirPrefixes[0].Addr(),
  1593. LocalPort: 10,
  1594. RemotePort: 90,
  1595. Protocol: iputil.IPProtocolUDP,
  1596. Fragment: false,
  1597. }
  1598. return testcase{
  1599. h: &h,
  1600. p: p,
  1601. c: &c1,
  1602. err: err,
  1603. }
  1604. }
  1605. type testsetup struct {
  1606. c dummyCert
  1607. myVpnNetworksTable *bart.Lite
  1608. fw *Firewall
  1609. }
  1610. func newSetup(t *testing.T, l *slog.Logger, myPrefixes ...netip.Prefix) testsetup {
  1611. c := dummyCert{
  1612. name: "me",
  1613. networks: myPrefixes,
  1614. groups: []string{"default-group"},
  1615. issuer: "signer-shasum",
  1616. }
  1617. return newSetupFromCert(t, l, c)
  1618. }
  1619. func newSetupFromCert(t *testing.T, l *slog.Logger, c dummyCert) testsetup {
  1620. myVpnNetworksTable := new(bart.Lite)
  1621. for _, prefix := range c.Networks() {
  1622. myVpnNetworksTable.Insert(prefix)
  1623. }
  1624. fw := NewFirewall(l, time.Second, time.Minute, time.Hour, &c)
  1625. require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"any"}, "", "", "", "", ""))
  1626. return testsetup{
  1627. c: c,
  1628. fw: fw,
  1629. myVpnNetworksTable: myVpnNetworksTable,
  1630. }
  1631. }
  1632. func TestFirewall_Drop_EnforceIPMatch(t *testing.T) {
  1633. t.Parallel()
  1634. ob := &bytes.Buffer{}
  1635. l := test.NewLoggerWithOutput(ob)
  1636. myPrefix := netip.MustParsePrefix("1.1.1.1/8")
  1637. // for now, it's okay that these are all "incoming", the logic this test tries to check doesn't care about in/out
  1638. t.Run("allow inbound all matching", func(t *testing.T) {
  1639. t.Parallel()
  1640. setup := newSetup(t, l, myPrefix)
  1641. tc := buildTestCase(setup, nil, netip.MustParsePrefix("1.2.3.4/24"))
  1642. tc.Test(t, setup.fw)
  1643. })
  1644. t.Run("allow inbound local matching", func(t *testing.T) {
  1645. t.Parallel()
  1646. setup := newSetup(t, l, myPrefix)
  1647. tc := buildTestCase(setup, ErrInvalidLocalIP, netip.MustParsePrefix("1.2.3.4/24"))
  1648. tc.p.LocalAddr = netip.MustParseAddr("1.2.3.8")
  1649. tc.Test(t, setup.fw)
  1650. })
  1651. t.Run("block inbound remote mismatched", func(t *testing.T) {
  1652. t.Parallel()
  1653. setup := newSetup(t, l, myPrefix)
  1654. tc := buildTestCase(setup, ErrInvalidRemoteIP, netip.MustParsePrefix("1.2.3.4/24"))
  1655. tc.p.RemoteAddr = netip.MustParseAddr("9.9.9.9")
  1656. tc.Test(t, setup.fw)
  1657. })
  1658. t.Run("Block a vpn peer packet", func(t *testing.T) {
  1659. t.Parallel()
  1660. setup := newSetup(t, l, myPrefix)
  1661. tc := buildTestCase(setup, ErrPeerRejected, netip.MustParsePrefix("2.2.2.2/24"))
  1662. tc.Test(t, setup.fw)
  1663. })
  1664. twoPrefixes := []netip.Prefix{
  1665. netip.MustParsePrefix("1.2.3.4/24"), netip.MustParsePrefix("2.2.2.2/24"),
  1666. }
  1667. t.Run("allow inbound one matching", func(t *testing.T) {
  1668. t.Parallel()
  1669. setup := newSetup(t, l, myPrefix)
  1670. tc := buildTestCase(setup, nil, twoPrefixes...)
  1671. tc.Test(t, setup.fw)
  1672. })
  1673. t.Run("block inbound multimismatch", func(t *testing.T) {
  1674. t.Parallel()
  1675. setup := newSetup(t, l, myPrefix)
  1676. tc := buildTestCase(setup, ErrInvalidRemoteIP, twoPrefixes...)
  1677. tc.p.RemoteAddr = netip.MustParseAddr("9.9.9.9")
  1678. tc.Test(t, setup.fw)
  1679. })
  1680. t.Run("allow inbound 2nd one matching", func(t *testing.T) {
  1681. t.Parallel()
  1682. setup2 := newSetup(t, l, netip.MustParsePrefix("2.2.2.1/24"))
  1683. tc := buildTestCase(setup2, nil, twoPrefixes...)
  1684. tc.p.RemoteAddr = twoPrefixes[1].Addr()
  1685. tc.Test(t, setup2.fw)
  1686. })
  1687. t.Run("allow inbound unsafe route", func(t *testing.T) {
  1688. t.Parallel()
  1689. unsafePrefix := netip.MustParsePrefix("192.168.0.0/24")
  1690. c := dummyCert{
  1691. name: "me",
  1692. networks: []netip.Prefix{myPrefix},
  1693. unsafeNetworks: []netip.Prefix{unsafePrefix},
  1694. groups: []string{"default-group"},
  1695. issuer: "signer-shasum",
  1696. }
  1697. unsafeSetup := newSetupFromCert(t, l, c)
  1698. tc := buildTestCase(unsafeSetup, nil, twoPrefixes...)
  1699. tc.p.LocalAddr = netip.MustParseAddr("192.168.0.3")
  1700. tc.err = ErrNoMatchingRule
  1701. tc.Test(t, unsafeSetup.fw) //should hit firewall and bounce off
  1702. require.NoError(t, unsafeSetup.fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"any"}, "", "", unsafePrefix.String(), "", ""))
  1703. tc.err = nil
  1704. tc.Test(t, unsafeSetup.fw) //should pass
  1705. })
  1706. }
  1707. type addRuleCall struct {
  1708. incoming bool
  1709. proto uint8
  1710. startPort int32
  1711. endPort int32
  1712. groups []string
  1713. host string
  1714. ip string
  1715. localIp string
  1716. caName string
  1717. caSha string
  1718. }
  1719. type mockFirewall struct {
  1720. lastCall addRuleCall
  1721. nextCallReturn error
  1722. }
  1723. func (mf *mockFirewall) AddRule(incoming bool, proto uint8, startPort int32, endPort int32, groups []string, host string, ip, localIp, caName string, caSha string) error {
  1724. mf.lastCall = addRuleCall{
  1725. incoming: incoming,
  1726. proto: proto,
  1727. startPort: startPort,
  1728. endPort: endPort,
  1729. groups: groups,
  1730. host: host,
  1731. ip: ip,
  1732. localIp: localIp,
  1733. caName: caName,
  1734. caSha: caSha,
  1735. }
  1736. err := mf.nextCallReturn
  1737. mf.nextCallReturn = nil
  1738. return err
  1739. }
  1740. func resetConntrack(fw *Firewall) {
  1741. fw.Conntrack.Lock()
  1742. fw.Conntrack.Conns = map[firewall.ConntrackKey]*conn{}
  1743. fw.Conntrack.Unlock()
  1744. }