firewall_test.go 72 KB

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