123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017 |
- package nebula
- import (
- "crypto/sha256"
- "encoding/hex"
- "errors"
- "fmt"
- "hash/fnv"
- "net/netip"
- "reflect"
- "strconv"
- "strings"
- "sync"
- "time"
- "github.com/gaissmai/bart"
- "github.com/rcrowley/go-metrics"
- "github.com/sirupsen/logrus"
- "github.com/slackhq/nebula/cert"
- "github.com/slackhq/nebula/config"
- "github.com/slackhq/nebula/firewall"
- )
- type FirewallInterface interface {
- AddRule(incoming bool, proto uint8, startPort int32, endPort int32, groups []string, host string, addr, localAddr netip.Prefix, caName string, caSha string) error
- }
- type conn struct {
- Expires time.Time // Time when this conntrack entry will expire
- // record why the original connection passed the firewall, so we can re-validate
- // after ruleset changes. Note, rulesVersion is a uint16 so that these two
- // fields pack for free after the uint32 above
- incoming bool
- rulesVersion uint16
- }
- // TODO: need conntrack max tracked connections handling
- type Firewall struct {
- Conntrack *FirewallConntrack
- InRules *FirewallTable
- OutRules *FirewallTable
- InSendReject bool
- OutSendReject bool
- //TODO: we should have many more options for TCP, an option for ICMP, and mimic the kernel a bit better
- // https://www.kernel.org/doc/Documentation/networking/nf_conntrack-sysctl.txt
- TCPTimeout time.Duration //linux: 5 days max
- UDPTimeout time.Duration //linux: 180s max
- DefaultTimeout time.Duration //linux: 600s
- // routableNetworks describes the vpn addresses as well as any unsafe networks issued to us in the certificate.
- // The vpn addresses are a full bit match while the unsafe networks only match the prefix
- routableNetworks *bart.Lite
- // assignedNetworks is a list of vpn networks assigned to us in the certificate.
- assignedNetworks []netip.Prefix
- hasUnsafeNetworks bool
- rules string
- rulesVersion uint16
- defaultLocalCIDRAny bool
- incomingMetrics firewallMetrics
- outgoingMetrics firewallMetrics
- l *logrus.Logger
- }
- type firewallMetrics struct {
- droppedLocalAddr metrics.Counter
- droppedRemoteAddr metrics.Counter
- droppedNoRule metrics.Counter
- }
- type FirewallConntrack struct {
- sync.Mutex
- Conns map[firewall.Packet]*conn
- TimerWheel *TimerWheel[firewall.Packet]
- }
- // FirewallTable is the entry point for a rule, the evaluation order is:
- // Proto AND port AND (CA SHA or CA name) AND local CIDR AND (group OR groups OR name OR remote CIDR)
- type FirewallTable struct {
- TCP firewallPort
- UDP firewallPort
- ICMP firewallPort
- AnyProto firewallPort
- }
- func newFirewallTable() *FirewallTable {
- return &FirewallTable{
- TCP: firewallPort{},
- UDP: firewallPort{},
- ICMP: firewallPort{},
- AnyProto: firewallPort{},
- }
- }
- type FirewallCA struct {
- Any *FirewallRule
- CANames map[string]*FirewallRule
- CAShas map[string]*FirewallRule
- }
- type FirewallRule struct {
- // Any makes Hosts, Groups, and CIDR irrelevant
- Any *firewallLocalCIDR
- Hosts map[string]*firewallLocalCIDR
- Groups []*firewallGroups
- CIDR *bart.Table[*firewallLocalCIDR]
- }
- type firewallGroups struct {
- Groups []string
- LocalCIDR *firewallLocalCIDR
- }
- // Even though ports are uint16, int32 maps are faster for lookup
- // Plus we can use `-1` for fragment rules
- type firewallPort map[int32]*FirewallCA
- type firewallLocalCIDR struct {
- Any bool
- LocalCIDR *bart.Lite
- }
- // NewFirewall creates a new Firewall object. A TimerWheel is created for you from the provided timeouts.
- // The certificate provided should be the highest version loaded in memory.
- func NewFirewall(l *logrus.Logger, tcpTimeout, UDPTimeout, defaultTimeout time.Duration, c cert.Certificate) *Firewall {
- //TODO: error on 0 duration
- var tmin, tmax time.Duration
- if tcpTimeout < UDPTimeout {
- tmin = tcpTimeout
- tmax = UDPTimeout
- } else {
- tmin = UDPTimeout
- tmax = tcpTimeout
- }
- if defaultTimeout < tmin {
- tmin = defaultTimeout
- } else if defaultTimeout > tmax {
- tmax = defaultTimeout
- }
- routableNetworks := new(bart.Lite)
- var assignedNetworks []netip.Prefix
- for _, network := range c.Networks() {
- nprefix := netip.PrefixFrom(network.Addr(), network.Addr().BitLen())
- routableNetworks.Insert(nprefix)
- assignedNetworks = append(assignedNetworks, network)
- }
- hasUnsafeNetworks := false
- for _, n := range c.UnsafeNetworks() {
- routableNetworks.Insert(n)
- hasUnsafeNetworks = true
- }
- return &Firewall{
- Conntrack: &FirewallConntrack{
- Conns: make(map[firewall.Packet]*conn),
- TimerWheel: NewTimerWheel[firewall.Packet](tmin, tmax),
- },
- InRules: newFirewallTable(),
- OutRules: newFirewallTable(),
- TCPTimeout: tcpTimeout,
- UDPTimeout: UDPTimeout,
- DefaultTimeout: defaultTimeout,
- routableNetworks: routableNetworks,
- assignedNetworks: assignedNetworks,
- hasUnsafeNetworks: hasUnsafeNetworks,
- l: l,
- incomingMetrics: firewallMetrics{
- droppedLocalAddr: metrics.GetOrRegisterCounter("firewall.incoming.dropped.local_addr", nil),
- droppedRemoteAddr: metrics.GetOrRegisterCounter("firewall.incoming.dropped.remote_addr", nil),
- droppedNoRule: metrics.GetOrRegisterCounter("firewall.incoming.dropped.no_rule", nil),
- },
- outgoingMetrics: firewallMetrics{
- droppedLocalAddr: metrics.GetOrRegisterCounter("firewall.outgoing.dropped.local_addr", nil),
- droppedRemoteAddr: metrics.GetOrRegisterCounter("firewall.outgoing.dropped.remote_addr", nil),
- droppedNoRule: metrics.GetOrRegisterCounter("firewall.outgoing.dropped.no_rule", nil),
- },
- }
- }
- func NewFirewallFromConfig(l *logrus.Logger, cs *CertState, c *config.C) (*Firewall, error) {
- certificate := cs.getCertificate(cert.Version2)
- if certificate == nil {
- certificate = cs.getCertificate(cert.Version1)
- }
- if certificate == nil {
- panic("No certificate available to reconfigure the firewall")
- }
- fw := NewFirewall(
- l,
- c.GetDuration("firewall.conntrack.tcp_timeout", time.Minute*12),
- c.GetDuration("firewall.conntrack.udp_timeout", time.Minute*3),
- c.GetDuration("firewall.conntrack.default_timeout", time.Minute*10),
- certificate,
- //TODO: max_connections
- )
- fw.defaultLocalCIDRAny = c.GetBool("firewall.default_local_cidr_any", false)
- inboundAction := c.GetString("firewall.inbound_action", "drop")
- switch inboundAction {
- case "reject":
- fw.InSendReject = true
- case "drop":
- fw.InSendReject = false
- default:
- l.WithField("action", inboundAction).Warn("invalid firewall.inbound_action, defaulting to `drop`")
- fw.InSendReject = false
- }
- outboundAction := c.GetString("firewall.outbound_action", "drop")
- switch outboundAction {
- case "reject":
- fw.OutSendReject = true
- case "drop":
- fw.OutSendReject = false
- default:
- l.WithField("action", inboundAction).Warn("invalid firewall.outbound_action, defaulting to `drop`")
- fw.OutSendReject = false
- }
- err := AddFirewallRulesFromConfig(l, false, c, fw)
- if err != nil {
- return nil, err
- }
- err = AddFirewallRulesFromConfig(l, true, c, fw)
- if err != nil {
- return nil, err
- }
- return fw, nil
- }
- // AddRule properly creates the in memory rule structure for a firewall table.
- func (f *Firewall) AddRule(incoming bool, proto uint8, startPort int32, endPort int32, groups []string, host string, ip, localIp netip.Prefix, caName string, caSha string) error {
- // Under gomobile, stringing a nil pointer with fmt causes an abort in debug mode for iOS
- // https://github.com/golang/go/issues/14131
- sIp := ""
- if ip.IsValid() {
- sIp = ip.String()
- }
- lIp := ""
- if localIp.IsValid() {
- lIp = localIp.String()
- }
- // We need this rule string because we generate a hash. Removing this will break firewall reload.
- ruleString := fmt.Sprintf(
- "incoming: %v, proto: %v, startPort: %v, endPort: %v, groups: %v, host: %v, ip: %v, localIp: %v, caName: %v, caSha: %s",
- incoming, proto, startPort, endPort, groups, host, sIp, lIp, caName, caSha,
- )
- f.rules += ruleString + "\n"
- direction := "incoming"
- if !incoming {
- direction = "outgoing"
- }
- f.l.WithField("firewallRule", m{"direction": direction, "proto": proto, "startPort": startPort, "endPort": endPort, "groups": groups, "host": host, "ip": sIp, "localIp": lIp, "caName": caName, "caSha": caSha}).
- Info("Firewall rule added")
- var (
- ft *FirewallTable
- fp firewallPort
- )
- if incoming {
- ft = f.InRules
- } else {
- ft = f.OutRules
- }
- switch proto {
- case firewall.ProtoTCP:
- fp = ft.TCP
- case firewall.ProtoUDP:
- fp = ft.UDP
- case firewall.ProtoICMP, firewall.ProtoICMPv6:
- fp = ft.ICMP
- case firewall.ProtoAny:
- fp = ft.AnyProto
- default:
- return fmt.Errorf("unknown protocol %v", proto)
- }
- return fp.addRule(f, startPort, endPort, groups, host, ip, localIp, caName, caSha)
- }
- // GetRuleHash returns a hash representation of all inbound and outbound rules
- func (f *Firewall) GetRuleHash() string {
- sum := sha256.Sum256([]byte(f.rules))
- return hex.EncodeToString(sum[:])
- }
- // GetRuleHashFNV returns a uint32 FNV-1 hash representation the rules, for use as a metric value
- func (f *Firewall) GetRuleHashFNV() uint32 {
- h := fnv.New32a()
- h.Write([]byte(f.rules))
- return h.Sum32()
- }
- // GetRuleHashes returns both the sha256 and FNV-1 hashes, suitable for logging
- func (f *Firewall) GetRuleHashes() string {
- return "SHA:" + f.GetRuleHash() + ",FNV:" + strconv.FormatUint(uint64(f.GetRuleHashFNV()), 10)
- }
- func AddFirewallRulesFromConfig(l *logrus.Logger, inbound bool, c *config.C, fw FirewallInterface) error {
- var table string
- if inbound {
- table = "firewall.inbound"
- } else {
- table = "firewall.outbound"
- }
- r := c.Get(table)
- if r == nil {
- return nil
- }
- rs, ok := r.([]any)
- if !ok {
- return fmt.Errorf("%s failed to parse, should be an array of rules", table)
- }
- for i, t := range rs {
- var groups []string
- r, err := convertRule(l, t, table, i)
- if err != nil {
- return fmt.Errorf("%s rule #%v; %s", table, i, err)
- }
- if r.Code != "" && r.Port != "" {
- return fmt.Errorf("%s rule #%v; only one of port or code should be provided", table, i)
- }
- if r.Host == "" && len(r.Groups) == 0 && r.Group == "" && r.Cidr == "" && r.LocalCidr == "" && r.CAName == "" && r.CASha == "" {
- return fmt.Errorf("%s rule #%v; at least one of host, group, cidr, local_cidr, ca_name, or ca_sha must be provided", table, i)
- }
- if len(r.Groups) > 0 {
- groups = r.Groups
- }
- if r.Group != "" {
- // Check if we have both groups and group provided in the rule config
- if len(groups) > 0 {
- return fmt.Errorf("%s rule #%v; only one of group or groups should be defined, both provided", table, i)
- }
- groups = []string{r.Group}
- }
- var sPort, errPort string
- if r.Code != "" {
- errPort = "code"
- sPort = r.Code
- } else {
- errPort = "port"
- sPort = r.Port
- }
- startPort, endPort, err := parsePort(sPort)
- if err != nil {
- return fmt.Errorf("%s rule #%v; %s %s", table, i, errPort, err)
- }
- var proto uint8
- switch r.Proto {
- case "any":
- proto = firewall.ProtoAny
- case "tcp":
- proto = firewall.ProtoTCP
- case "udp":
- proto = firewall.ProtoUDP
- case "icmp":
- proto = firewall.ProtoICMP
- default:
- return fmt.Errorf("%s rule #%v; proto was not understood; `%s`", table, i, r.Proto)
- }
- var cidr netip.Prefix
- if r.Cidr != "" {
- cidr, err = netip.ParsePrefix(r.Cidr)
- if err != nil {
- return fmt.Errorf("%s rule #%v; cidr did not parse; %s", table, i, err)
- }
- }
- var localCidr netip.Prefix
- if r.LocalCidr != "" {
- localCidr, err = netip.ParsePrefix(r.LocalCidr)
- if err != nil {
- return fmt.Errorf("%s rule #%v; local_cidr did not parse; %s", table, i, err)
- }
- }
- err = fw.AddRule(inbound, proto, startPort, endPort, groups, r.Host, cidr, localCidr, r.CAName, r.CASha)
- if err != nil {
- return fmt.Errorf("%s rule #%v; `%s`", table, i, err)
- }
- }
- return nil
- }
- var ErrInvalidRemoteIP = errors.New("remote IP is not in remote certificate subnets")
- var ErrInvalidLocalIP = errors.New("local IP is not in list of handled local IPs")
- var ErrNoMatchingRule = errors.New("no matching rule in firewall table")
- // Drop returns an error if the packet should be dropped, explaining why. It
- // returns nil if the packet should not be dropped.
- func (f *Firewall) Drop(fp firewall.Packet, incoming bool, h *HostInfo, caPool *cert.CAPool, localCache firewall.ConntrackCache) error {
- // Check if we spoke to this tuple, if we did then allow this packet
- if f.inConns(fp, h, caPool, localCache) {
- return nil
- }
- // Make sure remote address matches nebula certificate
- if h.networks != nil {
- if !h.networks.Contains(fp.RemoteAddr) {
- f.metrics(incoming).droppedRemoteAddr.Inc(1)
- return ErrInvalidRemoteIP
- }
- } else {
- // Simple case: Certificate has one address and no unsafe networks
- if h.vpnAddrs[0] != fp.RemoteAddr {
- f.metrics(incoming).droppedRemoteAddr.Inc(1)
- return ErrInvalidRemoteIP
- }
- }
- // Make sure we are supposed to be handling this local ip address
- if !f.routableNetworks.Contains(fp.LocalAddr) {
- f.metrics(incoming).droppedLocalAddr.Inc(1)
- return ErrInvalidLocalIP
- }
- table := f.OutRules
- if incoming {
- table = f.InRules
- }
- // We now know which firewall table to check against
- if !table.match(fp, incoming, h.ConnectionState.peerCert, caPool) {
- f.metrics(incoming).droppedNoRule.Inc(1)
- return ErrNoMatchingRule
- }
- // We always want to conntrack since it is a faster operation
- f.addConn(fp, incoming)
- return nil
- }
- func (f *Firewall) metrics(incoming bool) firewallMetrics {
- if incoming {
- return f.incomingMetrics
- } else {
- return f.outgoingMetrics
- }
- }
- // Destroy cleans up any known cyclical references so the object can be free'd my GC. This should be called if a new
- // firewall object is created
- func (f *Firewall) Destroy() {
- //TODO: clean references if/when needed
- }
- func (f *Firewall) EmitStats() {
- conntrack := f.Conntrack
- conntrack.Lock()
- conntrackCount := len(conntrack.Conns)
- conntrack.Unlock()
- metrics.GetOrRegisterGauge("firewall.conntrack.count", nil).Update(int64(conntrackCount))
- metrics.GetOrRegisterGauge("firewall.rules.version", nil).Update(int64(f.rulesVersion))
- metrics.GetOrRegisterGauge("firewall.rules.hash", nil).Update(int64(f.GetRuleHashFNV()))
- }
- func (f *Firewall) inConns(fp firewall.Packet, h *HostInfo, caPool *cert.CAPool, localCache firewall.ConntrackCache) bool {
- if localCache != nil {
- if _, ok := localCache[fp]; ok {
- return true
- }
- }
- conntrack := f.Conntrack
- conntrack.Lock()
- // Purge every time we test
- ep, has := conntrack.TimerWheel.Purge()
- if has {
- f.evict(ep)
- }
- c, ok := conntrack.Conns[fp]
- if !ok {
- conntrack.Unlock()
- return false
- }
- if c.rulesVersion != f.rulesVersion {
- // This conntrack entry was for an older rule set, validate
- // it still passes with the current rule set
- table := f.OutRules
- if c.incoming {
- table = f.InRules
- }
- // We now know which firewall table to check against
- if !table.match(fp, c.incoming, h.ConnectionState.peerCert, caPool) {
- if f.l.Level >= logrus.DebugLevel {
- h.logger(f.l).
- WithField("fwPacket", fp).
- WithField("incoming", c.incoming).
- WithField("rulesVersion", f.rulesVersion).
- WithField("oldRulesVersion", c.rulesVersion).
- Debugln("dropping old conntrack entry, does not match new ruleset")
- }
- delete(conntrack.Conns, fp)
- conntrack.Unlock()
- return false
- }
- if f.l.Level >= logrus.DebugLevel {
- h.logger(f.l).
- WithField("fwPacket", fp).
- WithField("incoming", c.incoming).
- WithField("rulesVersion", f.rulesVersion).
- WithField("oldRulesVersion", c.rulesVersion).
- Debugln("keeping old conntrack entry, does match new ruleset")
- }
- c.rulesVersion = f.rulesVersion
- }
- switch fp.Protocol {
- case firewall.ProtoTCP:
- c.Expires = time.Now().Add(f.TCPTimeout)
- case firewall.ProtoUDP:
- c.Expires = time.Now().Add(f.UDPTimeout)
- default:
- c.Expires = time.Now().Add(f.DefaultTimeout)
- }
- conntrack.Unlock()
- if localCache != nil {
- localCache[fp] = struct{}{}
- }
- return true
- }
- func (f *Firewall) addConn(fp firewall.Packet, incoming bool) {
- var timeout time.Duration
- c := &conn{}
- switch fp.Protocol {
- case firewall.ProtoTCP:
- timeout = f.TCPTimeout
- case firewall.ProtoUDP:
- timeout = f.UDPTimeout
- default:
- timeout = f.DefaultTimeout
- }
- conntrack := f.Conntrack
- conntrack.Lock()
- if _, ok := conntrack.Conns[fp]; !ok {
- conntrack.TimerWheel.Advance(time.Now())
- conntrack.TimerWheel.Add(fp, timeout)
- }
- // Record which rulesVersion allowed this connection, so we can retest after
- // firewall reload
- c.incoming = incoming
- c.rulesVersion = f.rulesVersion
- c.Expires = time.Now().Add(timeout)
- conntrack.Conns[fp] = c
- conntrack.Unlock()
- }
- // Evict checks if a conntrack entry has expired, if so it is removed, if not it is re-added to the wheel
- // Caller must own the connMutex lock!
- func (f *Firewall) evict(p firewall.Packet) {
- // Are we still tracking this conn?
- conntrack := f.Conntrack
- t, ok := conntrack.Conns[p]
- if !ok {
- return
- }
- newT := t.Expires.Sub(time.Now())
- // Timeout is in the future, re-add the timer
- if newT > 0 {
- conntrack.TimerWheel.Advance(time.Now())
- conntrack.TimerWheel.Add(p, newT)
- return
- }
- // This conn is done
- delete(conntrack.Conns, p)
- }
- func (ft *FirewallTable) match(p firewall.Packet, incoming bool, c *cert.CachedCertificate, caPool *cert.CAPool) bool {
- if ft.AnyProto.match(p, incoming, c, caPool) {
- return true
- }
- switch p.Protocol {
- case firewall.ProtoTCP:
- if ft.TCP.match(p, incoming, c, caPool) {
- return true
- }
- case firewall.ProtoUDP:
- if ft.UDP.match(p, incoming, c, caPool) {
- return true
- }
- case firewall.ProtoICMP, firewall.ProtoICMPv6:
- if ft.ICMP.match(p, incoming, c, caPool) {
- return true
- }
- }
- return false
- }
- func (fp firewallPort) addRule(f *Firewall, startPort int32, endPort int32, groups []string, host string, ip, localIp netip.Prefix, caName string, caSha string) error {
- if startPort > endPort {
- return fmt.Errorf("start port was lower than end port")
- }
- for i := startPort; i <= endPort; i++ {
- if _, ok := fp[i]; !ok {
- fp[i] = &FirewallCA{
- CANames: make(map[string]*FirewallRule),
- CAShas: make(map[string]*FirewallRule),
- }
- }
- if err := fp[i].addRule(f, groups, host, ip, localIp, caName, caSha); err != nil {
- return err
- }
- }
- return nil
- }
- func (fp firewallPort) match(p firewall.Packet, incoming bool, c *cert.CachedCertificate, caPool *cert.CAPool) bool {
- // We don't have any allowed ports, bail
- if fp == nil {
- return false
- }
- var port int32
- if p.Fragment {
- port = firewall.PortFragment
- } else if incoming {
- port = int32(p.LocalPort)
- } else {
- port = int32(p.RemotePort)
- }
- if fp[port].match(p, c, caPool) {
- return true
- }
- return fp[firewall.PortAny].match(p, c, caPool)
- }
- func (fc *FirewallCA) addRule(f *Firewall, groups []string, host string, ip, localIp netip.Prefix, caName, caSha string) error {
- fr := func() *FirewallRule {
- return &FirewallRule{
- Hosts: make(map[string]*firewallLocalCIDR),
- Groups: make([]*firewallGroups, 0),
- CIDR: new(bart.Table[*firewallLocalCIDR]),
- }
- }
- if caSha == "" && caName == "" {
- if fc.Any == nil {
- fc.Any = fr()
- }
- return fc.Any.addRule(f, groups, host, ip, localIp)
- }
- if caSha != "" {
- if _, ok := fc.CAShas[caSha]; !ok {
- fc.CAShas[caSha] = fr()
- }
- err := fc.CAShas[caSha].addRule(f, groups, host, ip, localIp)
- if err != nil {
- return err
- }
- }
- if caName != "" {
- if _, ok := fc.CANames[caName]; !ok {
- fc.CANames[caName] = fr()
- }
- err := fc.CANames[caName].addRule(f, groups, host, ip, localIp)
- if err != nil {
- return err
- }
- }
- return nil
- }
- func (fc *FirewallCA) match(p firewall.Packet, c *cert.CachedCertificate, caPool *cert.CAPool) bool {
- if fc == nil {
- return false
- }
- if fc.Any.match(p, c) {
- return true
- }
- if t, ok := fc.CAShas[c.Certificate.Issuer()]; ok {
- if t.match(p, c) {
- return true
- }
- }
- s, err := caPool.GetCAForCert(c.Certificate)
- if err != nil {
- return false
- }
- return fc.CANames[s.Certificate.Name()].match(p, c)
- }
- func (fr *FirewallRule) addRule(f *Firewall, groups []string, host string, ip, localCIDR netip.Prefix) error {
- flc := func() *firewallLocalCIDR {
- return &firewallLocalCIDR{
- LocalCIDR: new(bart.Lite),
- }
- }
- if fr.isAny(groups, host, ip) {
- if fr.Any == nil {
- fr.Any = flc()
- }
- return fr.Any.addRule(f, localCIDR)
- }
- if len(groups) > 0 {
- nlc := flc()
- err := nlc.addRule(f, localCIDR)
- if err != nil {
- return err
- }
- fr.Groups = append(fr.Groups, &firewallGroups{
- Groups: groups,
- LocalCIDR: nlc,
- })
- }
- if host != "" {
- nlc := fr.Hosts[host]
- if nlc == nil {
- nlc = flc()
- }
- err := nlc.addRule(f, localCIDR)
- if err != nil {
- return err
- }
- fr.Hosts[host] = nlc
- }
- if ip.IsValid() {
- nlc, _ := fr.CIDR.Get(ip)
- if nlc == nil {
- nlc = flc()
- }
- err := nlc.addRule(f, localCIDR)
- if err != nil {
- return err
- }
- fr.CIDR.Insert(ip, nlc)
- }
- return nil
- }
- func (fr *FirewallRule) isAny(groups []string, host string, ip netip.Prefix) bool {
- if len(groups) == 0 && host == "" && !ip.IsValid() {
- return true
- }
- for _, group := range groups {
- if group == "any" {
- return true
- }
- }
- if host == "any" {
- return true
- }
- if ip.IsValid() && ip.Bits() == 0 {
- return true
- }
- return false
- }
- func (fr *FirewallRule) match(p firewall.Packet, c *cert.CachedCertificate) bool {
- if fr == nil {
- return false
- }
- // Shortcut path for if groups, hosts, or cidr contained an `any`
- if fr.Any.match(p, c) {
- return true
- }
- // Need any of group, host, or cidr to match
- for _, sg := range fr.Groups {
- found := false
- for _, g := range sg.Groups {
- if _, ok := c.InvertedGroups[g]; !ok {
- found = false
- break
- }
- found = true
- }
- if found && sg.LocalCIDR.match(p, c) {
- return true
- }
- }
- if fr.Hosts != nil {
- if flc, ok := fr.Hosts[c.Certificate.Name()]; ok {
- if flc.match(p, c) {
- return true
- }
- }
- }
- for _, v := range fr.CIDR.Supernets(netip.PrefixFrom(p.RemoteAddr, p.RemoteAddr.BitLen())) {
- if v.match(p, c) {
- return true
- }
- }
- return false
- }
- func (flc *firewallLocalCIDR) addRule(f *Firewall, localIp netip.Prefix) error {
- if !localIp.IsValid() {
- if !f.hasUnsafeNetworks || f.defaultLocalCIDRAny {
- flc.Any = true
- return nil
- }
- for _, network := range f.assignedNetworks {
- flc.LocalCIDR.Insert(network)
- }
- return nil
- } else if localIp.Bits() == 0 {
- flc.Any = true
- return nil
- }
- flc.LocalCIDR.Insert(localIp)
- return nil
- }
- func (flc *firewallLocalCIDR) match(p firewall.Packet, c *cert.CachedCertificate) bool {
- if flc == nil {
- return false
- }
- if flc.Any {
- return true
- }
- return flc.LocalCIDR.Contains(p.LocalAddr)
- }
- type rule struct {
- Port string
- Code string
- Proto string
- Host string
- Group string
- Groups []string
- Cidr string
- LocalCidr string
- CAName string
- CASha string
- }
- func convertRule(l *logrus.Logger, p any, table string, i int) (rule, error) {
- r := rule{}
- m, ok := p.(map[string]any)
- if !ok {
- return r, errors.New("could not parse rule")
- }
- toString := func(k string, m map[string]any) string {
- v, ok := m[k]
- if !ok {
- return ""
- }
- return fmt.Sprintf("%v", v)
- }
- r.Port = toString("port", m)
- r.Code = toString("code", m)
- r.Proto = toString("proto", m)
- r.Host = toString("host", m)
- r.Cidr = toString("cidr", m)
- r.LocalCidr = toString("local_cidr", m)
- r.CAName = toString("ca_name", m)
- r.CASha = toString("ca_sha", m)
- // Make sure group isn't an array
- if v, ok := m["group"].([]any); ok {
- if len(v) > 1 {
- return r, errors.New("group should contain a single value, an array with more than one entry was provided")
- }
- l.Warnf("%s rule #%v; group was an array with a single value, converting to simple value", table, i)
- m["group"] = v[0]
- }
- r.Group = toString("group", m)
- if rg, ok := m["groups"]; ok {
- switch reflect.TypeOf(rg).Kind() {
- case reflect.Slice:
- v := reflect.ValueOf(rg)
- r.Groups = make([]string, v.Len())
- for i := 0; i < v.Len(); i++ {
- r.Groups[i] = v.Index(i).Interface().(string)
- }
- case reflect.String:
- r.Groups = []string{rg.(string)}
- default:
- r.Groups = []string{fmt.Sprintf("%v", rg)}
- }
- }
- return r, nil
- }
- func parsePort(s string) (startPort, endPort int32, err error) {
- if s == "any" {
- startPort = firewall.PortAny
- endPort = firewall.PortAny
- } else if s == "fragment" {
- startPort = firewall.PortFragment
- endPort = firewall.PortFragment
- } else if strings.Contains(s, `-`) {
- sPorts := strings.SplitN(s, `-`, 2)
- sPorts[0] = strings.Trim(sPorts[0], " ")
- sPorts[1] = strings.Trim(sPorts[1], " ")
- if len(sPorts) != 2 || sPorts[0] == "" || sPorts[1] == "" {
- return 0, 0, fmt.Errorf("appears to be a range but could not be parsed; `%s`", s)
- }
- rStartPort, err := strconv.Atoi(sPorts[0])
- if err != nil {
- return 0, 0, fmt.Errorf("beginning range was not a number; `%s`", sPorts[0])
- }
- rEndPort, err := strconv.Atoi(sPorts[1])
- if err != nil {
- return 0, 0, fmt.Errorf("ending range was not a number; `%s`", sPorts[1])
- }
- startPort = int32(rStartPort)
- endPort = int32(rEndPort)
- if startPort == firewall.PortAny {
- endPort = firewall.PortAny
- }
- } else {
- rPort, err := strconv.Atoi(s)
- if err != nil {
- return 0, 0, fmt.Errorf("was not a number; `%s`", s)
- }
- startPort = int32(rPort)
- endPort = startPort
- }
- return
- }
|