1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018 |
- 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, ip, localIp 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
- // Used to ensure we don't emit local packets for ips we don't own
- localIps *bart.Table[struct{}]
- assignedCIDR netip.Prefix
- hasSubnets bool
- rules string
- rulesVersion uint16
- defaultLocalCIDRAny bool
- incomingMetrics firewallMetrics
- outgoingMetrics firewallMetrics
- l *logrus.Logger
- }
- type firewallMetrics struct {
- droppedLocalIP metrics.Counter
- droppedRemoteIP 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.Table[struct{}]
- }
- // NewFirewall creates a new Firewall object. A TimerWheel is created for you from the provided timeouts.
- func NewFirewall(l *logrus.Logger, tcpTimeout, UDPTimeout, defaultTimeout time.Duration, c *cert.NebulaCertificate) *Firewall {
- //TODO: error on 0 duration
- var min, max time.Duration
- if tcpTimeout < UDPTimeout {
- min = tcpTimeout
- max = UDPTimeout
- } else {
- min = UDPTimeout
- max = tcpTimeout
- }
- if defaultTimeout < min {
- min = defaultTimeout
- } else if defaultTimeout > max {
- max = defaultTimeout
- }
- localIps := new(bart.Table[struct{}])
- var assignedCIDR netip.Prefix
- var assignedSet bool
- for _, ip := range c.Details.Ips {
- //TODO: IPV6-WORK the unmap is a bit unfortunate
- nip, _ := netip.AddrFromSlice(ip.IP)
- nip = nip.Unmap()
- nprefix := netip.PrefixFrom(nip, nip.BitLen())
- localIps.Insert(nprefix, struct{}{})
- if !assignedSet {
- // Only grabbing the first one in the cert since any more than that currently has undefined behavior
- assignedCIDR = nprefix
- assignedSet = true
- }
- }
- for _, n := range c.Details.Subnets {
- nip, _ := netip.AddrFromSlice(n.IP)
- ones, _ := n.Mask.Size()
- nip = nip.Unmap()
- localIps.Insert(netip.PrefixFrom(nip, ones), struct{}{})
- }
- return &Firewall{
- Conntrack: &FirewallConntrack{
- Conns: make(map[firewall.Packet]*conn),
- TimerWheel: NewTimerWheel[firewall.Packet](min, max),
- },
- InRules: newFirewallTable(),
- OutRules: newFirewallTable(),
- TCPTimeout: tcpTimeout,
- UDPTimeout: UDPTimeout,
- DefaultTimeout: defaultTimeout,
- localIps: localIps,
- assignedCIDR: assignedCIDR,
- hasSubnets: len(c.Details.Subnets) > 0,
- l: l,
- incomingMetrics: firewallMetrics{
- droppedLocalIP: metrics.GetOrRegisterCounter("firewall.incoming.dropped.local_ip", nil),
- droppedRemoteIP: metrics.GetOrRegisterCounter("firewall.incoming.dropped.remote_ip", nil),
- droppedNoRule: metrics.GetOrRegisterCounter("firewall.incoming.dropped.no_rule", nil),
- },
- outgoingMetrics: firewallMetrics{
- droppedLocalIP: metrics.GetOrRegisterCounter("firewall.outgoing.dropped.local_ip", nil),
- droppedRemoteIP: metrics.GetOrRegisterCounter("firewall.outgoing.dropped.remote_ip", nil),
- droppedNoRule: metrics.GetOrRegisterCounter("firewall.outgoing.dropped.no_rule", nil),
- },
- }
- }
- func NewFirewallFromConfig(l *logrus.Logger, nc *cert.NebulaCertificate, c *config.C) (*Firewall, error) {
- 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),
- nc,
- //TODO: max_connections
- )
- //TODO: Flip to false after v1.9 release
- fw.defaultLocalCIDRAny = c.GetBool("firewall.default_local_cidr_any", true)
- 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:
- 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.([]interface{})
- 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.NebulaCAPool, 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 remoteCidr := h.remoteCidr; remoteCidr != nil {
- //TODO: this would be better if we had a least specific match lookup, could waste time here, need to benchmark since the algo is different
- _, ok := remoteCidr.Lookup(fp.RemoteIP)
- if !ok {
- f.metrics(incoming).droppedRemoteIP.Inc(1)
- return ErrInvalidRemoteIP
- }
- } else {
- // Simple case: Certificate has one IP and no subnets
- if fp.RemoteIP != h.vpnIp {
- f.metrics(incoming).droppedRemoteIP.Inc(1)
- return ErrInvalidRemoteIP
- }
- }
- // Make sure we are supposed to be handling this local ip address
- //TODO: this would be better if we had a least specific match lookup, could waste time here, need to benchmark since the algo is different
- _, ok := f.localIps.Lookup(fp.LocalIP)
- if !ok {
- f.metrics(incoming).droppedLocalIP.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.NebulaCAPool, 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.NebulaCertificate, caPool *cert.NebulaCAPool) 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:
- 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.NebulaCertificate, caPool *cert.NebulaCAPool) 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.NebulaCertificate, caPool *cert.NebulaCAPool) bool {
- if fc == nil {
- return false
- }
- if fc.Any.match(p, c) {
- return true
- }
- if t, ok := fc.CAShas[c.Details.Issuer]; ok {
- if t.match(p, c) {
- return true
- }
- }
- s, err := caPool.GetCAForCert(c)
- if err != nil {
- return false
- }
- return fc.CANames[s.Details.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.Table[struct{}]),
- }
- }
- 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.NebulaCertificate) 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.Details.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.Details.Name]; ok {
- if flc.match(p, c) {
- return true
- }
- }
- }
- matched := false
- prefix := netip.PrefixFrom(p.RemoteIP, p.RemoteIP.BitLen())
- fr.CIDR.EachLookupPrefix(prefix, func(prefix netip.Prefix, val *firewallLocalCIDR) bool {
- if prefix.Contains(p.RemoteIP) && val.match(p, c) {
- matched = true
- return false
- }
- return true
- })
- return matched
- }
- func (flc *firewallLocalCIDR) addRule(f *Firewall, localIp netip.Prefix) error {
- if !localIp.IsValid() {
- if !f.hasSubnets || f.defaultLocalCIDRAny {
- flc.Any = true
- return nil
- }
- localIp = f.assignedCIDR
- } else if localIp.Bits() == 0 {
- flc.Any = true
- }
- flc.LocalCIDR.Insert(localIp, struct{}{})
- return nil
- }
- func (flc *firewallLocalCIDR) match(p firewall.Packet, c *cert.NebulaCertificate) bool {
- if flc == nil {
- return false
- }
- if flc.Any {
- return true
- }
- _, ok := flc.LocalCIDR.Lookup(p.LocalIP)
- return ok
- }
- 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 interface{}, table string, i int) (rule, error) {
- r := rule{}
- m, ok := p.(map[interface{}]interface{})
- if !ok {
- return r, errors.New("could not parse rule")
- }
- toString := func(k string, m map[interface{}]interface{}) 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"].([]interface{}); 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
- }
|