rules_builder.go 8.0 KB

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