diff --git a/.github/workflows/golang-test-linux.yml b/.github/workflows/golang-test-linux.yml index 886e232ec..d21ecd784 100644 --- a/.github/workflows/golang-test-linux.yml +++ b/.github/workflows/golang-test-linux.yml @@ -78,6 +78,9 @@ jobs: - name: Generate RouteManager Test bin run: CGO_ENABLED=0 go test -c -o routemanager-testing.bin ./client/internal/routemanager/... + - name: Generate nftables Manager Test bin + run: CGO_ENABLED=0 go test -c -o nftablesmanager-testing.bin ./client/firewall/nftables/... + - name: Generate Engine Test bin run: CGO_ENABLED=0 go test -c -o engine-testing.bin ./client/internal @@ -96,6 +99,9 @@ jobs: - name: Run RouteManager tests in docker run: docker run -t --cap-add=NET_ADMIN --privileged --rm -v $PWD:/ci -w /ci/client/internal/routemanager --entrypoint /busybox/sh gcr.io/distroless/base:debug -c /ci/routemanager-testing.bin -test.timeout 5m -test.parallel 1 + - name: Run nftables Manager tests in docker + run: docker run -t --cap-add=NET_ADMIN --privileged --rm -v $PWD:/ci -w /ci/client/firewall --entrypoint /busybox/sh gcr.io/distroless/base:debug -c /ci/nftablesmanager-testing.bin -test.timeout 5m -test.parallel 1 + - name: Run Engine tests in docker run: docker run -t --cap-add=NET_ADMIN --privileged --rm -v $PWD:/ci -w /ci/client/internal --entrypoint /busybox/sh gcr.io/distroless/base:debug -c /ci/engine-testing.bin -test.timeout 5m -test.parallel 1 diff --git a/client/firewall/firewall.go b/client/firewall/firewall.go index 2e685e15c..f91adb7c1 100644 --- a/client/firewall/firewall.go +++ b/client/firewall/firewall.go @@ -13,22 +13,24 @@ type Rule interface { GetRuleID() string } -// Direction is the direction of the traffic -type Direction int +// RuleDirection is the traffic direction which a rule is applied +type RuleDirection int const ( - // DirectionSrc is the direction of the traffic from the source - DirectionSrc Direction = iota - // DirectionDst is the direction of the traffic from the destination - DirectionDst + // RuleDirectionIN applies to filters that handlers incoming traffic + RuleDirectionIN RuleDirection = iota + // RuleDirectionOUT applies to filters that handlers outgoing traffic + RuleDirectionOUT ) // Action is the action to be taken on a rule type Action int const ( + // ActionUnknown is a unknown action + ActionUnknown Action = iota // ActionAccept is the action to accept a packet - ActionAccept Action = iota + ActionAccept // ActionDrop is the action to drop a packet ActionDrop ) @@ -39,10 +41,15 @@ const ( // Netbird client for ACL and routing functionality type Manager interface { // AddFiltering rule to the firewall + // + // If comment argument is empty firewall manager should set + // rule ID as comment for the rule AddFiltering( ip net.IP, - port *Port, - direction Direction, + proto Protocol, + sPort *Port, + dPort *Port, + direction RuleDirection, action Action, comment string, ) (Rule, error) diff --git a/client/firewall/iptables/manager_linux.go b/client/firewall/iptables/manager_linux.go index e5aafd6d8..c66c27195 100644 --- a/client/firewall/iptables/manager_linux.go +++ b/client/firewall/iptables/manager_linux.go @@ -8,26 +8,43 @@ import ( "github.com/coreos/go-iptables/iptables" "github.com/google/uuid" + log "github.com/sirupsen/logrus" fw "github.com/netbirdio/netbird/client/firewall" ) const ( - // ChainFilterName is the name of the chain that is used for filtering by the Netbird client - ChainFilterName = "NETBIRD-ACL" + // ChainInputFilterName is the name of the chain that is used for filtering incoming packets + ChainInputFilterName = "NETBIRD-ACL-INPUT" + + // ChainOutputFilterName is the name of the chain that is used for filtering outgoing packets + ChainOutputFilterName = "NETBIRD-ACL-OUTPUT" ) +// jumpNetbirdInputDefaultRule always added by manager to the input chain for all trafic from the Netbird interface +var jumpNetbirdInputDefaultRule = []string{"-j", ChainInputFilterName} + +// jumpNetbirdOutputDefaultRule always added by manager to the output chain for all trafic from the Netbird interface +var jumpNetbirdOutputDefaultRule = []string{"-j", ChainOutputFilterName} + +// dropAllDefaultRule in the Netbird chain +var dropAllDefaultRule = []string{"-j", "DROP"} + // Manager of iptables firewall type Manager struct { mutex sync.Mutex ipv4Client *iptables.IPTables ipv6Client *iptables.IPTables + + wgIfaceName string } // Create iptables firewall manager -func Create() (*Manager, error) { - m := &Manager{} +func Create(wgIfaceName string) (*Manager, error) { + m := &Manager{ + wgIfaceName: wgIfaceName, + } // init clients for booth ipv4 and ipv6 ipv4Client, err := iptables.NewWithProtocol(iptables.ProtocolIPv4) @@ -38,118 +55,266 @@ func Create() (*Manager, error) { ipv6Client, err := iptables.NewWithProtocol(iptables.ProtocolIPv6) if err != nil { - return nil, fmt.Errorf("ip6tables is not installed in the system or not supported") + log.Errorf("ip6tables is not installed in the system or not supported: %v", err) + } else { + m.ipv6Client = ipv6Client } - m.ipv6Client = ipv6Client if err := m.Reset(); err != nil { - return nil, fmt.Errorf("failed to reset firewall: %s", err) + return nil, fmt.Errorf("failed to reset firewall: %v", err) } - return m, nil } // AddFiltering rule to the firewall +// +// If comment is empty rule ID is used as comment func (m *Manager) AddFiltering( ip net.IP, - port *fw.Port, - direction fw.Direction, + protocol fw.Protocol, + sPort *fw.Port, + dPort *fw.Port, + direction fw.RuleDirection, action fw.Action, comment string, ) (fw.Rule, error) { m.mutex.Lock() defer m.mutex.Unlock() - client := m.client(ip) - ok, err := client.ChainExists("filter", ChainFilterName) + + client, err := m.client(ip) if err != nil { - return nil, fmt.Errorf("failed to check if chain exists: %s", err) - } - if !ok { - if err := client.NewChain("filter", ChainFilterName); err != nil { - return nil, fmt.Errorf("failed to create chain: %s", err) - } - } - if port == nil || port.Values == nil || (port.IsRange && len(port.Values) != 2) { - return nil, fmt.Errorf("invalid port definition") - } - pv := strconv.Itoa(port.Values[0]) - if port.IsRange { - pv += ":" + strconv.Itoa(port.Values[1]) - } - specs := m.filterRuleSpecs("filter", ChainFilterName, ip, pv, direction, action, comment) - if err := client.AppendUnique("filter", ChainFilterName, specs...); err != nil { return nil, err } - rule := &Rule{ - id: uuid.New().String(), - specs: specs, - v6: ip.To4() == nil, + + var dPortVal, sPortVal string + if dPort != nil && dPort.Values != nil { + // TODO: we support only one port per rule in current implementation of ACLs + dPortVal = strconv.Itoa(dPort.Values[0]) } - return rule, nil + if sPort != nil && sPort.Values != nil { + sPortVal = strconv.Itoa(sPort.Values[0]) + } + + ruleID := uuid.New().String() + if comment == "" { + comment = ruleID + } + + specs := m.filterRuleSpecs( + "filter", + ip, + string(protocol), + sPortVal, + dPortVal, + direction, + action, + comment, + ) + + if direction == fw.RuleDirectionOUT { + ok, err := client.Exists("filter", ChainOutputFilterName, specs...) + if err != nil { + return nil, fmt.Errorf("check is output rule already exists: %w", err) + } + if ok { + return nil, fmt.Errorf("input rule already exists") + } + + if err := client.Insert("filter", ChainOutputFilterName, 1, specs...); err != nil { + return nil, err + } + } else { + ok, err := client.Exists("filter", ChainInputFilterName, specs...) + if err != nil { + return nil, fmt.Errorf("check is input rule already exists: %w", err) + } + if ok { + return nil, fmt.Errorf("input rule already exists") + } + + if err := client.Insert("filter", ChainInputFilterName, 1, specs...); err != nil { + return nil, err + } + } + + return &Rule{ + id: ruleID, + specs: specs, + dst: direction == fw.RuleDirectionOUT, + v6: ip.To4() == nil, + }, nil } // DeleteRule from the firewall by rule definition func (m *Manager) DeleteRule(rule fw.Rule) error { m.mutex.Lock() defer m.mutex.Unlock() + r, ok := rule.(*Rule) if !ok { return fmt.Errorf("invalid rule type") } + client := m.ipv4Client if r.v6 { + if m.ipv6Client == nil { + return fmt.Errorf("ipv6 is not supported") + } client = m.ipv6Client } - return client.Delete("filter", ChainFilterName, r.specs...) + + if r.dst { + return client.Delete("filter", ChainOutputFilterName, r.specs...) + } + return client.Delete("filter", ChainInputFilterName, r.specs...) } // Reset firewall to the default state func (m *Manager) Reset() error { m.mutex.Lock() defer m.mutex.Unlock() - if err := m.reset(m.ipv4Client, "filter", ChainFilterName); err != nil { - return fmt.Errorf("clean ipv4 firewall ACL chain: %w", err) + + if err := m.reset(m.ipv4Client, "filter"); err != nil { + return fmt.Errorf("clean ipv4 firewall ACL input chain: %w", err) } - if err := m.reset(m.ipv6Client, "filter", ChainFilterName); err != nil { - return fmt.Errorf("clean ipv6 firewall ACL chain: %w", err) + if m.ipv6Client != nil { + if err := m.reset(m.ipv6Client, "filter"); err != nil { + return fmt.Errorf("clean ipv6 firewall ACL input chain: %w", err) + } } + return nil } // reset firewall chain, clear it and drop it -func (m *Manager) reset(client *iptables.IPTables, table, chain string) error { - ok, err := client.ChainExists(table, chain) +func (m *Manager) reset(client *iptables.IPTables, table string) error { + ok, err := client.ChainExists(table, ChainInputFilterName) if err != nil { - return fmt.Errorf("failed to check if chain exists: %w", err) + return fmt.Errorf("failed to check if input chain exists: %w", err) } - if !ok { + if ok { + specs := append([]string{"-i", m.wgIfaceName}, jumpNetbirdInputDefaultRule...) + if ok, err := client.Exists("filter", "INPUT", specs...); err != nil { + return err + } else if ok { + if err := client.Delete("filter", "INPUT", specs...); err != nil { + log.WithError(err).Errorf("failed to delete default input rule: %v", err) + } + } + } + + ok, err = client.ChainExists(table, ChainOutputFilterName) + if err != nil { + return fmt.Errorf("failed to check if output chain exists: %w", err) + } + if ok { + specs := append([]string{"-o", m.wgIfaceName}, jumpNetbirdOutputDefaultRule...) + if ok, err := client.Exists("filter", "OUTPUT", specs...); err != nil { + return err + } else if ok { + if err := client.Delete("filter", "OUTPUT", specs...); err != nil { + log.WithError(err).Errorf("failed to delete default output rule: %v", err) + } + } + } + + if err := client.ClearAndDeleteChain(table, ChainInputFilterName); err != nil { + log.Errorf("failed to clear and delete input chain: %v", err) return nil } - if err := client.ClearChain(table, ChainFilterName); err != nil { - return fmt.Errorf("failed to clear chain: %w", err) + + if err := client.ClearAndDeleteChain(table, ChainOutputFilterName); err != nil { + log.Errorf("failed to clear and delete input chain: %v", err) + return nil } - return client.DeleteChain(table, ChainFilterName) + + return nil } // filterRuleSpecs returns the specs of a filtering rule func (m *Manager) filterRuleSpecs( - table string, chain string, ip net.IP, port string, - direction fw.Direction, action fw.Action, comment string, + table string, ip net.IP, protocol string, sPort, dPort string, + direction fw.RuleDirection, action fw.Action, comment string, ) (specs []string) { - if direction == fw.DirectionSrc { + switch direction { + case fw.RuleDirectionIN: specs = append(specs, "-s", ip.String()) + case fw.RuleDirectionOUT: + specs = append(specs, "-d", ip.String()) + } + if protocol != "all" { + specs = append(specs, "-p", protocol) + } + if sPort != "" { + specs = append(specs, "--sport", sPort) + } + if dPort != "" { + specs = append(specs, "--dport", dPort) } - specs = append(specs, "-p", "tcp", "--dport", port) specs = append(specs, "-j", m.actionToStr(action)) return append(specs, "-m", "comment", "--comment", comment) } -// client returns corresponding iptables client for the given ip -func (m *Manager) client(ip net.IP) *iptables.IPTables { +// rawClient returns corresponding iptables client for the given ip +func (m *Manager) rawClient(ip net.IP) (*iptables.IPTables, error) { if ip.To4() != nil { - return m.ipv4Client + return m.ipv4Client, nil } - return m.ipv6Client + if m.ipv6Client == nil { + return nil, fmt.Errorf("ipv6 is not supported") + } + return m.ipv6Client, nil +} + +// client returns client with initialized chain and default rules +func (m *Manager) client(ip net.IP) (*iptables.IPTables, error) { + client, err := m.rawClient(ip) + if err != nil { + return nil, err + } + + ok, err := client.ChainExists("filter", ChainInputFilterName) + if err != nil { + return nil, fmt.Errorf("failed to check if chain exists: %w", err) + } + + if !ok { + if err := client.NewChain("filter", ChainInputFilterName); err != nil { + return nil, fmt.Errorf("failed to create input chain: %w", err) + } + + if err := client.AppendUnique("filter", ChainInputFilterName, dropAllDefaultRule...); err != nil { + return nil, fmt.Errorf("failed to create default drop all in netbird input chain: %w", err) + } + + specs := append([]string{"-i", m.wgIfaceName}, jumpNetbirdInputDefaultRule...) + if err := client.AppendUnique("filter", "INPUT", specs...); err != nil { + return nil, fmt.Errorf("failed to create input chain jump rule: %w", err) + } + + } + + ok, err = client.ChainExists("filter", ChainOutputFilterName) + if err != nil { + return nil, fmt.Errorf("failed to check if chain exists: %w", err) + } + + if !ok { + if err := client.NewChain("filter", ChainOutputFilterName); err != nil { + return nil, fmt.Errorf("failed to create output chain: %w", err) + } + + if err := client.AppendUnique("filter", ChainOutputFilterName, dropAllDefaultRule...); err != nil { + return nil, fmt.Errorf("failed to create default drop all in netbird output chain: %w", err) + } + + specs := append([]string{"-o", m.wgIfaceName}, jumpNetbirdOutputDefaultRule...) + if err := client.AppendUnique("filter", "OUTPUT", specs...); err != nil { + return nil, fmt.Errorf("failed to create output chain jump rule: %w", err) + } + } + + return client, nil } func (m *Manager) actionToStr(action fw.Action) string { diff --git a/client/firewall/iptables/manager_linux_test.go b/client/firewall/iptables/manager_linux_test.go index d576cb803..bc6e14f92 100644 --- a/client/firewall/iptables/manager_linux_test.go +++ b/client/firewall/iptables/manager_linux_test.go @@ -1,105 +1,129 @@ package iptables import ( + "fmt" "net" "testing" + "time" "github.com/coreos/go-iptables/iptables" + "github.com/stretchr/testify/require" + fw "github.com/netbirdio/netbird/client/firewall" ) -func TestNewManager(t *testing.T) { +func TestIptablesManager(t *testing.T) { ipv4Client, err := iptables.NewWithProtocol(iptables.ProtocolIPv4) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) - manager, err := Create() - if err != nil { - t.Fatal(err) - } + // just check on the local interface + manager, err := Create("lo") + require.NoError(t, err) + time.Sleep(time.Second) + + defer func() { + if err := manager.Reset(); err != nil { + t.Errorf("clear the manager state: %v", err) + } + time.Sleep(time.Second) + }() var rule1 fw.Rule t.Run("add first rule", func(t *testing.T) { ip := net.ParseIP("10.20.0.2") - port := &fw.Port{Proto: fw.PortProtocolTCP, Values: []int{8080}} - rule1, err = manager.AddFiltering(ip, port, fw.DirectionDst, fw.ActionAccept, "accept HTTP traffic") - if err != nil { - t.Errorf("failed to add rule: %v", err) - } + port := &fw.Port{Values: []int{8080}} + rule1, err = manager.AddFiltering(ip, "tcp", nil, port, fw.RuleDirectionOUT, fw.ActionAccept, "accept HTTP traffic") + require.NoError(t, err, "failed to add rule") - checkRuleSpecs(t, ipv4Client, true, rule1.(*Rule).specs...) + checkRuleSpecs(t, ipv4Client, ChainOutputFilterName, true, rule1.(*Rule).specs...) }) var rule2 fw.Rule t.Run("add second rule", func(t *testing.T) { ip := net.ParseIP("10.20.0.3") port := &fw.Port{ - Proto: fw.PortProtocolTCP, Values: []int{8043: 8046}, } rule2, err = manager.AddFiltering( - ip, port, fw.DirectionDst, fw.ActionAccept, "accept HTTPS traffic from ports range") - if err != nil { - t.Errorf("failed to add rule: %v", err) - } + ip, "tcp", port, nil, fw.RuleDirectionIN, fw.ActionAccept, "accept HTTPS traffic from ports range") + require.NoError(t, err, "failed to add rule") - checkRuleSpecs(t, ipv4Client, true, rule2.(*Rule).specs...) + checkRuleSpecs(t, ipv4Client, ChainInputFilterName, true, rule2.(*Rule).specs...) }) t.Run("delete first rule", func(t *testing.T) { if err := manager.DeleteRule(rule1); err != nil { - t.Errorf("failed to delete rule: %v", err) + require.NoError(t, err, "failed to delete rule") } - checkRuleSpecs(t, ipv4Client, false, rule1.(*Rule).specs...) + checkRuleSpecs(t, ipv4Client, ChainOutputFilterName, false, rule1.(*Rule).specs...) }) t.Run("delete second rule", func(t *testing.T) { if err := manager.DeleteRule(rule2); err != nil { - t.Errorf("failed to delete rule: %v", err) + require.NoError(t, err, "failed to delete rule") } - checkRuleSpecs(t, ipv4Client, false, rule2.(*Rule).specs...) + checkRuleSpecs(t, ipv4Client, ChainInputFilterName, false, rule2.(*Rule).specs...) }) t.Run("reset check", func(t *testing.T) { // add second rule ip := net.ParseIP("10.20.0.3") - port := &fw.Port{Proto: fw.PortProtocolUDP, Values: []int{5353}} - _, err = manager.AddFiltering(ip, port, fw.DirectionDst, fw.ActionAccept, "accept Fake DNS traffic") - if err != nil { - t.Errorf("failed to add rule: %v", err) - } + port := &fw.Port{Values: []int{5353}} + _, err = manager.AddFiltering(ip, "udp", nil, port, fw.RuleDirectionOUT, fw.ActionAccept, "accept Fake DNS traffic") + require.NoError(t, err, "failed to add rule") - if err := manager.Reset(); err != nil { - t.Errorf("failed to reset: %v", err) - } + err = manager.Reset() + require.NoError(t, err, "failed to reset") - ok, err := ipv4Client.ChainExists("filter", ChainFilterName) - if err != nil { - t.Errorf("failed to drop chain: %v", err) - } + ok, err := ipv4Client.ChainExists("filter", ChainInputFilterName) + require.NoError(t, err, "failed check chain exists") if ok { - t.Errorf("chain '%v' still exists after Reset", ChainFilterName) + require.NoErrorf(t, err, "chain '%v' still exists after Reset", ChainInputFilterName) } }) } -func checkRuleSpecs(t *testing.T, ipv4Client *iptables.IPTables, mustExists bool, rulespec ...string) { - exists, err := ipv4Client.Exists("filter", ChainFilterName, rulespec...) - if err != nil { - t.Errorf("failed to check rule: %v", err) - return - } +func checkRuleSpecs(t *testing.T, ipv4Client *iptables.IPTables, chainName string, mustExists bool, rulespec ...string) { + exists, err := ipv4Client.Exists("filter", chainName, rulespec...) + require.NoError(t, err, "failed to check rule") + require.Falsef(t, !exists && mustExists, "rule '%v' does not exist", rulespec) + require.Falsef(t, exists && !mustExists, "rule '%v' exist", rulespec) +} - if !exists && mustExists { - t.Errorf("rule '%v' does not exist", rulespec) - return - } - if exists && !mustExists { - t.Errorf("rule '%v' exist", rulespec) - return +func TestIptablesCreatePerformance(t *testing.T) { + for _, testMax := range []int{10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000} { + t.Run(fmt.Sprintf("Testing %d rules", testMax), func(t *testing.T) { + // just check on the local interface + manager, err := Create("lo") + require.NoError(t, err) + time.Sleep(time.Second) + + defer func() { + if err := manager.Reset(); err != nil { + t.Errorf("clear the manager state: %v", err) + } + time.Sleep(time.Second) + }() + + _, err = manager.client(net.ParseIP("10.20.0.100")) + require.NoError(t, err) + + ip := net.ParseIP("10.20.0.100") + start := time.Now() + for i := 0; i < testMax; i++ { + port := &fw.Port{Values: []int{1000 + i}} + if i%2 == 0 { + _, err = manager.AddFiltering(ip, "tcp", nil, port, fw.RuleDirectionOUT, fw.ActionAccept, "accept HTTP traffic") + } else { + _, err = manager.AddFiltering(ip, "tcp", nil, port, fw.RuleDirectionIN, fw.ActionAccept, "accept HTTP traffic") + } + + require.NoError(t, err, "failed to add rule") + } + t.Logf("execution avg per rule: %s", time.Since(start)/time.Duration(testMax)) + }) } } diff --git a/client/firewall/iptables/rule.go b/client/firewall/iptables/rule.go index 4b5807a9b..a417891ea 100644 --- a/client/firewall/iptables/rule.go +++ b/client/firewall/iptables/rule.go @@ -4,6 +4,7 @@ package iptables type Rule struct { id string specs []string + dst bool v6 bool } diff --git a/client/firewall/nftables/manager_linux.go b/client/firewall/nftables/manager_linux.go new file mode 100644 index 000000000..4919d8150 --- /dev/null +++ b/client/firewall/nftables/manager_linux.go @@ -0,0 +1,435 @@ +package nftables + +import ( + "bytes" + "encoding/binary" + "fmt" + "net" + "net/netip" + "strings" + "sync" + + "github.com/google/nftables" + "github.com/google/nftables/expr" + "github.com/google/uuid" + "golang.org/x/sys/unix" + + fw "github.com/netbirdio/netbird/client/firewall" +) + +const ( + // FilterTableName is the name of the table that is used for filtering by the Netbird client + FilterTableName = "netbird-acl" + + // FilterInputChainName is the name of the chain that is used for filtering incoming packets + FilterInputChainName = "netbird-acl-input-filter" + + // FilterOutputChainName is the name of the chain that is used for filtering outgoing packets + FilterOutputChainName = "netbird-acl-output-filter" +) + +// Manager of iptables firewall +type Manager struct { + mutex sync.Mutex + + conn *nftables.Conn + tableIPv4 *nftables.Table + tableIPv6 *nftables.Table + + filterInputChainIPv4 *nftables.Chain + filterOutputChainIPv4 *nftables.Chain + + filterInputChainIPv6 *nftables.Chain + filterOutputChainIPv6 *nftables.Chain + + wgIfaceName string +} + +// Create nftables firewall manager +func Create(wgIfaceName string) (*Manager, error) { + m := &Manager{ + conn: &nftables.Conn{}, + wgIfaceName: wgIfaceName, + } + + if err := m.Reset(); err != nil { + return nil, err + } + + return m, nil +} + +// AddFiltering rule to the firewall +// +// If comment argument is empty firewall manager should set +// rule ID as comment for the rule +func (m *Manager) AddFiltering( + ip net.IP, + proto fw.Protocol, + sPort *fw.Port, + dPort *fw.Port, + direction fw.RuleDirection, + action fw.Action, + comment string, +) (fw.Rule, error) { + m.mutex.Lock() + defer m.mutex.Unlock() + + var ( + err error + table *nftables.Table + chain *nftables.Chain + ) + + if direction == fw.RuleDirectionOUT { + table, chain, err = m.chain( + ip, + FilterOutputChainName, + nftables.ChainHookOutput, + nftables.ChainPriorityFilter, + nftables.ChainTypeFilter) + } else { + table, chain, err = m.chain( + ip, + FilterInputChainName, + nftables.ChainHookInput, + nftables.ChainPriorityFilter, + nftables.ChainTypeFilter) + } + if err != nil { + return nil, err + } + + ifaceKey := expr.MetaKeyIIFNAME + if direction == fw.RuleDirectionOUT { + ifaceKey = expr.MetaKeyOIFNAME + } + expressions := []expr.Any{ + &expr.Meta{Key: ifaceKey, Register: 1}, + &expr.Cmp{ + Op: expr.CmpOpEq, + Register: 1, + Data: ifname(m.wgIfaceName), + }, + } + + if proto != "all" { + expressions = append(expressions, &expr.Payload{ + DestRegister: 1, + Base: expr.PayloadBaseNetworkHeader, + Offset: uint32(9), + Len: uint32(1), + }) + + var protoData []byte + switch proto { + case fw.ProtocolTCP: + protoData = []byte{unix.IPPROTO_TCP} + case fw.ProtocolUDP: + protoData = []byte{unix.IPPROTO_UDP} + case fw.ProtocolICMP: + protoData = []byte{unix.IPPROTO_ICMP} + default: + return nil, fmt.Errorf("unsupported protocol: %s", proto) + } + expressions = append(expressions, &expr.Cmp{ + Register: 1, + Op: expr.CmpOpEq, + Data: protoData, + }) + } + + // source address position + var adrLen, adrOffset uint32 + if ip.To4() == nil { + adrLen = 16 + adrOffset = 8 + } else { + adrLen = 4 + adrOffset = 12 + } + + // change to destination address position if need + if direction == fw.RuleDirectionOUT { + adrOffset += adrLen + } + + ipToAdd, _ := netip.AddrFromSlice(ip) + add := ipToAdd.Unmap() + + expressions = append(expressions, + &expr.Payload{ + DestRegister: 1, + Base: expr.PayloadBaseNetworkHeader, + Offset: adrOffset, + Len: adrLen, + }, + &expr.Cmp{ + Op: expr.CmpOpEq, + Register: 1, + Data: add.AsSlice(), + }, + ) + + if sPort != nil && len(sPort.Values) != 0 { + expressions = append(expressions, + &expr.Payload{ + DestRegister: 1, + Base: expr.PayloadBaseTransportHeader, + Offset: 0, + Len: 2, + }, + &expr.Cmp{ + Op: expr.CmpOpEq, + Register: 1, + Data: encodePort(*sPort), + }, + ) + } + + if dPort != nil && len(dPort.Values) != 0 { + expressions = append(expressions, + &expr.Payload{ + DestRegister: 1, + Base: expr.PayloadBaseTransportHeader, + Offset: 2, + Len: 2, + }, + &expr.Cmp{ + Op: expr.CmpOpEq, + Register: 1, + Data: encodePort(*dPort), + }, + ) + } + + if action == fw.ActionAccept { + expressions = append(expressions, &expr.Verdict{Kind: expr.VerdictAccept}) + } else { + expressions = append(expressions, &expr.Verdict{Kind: expr.VerdictDrop}) + } + + id := uuid.New().String() + userData := []byte(strings.Join([]string{id, comment}, " ")) + + _ = m.conn.InsertRule(&nftables.Rule{ + Table: table, + Chain: chain, + Position: 0, + Exprs: expressions, + UserData: userData, + }) + + if err := m.conn.Flush(); err != nil { + return nil, err + } + + list, err := m.conn.GetRules(table, chain) + if err != nil { + return nil, err + } + + // Add the rule to the chain + rule := &Rule{id: id} + for _, r := range list { + if bytes.Equal(r.UserData, userData) { + rule.Rule = r + break + } + } + if rule.Rule == nil { + return nil, fmt.Errorf("rule not found") + } + + return rule, nil +} + +// chain returns the chain for the given IP address with specific settings +func (m *Manager) chain( + ip net.IP, + name string, + hook nftables.ChainHook, + priority nftables.ChainPriority, + cType nftables.ChainType, +) (*nftables.Table, *nftables.Chain, error) { + var err error + + getChain := func(c *nftables.Chain, tf nftables.TableFamily) (*nftables.Chain, error) { + if c != nil { + return c, nil + } + return m.createChainIfNotExists(tf, name, hook, priority, cType) + } + + if ip.To4() != nil { + if name == FilterInputChainName { + m.filterInputChainIPv4, err = getChain(m.filterInputChainIPv4, nftables.TableFamilyIPv4) + return m.tableIPv4, m.filterInputChainIPv4, err + } + m.filterOutputChainIPv4, err = getChain(m.filterOutputChainIPv4, nftables.TableFamilyIPv4) + return m.tableIPv4, m.filterOutputChainIPv4, err + } + if name == FilterInputChainName { + m.filterInputChainIPv6, err = getChain(m.filterInputChainIPv6, nftables.TableFamilyIPv6) + return m.tableIPv4, m.filterInputChainIPv6, err + } + m.filterOutputChainIPv6, err = getChain(m.filterOutputChainIPv6, nftables.TableFamilyIPv6) + return m.tableIPv4, m.filterOutputChainIPv6, err +} + +// table returns the table for the given family of the IP address +func (m *Manager) table(family nftables.TableFamily) (*nftables.Table, error) { + if family == nftables.TableFamilyIPv4 { + if m.tableIPv4 != nil { + return m.tableIPv4, nil + } + + table, err := m.createTableIfNotExists(nftables.TableFamilyIPv4) + if err != nil { + return nil, err + } + m.tableIPv4 = table + return m.tableIPv4, nil + } + + if m.tableIPv6 != nil { + return m.tableIPv6, nil + } + + table, err := m.createTableIfNotExists(nftables.TableFamilyIPv6) + if err != nil { + return nil, err + } + m.tableIPv6 = table + return m.tableIPv6, nil +} + +func (m *Manager) createTableIfNotExists(family nftables.TableFamily) (*nftables.Table, error) { + tables, err := m.conn.ListTablesOfFamily(family) + if err != nil { + return nil, fmt.Errorf("list of tables: %w", err) + } + + for _, t := range tables { + if t.Name == FilterTableName { + return t, nil + } + } + + return m.conn.AddTable(&nftables.Table{Name: FilterTableName, Family: nftables.TableFamilyIPv4}), nil +} + +func (m *Manager) createChainIfNotExists( + family nftables.TableFamily, + name string, + hooknum nftables.ChainHook, + priority nftables.ChainPriority, + chainType nftables.ChainType, +) (*nftables.Chain, error) { + table, err := m.table(family) + if err != nil { + return nil, err + } + + chains, err := m.conn.ListChainsOfTableFamily(family) + if err != nil { + return nil, fmt.Errorf("list of chains: %w", err) + } + + for _, c := range chains { + if c.Name == name && c.Table.Name == table.Name { + return c, nil + } + } + + polAccept := nftables.ChainPolicyAccept + chain := &nftables.Chain{ + Name: name, + Table: table, + Hooknum: hooknum, + Priority: priority, + Type: chainType, + Policy: &polAccept, + } + + chain = m.conn.AddChain(chain) + + ifaceKey := expr.MetaKeyIIFNAME + if name == FilterOutputChainName { + ifaceKey = expr.MetaKeyOIFNAME + } + expressions := []expr.Any{ + &expr.Meta{Key: ifaceKey, Register: 1}, + &expr.Cmp{ + Op: expr.CmpOpEq, + Register: 1, + Data: ifname(m.wgIfaceName), + }, + &expr.Verdict{Kind: expr.VerdictDrop}, + } + _ = m.conn.AddRule(&nftables.Rule{ + Table: table, + Chain: chain, + Exprs: expressions, + }) + + if err := m.conn.Flush(); err != nil { + return nil, err + } + + return chain, nil +} + +// DeleteRule from the firewall by rule definition +func (m *Manager) DeleteRule(rule fw.Rule) error { + nativeRule, ok := rule.(*Rule) + if !ok { + return fmt.Errorf("invalid rule type") + } + + if err := m.conn.DelRule(nativeRule.Rule); err != nil { + return err + } + + return m.conn.Flush() +} + +// Reset firewall to the default state +func (m *Manager) Reset() error { + m.mutex.Lock() + defer m.mutex.Unlock() + + chains, err := m.conn.ListChains() + if err != nil { + return fmt.Errorf("list of chains: %w", err) + } + for _, c := range chains { + if c.Name == FilterInputChainName || c.Name == FilterOutputChainName { + m.conn.DelChain(c) + } + } + + tables, err := m.conn.ListTables() + if err != nil { + return fmt.Errorf("list of tables: %w", err) + } + for _, t := range tables { + if t.Name == FilterTableName { + m.conn.DelTable(t) + } + } + + return m.conn.Flush() +} + +func encodePort(port fw.Port) []byte { + bs := make([]byte, 2) + binary.BigEndian.PutUint16(bs, uint16(port.Values[0])) + return bs +} + +func ifname(n string) []byte { + b := make([]byte, 16) + copy(b, []byte(n+"\x00")) + return b +} diff --git a/client/firewall/nftables/manager_linux_test.go b/client/firewall/nftables/manager_linux_test.go new file mode 100644 index 000000000..558ff005f --- /dev/null +++ b/client/firewall/nftables/manager_linux_test.go @@ -0,0 +1,137 @@ +package nftables + +import ( + "fmt" + "net" + "net/netip" + "testing" + "time" + + "github.com/google/nftables" + "github.com/google/nftables/expr" + "github.com/stretchr/testify/require" + "golang.org/x/sys/unix" + + fw "github.com/netbirdio/netbird/client/firewall" +) + +func TestNftablesManager(t *testing.T) { + // just check on the local interface + manager, err := Create("lo") + require.NoError(t, err) + time.Sleep(time.Second) + + defer func() { + err = manager.Reset() + require.NoError(t, err, "failed to reset") + time.Sleep(time.Second) + }() + + ip := net.ParseIP("100.96.0.1") + + testClient := &nftables.Conn{} + + rule, err := manager.AddFiltering( + ip, + fw.ProtocolTCP, + nil, + &fw.Port{Values: []int{53}}, + fw.RuleDirectionIN, + fw.ActionDrop, + "", + ) + require.NoError(t, err, "failed to add rule") + + rules, err := testClient.GetRules(manager.tableIPv4, manager.filterInputChainIPv4) + require.NoError(t, err, "failed to get rules") + // 1 regular rule and other "drop all rule" for the interface + require.Len(t, rules, 2, "expected 1 rule") + + ipToAdd, _ := netip.AddrFromSlice(ip) + add := ipToAdd.Unmap() + expectedExprs := []expr.Any{ + &expr.Meta{Key: expr.MetaKeyIIFNAME, Register: 1}, + &expr.Cmp{ + Op: expr.CmpOpEq, + Register: 1, + Data: ifname("lo"), + }, + &expr.Payload{ + DestRegister: 1, + Base: expr.PayloadBaseNetworkHeader, + Offset: uint32(9), + Len: uint32(1), + }, + &expr.Cmp{ + Register: 1, + Op: expr.CmpOpEq, + Data: []byte{unix.IPPROTO_TCP}, + }, + &expr.Payload{ + DestRegister: 1, + Base: expr.PayloadBaseNetworkHeader, + Offset: 12, + Len: 4, + }, + &expr.Cmp{ + Op: expr.CmpOpEq, + Register: 1, + Data: add.AsSlice(), + }, + &expr.Payload{ + DestRegister: 1, + Base: expr.PayloadBaseTransportHeader, + Offset: 2, + Len: 2, + }, + &expr.Cmp{ + Op: expr.CmpOpEq, + Register: 1, + Data: []byte{0, 53}, + }, + &expr.Verdict{Kind: expr.VerdictDrop}, + } + require.ElementsMatch(t, rules[0].Exprs, expectedExprs, "expected the same expressions") + + err = manager.DeleteRule(rule) + require.NoError(t, err, "failed to delete rule") + + rules, err = testClient.GetRules(manager.tableIPv4, manager.filterInputChainIPv4) + require.NoError(t, err, "failed to get rules") + require.Len(t, rules, 1, "expected 1 rules after deleteion") + + err = manager.Reset() + require.NoError(t, err, "failed to reset") +} + +func TestNFtablesCreatePerformance(t *testing.T) { + for _, testMax := range []int{10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000} { + t.Run(fmt.Sprintf("Testing %d rules", testMax), func(t *testing.T) { + // just check on the local interface + manager, err := Create("lo") + require.NoError(t, err) + time.Sleep(time.Second) + + defer func() { + if err := manager.Reset(); err != nil { + t.Errorf("clear the manager state: %v", err) + } + time.Sleep(time.Second) + }() + + ip := net.ParseIP("10.20.0.100") + start := time.Now() + for i := 0; i < testMax; i++ { + port := &fw.Port{Values: []int{1000 + i}} + if i%2 == 0 { + _, err = manager.AddFiltering(ip, "tcp", nil, port, fw.RuleDirectionOUT, fw.ActionAccept, "accept HTTP traffic") + } else { + _, err = manager.AddFiltering(ip, "tcp", nil, port, fw.RuleDirectionIN, fw.ActionAccept, "accept HTTP traffic") + } + + require.NoError(t, err, "failed to add rule") + } + t.Logf("execution avg per rule: %s", time.Since(start)/time.Duration(testMax)) + }) + } +} diff --git a/client/firewall/nftables/rule_linux.go b/client/firewall/nftables/rule_linux.go new file mode 100644 index 000000000..7fe0dcb5e --- /dev/null +++ b/client/firewall/nftables/rule_linux.go @@ -0,0 +1,16 @@ +package nftables + +import ( + "github.com/google/nftables" +) + +// Rule to handle management of rules +type Rule struct { + *nftables.Rule + id string +} + +// GetRuleID returns the rule id +func (r *Rule) GetRuleID() string { + return r.id +} diff --git a/client/firewall/port.go b/client/firewall/port.go index fc09c51f3..65a16a16e 100644 --- a/client/firewall/port.go +++ b/client/firewall/port.go @@ -1,14 +1,23 @@ package firewall -// PortProtocol is the protocol of the port -type PortProtocol string +// Protocol is the protocol of the port +type Protocol string const ( - // PortProtocolTCP is the TCP protocol - PortProtocolTCP PortProtocol = "tcp" + // ProtocolTCP is the TCP protocol + ProtocolTCP Protocol = "tcp" - // PortProtocolUDP is the UDP protocol - PortProtocolUDP PortProtocol = "udp" + // ProtocolUDP is the UDP protocol + ProtocolUDP Protocol = "udp" + + // ProtocolICMP is the ICMP protocol + ProtocolICMP Protocol = "icmp" + + // ProtocolALL cover all supported protocols + ProtocolALL Protocol = "all" + + // ProtocolUnknown unknown protocol + ProtocolUnknown Protocol = "unknown" ) // Port of the address for firewall rule @@ -18,7 +27,4 @@ type Port struct { // Values contains one value for single port, multiple values for the list of ports, or two values for the range of ports Values []int - - // Proto is the protocol of the port - Proto PortProtocol } diff --git a/client/firewall/uspfilter/rule.go b/client/firewall/uspfilter/rule.go new file mode 100644 index 000000000..437d0d8ff --- /dev/null +++ b/client/firewall/uspfilter/rule.go @@ -0,0 +1,27 @@ +package uspfilter + +import ( + "net" + + "github.com/google/gopacket" + + fw "github.com/netbirdio/netbird/client/firewall" +) + +// Rule to handle management of rules +type Rule struct { + id string + ip net.IP + ipLayer gopacket.LayerType + protoLayer gopacket.LayerType + direction fw.RuleDirection + sPort uint16 + dPort uint16 + drop bool + comment string +} + +// GetRuleID returns the rule id +func (r *Rule) GetRuleID() string { + return r.id +} diff --git a/client/firewall/uspfilter/uspfilter.go b/client/firewall/uspfilter/uspfilter.go new file mode 100644 index 000000000..216c6577b --- /dev/null +++ b/client/firewall/uspfilter/uspfilter.go @@ -0,0 +1,291 @@ +package uspfilter + +import ( + "fmt" + "net" + "sync" + + "github.com/google/gopacket" + "github.com/google/gopacket/layers" + "github.com/google/uuid" + log "github.com/sirupsen/logrus" + + fw "github.com/netbirdio/netbird/client/firewall" + "github.com/netbirdio/netbird/iface" +) + +const layerTypeAll = 0 + +// IFaceMapper defines subset methods of interface required for manager +type IFaceMapper interface { + SetFiltering(iface.PacketFilter) error +} + +// Manager userspace firewall manager +type Manager struct { + outgoingRules []Rule + incomingRules []Rule + rulesIndex map[string]int + wgNetwork *net.IPNet + decoders sync.Pool + + mutex sync.RWMutex +} + +// decoder for packages +type decoder struct { + eth layers.Ethernet + ip4 layers.IPv4 + ip6 layers.IPv6 + tcp layers.TCP + udp layers.UDP + icmp4 layers.ICMPv4 + icmp6 layers.ICMPv6 + decoded []gopacket.LayerType + parser *gopacket.DecodingLayerParser +} + +// Create userspace firewall manager constructor +func Create(iface IFaceMapper) (*Manager, error) { + m := &Manager{ + rulesIndex: make(map[string]int), + decoders: sync.Pool{ + New: func() any { + d := &decoder{ + decoded: []gopacket.LayerType{}, + } + d.parser = gopacket.NewDecodingLayerParser( + layers.LayerTypeIPv4, + &d.eth, &d.ip4, &d.ip6, &d.icmp4, &d.icmp6, &d.tcp, &d.udp, + ) + d.parser.IgnoreUnsupported = true + return d + }, + }, + } + + if err := iface.SetFiltering(m); err != nil { + return nil, err + } + return m, nil +} + +// AddFiltering rule to the firewall +// +// If comment argument is empty firewall manager should set +// rule ID as comment for the rule +func (m *Manager) AddFiltering( + ip net.IP, + proto fw.Protocol, + sPort *fw.Port, + dPort *fw.Port, + direction fw.RuleDirection, + action fw.Action, + comment string, +) (fw.Rule, error) { + r := Rule{ + id: uuid.New().String(), + ip: ip, + ipLayer: layers.LayerTypeIPv6, + direction: direction, + drop: action == fw.ActionDrop, + comment: comment, + } + if ipNormalized := ip.To4(); ipNormalized != nil { + r.ipLayer = layers.LayerTypeIPv4 + r.ip = ipNormalized + } + + if sPort != nil && len(sPort.Values) == 1 { + r.sPort = uint16(sPort.Values[0]) + } + + if dPort != nil && len(dPort.Values) == 1 { + r.dPort = uint16(dPort.Values[0]) + } + + switch proto { + case fw.ProtocolTCP: + r.protoLayer = layers.LayerTypeTCP + case fw.ProtocolUDP: + r.protoLayer = layers.LayerTypeUDP + case fw.ProtocolICMP: + r.protoLayer = layers.LayerTypeICMPv4 + if r.ipLayer == layers.LayerTypeIPv6 { + r.protoLayer = layers.LayerTypeICMPv6 + } + case fw.ProtocolALL: + r.protoLayer = layerTypeAll + } + + m.mutex.Lock() + var p int + if direction == fw.RuleDirectionIN { + m.incomingRules = append(m.incomingRules, r) + p = len(m.incomingRules) - 1 + } else { + m.outgoingRules = append(m.outgoingRules, r) + p = len(m.outgoingRules) - 1 + } + m.rulesIndex[r.id] = p + m.mutex.Unlock() + + return &r, nil +} + +// DeleteRule from the firewall by rule definition +func (m *Manager) DeleteRule(rule fw.Rule) error { + m.mutex.Lock() + defer m.mutex.Unlock() + + r, ok := rule.(*Rule) + if !ok { + return fmt.Errorf("delete rule: invalid rule type: %T", rule) + } + + p, ok := m.rulesIndex[r.id] + if !ok { + return fmt.Errorf("delete rule: no rule with such id: %v", r.id) + } + delete(m.rulesIndex, r.id) + + var toUpdate []Rule + if r.direction == fw.RuleDirectionIN { + m.incomingRules = append(m.incomingRules[:p], m.incomingRules[p+1:]...) + toUpdate = m.incomingRules + } else { + m.outgoingRules = append(m.outgoingRules[:p], m.outgoingRules[p+1:]...) + toUpdate = m.outgoingRules + } + + for i := 0; i < len(toUpdate); i++ { + m.rulesIndex[toUpdate[i].id] = i + } + return nil +} + +// Reset firewall to the default state +func (m *Manager) Reset() error { + m.mutex.Lock() + defer m.mutex.Unlock() + + m.outgoingRules = m.outgoingRules[:0] + m.incomingRules = m.incomingRules[:0] + m.rulesIndex = make(map[string]int) + + return nil +} + +// DropOutgoing filter outgoing packets +func (m *Manager) DropOutgoing(packetData []byte) bool { + return m.dropFilter(packetData, m.outgoingRules, false) +} + +// DropIncoming filter incoming packets +func (m *Manager) DropIncoming(packetData []byte) bool { + return m.dropFilter(packetData, m.incomingRules, true) +} + +// dropFilter imlements same logic for booth direction of the traffic +func (m *Manager) dropFilter(packetData []byte, rules []Rule, isIncomingPacket bool) bool { + m.mutex.RLock() + defer m.mutex.RUnlock() + + d := m.decoders.Get().(*decoder) + defer m.decoders.Put(d) + + if err := d.parser.DecodeLayers(packetData, &d.decoded); err != nil { + log.Tracef("couldn't decode layer, err: %s", err) + return true + } + + if len(d.decoded) < 2 { + log.Tracef("not enough levels in network packet") + return true + } + + ipLayer := d.decoded[0] + + switch ipLayer { + case layers.LayerTypeIPv4: + if !m.wgNetwork.Contains(d.ip4.SrcIP) || !m.wgNetwork.Contains(d.ip4.DstIP) { + return false + } + case layers.LayerTypeIPv6: + if !m.wgNetwork.Contains(d.ip6.SrcIP) || !m.wgNetwork.Contains(d.ip6.DstIP) { + return false + } + default: + log.Errorf("unknown layer: %v", d.decoded[0]) + return true + } + payloadLayer := d.decoded[1] + + // check if IP address match by IP + for _, rule := range rules { + switch ipLayer { + case layers.LayerTypeIPv4: + if isIncomingPacket { + if !d.ip4.SrcIP.Equal(rule.ip) { + continue + } + } else { + if !d.ip4.DstIP.Equal(rule.ip) { + continue + } + } + case layers.LayerTypeIPv6: + if isIncomingPacket { + if !d.ip6.SrcIP.Equal(rule.ip) { + continue + } + } else { + if !d.ip6.DstIP.Equal(rule.ip) { + continue + } + } + } + + if rule.protoLayer == layerTypeAll { + return rule.drop + } + + if payloadLayer != rule.protoLayer { + continue + } + + switch payloadLayer { + case layers.LayerTypeTCP: + if rule.sPort == 0 && rule.dPort == 0 { + return rule.drop + } + if rule.sPort != 0 && rule.sPort == uint16(d.tcp.SrcPort) { + return rule.drop + } + if rule.dPort != 0 && rule.dPort == uint16(d.tcp.DstPort) { + return rule.drop + } + case layers.LayerTypeUDP: + if rule.sPort == 0 && rule.dPort == 0 { + return rule.drop + } + if rule.sPort != 0 && rule.sPort == uint16(d.udp.SrcPort) { + return rule.drop + } + if rule.dPort != 0 && rule.dPort == uint16(d.udp.DstPort) { + return rule.drop + } + return rule.drop + case layers.LayerTypeICMPv4, layers.LayerTypeICMPv6: + return rule.drop + } + } + + // default policy is DROP ALL + return true +} + +// SetNetwork of the wireguard interface to which filtering applied +func (m *Manager) SetNetwork(network *net.IPNet) { + m.wgNetwork = network +} diff --git a/client/firewall/uspfilter/uspfilter_test.go b/client/firewall/uspfilter/uspfilter_test.go new file mode 100644 index 000000000..1e9bda68c --- /dev/null +++ b/client/firewall/uspfilter/uspfilter_test.go @@ -0,0 +1,207 @@ +package uspfilter + +import ( + "fmt" + "net" + "testing" + "time" + + "github.com/stretchr/testify/require" + + fw "github.com/netbirdio/netbird/client/firewall" + "github.com/netbirdio/netbird/iface" +) + +type IFaceMock struct { + SetFilteringFunc func(iface.PacketFilter) error +} + +func (i *IFaceMock) SetFiltering(iface iface.PacketFilter) error { + if i.SetFilteringFunc == nil { + return fmt.Errorf("not implemented") + } + return i.SetFilteringFunc(iface) +} + +func TestManagerCreate(t *testing.T) { + ifaceMock := &IFaceMock{ + SetFilteringFunc: func(iface.PacketFilter) error { return nil }, + } + + m, err := Create(ifaceMock) + if err != nil { + t.Errorf("failed to create Manager: %v", err) + return + } + + if m == nil { + t.Error("Manager is nil") + } +} + +func TestManagerAddFiltering(t *testing.T) { + isSetFilteringCalled := false + ifaceMock := &IFaceMock{ + SetFilteringFunc: func(iface.PacketFilter) error { + isSetFilteringCalled = true + return nil + }, + } + + m, err := Create(ifaceMock) + if err != nil { + t.Errorf("failed to create Manager: %v", err) + return + } + + ip := net.ParseIP("192.168.1.1") + proto := fw.ProtocolTCP + port := &fw.Port{Values: []int{80}} + direction := fw.RuleDirectionOUT + action := fw.ActionDrop + comment := "Test rule" + + rule, err := m.AddFiltering(ip, proto, nil, port, direction, action, comment) + if err != nil { + t.Errorf("failed to add filtering: %v", err) + return + } + + if rule == nil { + t.Error("Rule is nil") + return + } + + if !isSetFilteringCalled { + t.Error("SetFiltering was not called") + return + } +} + +func TestManagerDeleteRule(t *testing.T) { + ifaceMock := &IFaceMock{ + SetFilteringFunc: func(iface.PacketFilter) error { return nil }, + } + + m, err := Create(ifaceMock) + if err != nil { + t.Errorf("failed to create Manager: %v", err) + return + } + + ip := net.ParseIP("192.168.1.1") + proto := fw.ProtocolTCP + port := &fw.Port{Values: []int{80}} + direction := fw.RuleDirectionOUT + action := fw.ActionDrop + comment := "Test rule" + + rule, err := m.AddFiltering(ip, proto, nil, port, direction, action, comment) + if err != nil { + t.Errorf("failed to add filtering: %v", err) + return + } + + ip = net.ParseIP("192.168.1.1") + proto = fw.ProtocolTCP + port = &fw.Port{Values: []int{80}} + direction = fw.RuleDirectionIN + action = fw.ActionDrop + comment = "Test rule 2" + + rule2, err := m.AddFiltering(ip, proto, nil, port, direction, action, comment) + if err != nil { + t.Errorf("failed to add filtering: %v", err) + return + } + + err = m.DeleteRule(rule) + if err != nil { + t.Errorf("failed to delete rule: %v", err) + return + } + + if idx, ok := m.rulesIndex[rule2.GetRuleID()]; !ok || len(m.incomingRules) != 1 || idx != 0 { + t.Errorf("rule2 is not in the rulesIndex") + } + + err = m.DeleteRule(rule2) + if err != nil { + t.Errorf("failed to delete rule: %v", err) + return + } + + if len(m.rulesIndex) != 0 || len(m.incomingRules) != 0 { + t.Errorf("rule1 still in the rulesIndex") + } +} + +func TestManagerReset(t *testing.T) { + ifaceMock := &IFaceMock{ + SetFilteringFunc: func(iface.PacketFilter) error { return nil }, + } + + m, err := Create(ifaceMock) + if err != nil { + t.Errorf("failed to create Manager: %v", err) + return + } + + ip := net.ParseIP("192.168.1.1") + proto := fw.ProtocolTCP + port := &fw.Port{Values: []int{80}} + direction := fw.RuleDirectionOUT + action := fw.ActionDrop + comment := "Test rule" + + _, err = m.AddFiltering(ip, proto, nil, port, direction, action, comment) + if err != nil { + t.Errorf("failed to add filtering: %v", err) + return + } + + err = m.Reset() + if err != nil { + t.Errorf("failed to reset Manager: %v", err) + return + } + + if len(m.rulesIndex) != 0 || len(m.outgoingRules) != 0 || len(m.incomingRules) != 0 { + t.Errorf("rules is not empty") + } +} + +func TestUSPFilterCreatePerformance(t *testing.T) { + for _, testMax := range []int{10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000} { + t.Run(fmt.Sprintf("Testing %d rules", testMax), func(t *testing.T) { + // just check on the local interface + ifaceMock := &IFaceMock{ + SetFilteringFunc: func(iface.PacketFilter) error { return nil }, + } + manager, err := Create(ifaceMock) + require.NoError(t, err) + time.Sleep(time.Second) + + defer func() { + if err := manager.Reset(); err != nil { + t.Errorf("clear the manager state: %v", err) + } + time.Sleep(time.Second) + }() + + ip := net.ParseIP("10.20.0.100") + start := time.Now() + for i := 0; i < testMax; i++ { + port := &fw.Port{Values: []int{1000 + i}} + if i%2 == 0 { + _, err = manager.AddFiltering(ip, "tcp", nil, port, fw.RuleDirectionOUT, fw.ActionAccept, "accept HTTP traffic") + } else { + _, err = manager.AddFiltering(ip, "tcp", nil, port, fw.RuleDirectionIN, fw.ActionAccept, "accept HTTP traffic") + } + + require.NoError(t, err, "failed to add rule") + } + t.Logf("execution avg per rule: %s", time.Since(start)/time.Duration(testMax)) + }) + } +} diff --git a/client/internal/acl/manager.go b/client/internal/acl/manager.go new file mode 100644 index 000000000..5d5b21269 --- /dev/null +++ b/client/internal/acl/manager.go @@ -0,0 +1,209 @@ +package acl + +import ( + "fmt" + "net" + "strconv" + "sync" + + log "github.com/sirupsen/logrus" + + "github.com/netbirdio/netbird/client/firewall" + "github.com/netbirdio/netbird/iface" + mgmProto "github.com/netbirdio/netbird/management/proto" +) + +// iFaceMapper defines subset methods of interface required for manager +type iFaceMapper interface { + Name() string + IsUserspaceBind() bool + SetFiltering(iface.PacketFilter) error +} + +// Manager is a ACL rules manager +type Manager interface { + ApplyFiltering(rules []*mgmProto.FirewallRule) + Stop() +} + +// DefaultManager uses firewall manager to handle +type DefaultManager struct { + manager firewall.Manager + rulesPairs map[string][]firewall.Rule + mutex sync.Mutex +} + +// ApplyFiltering firewall rules to the local firewall manager processed by ACL policy. +func (d *DefaultManager) ApplyFiltering(rules []*mgmProto.FirewallRule) { + d.mutex.Lock() + defer d.mutex.Unlock() + + if d.manager == nil { + log.Debug("firewall manager is not supported, skipping firewall rules") + return + } + + var ( + applyFailed bool + newRulePairs = make(map[string][]firewall.Rule) + ) + for _, r := range rules { + rules, err := d.protoRuleToFirewallRule(r) + if err != nil { + log.Errorf("failed to apply firewall rule: %+v, %v", r, err) + applyFailed = true + break + } + newRulePairs[rules[0].GetRuleID()] = rules + } + if applyFailed { + log.Error("failed to apply firewall rules, rollback ACL to previous state") + for _, rules := range newRulePairs { + for _, rule := range rules { + if err := d.manager.DeleteRule(rule); err != nil { + log.Errorf("failed to delete new firewall rule (id: %v) during rollback: %v", rule.GetRuleID(), err) + continue + } + } + } + return + } + + for pairID, rules := range d.rulesPairs { + if _, ok := newRulePairs[pairID]; !ok { + for _, rule := range rules { + if err := d.manager.DeleteRule(rule); err != nil { + log.Errorf("failed to delete firewall rule: %v", err) + continue + } + } + delete(d.rulesPairs, pairID) + } + } + d.rulesPairs = newRulePairs +} + +// Stop ACL controller and clear firewall state +func (d *DefaultManager) Stop() { + d.mutex.Lock() + defer d.mutex.Unlock() + + if err := d.manager.Reset(); err != nil { + log.WithError(err).Error("reset firewall state") + } +} + +func (d *DefaultManager) protoRuleToFirewallRule(r *mgmProto.FirewallRule) ([]firewall.Rule, error) { + ip := net.ParseIP(r.PeerIP) + if ip == nil { + return nil, fmt.Errorf("invalid IP address, skipping firewall rule") + } + + protocol := convertToFirewallProtocol(r.Protocol) + if protocol == firewall.ProtocolUnknown { + return nil, fmt.Errorf("invalid protocol type: %d, skipping firewall rule", r.Protocol) + } + + action := convertFirewallAction(r.Action) + if action == firewall.ActionUnknown { + return nil, fmt.Errorf("invalid action type: %d, skipping firewall rule", r.Action) + } + + var port *firewall.Port + if r.Port != "" { + value, err := strconv.Atoi(r.Port) + if err != nil { + return nil, fmt.Errorf("invalid port, skipping firewall rule") + } + port = &firewall.Port{ + Values: []int{value}, + } + } + + var rules []firewall.Rule + var err error + switch r.Direction { + case mgmProto.FirewallRule_IN: + rules, err = d.addInRules(ip, protocol, port, action, "") + case mgmProto.FirewallRule_OUT: + rules, err = d.addOutRules(ip, protocol, port, action, "") + default: + return nil, fmt.Errorf("invalid direction, skipping firewall rule") + } + + if err != nil { + return nil, err + } + + d.rulesPairs[rules[0].GetRuleID()] = rules + return rules, nil +} + +func (d *DefaultManager) addInRules(ip net.IP, protocol firewall.Protocol, port *firewall.Port, action firewall.Action, comment string) ([]firewall.Rule, error) { + var rules []firewall.Rule + rule, err := d.manager.AddFiltering(ip, protocol, nil, port, firewall.RuleDirectionIN, action, comment) + if err != nil { + return nil, fmt.Errorf("failed to add firewall rule: %v", err) + } + rules = append(rules, rule) + + if shouldSkipInvertedRule(protocol) { + return rules, nil + } + + rule, err = d.manager.AddFiltering(ip, protocol, port, nil, firewall.RuleDirectionOUT, action, comment) + if err != nil { + return nil, fmt.Errorf("failed to add firewall rule: %v", err) + } + + return append(rules, rule), nil +} + +func (d *DefaultManager) addOutRules(ip net.IP, protocol firewall.Protocol, port *firewall.Port, action firewall.Action, comment string) ([]firewall.Rule, error) { + var rules []firewall.Rule + rule, err := d.manager.AddFiltering(ip, protocol, nil, port, firewall.RuleDirectionOUT, action, comment) + if err != nil { + return nil, fmt.Errorf("failed to add firewall rule: %v", err) + } + rules = append(rules, rule) + + if shouldSkipInvertedRule(protocol) { + return rules, nil + } + + rule, err = d.manager.AddFiltering(ip, protocol, port, nil, firewall.RuleDirectionIN, action, comment) + if err != nil { + return nil, fmt.Errorf("failed to add firewall rule: %v", err) + } + + return append(rules, rule), nil +} +func convertToFirewallProtocol(protocol mgmProto.FirewallRuleProtocol) firewall.Protocol { + switch protocol { + case mgmProto.FirewallRule_TCP: + return firewall.ProtocolTCP + case mgmProto.FirewallRule_UDP: + return firewall.ProtocolUDP + case mgmProto.FirewallRule_ICMP: + return firewall.ProtocolICMP + case mgmProto.FirewallRule_ALL: + return firewall.ProtocolALL + default: + return firewall.ProtocolUnknown + } +} + +func shouldSkipInvertedRule(protocol firewall.Protocol) bool { + return protocol == firewall.ProtocolALL || protocol == firewall.ProtocolICMP +} + +func convertFirewallAction(action mgmProto.FirewallRuleAction) firewall.Action { + switch action { + case mgmProto.FirewallRule_ACCEPT: + return firewall.ActionAccept + case mgmProto.FirewallRule_DROP: + return firewall.ActionDrop + default: + return firewall.ActionUnknown + } +} diff --git a/client/internal/acl/manager_create.go b/client/internal/acl/manager_create.go new file mode 100644 index 000000000..335bfcac0 --- /dev/null +++ b/client/internal/acl/manager_create.go @@ -0,0 +1,27 @@ +//go:build !linux + +package acl + +import ( + "fmt" + "runtime" + + "github.com/netbirdio/netbird/client/firewall" + "github.com/netbirdio/netbird/client/firewall/uspfilter" +) + +// Create creates a firewall manager instance +func Create(iface iFaceMapper) (manager *DefaultManager, err error) { + if iface.IsUserspaceBind() { + // use userspace packet filtering firewall + fm, err := uspfilter.Create(iface) + if err != nil { + return nil, err + } + return &DefaultManager{ + manager: fm, + rulesPairs: make(map[string][]firewall.Rule), + }, nil + } + return nil, fmt.Errorf("not implemented for this OS: %s", runtime.GOOS) +} diff --git a/client/internal/acl/manager_create_linux.go b/client/internal/acl/manager_create_linux.go new file mode 100644 index 000000000..1c3767397 --- /dev/null +++ b/client/internal/acl/manager_create_linux.go @@ -0,0 +1,36 @@ +package acl + +import ( + log "github.com/sirupsen/logrus" + + "github.com/netbirdio/netbird/client/firewall" + "github.com/netbirdio/netbird/client/firewall/iptables" + "github.com/netbirdio/netbird/client/firewall/nftables" + "github.com/netbirdio/netbird/client/firewall/uspfilter" +) + +// Create creates a firewall manager instance for the Linux +func Create(iface iFaceMapper) (manager *DefaultManager, err error) { + var fm firewall.Manager + if iface.IsUserspaceBind() { + // use userspace packet filtering firewall + if fm, err = uspfilter.Create(iface); err != nil { + log.Debugf("failed to create userspace filtering firewall: %s", err) + return nil, err + } + } else { + if fm, err = iptables.Create(iface.Name()); err != nil { + log.Debugf("failed to create iptables manager: %s", err) + // fallback to nftables + if fm, err = nftables.Create(iface.Name()); err != nil { + log.Errorf("failed to create nftables manager: %s", err) + return nil, err + } + } + } + + return &DefaultManager{ + manager: fm, + rulesPairs: make(map[string][]firewall.Rule), + }, nil +} diff --git a/client/internal/acl/manager_test.go b/client/internal/acl/manager_test.go new file mode 100644 index 000000000..5aa7f5a36 --- /dev/null +++ b/client/internal/acl/manager_test.go @@ -0,0 +1,92 @@ +package acl + +import ( + "runtime" + "testing" + + "github.com/golang/mock/gomock" + + "github.com/netbirdio/netbird/client/internal/acl/mocks" + mgmProto "github.com/netbirdio/netbird/management/proto" +) + +func TestDefaultManager(t *testing.T) { + // TODO: enable when other platform will be added + if runtime.GOOS != "linux" { + t.Skipf("ACL manager not supported in the: %s", runtime.GOOS) + return + } + + fwRules := []*mgmProto.FirewallRule{ + { + PeerIP: "10.93.0.1", + Direction: mgmProto.FirewallRule_OUT, + Action: mgmProto.FirewallRule_ACCEPT, + Protocol: mgmProto.FirewallRule_TCP, + Port: "80", + }, + { + PeerIP: "10.93.0.2", + Direction: mgmProto.FirewallRule_OUT, + Action: mgmProto.FirewallRule_DROP, + Protocol: mgmProto.FirewallRule_UDP, + Port: "53", + }, + } + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + iface := mocks.NewMockIFaceMapper(ctrl) + iface.EXPECT().IsUserspaceBind().Return(false) + iface.EXPECT().Name().Return("lo") + + // we receive one rule from the management so for testing purposes ignore it + acl, err := Create(iface) + if err != nil { + t.Errorf("create ACL manager: %v", err) + return + } + defer acl.Stop() + + t.Run("apply firewall rules", func(t *testing.T) { + acl.ApplyFiltering(fwRules) + + if len(acl.rulesPairs) != 2 { + t.Errorf("firewall rules not applied: %v", acl.rulesPairs) + return + } + }) + + t.Run("add extra rules", func(t *testing.T) { + // remove first rule + fwRules = fwRules[1:] + fwRules = append(fwRules, &mgmProto.FirewallRule{ + PeerIP: "10.93.0.3", + Direction: mgmProto.FirewallRule_IN, + Action: mgmProto.FirewallRule_DROP, + Protocol: mgmProto.FirewallRule_ICMP, + }) + + existedRulesID := map[string]struct{}{} + for id := range acl.rulesPairs { + existedRulesID[id] = struct{}{} + } + + acl.ApplyFiltering(fwRules) + + // we should have one old and one new rule in the existed rules + if len(acl.rulesPairs) != 2 { + t.Errorf("firewall rules not applied") + return + } + + // check that old rules was removed + for id := range existedRulesID { + if _, ok := acl.rulesPairs[id]; ok { + t.Errorf("old rule was not removed") + return + } + } + }) +} diff --git a/client/internal/acl/mocks/iface_mapper.go b/client/internal/acl/mocks/iface_mapper.go new file mode 100644 index 000000000..cd4fb75ee --- /dev/null +++ b/client/internal/acl/mocks/iface_mapper.go @@ -0,0 +1,77 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/netbirdio/netbird/client/internal/acl (interfaces: IFaceMapper) + +// Package mocks is a generated GoMock package. +package mocks + +import ( + reflect "reflect" + + gomock "github.com/golang/mock/gomock" + iface "github.com/netbirdio/netbird/iface" +) + +// MockIFaceMapper is a mock of IFaceMapper interface. +type MockIFaceMapper struct { + ctrl *gomock.Controller + recorder *MockIFaceMapperMockRecorder +} + +// MockIFaceMapperMockRecorder is the mock recorder for MockIFaceMapper. +type MockIFaceMapperMockRecorder struct { + mock *MockIFaceMapper +} + +// NewMockIFaceMapper creates a new mock instance. +func NewMockIFaceMapper(ctrl *gomock.Controller) *MockIFaceMapper { + mock := &MockIFaceMapper{ctrl: ctrl} + mock.recorder = &MockIFaceMapperMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockIFaceMapper) EXPECT() *MockIFaceMapperMockRecorder { + return m.recorder +} + +// IsUserspaceBind mocks base method. +func (m *MockIFaceMapper) IsUserspaceBind() bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IsUserspaceBind") + ret0, _ := ret[0].(bool) + return ret0 +} + +// IsUserspaceBind indicates an expected call of IsUserspaceBind. +func (mr *MockIFaceMapperMockRecorder) IsUserspaceBind() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsUserspaceBind", reflect.TypeOf((*MockIFaceMapper)(nil).IsUserspaceBind)) +} + +// Name mocks base method. +func (m *MockIFaceMapper) Name() string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Name") + ret0, _ := ret[0].(string) + return ret0 +} + +// Name indicates an expected call of Name. +func (mr *MockIFaceMapperMockRecorder) Name() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Name", reflect.TypeOf((*MockIFaceMapper)(nil).Name)) +} + +// SetFiltering mocks base method. +func (m *MockIFaceMapper) SetFiltering(arg0 iface.PacketFilter) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SetFiltering", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// SetFiltering indicates an expected call of SetFiltering. +func (mr *MockIFaceMapperMockRecorder) SetFiltering(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetFiltering", reflect.TypeOf((*MockIFaceMapper)(nil).SetFiltering), arg0) +} diff --git a/client/internal/engine.go b/client/internal/engine.go index 3011495db..5adb645d3 100644 --- a/client/internal/engine.go +++ b/client/internal/engine.go @@ -17,6 +17,7 @@ import ( log "github.com/sirupsen/logrus" "golang.zx2c4.com/wireguard/wgctrl/wgtypes" + "github.com/netbirdio/netbird/client/internal/acl" "github.com/netbirdio/netbird/client/internal/dns" "github.com/netbirdio/netbird/client/internal/peer" "github.com/netbirdio/netbird/client/internal/proxy" @@ -114,6 +115,7 @@ type Engine struct { statusRecorder *peer.Status routeManager routemanager.Manager + acl acl.Manager dnsServer dns.Server } @@ -222,6 +224,12 @@ func (e *Engine) Start() error { e.routeManager = routemanager.NewManager(e.ctx, e.config.WgPrivateKey.PublicKey().String(), e.wgInterface, e.statusRecorder) + if acl, err := acl.Create(e.wgInterface); err != nil { + log.Errorf("failed to create ACL manager, policy will not work: %s", err.Error()) + } else { + e.acl = acl + } + if e.dnsServer == nil { // todo fix custom address dnsServer, err := dns.NewDefaultServer(e.ctx, e.wgInterface, e.config.CustomDNSAddress) @@ -622,6 +630,9 @@ func (e *Engine) updateNetworkMap(networkMap *mgmProto.NetworkMap) error { log.Errorf("failed to update dns server, err: %v", err) } + if e.acl != nil { + e.acl.ApplyFiltering(networkMap.FirewallRules) + } e.networkSerial = serial return nil } @@ -1005,6 +1016,9 @@ func (e *Engine) close() { e.dnsServer.Stop() } + if e.acl != nil { + e.acl.Stop() + } } func findIPFromInterfaceName(ifaceName string) (net.IP, error) { diff --git a/client/internal/engine_test.go b/client/internal/engine_test.go index 9ccfd5f7c..c21e15cd9 100644 --- a/client/internal/engine_test.go +++ b/client/internal/engine_test.go @@ -14,9 +14,6 @@ import ( "time" "github.com/pion/transport/v2/stdnet" - - "github.com/netbirdio/netbird/iface/bind" - log "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -31,6 +28,7 @@ import ( "github.com/netbirdio/netbird/client/system" nbdns "github.com/netbirdio/netbird/dns" "github.com/netbirdio/netbird/iface" + "github.com/netbirdio/netbird/iface/bind" mgmt "github.com/netbirdio/netbird/management/client" mgmtProto "github.com/netbirdio/netbird/management/proto" "github.com/netbirdio/netbird/management/server" diff --git a/go.mod b/go.mod index c674cf06a..39fc6dd38 100644 --- a/go.mod +++ b/go.mod @@ -36,6 +36,7 @@ require ( github.com/getlantern/systray v1.2.1 github.com/gliderlabs/ssh v0.3.4 github.com/godbus/dbus/v5 v5.1.0 + github.com/golang/mock v1.6.0 github.com/google/go-cmp v0.5.9 github.com/google/gopacket v1.1.19 github.com/google/nftables v0.0.0-20220808154552-2eca00135732 @@ -48,7 +49,6 @@ require ( github.com/miekg/dns v1.1.43 github.com/mitchellh/hashstructure/v2 v2.0.2 github.com/okta/okta-sdk-golang/v2 v2.18.0 - github.com/open-policy-agent/opa v0.49.0 github.com/patrickmn/go-cache v2.1.0+incompatible github.com/pion/logging v0.2.2 github.com/pion/stun v0.4.0 @@ -71,14 +71,13 @@ require ( require ( github.com/BurntSushi/toml v1.2.1 // indirect - github.com/OneOfOne/xxhash v1.2.8 // indirect github.com/XiaoMi/pegasus-go-client v0.0.0-20210427083443-f3b6b08bc4c2 // indirect - github.com/agnivade/levenshtein v1.1.1 // indirect github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bradfitz/gomemcache v0.0.0-20220106215444-fb4bf637b56d // indirect github.com/cespare/xxhash/v2 v2.1.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dgraph-io/ristretto v0.1.1 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/fredbi/uri v0.0.0-20181227131451-3dcfdacbaaf3 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect @@ -88,14 +87,12 @@ require ( github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7 // indirect github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55 // indirect github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f // indirect - github.com/ghodss/yaml v1.0.0 // indirect github.com/go-gl/gl v0.0.0-20210813123233-e4099ee2221f // indirect github.com/go-gl/glfw/v3.3/glfw v0.0.0-20211024062804-40e447a793be // indirect github.com/go-logr/logr v1.2.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-redis/redis/v8 v8.11.5 // indirect github.com/go-stack/stack v1.8.0 // indirect - github.com/gobwas/glob v0.2.3 // indirect github.com/goki/freetype v0.0.0-20181231101311-fa8a33aabaff // indirect github.com/hashicorp/go-uuid v1.0.2 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect @@ -116,16 +113,11 @@ require ( github.com/prometheus/client_model v0.3.0 // indirect github.com/prometheus/common v0.37.0 // indirect github.com/prometheus/procfs v0.8.0 // indirect - github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0 // indirect github.com/rogpeppe/go-internal v1.8.0 // indirect github.com/spf13/cast v1.5.0 // indirect github.com/srwiley/oksvg v0.0.0-20200311192757-870daf9aa564 // indirect github.com/srwiley/rasterx v0.0.0-20200120212402-85cb7272f5e9 // indirect - github.com/tchap/go-patricia/v2 v2.3.1 // indirect github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 // indirect - github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect - github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect - github.com/yashtewari/glob-intersection v0.1.0 // indirect github.com/yuin/goldmark v1.4.13 // indirect go.opentelemetry.io/otel/sdk v1.11.1 // indirect go.opentelemetry.io/otel/trace v1.11.1 // indirect @@ -133,10 +125,12 @@ require ( golang.org/x/mod v0.8.0 // indirect golang.org/x/oauth2 v0.8.0 // indirect golang.org/x/text v0.9.0 // indirect + golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect golang.org/x/tools v0.6.0 // indirect golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/genproto v0.0.0-20221118155620-16455021b5e6 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/square/go-jose.v2 v2.6.0 // indirect gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637 // indirect diff --git a/go.sum b/go.sum index 5db15143c..9c719d57c 100644 --- a/go.sum +++ b/go.sum @@ -53,16 +53,12 @@ github.com/Microsoft/go-winio v0.5.1/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpz github.com/Microsoft/hcsshim v0.8.14/go.mod h1:NtVKoYxQuTLx6gEq0L96c9Ju4JbRJ4nY2ow3VK6a9Lg= github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= -github.com/OneOfOne/xxhash v1.2.8 h1:31czK/TI9sNkxIKfaUfGlU47BAxQ0ztGgd9vPyqimf8= -github.com/OneOfOne/xxhash v1.2.8/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q= github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/XiaoMi/pegasus-go-client v0.0.0-20210427083443-f3b6b08bc4c2 h1:pami0oPhVosjOu/qRHepRmdjD6hGILF7DBr+qQZeP10= github.com/XiaoMi/pegasus-go-client v0.0.0-20210427083443-f3b6b08bc4c2/go.mod h1:jNIx5ykW1MroBuaTja9+VpglmaJOUzezumfhLlER3oY= -github.com/agnivade/levenshtein v1.1.1 h1:QY8M92nrzkmr798gCo3kmMyqXFzdQVpxLlGPRBij0P8= -github.com/agnivade/levenshtein v1.1.1/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVbJomOvKkmgYbo= github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= @@ -73,8 +69,6 @@ github.com/allegro/bigcache/v3 v3.0.2 h1:AKZCw+5eAaVyNTBmI2fgyPVJhHkdWder3O9Irpr github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= -github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q= -github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/bazelbuild/rules_go v0.30.0/go.mod h1:MC23Dc/wkXEyk3Wpq6lCqz0ZAYOZDw2DR5y3N1q2i7M= @@ -84,7 +78,6 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bradfitz/gomemcache v0.0.0-20220106215444-fb4bf637b56d h1:pVrfxiGfwelyab6n21ZBkbkmbevaf+WvMIiR7sr97hw= github.com/bradfitz/gomemcache v0.0.0-20220106215444-fb4bf637b56d/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA= -github.com/bytecodealliance/wasmtime-go/v3 v3.0.2 h1:3uZCA/BLTIu+DqCfguByNMJa2HVHpXvjfy0Dy7g6fuA= github.com/c-robinson/iplib v1.0.3 h1:NG0UF0GoEsrC1/vyfX1Lx2Ss7CySWl3KqqXh3q4DdPU= github.com/c-robinson/iplib v1.0.3/go.mod h1:i3LuuFL1hRT5gFpBRnEydzw8R6yhGkF4szNDIbF8pgo= github.com/cenkalti/backoff v1.1.1-0.20190506075156-2146c9339422/go.mod h1:b6Nc7NRH5C4aCISLry0tLnTjcuTEvoiqcWDdsU0sOGM= @@ -92,7 +85,6 @@ github.com/cenkalti/backoff/v4 v4.1.0/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInq github.com/cenkalti/backoff/v4 v4.1.3 h1:cFAlzYUlVYDysBEH2T5hyJZMh3+5+WCBvSnK6Q8UtC4= github.com/cenkalti/backoff/v4 v4.1.3/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= @@ -149,14 +141,13 @@ github.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dgraph-io/badger/v3 v3.2103.5 h1:ylPa6qzbjYRQMU6jokoj4wzcaweHylt//CH0AKt0akg= github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8= +github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= -github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48 h1:fRzb/w+pyskVMQ+UbP35JkH8yB7MYb4q/qhBarqZE6g= -github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= @@ -178,7 +169,6 @@ github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLi github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= -github.com/foxcpp/go-mockdns v0.0.0-20210729171921-fb145fc6f897 h1:E52jfcE64UG42SwLmrW0QByONfGynWuzBvm86BoB9z8= github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= github.com/fredbi/uri v0.0.0-20181227131451-3dcfdacbaaf3 h1:FDqhDm7pcsLhhWl1QtD8vlzI4mm59llRvNzrFg6/LAA= @@ -201,7 +191,6 @@ github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55/go.mod h1:6mmzY2 github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f h1:wrYrQttPS8FHIRSlsrcuKazukx/xqO/PpLZzZXsF+EA= github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f/go.mod h1:D5ao98qkA6pxftxoqzibIBBrLSUli+kYnJqrgBf9cIA= github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmCsR2Do= @@ -246,8 +235,6 @@ github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= -github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= -github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= @@ -257,7 +244,6 @@ github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7a github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= -github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/goki/freetype v0.0.0-20181231101311-fa8a33aabaff h1:W71vTCKoxtdXgnm1ECDFkfQnpdqAO00zzGXLA5yaEX8= github.com/goki/freetype v0.0.0-20181231101311-fa8a33aabaff/go.mod h1:wfqRWLHRBsRgkp5dmbG56SA0DmVtwrF5N3oPdI8t+Aw= @@ -270,7 +256,6 @@ github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4er github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= @@ -279,6 +264,7 @@ github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -298,12 +284,10 @@ github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaS github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= -github.com/google/flatbuffers v1.12.1 h1:MVlul7pQNoDzWRLTw5imwYsl+usrS1TXG2H4jg6ImGw= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= @@ -412,7 +396,6 @@ github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvW github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= @@ -519,8 +502,6 @@ github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1y github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE= github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs= -github.com/open-policy-agent/opa v0.49.0 h1:TIlpCT1B5FSm8Dqo/a4t23gKmHkQysC3+7W77F99P4k= -github.com/open-policy-agent/opa v0.49.0/go.mod h1:WTLWtu498/QNTDkiHx76Xj7jaJUPvLJAPtdMkCcst0w= github.com/opencontainers/go-digest v0.0.0-20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/runc v0.0.0-20190115041553-12f6a991201f/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= @@ -594,8 +575,6 @@ github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1 github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo= github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= -github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0 h1:MkV+77GLUNo5oJ0jf870itWm3D0Sjh7+Za9gazKc5LQ= -github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= @@ -657,8 +636,6 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= -github.com/tchap/go-patricia/v2 v2.3.1 h1:6rQp39lgIYZ+MHmdEq4xzuk1t7OdC35z/xm0BGhTkes= -github.com/tchap/go-patricia/v2 v2.3.1/go.mod h1:VZRHKAb53DLaG+nA9EaYYiaEx6YztwDlLElMsnSHD4k= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c/go.mod h1:hzIxponao9Kjc7aWznkXaL4U4TWaDSs8zcsY4Ka08nM= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= @@ -672,14 +649,8 @@ github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYp github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU= github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 h1:gga7acRE695APm9hlsSMoOoE65U4/TcqNj90mc69Rlg= github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= -github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= -github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= -github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= -github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= -github.com/yashtewari/glob-intersection v0.1.0 h1:6gJvMYQlTDOL3dMsPF6J0+26vwX9MB8/1q3uAdhmTrg= -github.com/yashtewari/glob-intersection v0.1.0/go.mod h1:LK7pIC3piUjovexikBbJ26Yml7g8xa5bsjfx2v1fwok= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -696,7 +667,6 @@ go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= go.opentelemetry.io/otel v1.11.1 h1:4WLLAmcfkmDk2ukNXJyq3/kiz/3UzCaYq6PskJsaou4= go.opentelemetry.io/otel v1.11.1/go.mod h1:1nNhXBbWSD0nsL38H6btgnFN2k4i0sNLHNNMZMSbUGE= @@ -954,6 +924,7 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -986,6 +957,7 @@ golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxb golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac h1:7zkz7BUtwNFFqcowJ+RIgu2MaV/MapERkDIy+mwPyjs= +golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -1034,6 +1006,7 @@ golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= @@ -1153,6 +1126,7 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= diff --git a/iface/device_wrapper.go b/iface/device_wrapper.go new file mode 100644 index 000000000..d6e39b81b --- /dev/null +++ b/iface/device_wrapper.go @@ -0,0 +1,90 @@ +package iface + +import ( + "net" + "sync" + + "golang.zx2c4.com/wireguard/tun" +) + +// PacketFilter interface for firewall abilities +type PacketFilter interface { + // DropOutgoing filter outgoing packets from host to external destinations + DropOutgoing(packetData []byte) bool + + // DropIncoming filter incoming packets from external sources to host + DropIncoming(packetData []byte) bool + + // SetNetwork of the wireguard interface to which filtering applied + SetNetwork(*net.IPNet) +} + +// DeviceWrapper to override Read or Write of packets +type DeviceWrapper struct { + tun.Device + filter PacketFilter + mutex sync.RWMutex +} + +// newDeviceWrapper constructor function +func newDeviceWrapper(device tun.Device) *DeviceWrapper { + return &DeviceWrapper{ + Device: device, + } +} + +// Read wraps read method with filtering feature +func (d *DeviceWrapper) Read(bufs [][]byte, sizes []int, offset int) (n int, err error) { + if n, err = d.Device.Read(bufs, sizes, offset); err != nil { + return 0, err + } + d.mutex.RLock() + filter := d.filter + d.mutex.RUnlock() + + if filter == nil { + return + } + + for i := 0; i < n; i++ { + if filter.DropOutgoing(bufs[i][offset : offset+sizes[i]]) { + bufs = append(bufs[:i], bufs[i+1:]...) + sizes = append(sizes[:i], sizes[i+1:]...) + n-- + i-- + } + } + + return n, nil +} + +// Write wraps write method with filtering feature +func (d *DeviceWrapper) Write(bufs [][]byte, offset int) (int, error) { + d.mutex.RLock() + filter := d.filter + d.mutex.RUnlock() + + if filter == nil { + return d.Device.Write(bufs, offset) + } + + filteredBufs := make([][]byte, 0, len(bufs)) + dropped := 0 + for _, buf := range bufs { + if !filter.DropIncoming(buf[offset:]) { + filteredBufs = append(filteredBufs, buf) + dropped++ + } + } + + n, err := d.Device.Write(filteredBufs, offset) + n += dropped + return n, err +} + +// SetFiltering sets packet filter to device +func (d *DeviceWrapper) SetFiltering(filter PacketFilter) { + d.mutex.Lock() + d.filter = filter + d.mutex.Unlock() +} diff --git a/iface/device_wrapper_test.go b/iface/device_wrapper_test.go new file mode 100644 index 000000000..9d1045100 --- /dev/null +++ b/iface/device_wrapper_test.go @@ -0,0 +1,216 @@ +package iface + +import ( + "net" + "testing" + + "github.com/golang/mock/gomock" + "github.com/google/gopacket" + "github.com/google/gopacket/layers" + mocks "github.com/netbirdio/netbird/iface/mocks" +) + +func TestDeviceWrapperRead(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + tun := mocks.NewMockDevice(ctrl) + filter := mocks.NewMockPacketFilter(ctrl) + + mockBufs := [][]byte{{}} + mockSizes := []int{0} + mockOffset := 0 + + t.Run("read ICMP", func(t *testing.T) { + ipLayer := &layers.IPv4{ + Version: 4, + TTL: 64, + Protocol: layers.IPProtocolICMPv4, + SrcIP: net.IP{192, 168, 0, 1}, + DstIP: net.IP{100, 200, 0, 1}, + } + + icmpLayer := &layers.ICMPv4{ + TypeCode: layers.CreateICMPv4TypeCode(layers.ICMPv4TypeEchoRequest, 0), + Id: 1, + Seq: 1, + } + + buffer := gopacket.NewSerializeBuffer() + err := gopacket.SerializeLayers(buffer, gopacket.SerializeOptions{}, + ipLayer, + icmpLayer, + ) + if err != nil { + t.Errorf("serialize packet: %v", err) + return + } + + tun.EXPECT().Read(mockBufs, mockSizes, mockOffset). + DoAndReturn(func(bufs [][]byte, sizes []int, offset int) (int, error) { + bufs[0] = buffer.Bytes() + sizes[0] = len(bufs[0]) + return 1, nil + }) + + wrapped := newDeviceWrapper(tun) + + bufs := [][]byte{{}} + sizes := []int{0} + offset := 0 + + n, err := wrapped.Read(bufs, sizes, offset) + if err != nil { + t.Errorf("unexpeted error: %v", err) + return + } + if n != 1 { + t.Errorf("expected n=1, got %d", n) + return + } + }) + + t.Run("write TCP", func(t *testing.T) { + ipLayer := &layers.IPv4{ + Version: 4, + TTL: 64, + Protocol: layers.IPProtocolICMPv4, + SrcIP: net.IP{100, 200, 0, 9}, + DstIP: net.IP{100, 200, 0, 10}, + } + + // create TCP layer packet + tcpLayer := &layers.TCP{ + SrcPort: layers.TCPPort(34423), + DstPort: layers.TCPPort(8080), + } + + buffer := gopacket.NewSerializeBuffer() + err := gopacket.SerializeLayers(buffer, gopacket.SerializeOptions{}, + ipLayer, + tcpLayer, + ) + if err != nil { + t.Errorf("serialize packet: %v", err) + return + } + + mockBufs[0] = buffer.Bytes() + tun.EXPECT().Write(mockBufs, 0).Return(1, nil) + + wrapped := newDeviceWrapper(tun) + + bufs := [][]byte{buffer.Bytes()} + + n, err := wrapped.Write(bufs, 0) + if err != nil { + t.Errorf("unexpeted error: %v", err) + return + } + if n != 1 { + t.Errorf("expected n=1, got %d", n) + return + } + }) + + t.Run("drop write UDP package", func(t *testing.T) { + ipLayer := &layers.IPv4{ + Version: 4, + TTL: 64, + Protocol: layers.IPProtocolICMPv4, + SrcIP: net.IP{100, 200, 0, 11}, + DstIP: net.IP{100, 200, 0, 20}, + } + + // create TCP layer packet + tcpLayer := &layers.UDP{ + SrcPort: layers.UDPPort(27278), + DstPort: layers.UDPPort(53), + } + + buffer := gopacket.NewSerializeBuffer() + err := gopacket.SerializeLayers(buffer, gopacket.SerializeOptions{}, + ipLayer, + tcpLayer, + ) + if err != nil { + t.Errorf("serialize packet: %v", err) + return + } + + mockBufs = [][]byte{} + + tun.EXPECT().Write(mockBufs, 0).Return(0, nil) + filter.EXPECT().DropOutput(gomock.Any()).Return(true) + + wrapped := newDeviceWrapper(tun) + wrapped.filter = filter + + bufs := [][]byte{buffer.Bytes()} + + n, err := wrapped.Write(bufs, 0) + if err != nil { + t.Errorf("unexpeted error: %v", err) + return + } + if n != 0 { + t.Errorf("expected n=1, got %d", n) + return + } + }) + + t.Run("drop read UDP package", func(t *testing.T) { + ipLayer := &layers.IPv4{ + Version: 4, + TTL: 64, + Protocol: layers.IPProtocolICMPv4, + SrcIP: net.IP{100, 200, 0, 11}, + DstIP: net.IP{100, 200, 0, 20}, + } + + // create TCP layer packet + tcpLayer := &layers.UDP{ + SrcPort: layers.UDPPort(19243), + DstPort: layers.UDPPort(1024), + } + + buffer := gopacket.NewSerializeBuffer() + err := gopacket.SerializeLayers(buffer, gopacket.SerializeOptions{}, + ipLayer, + tcpLayer, + ) + if err != nil { + t.Errorf("serialize packet: %v", err) + return + } + + mockBufs := [][]byte{{}} + mockSizes := []int{0} + mockOffset := 0 + + tun.EXPECT().Read(mockBufs, mockSizes, mockOffset). + DoAndReturn(func(bufs [][]byte, sizes []int, offset int) (int, error) { + bufs[0] = buffer.Bytes() + sizes[0] = len(bufs[0]) + return 1, nil + }) + filter.EXPECT().DropInput(gomock.Any()).Return(true) + + wrapped := newDeviceWrapper(tun) + wrapped.filter = filter + + bufs := [][]byte{{}} + sizes := []int{0} + offset := 0 + + n, err := wrapped.Read(bufs, sizes, offset) + if err != nil { + t.Errorf("unexpeted error: %v", err) + return + } + if n != 0 { + t.Errorf("expected n=0, got %d", n) + return + } + }) +} diff --git a/iface/iface.go b/iface/iface.go index 788a180b8..100e16982 100644 --- a/iface/iface.go +++ b/iface/iface.go @@ -1,6 +1,7 @@ package iface import ( + "fmt" "net" "sync" "time" @@ -118,3 +119,17 @@ func (w *WGIface) Close() error { defer w.mu.Unlock() return w.tun.Close() } + +// SetFiltering sets packet filters for the userspace impelemntation +func (w *WGIface) SetFiltering(filter PacketFilter) error { + w.mu.Lock() + defer w.mu.Unlock() + + if w.tun.wrapper == nil { + return fmt.Errorf("userspace packet filtering not handled on this device") + } + + filter.SetNetwork(w.tun.address.Network) + w.tun.wrapper.SetFiltering(filter) + return nil +} diff --git a/iface/mocks/filter.go b/iface/mocks/filter.go new file mode 100644 index 000000000..9537520a2 --- /dev/null +++ b/iface/mocks/filter.go @@ -0,0 +1,75 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/netbirdio/netbird/iface (interfaces: PacketFilter) + +// Package mocks is a generated GoMock package. +package mocks + +import ( + net "net" + reflect "reflect" + + gomock "github.com/golang/mock/gomock" +) + +// MockPacketFilter is a mock of PacketFilter interface. +type MockPacketFilter struct { + ctrl *gomock.Controller + recorder *MockPacketFilterMockRecorder +} + +// MockPacketFilterMockRecorder is the mock recorder for MockPacketFilter. +type MockPacketFilterMockRecorder struct { + mock *MockPacketFilter +} + +// NewMockPacketFilter creates a new mock instance. +func NewMockPacketFilter(ctrl *gomock.Controller) *MockPacketFilter { + mock := &MockPacketFilter{ctrl: ctrl} + mock.recorder = &MockPacketFilterMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockPacketFilter) EXPECT() *MockPacketFilterMockRecorder { + return m.recorder +} + +// DropInput mocks base method. +func (m *MockPacketFilter) DropOutgoing(arg0 []byte) bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DropOutgoing", arg0) + ret0, _ := ret[0].(bool) + return ret0 +} + +// DropInput indicates an expected call of DropInput. +func (mr *MockPacketFilterMockRecorder) DropInput(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DropOutgoing", reflect.TypeOf((*MockPacketFilter)(nil).DropOutgoing), arg0) +} + +// DropOutput mocks base method. +func (m *MockPacketFilter) DropIncoming(arg0 []byte) bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DropIncoming", arg0) + ret0, _ := ret[0].(bool) + return ret0 +} + +// DropOutput indicates an expected call of DropOutput. +func (mr *MockPacketFilterMockRecorder) DropOutput(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DropIncoming", reflect.TypeOf((*MockPacketFilter)(nil).DropIncoming), arg0) +} + +// SetNetwork mocks base method. +func (m *MockPacketFilter) SetNetwork(arg0 *net.IPNet) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetNetwork", arg0) +} + +// SetNetwork indicates an expected call of SetNetwork. +func (mr *MockPacketFilterMockRecorder) SetNetwork(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetNetwork", reflect.TypeOf((*MockPacketFilter)(nil).SetNetwork), arg0) +} diff --git a/iface/mocks/tun.go b/iface/mocks/tun.go new file mode 100644 index 000000000..677c82b0b --- /dev/null +++ b/iface/mocks/tun.go @@ -0,0 +1,152 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: golang.zx2c4.com/wireguard/tun (interfaces: Device) + +// Package mocks is a generated GoMock package. +package mocks + +import ( + os "os" + reflect "reflect" + + gomock "github.com/golang/mock/gomock" + tun "golang.zx2c4.com/wireguard/tun" +) + +// MockDevice is a mock of Device interface. +type MockDevice struct { + ctrl *gomock.Controller + recorder *MockDeviceMockRecorder +} + +// MockDeviceMockRecorder is the mock recorder for MockDevice. +type MockDeviceMockRecorder struct { + mock *MockDevice +} + +// NewMockDevice creates a new mock instance. +func NewMockDevice(ctrl *gomock.Controller) *MockDevice { + mock := &MockDevice{ctrl: ctrl} + mock.recorder = &MockDeviceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockDevice) EXPECT() *MockDeviceMockRecorder { + return m.recorder +} + +// BatchSize mocks base method. +func (m *MockDevice) BatchSize() int { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "BatchSize") + ret0, _ := ret[0].(int) + return ret0 +} + +// BatchSize indicates an expected call of BatchSize. +func (mr *MockDeviceMockRecorder) BatchSize() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BatchSize", reflect.TypeOf((*MockDevice)(nil).BatchSize)) +} + +// Close mocks base method. +func (m *MockDevice) Close() error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Close") + ret0, _ := ret[0].(error) + return ret0 +} + +// Close indicates an expected call of Close. +func (mr *MockDeviceMockRecorder) Close() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockDevice)(nil).Close)) +} + +// Events mocks base method. +func (m *MockDevice) Events() <-chan tun.Event { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Events") + ret0, _ := ret[0].(<-chan tun.Event) + return ret0 +} + +// Events indicates an expected call of Events. +func (mr *MockDeviceMockRecorder) Events() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Events", reflect.TypeOf((*MockDevice)(nil).Events)) +} + +// File mocks base method. +func (m *MockDevice) File() *os.File { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "File") + ret0, _ := ret[0].(*os.File) + return ret0 +} + +// File indicates an expected call of File. +func (mr *MockDeviceMockRecorder) File() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "File", reflect.TypeOf((*MockDevice)(nil).File)) +} + +// MTU mocks base method. +func (m *MockDevice) MTU() (int, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "MTU") + ret0, _ := ret[0].(int) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// MTU indicates an expected call of MTU. +func (mr *MockDeviceMockRecorder) MTU() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MTU", reflect.TypeOf((*MockDevice)(nil).MTU)) +} + +// Name mocks base method. +func (m *MockDevice) Name() (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Name") + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Name indicates an expected call of Name. +func (mr *MockDeviceMockRecorder) Name() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Name", reflect.TypeOf((*MockDevice)(nil).Name)) +} + +// Read mocks base method. +func (m *MockDevice) Read(arg0 [][]byte, arg1 []int, arg2 int) (int, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Read", arg0, arg1, arg2) + ret0, _ := ret[0].(int) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Read indicates an expected call of Read. +func (mr *MockDeviceMockRecorder) Read(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockDevice)(nil).Read), arg0, arg1, arg2) +} + +// Write mocks base method. +func (m *MockDevice) Write(arg0 [][]byte, arg1 int) (int, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Write", arg0, arg1) + ret0, _ := ret[0].(int) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Write indicates an expected call of Write. +func (mr *MockDeviceMockRecorder) Write(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Write", reflect.TypeOf((*MockDevice)(nil).Write), arg0, arg1) +} diff --git a/iface/tun_android.go b/iface/tun_android.go index cedb016c0..93cfe522c 100644 --- a/iface/tun_android.go +++ b/iface/tun_android.go @@ -22,6 +22,7 @@ type tunDevice struct { name string device *device.Device iceBind *bind.ICEBind + wrapper *DeviceWrapper } func newTunDevice(address WGAddress, mtu int, routes []string, tunAdapter TunAdapter, transportNet transport.Net) *tunDevice { @@ -49,9 +50,10 @@ func (t *tunDevice) Create() error { return err } t.name = name + t.wrapper = newDeviceWrapper(tunDevice) log.Debugf("attaching to interface %v", name) - t.device = device.NewDevice(tunDevice, t.iceBind, device.NewLogger(device.LogLevelSilent, "[wiretrustee] ")) + t.device = device.NewDevice(t.wrapper, t.iceBind, device.NewLogger(device.LogLevelSilent, "[wiretrustee] ")) // without this property mobile devices can discover remote endpoints if the configured one was wrong. // this helps with support for the older NetBird clients that had a hardcoded direct mode //t.device.DisableSomeRoamingForBrokenMobileSemantics() diff --git a/iface/tun_unix.go b/iface/tun_unix.go index 49d3f6cb0..f923362a4 100644 --- a/iface/tun_unix.go +++ b/iface/tun_unix.go @@ -23,6 +23,7 @@ type tunDevice struct { netInterface NetInterface iceBind *bind.ICEBind uapi net.Listener + wrapper *DeviceWrapper close chan struct{} } @@ -90,9 +91,14 @@ func (c *tunDevice) createWithUserspace() (NetInterface, error) { if err != nil { return nil, err } + c.wrapper = newDeviceWrapper(tunIface) // We need to create a wireguard-go device and listen to configuration requests - tunDev := device.NewDevice(tunIface, c.iceBind, device.NewLogger(device.LogLevelSilent, "[netbird] ")) + tunDev := device.NewDevice( + c.wrapper, + c.iceBind, + device.NewLogger(device.LogLevelSilent, "[netbird] "), + ) err = tunDev.Up() if err != nil { _ = tunIface.Close() diff --git a/iface/tun_windows.go b/iface/tun_windows.go index 6fdab43c1..851ce9039 100644 --- a/iface/tun_windows.go +++ b/iface/tun_windows.go @@ -23,6 +23,7 @@ type tunDevice struct { iceBind *bind.ICEBind mtu int uapi net.Listener + wrapper *DeviceWrapper close chan struct{} } @@ -52,6 +53,8 @@ func (c *tunDevice) createWithUserspace() (NetInterface, error) { if err != nil { return nil, err } + c.wrapper = newDeviceWrapper(tunIface) + // We need to create a wireguard-go device and listen to configuration requests tunDev := device.NewDevice(tunIface, c.iceBind, device.NewLogger(device.LogLevelSilent, "[netbird] ")) err = tunDev.Up() diff --git a/management/proto/management.pb.go b/management/proto/management.pb.go index ff2133526..59b753999 100644 --- a/management/proto/management.pb.go +++ b/management/proto/management.pb.go @@ -119,6 +119,153 @@ func (DeviceAuthorizationFlowProvider) EnumDescriptor() ([]byte, []int) { return file_management_proto_rawDescGZIP(), []int{17, 0} } +type FirewallRuleDirection int32 + +const ( + FirewallRule_IN FirewallRuleDirection = 0 + FirewallRule_OUT FirewallRuleDirection = 1 +) + +// Enum value maps for FirewallRuleDirection. +var ( + FirewallRuleDirection_name = map[int32]string{ + 0: "IN", + 1: "OUT", + } + FirewallRuleDirection_value = map[string]int32{ + "IN": 0, + "OUT": 1, + } +) + +func (x FirewallRuleDirection) Enum() *FirewallRuleDirection { + p := new(FirewallRuleDirection) + *p = x + return p +} + +func (x FirewallRuleDirection) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (FirewallRuleDirection) Descriptor() protoreflect.EnumDescriptor { + return file_management_proto_enumTypes[2].Descriptor() +} + +func (FirewallRuleDirection) Type() protoreflect.EnumType { + return &file_management_proto_enumTypes[2] +} + +func (x FirewallRuleDirection) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use FirewallRuleDirection.Descriptor instead. +func (FirewallRuleDirection) EnumDescriptor() ([]byte, []int) { + return file_management_proto_rawDescGZIP(), []int{25, 0} +} + +type FirewallRuleAction int32 + +const ( + FirewallRule_ACCEPT FirewallRuleAction = 0 + FirewallRule_DROP FirewallRuleAction = 1 +) + +// Enum value maps for FirewallRuleAction. +var ( + FirewallRuleAction_name = map[int32]string{ + 0: "ACCEPT", + 1: "DROP", + } + FirewallRuleAction_value = map[string]int32{ + "ACCEPT": 0, + "DROP": 1, + } +) + +func (x FirewallRuleAction) Enum() *FirewallRuleAction { + p := new(FirewallRuleAction) + *p = x + return p +} + +func (x FirewallRuleAction) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (FirewallRuleAction) Descriptor() protoreflect.EnumDescriptor { + return file_management_proto_enumTypes[3].Descriptor() +} + +func (FirewallRuleAction) Type() protoreflect.EnumType { + return &file_management_proto_enumTypes[3] +} + +func (x FirewallRuleAction) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use FirewallRuleAction.Descriptor instead. +func (FirewallRuleAction) EnumDescriptor() ([]byte, []int) { + return file_management_proto_rawDescGZIP(), []int{25, 1} +} + +type FirewallRuleProtocol int32 + +const ( + FirewallRule_UNKNOWN FirewallRuleProtocol = 0 + FirewallRule_ALL FirewallRuleProtocol = 1 + FirewallRule_TCP FirewallRuleProtocol = 2 + FirewallRule_UDP FirewallRuleProtocol = 3 + FirewallRule_ICMP FirewallRuleProtocol = 4 +) + +// Enum value maps for FirewallRuleProtocol. +var ( + FirewallRuleProtocol_name = map[int32]string{ + 0: "UNKNOWN", + 1: "ALL", + 2: "TCP", + 3: "UDP", + 4: "ICMP", + } + FirewallRuleProtocol_value = map[string]int32{ + "UNKNOWN": 0, + "ALL": 1, + "TCP": 2, + "UDP": 3, + "ICMP": 4, + } +) + +func (x FirewallRuleProtocol) Enum() *FirewallRuleProtocol { + p := new(FirewallRuleProtocol) + *p = x + return p +} + +func (x FirewallRuleProtocol) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (FirewallRuleProtocol) Descriptor() protoreflect.EnumDescriptor { + return file_management_proto_enumTypes[4].Descriptor() +} + +func (FirewallRuleProtocol) Type() protoreflect.EnumType { + return &file_management_proto_enumTypes[4] +} + +func (x FirewallRuleProtocol) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use FirewallRuleProtocol.Descriptor instead. +func (FirewallRuleProtocol) EnumDescriptor() ([]byte, []int) { + return file_management_proto_rawDescGZIP(), []int{25, 2} +} + type EncryptedMessage struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -995,6 +1142,8 @@ type NetworkMap struct { DNSConfig *DNSConfig `protobuf:"bytes,6,opt,name=DNSConfig,proto3" json:"DNSConfig,omitempty"` // RemotePeerConfig represents a list of remote peers that the receiver can connect to OfflinePeers []*RemotePeerConfig `protobuf:"bytes,7,rep,name=offlinePeers,proto3" json:"offlinePeers,omitempty"` + // FirewallRule represents a list of firewall rules to be applied to peer + FirewallRules []*FirewallRule `protobuf:"bytes,8,rep,name=FirewallRules,proto3" json:"FirewallRules,omitempty"` } func (x *NetworkMap) Reset() { @@ -1078,6 +1227,13 @@ func (x *NetworkMap) GetOfflinePeers() []*RemotePeerConfig { return nil } +func (x *NetworkMap) GetFirewallRules() []*FirewallRule { + if x != nil { + return x.FirewallRules + } + return nil +} + // RemotePeerConfig represents a configuration of a remote peer. // The properties are used to configure WireGuard Peers sections type RemotePeerConfig struct { @@ -1849,6 +2005,86 @@ func (x *NameServer) GetPort() int64 { return 0 } +// FirewallRule represents a firewall rule +type FirewallRule struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + PeerIP string `protobuf:"bytes,1,opt,name=PeerIP,proto3" json:"PeerIP,omitempty"` + Direction FirewallRuleDirection `protobuf:"varint,2,opt,name=Direction,proto3,enum=management.FirewallRuleDirection" json:"Direction,omitempty"` + Action FirewallRuleAction `protobuf:"varint,3,opt,name=Action,proto3,enum=management.FirewallRuleAction" json:"Action,omitempty"` + Protocol FirewallRuleProtocol `protobuf:"varint,4,opt,name=Protocol,proto3,enum=management.FirewallRuleProtocol" json:"Protocol,omitempty"` + Port string `protobuf:"bytes,5,opt,name=Port,proto3" json:"Port,omitempty"` +} + +func (x *FirewallRule) Reset() { + *x = FirewallRule{} + if protoimpl.UnsafeEnabled { + mi := &file_management_proto_msgTypes[25] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *FirewallRule) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FirewallRule) ProtoMessage() {} + +func (x *FirewallRule) ProtoReflect() protoreflect.Message { + mi := &file_management_proto_msgTypes[25] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use FirewallRule.ProtoReflect.Descriptor instead. +func (*FirewallRule) Descriptor() ([]byte, []int) { + return file_management_proto_rawDescGZIP(), []int{25} +} + +func (x *FirewallRule) GetPeerIP() string { + if x != nil { + return x.PeerIP + } + return "" +} + +func (x *FirewallRule) GetDirection() FirewallRuleDirection { + if x != nil { + return x.Direction + } + return FirewallRule_IN +} + +func (x *FirewallRule) GetAction() FirewallRuleAction { + if x != nil { + return x.Action + } + return FirewallRule_ACCEPT +} + +func (x *FirewallRule) GetProtocol() FirewallRuleProtocol { + if x != nil { + return x.Protocol + } + return FirewallRule_UNKNOWN +} + +func (x *FirewallRule) GetPort() string { + if x != nil { + return x.Port + } + return "" +} + var File_management_proto protoreflect.FileDescriptor var file_management_proto_rawDesc = []byte{ @@ -1966,7 +2202,7 @@ var file_management_proto_rawDesc = []byte{ 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x53, 0x48, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x09, 0x73, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x18, 0x04, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x22, 0xee, 0x02, 0x0a, 0x0a, 0x4e, 0x65, 0x74, + 0x28, 0x09, 0x52, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x22, 0xae, 0x03, 0x0a, 0x0a, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x12, 0x16, 0x0a, 0x06, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x12, 0x36, 0x0a, 0x0a, 0x70, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x02, 0x20, @@ -1989,7 +2225,11 @@ var file_management_proto_rawDesc = []byte{ 0x6e, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0c, 0x6f, 0x66, 0x66, - 0x6c, 0x69, 0x6e, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x22, 0x97, 0x01, 0x0a, 0x10, 0x52, 0x65, + 0x6c, 0x69, 0x6e, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x12, 0x3e, 0x0a, 0x0d, 0x46, 0x69, 0x72, + 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x18, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x46, 0x69, + 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x52, 0x0d, 0x46, 0x69, 0x72, 0x65, + 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x22, 0x97, 0x01, 0x0a, 0x10, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1a, 0x0a, 0x08, 0x77, 0x67, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x77, 0x67, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x12, 0x1e, 0x0a, 0x0a, 0x61, 0x6c, @@ -2083,32 +2323,55 @@ var file_management_proto_rawDesc = []byte{ 0x76, 0x65, 0x72, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x50, 0x12, 0x16, 0x0a, 0x06, 0x4e, 0x53, 0x54, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x4e, 0x53, 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x50, - 0x6f, 0x72, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x32, - 0xf7, 0x02, 0x0a, 0x11, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x65, - 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x45, 0x0a, 0x05, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x1c, + 0x6f, 0x72, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x22, + 0xf0, 0x02, 0x0a, 0x0c, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, + 0x12, 0x16, 0x0a, 0x06, 0x50, 0x65, 0x65, 0x72, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x06, 0x50, 0x65, 0x65, 0x72, 0x49, 0x50, 0x12, 0x40, 0x0a, 0x09, 0x44, 0x69, 0x72, 0x65, + 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x22, 0x2e, 0x6d, 0x61, + 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, + 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x2e, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, + 0x09, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x37, 0x0a, 0x06, 0x41, 0x63, + 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1f, 0x2e, 0x6d, 0x61, 0x6e, + 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, + 0x52, 0x75, 0x6c, 0x65, 0x2e, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x41, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x12, 0x3d, 0x0a, 0x08, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x21, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, + 0x6e, 0x74, 0x2e, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x52, 0x08, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, + 0x6f, 0x6c, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x22, 0x1c, 0x0a, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x12, 0x06, 0x0a, 0x02, 0x49, 0x4e, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x4f, + 0x55, 0x54, 0x10, 0x01, 0x22, 0x1e, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0a, + 0x0a, 0x06, 0x41, 0x43, 0x43, 0x45, 0x50, 0x54, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x44, 0x52, + 0x4f, 0x50, 0x10, 0x01, 0x22, 0x3c, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, + 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x07, 0x0a, + 0x03, 0x41, 0x4c, 0x4c, 0x10, 0x01, 0x12, 0x07, 0x0a, 0x03, 0x54, 0x43, 0x50, 0x10, 0x02, 0x12, + 0x07, 0x0a, 0x03, 0x55, 0x44, 0x50, 0x10, 0x03, 0x12, 0x08, 0x0a, 0x04, 0x49, 0x43, 0x4d, 0x50, + 0x10, 0x04, 0x32, 0xf7, 0x02, 0x0a, 0x11, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, + 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x45, 0x0a, 0x05, 0x4c, 0x6f, 0x67, 0x69, + 0x6e, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, + 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, + 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, + 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12, + 0x46, 0x0a, 0x04, 0x53, 0x79, 0x6e, 0x63, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, + 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, + 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, + 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, + 0x61, 0x67, 0x65, 0x22, 0x00, 0x30, 0x01, 0x12, 0x42, 0x0a, 0x0c, 0x47, 0x65, 0x74, 0x53, 0x65, + 0x72, 0x76, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x12, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, + 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1d, 0x2e, 0x6d, 0x61, 0x6e, + 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4b, 0x65, + 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x33, 0x0a, 0x09, 0x69, + 0x73, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x79, 0x12, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, + 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x11, 0x2e, 0x6d, 0x61, + 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, + 0x12, 0x5a, 0x0a, 0x1a, 0x47, 0x65, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, + 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, - 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12, 0x46, 0x0a, 0x04, - 0x53, 0x79, 0x6e, 0x63, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, - 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, - 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, - 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, - 0x22, 0x00, 0x30, 0x01, 0x12, 0x42, 0x0a, 0x0c, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x65, - 0x72, 0x4b, 0x65, 0x79, 0x12, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, - 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1d, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, - 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x33, 0x0a, 0x09, 0x69, 0x73, 0x48, 0x65, - 0x61, 0x6c, 0x74, 0x68, 0x79, 0x12, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, - 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, - 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x5a, 0x0a, - 0x1a, 0x47, 0x65, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, - 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x12, 0x1c, 0x2e, 0x6d, 0x61, - 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, - 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, - 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, - 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x42, 0x08, 0x5a, 0x06, 0x2f, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x42, 0x08, 0x5a, 0x06, + 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -2123,81 +2386,89 @@ func file_management_proto_rawDescGZIP() []byte { return file_management_proto_rawDescData } -var file_management_proto_enumTypes = make([]protoimpl.EnumInfo, 2) -var file_management_proto_msgTypes = make([]protoimpl.MessageInfo, 25) +var file_management_proto_enumTypes = make([]protoimpl.EnumInfo, 5) +var file_management_proto_msgTypes = make([]protoimpl.MessageInfo, 26) var file_management_proto_goTypes = []interface{}{ (HostConfig_Protocol)(0), // 0: management.HostConfig.Protocol (DeviceAuthorizationFlowProvider)(0), // 1: management.DeviceAuthorizationFlow.provider - (*EncryptedMessage)(nil), // 2: management.EncryptedMessage - (*SyncRequest)(nil), // 3: management.SyncRequest - (*SyncResponse)(nil), // 4: management.SyncResponse - (*LoginRequest)(nil), // 5: management.LoginRequest - (*PeerKeys)(nil), // 6: management.PeerKeys - (*PeerSystemMeta)(nil), // 7: management.PeerSystemMeta - (*LoginResponse)(nil), // 8: management.LoginResponse - (*ServerKeyResponse)(nil), // 9: management.ServerKeyResponse - (*Empty)(nil), // 10: management.Empty - (*WiretrusteeConfig)(nil), // 11: management.WiretrusteeConfig - (*HostConfig)(nil), // 12: management.HostConfig - (*ProtectedHostConfig)(nil), // 13: management.ProtectedHostConfig - (*PeerConfig)(nil), // 14: management.PeerConfig - (*NetworkMap)(nil), // 15: management.NetworkMap - (*RemotePeerConfig)(nil), // 16: management.RemotePeerConfig - (*SSHConfig)(nil), // 17: management.SSHConfig - (*DeviceAuthorizationFlowRequest)(nil), // 18: management.DeviceAuthorizationFlowRequest - (*DeviceAuthorizationFlow)(nil), // 19: management.DeviceAuthorizationFlow - (*ProviderConfig)(nil), // 20: management.ProviderConfig - (*Route)(nil), // 21: management.Route - (*DNSConfig)(nil), // 22: management.DNSConfig - (*CustomZone)(nil), // 23: management.CustomZone - (*SimpleRecord)(nil), // 24: management.SimpleRecord - (*NameServerGroup)(nil), // 25: management.NameServerGroup - (*NameServer)(nil), // 26: management.NameServer - (*timestamppb.Timestamp)(nil), // 27: google.protobuf.Timestamp + (FirewallRuleDirection)(0), // 2: management.FirewallRule.direction + (FirewallRuleAction)(0), // 3: management.FirewallRule.action + (FirewallRuleProtocol)(0), // 4: management.FirewallRule.protocol + (*EncryptedMessage)(nil), // 5: management.EncryptedMessage + (*SyncRequest)(nil), // 6: management.SyncRequest + (*SyncResponse)(nil), // 7: management.SyncResponse + (*LoginRequest)(nil), // 8: management.LoginRequest + (*PeerKeys)(nil), // 9: management.PeerKeys + (*PeerSystemMeta)(nil), // 10: management.PeerSystemMeta + (*LoginResponse)(nil), // 11: management.LoginResponse + (*ServerKeyResponse)(nil), // 12: management.ServerKeyResponse + (*Empty)(nil), // 13: management.Empty + (*WiretrusteeConfig)(nil), // 14: management.WiretrusteeConfig + (*HostConfig)(nil), // 15: management.HostConfig + (*ProtectedHostConfig)(nil), // 16: management.ProtectedHostConfig + (*PeerConfig)(nil), // 17: management.PeerConfig + (*NetworkMap)(nil), // 18: management.NetworkMap + (*RemotePeerConfig)(nil), // 19: management.RemotePeerConfig + (*SSHConfig)(nil), // 20: management.SSHConfig + (*DeviceAuthorizationFlowRequest)(nil), // 21: management.DeviceAuthorizationFlowRequest + (*DeviceAuthorizationFlow)(nil), // 22: management.DeviceAuthorizationFlow + (*ProviderConfig)(nil), // 23: management.ProviderConfig + (*Route)(nil), // 24: management.Route + (*DNSConfig)(nil), // 25: management.DNSConfig + (*CustomZone)(nil), // 26: management.CustomZone + (*SimpleRecord)(nil), // 27: management.SimpleRecord + (*NameServerGroup)(nil), // 28: management.NameServerGroup + (*NameServer)(nil), // 29: management.NameServer + (*FirewallRule)(nil), // 30: management.FirewallRule + (*timestamppb.Timestamp)(nil), // 31: google.protobuf.Timestamp } var file_management_proto_depIdxs = []int32{ - 11, // 0: management.SyncResponse.wiretrusteeConfig:type_name -> management.WiretrusteeConfig - 14, // 1: management.SyncResponse.peerConfig:type_name -> management.PeerConfig - 16, // 2: management.SyncResponse.remotePeers:type_name -> management.RemotePeerConfig - 15, // 3: management.SyncResponse.NetworkMap:type_name -> management.NetworkMap - 7, // 4: management.LoginRequest.meta:type_name -> management.PeerSystemMeta - 6, // 5: management.LoginRequest.peerKeys:type_name -> management.PeerKeys - 11, // 6: management.LoginResponse.wiretrusteeConfig:type_name -> management.WiretrusteeConfig - 14, // 7: management.LoginResponse.peerConfig:type_name -> management.PeerConfig - 27, // 8: management.ServerKeyResponse.expiresAt:type_name -> google.protobuf.Timestamp - 12, // 9: management.WiretrusteeConfig.stuns:type_name -> management.HostConfig - 13, // 10: management.WiretrusteeConfig.turns:type_name -> management.ProtectedHostConfig - 12, // 11: management.WiretrusteeConfig.signal:type_name -> management.HostConfig + 14, // 0: management.SyncResponse.wiretrusteeConfig:type_name -> management.WiretrusteeConfig + 17, // 1: management.SyncResponse.peerConfig:type_name -> management.PeerConfig + 19, // 2: management.SyncResponse.remotePeers:type_name -> management.RemotePeerConfig + 18, // 3: management.SyncResponse.NetworkMap:type_name -> management.NetworkMap + 10, // 4: management.LoginRequest.meta:type_name -> management.PeerSystemMeta + 9, // 5: management.LoginRequest.peerKeys:type_name -> management.PeerKeys + 14, // 6: management.LoginResponse.wiretrusteeConfig:type_name -> management.WiretrusteeConfig + 17, // 7: management.LoginResponse.peerConfig:type_name -> management.PeerConfig + 31, // 8: management.ServerKeyResponse.expiresAt:type_name -> google.protobuf.Timestamp + 15, // 9: management.WiretrusteeConfig.stuns:type_name -> management.HostConfig + 16, // 10: management.WiretrusteeConfig.turns:type_name -> management.ProtectedHostConfig + 15, // 11: management.WiretrusteeConfig.signal:type_name -> management.HostConfig 0, // 12: management.HostConfig.protocol:type_name -> management.HostConfig.Protocol - 12, // 13: management.ProtectedHostConfig.hostConfig:type_name -> management.HostConfig - 17, // 14: management.PeerConfig.sshConfig:type_name -> management.SSHConfig - 14, // 15: management.NetworkMap.peerConfig:type_name -> management.PeerConfig - 16, // 16: management.NetworkMap.remotePeers:type_name -> management.RemotePeerConfig - 21, // 17: management.NetworkMap.Routes:type_name -> management.Route - 22, // 18: management.NetworkMap.DNSConfig:type_name -> management.DNSConfig - 16, // 19: management.NetworkMap.offlinePeers:type_name -> management.RemotePeerConfig - 17, // 20: management.RemotePeerConfig.sshConfig:type_name -> management.SSHConfig - 1, // 21: management.DeviceAuthorizationFlow.Provider:type_name -> management.DeviceAuthorizationFlow.provider - 20, // 22: management.DeviceAuthorizationFlow.ProviderConfig:type_name -> management.ProviderConfig - 25, // 23: management.DNSConfig.NameServerGroups:type_name -> management.NameServerGroup - 23, // 24: management.DNSConfig.CustomZones:type_name -> management.CustomZone - 24, // 25: management.CustomZone.Records:type_name -> management.SimpleRecord - 26, // 26: management.NameServerGroup.NameServers:type_name -> management.NameServer - 2, // 27: management.ManagementService.Login:input_type -> management.EncryptedMessage - 2, // 28: management.ManagementService.Sync:input_type -> management.EncryptedMessage - 10, // 29: management.ManagementService.GetServerKey:input_type -> management.Empty - 10, // 30: management.ManagementService.isHealthy:input_type -> management.Empty - 2, // 31: management.ManagementService.GetDeviceAuthorizationFlow:input_type -> management.EncryptedMessage - 2, // 32: management.ManagementService.Login:output_type -> management.EncryptedMessage - 2, // 33: management.ManagementService.Sync:output_type -> management.EncryptedMessage - 9, // 34: management.ManagementService.GetServerKey:output_type -> management.ServerKeyResponse - 10, // 35: management.ManagementService.isHealthy:output_type -> management.Empty - 2, // 36: management.ManagementService.GetDeviceAuthorizationFlow:output_type -> management.EncryptedMessage - 32, // [32:37] is the sub-list for method output_type - 27, // [27:32] is the sub-list for method input_type - 27, // [27:27] is the sub-list for extension type_name - 27, // [27:27] is the sub-list for extension extendee - 0, // [0:27] is the sub-list for field type_name + 15, // 13: management.ProtectedHostConfig.hostConfig:type_name -> management.HostConfig + 20, // 14: management.PeerConfig.sshConfig:type_name -> management.SSHConfig + 17, // 15: management.NetworkMap.peerConfig:type_name -> management.PeerConfig + 19, // 16: management.NetworkMap.remotePeers:type_name -> management.RemotePeerConfig + 24, // 17: management.NetworkMap.Routes:type_name -> management.Route + 25, // 18: management.NetworkMap.DNSConfig:type_name -> management.DNSConfig + 19, // 19: management.NetworkMap.offlinePeers:type_name -> management.RemotePeerConfig + 30, // 20: management.NetworkMap.FirewallRules:type_name -> management.FirewallRule + 20, // 21: management.RemotePeerConfig.sshConfig:type_name -> management.SSHConfig + 1, // 22: management.DeviceAuthorizationFlow.Provider:type_name -> management.DeviceAuthorizationFlow.provider + 23, // 23: management.DeviceAuthorizationFlow.ProviderConfig:type_name -> management.ProviderConfig + 28, // 24: management.DNSConfig.NameServerGroups:type_name -> management.NameServerGroup + 26, // 25: management.DNSConfig.CustomZones:type_name -> management.CustomZone + 27, // 26: management.CustomZone.Records:type_name -> management.SimpleRecord + 29, // 27: management.NameServerGroup.NameServers:type_name -> management.NameServer + 2, // 28: management.FirewallRule.Direction:type_name -> management.FirewallRule.direction + 3, // 29: management.FirewallRule.Action:type_name -> management.FirewallRule.action + 4, // 30: management.FirewallRule.Protocol:type_name -> management.FirewallRule.protocol + 5, // 31: management.ManagementService.Login:input_type -> management.EncryptedMessage + 5, // 32: management.ManagementService.Sync:input_type -> management.EncryptedMessage + 13, // 33: management.ManagementService.GetServerKey:input_type -> management.Empty + 13, // 34: management.ManagementService.isHealthy:input_type -> management.Empty + 5, // 35: management.ManagementService.GetDeviceAuthorizationFlow:input_type -> management.EncryptedMessage + 5, // 36: management.ManagementService.Login:output_type -> management.EncryptedMessage + 5, // 37: management.ManagementService.Sync:output_type -> management.EncryptedMessage + 12, // 38: management.ManagementService.GetServerKey:output_type -> management.ServerKeyResponse + 13, // 39: management.ManagementService.isHealthy:output_type -> management.Empty + 5, // 40: management.ManagementService.GetDeviceAuthorizationFlow:output_type -> management.EncryptedMessage + 36, // [36:41] is the sub-list for method output_type + 31, // [31:36] is the sub-list for method input_type + 31, // [31:31] is the sub-list for extension type_name + 31, // [31:31] is the sub-list for extension extendee + 0, // [0:31] is the sub-list for field type_name } func init() { file_management_proto_init() } @@ -2506,14 +2777,26 @@ func file_management_proto_init() { return nil } } + file_management_proto_msgTypes[25].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*FirewallRule); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_management_proto_rawDesc, - NumEnums: 2, - NumMessages: 25, + NumEnums: 5, + NumMessages: 26, NumExtensions: 0, NumServices: 1, }, diff --git a/management/proto/management.proto b/management/proto/management.proto index 5447a9ee6..d18324033 100644 --- a/management/proto/management.proto +++ b/management/proto/management.proto @@ -186,6 +186,9 @@ message NetworkMap { // RemotePeerConfig represents a list of remote peers that the receiver can connect to repeated RemotePeerConfig offlinePeers = 7; + + // FirewallRule represents a list of firewall rules to be applied to peer + repeated FirewallRule FirewallRules = 8; } // RemotePeerConfig represents a configuration of a remote peer. @@ -297,4 +300,29 @@ message NameServer { string IP = 1; int64 NSType = 2; int64 Port = 3; -} \ No newline at end of file +} + +// FirewallRule represents a firewall rule +message FirewallRule { + string PeerIP = 1; + direction Direction = 2; + action Action = 3; + protocol Protocol = 4; + string Port = 5; + + enum direction { + IN = 0; + OUT = 1; + } + enum action { + ACCEPT = 0; + DROP = 1; + } + enum protocol { + UNKNOWN = 0; + ALL = 1; + TCP = 2; + UDP = 3; + ICMP = 4; + } +} diff --git a/management/server/account.go b/management/server/account.go index a7306e2e7..25ee756ad 100644 --- a/management/server/account.go +++ b/management/server/account.go @@ -286,7 +286,7 @@ func (a *Account) GetGroup(groupID string) *Group { // GetPeerNetworkMap returns a group by ID if exists, nil otherwise func (a *Account) GetPeerNetworkMap(peerID, dnsDomain string) *NetworkMap { - aclPeers := a.getPeersByACL(peerID) + aclPeers, firewallRules := a.getPeerConnectionResources(peerID) // exclude expired peers var peersToConnect []*Peer var expiredPeers []*Peer @@ -317,11 +317,12 @@ func (a *Account) GetPeerNetworkMap(peerID, dnsDomain string) *NetworkMap { } return &NetworkMap{ - Peers: peersToConnect, - Network: a.Network.Copy(), - Routes: routesUpdate, - DNSConfig: dnsUpdate, - OfflinePeers: expiredPeers, + Peers: peersToConnect, + Network: a.Network.Copy(), + Routes: routesUpdate, + DNSConfig: dnsUpdate, + OfflinePeers: expiredPeers, + FirewallRules: firewallRules, } } diff --git a/management/server/account_test.go b/management/server/account_test.go index 716fde187..495e93892 100644 --- a/management/server/account_test.go +++ b/management/server/account_test.go @@ -919,17 +919,14 @@ func TestAccountManager_NetworkUpdates(t *testing.T) { Enabled: true, Rules: []*PolicyRule{ { - Enabled: true, - Sources: []string{"group-id"}, - Destinations: []string{"group-id"}, - Action: PolicyTrafficActionAccept, + Enabled: true, + Sources: []string{"group-id"}, + Destinations: []string{"group-id"}, + Bidirectional: true, + Action: PolicyTrafficActionAccept, }, }, } - if err := policy.UpdateQueryFromRules(); err != nil { - t.Errorf("update policy query from rules: %v", err) - return - } wg := sync.WaitGroup{} t.Run("save group update", func(t *testing.T) { diff --git a/management/server/file_store.go b/management/server/file_store.go index e13a3746a..c39af154a 100644 --- a/management/server/file_store.go +++ b/management/server/file_store.go @@ -129,12 +129,11 @@ func restore(file string) (*FileStore, error) { store.PrivateDomain2AccountID[account.Domain] = accountID } - // TODO: policy query generated from the Go template and rule object. - // We need to refactor this part to avoid using templating for policies queries building - // and drop this migration part. + // TODO: delete this block after migration policies := make(map[string]int, len(account.Policies)) for i, policy := range account.Policies { policies[policy.ID] = i + policy.UpgradeAndFix() } if account.Policies == nil { account.Policies = make([]*Policy, 0) @@ -145,9 +144,9 @@ func restore(file string) (*FileStore, error) { log.Errorf("unable to migrate rule to policy: %v", err) continue } - if i, ok := policies[policy.ID]; ok { - account.Policies[i] = policy - } else { + // don't update policies from rules, rules deprecated, + // only append not existed rules as part of the migration process + if _, ok := policies[policy.ID]; !ok { account.Policies = append(account.Policies, policy) } } diff --git a/management/server/file_store_test.go b/management/server/file_store_test.go index 90c5d6dea..ce035ea83 100644 --- a/management/server/file_store_test.go +++ b/management/server/file_store_test.go @@ -285,10 +285,7 @@ func TestRestorePolicies_Migration(t *testing.T) { require.Equal(t, policy.Description, "This is a default rule that allows connections between all the resources", "failed to restore a FileStore file - missing Account Policies Description") - expectedPolicy := policy.Copy() - err = expectedPolicy.UpdateQueryFromRules() require.NoError(t, err, "failed to upldate query") - require.Equal(t, policy.Query, expectedPolicy.Query, "failed to restore a FileStore file - missing Account Policies Query") require.Len(t, policy.Rules, 1, "failed to restore a FileStore file - missing Account Policy Rules") require.Equal(t, policy.Rules[0].Action, PolicyTrafficActionAccept, "failed to restore a FileStore file - missing Account Policies Action") require.Equal(t, policy.Rules[0].Destinations, diff --git a/management/server/grpcserver.go b/management/server/grpcserver.go index 9c501c345..b1a717502 100644 --- a/management/server/grpcserver.go +++ b/management/server/grpcserver.go @@ -436,6 +436,8 @@ func toSyncResponse(config *Config, peer *Peer, turnCredentials *TURNCredentials offlinePeers := toRemotePeerConfig(networkMap.OfflinePeers, dnsName) + firewallRules := toProtocolFirewallRules(networkMap.FirewallRules) + return &proto.SyncResponse{ WiretrusteeConfig: wtConfig, PeerConfig: pConfig, @@ -449,6 +451,7 @@ func toSyncResponse(config *Config, peer *Peer, turnCredentials *TURNCredentials RemotePeersIsEmpty: len(remotePeers) == 0, Routes: routesUpdate, DNSConfig: dnsUpdate, + FirewallRules: firewallRules, }, } } diff --git a/management/server/http/api/openapi.yml b/management/server/http/api/openapi.yml index bec0e5579..67a2f8f53 100644 --- a/management/server/http/api/openapi.yml +++ b/management/server/http/api/openapi.yml @@ -551,49 +551,91 @@ components: required: - sources - destinations - PolicyRule: + PolicyRuleMinimum: type: object properties: id: - description: Rule ID + description: Policy rule ID type: string example: ch8i4ug6lnn4g9hqv7mg name: - description: Rule name identifier + description: Policy rule name identifier type: string example: Default description: - description: Rule friendly description + description: Policy rule friendly description type: string example: This is a default rule that allows connections between all the resources enabled: - description: Rules status + description: Policy rule status type: boolean - example: true - sources: - description: policy source groups - type: array - items: - $ref: '#/components/schemas/GroupMinimum' - destinations: - description: policy destination groups - type: array - items: - $ref: '#/components/schemas/GroupMinimum' action: - description: policy accept or drops packets + description: Policy rule accept or drops packets type: string enum: ["accept","drop"] - example: accept + bidirectional: + description: Define if the rule is applicable in both directions, sources, and destinations. + type: boolean + example: true + protocol: + description: Policy rule type of the traffic + type: string + enum: ["all", "tcp", "udp", "icmp"] + example: "tcp" + ports: + description: Policy rule affected ports or it ranges list + type: array + items: + type: string + example: [80,443] required: - name - - sources - - destinations - - action - enabled + - bidirectional + - protocol + - action + PolicyRuleUpdate: + allOf: + - $ref: '#/components/schemas/PolicyRuleMinimum' + - type: object + properties: + sources: + description: Policy rule source groups + type: array + items: + type: string + destinations: + description: Policy rule destination groups + type: array + items: + type: string + required: + - sources + - destinations + PolicyRule: + allOf: + - $ref: '#/components/schemas/PolicyRuleMinimum' + - type: object + properties: + sources: + description: Policy rule source groups + type: array + items: + $ref: '#/components/schemas/GroupMinimum' + destinations: + description: Policy rule destination groups + type: array + items: + $ref: '#/components/schemas/GroupMinimum' + required: + - sources + - destinations PolicyMinimum: type: object properties: + id: + description: Policy ID + type: string name: description: Policy name identifier type: string @@ -609,29 +651,35 @@ components: query: description: Policy Rego query type: string - example: package netbird\n\nall[rule] {\n is_peer_in_any_group([\"ch8i4ug6lnn4g9hqv7m0\",\"ch8i4ug6lnn4g9hqv7m0\"])\n rule := {\n rules_from_group(\"ch8i4ug6lnn4g9hqv7m0\", \"dst\", \"accept\", \"\"),\n rules_from_group(\"ch8i4ug6lnn4g9hqv7m0\", \"src\", \"accept\", \"\"),\n }[_][_]\n}\n - rules: - description: Policy rule object for policy UI editor - type: array - items: - $ref: '#/components/schemas/PolicyRule' required: - name - description - enabled - query - - rules + PolicyUpdate: + allOf: + - $ref: '#/components/schemas/PolicyMinimum' + - type: object + properties: + rules: + description: Policy rule object for policy UI editor + type: array + items: + $ref: '#/components/schemas/PolicyRuleUpdate' + required: + - rules Policy: allOf: - $ref: '#/components/schemas/PolicyMinimum' - type: object properties: - id: - description: Policy ID - type: string - example: ch8i4ug6lnn4g9hqv7mg + rules: + description: Policy rule object for policy UI editor + type: array + items: + $ref: '#/components/schemas/PolicyRule' required: - - id + - rules RouteRequest: type: object properties: @@ -884,7 +932,7 @@ security: paths: /api/accounts: get: - summary: List all Accounts + summary: List all accounts description: Returns a list of accounts of a user. Always returns a list of one account. tags: [ Accounts ] security: @@ -909,7 +957,7 @@ paths: "$ref": "#/components/responses/internal_error" /api/accounts/{accountId}: put: - summary: Update an Account + summary: Update an account description: Update information about an account tags: [ Accounts ] security: @@ -950,7 +998,7 @@ paths: "$ref": "#/components/responses/internal_error" /api/users: get: - summary: List all Users + summary: List all users description: Returns a list of all users tags: [ Users ] security: @@ -980,7 +1028,7 @@ paths: '500': "$ref": "#/components/responses/internal_error" post: - summary: Create a User + summary: Create a user description: Creates a new service user or sends an invite to a regular user tags: [ Users ] security: @@ -1009,7 +1057,7 @@ paths: "$ref": "#/components/responses/internal_error" /api/users/{userId}: put: - summary: Update a User + summary: Update a user description: Update information about a User tags: [ Users ] security: @@ -1044,8 +1092,8 @@ paths: '500': "$ref": "#/components/responses/internal_error" delete: - summary: Delete a User - description: Delete a User + summary: Delete a user + description: Delete a user tags: [ Users ] security: - BearerAuth: [ ] @@ -1071,7 +1119,7 @@ paths: "$ref": "#/components/responses/internal_error" /api/users/{userId}/tokens: get: - summary: List all Tokens + summary: List all tokens description: Returns a list of all tokens for a user tags: [ Tokens ] security: @@ -1102,7 +1150,7 @@ paths: '500': "$ref": "#/components/responses/internal_error" post: - summary: Create a Token + summary: Create a token description: Create a new token for a user tags: [ Tokens ] security: @@ -1138,7 +1186,7 @@ paths: "$ref": "#/components/responses/internal_error" /api/users/{userId}/tokens/{tokenId}: get: - summary: Retrieve a Token + summary: Retrieve a token description: Returns a specific token for a user tags: [ Tokens ] security: @@ -1173,7 +1221,7 @@ paths: '500': "$ref": "#/components/responses/internal_error" delete: - summary: Delete a Token + summary: Delete a token description: Delete a token for a user tags: [ Tokens ] security: @@ -1206,7 +1254,7 @@ paths: "$ref": "#/components/responses/internal_error" /api/peers: get: - summary: List all Peers + summary: List all peers description: Returns a list of all peers tags: [ Peers ] security: @@ -1231,7 +1279,7 @@ paths: "$ref": "#/components/responses/internal_error" /api/peers/{peerId}: get: - summary: Retrieve a Peer + summary: Retrieve a peer description: Get information about a peer tags: [ Peers ] security: @@ -1260,7 +1308,7 @@ paths: '500': "$ref": "#/components/responses/internal_error" put: - summary: Update a Peer + summary: Update a peer description: Update information about a peer tags: [ Peers ] security: @@ -1295,7 +1343,7 @@ paths: '500': "$ref": "#/components/responses/internal_error" delete: - summary: Delete a Peer + summary: Delete a peer description: Delete a peer tags: [ Peers ] security: @@ -1322,7 +1370,7 @@ paths: "$ref": "#/components/responses/internal_error" /api/setup-keys: get: - summary: List all Setup Keys + summary: List all setup keys description: Returns a list of all Setup Keys tags: [ Setup Keys ] security: @@ -1346,8 +1394,8 @@ paths: '500': "$ref": "#/components/responses/internal_error" post: - summary: Create a Setup Key - description: Creates a Setup Key + summary: Create a setup key + description: Creates a setup key tags: [ Setup Keys ] security: - BearerAuth: [ ] @@ -1375,8 +1423,8 @@ paths: "$ref": "#/components/responses/internal_error" /api/setup-keys/{keyId}: get: - summary: Retrieve a Setup Key - description: Get information about a Setup Key + summary: Retrieve a setup key + description: Get information about a setup key tags: [ Setup Keys ] security: - BearerAuth: [ ] @@ -1404,8 +1452,8 @@ paths: '500': "$ref": "#/components/responses/internal_error" put: - summary: Update a Setup Key - description: Update information about a Setup Key + summary: Update a setup key + description: Update information about a setup key tags: [ Setup Keys ] security: - BearerAuth: [ ] @@ -1440,8 +1488,8 @@ paths: "$ref": "#/components/responses/internal_error" /api/groups: get: - summary: List all Groups - description: Returns a list of all Groups + summary: List all groups + description: Returns a list of all groups tags: [ Groups ] security: - BearerAuth: [ ] @@ -1464,8 +1512,8 @@ paths: '500': "$ref": "#/components/responses/internal_error" post: - summary: Create a Group - description: Creates a Group + summary: Create a group + description: Creates a group tags: [ Groups ] security: - BearerAuth: [ ] @@ -1493,8 +1541,8 @@ paths: "$ref": "#/components/responses/internal_error" /api/groups/{groupId}: get: - summary: Retrieve a Group - description: Get information about a Group + summary: Retrieve a group + description: Get information about a group tags: [ Groups ] security: - BearerAuth: [ ] @@ -1522,8 +1570,8 @@ paths: '500': "$ref": "#/components/responses/internal_error" put: - summary: Update a Group - description: Update/Replace a Group + summary: Update a group + description: Update/Replace a group tags: [ Groups ] security: - BearerAuth: [ ] @@ -1558,7 +1606,7 @@ paths: "$ref": "#/components/responses/internal_error" delete: summary: Delete a Group - description: Delete a Group + description: Delete a group tags: [ Groups ] security: - BearerAuth: [ ] @@ -1584,8 +1632,8 @@ paths: "$ref": "#/components/responses/internal_error" /api/rules: get: - summary: List all Rules - description: Returns a list of all Rules + summary: List all rules + description: Returns a list of all rules tags: [ Rules ] security: - BearerAuth: [ ] @@ -1608,8 +1656,8 @@ paths: '500': "$ref": "#/components/responses/internal_error" post: - summary: Create a Rule - description: Creates a Rule + summary: Create a rule + description: Creates a rule tags: [ Rules ] security: - BearerAuth: [ ] @@ -1629,8 +1677,8 @@ paths: $ref: '#/components/schemas/Rule' /api/rules/{ruleId}: get: - summary: Retrieve a Rule - description: Get information about a Rules + summary: Retrieve a rule + description: Get information about a rules tags: [ Rules ] security: - BearerAuth: [ ] @@ -1658,8 +1706,8 @@ paths: '500': "$ref": "#/components/responses/internal_error" put: - summary: Update a Rule - description: Update/Replace a Rule + summary: Update a rule + description: Update/Replace a rule tags: [ Rules ] security: - BearerAuth: [ ] @@ -1693,8 +1741,8 @@ paths: '500': "$ref": "#/components/responses/internal_error" delete: - summary: Delete a Rule - description: Delete a Rule + summary: Delete a rule + description: Delete a rule tags: [ Rules ] security: - BearerAuth: [ ] @@ -1720,8 +1768,8 @@ paths: "$ref": "#/components/responses/internal_error" /api/policies: get: - summary: List all Policies - description: Returns a list of all Policies + summary: List all policies + description: Returns a list of all policies tags: [ Policies ] security: - BearerAuth: [ ] @@ -1744,8 +1792,8 @@ paths: '500': "$ref": "#/components/responses/internal_error" post: - summary: Create a Policy - description: Creates a Policy + summary: Create a policy + description: Creates a policy tags: [ Policies ] security: - BearerAuth: [ ] @@ -1755,7 +1803,7 @@ paths: content: 'application/json': schema: - $ref: '#/components/schemas/PolicyMinimum' + $ref: '#/components/schemas/PolicyUpdate' responses: '200': description: A Policy Object @@ -1765,7 +1813,7 @@ paths: $ref: '#/components/schemas/Policy' /api/policies/{policyId}: get: - summary: Retrieve a Policy + summary: Retrieve a policy description: Get information about a Policies tags: [ Policies ] security: @@ -1794,7 +1842,7 @@ paths: '500': "$ref": "#/components/responses/internal_error" put: - summary: Update a Policy + summary: Update a policy description: Update/Replace a Policy tags: [ Policies ] security: @@ -1812,7 +1860,7 @@ paths: content: 'application/json': schema: - $ref: '#/components/schemas/PolicyMinimum' + $ref: '#/components/schemas/PolicyUpdate' responses: '200': description: A Policy object @@ -1830,7 +1878,7 @@ paths: "$ref": "#/components/responses/internal_error" delete: summary: Delete a Policy - description: Delete a Policy + description: Delete a policy tags: [ Policies ] security: - BearerAuth: [ ] @@ -1856,7 +1904,7 @@ paths: "$ref": "#/components/responses/internal_error" /api/routes: get: - summary: List all Routes + summary: List all routes description: Returns a list of all routes tags: [ Routes ] security: @@ -1880,7 +1928,7 @@ paths: '500': "$ref": "#/components/responses/internal_error" post: - summary: Create a Route + summary: Create a route description: Creates a Route tags: [ Routes ] security: @@ -1910,7 +1958,7 @@ paths: /api/routes/{routeId}: get: - summary: Retrieve a Route + summary: Retrieve a route description: Get information about a Routes tags: [ Routes ] security: @@ -1939,7 +1987,7 @@ paths: '500': "$ref": "#/components/responses/internal_error" put: - summary: Update a Route + summary: Update a route description: Update/Replace a Route tags: [ Routes ] security: @@ -1975,7 +2023,7 @@ paths: "$ref": "#/components/responses/internal_error" delete: summary: Delete a Route - description: Delete a Route + description: Delete a route tags: [ Routes ] security: - BearerAuth: [ ] @@ -2001,7 +2049,7 @@ paths: "$ref": "#/components/responses/internal_error" /api/dns/nameservers: get: - summary: List all Nameserver Groups + summary: List all nameserver groups description: Returns a list of all Nameserver Groups tags: [ DNS ] security: @@ -2025,7 +2073,7 @@ paths: '500': "$ref": "#/components/responses/internal_error" post: - summary: Create a Nameserver Group + summary: Create a nameserver group description: Creates a Nameserver Group tags: [ DNS ] security: @@ -2052,9 +2100,10 @@ paths: "$ref": "#/components/responses/forbidden" '500': "$ref": "#/components/responses/internal_error" + /api/dns/nameservers/{nsgroupId}: get: - summary: Retrieve a Nameserver Group + summary: Retrieve a nameserver group description: Get information about a Nameserver Groups tags: [ DNS ] security: @@ -2083,7 +2132,7 @@ paths: '500': "$ref": "#/components/responses/internal_error" put: - summary: Update a Nameserver Group + summary: Update a nameserver group description: Update/Replace a Nameserver Group tags: [ DNS ] security: @@ -2118,7 +2167,7 @@ paths: '500': "$ref": "#/components/responses/internal_error" delete: - summary: Delete a Nameserver Group + summary: Delete a nameserver group description: Delete a Nameserver Group tags: [ DNS ] security: @@ -2143,9 +2192,10 @@ paths: "$ref": "#/components/responses/forbidden" '500': "$ref": "#/components/responses/internal_error" + /api/dns/settings: get: - summary: Retrieve DNS Settings + summary: Retrieve DNS settings description: Returns a DNS settings object tags: [ DNS ] security: @@ -2168,7 +2218,7 @@ paths: '500': "$ref": "#/components/responses/internal_error" put: - summary: Update DNS Settings + summary: Update DNS settings description: Updates a DNS settings object tags: [ DNS ] security: @@ -2197,7 +2247,7 @@ paths: "$ref": "#/components/responses/internal_error" /api/events: get: - summary: List all Events + summary: List all events description: Returns a list of all events tags: [ Events ] security: diff --git a/management/server/http/api/types.gen.go b/management/server/http/api/types.gen.go index c93071185..9c67425c4 100644 --- a/management/server/http/api/types.gen.go +++ b/management/server/http/api/types.gen.go @@ -72,6 +72,42 @@ const ( PolicyRuleActionDrop PolicyRuleAction = "drop" ) +// Defines values for PolicyRuleProtocol. +const ( + PolicyRuleProtocolAll PolicyRuleProtocol = "all" + PolicyRuleProtocolIcmp PolicyRuleProtocol = "icmp" + PolicyRuleProtocolTcp PolicyRuleProtocol = "tcp" + PolicyRuleProtocolUdp PolicyRuleProtocol = "udp" +) + +// Defines values for PolicyRuleMinimumAction. +const ( + PolicyRuleMinimumActionAccept PolicyRuleMinimumAction = "accept" + PolicyRuleMinimumActionDrop PolicyRuleMinimumAction = "drop" +) + +// Defines values for PolicyRuleMinimumProtocol. +const ( + PolicyRuleMinimumProtocolAll PolicyRuleMinimumProtocol = "all" + PolicyRuleMinimumProtocolIcmp PolicyRuleMinimumProtocol = "icmp" + PolicyRuleMinimumProtocolTcp PolicyRuleMinimumProtocol = "tcp" + PolicyRuleMinimumProtocolUdp PolicyRuleMinimumProtocol = "udp" +) + +// Defines values for PolicyRuleUpdateAction. +const ( + PolicyRuleUpdateActionAccept PolicyRuleUpdateAction = "accept" + PolicyRuleUpdateActionDrop PolicyRuleUpdateAction = "drop" +) + +// Defines values for PolicyRuleUpdateProtocol. +const ( + PolicyRuleUpdateProtocolAll PolicyRuleUpdateProtocol = "all" + PolicyRuleUpdateProtocolIcmp PolicyRuleUpdateProtocol = "icmp" + PolicyRuleUpdateProtocolTcp PolicyRuleUpdateProtocol = "tcp" + PolicyRuleUpdateProtocolUdp PolicyRuleUpdateProtocol = "udp" +) + // Defines values for UserStatus. const ( UserStatusActive UserStatus = "active" @@ -344,7 +380,7 @@ type Policy struct { Enabled bool `json:"enabled"` // Id Policy ID - Id string `json:"id"` + Id *string `json:"id,omitempty"` // Name Policy name identifier Name string `json:"name"` @@ -364,6 +400,138 @@ type PolicyMinimum struct { // Enabled Policy status Enabled bool `json:"enabled"` + // Id Policy ID + Id *string `json:"id,omitempty"` + + // Name Policy name identifier + Name string `json:"name"` + + // Query Policy Rego query + Query string `json:"query"` +} + +// PolicyRule defines model for PolicyRule. +type PolicyRule struct { + // Action Policy rule accept or drops packets + Action PolicyRuleAction `json:"action"` + + // Bidirectional Define if the rule is applicable in both directions, sources, and destinations. + Bidirectional bool `json:"bidirectional"` + + // Description Policy rule friendly description + Description *string `json:"description,omitempty"` + + // Destinations Policy rule destination groups + Destinations []GroupMinimum `json:"destinations"` + + // Enabled Policy rule status + Enabled bool `json:"enabled"` + + // Id Policy rule ID + Id *string `json:"id,omitempty"` + + // Name Policy rule name identifier + Name string `json:"name"` + + // Ports Policy rule affected ports or it ranges list + Ports *[]string `json:"ports,omitempty"` + + // Protocol Policy rule type of the traffic + Protocol PolicyRuleProtocol `json:"protocol"` + + // Sources Policy rule source groups + Sources []GroupMinimum `json:"sources"` +} + +// PolicyRuleAction Policy rule accept or drops packets +type PolicyRuleAction string + +// PolicyRuleProtocol Policy rule type of the traffic +type PolicyRuleProtocol string + +// PolicyRuleMinimum defines model for PolicyRuleMinimum. +type PolicyRuleMinimum struct { + // Action Policy rule accept or drops packets + Action PolicyRuleMinimumAction `json:"action"` + + // Bidirectional Define if the rule is applicable in both directions, sources, and destinations. + Bidirectional bool `json:"bidirectional"` + + // Description Policy rule friendly description + Description *string `json:"description,omitempty"` + + // Enabled Policy rule status + Enabled bool `json:"enabled"` + + // Id Policy rule ID + Id *string `json:"id,omitempty"` + + // Name Policy rule name identifier + Name string `json:"name"` + + // Ports Policy rule affected ports or it ranges list + Ports *[]string `json:"ports,omitempty"` + + // Protocol Policy rule type of the traffic + Protocol PolicyRuleMinimumProtocol `json:"protocol"` +} + +// PolicyRuleMinimumAction Policy rule accept or drops packets +type PolicyRuleMinimumAction string + +// PolicyRuleMinimumProtocol Policy rule type of the traffic +type PolicyRuleMinimumProtocol string + +// PolicyRuleUpdate defines model for PolicyRuleUpdate. +type PolicyRuleUpdate struct { + // Action Policy rule accept or drops packets + Action PolicyRuleUpdateAction `json:"action"` + + // Bidirectional Define if the rule is applicable in both directions, sources, and destinations. + Bidirectional bool `json:"bidirectional"` + + // Description Policy rule friendly description + Description *string `json:"description,omitempty"` + + // Destinations Policy rule destination groups + Destinations []string `json:"destinations"` + + // Enabled Policy rule status + Enabled bool `json:"enabled"` + + // Id Policy rule ID + Id *string `json:"id,omitempty"` + + // Name Policy rule name identifier + Name string `json:"name"` + + // Ports Policy rule affected ports or it ranges list + Ports *[]string `json:"ports,omitempty"` + + // Protocol Policy rule type of the traffic + Protocol PolicyRuleUpdateProtocol `json:"protocol"` + + // Sources Policy rule source groups + Sources []string `json:"sources"` +} + +// PolicyRuleUpdateAction Policy rule accept or drops packets +type PolicyRuleUpdateAction string + +// PolicyRuleUpdateProtocol Policy rule type of the traffic +type PolicyRuleUpdateProtocol string + +// PolicyUpdate defines model for PolicyUpdate. +type PolicyUpdate struct { + // Description Policy friendly description + Description string `json:"description"` + + // Enabled Policy status + Enabled bool `json:"enabled"` + + // Id Policy ID + Id *string `json:"id,omitempty"` + // Name Policy name identifier Name string `json:"name"` @@ -371,36 +539,9 @@ type PolicyMinimum struct { Query string `json:"query"` // Rules Policy rule object for policy UI editor - Rules []PolicyRule `json:"rules"` + Rules []PolicyRuleUpdate `json:"rules"` } -// PolicyRule defines model for PolicyRule. -type PolicyRule struct { - // Action policy accept or drops packets - Action PolicyRuleAction `json:"action"` - - // Description Rule friendly description - Description *string `json:"description,omitempty"` - - // Destinations policy destination groups - Destinations []GroupMinimum `json:"destinations"` - - // Enabled Rules status - Enabled bool `json:"enabled"` - - // Id Rule ID - Id *string `json:"id,omitempty"` - - // Name Rule name identifier - Name string `json:"name"` - - // Sources policy source groups - Sources []GroupMinimum `json:"sources"` -} - -// PolicyRuleAction policy accept or drops packets -type PolicyRuleAction string - // Route defines model for Route. type Route struct { // Description Route description @@ -680,10 +821,10 @@ type PutApiGroupsGroupIdJSONRequestBody = GroupRequest type PutApiPeersPeerIdJSONRequestBody = PeerRequest // PostApiPoliciesJSONRequestBody defines body for PostApiPolicies for application/json ContentType. -type PostApiPoliciesJSONRequestBody = PolicyMinimum +type PostApiPoliciesJSONRequestBody = PolicyUpdate // PutApiPoliciesPolicyIdJSONRequestBody defines body for PutApiPoliciesPolicyId for application/json ContentType. -type PutApiPoliciesPolicyIdJSONRequestBody = PolicyMinimum +type PutApiPoliciesPolicyIdJSONRequestBody = PolicyUpdate // PostApiRoutesJSONRequestBody defines body for PostApiRoutes for application/json ContentType. type PostApiRoutesJSONRequestBody = RouteRequest diff --git a/management/server/http/policies_handler.go b/management/server/http/policies_handler.go index 801442b39..2c83c2d1e 100644 --- a/management/server/http/policies_handler.go +++ b/management/server/http/policies_handler.go @@ -6,7 +6,6 @@ import ( "github.com/gorilla/mux" "github.com/rs/xid" - log "github.com/sirupsen/logrus" "github.com/netbirdio/netbird/management/server" "github.com/netbirdio/netbird/management/server/http/api" @@ -47,7 +46,17 @@ func (h *Policies) GetAllPolicies(w http.ResponseWriter, r *http.Request) { return } - util.WriteJSONObject(w, accountPolicies) + policies := []*api.Policy{} + for _, policy := range accountPolicies { + resp := toPolicyResponse(account, policy) + if len(resp.Rules) == 0 { + util.WriteError(status.Errorf(status.Internal, "no rules in the policy"), w) + return + } + policies = append(policies, resp) + } + + util.WriteJSONObject(w, policies) } // UpdatePolicy handles update to a policy identified by a given ID @@ -78,63 +87,7 @@ func (h *Policies) UpdatePolicy(w http.ResponseWriter, r *http.Request) { return } - var req api.PutApiPoliciesPolicyIdJSONRequestBody - err = json.NewDecoder(r.Body).Decode(&req) - if err != nil { - util.WriteErrorResponse("couldn't parse JSON request", http.StatusBadRequest, w) - return - } - - if req.Name == "" { - util.WriteError(status.Errorf(status.InvalidArgument, "policy name shouldn't be empty"), w) - return - } - - policy := server.Policy{ - ID: policyID, - Name: req.Name, - Enabled: req.Enabled, - Description: req.Description, - Query: req.Query, - } - if req.Rules != nil { - for _, r := range req.Rules { - pr := server.PolicyRule{ - Destinations: groupMinimumsToStrings(account, r.Destinations), - Sources: groupMinimumsToStrings(account, r.Sources), - Name: r.Name, - } - pr.Enabled = r.Enabled - if r.Description != nil { - pr.Description = *r.Description - } - if r.Id != nil { - pr.ID = *r.Id - } - switch r.Action { - case api.PolicyRuleActionAccept: - pr.Action = server.PolicyTrafficActionAccept - case api.PolicyRuleActionDrop: - pr.Action = server.PolicyTrafficActionDrop - default: - util.WriteError(status.Errorf(status.InvalidArgument, "unknown action type"), w) - return - } - policy.Rules = append(policy.Rules, &pr) - } - } - if err := policy.UpdateQueryFromRules(); err != nil { - log.Errorf("failed to update policy query: %v", err) - util.WriteError(err, w) - return - } - - if err = h.accountManager.SavePolicy(account.Id, user.Id, &policy); err != nil { - util.WriteError(err, w) - return - } - - util.WriteJSONObject(w, toPolicyResponse(account, &policy)) + h.savePolicy(w, r, account, user, policyID) } // CreatePolicy handles policy creation request @@ -146,9 +99,19 @@ func (h *Policies) CreatePolicy(w http.ResponseWriter, r *http.Request) { return } - var req api.PostApiPoliciesJSONRequestBody - err = json.NewDecoder(r.Body).Decode(&req) - if err != nil { + h.savePolicy(w, r, account, user, "") +} + +// savePolicy handles policy creation and update +func (h *Policies) savePolicy( + w http.ResponseWriter, + r *http.Request, + account *server.Account, + user *server.User, + policyID string, +) { + var req api.PutApiPoliciesPolicyIdJSONRequestBody + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { util.WriteErrorResponse("couldn't parse JSON request", http.StatusBadRequest, w) return } @@ -158,49 +121,97 @@ func (h *Policies) CreatePolicy(w http.ResponseWriter, r *http.Request) { return } - policy := &server.Policy{ - ID: xid.New().String(), + if len(req.Rules) == 0 { + util.WriteError(status.Errorf(status.InvalidArgument, "policy rules shouldn't be empty"), w) + return + } + + if policyID == "" { + policyID = xid.New().String() + } + + policy := server.Policy{ + ID: policyID, Name: req.Name, Enabled: req.Enabled, Description: req.Description, - Query: req.Query, } + for _, r := range req.Rules { + pr := server.PolicyRule{ + ID: policyID, //TODO: when policy can contain multiple rules, need refactor + Name: r.Name, + Destinations: groupMinimumsToStrings(account, r.Destinations), + Sources: groupMinimumsToStrings(account, r.Sources), + Bidirectional: r.Bidirectional, + } - if req.Rules != nil { - for _, r := range req.Rules { - pr := server.PolicyRule{ - ID: xid.New().String(), - Destinations: groupMinimumsToStrings(account, r.Destinations), - Sources: groupMinimumsToStrings(account, r.Sources), - Name: r.Name, - } - pr.Enabled = r.Enabled - if r.Description != nil { - pr.Description = *r.Description - } - switch r.Action { - case api.PolicyRuleActionAccept: - pr.Action = server.PolicyTrafficActionAccept - case api.PolicyRuleActionDrop: - pr.Action = server.PolicyTrafficActionDrop - default: - util.WriteError(status.Errorf(status.InvalidArgument, "unknown action type"), w) + pr.Enabled = r.Enabled + if r.Description != nil { + pr.Description = *r.Description + } + + switch r.Action { + case api.PolicyRuleUpdateActionAccept: + pr.Action = server.PolicyTrafficActionAccept + case api.PolicyRuleUpdateActionDrop: + pr.Action = server.PolicyTrafficActionDrop + default: + util.WriteError(status.Errorf(status.InvalidArgument, "unknown action type"), w) + return + } + + switch r.Protocol { + case api.PolicyRuleUpdateProtocolAll: + pr.Protocol = server.PolicyRuleProtocolALL + case api.PolicyRuleUpdateProtocolTcp: + pr.Protocol = server.PolicyRuleProtocolTCP + case api.PolicyRuleUpdateProtocolUdp: + pr.Protocol = server.PolicyRuleProtocolUDP + case api.PolicyRuleUpdateProtocolIcmp: + pr.Protocol = server.PolicyRuleProtocolICMP + default: + util.WriteError(status.Errorf(status.InvalidArgument, "unknown protocol type: %v", r.Protocol), w) + return + } + + if r.Ports != nil && len(*r.Ports) != 0 { + ports := *r.Ports + pr.Ports = ports[:] + } + + // validate policy object + switch pr.Protocol { + case server.PolicyRuleProtocolALL, server.PolicyRuleProtocolICMP: + if len(pr.Ports) != 0 { + util.WriteError(status.Errorf(status.InvalidArgument, "for ALL or ICMP protocol ports is not allowed"), w) + return + } + if !pr.Bidirectional { + util.WriteError(status.Errorf(status.InvalidArgument, "for ALL or ICMP protocol type flow can be only bi-directional"), w) + return + } + case server.PolicyRuleProtocolTCP, server.PolicyRuleProtocolUDP: + if !pr.Bidirectional && len(pr.Ports) == 0 { + util.WriteError(status.Errorf(status.InvalidArgument, "for ALL or ICMP protocol type flow can be only bi-directional"), w) return } - policy.Rules = append(policy.Rules, &pr) } + + policy.Rules = append(policy.Rules, &pr) } - if err := policy.UpdateQueryFromRules(); err != nil { + + if err := h.accountManager.SavePolicy(account.Id, user.Id, &policy); err != nil { util.WriteError(err, w) return } - if err = h.accountManager.SavePolicy(account.Id, user.Id, policy); err != nil { - util.WriteError(err, w) + resp := toPolicyResponse(account, &policy) + if len(resp.Rules) == 0 { + util.WriteError(status.Errorf(status.Internal, "no rules in the policy"), w) return } - util.WriteJSONObject(w, toPolicyResponse(account, policy)) + util.WriteJSONObject(w, resp) } // DeletePolicy handles policy deletion request @@ -252,7 +263,13 @@ func (h *Policies) GetPolicy(w http.ResponseWriter, r *http.Request) { return } - util.WriteJSONObject(w, toPolicyResponse(account, policy)) + resp := toPolicyResponse(account, policy) + if len(resp.Rules) == 0 { + util.WriteError(status.Errorf(status.Internal, "no rules in the policy"), w) + return + } + + util.WriteJSONObject(w, resp) default: util.WriteError(status.Errorf(status.NotFound, "method not found"), w) } @@ -261,22 +278,24 @@ func (h *Policies) GetPolicy(w http.ResponseWriter, r *http.Request) { func toPolicyResponse(account *server.Account, policy *server.Policy) *api.Policy { cache := make(map[string]api.GroupMinimum) ap := &api.Policy{ - Id: policy.ID, + Id: &policy.ID, Name: policy.Name, Description: policy.Description, Enabled: policy.Enabled, - Query: policy.Query, } - if len(policy.Rules) == 0 { - return ap - } - for _, r := range policy.Rules { rule := api.PolicyRule{ - Id: &r.ID, - Name: r.Name, - Enabled: r.Enabled, - Description: &r.Description, + Id: &r.ID, + Name: r.Name, + Enabled: r.Enabled, + Description: &r.Description, + Bidirectional: r.Bidirectional, + Protocol: api.PolicyRuleProtocol(r.Protocol), + Action: api.PolicyRuleAction(r.Action), + } + if len(r.Ports) != 0 { + portsCopy := r.Ports[:] + rule.Ports = &portsCopy } for _, gid := range r.Sources { _, ok := cache[gid] @@ -314,13 +333,13 @@ func toPolicyResponse(account *server.Account, policy *server.Policy) *api.Polic return ap } -func groupMinimumsToStrings(account *server.Account, gm []api.GroupMinimum) []string { +func groupMinimumsToStrings(account *server.Account, gm []string) []string { result := make([]string, 0, len(gm)) - for _, gm := range gm { - if _, ok := account.Groups[gm.Id]; ok { + for _, g := range gm { + if _, ok := account.Groups[g]; !ok { continue } - result = append(result, gm.Id) + result = append(result, g) } return result } diff --git a/management/server/http/policies_handler_test.go b/management/server/http/policies_handler_test.go new file mode 100644 index 000000000..86665848b --- /dev/null +++ b/management/server/http/policies_handler_test.go @@ -0,0 +1,315 @@ +package http + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/netbirdio/netbird/management/server/http/api" + "github.com/netbirdio/netbird/management/server/status" + + "github.com/gorilla/mux" + + "github.com/netbirdio/netbird/management/server/jwtclaims" + + "github.com/magiconair/properties/assert" + + "github.com/netbirdio/netbird/management/server" + "github.com/netbirdio/netbird/management/server/mock_server" +) + +func initPoliciesTestData(policies ...*server.Policy) *Policies { + testPolicies := make(map[string]*server.Policy, len(policies)) + for _, policy := range policies { + testPolicies[policy.ID] = policy + } + return &Policies{ + accountManager: &mock_server.MockAccountManager{ + GetPolicyFunc: func(_, policyID, _ string) (*server.Policy, error) { + policy, ok := testPolicies[policyID] + if !ok { + return nil, status.Errorf(status.NotFound, "policy not found") + } + return policy, nil + }, + SavePolicyFunc: func(_, _ string, policy *server.Policy) error { + if !strings.HasPrefix(policy.ID, "id-") { + policy.ID = "id-was-set" + policy.Rules[0].ID = "id-was-set" + } + return nil + }, + SaveRuleFunc: func(_, _ string, rule *server.Rule) error { + if !strings.HasPrefix(rule.ID, "id-") { + rule.ID = "id-was-set" + } + return nil + }, + GetRuleFunc: func(_, ruleID, _ string) (*server.Rule, error) { + if ruleID != "idoftherule" { + return nil, fmt.Errorf("not found") + } + return &server.Rule{ + ID: "idoftherule", + Name: "Rule", + Source: []string{"idofsrcrule"}, + Destination: []string{"idofdestrule"}, + Flow: server.TrafficFlowBidirect, + }, nil + }, + GetAccountFromTokenFunc: func(claims jwtclaims.AuthorizationClaims) (*server.Account, *server.User, error) { + user := server.NewAdminUser("test_user") + return &server.Account{ + Id: claims.AccountId, + Domain: "hotmail.com", + Policies: []*server.Policy{ + {ID: "id-existed"}, + }, + Groups: map[string]*server.Group{ + "F": {ID: "F"}, + "G": {ID: "G"}, + }, + Users: map[string]*server.User{ + "test_user": user, + }, + }, user, nil + }, + }, + claimsExtractor: jwtclaims.NewClaimsExtractor( + jwtclaims.WithFromRequestContext(func(r *http.Request) jwtclaims.AuthorizationClaims { + return jwtclaims.AuthorizationClaims{ + UserId: "test_user", + Domain: "hotmail.com", + AccountId: "test_id", + } + }), + ), + } +} + +func TestPoliciesGetPolicy(t *testing.T) { + tt := []struct { + name string + expectedStatus int + expectedBody bool + requestType string + requestPath string + requestBody io.Reader + }{ + { + name: "GetPolicy OK", + expectedBody: true, + requestType: http.MethodGet, + requestPath: "/api/policies/idofthepolicy", + expectedStatus: http.StatusOK, + }, + { + name: "GetPolicy not found", + requestType: http.MethodGet, + requestPath: "/api/policies/notexists", + expectedStatus: http.StatusNotFound, + }, + } + + policy := &server.Policy{ + ID: "idofthepolicy", + Name: "Rule", + Rules: []*server.PolicyRule{ + {ID: "idoftherule", Name: "Rule"}, + }, + } + + p := initPoliciesTestData(policy) + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + recorder := httptest.NewRecorder() + req := httptest.NewRequest(tc.requestType, tc.requestPath, tc.requestBody) + + router := mux.NewRouter() + router.HandleFunc("/api/policies/{policyId}", p.GetPolicy).Methods("GET") + router.ServeHTTP(recorder, req) + + res := recorder.Result() + defer res.Body.Close() + + if status := recorder.Code; status != tc.expectedStatus { + t.Errorf("handler returned wrong status code: got %v want %v", + status, tc.expectedStatus) + return + } + + if !tc.expectedBody { + return + } + + content, err := io.ReadAll(res.Body) + if err != nil { + t.Fatalf("I don't know what I expected; %v", err) + } + + var got api.Policy + if err = json.Unmarshal(content, &got); err != nil { + t.Fatalf("Sent content is not in correct json format; %v", err) + } + + assert.Equal(t, *got.Id, policy.ID) + assert.Equal(t, got.Name, policy.Name) + }) + } +} + +func TestPoliciesWritePolicy(t *testing.T) { + str := func(s string) *string { return &s } + tt := []struct { + name string + expectedStatus int + expectedBody bool + expectedPolicy *api.Policy + requestType string + requestPath string + requestBody io.Reader + }{ + { + name: "WritePolicy POST OK", + requestType: http.MethodPost, + requestPath: "/api/policies", + requestBody: bytes.NewBuffer( + []byte(`{ + "Name":"Default POSTed Policy", + "Rules":[ + { + "Name":"Default POSTed Policy", + "Description": "Description", + "Protocol": "tcp", + "Action": "accept", + "Bidirectional":true + } + ]}`)), + expectedStatus: http.StatusOK, + expectedBody: true, + expectedPolicy: &api.Policy{ + Id: str("id-was-set"), + Name: "Default POSTed Policy", + Rules: []api.PolicyRule{ + { + Id: str("id-was-set"), + Name: "Default POSTed Policy", + Description: str("Description"), + Protocol: "tcp", + Action: "accept", + Bidirectional: true, + }, + }, + }, + }, + { + name: "WritePolicy POST Invalid Name", + requestType: http.MethodPost, + requestPath: "/api/policies", + requestBody: bytes.NewBuffer( + []byte(`{"Name":""}`)), + expectedStatus: http.StatusUnprocessableEntity, + expectedBody: false, + }, + { + name: "WritePolicy PUT OK", + requestType: http.MethodPut, + requestPath: "/api/policies/id-existed", + requestBody: bytes.NewBuffer( + []byte(`{ + "ID": "id-existed", + "Name":"Default POSTed Policy", + "Rules":[ + { + "ID": "id-existed", + "Name":"Default POSTed Policy", + "Description": "Description", + "Protocol": "tcp", + "Action": "accept", + "Bidirectional":true + } + ]}`)), + expectedStatus: http.StatusOK, + expectedBody: true, + expectedPolicy: &api.Policy{ + Id: str("id-existed"), + Name: "Default POSTed Policy", + Rules: []api.PolicyRule{ + { + Id: str("id-existed"), + Name: "Default POSTed Policy", + Description: str("Description"), + Protocol: "tcp", + Action: "accept", + Bidirectional: true, + }, + }, + }, + }, + { + name: "WritePolicy PUT Invalid Name", + requestType: http.MethodPut, + requestPath: "/api/policies/id-existed", + requestBody: bytes.NewBuffer( + []byte(`{"ID":"id-existed","Name":"","Rules":[{"ID":"id-existed"}]}`)), + expectedStatus: http.StatusUnprocessableEntity, + }, + } + + p := initPoliciesTestData(&server.Policy{ + ID: "id-existed", + Name: "Default POSTed Rule", + Rules: []*server.PolicyRule{ + { + ID: "id-existed", + Name: "Default POSTed Rule", + Bidirectional: true, + }, + }, + }) + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + recorder := httptest.NewRecorder() + req := httptest.NewRequest(tc.requestType, tc.requestPath, tc.requestBody) + + router := mux.NewRouter() + router.HandleFunc("/api/policies", p.CreatePolicy).Methods("POST") + router.HandleFunc("/api/policies/{policyId}", p.UpdatePolicy).Methods("PUT") + router.ServeHTTP(recorder, req) + + res := recorder.Result() + defer res.Body.Close() + + content, err := io.ReadAll(res.Body) + if err != nil { + t.Fatalf("I don't know what I expected; %v", err) + return + } + + if status := recorder.Code; status != tc.expectedStatus { + t.Errorf("handler returned wrong status code: got %v want %v, content: %s", + status, tc.expectedStatus, string(content)) + return + } + + if !tc.expectedBody { + return + } + + expected, err := json.Marshal(tc.expectedPolicy) + if err != nil { + t.Fatalf("marshal expected policy: %v", err) + return + } + + assert.Equal(t, strings.Trim(string(content), " \n"), string(expected), "content mismatch") + }) + } +} diff --git a/management/server/http/rules_handler.go b/management/server/http/rules_handler.go index 93ddeacbd..bd501acf9 100644 --- a/management/server/http/rules_handler.go +++ b/management/server/http/rules_handler.go @@ -112,10 +112,6 @@ func (h *RulesHandler) UpdateRule(w http.ResponseWriter, r *http.Request) { policy.Rules[0].Destinations = reqDestinations policy.Rules[0].Enabled = !req.Disabled policy.Rules[0].Description = req.Description - if err := policy.UpdateQueryFromRules(); err != nil { - util.WriteError(err, w) - return - } switch req.Flow { case server.TrafficFlowBidirectString: diff --git a/management/server/http/rules_handler_test.go b/management/server/http/rules_handler_test.go index f8774f1aa..27a308a0a 100644 --- a/management/server/http/rules_handler_test.go +++ b/management/server/http/rules_handler_test.go @@ -30,9 +30,6 @@ func initRulesTestData(rules ...*server.Rule) *RulesHandler { if err != nil { panic(err) } - if err := policy.UpdateQueryFromRules(); err != nil { - panic(err) - } testPolicies[policy.ID] = policy } return &RulesHandler{ diff --git a/management/server/network.go b/management/server/network.go index 5237fa441..295fec143 100644 --- a/management/server/network.go +++ b/management/server/network.go @@ -25,11 +25,12 @@ const ( ) type NetworkMap struct { - Peers []*Peer - Network *Network - Routes []*route.Route - DNSConfig nbdns.Config - OfflinePeers []*Peer + Peers []*Peer + Network *Network + Routes []*route.Route + DNSConfig nbdns.Config + OfflinePeers []*Peer + FirewallRules []*FirewallRule } type Network struct { diff --git a/management/server/peer.go b/management/server/peer.go index 1565a4566..0584d27ca 100644 --- a/management/server/peer.go +++ b/management/server/peer.go @@ -225,8 +225,7 @@ func (am *DefaultAccountManager) GetPeers(accountID, userID string) ([]*Peer, er // fetch all the peers that have access to the user's peers for _, peer := range peers { - // TODO: use firewall rules - aclPeers := account.getPeersByACL(peer.ID) + aclPeers, _ := account.getPeerConnectionResources(peer.ID) for _, p := range aclPeers { peersMap[p.ID] = p } @@ -865,7 +864,7 @@ func (am *DefaultAccountManager) GetPeer(accountID, peerID, userID string) (*Pee } for _, p := range userPeers { - aclPeers := account.getPeersByACL(p.ID) + aclPeers, _ := account.getPeerConnectionResources(p.ID) for _, aclPeer := range aclPeers { if aclPeer.ID == peerID { return peer, nil @@ -884,98 +883,6 @@ func updatePeerMeta(peer *Peer, meta PeerSystemMeta, account *Account) (*Peer, b return peer, false } -// GetPeerRules returns a list of source or destination rules of a given peer. -func (a *Account) GetPeerRules(peerID string) (srcRules []*Rule, dstRules []*Rule) { - // Rules are group based so there is no direct access to peers. - // First, find all groups that the given peer belongs to - peerGroups := make(map[string]struct{}) - - for s, group := range a.Groups { - for _, peer := range group.Peers { - if peerID == peer { - peerGroups[s] = struct{}{} - break - } - } - } - - // Second, find all rules that have discovered source and destination groups - srcRulesMap := make(map[string]*Rule) - dstRulesMap := make(map[string]*Rule) - for _, rule := range a.Rules { - for _, g := range rule.Source { - if _, ok := peerGroups[g]; ok && srcRulesMap[rule.ID] == nil { - srcRules = append(srcRules, rule) - srcRulesMap[rule.ID] = rule - } - } - for _, g := range rule.Destination { - if _, ok := peerGroups[g]; ok && dstRulesMap[rule.ID] == nil { - dstRules = append(dstRules, rule) - dstRulesMap[rule.ID] = rule - } - } - } - - return srcRules, dstRules -} - -// getPeersByACL returns all peers that given peer has access to. -func (a *Account) getPeersByACL(peerID string) []*Peer { - var peers []*Peer - srcRules, dstRules := a.GetPeerRules(peerID) - - groups := map[string]*Group{} - for _, r := range srcRules { - if r.Disabled { - continue - } - if r.Flow == TrafficFlowBidirect { - for _, gid := range r.Destination { - if group, ok := a.Groups[gid]; ok { - groups[gid] = group - } - } - } - } - - for _, r := range dstRules { - if r.Disabled { - continue - } - if r.Flow == TrafficFlowBidirect { - for _, gid := range r.Source { - if group, ok := a.Groups[gid]; ok { - groups[gid] = group - } - } - } - } - - peersSet := make(map[string]struct{}) - for _, g := range groups { - for _, pid := range g.Peers { - peer, ok := a.Peers[pid] - if !ok { - log.Warnf( - "peer %s found in group %s but doesn't belong to account %s", - pid, - g.ID, - a.Id, - ) - continue - } - // exclude original peer - if _, ok := peersSet[peer.ID]; peer.ID != peerID && !ok { - peersSet[peer.ID] = struct{}{} - peers = append(peers, peer.Copy()) - } - } - } - - return peers -} - // updateAccountPeers updates all peers that belong to an account. // Should be called when changes have to be synced to peers. func (am *DefaultAccountManager) updateAccountPeers(account *Account) error { diff --git a/management/server/peer_test.go b/management/server/peer_test.go index 9dbd28d29..43629224d 100644 --- a/management/server/peer_test.go +++ b/management/server/peer_test.go @@ -227,16 +227,13 @@ func TestAccountManager_GetNetworkMapWithPolicy(t *testing.T) { policy.Enabled = true policy.Rules = []*PolicyRule{ { - Enabled: true, - Sources: []string{group1.ID}, - Destinations: []string{group2.ID}, - Action: PolicyTrafficActionAccept, + Enabled: true, + Sources: []string{group1.ID}, + Destinations: []string{group2.ID}, + Bidirectional: true, + Action: PolicyTrafficActionAccept, }, } - if err := policy.UpdateQueryFromRules(); err != nil { - t.Errorf("expecting policy to be updated, got failure %v", err) - return - } err = manager.SavePolicy(account.Id, userID, &policy) if err != nil { t.Errorf("expecting rule to be added, got failure %v", err) diff --git a/management/server/policy.go b/management/server/policy.go index 8a166c25c..3979b88b0 100644 --- a/management/server/policy.go +++ b/management/server/policy.go @@ -1,18 +1,13 @@ package server import ( - "bytes" - "context" _ "embed" "fmt" - "html/template" "strings" + "github.com/netbirdio/netbird/management/proto" "github.com/netbirdio/netbird/management/server/activity" "github.com/netbirdio/netbird/management/server/status" - - "github.com/open-policy-agent/opa/rego" - log "github.com/sirupsen/logrus" ) // PolicyUpdateOperationType operation type @@ -21,6 +16,12 @@ type PolicyUpdateOperationType int // PolicyTrafficActionType action type for the firewall type PolicyTrafficActionType string +// PolicyRuleProtocolType type of traffic +type PolicyRuleProtocolType string + +// PolicyRuleDirection direction of traffic +type PolicyRuleDirection string + const ( // PolicyTrafficActionAccept indicates that the traffic is accepted PolicyTrafficActionAccept = PolicyTrafficActionType("accept") @@ -28,21 +29,35 @@ const ( PolicyTrafficActionDrop = PolicyTrafficActionType("drop") ) +const ( + // PolicyRuleProtocolALL type of traffic + PolicyRuleProtocolALL = PolicyRuleProtocolType("all") + // PolicyRuleProtocolTCP type of traffic + PolicyRuleProtocolTCP = PolicyRuleProtocolType("tcp") + // PolicyRuleProtocolUDP type of traffic + PolicyRuleProtocolUDP = PolicyRuleProtocolType("udp") + // PolicyRuleProtocolICMP type of traffic + PolicyRuleProtocolICMP = PolicyRuleProtocolType("icmp") +) + +const ( + // PolicyRuleFlowDirect allows trafic from source to destination + PolicyRuleFlowDirect = PolicyRuleDirection("direct") + // PolicyRuleFlowBidirect allows traffic to both directions + PolicyRuleFlowBidirect = PolicyRuleDirection("bidirect") +) + +const ( + firewallRuleDirectionIN = 0 + firewallRuleDirectionOUT = 1 +) + // PolicyUpdateOperation operation object with type and values to be applied type PolicyUpdateOperation struct { Type PolicyUpdateOperationType Values []string } -//go:embed rego/default_policy_module.rego -var defaultPolicyModule string - -//go:embed rego/default_policy.rego -var defaultPolicyText string - -// defaultPolicyTemplate is a template for the default policy -var defaultPolicyTemplate = template.Must(template.New("policy").Parse(defaultPolicyText)) - // PolicyRule is the metadata of the policy type PolicyRule struct { // ID of the policy rule @@ -65,18 +80,30 @@ type PolicyRule struct { // Sources policy source groups Sources []string + + // Bidirectional define if the rule is applicable in both directions, sources, and destinations + Bidirectional bool + + // Protocol type of the traffic + Protocol PolicyRuleProtocolType + + // Ports or it ranges list + Ports []string } // Copy returns a copy of a policy rule func (pm *PolicyRule) Copy() *PolicyRule { return &PolicyRule{ - ID: pm.ID, - Name: pm.Name, - Description: pm.Description, - Enabled: pm.Enabled, - Action: pm.Action, - Destinations: pm.Destinations[:], - Sources: pm.Sources[:], + ID: pm.ID, + Name: pm.Name, + Description: pm.Description, + Enabled: pm.Enabled, + Action: pm.Action, + Destinations: pm.Destinations[:], + Sources: pm.Sources[:], + Bidirectional: pm.Bidirectional, + Protocol: pm.Protocol, + Ports: pm.Ports[:], } } @@ -107,9 +134,6 @@ type Policy struct { // Enabled status of the policy Enabled bool - // Query of Rego the policy - Query string - // Rules of the policy Rules []*PolicyRule } @@ -121,7 +145,6 @@ func (p *Policy) Copy() *Policy { Name: p.Name, Description: p.Description, Enabled: p.Enabled, - Query: p.Query, } for _, r := range p.Rules { c.Rules = append(c.Rules, r.Copy()) @@ -134,214 +157,124 @@ func (p *Policy) EventMeta() map[string]any { return map[string]any{"name": p.Name} } -// UpdateQueryFromRules marshals policy rules to Rego string and set it to Query -func (p *Policy) UpdateQueryFromRules() error { - type templateVars struct { - All []string - Source []string - Destination []string - } - queries := []string{} +// UpgradeAndFix different version of policies to latest version +func (p *Policy) UpgradeAndFix() { for _, r := range p.Rules { - if !r.Enabled { - continue + // start migrate from version v0.20.3 + if r.Protocol == "" { + r.Protocol = PolicyRuleProtocolALL } - - buff := new(bytes.Buffer) - input := templateVars{ - All: append(r.Destinations[:], r.Sources...), - Source: r.Sources, - Destination: r.Destinations, + if r.Protocol == PolicyRuleProtocolALL && !r.Bidirectional { + r.Bidirectional = true } - if err := defaultPolicyTemplate.Execute(buff, input); err != nil { - return status.Errorf(status.BadRequest, "failed to update policy query: %v", err) - } - queries = append(queries, buff.String()) + // -- v0.20.4 } - p.Query = strings.Join(queries, "\n") - return nil } // FirewallRule is a rule of the firewall. type FirewallRule struct { - // PeerID of the peer - PeerID string - // PeerIP of the peer PeerIP string // Direction of the traffic - Direction string + Direction int // Action of the traffic Action string + // Protocol of the traffic + Protocol string + // Port of the traffic Port string - - // id for internal purposes - id string } -// parseFromRegoResult parses the Rego result to a FirewallRule. -func (f *FirewallRule) parseFromRegoResult(value interface{}) error { - object, ok := value.(map[string]interface{}) - if !ok { - return fmt.Errorf("invalid Rego query eval result") - } +// getPeerConnectionResources for a given peer +// +// This function returns the list of peers and firewall rules that are applicable to a given peer. +func (a *Account) getPeerConnectionResources(peerID string) ([]*Peer, []*FirewallRule) { + generateResources, getAccumulatedResources := a.connResourcesGenerator() - peerID, ok := object["ID"].(string) - if !ok { - return fmt.Errorf("invalid Rego query eval result peer ID type") - } - - peerIP, ok := object["IP"].(string) - if !ok { - return fmt.Errorf("invalid Rego query eval result peer IP type") - } - - direction, ok := object["Direction"].(string) - if !ok { - return fmt.Errorf("invalid Rego query eval result peer direction type") - } - - action, ok := object["Action"].(string) - if !ok { - return fmt.Errorf("invalid Rego query eval result peer action type") - } - - port, ok := object["Port"].(string) - if !ok { - return fmt.Errorf("invalid Rego query eval result peer port type") - } - - f.PeerID = peerID - f.PeerIP = peerIP - f.Direction = direction - f.Action = action - f.Port = port - - // NOTE: update this id each time when new field added - f.id = peerID + peerIP + direction + action + port - - return nil -} - -// queryPeersAndFwRulesByRego returns a list associated Peers and firewall rules list for this peer. -func (a *Account) queryPeersAndFwRulesByRego( - peerID string, - queryNumber int, - query string, -) ([]*Peer, []*FirewallRule) { - input := map[string]interface{}{ - "peer_id": peerID, - "peers": a.Peers, - "groups": a.Groups, - } - - stmt, err := rego.New( - rego.Query("data.netbird.all"), - rego.Module("netbird", defaultPolicyModule), - rego.Module(fmt.Sprintf("netbird-%d", queryNumber), query), - ).PrepareForEval(context.TODO()) - if err != nil { - log.WithError(err).Error("get Rego query") - return nil, nil - } - - evalResult, err := stmt.Eval( - context.TODO(), - rego.EvalInput(input), - ) - if err != nil { - log.WithError(err).Error("eval Rego query") - return nil, nil - } - - if len(evalResult) == 0 || len(evalResult[0].Expressions) == 0 { - log.Trace("empty Rego query eval result") - return nil, nil - } - expressions, ok := evalResult[0].Expressions[0].Value.([]interface{}) - if !ok { - return nil, nil - } - - dst := make(map[string]struct{}) - src := make(map[string]struct{}) - peers := make([]*Peer, 0, len(expressions)) - rules := make([]*FirewallRule, 0, len(expressions)) - for _, v := range expressions { - rule := &FirewallRule{} - if err := rule.parseFromRegoResult(v); err != nil { - log.WithError(err).Error("parse Rego query eval result") - continue - } - rules = append(rules, rule) - switch rule.Direction { - case "dst": - if _, ok := dst[rule.PeerID]; ok { - continue - } - dst[rule.PeerID] = struct{}{} - case "src": - if _, ok := src[rule.PeerID]; ok { - continue - } - src[rule.PeerID] = struct{}{} - default: - log.WithField("direction", rule.Direction).Error("invalid direction") - continue - } - } - - added := make(map[string]struct{}) - if _, ok := src[peerID]; ok { - for id := range dst { - if _, ok := added[id]; !ok && id != peerID { - added[id] = struct{}{} - } - } - } - if _, ok := dst[peerID]; ok { - for id := range src { - if _, ok := added[id]; !ok && id != peerID { - added[id] = struct{}{} - } - } - } - - for id := range added { - peers = append(peers, a.Peers[id]) - } - return peers, rules -} - -// getPeersByPolicy returns all peers that given peer has access to. -func (a *Account) getPeersByPolicy(peerID string) (peers []*Peer, rules []*FirewallRule) { - peersSeen := make(map[string]struct{}) - ruleSeen := make(map[string]struct{}) - for i, policy := range a.Policies { + for _, policy := range a.Policies { if !policy.Enabled { continue } - p, r := a.queryPeersAndFwRulesByRego(peerID, i, policy.Query) - for _, peer := range p { - if _, ok := peersSeen[peer.ID]; ok { + + for _, rule := range policy.Rules { + if !rule.Enabled { continue } - peers = append(peers, peer) - peersSeen[peer.ID] = struct{}{} - } - for _, rule := range r { - if _, ok := ruleSeen[rule.id]; ok { - continue + + sourcePeers, peerInSources := getAllPeersFromGroups(a, rule.Sources, peerID) + destinationPeers, peerInDestinations := getAllPeersFromGroups(a, rule.Destinations, peerID) + + if rule.Bidirectional { + if peerInSources { + generateResources(rule, destinationPeers, firewallRuleDirectionIN) + } + if peerInDestinations { + generateResources(rule, sourcePeers, firewallRuleDirectionOUT) + } + } + + if peerInSources { + generateResources(rule, destinationPeers, firewallRuleDirectionOUT) + } + + if peerInDestinations { + generateResources(rule, sourcePeers, firewallRuleDirectionIN) } - rules = append(rules, rule) - ruleSeen[rule.id] = struct{}{} } } - return + + return getAccumulatedResources() +} + +// connResourcesGenerator returns generator and accumulator function which returns the result of generator calls +// +// The generator function is used to generate the list of peers and firewall rules that are applicable to a given peer. +// It safe to call the generator function multiple times for same peer and different rules no duplicates will be +// generated. The accumulator function returns the result of all the generator calls. +func (a *Account) connResourcesGenerator() (func(*PolicyRule, []*Peer, int), func() ([]*Peer, []*FirewallRule)) { + rulesExists := make(map[string]struct{}) + peersExists := make(map[string]struct{}) + rules := make([]*FirewallRule, 0) + peers := make([]*Peer, 0) + return func(rule *PolicyRule, groupPeers []*Peer, direction int) { + for _, peer := range groupPeers { + if _, ok := peersExists[peer.ID]; !ok { + peers = append(peers, peer) + peersExists[peer.ID] = struct{}{} + } + + fwRule := FirewallRule{ + PeerIP: peer.IP.String(), + Direction: direction, + Action: string(rule.Action), + Protocol: string(rule.Protocol), + } + + ruleID := fmt.Sprintf("%s%d", peer.ID+peer.IP.String(), direction) + ruleID += string(rule.Protocol) + string(rule.Action) + strings.Join(rule.Ports, ",") + if _, ok := rulesExists[ruleID]; ok { + continue + } + rulesExists[ruleID] = struct{}{} + + if len(rule.Ports) == 0 { + rules = append(rules, &fwRule) + continue + } + + for _, port := range rule.Ports { + addRule := fwRule + addRule.Port = port + rules = append(rules, &addRule) + } + } + }, func() ([]*Peer, []*FirewallRule) { + return peers, rules + } } // GetPolicy from the store @@ -475,3 +408,63 @@ func (am *DefaultAccountManager) savePolicy(account *Account, policy *Policy) (e } return } + +func toProtocolFirewallRules(update []*FirewallRule) []*proto.FirewallRule { + result := make([]*proto.FirewallRule, len(update)) + for i := range update { + direction := proto.FirewallRule_IN + if update[i].Direction == firewallRuleDirectionOUT { + direction = proto.FirewallRule_OUT + } + action := proto.FirewallRule_ACCEPT + if update[i].Action == string(PolicyTrafficActionDrop) { + action = proto.FirewallRule_DROP + } + + protocol := proto.FirewallRule_UNKNOWN + switch PolicyRuleProtocolType(update[i].Protocol) { + case PolicyRuleProtocolALL: + protocol = proto.FirewallRule_ALL + case PolicyRuleProtocolTCP: + protocol = proto.FirewallRule_TCP + case PolicyRuleProtocolUDP: + protocol = proto.FirewallRule_UDP + case PolicyRuleProtocolICMP: + protocol = proto.FirewallRule_ICMP + } + + result[i] = &proto.FirewallRule{ + PeerIP: update[i].PeerIP, + Direction: direction, + Action: action, + Protocol: protocol, + Port: update[i].Port, + } + } + return result +} + +// getAllPeersFromGroups for given peer ID and list of groups +// +// Returns list of peers and boolean indicating if peer is in any of the groups +func getAllPeersFromGroups(account *Account, groups []string, peerID string) ([]*Peer, bool) { + peerInGroups := false + filteredPeers := make([]*Peer, 0, len(groups)) + for _, g := range groups { + group, ok := account.Groups[g] + if !ok { + continue + } + + for _, p := range group.Peers { + peer := account.Peers[p] + if peer.ID == peerID { + peerInGroups = true + continue + } + + filteredPeers = append(filteredPeers, peer) + } + } + return filteredPeers, peerInGroups +} diff --git a/management/server/policy_test.go b/management/server/policy_test.go index 39ac44843..d154e54f1 100644 --- a/management/server/policy_test.go +++ b/management/server/policy_test.go @@ -1,6 +1,7 @@ package server import ( + "fmt" "net" "testing" @@ -11,262 +12,412 @@ import ( func TestAccount_getPeersByPolicy(t *testing.T) { account := &Account{ Peers: map[string]*Peer{ - "cfif97at2r9s73au3q00": { - ID: "cfif97at2r9s73au3q00", + "peerA": { + ID: "peerA", IP: net.ParseIP("100.65.14.88"), }, - "cfif97at2r9s73au3q0g": { - ID: "cfif97at2r9s73au3q0g", + "peerB": { + ID: "peerB", IP: net.ParseIP("100.65.80.39"), }, - "cfif97at2r9s73au3q10": { - ID: "cfif97at2r9s73au3q10", + "peerC": { + ID: "peerC", IP: net.ParseIP("100.65.254.139"), }, - "cfif97at2r9s73au3q20": { - ID: "cfif97at2r9s73au3q20", + "peerD": { + ID: "peerD", IP: net.ParseIP("100.65.62.5"), }, - "cfj4tiqt2r9s73dmeun0": { - ID: "cfj4tiqt2r9s73dmeun0", + "peerE": { + ID: "peerE", IP: net.ParseIP("100.65.32.206"), }, - "cg7h032t2r9s73cg5fk0": { - ID: "cg7h032t2r9s73cg5fk0", + "peerF": { + ID: "peerF", IP: net.ParseIP("100.65.250.202"), }, - "cgcnkj2t2r9s73cg5vv0": { - ID: "cgcnkj2t2r9s73cg5vv0", + "peerG": { + ID: "peerG", IP: net.ParseIP("100.65.13.186"), }, - "cgcol4qt2r9s73cg601g": { - ID: "cgcol4qt2r9s73cg601g", + "peerH": { + ID: "peerH", IP: net.ParseIP("100.65.29.55"), }, }, Groups: map[string]*Group{ - "cet9e92t2r9s7383ns20": { - ID: "cet9e92t2r9s7383ns20", + "GroupAll": { + ID: "GroupAll", Name: "All", Peers: []string{ - "cfif97at2r9s73au3q0g", - "cfif97at2r9s73au3q00", - "cfif97at2r9s73au3q20", - "cfif97at2r9s73au3q10", - "cfj4tiqt2r9s73dmeun0", - "cg7h032t2r9s73cg5fk0", - "cgcnkj2t2r9s73cg5vv0", - "cgcol4qt2r9s73cg601g", + "peerB", + "peerA", + "peerD", + "peerC", + "peerE", + "peerF", + "peerG", + "peerH", }, }, - "cev90bat2r9s7383o150": { - ID: "cev90bat2r9s7383o150", + "GroupSwarm": { + ID: "GroupSwarm", Name: "swarm", Peers: []string{ - "cfif97at2r9s73au3q0g", - "cfif97at2r9s73au3q00", - "cfif97at2r9s73au3q20", - "cfj4tiqt2r9s73dmeun0", - "cgcnkj2t2r9s73cg5vv0", - "cgcol4qt2r9s73cg601g", + "peerB", + "peerA", + "peerD", + "peerE", + "peerG", + "peerH", }, }, }, Rules: map[string]*Rule{ - "cet9e92t2r9s7383ns2g": { - ID: "cet9e92t2r9s7383ns2g", + "RuleDefault": { + ID: "RuleDefault", Name: "Default", Description: "This is a default rule that allows connections between all the resources", Source: []string{ - "cet9e92t2r9s7383ns20", + "GroupAll", }, Destination: []string{ - "cet9e92t2r9s7383ns20", + "GroupAll", }, }, - "cev90bat2r9s7383o15g": { - ID: "cev90bat2r9s7383o15g", + "RuleSwarm": { + ID: "RuleSwarm", Name: "Swarm", Description: "", Source: []string{ - "cev90bat2r9s7383o150", - "cet9e92t2r9s7383ns20", + "GroupSwarm", + "GroupAll", }, Destination: []string{ - "cev90bat2r9s7383o150", + "GroupSwarm", }, }, }, } - rule1, err := RuleToPolicy(account.Rules["cet9e92t2r9s7383ns2g"]) + rule1, err := RuleToPolicy(account.Rules["RuleDefault"]) assert.NoError(t, err) - rule2, err := RuleToPolicy(account.Rules["cev90bat2r9s7383o15g"]) + rule2, err := RuleToPolicy(account.Rules["RuleSwarm"]) assert.NoError(t, err) account.Policies = append(account.Policies, rule1, rule2) t.Run("check that all peers get map", func(t *testing.T) { for _, p := range account.Peers { - peers, firewallRules := account.getPeersByPolicy(p.ID) + peers, firewallRules := account.getPeerConnectionResources(p.ID) assert.GreaterOrEqual(t, len(peers), 2, "mininum number peers should present") assert.GreaterOrEqual(t, len(firewallRules), 2, "mininum number of firewall rules should present") } }) t.Run("check first peer map details", func(t *testing.T) { - peers, firewallRules := account.getPeersByPolicy("cfif97at2r9s73au3q0g") + peers, firewallRules := account.getPeerConnectionResources("peerB") assert.Len(t, peers, 7) - assert.Contains(t, peers, account.Peers["cfif97at2r9s73au3q00"]) - assert.Contains(t, peers, account.Peers["cfif97at2r9s73au3q10"]) - assert.Contains(t, peers, account.Peers["cfif97at2r9s73au3q20"]) - assert.Contains(t, peers, account.Peers["cfj4tiqt2r9s73dmeun0"]) - assert.Contains(t, peers, account.Peers["cg7h032t2r9s73cg5fk0"]) + assert.Contains(t, peers, account.Peers["peerA"]) + assert.Contains(t, peers, account.Peers["peerC"]) + assert.Contains(t, peers, account.Peers["peerD"]) + assert.Contains(t, peers, account.Peers["peerE"]) + assert.Contains(t, peers, account.Peers["peerF"]) epectedFirewallRules := []*FirewallRule{ { - PeerID: "cfif97at2r9s73au3q00", PeerIP: "100.65.14.88", - Direction: "src", + Direction: firewallRuleDirectionIN, Action: "accept", + Protocol: "all", Port: "", - id: "cfif97at2r9s73au3q00100.65.14.88srcaccept", }, { - PeerID: "cfif97at2r9s73au3q00", PeerIP: "100.65.14.88", - Direction: "dst", + Direction: firewallRuleDirectionOUT, Action: "accept", + Protocol: "all", Port: "", - id: "cfif97at2r9s73au3q00100.65.14.88dstaccept", - }, - - { - PeerID: "cfif97at2r9s73au3q0g", - PeerIP: "100.65.80.39", - Direction: "dst", - Action: "accept", - Port: "", - id: "cfif97at2r9s73au3q0g100.65.80.39dstaccept", }, { - PeerID: "cfif97at2r9s73au3q0g", - PeerIP: "100.65.80.39", - Direction: "src", - Action: "accept", - Port: "", - id: "cfif97at2r9s73au3q0g100.65.80.39srcaccept", - }, - - { - PeerID: "cfif97at2r9s73au3q10", PeerIP: "100.65.254.139", - Direction: "dst", + Direction: firewallRuleDirectionOUT, Action: "accept", + Protocol: "all", Port: "", - id: "cfif97at2r9s73au3q10100.65.254.139dstaccept", }, { - PeerID: "cfif97at2r9s73au3q10", PeerIP: "100.65.254.139", - Direction: "src", + Direction: firewallRuleDirectionIN, Action: "accept", + Protocol: "all", Port: "", - id: "cfif97at2r9s73au3q10100.65.254.139srcaccept", }, { - PeerID: "cfif97at2r9s73au3q20", PeerIP: "100.65.62.5", - Direction: "dst", + Direction: firewallRuleDirectionOUT, Action: "accept", + Protocol: "all", Port: "", - id: "cfif97at2r9s73au3q20100.65.62.5dstaccept", }, { - PeerID: "cfif97at2r9s73au3q20", PeerIP: "100.65.62.5", - Direction: "src", + Direction: firewallRuleDirectionIN, Action: "accept", + Protocol: "all", Port: "", - id: "cfif97at2r9s73au3q20100.65.62.5srcaccept", }, { - PeerID: "cfj4tiqt2r9s73dmeun0", PeerIP: "100.65.32.206", - Direction: "dst", + Direction: firewallRuleDirectionOUT, Action: "accept", + Protocol: "all", Port: "", - id: "cfj4tiqt2r9s73dmeun0100.65.32.206dstaccept", }, { - PeerID: "cfj4tiqt2r9s73dmeun0", PeerIP: "100.65.32.206", - Direction: "src", + Direction: firewallRuleDirectionIN, Action: "accept", + Protocol: "all", Port: "", - id: "cfj4tiqt2r9s73dmeun0100.65.32.206srcaccept", }, { - PeerID: "cg7h032t2r9s73cg5fk0", PeerIP: "100.65.250.202", - Direction: "dst", + Direction: firewallRuleDirectionOUT, Action: "accept", + Protocol: "all", Port: "", - id: "cg7h032t2r9s73cg5fk0100.65.250.202dstaccept", }, { - PeerID: "cg7h032t2r9s73cg5fk0", PeerIP: "100.65.250.202", - Direction: "src", + Direction: firewallRuleDirectionIN, Action: "accept", + Protocol: "all", Port: "", - id: "cg7h032t2r9s73cg5fk0100.65.250.202srcaccept", }, { - PeerID: "cgcnkj2t2r9s73cg5vv0", PeerIP: "100.65.13.186", - Direction: "dst", + Direction: firewallRuleDirectionOUT, Action: "accept", + Protocol: "all", Port: "", - id: "cgcnkj2t2r9s73cg5vv0100.65.13.186dstaccept", }, { - PeerID: "cgcnkj2t2r9s73cg5vv0", PeerIP: "100.65.13.186", - Direction: "src", + Direction: firewallRuleDirectionIN, Action: "accept", + Protocol: "all", Port: "", - id: "cgcnkj2t2r9s73cg5vv0100.65.13.186srcaccept", }, { - PeerID: "cgcol4qt2r9s73cg601g", PeerIP: "100.65.29.55", - Direction: "dst", + Direction: firewallRuleDirectionOUT, Action: "accept", + Protocol: "all", Port: "", - id: "cgcol4qt2r9s73cg601g100.65.29.55dstaccept", }, { - PeerID: "cgcol4qt2r9s73cg601g", PeerIP: "100.65.29.55", - Direction: "src", + Direction: firewallRuleDirectionIN, Action: "accept", + Protocol: "all", Port: "", - id: "cgcol4qt2r9s73cg601g100.65.29.55srcaccept", }, } assert.Len(t, firewallRules, len(epectedFirewallRules)) - slices.SortFunc(firewallRules, func(a, b *FirewallRule) bool { - return a.PeerID < b.PeerID - }) + slices.SortFunc(epectedFirewallRules, sortFunc()) + slices.SortFunc(firewallRules, sortFunc()) for i := range firewallRules { assert.Equal(t, epectedFirewallRules[i], firewallRules[i]) } }) } + +func TestAccount_getPeersByPolicyDirect(t *testing.T) { + account := &Account{ + Peers: map[string]*Peer{ + "peerA": { + ID: "peerA", + IP: net.ParseIP("100.65.14.88"), + }, + "peerB": { + ID: "peerB", + IP: net.ParseIP("100.65.80.39"), + }, + "peerC": { + ID: "peerC", + IP: net.ParseIP("100.65.254.139"), + }, + }, + Groups: map[string]*Group{ + "GroupAll": { + ID: "GroupAll", + Name: "All", + Peers: []string{ + "peerB", + "peerA", + "peerC", + }, + }, + "GroupSwarm": { + ID: "GroupSwarm", + Name: "swarm", + Peers: []string{ + "peerB", + }, + }, + "peerF": { + ID: "peerF", + Name: "dmz", + Peers: []string{ + "peerC", + }, + }, + }, + Rules: map[string]*Rule{ + "RuleDefault": { + ID: "RuleDefault", + Name: "Default", + Disabled: true, + Description: "This is a default rule that allows connections between all the resources", + Source: []string{ + "GroupAll", + }, + Destination: []string{ + "GroupAll", + }, + }, + "RuleSwarm": { + ID: "RuleSwarm", + Name: "Swarm", + Description: "", + Source: []string{ + "GroupSwarm", + }, + Destination: []string{ + "peerF", + }, + }, + }, + } + + rule1, err := RuleToPolicy(account.Rules["RuleDefault"]) + assert.NoError(t, err) + + rule2, err := RuleToPolicy(account.Rules["RuleSwarm"]) + assert.NoError(t, err) + + account.Policies = append(account.Policies, rule1, rule2) + + t.Run("check first peer map", func(t *testing.T) { + peers, firewallRules := account.getPeerConnectionResources("peerB") + assert.Contains(t, peers, account.Peers["peerC"]) + + epectedFirewallRules := []*FirewallRule{ + { + PeerIP: "100.65.254.139", + Direction: firewallRuleDirectionIN, + Action: "accept", + Protocol: "all", + Port: "", + }, + { + PeerIP: "100.65.254.139", + Direction: firewallRuleDirectionOUT, + Action: "accept", + Protocol: "all", + Port: "", + }, + } + assert.Len(t, firewallRules, len(epectedFirewallRules)) + slices.SortFunc(epectedFirewallRules, sortFunc()) + slices.SortFunc(firewallRules, sortFunc()) + for i := range firewallRules { + assert.Equal(t, epectedFirewallRules[i], firewallRules[i]) + } + }) + + t.Run("check second peer map", func(t *testing.T) { + peers, firewallRules := account.getPeerConnectionResources("peerC") + assert.Contains(t, peers, account.Peers["peerB"]) + + epectedFirewallRules := []*FirewallRule{ + { + PeerIP: "100.65.80.39", + Direction: firewallRuleDirectionIN, + Action: "accept", + Protocol: "all", + Port: "", + }, + { + PeerIP: "100.65.80.39", + Direction: firewallRuleDirectionOUT, + Action: "accept", + Protocol: "all", + Port: "", + }, + } + assert.Len(t, firewallRules, len(epectedFirewallRules)) + slices.SortFunc(epectedFirewallRules, sortFunc()) + slices.SortFunc(firewallRules, sortFunc()) + for i := range firewallRules { + assert.Equal(t, epectedFirewallRules[i], firewallRules[i]) + } + }) + + account.Policies[1].Rules[0].Bidirectional = false + + t.Run("check first peer map directional only", func(t *testing.T) { + peers, firewallRules := account.getPeerConnectionResources("peerB") + assert.Contains(t, peers, account.Peers["peerC"]) + + epectedFirewallRules := []*FirewallRule{ + { + PeerIP: "100.65.254.139", + Direction: firewallRuleDirectionOUT, + Action: "accept", + Protocol: "all", + Port: "", + }, + } + assert.Len(t, firewallRules, len(epectedFirewallRules)) + slices.SortFunc(epectedFirewallRules, sortFunc()) + slices.SortFunc(firewallRules, sortFunc()) + for i := range firewallRules { + assert.Equal(t, epectedFirewallRules[i], firewallRules[i]) + } + }) + + t.Run("check second peer map directional only", func(t *testing.T) { + peers, firewallRules := account.getPeerConnectionResources("peerC") + assert.Contains(t, peers, account.Peers["peerB"]) + + epectedFirewallRules := []*FirewallRule{ + { + PeerIP: "100.65.80.39", + Direction: firewallRuleDirectionIN, + Action: "accept", + Protocol: "all", + Port: "", + }, + } + assert.Len(t, firewallRules, len(epectedFirewallRules)) + slices.SortFunc(epectedFirewallRules, sortFunc()) + slices.SortFunc(firewallRules, sortFunc()) + for i := range firewallRules { + assert.Equal(t, epectedFirewallRules[i], firewallRules[i]) + } + }) +} + +func sortFunc() func(a *FirewallRule, b *FirewallRule) bool { + return func(a, b *FirewallRule) bool { + return a.PeerIP+fmt.Sprintf("%d", a.Direction) < b.PeerIP+fmt.Sprintf("%d", b.Direction) + } +} diff --git a/management/server/rego/default_policy.rego b/management/server/rego/default_policy.rego deleted file mode 100644 index a1012ae76..000000000 --- a/management/server/rego/default_policy.rego +++ /dev/null @@ -1,9 +0,0 @@ -package netbird - -all[rule] { - is_peer_in_any_group([{{range $i, $e := .All}}{{if $i}},{{end}}"{{$e}}"{{end}}]) - rule := { - {{range $i, $e := .Destination}}rules_from_group("{{$e}}", "dst", "accept", ""),{{end}} - {{range $i, $e := .Source}}rules_from_group("{{$e}}", "src", "accept", ""),{{end}} - }[_][_] -} diff --git a/management/server/rego/default_policy_module.rego b/management/server/rego/default_policy_module.rego deleted file mode 100644 index 7411db36a..000000000 --- a/management/server/rego/default_policy_module.rego +++ /dev/null @@ -1,34 +0,0 @@ -package netbird - -import future.keywords.if -import future.keywords.in -import future.keywords.contains - -# get_rule builds a netbird rule object from given parameters -get_rule(peer_id, direction, action, port) := rule if { - peer := input.peers[_] - peer.ID == peer_id - rule := { - "ID": peer.ID, - "IP": peer.IP, - "Direction": direction, - "Action": action, - "Port": port, - } -} - -# netbird_rules_from_group returns a list of netbird rules for a given group_id -rules_from_group(group_id, direction, action, port) := rules if { - group := input.groups[_] - group.ID == group_id - rules := [get_rule(peer, direction, action, port) | peer := group.Peers[_]] -} - -# is_peer_in_any_group checks that input peer present at least in one group -is_peer_in_any_group(groups) := count([group_id]) > 0 if { - group_id := groups[_] - group := input.groups[_] - group.ID == group_id - peer := group.Peers[_] - peer == input.peer_id -} diff --git a/management/server/route_test.go b/management/server/route_test.go index a6cd3035e..c943aee0b 100644 --- a/management/server/route_test.go +++ b/management/server/route_test.go @@ -911,8 +911,6 @@ func TestGetNetworkMap_RouteSync(t *testing.T) { newPolicy.Name = "peer1 only" newPolicy.Rules[0].Sources = []string{newGroup.ID} newPolicy.Rules[0].Destinations = []string{newGroup.ID} - err = newPolicy.UpdateQueryFromRules() - require.NoError(t, err) err = am.SavePolicy(account.Id, userID, newPolicy) require.NoError(t, err) diff --git a/management/server/rule.go b/management/server/rule.go index b7e24d033..68b1cc4fb 100644 --- a/management/server/rule.go +++ b/management/server/rule.go @@ -67,13 +67,15 @@ func (r *Rule) ToPolicyRule() *PolicyRule { return nil } return &PolicyRule{ - ID: r.ID, - Name: r.Name, - Enabled: !r.Disabled, - Description: r.Description, - Action: PolicyTrafficActionAccept, - Destinations: r.Destination, - Sources: r.Source, + ID: r.ID, + Name: r.Name, + Enabled: !r.Disabled, + Description: r.Description, + Destinations: r.Destination, + Sources: r.Source, + Bidirectional: true, + Protocol: PolicyRuleProtocolALL, + Action: PolicyTrafficActionAccept, } } @@ -82,15 +84,11 @@ func RuleToPolicy(rule *Rule) (*Policy, error) { if rule == nil { return nil, fmt.Errorf("rule is empty") } - policy := &Policy{ + return &Policy{ ID: rule.ID, Name: rule.Name, Description: rule.Description, Enabled: !rule.Disabled, Rules: []*PolicyRule{rule.ToPolicyRule()}, - } - if err := policy.UpdateQueryFromRules(); err != nil { - return nil, err - } - return policy, nil + }, nil }