rules_builder.go 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330
  1. package firewall
  2. import (
  3. "crypto/sha256"
  4. "encoding/hex"
  5. "fmt"
  6. "hash"
  7. "hash/fnv"
  8. "io"
  9. "log/slog"
  10. "net/netip"
  11. "slices"
  12. "github.com/gaissmai/bart"
  13. "github.com/slackhq/nebula/iputil"
  14. )
  15. // RulesBuilder collects the inbound and outbound rules, Build turns them into a Table for each direction
  16. type RulesBuilder struct {
  17. in, out tableRules
  18. // defaultLocalCIDRAny makes rules without a local cidr allow any local address, even with unsafe networks
  19. defaultLocalCIDRAny bool
  20. // ruleSHA and ruleFNV hash every rule as it's added, see Rules
  21. ruleSHA hash.Hash
  22. ruleFNV hash.Hash32
  23. l *slog.Logger
  24. }
  25. // tableRules are one direction's rules, waiting to be built into a Table
  26. type tableRules struct {
  27. anyRules []rule
  28. // protoRules holds the rules for each protocol besides proto `any`, ICMPv6 rules are kept with ICMP
  29. protoRules [256][]rule
  30. }
  31. // rule is a parsed rule, so adding it to a Table can't fail
  32. type rule struct {
  33. startPort, endPort int32
  34. groups []string
  35. host string
  36. caName, caSha string
  37. // cidr is the remote cidr, not valid when the rule doesn't have one
  38. cidr netip.Prefix
  39. anyCIDR bool
  40. // localCIDR is the local addresses the rule allows, anyLocalCIDR allows all of them.
  41. // Neither is set for a rule without a local cidr until the Table is built and it gets the default.
  42. // localCIDR is shared by every localRules the rule is added to, so it's never changed, see localRules.add
  43. localCIDR *bart.Lite
  44. anyLocalCIDR bool
  45. }
  46. func NewRulesBuilder(l *slog.Logger) *RulesBuilder {
  47. return &RulesBuilder{
  48. ruleSHA: sha256.New(),
  49. ruleFNV: fnv.New32a(),
  50. l: l,
  51. }
  52. }
  53. // AddRule adds a rule for incoming or outgoing traffic, on proto or on every protocol when proto is ProtoAny.
  54. // The rule is parsed now, so a bad rule is reported when it's added rather than when the Tables are built.
  55. func (b *RulesBuilder) AddRule(incoming bool, proto uint8, startPort int32, endPort int32, groups []string, host string, cidr, localCidr, caName string, caSha string) error {
  56. if proto == iputil.IPProtocolICMP || proto == iputil.IPProtocolICMPv6 {
  57. //ICMP traffic doesn't have ports, so we always coerce to "any", even if a value is provided
  58. if startPort != PortAny {
  59. b.l.Warn("ignoring port specification for ICMP firewall rule", "startPort", startPort)
  60. }
  61. startPort = PortAny
  62. endPort = PortAny
  63. }
  64. r, err := parseRule(startPort, endPort, groups, host, cidr, localCidr, caName, caSha)
  65. if err != nil {
  66. return err
  67. }
  68. if incoming {
  69. b.in.add(proto, r)
  70. } else {
  71. b.out.add(proto, r)
  72. }
  73. // The rule hashes are of these lines, changing them changes every hash
  74. fmt.Fprintf(io.MultiWriter(b.ruleSHA, b.ruleFNV),
  75. "incoming: %v, proto: %v, startPort: %v, endPort: %v, groups: %v, host: %v, ip: %v, localIp: %v, caName: %v, caSha: %s\n",
  76. incoming, proto, startPort, endPort, groups, host, cidr, localCidr, caName, caSha,
  77. )
  78. direction := "incoming"
  79. if !incoming {
  80. direction = "outgoing"
  81. }
  82. b.l.Info("Firewall rule added",
  83. "firewallRule", m{"direction": direction, "proto": proto, "startPort": startPort, "endPort": endPort, "groups": groups, "host": host, "cidr": cidr, "localCidr": localCidr, "caName": caName, "caSha": caSha},
  84. )
  85. return nil
  86. }
  87. // Build turns the rules into a Table for each direction, and hashes them. Rules without a local cidr allow
  88. // vpnNetworks when there are unsafe networks, otherwise any local address.
  89. func (b *RulesBuilder) Build(vpnNetworks, unsafeNetworks []netip.Prefix) Rules {
  90. var defaultLocal *bart.Lite
  91. if len(unsafeNetworks) > 0 && !b.defaultLocalCIDRAny {
  92. defaultLocal = new(bart.Lite)
  93. for _, network := range vpnNetworks {
  94. defaultLocal.Insert(network)
  95. }
  96. }
  97. return Rules{
  98. In: b.in.build(defaultLocal),
  99. Out: b.out.build(defaultLocal),
  100. Hash: hex.EncodeToString(b.ruleSHA.Sum(nil)),
  101. HashFNV: b.ruleFNV.Sum32(),
  102. }
  103. }
  104. func parseRule(startPort, endPort int32, groups []string, host, cidr, localCidr, caName, caSha string) (rule, error) {
  105. if startPort > endPort {
  106. return rule{}, fmt.Errorf("start port was lower than end port")
  107. }
  108. r := rule{
  109. startPort: startPort,
  110. endPort: endPort,
  111. groups: groups,
  112. host: host,
  113. caName: caName,
  114. caSha: caSha,
  115. }
  116. var err error
  117. r.cidr, r.anyCIDR, err = parseCIDR(cidr)
  118. if err != nil {
  119. return rule{}, fmt.Errorf("cidr did not parse; %w", err)
  120. }
  121. var localCIDR netip.Prefix
  122. localCIDR, r.anyLocalCIDR, err = parseCIDR(localCidr)
  123. if err != nil {
  124. return rule{}, fmt.Errorf("local_cidr did not parse; %w", err)
  125. }
  126. if localCIDR.IsValid() {
  127. r.localCIDR = new(bart.Lite)
  128. r.localCIDR.Insert(localCIDR)
  129. }
  130. return r, nil
  131. }
  132. // add keeps r for proto, or for every protocol when proto is ProtoAny
  133. func (tr *tableRules) add(proto uint8, r rule) {
  134. switch proto {
  135. case ProtoAny:
  136. tr.anyRules = append(tr.anyRules, r)
  137. case iputil.IPProtocolICMPv6:
  138. tr.protoRules[iputil.IPProtocolICMP] = append(tr.protoRules[iputil.IPProtocolICMP], r)
  139. default:
  140. tr.protoRules[proto] = append(tr.protoRules[proto], r)
  141. }
  142. }
  143. // build turns the rules into a Table. Rules without a local cidr allow defaultLocal,
  144. // or any local address when defaultLocal is nil.
  145. func (tr *tableRules) build(defaultLocal *bart.Lite) *Table {
  146. t := &Table{}
  147. // Every protocol starts out sharing the proto `any` rules, including protos[ProtoAny]
  148. anyProto := newPortRules(defaultLocal, tr.anyRules)
  149. for proto := range t.protos {
  150. t.protos[proto] = anyProto
  151. }
  152. // Protocols with rules of their own get a copy of the proto `any` rules too, so a packet only checks one set
  153. for proto, rules := range tr.protoRules {
  154. if len(rules) > 0 {
  155. t.protos[proto] = newPortRules(defaultLocal, tr.anyRules, rules)
  156. }
  157. }
  158. // ICMP and ICMPv6 share one set of rules, add keeps them together under ICMP
  159. t.protos[iputil.IPProtocolICMPv6] = t.protos[iputil.IPProtocolICMP]
  160. return t
  161. }
  162. // newPortRules builds portRules from each set of rules, or returns nil when there are none
  163. func newPortRules(defaultLocal *bart.Lite, ruleSets ...[]rule) portRules {
  164. var pr portRules
  165. for _, rules := range ruleSets {
  166. for _, r := range rules {
  167. if pr == nil {
  168. pr = portRules{}
  169. }
  170. pr.add(r.withDefaultLocal(defaultLocal))
  171. }
  172. }
  173. return pr
  174. }
  175. // parseCIDR parses a rule's cidr, which is empty, `any`, or a prefix
  176. func parseCIDR(s string) (prefix netip.Prefix, isAny bool, err error) {
  177. switch s {
  178. case "":
  179. return netip.Prefix{}, false, nil
  180. case "any":
  181. return netip.Prefix{}, true, nil
  182. }
  183. prefix, err = netip.ParsePrefix(s)
  184. return prefix, false, err
  185. }
  186. // isAny reports whether r matches any host, making its groups, host, and cidr irrelevant
  187. func (r rule) isAny() bool {
  188. if len(r.groups) == 0 && r.host == "" && !r.cidr.IsValid() {
  189. return true
  190. }
  191. return slices.Contains(r.groups, "any") || r.host == "any" || r.anyCIDR
  192. }
  193. // withDefaultLocal returns r allowing defaultLocal, or any local address when defaultLocal is nil,
  194. // unless r has a local cidr of its own
  195. func (r rule) withDefaultLocal(defaultLocal *bart.Lite) rule {
  196. if !r.anyLocalCIDR && r.localCIDR == nil {
  197. r.anyLocalCIDR = defaultLocal == nil
  198. r.localCIDR = defaultLocal
  199. }
  200. return r
  201. }
  202. func (pr portRules) add(r rule) {
  203. for i := r.startPort; i <= r.endPort; i++ {
  204. cr := pr[i]
  205. if cr == nil {
  206. cr = &caRules{}
  207. pr[i] = cr
  208. }
  209. cr.add(r)
  210. }
  211. }
  212. func (cr *caRules) add(r rule) {
  213. if r.caSha == "" && r.caName == "" {
  214. if cr.Any == nil {
  215. cr.Any = &remoteRules{}
  216. }
  217. cr.Any.add(r)
  218. return
  219. }
  220. if r.caSha != "" {
  221. getOrNew(&cr.CAShas, r.caSha).add(r)
  222. }
  223. if r.caName != "" {
  224. getOrNew(&cr.CANames, r.caName).add(r)
  225. }
  226. }
  227. func (rr *remoteRules) add(r rule) {
  228. if r.isAny() {
  229. if rr.Any == nil {
  230. rr.Any = &localRules{}
  231. }
  232. rr.Any.add(r)
  233. return
  234. }
  235. if len(r.groups) > 0 {
  236. lr := &localRules{}
  237. lr.add(r)
  238. rr.Groups = append(rr.Groups, &groupsRule{
  239. Groups: r.groups,
  240. LocalCIDR: lr,
  241. })
  242. }
  243. if r.host != "" {
  244. getOrNew(&rr.Hosts, r.host).add(r)
  245. }
  246. if r.cidr.IsValid() {
  247. if rr.CIDR == nil {
  248. rr.CIDR = new(bart.Table[*localRules])
  249. }
  250. lr, ok := rr.CIDR.Get(r.cidr)
  251. if !ok {
  252. lr = &localRules{}
  253. rr.CIDR.Insert(r.cidr, lr)
  254. }
  255. lr.add(r)
  256. }
  257. }
  258. func (lr *localRules) add(r rule) {
  259. if r.anyLocalCIDR {
  260. lr.Any = true
  261. return
  262. }
  263. // Share the rule's local cidrs until another rule's need adding, then copy them instead of changing them
  264. if lr.LocalCIDR == nil {
  265. lr.LocalCIDR = r.localCIDR
  266. return
  267. }
  268. for prefix := range r.localCIDR.All() {
  269. lr.LocalCIDR = lr.LocalCIDR.InsertPersist(prefix)
  270. }
  271. }
  272. // getOrNew returns m[k], adding a new value for k first when there isn't one. m is made if it's nil.
  273. func getOrNew[M ~map[K]*V, K comparable, V any](m *M, k K) *V {
  274. if *m == nil {
  275. *m = make(M)
  276. }
  277. v := (*m)[k]
  278. if v == nil {
  279. v = new(V)
  280. (*m)[k] = v
  281. }
  282. return v
  283. }