| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307 |
- package firewall
- import (
- "crypto/sha256"
- "encoding/hex"
- "fmt"
- "hash"
- "hash/fnv"
- "io"
- "log/slog"
- "net/netip"
- "slices"
- "github.com/gaissmai/bart"
- "github.com/slackhq/nebula/iputil"
- )
- // RulesBuilder collects the inbound and outbound rules, Build turns them into a Table for each direction
- type RulesBuilder struct {
- in, out tableRules
- // defaultLocalCIDRAny makes rules without a local cidr allow any local address, even with unsafe networks
- defaultLocalCIDRAny bool
- // ruleSHA and ruleFNV hash every rule as it's added, see Rules
- ruleSHA hash.Hash
- ruleFNV hash.Hash32
- l *slog.Logger
- }
- // tableRules are one direction's rules, waiting to be built into a Table
- type tableRules struct {
- anyRules []rule
- // protoRules holds the rules for each protocol besides proto `any`, ICMPv6 rules are kept with ICMP
- protoRules [256][]rule
- }
- // rule is a parsed rule, so adding it to a Table can't fail
- type rule struct {
- startPort, endPort int32
- groups []string
- host string
- caName, caSha string
- // cidr is the remote cidr, not valid when the rule doesn't have one
- cidr netip.Prefix
- anyCIDR bool
- // localCIDRs are the local addresses the rule allows, anyLocalCIDR allows all of them.
- // Neither is set for a rule without a local cidr until the Table is built and it gets the default.
- localCIDRs []netip.Prefix
- anyLocalCIDR bool
- }
- func NewRulesBuilder(l *slog.Logger) *RulesBuilder {
- return &RulesBuilder{
- ruleSHA: sha256.New(),
- ruleFNV: fnv.New32a(),
- l: l,
- }
- }
- // AddRule adds a rule for incoming or outgoing traffic, on proto or on every protocol when proto is ProtoAny.
- // The rule is parsed now, so a bad rule is reported when it's added rather than when the Tables are built.
- func (b *RulesBuilder) AddRule(incoming bool, proto uint8, startPort int32, endPort int32, groups []string, host string, cidr, localCidr, caName string, caSha string) error {
- if proto == iputil.IPProtocolICMP || proto == iputil.IPProtocolICMPv6 {
- //ICMP traffic doesn't have ports, so we always coerce to "any", even if a value is provided
- if startPort != PortAny {
- b.l.Warn("ignoring port specification for ICMP firewall rule", "startPort", startPort)
- }
- startPort = PortAny
- endPort = PortAny
- }
- r, err := parseRule(startPort, endPort, groups, host, cidr, localCidr, caName, caSha)
- if err != nil {
- return err
- }
- if incoming {
- b.in.add(proto, r)
- } else {
- b.out.add(proto, r)
- }
- // The rule hashes are of these lines, changing them changes every hash
- fmt.Fprintf(io.MultiWriter(b.ruleSHA, b.ruleFNV),
- "incoming: %v, proto: %v, startPort: %v, endPort: %v, groups: %v, host: %v, ip: %v, localIp: %v, caName: %v, caSha: %s\n",
- incoming, proto, startPort, endPort, groups, host, cidr, localCidr, caName, caSha,
- )
- direction := "incoming"
- if !incoming {
- direction = "outgoing"
- }
- b.l.Info("Firewall rule added",
- "firewallRule", m{"direction": direction, "proto": proto, "startPort": startPort, "endPort": endPort, "groups": groups, "host": host, "cidr": cidr, "localCidr": localCidr, "caName": caName, "caSha": caSha},
- )
- return nil
- }
- // Build turns the rules into a Table for each direction, and hashes them. Rules without a local cidr allow
- // vpnNetworks when there are unsafe networks, otherwise any local address.
- func (b *RulesBuilder) Build(vpnNetworks, unsafeNetworks []netip.Prefix) Rules {
- anyLocal := len(unsafeNetworks) == 0 || b.defaultLocalCIDRAny
- return Rules{
- In: b.in.build(anyLocal, vpnNetworks),
- Out: b.out.build(anyLocal, vpnNetworks),
- Hash: hex.EncodeToString(b.ruleSHA.Sum(nil)),
- HashFNV: b.ruleFNV.Sum32(),
- }
- }
- func parseRule(startPort, endPort int32, groups []string, host, cidr, localCidr, caName, caSha string) (rule, error) {
- if startPort > endPort {
- return rule{}, fmt.Errorf("start port was lower than end port")
- }
- r := rule{
- startPort: startPort,
- endPort: endPort,
- groups: groups,
- host: host,
- caName: caName,
- caSha: caSha,
- }
- var err error
- r.cidr, r.anyCIDR, err = parseCIDR(cidr)
- if err != nil {
- return rule{}, fmt.Errorf("cidr did not parse; %w", err)
- }
- var localCIDR netip.Prefix
- localCIDR, r.anyLocalCIDR, err = parseCIDR(localCidr)
- if err != nil {
- return rule{}, fmt.Errorf("local_cidr did not parse; %w", err)
- }
- if localCIDR.IsValid() {
- r.localCIDRs = []netip.Prefix{localCIDR}
- }
- return r, nil
- }
- // add keeps r for proto, or for every protocol when proto is ProtoAny
- func (tr *tableRules) add(proto uint8, r rule) {
- switch proto {
- case ProtoAny:
- tr.anyRules = append(tr.anyRules, r)
- case iputil.IPProtocolICMPv6:
- tr.protoRules[iputil.IPProtocolICMP] = append(tr.protoRules[iputil.IPProtocolICMP], r)
- default:
- tr.protoRules[proto] = append(tr.protoRules[proto], r)
- }
- }
- // build turns the rules into a Table. Rules without a local cidr allow defaultLocalCIDRs,
- // or any local address when defaultLocalAny is set.
- func (tr *tableRules) build(defaultLocalAny bool, defaultLocalCIDRs []netip.Prefix) *Table {
- add := func(pr portRules, rules []rule) {
- for _, r := range rules {
- if !r.anyLocalCIDR && r.localCIDRs == nil {
- r.anyLocalCIDR, r.localCIDRs = defaultLocalAny, defaultLocalCIDRs
- }
- pr.add(r)
- }
- }
- var anyProto portRules
- if len(tr.anyRules) > 0 {
- anyProto = portRules{}
- add(anyProto, tr.anyRules)
- }
- t := &Table{}
- for proto := range t.protos {
- rules := tr.protoRules[proto]
- if len(rules) == 0 {
- t.protos[proto] = anyProto
- continue
- }
- pr := portRules{}
- add(pr, tr.anyRules)
- add(pr, rules)
- t.protos[proto] = pr
- }
- t.protos[iputil.IPProtocolICMPv6] = t.protos[iputil.IPProtocolICMP]
- return t
- }
- // parseCIDR parses a rule's cidr, which is empty, `any`, or a prefix
- func parseCIDR(s string) (prefix netip.Prefix, isAny bool, err error) {
- switch s {
- case "":
- return netip.Prefix{}, false, nil
- case "any":
- return netip.Prefix{}, true, nil
- }
- prefix, err = netip.ParsePrefix(s)
- return prefix, false, err
- }
- // isAny reports whether r matches any host, making its groups, host, and cidr irrelevant
- func (r rule) isAny() bool {
- if len(r.groups) == 0 && r.host == "" && !r.cidr.IsValid() {
- return true
- }
- return slices.Contains(r.groups, "any") || r.host == "any" || r.anyCIDR
- }
- func (pr portRules) add(r rule) {
- for i := r.startPort; i <= r.endPort; i++ {
- cr := pr[i]
- if cr == nil {
- cr = &caRules{}
- pr[i] = cr
- }
- cr.add(r)
- }
- }
- func (cr *caRules) add(r rule) {
- if r.caSha == "" && r.caName == "" {
- if cr.Any == nil {
- cr.Any = &remoteRules{}
- }
- cr.Any.add(r)
- return
- }
- if r.caSha != "" {
- getOrNew(&cr.CAShas, r.caSha).add(r)
- }
- if r.caName != "" {
- getOrNew(&cr.CANames, r.caName).add(r)
- }
- }
- func (rr *remoteRules) add(r rule) {
- if r.isAny() {
- if rr.Any == nil {
- rr.Any = &localRules{}
- }
- rr.Any.add(r)
- return
- }
- if len(r.groups) > 0 {
- lr := &localRules{}
- lr.add(r)
- rr.Groups = append(rr.Groups, &groupsRule{
- Groups: r.groups,
- LocalCIDR: lr,
- })
- }
- if r.host != "" {
- getOrNew(&rr.Hosts, r.host).add(r)
- }
- if r.cidr.IsValid() {
- if rr.CIDR == nil {
- rr.CIDR = new(bart.Table[*localRules])
- }
- lr, ok := rr.CIDR.Get(r.cidr)
- if !ok {
- lr = &localRules{}
- rr.CIDR.Insert(r.cidr, lr)
- }
- lr.add(r)
- }
- }
- func (lr *localRules) add(r rule) {
- if r.anyLocalCIDR {
- lr.Any = true
- return
- }
- if lr.LocalCIDR == nil {
- lr.LocalCIDR = new(bart.Lite)
- }
- for _, prefix := range r.localCIDRs {
- lr.LocalCIDR.Insert(prefix)
- }
- }
- // getOrNew returns m[k], adding a new value for k first when there isn't one. m is made if it's nil.
- func getOrNew[M ~map[K]*V, K comparable, V any](m *M, k K) *V {
- if *m == nil {
- *m = make(M)
- }
- v := (*m)[k]
- if v == nil {
- v = new(V)
- (*m)[k] = v
- }
- return v
- }
|