rules_builder.go 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307
  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. // localCIDRs are 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. localCIDRs []netip.Prefix
  43. anyLocalCIDR bool
  44. }
  45. func NewRulesBuilder(l *slog.Logger) *RulesBuilder {
  46. return &RulesBuilder{
  47. ruleSHA: sha256.New(),
  48. ruleFNV: fnv.New32a(),
  49. l: l,
  50. }
  51. }
  52. // AddRule adds a rule for incoming or outgoing traffic, on proto or on every protocol when proto is ProtoAny.
  53. // The rule is parsed now, so a bad rule is reported when it's added rather than when the Tables are built.
  54. func (b *RulesBuilder) AddRule(incoming bool, proto uint8, startPort int32, endPort int32, groups []string, host string, cidr, localCidr, caName string, caSha string) error {
  55. if proto == iputil.IPProtocolICMP || proto == iputil.IPProtocolICMPv6 {
  56. //ICMP traffic doesn't have ports, so we always coerce to "any", even if a value is provided
  57. if startPort != PortAny {
  58. b.l.Warn("ignoring port specification for ICMP firewall rule", "startPort", startPort)
  59. }
  60. startPort = PortAny
  61. endPort = PortAny
  62. }
  63. r, err := parseRule(startPort, endPort, groups, host, cidr, localCidr, caName, caSha)
  64. if err != nil {
  65. return err
  66. }
  67. if incoming {
  68. b.in.add(proto, r)
  69. } else {
  70. b.out.add(proto, r)
  71. }
  72. // The rule hashes are of these lines, changing them changes every hash
  73. fmt.Fprintf(io.MultiWriter(b.ruleSHA, b.ruleFNV),
  74. "incoming: %v, proto: %v, startPort: %v, endPort: %v, groups: %v, host: %v, ip: %v, localIp: %v, caName: %v, caSha: %s\n",
  75. incoming, proto, startPort, endPort, groups, host, cidr, localCidr, caName, caSha,
  76. )
  77. direction := "incoming"
  78. if !incoming {
  79. direction = "outgoing"
  80. }
  81. b.l.Info("Firewall rule added",
  82. "firewallRule", m{"direction": direction, "proto": proto, "startPort": startPort, "endPort": endPort, "groups": groups, "host": host, "cidr": cidr, "localCidr": localCidr, "caName": caName, "caSha": caSha},
  83. )
  84. return nil
  85. }
  86. // Build turns the rules into a Table for each direction, and hashes them. Rules without a local cidr allow
  87. // vpnNetworks when there are unsafe networks, otherwise any local address.
  88. func (b *RulesBuilder) Build(vpnNetworks, unsafeNetworks []netip.Prefix) Rules {
  89. anyLocal := len(unsafeNetworks) == 0 || b.defaultLocalCIDRAny
  90. return Rules{
  91. In: b.in.build(anyLocal, vpnNetworks),
  92. Out: b.out.build(anyLocal, vpnNetworks),
  93. Hash: hex.EncodeToString(b.ruleSHA.Sum(nil)),
  94. HashFNV: b.ruleFNV.Sum32(),
  95. }
  96. }
  97. func parseRule(startPort, endPort int32, groups []string, host, cidr, localCidr, caName, caSha string) (rule, error) {
  98. if startPort > endPort {
  99. return rule{}, fmt.Errorf("start port was lower than end port")
  100. }
  101. r := rule{
  102. startPort: startPort,
  103. endPort: endPort,
  104. groups: groups,
  105. host: host,
  106. caName: caName,
  107. caSha: caSha,
  108. }
  109. var err error
  110. r.cidr, r.anyCIDR, err = parseCIDR(cidr)
  111. if err != nil {
  112. return rule{}, fmt.Errorf("cidr did not parse; %w", err)
  113. }
  114. var localCIDR netip.Prefix
  115. localCIDR, r.anyLocalCIDR, err = parseCIDR(localCidr)
  116. if err != nil {
  117. return rule{}, fmt.Errorf("local_cidr did not parse; %w", err)
  118. }
  119. if localCIDR.IsValid() {
  120. r.localCIDRs = []netip.Prefix{localCIDR}
  121. }
  122. return r, nil
  123. }
  124. // add keeps r for proto, or for every protocol when proto is ProtoAny
  125. func (tr *tableRules) add(proto uint8, r rule) {
  126. switch proto {
  127. case ProtoAny:
  128. tr.anyRules = append(tr.anyRules, r)
  129. case iputil.IPProtocolICMPv6:
  130. tr.protoRules[iputil.IPProtocolICMP] = append(tr.protoRules[iputil.IPProtocolICMP], r)
  131. default:
  132. tr.protoRules[proto] = append(tr.protoRules[proto], r)
  133. }
  134. }
  135. // build turns the rules into a Table. Rules without a local cidr allow defaultLocalCIDRs,
  136. // or any local address when defaultLocalAny is set.
  137. func (tr *tableRules) build(defaultLocalAny bool, defaultLocalCIDRs []netip.Prefix) *Table {
  138. add := func(pr portRules, rules []rule) {
  139. for _, r := range rules {
  140. if !r.anyLocalCIDR && r.localCIDRs == nil {
  141. r.anyLocalCIDR, r.localCIDRs = defaultLocalAny, defaultLocalCIDRs
  142. }
  143. pr.add(r)
  144. }
  145. }
  146. var anyProto portRules
  147. if len(tr.anyRules) > 0 {
  148. anyProto = portRules{}
  149. add(anyProto, tr.anyRules)
  150. }
  151. t := &Table{}
  152. for proto := range t.protos {
  153. rules := tr.protoRules[proto]
  154. if len(rules) == 0 {
  155. t.protos[proto] = anyProto
  156. continue
  157. }
  158. pr := portRules{}
  159. add(pr, tr.anyRules)
  160. add(pr, rules)
  161. t.protos[proto] = pr
  162. }
  163. t.protos[iputil.IPProtocolICMPv6] = t.protos[iputil.IPProtocolICMP]
  164. return t
  165. }
  166. // parseCIDR parses a rule's cidr, which is empty, `any`, or a prefix
  167. func parseCIDR(s string) (prefix netip.Prefix, isAny bool, err error) {
  168. switch s {
  169. case "":
  170. return netip.Prefix{}, false, nil
  171. case "any":
  172. return netip.Prefix{}, true, nil
  173. }
  174. prefix, err = netip.ParsePrefix(s)
  175. return prefix, false, err
  176. }
  177. // isAny reports whether r matches any host, making its groups, host, and cidr irrelevant
  178. func (r rule) isAny() bool {
  179. if len(r.groups) == 0 && r.host == "" && !r.cidr.IsValid() {
  180. return true
  181. }
  182. return slices.Contains(r.groups, "any") || r.host == "any" || r.anyCIDR
  183. }
  184. func (pr portRules) add(r rule) {
  185. for i := r.startPort; i <= r.endPort; i++ {
  186. cr := pr[i]
  187. if cr == nil {
  188. cr = &caRules{}
  189. pr[i] = cr
  190. }
  191. cr.add(r)
  192. }
  193. }
  194. func (cr *caRules) add(r rule) {
  195. if r.caSha == "" && r.caName == "" {
  196. if cr.Any == nil {
  197. cr.Any = &remoteRules{}
  198. }
  199. cr.Any.add(r)
  200. return
  201. }
  202. if r.caSha != "" {
  203. getOrNew(&cr.CAShas, r.caSha).add(r)
  204. }
  205. if r.caName != "" {
  206. getOrNew(&cr.CANames, r.caName).add(r)
  207. }
  208. }
  209. func (rr *remoteRules) add(r rule) {
  210. if r.isAny() {
  211. if rr.Any == nil {
  212. rr.Any = &localRules{}
  213. }
  214. rr.Any.add(r)
  215. return
  216. }
  217. if len(r.groups) > 0 {
  218. lr := &localRules{}
  219. lr.add(r)
  220. rr.Groups = append(rr.Groups, &groupsRule{
  221. Groups: r.groups,
  222. LocalCIDR: lr,
  223. })
  224. }
  225. if r.host != "" {
  226. getOrNew(&rr.Hosts, r.host).add(r)
  227. }
  228. if r.cidr.IsValid() {
  229. if rr.CIDR == nil {
  230. rr.CIDR = new(bart.Table[*localRules])
  231. }
  232. lr, ok := rr.CIDR.Get(r.cidr)
  233. if !ok {
  234. lr = &localRules{}
  235. rr.CIDR.Insert(r.cidr, lr)
  236. }
  237. lr.add(r)
  238. }
  239. }
  240. func (lr *localRules) add(r rule) {
  241. if r.anyLocalCIDR {
  242. lr.Any = true
  243. return
  244. }
  245. if lr.LocalCIDR == nil {
  246. lr.LocalCIDR = new(bart.Lite)
  247. }
  248. for _, prefix := range r.localCIDRs {
  249. lr.LocalCIDR.Insert(prefix)
  250. }
  251. }
  252. // getOrNew returns m[k], adding a new value for k first when there isn't one. m is made if it's nil.
  253. func getOrNew[M ~map[K]*V, K comparable, V any](m *M, k K) *V {
  254. if *m == nil {
  255. *m = make(M)
  256. }
  257. v := (*m)[k]
  258. if v == nil {
  259. v = new(V)
  260. (*m)[k] = v
  261. }
  262. return v
  263. }