rules_builder.go 8.0 KB

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