rules_builder.go 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260
  1. package firewall
  2. import (
  3. "crypto/sha256"
  4. "encoding/hex"
  5. "fmt"
  6. "hash"
  7. "hash/fnv"
  8. "io"
  9. "log/slog"
  10. "math"
  11. "net/netip"
  12. "slices"
  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 in the order they were added, waiting to be built into a Table
  26. type tableRules []rule
  27. func NewRulesBuilder(l *slog.Logger) *RulesBuilder {
  28. return &RulesBuilder{
  29. ruleSHA: sha256.New(),
  30. ruleFNV: fnv.New32a(),
  31. l: l,
  32. }
  33. }
  34. // AddRule adds a rule for incoming or outgoing traffic, on proto or on every protocol when proto is ProtoAny.
  35. // The rule is parsed now, so a bad rule is reported when it's added rather than when the Tables are built.
  36. func (b *RulesBuilder) AddRule(incoming bool, proto uint8, startPort int32, endPort int32, groups []string, host string, cidr, localCidr, caName string, caSha string) error {
  37. // ICMPv6 rules are ICMP rules, and a packet of either protocol is checked against both
  38. if proto == iputil.IPProtocolICMPv6 {
  39. proto = iputil.IPProtocolICMP
  40. }
  41. if proto == iputil.IPProtocolICMP {
  42. //ICMP traffic doesn't have ports, so we always coerce to "any", even if a value is provided
  43. if startPort != PortAny {
  44. b.l.Warn("ignoring port specification for ICMP firewall rule", "startPort", startPort)
  45. }
  46. startPort = PortAny
  47. endPort = PortAny
  48. }
  49. r, err := parseRule(startPort, endPort, groups, host, cidr, localCidr, caName, caSha)
  50. if err != nil {
  51. return err
  52. }
  53. if incoming {
  54. b.in.add(proto, r)
  55. } else {
  56. b.out.add(proto, r)
  57. }
  58. // The rule hashes are of these lines, changing them changes every hash
  59. fmt.Fprintf(io.MultiWriter(b.ruleSHA, b.ruleFNV),
  60. "incoming: %v, proto: %v, startPort: %v, endPort: %v, groups: %v, host: %v, ip: %v, localIp: %v, caName: %v, caSha: %s\n",
  61. incoming, proto, startPort, endPort, groups, host, cidr, localCidr, caName, caSha,
  62. )
  63. direction := "incoming"
  64. if !incoming {
  65. direction = "outgoing"
  66. }
  67. b.l.Info("Firewall rule added",
  68. "firewallRule", m{"direction": direction, "proto": proto, "startPort": startPort, "endPort": endPort, "groups": groups, "host": host, "cidr": cidr, "localCidr": localCidr, "caName": caName, "caSha": caSha},
  69. )
  70. return nil
  71. }
  72. // Build turns the rules into a Table for each direction, and hashes them. Rules without a local cidr allow
  73. // vpnNetworks when there are unsafe networks, otherwise any local address.
  74. func (b *RulesBuilder) Build(vpnNetworks, unsafeNetworks []netip.Prefix) Rules {
  75. var defaultLocal []netip.Prefix
  76. if len(unsafeNetworks) > 0 && !b.defaultLocalCIDRAny {
  77. // Never nil, even when there are no vpn networks, so that the rules allow nothing rather than everything.
  78. defaultLocal = append([]netip.Prefix{}, vpnNetworks...)
  79. }
  80. return Rules{
  81. In: b.in.build(defaultLocal),
  82. Out: b.out.build(defaultLocal),
  83. Hash: hex.EncodeToString(b.ruleSHA.Sum(nil)),
  84. HashFNV: b.ruleFNV.Sum32(),
  85. }
  86. }
  87. func parseRule(startPort, endPort int32, groups []string, host, cidr, localCidr, caName, caSha string) (rule, error) {
  88. if startPort > endPort {
  89. return rule{}, fmt.Errorf("start port was lower than end port")
  90. }
  91. if startPort < PortFragment || endPort > math.MaxUint16 {
  92. return rule{}, fmt.Errorf("port range %d-%d is outside of %d-%d", startPort, endPort, PortFragment, math.MaxUint16)
  93. }
  94. // A range that includes port 0 is port `any`.
  95. if startPort <= PortAny && PortAny <= endPort {
  96. startPort, endPort = PortAny, PortAny
  97. }
  98. r := rule{
  99. startPort: startPort,
  100. endPort: endPort,
  101. groups: groups,
  102. host: host,
  103. caName: caName,
  104. caSha: caSha,
  105. }
  106. var anyCIDR bool
  107. var err error
  108. r.cidr, anyCIDR, err = parseCIDR(cidr)
  109. if err != nil {
  110. return rule{}, fmt.Errorf("cidr did not parse; %w", err)
  111. }
  112. // A rule with no group, host, or cidr allows any host, as does a rule with `any` for one of them.
  113. r.remoteAny = anyCIDR || host == "any" || slices.Contains(groups, "any") ||
  114. (len(groups) == 0 && host == "" && !r.cidr.IsValid())
  115. var localCIDR netip.Prefix
  116. localCIDR, r.localAny, err = parseCIDR(localCidr)
  117. if err != nil {
  118. return rule{}, fmt.Errorf("local_cidr did not parse; %w", err)
  119. }
  120. if localCIDR.IsValid() {
  121. r.local = []netip.Prefix{localCIDR}
  122. }
  123. return r, nil
  124. }
  125. // add keeps r for proto, or for every protocol when proto is ProtoAny.
  126. func (tr *tableRules) add(proto uint8, r rule) {
  127. r.proto = proto
  128. *tr = append(*tr, r)
  129. }
  130. // build turns the rules into a Table. Rules without a local cidr allow defaultLocal,
  131. // or any local address when defaultLocal is nil.
  132. func (tr tableRules) build(defaultLocal []netip.Prefix) *Table {
  133. t := &Table{rules: make([]rule, len(tr))}
  134. var hasOwn [256]bool
  135. for id, r := range tr {
  136. t.rules[id] = r.withDefaultLocal(defaultLocal)
  137. if r.proto != ProtoAny {
  138. hasOwn[r.proto] = true
  139. }
  140. }
  141. // A protocol with rules of its own gets an index of those rules plus the proto `any` rules, so a packet
  142. // checks a single index. The other protocols share an index of the proto `any` rules, one for protocols with
  143. // ports and one for protocols without.
  144. anyPorts := newProtoIndex(true, t.rules, ProtoAny)
  145. anyNoPorts := newProtoIndex(false, t.rules, ProtoAny)
  146. for proto := range t.protos {
  147. switch {
  148. case hasOwn[proto]:
  149. t.protos[proto] = newProtoIndex(iputil.HasPorts(uint8(proto)), t.rules, uint8(proto))
  150. case iputil.HasPorts(uint8(proto)):
  151. t.protos[proto] = anyPorts
  152. default:
  153. t.protos[proto] = anyNoPorts
  154. }
  155. }
  156. // ICMPv6 rules are ICMP rules, see AddRule, so ICMPv6 packets are checked against the ICMP index
  157. t.protos[iputil.IPProtocolICMPv6] = t.protos[iputil.IPProtocolICMP]
  158. return t
  159. }
  160. // newProtoIndex builds an index of the proto `any` rules and proto's own rules, for a protocol with or without
  161. // ports, or returns nil when there are none. With ProtoAny, it indexes only the proto `any` rules.
  162. func newProtoIndex(hasPorts bool, rules []rule, proto uint8) *protoIndex {
  163. var pi *protoIndex
  164. for id := range rules {
  165. r := &rules[id]
  166. if r.proto != ProtoAny && r.proto != proto {
  167. continue
  168. }
  169. if pi == nil {
  170. pi = &protoIndex{hasPorts: hasPorts, fragment: newRuleSet(len(rules))}
  171. if hasPorts {
  172. pi.byPort = newRuleSets(math.MaxUint16+1, len(rules))
  173. } else {
  174. pi.packet = newRuleSet(len(rules))
  175. }
  176. }
  177. pi.add(r, id)
  178. }
  179. return pi
  180. }
  181. // add puts id in every set that r's port clause covers.
  182. func (pi *protoIndex) add(r *rule, id int) {
  183. switch r.startPort {
  184. case PortAny:
  185. pi.fragment.add(id)
  186. if pi.hasPorts {
  187. pi.addPorts(0, math.MaxUint16, id)
  188. } else {
  189. pi.packet.add(id)
  190. }
  191. case PortFragment:
  192. pi.fragment.add(id)
  193. default:
  194. // A range only applies to a protocol with ports.
  195. if pi.hasPorts {
  196. pi.addPorts(int(r.startPort), int(r.endPort), id)
  197. }
  198. }
  199. }
  200. // addPorts puts id in the set of every port from lo through hi.
  201. func (pi *protoIndex) addPorts(lo, hi, id int) {
  202. for port := lo; port <= hi; port++ {
  203. pi.byPort.at(port).add(id)
  204. }
  205. }
  206. // parseCIDR parses a rule's cidr, which is empty, `any`, or a prefix
  207. func parseCIDR(s string) (prefix netip.Prefix, isAny bool, err error) {
  208. switch s {
  209. case "":
  210. return netip.Prefix{}, false, nil
  211. case "any":
  212. return netip.Prefix{}, true, nil
  213. }
  214. prefix, err = netip.ParsePrefix(s)
  215. return prefix, false, err
  216. }
  217. // withDefaultLocal returns r allowing defaultLocal, or any local address when defaultLocal is nil,
  218. // unless r has a local cidr of its own
  219. func (r rule) withDefaultLocal(defaultLocal []netip.Prefix) rule {
  220. if !r.localAny && r.local == nil {
  221. r.localAny = defaultLocal == nil
  222. r.local = defaultLocal
  223. }
  224. return r
  225. }