firewall_test.go 65 KB

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