mirror of
https://github.com/slackhq/nebula.git
synced 2026-11-15 15:56:52 +01:00
329 lines
8.0 KiB
Go
329 lines
8.0 KiB
Go
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
|
|
}
|
|
|
|
// Rules are the Tables built for each direction, with hashes of the rules they were built from
|
|
type Rules struct {
|
|
In, Out *Table
|
|
|
|
// Hash is a sha256 of the rules, HashFNV is an FNV-1a of them for use as a metric value.
|
|
// Rule sets with the same rules have the same hashes.
|
|
Hash string
|
|
HashFNV uint32
|
|
}
|
|
|
|
// 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{}, err
|
|
}
|
|
|
|
var localCIDR netip.Prefix
|
|
localCIDR, r.anyLocalCIDR, err = parseCIDR(localCidr)
|
|
if err != nil {
|
|
return rule{}, 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++ {
|
|
if _, ok := pr[i]; !ok {
|
|
pr[i] = &caRules{
|
|
CANames: make(map[string]*remoteRules),
|
|
CAShas: make(map[string]*remoteRules),
|
|
}
|
|
}
|
|
|
|
pr[i].add(r)
|
|
}
|
|
}
|
|
|
|
func (cr *caRules) add(r rule) {
|
|
newRemoteRules := func() *remoteRules {
|
|
return &remoteRules{
|
|
Hosts: make(map[string]*localRules),
|
|
Groups: make([]*groupsRule, 0),
|
|
CIDR: new(bart.Table[*localRules]),
|
|
}
|
|
}
|
|
|
|
if r.caSha == "" && r.caName == "" {
|
|
if cr.Any == nil {
|
|
cr.Any = newRemoteRules()
|
|
}
|
|
|
|
cr.Any.add(r)
|
|
return
|
|
}
|
|
|
|
if r.caSha != "" {
|
|
if _, ok := cr.CAShas[r.caSha]; !ok {
|
|
cr.CAShas[r.caSha] = newRemoteRules()
|
|
}
|
|
cr.CAShas[r.caSha].add(r)
|
|
}
|
|
|
|
if r.caName != "" {
|
|
if _, ok := cr.CANames[r.caName]; !ok {
|
|
cr.CANames[r.caName] = newRemoteRules()
|
|
}
|
|
cr.CANames[r.caName].add(r)
|
|
}
|
|
}
|
|
|
|
func (rr *remoteRules) add(r rule) {
|
|
newLocalRules := func() *localRules {
|
|
return &localRules{
|
|
LocalCIDR: new(bart.Lite),
|
|
}
|
|
}
|
|
|
|
if r.isAny() {
|
|
if rr.Any == nil {
|
|
rr.Any = newLocalRules()
|
|
}
|
|
|
|
rr.Any.add(r)
|
|
return
|
|
}
|
|
|
|
if len(r.groups) > 0 {
|
|
lr := newLocalRules()
|
|
lr.add(r)
|
|
|
|
rr.Groups = append(rr.Groups, &groupsRule{
|
|
Groups: r.groups,
|
|
LocalCIDR: lr,
|
|
})
|
|
}
|
|
|
|
if r.host != "" {
|
|
lr := rr.Hosts[r.host]
|
|
if lr == nil {
|
|
lr = newLocalRules()
|
|
}
|
|
lr.add(r)
|
|
rr.Hosts[r.host] = lr
|
|
}
|
|
|
|
if r.cidr.IsValid() {
|
|
lr, _ := rr.CIDR.Get(r.cidr)
|
|
if lr == nil {
|
|
lr = newLocalRules()
|
|
}
|
|
lr.add(r)
|
|
rr.CIDR.Insert(r.cidr, lr)
|
|
}
|
|
}
|
|
|
|
func (lr *localRules) add(r rule) {
|
|
if r.anyLocalCIDR {
|
|
lr.Any = true
|
|
return
|
|
}
|
|
|
|
for _, prefix := range r.localCIDRs {
|
|
lr.LocalCIDR.Insert(prefix)
|
|
}
|
|
}
|