diff --git a/client/cmd/testutil_test.go b/client/cmd/testutil_test.go index 4c06a7da0..22b982f61 100644 --- a/client/cmd/testutil_test.go +++ b/client/cmd/testutil_test.go @@ -6,6 +6,7 @@ import ( "testing" "time" + "github.com/golang/mock/gomock" "github.com/stretchr/testify/require" "go.opentelemetry.io/otel" @@ -90,13 +91,18 @@ func startManagement(t *testing.T, config *mgmt.Config, testFile string) (*grpc. metrics, err := telemetry.NewDefaultAppMetrics(context.Background()) require.NoError(t, err) - accountManager, err := mgmt.BuildManager(context.Background(), store, peersUpdateManager, nil, "", "netbird.selfhosted", eventStore, nil, false, iv, metrics, port_forwarding.NewControllerMock()) + ctrl := gomock.NewController(t) + t.Cleanup(ctrl.Finish) + + settingsMockManager := settings.NewMockManager(ctrl) + + accountManager, err := mgmt.BuildManager(context.Background(), store, peersUpdateManager, nil, "", "netbird.selfhosted", eventStore, nil, false, iv, metrics, port_forwarding.NewControllerMock(), settingsMockManager) if err != nil { t.Fatal(err) } - secretsManager := mgmt.NewTimeBasedAuthSecretsManager(peersUpdateManager, config.TURNConfig, config.Relay) - mgmtServer, err := mgmt.NewServer(context.Background(), config, accountManager, settings.NewManager(store), peersUpdateManager, secretsManager, nil, nil, nil) + secretsManager := mgmt.NewTimeBasedAuthSecretsManager(peersUpdateManager, config.TURNConfig, config.Relay, settingsMockManager) + mgmtServer, err := mgmt.NewServer(context.Background(), config, accountManager, settingsMockManager, peersUpdateManager, secretsManager, nil, nil, nil) if err != nil { t.Fatal(err) } diff --git a/client/firewall/create.go b/client/firewall/create.go index 37ea5ceb3..7b265e1d1 100644 --- a/client/firewall/create.go +++ b/client/firewall/create.go @@ -10,17 +10,18 @@ import ( firewall "github.com/netbirdio/netbird/client/firewall/manager" "github.com/netbirdio/netbird/client/firewall/uspfilter" + nftypes "github.com/netbirdio/netbird/client/internal/netflow/types" "github.com/netbirdio/netbird/client/internal/statemanager" ) // NewFirewall creates a firewall manager instance -func NewFirewall(iface IFaceMapper, _ *statemanager.Manager, disableServerRoutes bool) (firewall.Manager, error) { +func NewFirewall(iface IFaceMapper, _ *statemanager.Manager, flowLogger nftypes.FlowLogger, disableServerRoutes bool) (firewall.Manager, error) { if !iface.IsUserspaceBind() { return nil, fmt.Errorf("not implemented for this OS: %s", runtime.GOOS) } // use userspace packet filtering firewall - fm, err := uspfilter.Create(iface, disableServerRoutes) + fm, err := uspfilter.Create(iface, disableServerRoutes, flowLogger) if err != nil { return nil, err } diff --git a/client/firewall/create_linux.go b/client/firewall/create_linux.go index be1b37916..aa2f0d4d1 100644 --- a/client/firewall/create_linux.go +++ b/client/firewall/create_linux.go @@ -15,6 +15,7 @@ import ( firewall "github.com/netbirdio/netbird/client/firewall/manager" nbnftables "github.com/netbirdio/netbird/client/firewall/nftables" "github.com/netbirdio/netbird/client/firewall/uspfilter" + nftypes "github.com/netbirdio/netbird/client/internal/netflow/types" "github.com/netbirdio/netbird/client/internal/statemanager" ) @@ -33,7 +34,7 @@ const SKIP_NFTABLES_ENV = "NB_SKIP_NFTABLES_CHECK" // FWType is the type for the firewall type type FWType int -func NewFirewall(iface IFaceMapper, stateManager *statemanager.Manager, disableServerRoutes bool) (firewall.Manager, error) { +func NewFirewall(iface IFaceMapper, stateManager *statemanager.Manager, flowLogger nftypes.FlowLogger, disableServerRoutes bool) (firewall.Manager, error) { // on the linux system we try to user nftables or iptables // in any case, because we need to allow netbird interface traffic // so we use AllowNetbird traffic from these firewall managers @@ -47,7 +48,7 @@ func NewFirewall(iface IFaceMapper, stateManager *statemanager.Manager, disableS if err != nil { log.Warnf("failed to create native firewall: %v. Proceeding with userspace", err) } - return createUserspaceFirewall(iface, fm, disableServerRoutes) + return createUserspaceFirewall(iface, fm, disableServerRoutes, flowLogger) } func createNativeFirewall(iface IFaceMapper, stateManager *statemanager.Manager, routes bool) (firewall.Manager, error) { @@ -77,12 +78,12 @@ func createFW(iface IFaceMapper) (firewall.Manager, error) { } } -func createUserspaceFirewall(iface IFaceMapper, fm firewall.Manager, disableServerRoutes bool) (firewall.Manager, error) { +func createUserspaceFirewall(iface IFaceMapper, fm firewall.Manager, disableServerRoutes bool, flowLogger nftypes.FlowLogger) (firewall.Manager, error) { var errUsp error if fm != nil { - fm, errUsp = uspfilter.CreateWithNativeFirewall(iface, fm, disableServerRoutes) + fm, errUsp = uspfilter.CreateWithNativeFirewall(iface, fm, disableServerRoutes, flowLogger) } else { - fm, errUsp = uspfilter.Create(iface, disableServerRoutes) + fm, errUsp = uspfilter.Create(iface, disableServerRoutes, flowLogger) } if errUsp != nil { diff --git a/client/firewall/iptables/acl_linux.go b/client/firewall/iptables/acl_linux.go index 8f1b231b8..183417327 100644 --- a/client/firewall/iptables/acl_linux.go +++ b/client/firewall/iptables/acl_linux.go @@ -75,6 +75,7 @@ func (m *aclManager) init(stateManager *statemanager.Manager) error { } func (m *aclManager) AddPeerFiltering( + id []byte, ip net.IP, protocol firewall.Protocol, sPort *firewall.Port, diff --git a/client/firewall/iptables/manager_linux.go b/client/firewall/iptables/manager_linux.go index 4b8606834..652ab1b3e 100644 --- a/client/firewall/iptables/manager_linux.go +++ b/client/firewall/iptables/manager_linux.go @@ -96,21 +96,22 @@ func (m *Manager) Init(stateManager *statemanager.Manager) error { // // Comment will be ignored because some system this feature is not supported func (m *Manager) AddPeerFiltering( + id []byte, ip net.IP, - protocol firewall.Protocol, + proto firewall.Protocol, sPort *firewall.Port, dPort *firewall.Port, action firewall.Action, ipsetName string, - _ string, ) ([]firewall.Rule, error) { m.mutex.Lock() defer m.mutex.Unlock() - return m.aclMgr.AddPeerFiltering(ip, protocol, sPort, dPort, action, ipsetName) + return m.aclMgr.AddPeerFiltering(id, ip, proto, sPort, dPort, action, ipsetName) } func (m *Manager) AddRouteFiltering( + id []byte, sources []netip.Prefix, destination netip.Prefix, proto firewall.Protocol, @@ -125,7 +126,7 @@ func (m *Manager) AddRouteFiltering( return nil, fmt.Errorf("unsupported IP version: %s", destination.Addr().String()) } - return m.router.AddRouteFiltering(sources, destination, proto, sPort, dPort, action) + return m.router.AddRouteFiltering(id, sources, destination, proto, sPort, dPort, action) } // DeletePeerRule from the firewall by rule definition @@ -196,13 +197,13 @@ func (m *Manager) AllowNetbird() error { } _, err := m.AddPeerFiltering( + nil, net.IP{0, 0, 0, 0}, "all", nil, nil, firewall.ActionAccept, "", - "", ) if err != nil { return fmt.Errorf("allow netbird interface traffic: %w", err) diff --git a/client/firewall/iptables/manager_linux_test.go b/client/firewall/iptables/manager_linux_test.go index ad282670b..af9f5dd23 100644 --- a/client/firewall/iptables/manager_linux_test.go +++ b/client/firewall/iptables/manager_linux_test.go @@ -75,7 +75,7 @@ func TestIptablesManager(t *testing.T) { IsRange: true, Values: []uint16{8043, 8046}, } - rule2, err = manager.AddPeerFiltering(ip, "tcp", port, nil, fw.ActionAccept, "", "accept HTTPS traffic from ports range") + rule2, err = manager.AddPeerFiltering(nil, ip, "tcp", port, nil, fw.ActionAccept, "") require.NoError(t, err, "failed to add rule") for _, r := range rule2 { @@ -97,7 +97,7 @@ func TestIptablesManager(t *testing.T) { // add second rule ip := net.ParseIP("10.20.0.3") port := &fw.Port{Values: []uint16{5353}} - _, err = manager.AddPeerFiltering(ip, "udp", nil, port, fw.ActionAccept, "", "accept Fake DNS traffic") + _, err = manager.AddPeerFiltering(nil, ip, "udp", nil, port, fw.ActionAccept, "") require.NoError(t, err, "failed to add rule") err = manager.Close(nil) @@ -148,7 +148,7 @@ func TestIptablesManagerIPSet(t *testing.T) { port := &fw.Port{ Values: []uint16{443}, } - rule2, err = manager.AddPeerFiltering(ip, "tcp", port, nil, fw.ActionAccept, "default", "accept HTTPS traffic from ports range") + rule2, err = manager.AddPeerFiltering(nil, ip, "tcp", port, nil, fw.ActionAccept, "default") for _, r := range rule2 { require.NoError(t, err, "failed to add rule") require.Equal(t, r.(*Rule).ipsetName, "default-sport", "ipset name must be set") @@ -216,7 +216,7 @@ func TestIptablesCreatePerformance(t *testing.T) { start := time.Now() for i := 0; i < testMax; i++ { port := &fw.Port{Values: []uint16{uint16(1000 + i)}} - _, err = manager.AddPeerFiltering(ip, "tcp", nil, port, fw.ActionAccept, "", "accept HTTP traffic") + _, err = manager.AddPeerFiltering(nil, ip, "tcp", nil, port, fw.ActionAccept, "") require.NoError(t, err, "failed to add rule") } diff --git a/client/firewall/iptables/router_linux.go b/client/firewall/iptables/router_linux.go index cc2c25e55..eae9f7e25 100644 --- a/client/firewall/iptables/router_linux.go +++ b/client/firewall/iptables/router_linux.go @@ -15,7 +15,7 @@ import ( nberrors "github.com/netbirdio/netbird/client/errors" firewall "github.com/netbirdio/netbird/client/firewall/manager" - "github.com/netbirdio/netbird/client/internal/acl/id" + nbid "github.com/netbirdio/netbird/client/internal/acl/id" "github.com/netbirdio/netbird/client/internal/routemanager/ipfwdstate" "github.com/netbirdio/netbird/client/internal/routemanager/refcounter" "github.com/netbirdio/netbird/client/internal/statemanager" @@ -121,6 +121,7 @@ func (r *router) init(stateManager *statemanager.Manager) error { } func (r *router) AddRouteFiltering( + id []byte, sources []netip.Prefix, destination netip.Prefix, proto firewall.Protocol, @@ -128,7 +129,7 @@ func (r *router) AddRouteFiltering( dPort *firewall.Port, action firewall.Action, ) (firewall.Rule, error) { - ruleKey := id.GenerateRouteRuleKey(sources, destination, proto, sPort, dPort, action) + ruleKey := nbid.GenerateRouteRuleKey(sources, destination, proto, sPort, dPort, action) if _, ok := r.rules[string(ruleKey)]; ok { return ruleKey, nil } diff --git a/client/firewall/iptables/router_linux_test.go b/client/firewall/iptables/router_linux_test.go index 3f132504f..c039f3674 100644 --- a/client/firewall/iptables/router_linux_test.go +++ b/client/firewall/iptables/router_linux_test.go @@ -330,7 +330,7 @@ func TestRouter_AddRouteFiltering(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - ruleKey, err := r.AddRouteFiltering(tt.sources, tt.destination, tt.proto, tt.sPort, tt.dPort, tt.action) + ruleKey, err := r.AddRouteFiltering(nil, tt.sources, tt.destination, tt.proto, tt.sPort, tt.dPort, tt.action) require.NoError(t, err, "AddRouteFiltering failed") // Check if the rule is in the internal map diff --git a/client/firewall/manager/firewall.go b/client/firewall/manager/firewall.go index cf2387840..1d71051ef 100644 --- a/client/firewall/manager/firewall.go +++ b/client/firewall/manager/firewall.go @@ -65,13 +65,13 @@ type Manager interface { // If comment argument is empty firewall manager should set // rule ID as comment for the rule AddPeerFiltering( + id []byte, ip net.IP, proto Protocol, sPort *Port, dPort *Port, action Action, ipsetName string, - comment string, ) ([]Rule, error) // DeletePeerRule from the firewall by rule definition @@ -80,7 +80,15 @@ type Manager interface { // IsServerRouteSupported returns true if the firewall supports server side routing operations IsServerRouteSupported() bool - AddRouteFiltering(source []netip.Prefix, destination netip.Prefix, proto Protocol, sPort *Port, dPort *Port, action Action) (Rule, error) + AddRouteFiltering( + id []byte, + sources []netip.Prefix, + destination netip.Prefix, + proto Protocol, + sPort *Port, + dPort *Port, + action Action, + ) (Rule, error) // DeleteRouteRule deletes a routing rule DeleteRouteRule(rule Rule) error diff --git a/client/firewall/nftables/acl_linux.go b/client/firewall/nftables/acl_linux.go index 4643b8a26..24ffe3386 100644 --- a/client/firewall/nftables/acl_linux.go +++ b/client/firewall/nftables/acl_linux.go @@ -84,13 +84,13 @@ func (m *AclManager) init(workTable *nftables.Table) error { // If comment argument is empty firewall manager should set // rule ID as comment for the rule func (m *AclManager) AddPeerFiltering( + id []byte, ip net.IP, proto firewall.Protocol, sPort *firewall.Port, dPort *firewall.Port, action firewall.Action, ipsetName string, - comment string, ) ([]firewall.Rule, error) { var ipset *nftables.Set if ipsetName != "" { @@ -102,7 +102,7 @@ func (m *AclManager) AddPeerFiltering( } newRules := make([]firewall.Rule, 0, 2) - ioRule, err := m.addIOFiltering(ip, proto, sPort, dPort, action, ipset, comment) + ioRule, err := m.addIOFiltering(ip, proto, sPort, dPort, action, ipset) if err != nil { return nil, err } @@ -256,7 +256,6 @@ func (m *AclManager) addIOFiltering( dPort *firewall.Port, action firewall.Action, ipset *nftables.Set, - comment string, ) (*Rule, error) { ruleId := generatePeerRuleId(ip, sPort, dPort, action, ipset) if r, ok := m.rules[ruleId]; ok { @@ -338,7 +337,7 @@ func (m *AclManager) addIOFiltering( mainExpressions = append(mainExpressions, &expr.Verdict{Kind: expr.VerdictDrop}) } - userData := []byte(strings.Join([]string{ruleId, comment}, " ")) + userData := []byte(ruleId) chain := m.chainInputRules nftRule := m.rConn.AddRule(&nftables.Rule{ diff --git a/client/firewall/nftables/manager_linux.go b/client/firewall/nftables/manager_linux.go index 3cdd12c72..a5809471c 100644 --- a/client/firewall/nftables/manager_linux.go +++ b/client/firewall/nftables/manager_linux.go @@ -113,13 +113,13 @@ func (m *Manager) Init(stateManager *statemanager.Manager) error { // If comment argument is empty firewall manager should set // rule ID as comment for the rule func (m *Manager) AddPeerFiltering( + id []byte, ip net.IP, proto firewall.Protocol, sPort *firewall.Port, dPort *firewall.Port, action firewall.Action, ipsetName string, - comment string, ) ([]firewall.Rule, error) { m.mutex.Lock() defer m.mutex.Unlock() @@ -129,10 +129,11 @@ func (m *Manager) AddPeerFiltering( return nil, fmt.Errorf("unsupported IP version: %s", ip.String()) } - return m.aclManager.AddPeerFiltering(ip, proto, sPort, dPort, action, ipsetName, comment) + return m.aclManager.AddPeerFiltering(id, ip, proto, sPort, dPort, action, ipsetName) } func (m *Manager) AddRouteFiltering( + id []byte, sources []netip.Prefix, destination netip.Prefix, proto firewall.Protocol, @@ -147,7 +148,7 @@ func (m *Manager) AddRouteFiltering( return nil, fmt.Errorf("unsupported IP version: %s", destination.Addr().String()) } - return m.router.AddRouteFiltering(sources, destination, proto, sPort, dPort, action) + return m.router.AddRouteFiltering(id, sources, destination, proto, sPort, dPort, action) } // DeletePeerRule from the firewall by rule definition diff --git a/client/firewall/nftables/manager_linux_test.go b/client/firewall/nftables/manager_linux_test.go index c4b13045e..373743a08 100644 --- a/client/firewall/nftables/manager_linux_test.go +++ b/client/firewall/nftables/manager_linux_test.go @@ -74,7 +74,7 @@ func TestNftablesManager(t *testing.T) { testClient := &nftables.Conn{} - rule, err := manager.AddPeerFiltering(ip, fw.ProtocolTCP, nil, &fw.Port{Values: []uint16{53}}, fw.ActionDrop, "", "") + rule, err := manager.AddPeerFiltering(nil, ip, fw.ProtocolTCP, nil, &fw.Port{Values: []uint16{53}}, fw.ActionDrop, "") require.NoError(t, err, "failed to add rule") err = manager.Flush() @@ -201,7 +201,7 @@ func TestNFtablesCreatePerformance(t *testing.T) { start := time.Now() for i := 0; i < testMax; i++ { port := &fw.Port{Values: []uint16{uint16(1000 + i)}} - _, err = manager.AddPeerFiltering(ip, "tcp", nil, port, fw.ActionAccept, "", "accept HTTP traffic") + _, err = manager.AddPeerFiltering(nil, ip, "tcp", nil, port, fw.ActionAccept, "") require.NoError(t, err, "failed to add rule") if i%100 == 0 { @@ -283,10 +283,11 @@ func TestNftablesManagerCompatibilityWithIptables(t *testing.T) { }) ip := net.ParseIP("100.96.0.1") - _, err = manager.AddPeerFiltering(ip, fw.ProtocolTCP, nil, &fw.Port{Values: []uint16{80}}, fw.ActionAccept, "", "test rule") + _, err = manager.AddPeerFiltering(nil, ip, fw.ProtocolTCP, nil, &fw.Port{Values: []uint16{80}}, fw.ActionAccept, "") require.NoError(t, err, "failed to add peer filtering rule") _, err = manager.AddRouteFiltering( + nil, []netip.Prefix{netip.MustParsePrefix("192.168.2.0/24")}, netip.MustParsePrefix("10.1.0.0/24"), fw.ProtocolTCP, diff --git a/client/firewall/nftables/router_linux.go b/client/firewall/nftables/router_linux.go index 6dd75ddb1..6def30bf0 100644 --- a/client/firewall/nftables/router_linux.go +++ b/client/firewall/nftables/router_linux.go @@ -20,7 +20,7 @@ import ( nberrors "github.com/netbirdio/netbird/client/errors" firewall "github.com/netbirdio/netbird/client/firewall/manager" - "github.com/netbirdio/netbird/client/internal/acl/id" + nbid "github.com/netbirdio/netbird/client/internal/acl/id" "github.com/netbirdio/netbird/client/internal/routemanager/ipfwdstate" "github.com/netbirdio/netbird/client/internal/routemanager/refcounter" nbnet "github.com/netbirdio/netbird/util/net" @@ -228,6 +228,7 @@ func (r *router) createContainers() error { // AddRouteFiltering appends a nftables rule to the routing chain func (r *router) AddRouteFiltering( + id []byte, sources []netip.Prefix, destination netip.Prefix, proto firewall.Protocol, @@ -236,7 +237,7 @@ func (r *router) AddRouteFiltering( action firewall.Action, ) (firewall.Rule, error) { - ruleKey := id.GenerateRouteRuleKey(sources, destination, proto, sPort, dPort, action) + ruleKey := nbid.GenerateRouteRuleKey(sources, destination, proto, sPort, dPort, action) if _, ok := r.rules[string(ruleKey)]; ok { return ruleKey, nil } diff --git a/client/firewall/nftables/router_linux_test.go b/client/firewall/nftables/router_linux_test.go index 7ead26909..498fdf882 100644 --- a/client/firewall/nftables/router_linux_test.go +++ b/client/firewall/nftables/router_linux_test.go @@ -311,7 +311,7 @@ func TestRouter_AddRouteFiltering(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - ruleKey, err := r.AddRouteFiltering(tt.sources, tt.destination, tt.proto, tt.sPort, tt.dPort, tt.action) + ruleKey, err := r.AddRouteFiltering(nil, tt.sources, tt.destination, tt.proto, tt.sPort, tt.dPort, tt.action) require.NoError(t, err, "AddRouteFiltering failed") t.Cleanup(func() { diff --git a/client/firewall/uspfilter/allow_netbird.go b/client/firewall/uspfilter/allow_netbird.go index aba79bc21..5fe698aa9 100644 --- a/client/firewall/uspfilter/allow_netbird.go +++ b/client/firewall/uspfilter/allow_netbird.go @@ -4,6 +4,7 @@ package uspfilter import ( "context" + "net/netip" "time" log "github.com/sirupsen/logrus" @@ -16,8 +17,8 @@ func (m *Manager) Close(stateManager *statemanager.Manager) error { m.mutex.Lock() defer m.mutex.Unlock() - m.outgoingRules = make(map[string]RuleSet) - m.incomingRules = make(map[string]RuleSet) + m.outgoingRules = make(map[netip.Addr]RuleSet) + m.incomingRules = make(map[netip.Addr]RuleSet) if m.udpTracker != nil { m.udpTracker.Close() @@ -31,8 +32,8 @@ func (m *Manager) Close(stateManager *statemanager.Manager) error { m.tcpTracker.Close() } - if m.forwarder != nil { - m.forwarder.Stop() + if fwder := m.forwarder.Load(); fwder != nil { + fwder.Stop() } if m.logger != nil { diff --git a/client/firewall/uspfilter/allow_netbird_windows.go b/client/firewall/uspfilter/allow_netbird_windows.go index ee540cb1d..f63792fec 100644 --- a/client/firewall/uspfilter/allow_netbird_windows.go +++ b/client/firewall/uspfilter/allow_netbird_windows.go @@ -3,12 +3,14 @@ package uspfilter import ( "context" "fmt" + "net/netip" "os/exec" "syscall" "time" log "github.com/sirupsen/logrus" + "github.com/netbirdio/netbird/client/firewall/uspfilter/conntrack" "github.com/netbirdio/netbird/client/internal/statemanager" ) @@ -20,28 +22,31 @@ const ( firewallRuleName = "Netbird" ) -// Close closes the firewall manager +// Reset firewall to the default state func (m *Manager) Close(*statemanager.Manager) error { m.mutex.Lock() defer m.mutex.Unlock() - m.outgoingRules = make(map[string]RuleSet) - m.incomingRules = make(map[string]RuleSet) + m.outgoingRules = make(map[netip.Addr]RuleSet) + m.incomingRules = make(map[netip.Addr]RuleSet) if m.udpTracker != nil { m.udpTracker.Close() + m.udpTracker = conntrack.NewUDPTracker(conntrack.DefaultUDPTimeout, m.logger, m.flowLogger) } if m.icmpTracker != nil { m.icmpTracker.Close() + m.icmpTracker = conntrack.NewICMPTracker(conntrack.DefaultICMPTimeout, m.logger, m.flowLogger) } if m.tcpTracker != nil { m.tcpTracker.Close() + m.tcpTracker = conntrack.NewTCPTracker(conntrack.DefaultTCPTimeout, m.logger, m.flowLogger) } - if m.forwarder != nil { - m.forwarder.Stop() + if fwder := m.forwarder.Load(); fwder != nil { + fwder.Stop() } if m.logger != nil { diff --git a/client/firewall/uspfilter/conntrack/common.go b/client/firewall/uspfilter/conntrack/common.go index f5f502540..3de0bb3f4 100644 --- a/client/firewall/uspfilter/conntrack/common.go +++ b/client/firewall/uspfilter/conntrack/common.go @@ -1,20 +1,27 @@ -// common.go package conntrack import ( - "net" - "sync" + "fmt" + "net/netip" "sync/atomic" "time" + + "github.com/google/uuid" + + nftypes "github.com/netbirdio/netbird/client/internal/netflow/types" ) // BaseConnTrack provides common fields and locking for all connection types type BaseConnTrack struct { - SourceIP net.IP - DestIP net.IP - SourcePort uint16 - DestPort uint16 - lastSeen atomic.Int64 // Unix nano for atomic access + FlowId uuid.UUID + Direction nftypes.Direction + SourceIP netip.Addr + DestIP netip.Addr + lastSeen atomic.Int64 + PacketsTx atomic.Uint64 + PacketsRx atomic.Uint64 + BytesTx atomic.Uint64 + BytesRx atomic.Uint64 } // these small methods will be inlined by the compiler @@ -24,6 +31,17 @@ func (b *BaseConnTrack) UpdateLastSeen() { b.lastSeen.Store(time.Now().UnixNano()) } +// UpdateCounters safely updates the packet and byte counters +func (b *BaseConnTrack) UpdateCounters(direction nftypes.Direction, bytes int) { + if direction == nftypes.Egress { + b.PacketsTx.Add(1) + b.BytesTx.Add(uint64(bytes)) + } else { + b.PacketsRx.Add(1) + b.BytesRx.Add(uint64(bytes)) + } +} + // GetLastSeen safely gets the last seen timestamp func (b *BaseConnTrack) GetLastSeen() time.Time { return time.Unix(0, b.lastSeen.Load()) @@ -35,92 +53,14 @@ func (b *BaseConnTrack) timeoutExceeded(timeout time.Duration) bool { return time.Since(lastSeen) > timeout } -// IPAddr is a fixed-size IP address to avoid allocations -type IPAddr [16]byte - -// MakeIPAddr creates an IPAddr from net.IP -func MakeIPAddr(ip net.IP) (addr IPAddr) { - // Optimization: check for v4 first as it's more common - if ip4 := ip.To4(); ip4 != nil { - copy(addr[12:], ip4) - } else { - copy(addr[:], ip.To16()) - } - return addr -} - // ConnKey uniquely identifies a connection type ConnKey struct { - SrcIP IPAddr - DstIP IPAddr + SrcIP netip.Addr + DstIP netip.Addr SrcPort uint16 DstPort uint16 } -// makeConnKey creates a connection key -func makeConnKey(srcIP net.IP, dstIP net.IP, srcPort uint16, dstPort uint16) ConnKey { - return ConnKey{ - SrcIP: MakeIPAddr(srcIP), - DstIP: MakeIPAddr(dstIP), - SrcPort: srcPort, - DstPort: dstPort, - } -} - -// ValidateIPs checks if IPs match without allocation -func ValidateIPs(connIP IPAddr, pktIP net.IP) bool { - if ip4 := pktIP.To4(); ip4 != nil { - // Compare IPv4 addresses (last 4 bytes) - for i := 0; i < 4; i++ { - if connIP[12+i] != ip4[i] { - return false - } - } - return true - } - // Compare full IPv6 addresses - ip6 := pktIP.To16() - for i := 0; i < 16; i++ { - if connIP[i] != ip6[i] { - return false - } - } - return true -} - -// PreallocatedIPs is a pool of IP byte slices to reduce allocations -type PreallocatedIPs struct { - sync.Pool -} - -// NewPreallocatedIPs creates a new IP pool -func NewPreallocatedIPs() *PreallocatedIPs { - return &PreallocatedIPs{ - Pool: sync.Pool{ - New: func() interface{} { - ip := make(net.IP, 16) - return &ip - }, - }, - } -} - -// Get retrieves an IP from the pool -func (p *PreallocatedIPs) Get() net.IP { - return *p.Pool.Get().(*net.IP) -} - -// Put returns an IP to the pool -func (p *PreallocatedIPs) Put(ip net.IP) { - p.Pool.Put(&ip) -} - -// copyIP copies an IP address efficiently -func copyIP(dst, src net.IP) { - if len(src) == 16 { - copy(dst, src) - } else { - // Handle IPv4 - copy(dst[12:], src.To4()) - } +func (c ConnKey) String() string { + return fmt.Sprintf("%s:%d -> %s:%d", c.SrcIP.Unmap(), c.SrcPort, c.DstIP.Unmap(), c.DstPort) } diff --git a/client/firewall/uspfilter/conntrack/common_test.go b/client/firewall/uspfilter/conntrack/common_test.go index 81fa64b19..f28cd56e5 100644 --- a/client/firewall/uspfilter/conntrack/common_test.go +++ b/client/firewall/uspfilter/conntrack/common_test.go @@ -1,94 +1,67 @@ package conntrack import ( - "net" + "context" + "net/netip" "testing" "github.com/sirupsen/logrus" "github.com/netbirdio/netbird/client/firewall/uspfilter/log" + "github.com/netbirdio/netbird/client/internal/netflow" ) var logger = log.NewFromLogrus(logrus.StandardLogger()) - -func BenchmarkIPOperations(b *testing.B) { - b.Run("MakeIPAddr", func(b *testing.B) { - ip := net.ParseIP("192.168.1.1") - b.ResetTimer() - for i := 0; i < b.N; i++ { - _ = MakeIPAddr(ip) - } - }) - - b.Run("ValidateIPs", func(b *testing.B) { - ip1 := net.ParseIP("192.168.1.1") - ip2 := net.ParseIP("192.168.1.1") - addr := MakeIPAddr(ip1) - b.ResetTimer() - for i := 0; i < b.N; i++ { - _ = ValidateIPs(addr, ip2) - } - }) - - b.Run("IPPool", func(b *testing.B) { - pool := NewPreallocatedIPs() - b.ResetTimer() - for i := 0; i < b.N; i++ { - ip := pool.Get() - pool.Put(ip) - } - }) - -} +var flowLogger = netflow.NewManager(context.Background(), nil, []byte{}, nil).GetLogger() // Memory pressure tests func BenchmarkMemoryPressure(b *testing.B) { b.Run("TCPHighLoad", func(b *testing.B) { - tracker := NewTCPTracker(DefaultTCPTimeout, logger) + tracker := NewTCPTracker(DefaultTCPTimeout, logger, flowLogger) defer tracker.Close() // Generate different IPs - srcIPs := make([]net.IP, 100) - dstIPs := make([]net.IP, 100) + srcIPs := make([]netip.Addr, 100) + dstIPs := make([]netip.Addr, 100) for i := 0; i < 100; i++ { - srcIPs[i] = net.IPv4(192, 168, byte(i/256), byte(i%256)) - dstIPs[i] = net.IPv4(10, 0, byte(i/256), byte(i%256)) + srcIPs[i] = netip.AddrFrom4([4]byte{192, 168, byte(i / 256), byte(i % 256)}) + dstIPs[i] = netip.AddrFrom4([4]byte{10, 0, byte(i / 256), byte(i % 256)}) } b.ResetTimer() for i := 0; i < b.N; i++ { srcIdx := i % len(srcIPs) dstIdx := (i + 1) % len(dstIPs) - tracker.TrackOutbound(srcIPs[srcIdx], dstIPs[dstIdx], uint16(i%65535), 80, TCPSyn) + tracker.TrackOutbound(srcIPs[srcIdx], dstIPs[dstIdx], uint16(i%65535), 80, TCPSyn, 0) // Simulate some valid inbound packets if i%3 == 0 { - tracker.IsValidInbound(dstIPs[dstIdx], srcIPs[srcIdx], 80, uint16(i%65535), TCPAck) + tracker.IsValidInbound(dstIPs[dstIdx], srcIPs[srcIdx], 80, uint16(i%65535), TCPAck, 0) } } }) b.Run("UDPHighLoad", func(b *testing.B) { - tracker := NewUDPTracker(DefaultUDPTimeout, logger) + tracker := NewUDPTracker(DefaultUDPTimeout, logger, flowLogger) defer tracker.Close() // Generate different IPs - srcIPs := make([]net.IP, 100) - dstIPs := make([]net.IP, 100) + srcIPs := make([]netip.Addr, 100) + dstIPs := make([]netip.Addr, 100) for i := 0; i < 100; i++ { - srcIPs[i] = net.IPv4(192, 168, byte(i/256), byte(i%256)) - dstIPs[i] = net.IPv4(10, 0, byte(i/256), byte(i%256)) + srcIPs[i] = netip.AddrFrom4([4]byte{192, 168, byte(i / 256), byte(i % 256)}) + dstIPs[i] = netip.AddrFrom4([4]byte{10, 0, byte(i / 256), byte(i % 256)}) } b.ResetTimer() for i := 0; i < b.N; i++ { srcIdx := i % len(srcIPs) dstIdx := (i + 1) % len(dstIPs) - tracker.TrackOutbound(srcIPs[srcIdx], dstIPs[dstIdx], uint16(i%65535), 80) + tracker.TrackOutbound(srcIPs[srcIdx], dstIPs[dstIdx], uint16(i%65535), 80, 0) // Simulate some valid inbound packets if i%3 == 0 { - tracker.IsValidInbound(dstIPs[dstIdx], srcIPs[srcIdx], 80, uint16(i%65535)) + tracker.IsValidInbound(dstIPs[dstIdx], srcIPs[srcIdx], 80, uint16(i%65535), 0) } } }) diff --git a/client/firewall/uspfilter/conntrack/icmp.go b/client/firewall/uspfilter/conntrack/icmp.go index a8cb01565..ae9926795 100644 --- a/client/firewall/uspfilter/conntrack/icmp.go +++ b/client/firewall/uspfilter/conntrack/icmp.go @@ -2,13 +2,16 @@ package conntrack import ( "context" - "net" + "fmt" + "net/netip" "sync" "time" "github.com/google/gopacket/layers" + "github.com/google/uuid" nblog "github.com/netbirdio/netbird/client/firewall/uspfilter/log" + nftypes "github.com/netbirdio/netbird/client/internal/netflow/types" ) const ( @@ -20,18 +23,20 @@ const ( // ICMPConnKey uniquely identifies an ICMP connection type ICMPConnKey struct { - // Supports both IPv4 and IPv6 - SrcIP [16]byte - DstIP [16]byte - Sequence uint16 // ICMP sequence number - ID uint16 // ICMP identifier + SrcIP netip.Addr + DstIP netip.Addr + ID uint16 +} + +func (i ICMPConnKey) String() string { + return fmt.Sprintf("%s -> %s (id %d)", i.SrcIP, i.DstIP, i.ID) } // ICMPConnTrack represents an ICMP connection state type ICMPConnTrack struct { BaseConnTrack - Sequence uint16 - ID uint16 + ICMPType uint8 + ICMPCode uint8 } // ICMPTracker manages ICMP connection states @@ -42,11 +47,11 @@ type ICMPTracker struct { cleanupTicker *time.Ticker tickerCancel context.CancelFunc mutex sync.RWMutex - ipPool *PreallocatedIPs + flowLogger nftypes.FlowLogger } // NewICMPTracker creates a new ICMP connection tracker -func NewICMPTracker(timeout time.Duration, logger *nblog.Logger) *ICMPTracker { +func NewICMPTracker(timeout time.Duration, logger *nblog.Logger, flowLogger nftypes.FlowLogger) *ICMPTracker { if timeout == 0 { timeout = DefaultICMPTimeout } @@ -59,67 +64,108 @@ func NewICMPTracker(timeout time.Duration, logger *nblog.Logger) *ICMPTracker { timeout: timeout, cleanupTicker: time.NewTicker(ICMPCleanupInterval), tickerCancel: cancel, - ipPool: NewPreallocatedIPs(), + flowLogger: flowLogger, } go tracker.cleanupRoutine(ctx) return tracker } -// TrackOutbound records an outbound ICMP Echo Request -func (t *ICMPTracker) TrackOutbound(srcIP net.IP, dstIP net.IP, id uint16, seq uint16) { - key := makeICMPKey(srcIP, dstIP, id, seq) - - t.mutex.Lock() - conn, exists := t.connections[key] - if !exists { - srcIPCopy := t.ipPool.Get() - dstIPCopy := t.ipPool.Get() - copyIP(srcIPCopy, srcIP) - copyIP(dstIPCopy, dstIP) - - conn = &ICMPConnTrack{ - BaseConnTrack: BaseConnTrack{ - SourceIP: srcIPCopy, - DestIP: dstIPCopy, - }, - ID: id, - Sequence: seq, - } - conn.UpdateLastSeen() - t.connections[key] = conn - - t.logger.Trace("New ICMP connection %v", key) +func (t *ICMPTracker) updateIfExists(srcIP netip.Addr, dstIP netip.Addr, id uint16, direction nftypes.Direction, size int) (ICMPConnKey, bool) { + key := ICMPConnKey{ + SrcIP: srcIP, + DstIP: dstIP, + ID: id, } - t.mutex.Unlock() - - conn.UpdateLastSeen() -} - -// IsValidInbound checks if an inbound ICMP Echo Reply matches a tracked request -func (t *ICMPTracker) IsValidInbound(srcIP net.IP, dstIP net.IP, id uint16, seq uint16, icmpType uint8) bool { - if icmpType != uint8(layers.ICMPv4TypeEchoReply) { - return false - } - - key := makeICMPKey(dstIP, srcIP, id, seq) t.mutex.RLock() conn, exists := t.connections[key] t.mutex.RUnlock() - if !exists { + if exists { + conn.UpdateLastSeen() + conn.UpdateCounters(direction, size) + + return key, true + } + + return key, false +} + +// TrackOutbound records an outbound ICMP connection +func (t *ICMPTracker) TrackOutbound(srcIP netip.Addr, dstIP netip.Addr, id uint16, typecode layers.ICMPv4TypeCode, size int) { + if _, exists := t.updateIfExists(dstIP, srcIP, id, nftypes.Egress, size); !exists { + // if (inverted direction) conn is not tracked, track this direction + t.track(srcIP, dstIP, id, typecode, nftypes.Egress, nil, size) + } +} + +// TrackInbound records an inbound ICMP Echo Request +func (t *ICMPTracker) TrackInbound(srcIP netip.Addr, dstIP netip.Addr, id uint16, typecode layers.ICMPv4TypeCode, ruleId []byte, size int) { + t.track(srcIP, dstIP, id, typecode, nftypes.Ingress, ruleId, size) +} + +// track is the common implementation for tracking both inbound and outbound ICMP connections +func (t *ICMPTracker) track(srcIP netip.Addr, dstIP netip.Addr, id uint16, typecode layers.ICMPv4TypeCode, direction nftypes.Direction, ruleId []byte, size int) { + key, exists := t.updateIfExists(srcIP, dstIP, id, direction, size) + if exists { + return + } + + typ, code := typecode.Type(), typecode.Code() + + // non echo requests don't need tracking + if typ != uint8(layers.ICMPv4TypeEchoRequest) { + t.logger.Trace("New %s ICMP connection %s type %d code %d", direction, key, typ, code) + t.sendStartEvent(direction, srcIP, dstIP, typ, code, ruleId, size) + return + } + + conn := &ICMPConnTrack{ + BaseConnTrack: BaseConnTrack{ + FlowId: uuid.New(), + Direction: direction, + SourceIP: srcIP, + DestIP: dstIP, + }, + ICMPType: typ, + ICMPCode: code, + } + conn.UpdateLastSeen() + conn.UpdateCounters(direction, size) + + t.mutex.Lock() + t.connections[key] = conn + t.mutex.Unlock() + + t.logger.Trace("New %s ICMP connection %s type %d code %d", direction, key, typ, code) + t.sendEvent(nftypes.TypeStart, conn, ruleId) +} + +// IsValidInbound checks if an inbound ICMP Echo Reply matches a tracked request +func (t *ICMPTracker) IsValidInbound(srcIP netip.Addr, dstIP netip.Addr, id uint16, icmpType uint8, size int) bool { + if icmpType != uint8(layers.ICMPv4TypeEchoReply) { return false } - if conn.timeoutExceeded(t.timeout) { + key := ICMPConnKey{ + SrcIP: dstIP, + DstIP: srcIP, + ID: id, + } + + t.mutex.RLock() + conn, exists := t.connections[key] + t.mutex.RUnlock() + + if !exists || conn.timeoutExceeded(t.timeout) { return false } - return ValidateIPs(MakeIPAddr(srcIP), conn.DestIP) && - ValidateIPs(MakeIPAddr(dstIP), conn.SourceIP) && - conn.ID == id && - conn.Sequence == seq + conn.UpdateLastSeen() + conn.UpdateCounters(nftypes.Ingress, size) + + return true } func (t *ICMPTracker) cleanupRoutine(ctx context.Context) { @@ -134,17 +180,18 @@ func (t *ICMPTracker) cleanupRoutine(ctx context.Context) { } } } + func (t *ICMPTracker) cleanup() { t.mutex.Lock() defer t.mutex.Unlock() for key, conn := range t.connections { if conn.timeoutExceeded(t.timeout) { - t.ipPool.Put(conn.SourceIP) - t.ipPool.Put(conn.DestIP) delete(t.connections, key) - t.logger.Debug("Removed ICMP connection %v (timeout)", key) + t.logger.Debug("Removed ICMP connection %s (timeout) [in: %d Pkts/%d B out: %d Pkts/%d B]", + key, conn.PacketsRx.Load(), conn.BytesRx.Load(), conn.PacketsTx.Load(), conn.BytesTx.Load()) + t.sendEvent(nftypes.TypeEnd, conn, nil) } } } @@ -154,20 +201,46 @@ func (t *ICMPTracker) Close() { t.tickerCancel() t.mutex.Lock() - for _, conn := range t.connections { - t.ipPool.Put(conn.SourceIP) - t.ipPool.Put(conn.DestIP) - } t.connections = nil t.mutex.Unlock() } -// makeICMPKey creates an ICMP connection key -func makeICMPKey(srcIP net.IP, dstIP net.IP, id uint16, seq uint16) ICMPConnKey { - return ICMPConnKey{ - SrcIP: MakeIPAddr(srcIP), - DstIP: MakeIPAddr(dstIP), - ID: id, - Sequence: seq, - } +func (t *ICMPTracker) sendEvent(typ nftypes.Type, conn *ICMPConnTrack, ruleID []byte) { + t.flowLogger.StoreEvent(nftypes.EventFields{ + FlowID: conn.FlowId, + Type: typ, + RuleID: ruleID, + Direction: conn.Direction, + Protocol: nftypes.ICMP, // TODO: adjust for IPv6/icmpv6 + SourceIP: conn.SourceIP, + DestIP: conn.DestIP, + ICMPType: conn.ICMPType, + ICMPCode: conn.ICMPCode, + RxPackets: conn.PacketsRx.Load(), + TxPackets: conn.PacketsTx.Load(), + RxBytes: conn.BytesRx.Load(), + TxBytes: conn.BytesTx.Load(), + }) +} + +func (t *ICMPTracker) sendStartEvent(direction nftypes.Direction, srcIP netip.Addr, dstIP netip.Addr, typ uint8, code uint8, ruleID []byte, size int) { + fields := nftypes.EventFields{ + FlowID: uuid.New(), + Type: nftypes.TypeStart, + RuleID: ruleID, + Direction: direction, + Protocol: nftypes.ICMP, + SourceIP: srcIP, + DestIP: dstIP, + ICMPType: typ, + ICMPCode: code, + } + if direction == nftypes.Ingress { + fields.RxPackets = 1 + fields.RxBytes = uint64(size) + } else { + fields.TxPackets = 1 + fields.TxBytes = uint64(size) + } + t.flowLogger.StoreEvent(fields) } diff --git a/client/firewall/uspfilter/conntrack/icmp_test.go b/client/firewall/uspfilter/conntrack/icmp_test.go index 32553c836..5a7b36a36 100644 --- a/client/firewall/uspfilter/conntrack/icmp_test.go +++ b/client/firewall/uspfilter/conntrack/icmp_test.go @@ -1,39 +1,39 @@ package conntrack import ( - "net" + "net/netip" "testing" ) func BenchmarkICMPTracker(b *testing.B) { b.Run("TrackOutbound", func(b *testing.B) { - tracker := NewICMPTracker(DefaultICMPTimeout, logger) + tracker := NewICMPTracker(DefaultICMPTimeout, logger, flowLogger) defer tracker.Close() - srcIP := net.ParseIP("192.168.1.1") - dstIP := net.ParseIP("192.168.1.2") + srcIP := netip.MustParseAddr("192.168.1.1") + dstIP := netip.MustParseAddr("192.168.1.2") b.ResetTimer() for i := 0; i < b.N; i++ { - tracker.TrackOutbound(srcIP, dstIP, uint16(i%65535), uint16(i%65535)) + tracker.TrackOutbound(srcIP, dstIP, uint16(i%65535), 0, 0) } }) b.Run("IsValidInbound", func(b *testing.B) { - tracker := NewICMPTracker(DefaultICMPTimeout, logger) + tracker := NewICMPTracker(DefaultICMPTimeout, logger, flowLogger) defer tracker.Close() - srcIP := net.ParseIP("192.168.1.1") - dstIP := net.ParseIP("192.168.1.2") + srcIP := netip.MustParseAddr("192.168.1.1") + dstIP := netip.MustParseAddr("192.168.1.2") // Pre-populate some connections for i := 0; i < 1000; i++ { - tracker.TrackOutbound(srcIP, dstIP, uint16(i), uint16(i)) + tracker.TrackOutbound(srcIP, dstIP, uint16(i), 0, 0) } b.ResetTimer() for i := 0; i < b.N; i++ { - tracker.IsValidInbound(dstIP, srcIP, uint16(i%1000), uint16(i%1000), 0) + tracker.IsValidInbound(dstIP, srcIP, uint16(i%1000), 0, 0) } }) } diff --git a/client/firewall/uspfilter/conntrack/tcp.go b/client/firewall/uspfilter/conntrack/tcp.go index 1b5cbae95..8109fff41 100644 --- a/client/firewall/uspfilter/conntrack/tcp.go +++ b/client/firewall/uspfilter/conntrack/tcp.go @@ -4,12 +4,15 @@ package conntrack import ( "context" - "net" + "net/netip" "sync" "sync/atomic" "time" + "github.com/google/uuid" + nblog "github.com/netbirdio/netbird/client/firewall/uspfilter/log" + nftypes "github.com/netbirdio/netbird/client/internal/netflow/types" ) const ( @@ -40,6 +43,35 @@ const ( // TCPState represents the state of a TCP connection type TCPState int +func (s TCPState) String() string { + switch s { + case TCPStateNew: + return "New" + case TCPStateSynSent: + return "SYN Sent" + case TCPStateSynReceived: + return "SYN Received" + case TCPStateEstablished: + return "Established" + case TCPStateFinWait1: + return "FIN Wait 1" + case TCPStateFinWait2: + return "FIN Wait 2" + case TCPStateClosing: + return "Closing" + case TCPStateTimeWait: + return "Time Wait" + case TCPStateCloseWait: + return "Close Wait" + case TCPStateLastAck: + return "Last ACK" + case TCPStateClosed: + return "Closed" + default: + return "Unknown" + } +} + const ( TCPStateNew TCPState = iota TCPStateSynSent @@ -54,19 +86,14 @@ const ( TCPStateClosed ) -// TCPConnKey uniquely identifies a TCP connection -type TCPConnKey struct { - SrcIP [16]byte - DstIP [16]byte - SrcPort uint16 - DstPort uint16 -} - // TCPConnTrack represents a TCP connection state type TCPConnTrack struct { BaseConnTrack + SourcePort uint16 + DestPort uint16 State TCPState established atomic.Bool + tombstone atomic.Bool sync.RWMutex } @@ -80,6 +107,16 @@ func (t *TCPConnTrack) SetEstablished(state bool) { t.established.Store(state) } +// IsTombstone safely checks if the connection is marked for deletion +func (t *TCPConnTrack) IsTombstone() bool { + return t.tombstone.Load() +} + +// SetTombstone safely marks the connection for deletion +func (t *TCPConnTrack) SetTombstone() { + t.tombstone.Store(true) +} + // TCPTracker manages TCP connection states type TCPTracker struct { logger *nblog.Logger @@ -88,11 +125,14 @@ type TCPTracker struct { cleanupTicker *time.Ticker tickerCancel context.CancelFunc timeout time.Duration - ipPool *PreallocatedIPs + flowLogger nftypes.FlowLogger } // NewTCPTracker creates a new TCP connection tracker -func NewTCPTracker(timeout time.Duration, logger *nblog.Logger) *TCPTracker { +func NewTCPTracker(timeout time.Duration, logger *nblog.Logger, flowLogger nftypes.FlowLogger) *TCPTracker { + if timeout == 0 { + timeout = DefaultTCPTimeout + } ctx, cancel := context.WithCancel(context.Background()) @@ -102,59 +142,92 @@ func NewTCPTracker(timeout time.Duration, logger *nblog.Logger) *TCPTracker { cleanupTicker: time.NewTicker(TCPCleanupInterval), tickerCancel: cancel, timeout: timeout, - ipPool: NewPreallocatedIPs(), + flowLogger: flowLogger, } go tracker.cleanupRoutine(ctx) return tracker } -// TrackOutbound processes an outbound TCP packet and updates connection state -func (t *TCPTracker) TrackOutbound(srcIP net.IP, dstIP net.IP, srcPort uint16, dstPort uint16, flags uint8) { - // Create key before lock - key := makeConnKey(srcIP, dstIP, srcPort, dstPort) +func (t *TCPTracker) updateIfExists(srcIP netip.Addr, dstIP netip.Addr, srcPort uint16, dstPort uint16, flags uint8, direction nftypes.Direction, size int) (ConnKey, bool) { + key := ConnKey{ + SrcIP: srcIP, + DstIP: dstIP, + SrcPort: srcPort, + DstPort: dstPort, + } + + t.mutex.RLock() + conn, exists := t.connections[key] + t.mutex.RUnlock() + + if exists { + conn.Lock() + t.updateState(key, conn, flags, conn.Direction == nftypes.Egress) + conn.Unlock() + + conn.UpdateCounters(direction, size) + + return key, true + } + + return key, false +} + +// TrackOutbound records an outbound TCP connection +func (t *TCPTracker) TrackOutbound(srcIP netip.Addr, dstIP netip.Addr, srcPort uint16, dstPort uint16, flags uint8, size int) { + if _, exists := t.updateIfExists(dstIP, srcIP, dstPort, srcPort, flags, nftypes.Egress, size); !exists { + // if (inverted direction) conn is not tracked, track this direction + t.track(srcIP, dstIP, srcPort, dstPort, flags, nftypes.Egress, nil, size) + } +} + +// TrackInbound processes an inbound TCP packet and updates connection state +func (t *TCPTracker) TrackInbound(srcIP netip.Addr, dstIP netip.Addr, srcPort uint16, dstPort uint16, flags uint8, ruleID []byte, size int) { + t.track(srcIP, dstIP, srcPort, dstPort, flags, nftypes.Ingress, ruleID, size) +} + +// track is the common implementation for tracking both inbound and outbound connections +func (t *TCPTracker) track(srcIP netip.Addr, dstIP netip.Addr, srcPort uint16, dstPort uint16, flags uint8, direction nftypes.Direction, ruleID []byte, size int) { + key, exists := t.updateIfExists(srcIP, dstIP, srcPort, dstPort, flags, direction, size) + if exists { + return + } + + conn := &TCPConnTrack{ + BaseConnTrack: BaseConnTrack{ + FlowId: uuid.New(), + Direction: direction, + SourceIP: srcIP, + DestIP: dstIP, + }, + SourcePort: srcPort, + DestPort: dstPort, + } + + conn.established.Store(false) + conn.tombstone.Store(false) + + t.logger.Trace("New %s TCP connection: %s", direction, key) + t.updateState(key, conn, flags, direction == nftypes.Egress) + conn.UpdateCounters(direction, size) t.mutex.Lock() - conn, exists := t.connections[key] - if !exists { - // Use preallocated IPs - srcIPCopy := t.ipPool.Get() - dstIPCopy := t.ipPool.Get() - copyIP(srcIPCopy, srcIP) - copyIP(dstIPCopy, dstIP) - - conn = &TCPConnTrack{ - BaseConnTrack: BaseConnTrack{ - SourceIP: srcIPCopy, - DestIP: dstIPCopy, - SourcePort: srcPort, - DestPort: dstPort, - }, - State: TCPStateNew, - } - conn.UpdateLastSeen() - conn.established.Store(false) - t.connections[key] = conn - - t.logger.Trace("New TCP connection: %s:%d -> %s:%d", srcIP, srcPort, dstIP, dstPort) - } + t.connections[key] = conn t.mutex.Unlock() - // Lock individual connection for state update - conn.Lock() - t.updateState(conn, flags, true) - conn.Unlock() - conn.UpdateLastSeen() + t.sendEvent(nftypes.TypeStart, conn, ruleID) } // IsValidInbound checks if an inbound TCP packet matches a tracked connection -func (t *TCPTracker) IsValidInbound(srcIP net.IP, dstIP net.IP, srcPort uint16, dstPort uint16, flags uint8) bool { - if !isValidFlagCombination(flags) { - return false +func (t *TCPTracker) IsValidInbound(srcIP netip.Addr, dstIP netip.Addr, srcPort uint16, dstPort uint16, flags uint8, size int) bool { + key := ConnKey{ + SrcIP: dstIP, + DstIP: srcIP, + SrcPort: dstPort, + DstPort: srcPort, } - key := makeConnKey(dstIP, srcIP, dstPort, srcPort) - t.mutex.RLock() conn, exists := t.connections[key] t.mutex.RUnlock() @@ -163,42 +236,50 @@ func (t *TCPTracker) IsValidInbound(srcIP net.IP, dstIP net.IP, srcPort uint16, return false } - // Handle RST packets + // Handle RST flag specially - it always causes transition to closed if flags&TCPRst != 0 { - conn.Lock() - if conn.IsEstablished() || conn.State == TCPStateSynSent || conn.State == TCPStateSynReceived { - conn.State = TCPStateClosed - conn.SetEstablished(false) - conn.Unlock() - return true - } - conn.Unlock() - return false + return t.handleRst(key, conn, size) } conn.Lock() - t.updateState(conn, flags, false) - conn.UpdateLastSeen() + t.updateState(key, conn, flags, false) isEstablished := conn.IsEstablished() isValidState := t.isValidStateForFlags(conn.State, flags) conn.Unlock() + conn.UpdateCounters(nftypes.Ingress, size) return isEstablished || isValidState } -// updateState updates the TCP connection state based on flags -func (t *TCPTracker) updateState(conn *TCPConnTrack, flags uint8, isOutbound bool) { - // Handle RST flag specially - it always causes transition to closed - if flags&TCPRst != 0 { - conn.State = TCPStateClosed - conn.SetEstablished(false) - - t.logger.Trace("TCP connection reset: %s:%d -> %s:%d", - conn.SourceIP, conn.SourcePort, conn.DestIP, conn.DestPort) - return +func (t *TCPTracker) handleRst(key ConnKey, conn *TCPConnTrack, size int) bool { + if conn.IsTombstone() { + return true } - switch conn.State { + conn.Lock() + conn.SetTombstone() + conn.State = TCPStateClosed + conn.SetEstablished(false) + conn.Unlock() + conn.UpdateCounters(nftypes.Ingress, size) + + t.logger.Trace("TCP connection reset: %s", key) + t.sendEvent(nftypes.TypeEnd, conn, nil) + return true +} + +// updateState updates the TCP connection state based on flags +func (t *TCPTracker) updateState(key ConnKey, conn *TCPConnTrack, flags uint8, isOutbound bool) { + conn.UpdateLastSeen() + + state := conn.State + defer func() { + if state != conn.State { + t.logger.Trace("TCP connection %s transitioned from %s to %s", key, state, conn.State) + } + }() + + switch state { case TCPStateNew: if flags&TCPSyn != 0 && flags&TCPAck == 0 { conn.State = TCPStateSynSent @@ -207,11 +288,11 @@ func (t *TCPTracker) updateState(conn *TCPConnTrack, flags uint8, isOutbound boo case TCPStateSynSent: if flags&TCPSyn != 0 && flags&TCPAck != 0 { if isOutbound { - conn.State = TCPStateSynReceived - } else { - // Simultaneous open conn.State = TCPStateEstablished conn.SetEstablished(true) + } else { + // Simultaneous open + conn.State = TCPStateSynReceived } } @@ -229,22 +310,32 @@ func (t *TCPTracker) updateState(conn *TCPConnTrack, flags uint8, isOutbound boo conn.State = TCPStateCloseWait } conn.SetEstablished(false) + } else if flags&TCPRst != 0 { + conn.State = TCPStateClosed + conn.SetTombstone() + t.sendEvent(nftypes.TypeEnd, conn, nil) } case TCPStateFinWait1: switch { case flags&TCPFin != 0 && flags&TCPAck != 0: - // Simultaneous close - both sides sent FIN conn.State = TCPStateClosing case flags&TCPFin != 0: conn.State = TCPStateFinWait2 case flags&TCPAck != 0: conn.State = TCPStateFinWait2 + case flags&TCPRst != 0: + conn.State = TCPStateClosed + conn.SetTombstone() + t.sendEvent(nftypes.TypeEnd, conn, nil) } case TCPStateFinWait2: if flags&TCPFin != 0 { conn.State = TCPStateTimeWait + + t.logger.Trace("TCP connection %s completed", key) + t.sendEvent(nftypes.TypeEnd, conn, nil) } case TCPStateClosing: @@ -252,8 +343,8 @@ func (t *TCPTracker) updateState(conn *TCPConnTrack, flags uint8, isOutbound boo conn.State = TCPStateTimeWait // Keep established = false from previous state - t.logger.Trace("TCP connection closed (simultaneous) - %s:%d -> %s:%d", - conn.SourceIP, conn.SourcePort, conn.DestIP, conn.DestPort) + t.logger.Trace("TCP connection %s closed (simultaneous)", key) + t.sendEvent(nftypes.TypeEnd, conn, nil) } case TCPStateCloseWait: @@ -264,17 +355,12 @@ func (t *TCPTracker) updateState(conn *TCPConnTrack, flags uint8, isOutbound boo case TCPStateLastAck: if flags&TCPAck != 0 { conn.State = TCPStateClosed + conn.SetTombstone() - t.logger.Trace("TCP connection gracefully closed: %s:%d -> %s:%d", - conn.SourceIP, conn.SourcePort, conn.DestIP, conn.DestPort) + // Send close event for gracefully closed connections + t.sendEvent(nftypes.TypeEnd, conn, nil) + t.logger.Trace("TCP connection %s closed gracefully", key) } - - case TCPStateTimeWait: - // Stay in TIME-WAIT for 2MSL before transitioning to closed - // This is handled by the cleanup routine - - t.logger.Trace("TCP connection completed - %s:%d -> %s:%d", - conn.SourceIP, conn.SourcePort, conn.DestIP, conn.DestPort) } } @@ -337,6 +423,12 @@ func (t *TCPTracker) cleanup() { defer t.mutex.Unlock() for key, conn := range t.connections { + if conn.IsTombstone() { + // Clean up tombstoned connections without sending an event + delete(t.connections, key) + continue + } + var timeout time.Duration switch { case conn.State == TCPStateTimeWait: @@ -347,14 +439,16 @@ func (t *TCPTracker) cleanup() { timeout = TCPHandshakeTimeout } - lastSeen := conn.GetLastSeen() - if time.Since(lastSeen) > timeout { + if conn.timeoutExceeded(timeout) { // Return IPs to pool - t.ipPool.Put(conn.SourceIP) - t.ipPool.Put(conn.DestIP) delete(t.connections, key) - t.logger.Trace("Cleaned up TCP connection: %s:%d -> %s:%d", conn.SourceIP, conn.SourcePort, conn.DestIP, conn.DestPort) + t.logger.Trace("Cleaned up timed-out TCP connection %s", key) + + // event already handled by state change + if conn.State != TCPStateTimeWait { + t.sendEvent(nftypes.TypeEnd, conn, nil) + } } } } @@ -365,10 +459,6 @@ func (t *TCPTracker) Close() { // Clean up all remaining IPs t.mutex.Lock() - for _, conn := range t.connections { - t.ipPool.Put(conn.SourceIP) - t.ipPool.Put(conn.DestIP) - } t.connections = nil t.mutex.Unlock() } @@ -386,3 +476,21 @@ func isValidFlagCombination(flags uint8) bool { return true } + +func (t *TCPTracker) sendEvent(typ nftypes.Type, conn *TCPConnTrack, ruleID []byte) { + t.flowLogger.StoreEvent(nftypes.EventFields{ + FlowID: conn.FlowId, + Type: typ, + RuleID: ruleID, + Direction: conn.Direction, + Protocol: nftypes.TCP, + SourceIP: conn.SourceIP, + DestIP: conn.DestIP, + SourcePort: conn.SourcePort, + DestPort: conn.DestPort, + RxPackets: conn.PacketsRx.Load(), + TxPackets: conn.PacketsTx.Load(), + RxBytes: conn.BytesRx.Load(), + TxBytes: conn.BytesTx.Load(), + }) +} diff --git a/client/firewall/uspfilter/conntrack/tcp_test.go b/client/firewall/uspfilter/conntrack/tcp_test.go index 5f4c43915..96558583d 100644 --- a/client/firewall/uspfilter/conntrack/tcp_test.go +++ b/client/firewall/uspfilter/conntrack/tcp_test.go @@ -1,7 +1,7 @@ package conntrack import ( - "net" + "net/netip" "testing" "time" @@ -9,11 +9,11 @@ import ( ) func TestTCPStateMachine(t *testing.T) { - tracker := NewTCPTracker(DefaultTCPTimeout, logger) + tracker := NewTCPTracker(DefaultTCPTimeout, logger, flowLogger) defer tracker.Close() - srcIP := net.ParseIP("100.64.0.1") - dstIP := net.ParseIP("100.64.0.2") + srcIP := netip.MustParseAddr("100.64.0.1") + dstIP := netip.MustParseAddr("100.64.0.2") srcPort := uint16(12345) dstPort := uint16(80) @@ -58,7 +58,7 @@ func TestTCPStateMachine(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - isValid := tracker.IsValidInbound(dstIP, srcIP, dstPort, srcPort, tt.flags) + isValid := tracker.IsValidInbound(dstIP, srcIP, dstPort, srcPort, tt.flags, 0) require.Equal(t, !tt.wantDrop, isValid, tt.desc) }) } @@ -76,17 +76,17 @@ func TestTCPStateMachine(t *testing.T) { t.Helper() // Send initial SYN - tracker.TrackOutbound(srcIP, dstIP, srcPort, dstPort, TCPSyn) + tracker.TrackOutbound(srcIP, dstIP, srcPort, dstPort, TCPSyn, 0) // Receive SYN-ACK - valid := tracker.IsValidInbound(dstIP, srcIP, dstPort, srcPort, TCPSyn|TCPAck) + valid := tracker.IsValidInbound(dstIP, srcIP, dstPort, srcPort, TCPSyn|TCPAck, 0) require.True(t, valid, "SYN-ACK should be allowed") // Send ACK - tracker.TrackOutbound(srcIP, dstIP, srcPort, dstPort, TCPAck) + tracker.TrackOutbound(srcIP, dstIP, srcPort, dstPort, TCPAck, 0) // Test data transfer - valid = tracker.IsValidInbound(dstIP, srcIP, dstPort, srcPort, TCPPush|TCPAck) + valid = tracker.IsValidInbound(dstIP, srcIP, dstPort, srcPort, TCPPush|TCPAck, 0) require.True(t, valid, "Data should be allowed after handshake") }, }, @@ -99,18 +99,18 @@ func TestTCPStateMachine(t *testing.T) { establishConnection(t, tracker, srcIP, dstIP, srcPort, dstPort) // Send FIN - tracker.TrackOutbound(srcIP, dstIP, srcPort, dstPort, TCPFin|TCPAck) + tracker.TrackOutbound(srcIP, dstIP, srcPort, dstPort, TCPFin|TCPAck, 0) // Receive ACK for FIN - valid := tracker.IsValidInbound(dstIP, srcIP, dstPort, srcPort, TCPAck) + valid := tracker.IsValidInbound(dstIP, srcIP, dstPort, srcPort, TCPAck, 0) require.True(t, valid, "ACK for FIN should be allowed") // Receive FIN from other side - valid = tracker.IsValidInbound(dstIP, srcIP, dstPort, srcPort, TCPFin|TCPAck) + valid = tracker.IsValidInbound(dstIP, srcIP, dstPort, srcPort, TCPFin|TCPAck, 0) require.True(t, valid, "FIN should be allowed") // Send final ACK - tracker.TrackOutbound(srcIP, dstIP, srcPort, dstPort, TCPAck) + tracker.TrackOutbound(srcIP, dstIP, srcPort, dstPort, TCPAck, 0) }, }, { @@ -122,7 +122,7 @@ func TestTCPStateMachine(t *testing.T) { establishConnection(t, tracker, srcIP, dstIP, srcPort, dstPort) // Receive RST - valid := tracker.IsValidInbound(dstIP, srcIP, dstPort, srcPort, TCPRst) + valid := tracker.IsValidInbound(dstIP, srcIP, dstPort, srcPort, TCPRst, 0) require.True(t, valid, "RST should be allowed for established connection") // Connection is logically dead but we don't enforce blocking subsequent packets @@ -138,13 +138,13 @@ func TestTCPStateMachine(t *testing.T) { establishConnection(t, tracker, srcIP, dstIP, srcPort, dstPort) // Both sides send FIN+ACK - tracker.TrackOutbound(srcIP, dstIP, srcPort, dstPort, TCPFin|TCPAck) - valid := tracker.IsValidInbound(dstIP, srcIP, dstPort, srcPort, TCPFin|TCPAck) + tracker.TrackOutbound(srcIP, dstIP, srcPort, dstPort, TCPFin|TCPAck, 0) + valid := tracker.IsValidInbound(dstIP, srcIP, dstPort, srcPort, TCPFin|TCPAck, 0) require.True(t, valid, "Simultaneous FIN should be allowed") // Both sides send final ACK - tracker.TrackOutbound(srcIP, dstIP, srcPort, dstPort, TCPAck) - valid = tracker.IsValidInbound(dstIP, srcIP, dstPort, srcPort, TCPAck) + tracker.TrackOutbound(srcIP, dstIP, srcPort, dstPort, TCPAck, 0) + valid = tracker.IsValidInbound(dstIP, srcIP, dstPort, srcPort, TCPAck, 0) require.True(t, valid, "Final ACKs should be allowed") }, }, @@ -154,7 +154,7 @@ func TestTCPStateMachine(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Helper() - tracker = NewTCPTracker(DefaultTCPTimeout, logger) + tracker = NewTCPTracker(DefaultTCPTimeout, logger, flowLogger) tt.test(t) }) } @@ -162,11 +162,11 @@ func TestTCPStateMachine(t *testing.T) { } func TestRSTHandling(t *testing.T) { - tracker := NewTCPTracker(DefaultTCPTimeout, logger) + tracker := NewTCPTracker(DefaultTCPTimeout, logger, flowLogger) defer tracker.Close() - srcIP := net.ParseIP("100.64.0.1") - dstIP := net.ParseIP("100.64.0.2") + srcIP := netip.MustParseAddr("100.64.0.1") + dstIP := netip.MustParseAddr("100.64.0.2") srcPort := uint16(12345) dstPort := uint16(80) @@ -181,12 +181,12 @@ func TestRSTHandling(t *testing.T) { name: "RST in established", setupState: func() { // Establish connection first - tracker.TrackOutbound(srcIP, dstIP, srcPort, dstPort, TCPSyn) - tracker.IsValidInbound(dstIP, srcIP, dstPort, srcPort, TCPSyn|TCPAck) - tracker.TrackOutbound(srcIP, dstIP, srcPort, dstPort, TCPAck) + tracker.TrackOutbound(srcIP, dstIP, srcPort, dstPort, TCPSyn, 0) + tracker.IsValidInbound(dstIP, srcIP, dstPort, srcPort, TCPSyn|TCPAck, 0) + tracker.TrackOutbound(srcIP, dstIP, srcPort, dstPort, TCPAck, 0) }, sendRST: func() { - tracker.IsValidInbound(dstIP, srcIP, dstPort, srcPort, TCPRst) + tracker.IsValidInbound(dstIP, srcIP, dstPort, srcPort, TCPRst, 0) }, wantValid: true, desc: "Should accept RST for established connection", @@ -195,7 +195,7 @@ func TestRSTHandling(t *testing.T) { name: "RST without connection", setupState: func() {}, sendRST: func() { - tracker.IsValidInbound(dstIP, srcIP, dstPort, srcPort, TCPRst) + tracker.IsValidInbound(dstIP, srcIP, dstPort, srcPort, TCPRst, 0) }, wantValid: false, desc: "Should reject RST without connection", @@ -208,7 +208,12 @@ func TestRSTHandling(t *testing.T) { tt.sendRST() // Verify connection state is as expected - key := makeConnKey(srcIP, dstIP, srcPort, dstPort) + key := ConnKey{ + SrcIP: srcIP, + DstIP: dstIP, + SrcPort: srcPort, + DstPort: dstPort, + } conn := tracker.connections[key] if tt.wantValid { require.NotNil(t, conn) @@ -220,63 +225,63 @@ func TestRSTHandling(t *testing.T) { } // Helper to establish a TCP connection -func establishConnection(t *testing.T, tracker *TCPTracker, srcIP, dstIP net.IP, srcPort, dstPort uint16) { +func establishConnection(t *testing.T, tracker *TCPTracker, srcIP, dstIP netip.Addr, srcPort, dstPort uint16) { t.Helper() - tracker.TrackOutbound(srcIP, dstIP, srcPort, dstPort, TCPSyn) + tracker.TrackOutbound(srcIP, dstIP, srcPort, dstPort, TCPSyn, 0) - valid := tracker.IsValidInbound(dstIP, srcIP, dstPort, srcPort, TCPSyn|TCPAck) + valid := tracker.IsValidInbound(dstIP, srcIP, dstPort, srcPort, TCPSyn|TCPAck, 0) require.True(t, valid, "SYN-ACK should be allowed") - tracker.TrackOutbound(srcIP, dstIP, srcPort, dstPort, TCPAck) + tracker.TrackOutbound(srcIP, dstIP, srcPort, dstPort, TCPAck, 0) } func BenchmarkTCPTracker(b *testing.B) { b.Run("TrackOutbound", func(b *testing.B) { - tracker := NewTCPTracker(DefaultTCPTimeout, logger) + tracker := NewTCPTracker(DefaultTCPTimeout, logger, flowLogger) defer tracker.Close() - srcIP := net.ParseIP("192.168.1.1") - dstIP := net.ParseIP("192.168.1.2") + srcIP := netip.MustParseAddr("192.168.1.1") + dstIP := netip.MustParseAddr("192.168.1.2") b.ResetTimer() for i := 0; i < b.N; i++ { - tracker.TrackOutbound(srcIP, dstIP, uint16(i%65535), 80, TCPSyn) + tracker.TrackOutbound(srcIP, dstIP, uint16(i%65535), 80, TCPSyn, 0) } }) b.Run("IsValidInbound", func(b *testing.B) { - tracker := NewTCPTracker(DefaultTCPTimeout, logger) + tracker := NewTCPTracker(DefaultTCPTimeout, logger, flowLogger) defer tracker.Close() - srcIP := net.ParseIP("192.168.1.1") - dstIP := net.ParseIP("192.168.1.2") + srcIP := netip.MustParseAddr("192.168.1.1") + dstIP := netip.MustParseAddr("192.168.1.2") // Pre-populate some connections for i := 0; i < 1000; i++ { - tracker.TrackOutbound(srcIP, dstIP, uint16(i), 80, TCPSyn) + tracker.TrackOutbound(srcIP, dstIP, uint16(i), 80, TCPSyn, 0) } b.ResetTimer() for i := 0; i < b.N; i++ { - tracker.IsValidInbound(dstIP, srcIP, 80, uint16(i%1000), TCPAck) + tracker.IsValidInbound(dstIP, srcIP, 80, uint16(i%1000), TCPAck, 0) } }) b.Run("ConcurrentAccess", func(b *testing.B) { - tracker := NewTCPTracker(DefaultTCPTimeout, logger) + tracker := NewTCPTracker(DefaultTCPTimeout, logger, flowLogger) defer tracker.Close() - srcIP := net.ParseIP("192.168.1.1") - dstIP := net.ParseIP("192.168.1.2") + srcIP := netip.MustParseAddr("192.168.1.1") + dstIP := netip.MustParseAddr("192.168.1.2") b.RunParallel(func(pb *testing.PB) { i := 0 for pb.Next() { if i%2 == 0 { - tracker.TrackOutbound(srcIP, dstIP, uint16(i%65535), 80, TCPSyn) + tracker.TrackOutbound(srcIP, dstIP, uint16(i%65535), 80, TCPSyn, 0) } else { - tracker.IsValidInbound(dstIP, srcIP, 80, uint16(i%65535), TCPAck) + tracker.IsValidInbound(dstIP, srcIP, 80, uint16(i%65535), TCPAck, 0) } i++ } @@ -287,14 +292,14 @@ func BenchmarkTCPTracker(b *testing.B) { // Benchmark connection cleanup func BenchmarkCleanup(b *testing.B) { b.Run("TCPCleanup", func(b *testing.B) { - tracker := NewTCPTracker(100*time.Millisecond, logger) // Short timeout for testing + tracker := NewTCPTracker(100*time.Millisecond, logger, flowLogger) // Short timeout for testing defer tracker.Close() // Pre-populate with expired connections - srcIP := net.ParseIP("192.168.1.1") - dstIP := net.ParseIP("192.168.1.2") + srcIP := netip.MustParseAddr("192.168.1.1") + dstIP := netip.MustParseAddr("192.168.1.2") for i := 0; i < 10000; i++ { - tracker.TrackOutbound(srcIP, dstIP, uint16(i), 80, TCPSyn) + tracker.TrackOutbound(srcIP, dstIP, uint16(i), 80, TCPSyn, 0) } // Wait for connections to expire diff --git a/client/firewall/uspfilter/conntrack/udp.go b/client/firewall/uspfilter/conntrack/udp.go index 073eb0fa2..d72988d27 100644 --- a/client/firewall/uspfilter/conntrack/udp.go +++ b/client/firewall/uspfilter/conntrack/udp.go @@ -2,11 +2,14 @@ package conntrack import ( "context" - "net" + "net/netip" "sync" "time" + "github.com/google/uuid" + nblog "github.com/netbirdio/netbird/client/firewall/uspfilter/log" + nftypes "github.com/netbirdio/netbird/client/internal/netflow/types" ) const ( @@ -19,6 +22,8 @@ const ( // UDPConnTrack represents a UDP connection state type UDPConnTrack struct { BaseConnTrack + SourcePort uint16 + DestPort uint16 } // UDPTracker manages UDP connection states @@ -29,11 +34,11 @@ type UDPTracker struct { cleanupTicker *time.Ticker tickerCancel context.CancelFunc mutex sync.RWMutex - ipPool *PreallocatedIPs + flowLogger nftypes.FlowLogger } // NewUDPTracker creates a new UDP connection tracker -func NewUDPTracker(timeout time.Duration, logger *nblog.Logger) *UDPTracker { +func NewUDPTracker(timeout time.Duration, logger *nblog.Logger, flowLogger nftypes.FlowLogger) *UDPTracker { if timeout == 0 { timeout = DefaultUDPTimeout } @@ -46,7 +51,7 @@ func NewUDPTracker(timeout time.Duration, logger *nblog.Logger) *UDPTracker { timeout: timeout, cleanupTicker: time.NewTicker(UDPCleanupInterval), tickerCancel: cancel, - ipPool: NewPreallocatedIPs(), + flowLogger: flowLogger, } go tracker.cleanupRoutine(ctx) @@ -54,55 +59,88 @@ func NewUDPTracker(timeout time.Duration, logger *nblog.Logger) *UDPTracker { } // TrackOutbound records an outbound UDP connection -func (t *UDPTracker) TrackOutbound(srcIP net.IP, dstIP net.IP, srcPort uint16, dstPort uint16) { - key := makeConnKey(srcIP, dstIP, srcPort, dstPort) - - t.mutex.Lock() - conn, exists := t.connections[key] - if !exists { - srcIPCopy := t.ipPool.Get() - dstIPCopy := t.ipPool.Get() - copyIP(srcIPCopy, srcIP) - copyIP(dstIPCopy, dstIP) - - conn = &UDPConnTrack{ - BaseConnTrack: BaseConnTrack{ - SourceIP: srcIPCopy, - DestIP: dstIPCopy, - SourcePort: srcPort, - DestPort: dstPort, - }, - } - conn.UpdateLastSeen() - t.connections[key] = conn - - t.logger.Trace("New UDP connection: %v", conn) +func (t *UDPTracker) TrackOutbound(srcIP netip.Addr, dstIP netip.Addr, srcPort uint16, dstPort uint16, size int) { + if _, exists := t.updateIfExists(dstIP, srcIP, dstPort, srcPort, nftypes.Egress, size); !exists { + // if (inverted direction) conn is not tracked, track this direction + t.track(srcIP, dstIP, srcPort, dstPort, nftypes.Egress, nil, size) } - t.mutex.Unlock() - - conn.UpdateLastSeen() } -// IsValidInbound checks if an inbound packet matches a tracked connection -func (t *UDPTracker) IsValidInbound(srcIP net.IP, dstIP net.IP, srcPort uint16, dstPort uint16) bool { - key := makeConnKey(dstIP, srcIP, dstPort, srcPort) +// TrackInbound records an inbound UDP connection +func (t *UDPTracker) TrackInbound(srcIP netip.Addr, dstIP netip.Addr, srcPort uint16, dstPort uint16, ruleID []byte, size int) { + t.track(srcIP, dstIP, srcPort, dstPort, nftypes.Ingress, ruleID, size) +} + +func (t *UDPTracker) updateIfExists(srcIP netip.Addr, dstIP netip.Addr, srcPort uint16, dstPort uint16, direction nftypes.Direction, size int) (ConnKey, bool) { + key := ConnKey{ + SrcIP: srcIP, + DstIP: dstIP, + SrcPort: srcPort, + DstPort: dstPort, + } t.mutex.RLock() conn, exists := t.connections[key] t.mutex.RUnlock() - if !exists { + if exists { + conn.UpdateLastSeen() + conn.UpdateCounters(direction, size) + return key, true + } + + return key, false +} + +// track is the common implementation for tracking both inbound and outbound connections +func (t *UDPTracker) track(srcIP netip.Addr, dstIP netip.Addr, srcPort uint16, dstPort uint16, direction nftypes.Direction, ruleID []byte, size int) { + key, exists := t.updateIfExists(srcIP, dstIP, srcPort, dstPort, direction, size) + if exists { + return + } + + conn := &UDPConnTrack{ + BaseConnTrack: BaseConnTrack{ + FlowId: uuid.New(), + Direction: direction, + SourceIP: srcIP, + DestIP: dstIP, + }, + SourcePort: srcPort, + DestPort: dstPort, + } + conn.UpdateLastSeen() + conn.UpdateCounters(direction, size) + + t.mutex.Lock() + t.connections[key] = conn + t.mutex.Unlock() + + t.logger.Trace("New %s UDP connection: %s", direction, key) + t.sendEvent(nftypes.TypeStart, conn, ruleID) +} + +// IsValidInbound checks if an inbound packet matches a tracked connection +func (t *UDPTracker) IsValidInbound(srcIP netip.Addr, dstIP netip.Addr, srcPort uint16, dstPort uint16, size int) bool { + key := ConnKey{ + SrcIP: dstIP, + DstIP: srcIP, + SrcPort: dstPort, + DstPort: srcPort, + } + + t.mutex.RLock() + conn, exists := t.connections[key] + t.mutex.RUnlock() + + if !exists || conn.timeoutExceeded(t.timeout) { return false } - if conn.timeoutExceeded(t.timeout) { - return false - } + conn.UpdateLastSeen() + conn.UpdateCounters(nftypes.Ingress, size) - return ValidateIPs(MakeIPAddr(srcIP), conn.DestIP) && - ValidateIPs(MakeIPAddr(dstIP), conn.SourceIP) && - conn.DestPort == srcPort && - conn.SourcePort == dstPort + return true } // cleanupRoutine periodically removes stale connections @@ -125,11 +163,11 @@ func (t *UDPTracker) cleanup() { for key, conn := range t.connections { if conn.timeoutExceeded(t.timeout) { - t.ipPool.Put(conn.SourceIP) - t.ipPool.Put(conn.DestIP) delete(t.connections, key) - t.logger.Trace("Removed UDP connection %v (timeout)", conn) + t.logger.Trace("Removed UDP connection %s (timeout) [in: %d Pkts/%d B out: %d Pkts/%d B]", + key, conn.PacketsRx.Load(), conn.BytesRx.Load(), conn.PacketsTx.Load(), conn.BytesTx.Load()) + t.sendEvent(nftypes.TypeEnd, conn, nil) } } } @@ -139,29 +177,44 @@ func (t *UDPTracker) Close() { t.tickerCancel() t.mutex.Lock() - for _, conn := range t.connections { - t.ipPool.Put(conn.SourceIP) - t.ipPool.Put(conn.DestIP) - } t.connections = nil t.mutex.Unlock() } // GetConnection safely retrieves a connection state -func (t *UDPTracker) GetConnection(srcIP net.IP, srcPort uint16, dstIP net.IP, dstPort uint16) (*UDPConnTrack, bool) { +func (t *UDPTracker) GetConnection(srcIP netip.Addr, srcPort uint16, dstIP netip.Addr, dstPort uint16) (*UDPConnTrack, bool) { t.mutex.RLock() defer t.mutex.RUnlock() - key := makeConnKey(srcIP, dstIP, srcPort, dstPort) - conn, exists := t.connections[key] - if !exists { - return nil, false + key := ConnKey{ + SrcIP: srcIP, + DstIP: dstIP, + SrcPort: srcPort, + DstPort: dstPort, } - - return conn, true + conn, exists := t.connections[key] + return conn, exists } // Timeout returns the configured timeout duration for the tracker func (t *UDPTracker) Timeout() time.Duration { return t.timeout } + +func (t *UDPTracker) sendEvent(typ nftypes.Type, conn *UDPConnTrack, ruleID []byte) { + t.flowLogger.StoreEvent(nftypes.EventFields{ + FlowID: conn.FlowId, + Type: typ, + RuleID: ruleID, + Direction: conn.Direction, + Protocol: nftypes.UDP, + SourceIP: conn.SourceIP, + DestIP: conn.DestIP, + SourcePort: conn.SourcePort, + DestPort: conn.DestPort, + RxPackets: conn.PacketsRx.Load(), + TxPackets: conn.PacketsTx.Load(), + RxBytes: conn.BytesRx.Load(), + TxBytes: conn.BytesTx.Load(), + }) +} diff --git a/client/firewall/uspfilter/conntrack/udp_test.go b/client/firewall/uspfilter/conntrack/udp_test.go index 40e73cbe0..7ad1e0e4b 100644 --- a/client/firewall/uspfilter/conntrack/udp_test.go +++ b/client/firewall/uspfilter/conntrack/udp_test.go @@ -2,7 +2,7 @@ package conntrack import ( "context" - "net" + "net/netip" "testing" "time" @@ -30,7 +30,7 @@ func TestNewUDPTracker(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - tracker := NewUDPTracker(tt.timeout, logger) + tracker := NewUDPTracker(tt.timeout, logger, flowLogger) assert.NotNil(t, tracker) assert.Equal(t, tt.wantTimeout, tracker.timeout) assert.NotNil(t, tracker.connections) @@ -41,43 +41,48 @@ func TestNewUDPTracker(t *testing.T) { } func TestUDPTracker_TrackOutbound(t *testing.T) { - tracker := NewUDPTracker(DefaultUDPTimeout, logger) + tracker := NewUDPTracker(DefaultUDPTimeout, logger, flowLogger) defer tracker.Close() - srcIP := net.ParseIP("192.168.1.2") - dstIP := net.ParseIP("192.168.1.3") + srcIP := netip.MustParseAddr("192.168.1.2") + dstIP := netip.MustParseAddr("192.168.1.3") srcPort := uint16(12345) dstPort := uint16(53) - tracker.TrackOutbound(srcIP, dstIP, srcPort, dstPort) + tracker.TrackOutbound(srcIP, dstIP, srcPort, dstPort, 0) // Verify connection was tracked - key := makeConnKey(srcIP, dstIP, srcPort, dstPort) + key := ConnKey{ + SrcIP: srcIP, + DstIP: dstIP, + SrcPort: srcPort, + DstPort: dstPort, + } conn, exists := tracker.connections[key] require.True(t, exists) - assert.True(t, conn.SourceIP.Equal(srcIP)) - assert.True(t, conn.DestIP.Equal(dstIP)) + assert.True(t, conn.SourceIP.Compare(srcIP) == 0) + assert.True(t, conn.DestIP.Compare(dstIP) == 0) assert.Equal(t, srcPort, conn.SourcePort) assert.Equal(t, dstPort, conn.DestPort) assert.WithinDuration(t, time.Now(), conn.GetLastSeen(), 1*time.Second) } func TestUDPTracker_IsValidInbound(t *testing.T) { - tracker := NewUDPTracker(1*time.Second, logger) + tracker := NewUDPTracker(1*time.Second, logger, flowLogger) defer tracker.Close() - srcIP := net.ParseIP("192.168.1.2") - dstIP := net.ParseIP("192.168.1.3") + srcIP := netip.MustParseAddr("192.168.1.2") + dstIP := netip.MustParseAddr("192.168.1.3") srcPort := uint16(12345) dstPort := uint16(53) // Track outbound connection - tracker.TrackOutbound(srcIP, dstIP, srcPort, dstPort) + tracker.TrackOutbound(srcIP, dstIP, srcPort, dstPort, 0) tests := []struct { name string - srcIP net.IP - dstIP net.IP + srcIP netip.Addr + dstIP netip.Addr srcPort uint16 dstPort uint16 sleep time.Duration @@ -94,7 +99,7 @@ func TestUDPTracker_IsValidInbound(t *testing.T) { }, { name: "invalid source IP", - srcIP: net.ParseIP("192.168.1.4"), + srcIP: netip.MustParseAddr("192.168.1.4"), dstIP: srcIP, srcPort: dstPort, dstPort: srcPort, @@ -104,7 +109,7 @@ func TestUDPTracker_IsValidInbound(t *testing.T) { { name: "invalid destination IP", srcIP: dstIP, - dstIP: net.ParseIP("192.168.1.4"), + dstIP: netip.MustParseAddr("192.168.1.4"), srcPort: dstPort, dstPort: srcPort, sleep: 0, @@ -144,7 +149,7 @@ func TestUDPTracker_IsValidInbound(t *testing.T) { if tt.sleep > 0 { time.Sleep(tt.sleep) } - got := tracker.IsValidInbound(tt.srcIP, tt.dstIP, tt.srcPort, tt.dstPort) + got := tracker.IsValidInbound(tt.srcIP, tt.dstIP, tt.srcPort, tt.dstPort, 0) assert.Equal(t, tt.want, got) }) } @@ -164,8 +169,8 @@ func TestUDPTracker_Cleanup(t *testing.T) { timeout: timeout, cleanupTicker: time.NewTicker(cleanupInterval), tickerCancel: tickerCancel, - ipPool: NewPreallocatedIPs(), logger: logger, + flowLogger: flowLogger, } // Start cleanup routine @@ -173,27 +178,27 @@ func TestUDPTracker_Cleanup(t *testing.T) { // Add some connections connections := []struct { - srcIP net.IP - dstIP net.IP + srcIP netip.Addr + dstIP netip.Addr srcPort uint16 dstPort uint16 }{ { - srcIP: net.ParseIP("192.168.1.2"), - dstIP: net.ParseIP("192.168.1.3"), + srcIP: netip.MustParseAddr("192.168.1.2"), + dstIP: netip.MustParseAddr("192.168.1.3"), srcPort: 12345, dstPort: 53, }, { - srcIP: net.ParseIP("192.168.1.4"), - dstIP: net.ParseIP("192.168.1.5"), + srcIP: netip.MustParseAddr("192.168.1.4"), + dstIP: netip.MustParseAddr("192.168.1.5"), srcPort: 12346, dstPort: 53, }, } for _, conn := range connections { - tracker.TrackOutbound(conn.srcIP, conn.dstIP, conn.srcPort, conn.dstPort) + tracker.TrackOutbound(conn.srcIP, conn.dstIP, conn.srcPort, conn.dstPort, 0) } // Verify initial connections @@ -215,33 +220,33 @@ func TestUDPTracker_Cleanup(t *testing.T) { func BenchmarkUDPTracker(b *testing.B) { b.Run("TrackOutbound", func(b *testing.B) { - tracker := NewUDPTracker(DefaultUDPTimeout, logger) + tracker := NewUDPTracker(DefaultUDPTimeout, logger, flowLogger) defer tracker.Close() - srcIP := net.ParseIP("192.168.1.1") - dstIP := net.ParseIP("192.168.1.2") + srcIP := netip.MustParseAddr("192.168.1.1") + dstIP := netip.MustParseAddr("192.168.1.2") b.ResetTimer() for i := 0; i < b.N; i++ { - tracker.TrackOutbound(srcIP, dstIP, uint16(i%65535), 80) + tracker.TrackOutbound(srcIP, dstIP, uint16(i%65535), 80, 0) } }) b.Run("IsValidInbound", func(b *testing.B) { - tracker := NewUDPTracker(DefaultUDPTimeout, logger) + tracker := NewUDPTracker(DefaultUDPTimeout, logger, flowLogger) defer tracker.Close() - srcIP := net.ParseIP("192.168.1.1") - dstIP := net.ParseIP("192.168.1.2") + srcIP := netip.MustParseAddr("192.168.1.1") + dstIP := netip.MustParseAddr("192.168.1.2") // Pre-populate some connections for i := 0; i < 1000; i++ { - tracker.TrackOutbound(srcIP, dstIP, uint16(i), 80) + tracker.TrackOutbound(srcIP, dstIP, uint16(i), 80, 0) } b.ResetTimer() for i := 0; i < b.N; i++ { - tracker.IsValidInbound(dstIP, srcIP, 80, uint16(i%1000)) + tracker.IsValidInbound(dstIP, srcIP, 80, uint16(i%1000), 0) } }) } diff --git a/client/firewall/uspfilter/forwarder/endpoint.go b/client/firewall/uspfilter/forwarder/endpoint.go index e8a265c94..3720eedfa 100644 --- a/client/firewall/uspfilter/forwarder/endpoint.go +++ b/client/firewall/uspfilter/forwarder/endpoint.go @@ -1,6 +1,8 @@ package forwarder import ( + "fmt" + wgdevice "golang.zx2c4.com/wireguard/device" "gvisor.dev/gvisor/pkg/tcpip" "gvisor.dev/gvisor/pkg/tcpip/header" @@ -79,3 +81,10 @@ func (e *endpoint) AddHeader(*stack.PacketBuffer) { func (e *endpoint) ParseHeader(*stack.PacketBuffer) bool { return true } + +type epID stack.TransportEndpointID + +func (i epID) String() string { + // src and remote is swapped + return fmt.Sprintf("%s:%d -> %s:%d", i.RemoteAddress, i.RemotePort, i.LocalAddress, i.LocalPort) +} diff --git a/client/firewall/uspfilter/forwarder/forwarder.go b/client/firewall/uspfilter/forwarder/forwarder.go index 4ed152b79..0dff3acc7 100644 --- a/client/firewall/uspfilter/forwarder/forwarder.go +++ b/client/firewall/uspfilter/forwarder/forwarder.go @@ -18,6 +18,7 @@ import ( "github.com/netbirdio/netbird/client/firewall/uspfilter/common" nblog "github.com/netbirdio/netbird/client/firewall/uspfilter/log" + nftypes "github.com/netbirdio/netbird/client/internal/netflow/types" ) const ( @@ -29,6 +30,7 @@ const ( type Forwarder struct { logger *nblog.Logger + flowLogger nftypes.FlowLogger stack *stack.Stack endpoint *endpoint udpForwarder *udpForwarder @@ -38,7 +40,7 @@ type Forwarder struct { netstack bool } -func New(iface common.IFaceMapper, logger *nblog.Logger, netstack bool) (*Forwarder, error) { +func New(iface common.IFaceMapper, logger *nblog.Logger, flowLogger nftypes.FlowLogger, netstack bool) (*Forwarder, error) { s := stack.New(stack.Options{ NetworkProtocols: []stack.NetworkProtocolFactory{ipv4.NewProtocol}, TransportProtocols: []stack.TransportProtocolFactory{ @@ -102,9 +104,10 @@ func New(iface common.IFaceMapper, logger *nblog.Logger, netstack bool) (*Forwar ctx, cancel := context.WithCancel(context.Background()) f := &Forwarder{ logger: logger, + flowLogger: flowLogger, stack: s, endpoint: endpoint, - udpForwarder: newUDPForwarder(mtu, logger), + udpForwarder: newUDPForwarder(mtu, logger, flowLogger), ctx: ctx, cancel: cancel, netstack: netstack, diff --git a/client/firewall/uspfilter/forwarder/icmp.go b/client/firewall/uspfilter/forwarder/icmp.go index 14cdc37be..a21ec2c87 100644 --- a/client/firewall/uspfilter/forwarder/icmp.go +++ b/client/firewall/uspfilter/forwarder/icmp.go @@ -3,14 +3,30 @@ package forwarder import ( "context" "net" + "net/netip" "time" + "github.com/google/uuid" "gvisor.dev/gvisor/pkg/tcpip/header" "gvisor.dev/gvisor/pkg/tcpip/stack" + + nftypes "github.com/netbirdio/netbird/client/internal/netflow/types" ) // handleICMP handles ICMP packets from the network stack func (f *Forwarder) handleICMP(id stack.TransportEndpointID, pkt stack.PacketBufferPtr) bool { + icmpHdr := header.ICMPv4(pkt.TransportHeader().View().AsSlice()) + icmpType := uint8(icmpHdr.Type()) + icmpCode := uint8(icmpHdr.Code()) + + if header.ICMPv4Type(icmpType) == header.ICMPv4EchoReply { + // dont process our own replies + return true + } + + flowID := uuid.New() + f.sendICMPEvent(nftypes.TypeStart, flowID, id, icmpType, icmpCode) + ctx, cancel := context.WithTimeout(f.ctx, 5*time.Second) defer cancel() @@ -18,7 +34,7 @@ func (f *Forwarder) handleICMP(id stack.TransportEndpointID, pkt stack.PacketBuf // TODO: support non-root conn, err := lc.ListenPacket(ctx, "ip4:icmp", "0.0.0.0") if err != nil { - f.logger.Error("Failed to create ICMP socket for %v: %v", id, err) + f.logger.Error("Failed to create ICMP socket for %v: %v", epID(id), err) // This will make netstack reply on behalf of the original destination, that's ok for now return false @@ -32,47 +48,31 @@ func (f *Forwarder) handleICMP(id stack.TransportEndpointID, pkt stack.PacketBuf dstIP := f.determineDialAddr(id.LocalAddress) dst := &net.IPAddr{IP: dstIP} - // Get the complete ICMP message (header + data) fullPacket := stack.PayloadSince(pkt.TransportHeader()) payload := fullPacket.AsSlice() - icmpHdr := header.ICMPv4(pkt.TransportHeader().View().AsSlice()) + if _, err = conn.WriteTo(payload, dst); err != nil { + f.logger.Error("Failed to write ICMP packet for %v: %v", epID(id), err) + return true + } + + f.logger.Trace("Forwarded ICMP packet %v type %v code %v", + epID(id), icmpHdr.Type(), icmpHdr.Code()) // For Echo Requests, send and handle response - switch icmpHdr.Type() { - case header.ICMPv4Echo: - return f.handleEchoResponse(icmpHdr, payload, dst, conn, id) - case header.ICMPv4EchoReply: - // dont process our own replies - return true - default: + if header.ICMPv4Type(icmpType) == header.ICMPv4Echo { + f.handleEchoResponse(icmpHdr, conn, id) + f.sendICMPEvent(nftypes.TypeEnd, flowID, id, icmpType, icmpCode) } - // For other ICMP types (Time Exceeded, Destination Unreachable, etc) - _, err = conn.WriteTo(payload, dst) - if err != nil { - f.logger.Error("Failed to write ICMP packet for %v: %v", id, err) - return true - } - - f.logger.Trace("Forwarded ICMP packet %v type=%v code=%v", - id, icmpHdr.Type(), icmpHdr.Code()) - + // For other ICMP types (Time Exceeded, Destination Unreachable, etc) do nothing return true } -func (f *Forwarder) handleEchoResponse(icmpHdr header.ICMPv4, payload []byte, dst *net.IPAddr, conn net.PacketConn, id stack.TransportEndpointID) bool { - if _, err := conn.WriteTo(payload, dst); err != nil { - f.logger.Error("Failed to write ICMP packet for %v: %v", id, err) - return true - } - - f.logger.Trace("Forwarded ICMP packet %v type=%v code=%v", - id, icmpHdr.Type(), icmpHdr.Code()) - +func (f *Forwarder) handleEchoResponse(icmpHdr header.ICMPv4, conn net.PacketConn, id stack.TransportEndpointID) { if err := conn.SetReadDeadline(time.Now().Add(5 * time.Second)); err != nil { f.logger.Error("Failed to set read deadline for ICMP response: %v", err) - return true + return } response := make([]byte, f.endpoint.mtu) @@ -81,7 +81,7 @@ func (f *Forwarder) handleEchoResponse(icmpHdr header.ICMPv4, payload []byte, ds if !isTimeout(err) { f.logger.Error("Failed to read ICMP response: %v", err) } - return true + return } ipHdr := make([]byte, header.IPv4MinimumSize) @@ -101,9 +101,27 @@ func (f *Forwarder) handleEchoResponse(icmpHdr header.ICMPv4, payload []byte, ds if err := f.InjectIncomingPacket(fullPacket); err != nil { f.logger.Error("Failed to inject ICMP response: %v", err) - return true + + return } - f.logger.Trace("Forwarded ICMP echo reply for %v", id) - return true + f.logger.Trace("Forwarded ICMP echo reply for %v type %v code %v", + epID(id), icmpHdr.Type(), icmpHdr.Code()) +} + +// sendICMPEvent stores flow events for ICMP packets +func (f *Forwarder) sendICMPEvent(typ nftypes.Type, flowID uuid.UUID, id stack.TransportEndpointID, icmpType, icmpCode uint8) { + f.flowLogger.StoreEvent(nftypes.EventFields{ + FlowID: flowID, + Type: typ, + Direction: nftypes.Ingress, + Protocol: nftypes.ICMP, + // TODO: handle ipv6 + SourceIP: netip.AddrFrom4(id.RemoteAddress.As4()), + DestIP: netip.AddrFrom4(id.LocalAddress.As4()), + ICMPType: icmpType, + ICMPCode: icmpCode, + + // TODO: get packets/bytes + }) } diff --git a/client/firewall/uspfilter/forwarder/tcp.go b/client/firewall/uspfilter/forwarder/tcp.go index 6d7cf3b6a..71cd457ef 100644 --- a/client/firewall/uspfilter/forwarder/tcp.go +++ b/client/firewall/uspfilter/forwarder/tcp.go @@ -5,24 +5,38 @@ import ( "fmt" "io" "net" + "net/netip" + "github.com/google/uuid" "gvisor.dev/gvisor/pkg/tcpip" "gvisor.dev/gvisor/pkg/tcpip/adapters/gonet" "gvisor.dev/gvisor/pkg/tcpip/stack" "gvisor.dev/gvisor/pkg/tcpip/transport/tcp" "gvisor.dev/gvisor/pkg/waiter" + + nftypes "github.com/netbirdio/netbird/client/internal/netflow/types" ) // handleTCP is called by the TCP forwarder for new connections. func (f *Forwarder) handleTCP(r *tcp.ForwarderRequest) { id := r.ID() + flowID := uuid.New() + + f.sendTCPEvent(nftypes.TypeStart, flowID, id, nil) + var success bool + defer func() { + if !success { + f.sendTCPEvent(nftypes.TypeEnd, flowID, id, nil) + } + }() + dialAddr := fmt.Sprintf("%s:%d", f.determineDialAddr(id.LocalAddress), id.LocalPort) outConn, err := (&net.Dialer{}).DialContext(f.ctx, "tcp", dialAddr) if err != nil { r.Complete(true) - f.logger.Trace("forwarder: dial error for %v: %v", id, err) + f.logger.Trace("forwarder: dial error for %v: %v", epID(id), err) return } @@ -44,12 +58,13 @@ func (f *Forwarder) handleTCP(r *tcp.ForwarderRequest) { inConn := gonet.NewTCPConn(&wq, ep) - f.logger.Trace("forwarder: established TCP connection %v", id) + success = true + f.logger.Trace("forwarder: established TCP connection %v", epID(id)) - go f.proxyTCP(id, inConn, outConn, ep) + go f.proxyTCP(id, inConn, outConn, ep, flowID) } -func (f *Forwarder) proxyTCP(id stack.TransportEndpointID, inConn *gonet.TCPConn, outConn net.Conn, ep tcpip.Endpoint) { +func (f *Forwarder) proxyTCP(id stack.TransportEndpointID, inConn *gonet.TCPConn, outConn net.Conn, ep tcpip.Endpoint, flowID uuid.UUID) { defer func() { if err := inConn.Close(); err != nil { f.logger.Debug("forwarder: inConn close error: %v", err) @@ -58,6 +73,8 @@ func (f *Forwarder) proxyTCP(id stack.TransportEndpointID, inConn *gonet.TCPConn f.logger.Debug("forwarder: outConn close error: %v", err) } ep.Close() + + f.sendTCPEvent(nftypes.TypeEnd, flowID, id, ep) }() // Create context for managing the proxy goroutines @@ -78,13 +95,38 @@ func (f *Forwarder) proxyTCP(id stack.TransportEndpointID, inConn *gonet.TCPConn select { case <-ctx.Done(): - f.logger.Trace("forwarder: tearing down TCP connection %v due to context done", id) + f.logger.Trace("forwarder: tearing down TCP connection %v due to context done", epID(id)) return case err := <-errChan: if err != nil && !isClosedError(err) { f.logger.Error("proxyTCP: copy error: %v", err) } - f.logger.Trace("forwarder: tearing down TCP connection %v", id) + f.logger.Trace("forwarder: tearing down TCP connection %v", epID(id)) return } } + +func (f *Forwarder) sendTCPEvent(typ nftypes.Type, flowID uuid.UUID, id stack.TransportEndpointID, ep tcpip.Endpoint) { + fields := nftypes.EventFields{ + FlowID: flowID, + Type: typ, + Direction: nftypes.Ingress, + Protocol: nftypes.TCP, + // TODO: handle ipv6 + SourceIP: netip.AddrFrom4(id.RemoteAddress.As4()), + DestIP: netip.AddrFrom4(id.LocalAddress.As4()), + SourcePort: id.RemotePort, + DestPort: id.LocalPort, + } + + if ep != nil { + if tcpStats, ok := ep.Stats().(*tcp.Stats); ok { + // fields are flipped since this is the in conn + // TODO: get bytes + fields.RxPackets = tcpStats.SegmentsSent.Value() + fields.TxPackets = tcpStats.SegmentsReceived.Value() + } + } + + f.flowLogger.StoreEvent(fields) +} diff --git a/client/firewall/uspfilter/forwarder/udp.go b/client/firewall/uspfilter/forwarder/udp.go index c37740587..7ce85e2b6 100644 --- a/client/firewall/uspfilter/forwarder/udp.go +++ b/client/firewall/uspfilter/forwarder/udp.go @@ -5,10 +5,12 @@ import ( "errors" "fmt" "net" + "net/netip" "sync" "sync/atomic" "time" + "github.com/google/uuid" "gvisor.dev/gvisor/pkg/tcpip" "gvisor.dev/gvisor/pkg/tcpip/adapters/gonet" "gvisor.dev/gvisor/pkg/tcpip/stack" @@ -16,6 +18,7 @@ import ( "gvisor.dev/gvisor/pkg/waiter" nblog "github.com/netbirdio/netbird/client/firewall/uspfilter/log" + nftypes "github.com/netbirdio/netbird/client/internal/netflow/types" ) const ( @@ -28,15 +31,17 @@ type udpPacketConn struct { lastSeen atomic.Int64 cancel context.CancelFunc ep tcpip.Endpoint + flowID uuid.UUID } type udpForwarder struct { sync.RWMutex - logger *nblog.Logger - conns map[stack.TransportEndpointID]*udpPacketConn - bufPool sync.Pool - ctx context.Context - cancel context.CancelFunc + logger *nblog.Logger + flowLogger nftypes.FlowLogger + conns map[stack.TransportEndpointID]*udpPacketConn + bufPool sync.Pool + ctx context.Context + cancel context.CancelFunc } type idleConn struct { @@ -44,13 +49,14 @@ type idleConn struct { conn *udpPacketConn } -func newUDPForwarder(mtu int, logger *nblog.Logger) *udpForwarder { +func newUDPForwarder(mtu int, logger *nblog.Logger, flowLogger nftypes.FlowLogger) *udpForwarder { ctx, cancel := context.WithCancel(context.Background()) f := &udpForwarder{ - logger: logger, - conns: make(map[stack.TransportEndpointID]*udpPacketConn), - ctx: ctx, - cancel: cancel, + logger: logger, + flowLogger: flowLogger, + conns: make(map[stack.TransportEndpointID]*udpPacketConn), + ctx: ctx, + cancel: cancel, bufPool: sync.Pool{ New: func() any { b := make([]byte, mtu) @@ -72,10 +78,10 @@ func (f *udpForwarder) Stop() { for id, conn := range f.conns { conn.cancel() if err := conn.conn.Close(); err != nil { - f.logger.Debug("forwarder: UDP conn close error for %v: %v", id, err) + f.logger.Debug("forwarder: UDP conn close error for %v: %v", epID(id), err) } if err := conn.outConn.Close(); err != nil { - f.logger.Debug("forwarder: UDP outConn close error for %v: %v", id, err) + f.logger.Debug("forwarder: UDP outConn close error for %v: %v", epID(id), err) } conn.ep.Close() @@ -106,10 +112,10 @@ func (f *udpForwarder) cleanup() { for _, idle := range idleConns { idle.conn.cancel() if err := idle.conn.conn.Close(); err != nil { - f.logger.Debug("forwarder: UDP conn close error for %v: %v", idle.id, err) + f.logger.Debug("forwarder: UDP conn close error for %v: %v", epID(idle.id), err) } if err := idle.conn.outConn.Close(); err != nil { - f.logger.Debug("forwarder: UDP outConn close error for %v: %v", idle.id, err) + f.logger.Debug("forwarder: UDP outConn close error for %v: %v", epID(idle.id), err) } idle.conn.ep.Close() @@ -118,7 +124,7 @@ func (f *udpForwarder) cleanup() { delete(f.conns, idle.id) f.Unlock() - f.logger.Trace("forwarder: cleaned up idle UDP connection %v", idle.id) + f.logger.Trace("forwarder: cleaned up idle UDP connection %v", epID(idle.id)) } } } @@ -137,14 +143,24 @@ func (f *Forwarder) handleUDP(r *udp.ForwarderRequest) { _, exists := f.udpForwarder.conns[id] f.udpForwarder.RUnlock() if exists { - f.logger.Trace("forwarder: existing UDP connection for %v", id) + f.logger.Trace("forwarder: existing UDP connection for %v", epID(id)) return } + flowID := uuid.New() + + f.sendUDPEvent(nftypes.TypeStart, flowID, id, nil) + var success bool + defer func() { + if !success { + f.sendUDPEvent(nftypes.TypeEnd, flowID, id, nil) + } + }() + dstAddr := fmt.Sprintf("%s:%d", f.determineDialAddr(id.LocalAddress), id.LocalPort) outConn, err := (&net.Dialer{}).DialContext(f.ctx, "udp", dstAddr) if err != nil { - f.logger.Debug("forwarder: UDP dial error for %v: %v", id, err) + f.logger.Debug("forwarder: UDP dial error for %v: %v", epID(id), err) // TODO: Send ICMP error message return } @@ -155,7 +171,7 @@ func (f *Forwarder) handleUDP(r *udp.ForwarderRequest) { if epErr != nil { f.logger.Debug("forwarder: failed to create UDP endpoint: %v", epErr) if err := outConn.Close(); err != nil { - f.logger.Debug("forwarder: UDP outConn close error for %v: %v", id, err) + f.logger.Debug("forwarder: UDP outConn close error for %v: %v", epID(id), err) } return } @@ -168,6 +184,7 @@ func (f *Forwarder) handleUDP(r *udp.ForwarderRequest) { outConn: outConn, cancel: connCancel, ep: ep, + flowID: flowID, } pConn.updateLastSeen() @@ -177,17 +194,20 @@ func (f *Forwarder) handleUDP(r *udp.ForwarderRequest) { f.udpForwarder.Unlock() pConn.cancel() if err := inConn.Close(); err != nil { - f.logger.Debug("forwarder: UDP inConn close error for %v: %v", id, err) + f.logger.Debug("forwarder: UDP inConn close error for %v: %v", epID(id), err) } if err := outConn.Close(); err != nil { - f.logger.Debug("forwarder: UDP outConn close error for %v: %v", id, err) + f.logger.Debug("forwarder: UDP outConn close error for %v: %v", epID(id), err) } + return } f.udpForwarder.conns[id] = pConn f.udpForwarder.Unlock() - f.logger.Trace("forwarder: established UDP connection to %v", id) + success = true + f.logger.Trace("forwarder: established UDP connection %v", epID(id)) + go f.proxyUDP(connCtx, pConn, id, ep) } @@ -195,10 +215,10 @@ func (f *Forwarder) proxyUDP(ctx context.Context, pConn *udpPacketConn, id stack defer func() { pConn.cancel() if err := pConn.conn.Close(); err != nil { - f.logger.Debug("forwarder: UDP inConn close error for %v: %v", id, err) + f.logger.Debug("forwarder: UDP inConn close error for %v: %v", epID(id), err) } if err := pConn.outConn.Close(); err != nil { - f.logger.Debug("forwarder: UDP outConn close error for %v: %v", id, err) + f.logger.Debug("forwarder: UDP outConn close error for %v: %v", epID(id), err) } ep.Close() @@ -206,6 +226,8 @@ func (f *Forwarder) proxyUDP(ctx context.Context, pConn *udpPacketConn, id stack f.udpForwarder.Lock() delete(f.udpForwarder.conns, id) f.udpForwarder.Unlock() + + f.sendUDPEvent(nftypes.TypeEnd, pConn.flowID, id, ep) }() errChan := make(chan error, 2) @@ -220,17 +242,43 @@ func (f *Forwarder) proxyUDP(ctx context.Context, pConn *udpPacketConn, id stack select { case <-ctx.Done(): - f.logger.Trace("forwarder: tearing down UDP connection %v due to context done", id) + f.logger.Trace("forwarder: tearing down UDP connection %v due to context done", epID(id)) return case err := <-errChan: if err != nil && !isClosedError(err) { f.logger.Error("proxyUDP: copy error: %v", err) } - f.logger.Trace("forwarder: tearing down UDP connection %v", id) + f.logger.Trace("forwarder: tearing down UDP connection %v", epID(id)) return } } +// sendUDPEvent stores flow events for UDP connections +func (f *Forwarder) sendUDPEvent(typ nftypes.Type, flowID uuid.UUID, id stack.TransportEndpointID, ep tcpip.Endpoint) { + fields := nftypes.EventFields{ + FlowID: flowID, + Type: typ, + Direction: nftypes.Ingress, + Protocol: nftypes.UDP, + // TODO: handle ipv6 + SourceIP: netip.AddrFrom4(id.RemoteAddress.As4()), + DestIP: netip.AddrFrom4(id.LocalAddress.As4()), + SourcePort: id.RemotePort, + DestPort: id.LocalPort, + } + + if ep != nil { + if tcpStats, ok := ep.Stats().(*tcpip.TransportEndpointStats); ok { + // fields are flipped since this is the in conn + // TODO: get bytes + fields.RxPackets = tcpStats.PacketsSent.Value() + fields.TxPackets = tcpStats.PacketsReceived.Value() + } + } + + f.flowLogger.StoreEvent(fields) +} + func (c *udpPacketConn) updateLastSeen() { c.lastSeen.Store(time.Now().UnixNano()) } diff --git a/client/firewall/uspfilter/localip.go b/client/firewall/uspfilter/localip.go index 7664b65d5..b86d16043 100644 --- a/client/firewall/uspfilter/localip.go +++ b/client/firewall/uspfilter/localip.go @@ -3,6 +3,7 @@ package uspfilter import ( "fmt" "net" + "net/netip" "sync" log "github.com/sirupsen/logrus" @@ -31,13 +32,9 @@ func (m *localIPManager) setBitmapBit(ip net.IP) { m.ipv4Bitmap[high] |= 1 << (low % 32) } -func (m *localIPManager) checkBitmapBit(ip net.IP) bool { - ipv4 := ip.To4() - if ipv4 == nil { - return false - } - high := (uint16(ipv4[0]) << 8) | uint16(ipv4[1]) - low := (uint16(ipv4[2]) << 8) | uint16(ipv4[3]) +func (m *localIPManager) checkBitmapBit(ip []byte) bool { + high := (uint16(ip[0]) << 8) | uint16(ip[1]) + low := (uint16(ip[2]) << 8) | uint16(ip[3]) return (m.ipv4Bitmap[high] & (1 << (low % 32))) != 0 } @@ -122,12 +119,12 @@ func (m *localIPManager) UpdateLocalIPs(iface common.IFaceMapper) (err error) { return nil } -func (m *localIPManager) IsLocalIP(ip net.IP) bool { +func (m *localIPManager) IsLocalIP(ip netip.Addr) bool { m.mu.RLock() defer m.mu.RUnlock() - if ipv4 := ip.To4(); ipv4 != nil { - return m.checkBitmapBit(ipv4) + if ip.Is4() { + return m.checkBitmapBit(ip.AsSlice()) } return false diff --git a/client/firewall/uspfilter/localip_test.go b/client/firewall/uspfilter/localip_test.go index 0a2a7b355..0715ddc41 100644 --- a/client/firewall/uspfilter/localip_test.go +++ b/client/firewall/uspfilter/localip_test.go @@ -2,6 +2,7 @@ package uspfilter import ( "net" + "net/netip" "testing" "github.com/stretchr/testify/require" @@ -13,7 +14,7 @@ func TestLocalIPManager(t *testing.T) { tests := []struct { name string setupAddr wgaddr.Address - testIP net.IP + testIP netip.Addr expected bool }{ { @@ -25,7 +26,7 @@ func TestLocalIPManager(t *testing.T) { Mask: net.CIDRMask(24, 32), }, }, - testIP: net.ParseIP("127.0.0.2"), + testIP: netip.MustParseAddr("127.0.0.2"), expected: true, }, { @@ -37,7 +38,7 @@ func TestLocalIPManager(t *testing.T) { Mask: net.CIDRMask(24, 32), }, }, - testIP: net.ParseIP("127.0.0.1"), + testIP: netip.MustParseAddr("127.0.0.1"), expected: true, }, { @@ -49,7 +50,7 @@ func TestLocalIPManager(t *testing.T) { Mask: net.CIDRMask(24, 32), }, }, - testIP: net.ParseIP("127.255.255.255"), + testIP: netip.MustParseAddr("127.255.255.255"), expected: true, }, { @@ -61,7 +62,7 @@ func TestLocalIPManager(t *testing.T) { Mask: net.CIDRMask(24, 32), }, }, - testIP: net.ParseIP("192.168.1.1"), + testIP: netip.MustParseAddr("192.168.1.1"), expected: true, }, { @@ -73,7 +74,7 @@ func TestLocalIPManager(t *testing.T) { Mask: net.CIDRMask(24, 32), }, }, - testIP: net.ParseIP("192.168.1.2"), + testIP: netip.MustParseAddr("192.168.1.2"), expected: false, }, { @@ -85,7 +86,7 @@ func TestLocalIPManager(t *testing.T) { Mask: net.CIDRMask(64, 128), }, }, - testIP: net.ParseIP("fe80::1"), + testIP: netip.MustParseAddr("fe80::1"), expected: false, }, } @@ -174,7 +175,7 @@ func TestLocalIPManager_AllInterfaces(t *testing.T) { t.Logf("Testing %d IPs", len(tests)) for _, tt := range tests { t.Run(tt.ip, func(t *testing.T) { - result := manager.IsLocalIP(net.ParseIP(tt.ip)) + result := manager.IsLocalIP(netip.MustParseAddr(tt.ip)) require.Equal(t, tt.expected, result, "IP: %s", tt.ip) }) } diff --git a/client/firewall/uspfilter/log/log.go b/client/firewall/uspfilter/log/log.go index 984b6ad08..d22421e2d 100644 --- a/client/firewall/uspfilter/log/log.go +++ b/client/firewall/uspfilter/log/log.go @@ -1,4 +1,4 @@ -// Package logger provides a high-performance, non-blocking logger for userspace networking +// Package log provides a high-performance, non-blocking logger for userspace networking package log import ( @@ -13,13 +13,12 @@ import ( ) const ( - maxBatchSize = 1024 * 16 // 16KB max batch size - maxMessageSize = 1024 * 2 // 2KB per message - bufferSize = 1024 * 256 // 256KB ring buffer + maxBatchSize = 1024 * 16 + maxMessageSize = 1024 * 2 defaultFlushInterval = 2 * time.Second + logChannelSize = 1000 ) -// Level represents log severity type Level uint32 const ( @@ -42,32 +41,37 @@ var levelStrings = map[Level]string{ LevelTrace: "TRAC", } -// Logger is a high-performance, non-blocking logger -type Logger struct { - output io.Writer - level atomic.Uint32 - buffer *ringBuffer - shutdown chan struct{} - closeOnce sync.Once - wg sync.WaitGroup - - // Reusable buffer pool for formatting messages - bufPool sync.Pool +type logMessage struct { + level Level + format string + args []any } +// Logger is a high-performance, non-blocking logger +type Logger struct { + output io.Writer + level atomic.Uint32 + msgChannel chan logMessage + shutdown chan struct{} + closeOnce sync.Once + wg sync.WaitGroup + bufPool sync.Pool +} + +// NewFromLogrus creates a new Logger that writes to the same output as the given logrus logger func NewFromLogrus(logrusLogger *log.Logger) *Logger { l := &Logger{ - output: logrusLogger.Out, - buffer: newRingBuffer(bufferSize), - shutdown: make(chan struct{}), + output: logrusLogger.Out, + msgChannel: make(chan logMessage, logChannelSize), + shutdown: make(chan struct{}), bufPool: sync.Pool{ - New: func() interface{} { - // Pre-allocate buffer for message formatting + New: func() any { b := make([]byte, 0, maxMessageSize) return &b }, }, } + logrusLevel := logrusLogger.GetLevel() l.level.Store(uint32(logrusLevel)) level := levelStrings[Level(logrusLevel)] @@ -79,97 +83,149 @@ func NewFromLogrus(logrusLogger *log.Logger) *Logger { return l } +// SetLevel sets the logging level func (l *Logger) SetLevel(level Level) { l.level.Store(uint32(level)) - log.Debugf("Set uspfilter logger loglevel to %v", levelStrings[level]) } -func (l *Logger) formatMessage(buf *[]byte, level Level, format string, args ...interface{}) { - *buf = (*buf)[:0] - - // Timestamp - *buf = time.Now().AppendFormat(*buf, "2006-01-02T15:04:05-07:00") - *buf = append(*buf, ' ') - - // Level - *buf = append(*buf, levelStrings[level]...) - *buf = append(*buf, ' ') - - // Message - if len(args) > 0 { - *buf = append(*buf, fmt.Sprintf(format, args...)...) - } else { - *buf = append(*buf, format...) +func (l *Logger) log(level Level, format string, args ...any) { + select { + case l.msgChannel <- logMessage{level: level, format: format, args: args}: + default: } - - *buf = append(*buf, '\n') } -func (l *Logger) log(level Level, format string, args ...interface{}) { - bufp := l.bufPool.Get().(*[]byte) - l.formatMessage(bufp, level, format, args...) - - if len(*bufp) > maxMessageSize { - *bufp = (*bufp)[:maxMessageSize] - } - _, _ = l.buffer.Write(*bufp) - - l.bufPool.Put(bufp) -} - -func (l *Logger) Error(format string, args ...interface{}) { +// Error logs a message at error level +func (l *Logger) Error(format string, args ...any) { if l.level.Load() >= uint32(LevelError) { l.log(LevelError, format, args...) } } -func (l *Logger) Warn(format string, args ...interface{}) { +// Warn logs a message at warning level +func (l *Logger) Warn(format string, args ...any) { if l.level.Load() >= uint32(LevelWarn) { l.log(LevelWarn, format, args...) } } -func (l *Logger) Info(format string, args ...interface{}) { +// Info logs a message at info level +func (l *Logger) Info(format string, args ...any) { if l.level.Load() >= uint32(LevelInfo) { l.log(LevelInfo, format, args...) } } -func (l *Logger) Debug(format string, args ...interface{}) { +// Debug logs a message at debug level +func (l *Logger) Debug(format string, args ...any) { if l.level.Load() >= uint32(LevelDebug) { l.log(LevelDebug, format, args...) } } -func (l *Logger) Trace(format string, args ...interface{}) { +// Trace logs a message at trace level +func (l *Logger) Trace(format string, args ...any) { if l.level.Load() >= uint32(LevelTrace) { l.log(LevelTrace, format, args...) } } -// worker periodically flushes the buffer +func (l *Logger) formatMessage(buf *[]byte, level Level, format string, args ...any) { + *buf = (*buf)[:0] + *buf = time.Now().AppendFormat(*buf, "2006-01-02T15:04:05-07:00") + *buf = append(*buf, ' ') + *buf = append(*buf, levelStrings[level]...) + *buf = append(*buf, ' ') + + var msg string + if len(args) > 0 { + msg = fmt.Sprintf(format, args...) + } else { + msg = format + } + *buf = append(*buf, msg...) + *buf = append(*buf, '\n') + + if len(*buf) > maxMessageSize { + *buf = (*buf)[:maxMessageSize] + } +} + +// processMessage handles a single log message and adds it to the buffer +func (l *Logger) processMessage(msg logMessage, buffer *[]byte) { + bufp := l.bufPool.Get().(*[]byte) + defer l.bufPool.Put(bufp) + + l.formatMessage(bufp, msg.level, msg.format, msg.args...) + + if len(*buffer)+len(*bufp) > maxBatchSize { + _, _ = l.output.Write(*buffer) + *buffer = (*buffer)[:0] + } + *buffer = append(*buffer, *bufp...) +} + +// flushBuffer writes the accumulated buffer to output +func (l *Logger) flushBuffer(buffer *[]byte) { + if len(*buffer) > 0 { + _, _ = l.output.Write(*buffer) + *buffer = (*buffer)[:0] + } +} + +// processBatch processes as many messages as possible without blocking +func (l *Logger) processBatch(buffer *[]byte) { + for len(*buffer) < maxBatchSize { + select { + case msg := <-l.msgChannel: + l.processMessage(msg, buffer) + default: + return + } + } +} + +// handleShutdown manages the graceful shutdown sequence with timeout +func (l *Logger) handleShutdown(buffer *[]byte) { + ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) + defer cancel() + + for { + select { + case msg := <-l.msgChannel: + l.processMessage(msg, buffer) + case <-ctx.Done(): + l.flushBuffer(buffer) + return + } + + if len(l.msgChannel) == 0 { + l.flushBuffer(buffer) + return + } + } +} + +// worker is the main goroutine that processes log messages func (l *Logger) worker() { defer l.wg.Done() ticker := time.NewTicker(defaultFlushInterval) defer ticker.Stop() - buf := make([]byte, 0, maxBatchSize) + buffer := make([]byte, 0, maxBatchSize) for { select { case <-l.shutdown: + l.handleShutdown(&buffer) return case <-ticker.C: - // Read accumulated messages - n, _ := l.buffer.Read(buf[:cap(buf)]) - if n == 0 { - continue - } - - // Write batch - _, _ = l.output.Write(buf[:n]) + l.flushBuffer(&buffer) + case msg := <-l.msgChannel: + l.processMessage(msg, &buffer) + l.processBatch(&buffer) } } } diff --git a/client/firewall/uspfilter/log/log_test.go b/client/firewall/uspfilter/log/log_test.go new file mode 100644 index 000000000..e7da9a8e9 --- /dev/null +++ b/client/firewall/uspfilter/log/log_test.go @@ -0,0 +1,121 @@ +package log_test + +import ( + "context" + "testing" + "time" + + "github.com/sirupsen/logrus" + + "github.com/netbirdio/netbird/client/firewall/uspfilter/log" +) + +type discard struct{} + +func (d *discard) Write(p []byte) (n int, err error) { + return len(p), nil +} + +func BenchmarkLogger(b *testing.B) { + simpleMessage := "Connection established" + + conntrackMessage := "TCP connection %s:%d -> %s:%d state changed to %d" + srcIP := "192.168.1.1" + srcPort := uint16(12345) + dstIP := "10.0.0.1" + dstPort := uint16(443) + state := 4 // TCPStateEstablished + + complexMessage := "Packet inspection result: protocol=%s, direction=%s, flags=0x%x, sequence=%d, acknowledged=%d, payload_size=%d, fragmented=%v, connection_id=%s" + protocol := "TCP" + direction := "outbound" + flags := uint16(0x18) // ACK + PSH + sequence := uint32(123456789) + acknowledged := uint32(987654321) + payloadSize := 1460 + fragmented := false + connID := "f7a12b3e-c456-7890-d123-456789abcdef" + + b.Run("SimpleMessage", func(b *testing.B) { + logger := createTestLogger() + defer cleanupLogger(logger) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + logger.Trace(simpleMessage) + } + }) + + b.Run("ConntrackMessage", func(b *testing.B) { + logger := createTestLogger() + defer cleanupLogger(logger) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + logger.Trace(conntrackMessage, srcIP, srcPort, dstIP, dstPort, state) + } + }) + + b.Run("ComplexMessage", func(b *testing.B) { + logger := createTestLogger() + defer cleanupLogger(logger) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + logger.Trace(complexMessage, protocol, direction, flags, sequence, acknowledged, payloadSize, fragmented, connID) + } + }) +} + +// BenchmarkLoggerParallel tests the logger under concurrent load +func BenchmarkLoggerParallel(b *testing.B) { + logger := createTestLogger() + defer cleanupLogger(logger) + + conntrackMessage := "TCP connection %s:%d -> %s:%d state changed to %d" + srcIP := "192.168.1.1" + srcPort := uint16(12345) + dstIP := "10.0.0.1" + dstPort := uint16(443) + state := 4 + + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + logger.Trace(conntrackMessage, srcIP, srcPort, dstIP, dstPort, state) + } + }) +} + +// BenchmarkLoggerBurst tests how the logger handles bursts of messages +func BenchmarkLoggerBurst(b *testing.B) { + logger := createTestLogger() + defer cleanupLogger(logger) + + conntrackMessage := "TCP connection %s:%d -> %s:%d state changed to %d" + srcIP := "192.168.1.1" + srcPort := uint16(12345) + dstIP := "10.0.0.1" + dstPort := uint16(443) + state := 4 + + b.ResetTimer() + for i := 0; i < b.N; i++ { + for j := 0; j < 100; j++ { + logger.Trace(conntrackMessage, srcIP, srcPort, dstIP, dstPort, state) + } + } +} + +func createTestLogger() *log.Logger { + logrusLogger := logrus.New() + logrusLogger.SetOutput(&discard{}) + logrusLogger.SetLevel(logrus.TraceLevel) + return log.NewFromLogrus(logrusLogger) +} + +func cleanupLogger(logger *log.Logger) { + ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) + defer cancel() + _ = logger.Stop(ctx) +} diff --git a/client/firewall/uspfilter/log/ringbuffer.go b/client/firewall/uspfilter/log/ringbuffer.go deleted file mode 100644 index dbc8f1289..000000000 --- a/client/firewall/uspfilter/log/ringbuffer.go +++ /dev/null @@ -1,85 +0,0 @@ -package log - -import "sync" - -// ringBuffer is a simple ring buffer implementation -type ringBuffer struct { - buf []byte - size int - r, w int64 // Read and write positions - mu sync.Mutex -} - -func newRingBuffer(size int) *ringBuffer { - return &ringBuffer{ - buf: make([]byte, size), - size: size, - } -} - -func (r *ringBuffer) Write(p []byte) (n int, err error) { - if len(p) == 0 { - return 0, nil - } - - r.mu.Lock() - defer r.mu.Unlock() - - if len(p) > r.size { - p = p[:r.size] - } - - n = len(p) - - // Write data, handling wrap-around - pos := int(r.w % int64(r.size)) - writeLen := min(len(p), r.size-pos) - copy(r.buf[pos:], p[:writeLen]) - - // If we have more data and need to wrap around - if writeLen < len(p) { - copy(r.buf, p[writeLen:]) - } - - // Update write position - r.w += int64(n) - - return n, nil -} - -func (r *ringBuffer) Read(p []byte) (n int, err error) { - r.mu.Lock() - defer r.mu.Unlock() - - if r.w == r.r { - return 0, nil - } - - // Calculate available data accounting for wraparound - available := int(r.w - r.r) - if available < 0 { - available += r.size - } - available = min(available, r.size) - - // Limit read to buffer size - toRead := min(available, len(p)) - if toRead == 0 { - return 0, nil - } - - // Read data, handling wrap-around - pos := int(r.r % int64(r.size)) - readLen := min(toRead, r.size-pos) - n = copy(p, r.buf[pos:pos+readLen]) - - // If we need more data and need to wrap around - if readLen < toRead { - n += copy(p[readLen:toRead], r.buf[:toRead-readLen]) - } - - // Update read position - r.r += int64(n) - - return n, nil -} diff --git a/client/firewall/uspfilter/rule.go b/client/firewall/uspfilter/rule.go index 100c35c0a..a23d2011b 100644 --- a/client/firewall/uspfilter/rule.go +++ b/client/firewall/uspfilter/rule.go @@ -1,7 +1,6 @@ package uspfilter import ( - "net" "net/netip" "github.com/google/gopacket" @@ -12,14 +11,14 @@ import ( // PeerRule to handle management of rules type PeerRule struct { id string - ip net.IP + mgmtId []byte + ip netip.Addr ipLayer gopacket.LayerType matchByIP bool protoLayer gopacket.LayerType sPort *firewall.Port dPort *firewall.Port drop bool - comment string udpHook func([]byte) bool } @@ -31,6 +30,7 @@ func (r *PeerRule) ID() string { type RouteRule struct { id string + mgmtId []byte sources []netip.Prefix destination netip.Prefix proto firewall.Protocol diff --git a/client/firewall/uspfilter/tracer.go b/client/firewall/uspfilter/tracer.go index a4c653b3b..53350797c 100644 --- a/client/firewall/uspfilter/tracer.go +++ b/client/firewall/uspfilter/tracer.go @@ -2,7 +2,7 @@ package uspfilter import ( "fmt" - "net" + "net/netip" "time" "github.com/google/gopacket" @@ -53,8 +53,8 @@ type TraceResult struct { } type PacketTrace struct { - SourceIP net.IP - DestinationIP net.IP + SourceIP netip.Addr + DestinationIP netip.Addr Protocol string SourcePort uint16 DestinationPort uint16 @@ -72,8 +72,8 @@ type TCPState struct { } type PacketBuilder struct { - SrcIP net.IP - DstIP net.IP + SrcIP netip.Addr + DstIP netip.Addr Protocol fw.Protocol SrcPort uint16 DstPort uint16 @@ -126,8 +126,8 @@ func (p *PacketBuilder) buildIPLayer() *layers.IPv4 { Version: 4, TTL: 64, Protocol: layers.IPProtocol(getIPProtocolNumber(p.Protocol)), - SrcIP: p.SrcIP, - DstIP: p.DstIP, + SrcIP: p.SrcIP.AsSlice(), + DstIP: p.DstIP.AsSlice(), } } @@ -260,28 +260,30 @@ func (m *Manager) TracePacket(packetData []byte, direction fw.RuleDirection) *Pa return m.traceInbound(packetData, trace, d, srcIP, dstIP) } -func (m *Manager) traceInbound(packetData []byte, trace *PacketTrace, d *decoder, srcIP net.IP, dstIP net.IP) *PacketTrace { +func (m *Manager) traceInbound(packetData []byte, trace *PacketTrace, d *decoder, srcIP netip.Addr, dstIP netip.Addr) *PacketTrace { if m.stateful && m.handleConntrackState(trace, d, srcIP, dstIP) { return trace } - if m.handleLocalDelivery(trace, packetData, d, srcIP, dstIP) { - return trace + if m.localipmanager.IsLocalIP(dstIP) { + if m.handleLocalDelivery(trace, packetData, d, srcIP, dstIP) { + return trace + } } if !m.handleRouting(trace) { return trace } - if m.nativeRouter { + if m.nativeRouter.Load() { return m.handleNativeRouter(trace) } return m.handleRouteACLs(trace, d, srcIP, dstIP) } -func (m *Manager) handleConntrackState(trace *PacketTrace, d *decoder, srcIP, dstIP net.IP) bool { - allowed := m.isValidTrackedConnection(d, srcIP, dstIP) +func (m *Manager) handleConntrackState(trace *PacketTrace, d *decoder, srcIP, dstIP netip.Addr) bool { + allowed := m.isValidTrackedConnection(d, srcIP, dstIP, 0) msg := "No existing connection found" if allowed { msg = m.buildConntrackStateMessage(d) @@ -309,32 +311,46 @@ func (m *Manager) buildConntrackStateMessage(d *decoder) string { return msg } -func (m *Manager) handleLocalDelivery(trace *PacketTrace, packetData []byte, d *decoder, srcIP, dstIP net.IP) bool { - if !m.localForwarding { - trace.AddResult(StageRouting, "Local forwarding disabled", false) - trace.AddResult(StageCompleted, "Packet dropped - local forwarding disabled", false) +func (m *Manager) handleLocalDelivery(trace *PacketTrace, packetData []byte, d *decoder, srcIP, dstIP netip.Addr) bool { + trace.AddResult(StageRouting, "Packet destined for local delivery", true) + + ruleId, blocked := m.peerACLsBlock(srcIP, packetData, m.incomingRules, d) + + strRuleId := "" + if ruleId != nil { + strRuleId = string(ruleId) + } + msg := fmt.Sprintf("Allowed by peer ACL rules (%s)", strRuleId) + if blocked { + msg = fmt.Sprintf("Blocked by peer ACL rules (%s)", strRuleId) + trace.AddResult(StagePeerACL, msg, false) + trace.AddResult(StageCompleted, "Packet dropped - ACL denied", false) return true } - trace.AddResult(StageRouting, "Packet destined for local delivery", true) - blocked := m.peerACLsBlock(srcIP, packetData, m.incomingRules, d) - - msg := "Allowed by peer ACL rules" - if blocked { - msg = "Blocked by peer ACL rules" - } - trace.AddResult(StagePeerACL, msg, !blocked) + trace.AddResult(StagePeerACL, msg, true) + // Handle netstack mode if m.netstack { - m.addForwardingResult(trace, "proxy-local", "127.0.0.1", !blocked) + switch { + case !m.localForwarding: + trace.AddResult(StageCompleted, "Packet sent to virtual stack", true) + case m.forwarder.Load() != nil: + m.addForwardingResult(trace, "proxy-local", "127.0.0.1", true) + trace.AddResult(StageCompleted, msgProcessingCompleted, true) + default: + trace.AddResult(StageCompleted, "Packet dropped - forwarder not initialized", false) + } + return true } - trace.AddResult(StageCompleted, msgProcessingCompleted, !blocked) + // In normal mode, packets are allowed through for local delivery + trace.AddResult(StageCompleted, msgProcessingCompleted, true) return true } func (m *Manager) handleRouting(trace *PacketTrace) bool { - if !m.routingEnabled { + if !m.routingEnabled.Load() { trace.AddResult(StageRouting, "Routing disabled", false) trace.AddResult(StageCompleted, "Packet dropped - routing disabled", false) return false @@ -350,18 +366,23 @@ func (m *Manager) handleNativeRouter(trace *PacketTrace) *PacketTrace { return trace } -func (m *Manager) handleRouteACLs(trace *PacketTrace, d *decoder, srcIP, dstIP net.IP) *PacketTrace { - proto := getProtocolFromPacket(d) +func (m *Manager) handleRouteACLs(trace *PacketTrace, d *decoder, srcIP, dstIP netip.Addr) *PacketTrace { + proto, _ := getProtocolFromPacket(d) srcPort, dstPort := getPortsFromPacket(d) - allowed := m.routeACLsPass(srcIP, dstIP, proto, srcPort, dstPort) + id, allowed := m.routeACLsPass(srcIP, dstIP, proto, srcPort, dstPort) - msg := "Allowed by route ACLs" + strId := string(id) + if id == nil { + strId = "" + } + + msg := fmt.Sprintf("Allowed by route ACLs (%s)", strId) if !allowed { - msg = "Blocked by route ACLs" + msg = fmt.Sprintf("Blocked by route ACLs (%s)", strId) } trace.AddResult(StageRouteACL, msg, allowed) - if allowed && m.forwarder != nil { + if allowed && m.forwarder.Load() != nil { m.addForwardingResult(trace, "proxy-remote", fmt.Sprintf("%s:%d", dstIP, dstPort), true) } @@ -380,7 +401,7 @@ func (m *Manager) addForwardingResult(trace *PacketTrace, action, remoteAddr str func (m *Manager) traceOutbound(packetData []byte, trace *PacketTrace) *PacketTrace { // will create or update the connection state - dropped := m.processOutgoingHooks(packetData) + dropped := m.processOutgoingHooks(packetData, 0) if dropped { trace.AddResult(StageCompleted, "Packet dropped by outgoing hook", false) } else { diff --git a/client/firewall/uspfilter/tracer_test.go b/client/firewall/uspfilter/tracer_test.go new file mode 100644 index 000000000..48b0ec44d --- /dev/null +++ b/client/firewall/uspfilter/tracer_test.go @@ -0,0 +1,440 @@ +package uspfilter + +import ( + "net" + "net/netip" + "testing" + + "github.com/stretchr/testify/require" + + fw "github.com/netbirdio/netbird/client/firewall/manager" + "github.com/netbirdio/netbird/client/firewall/uspfilter/conntrack" + "github.com/netbirdio/netbird/client/firewall/uspfilter/forwarder" + "github.com/netbirdio/netbird/client/iface/device" + "github.com/netbirdio/netbird/client/iface/wgaddr" +) + +func verifyTraceStages(t *testing.T, trace *PacketTrace, expectedStages []PacketStage) { + t.Logf("Trace results: %v", trace.Results) + actualStages := make([]PacketStage, 0, len(trace.Results)) + for _, result := range trace.Results { + actualStages = append(actualStages, result.Stage) + t.Logf("Stage: %s, Message: %s, Allowed: %v", result.Stage, result.Message, result.Allowed) + } + + require.ElementsMatch(t, expectedStages, actualStages, "Trace stages don't match expected stages") +} + +func verifyFinalDisposition(t *testing.T, trace *PacketTrace, expectedAllowed bool) { + require.NotEmpty(t, trace.Results, "Trace should have results") + lastResult := trace.Results[len(trace.Results)-1] + require.Equal(t, StageCompleted, lastResult.Stage, "Last stage should be 'Completed'") + require.Equal(t, expectedAllowed, lastResult.Allowed, "Final disposition incorrect") +} + +func TestTracePacket(t *testing.T) { + setupTracerTest := func(statefulMode bool) *Manager { + ifaceMock := &IFaceMock{ + SetFilterFunc: func(device.PacketFilter) error { return nil }, + AddressFunc: func() wgaddr.Address { + return wgaddr.Address{ + IP: net.ParseIP("100.10.0.100"), + Network: &net.IPNet{ + IP: net.ParseIP("100.10.0.0"), + Mask: net.CIDRMask(16, 32), + }, + } + }, + } + + m, err := Create(ifaceMock, false, flowLogger) + require.NoError(t, err) + + if !statefulMode { + m.stateful = false + } + + return m + } + + createPacketBuilder := func(srcIP, dstIP string, protocol fw.Protocol, srcPort, dstPort uint16, direction fw.RuleDirection) *PacketBuilder { + builder := &PacketBuilder{ + SrcIP: netip.MustParseAddr(srcIP), + DstIP: netip.MustParseAddr(dstIP), + Protocol: protocol, + SrcPort: srcPort, + DstPort: dstPort, + Direction: direction, + } + + if protocol == "tcp" { + builder.TCPState = &TCPState{SYN: true} + } + + return builder + } + + createICMPPacketBuilder := func(srcIP, dstIP string, icmpType, icmpCode uint8, direction fw.RuleDirection) *PacketBuilder { + return &PacketBuilder{ + SrcIP: netip.MustParseAddr(srcIP), + DstIP: netip.MustParseAddr(dstIP), + Protocol: "icmp", + ICMPType: icmpType, + ICMPCode: icmpCode, + Direction: direction, + } + } + + testCases := []struct { + name string + setup func(*Manager) + packetBuilder func() *PacketBuilder + expectedStages []PacketStage + expectedAllow bool + }{ + { + name: "LocalTraffic_ACLAllowed", + setup: func(m *Manager) { + ip := net.ParseIP("1.1.1.1") + proto := fw.ProtocolTCP + port := &fw.Port{Values: []uint16{80}} + action := fw.ActionAccept + _, err := m.AddPeerFiltering(nil, ip, proto, nil, port, action, "") + require.NoError(t, err) + }, + packetBuilder: func() *PacketBuilder { + return createPacketBuilder("1.1.1.1", "100.10.0.100", "tcp", 12345, 80, fw.RuleDirectionIN) + }, + expectedStages: []PacketStage{ + StageReceived, + StageConntrack, + StageRouting, + StagePeerACL, + StageCompleted, + }, + expectedAllow: true, + }, + { + name: "LocalTraffic_ACLDenied", + setup: func(m *Manager) { + ip := net.ParseIP("1.1.1.1") + proto := fw.ProtocolTCP + port := &fw.Port{Values: []uint16{80}} + action := fw.ActionDrop + _, err := m.AddPeerFiltering(nil, ip, proto, nil, port, action, "") + require.NoError(t, err) + }, + packetBuilder: func() *PacketBuilder { + return createPacketBuilder("1.1.1.1", "100.10.0.100", "tcp", 12345, 80, fw.RuleDirectionIN) + }, + expectedStages: []PacketStage{ + StageReceived, + StageConntrack, + StageRouting, + StagePeerACL, + StageCompleted, + }, + expectedAllow: false, + }, + { + name: "LocalTraffic_WithForwarder", + setup: func(m *Manager) { + m.netstack = true + m.localForwarding = true + + m.forwarder.Store(&forwarder.Forwarder{}) + + ip := net.ParseIP("1.1.1.1") + proto := fw.ProtocolTCP + port := &fw.Port{Values: []uint16{80}} + action := fw.ActionAccept + _, err := m.AddPeerFiltering(nil, ip, proto, nil, port, action, "") + require.NoError(t, err) + }, + packetBuilder: func() *PacketBuilder { + return createPacketBuilder("1.1.1.1", "100.10.0.100", "tcp", 12345, 80, fw.RuleDirectionIN) + }, + expectedStages: []PacketStage{ + StageReceived, + StageConntrack, + StageRouting, + StagePeerACL, + StageForwarding, + StageCompleted, + }, + expectedAllow: true, + }, + { + name: "LocalTraffic_WithoutForwarder", + setup: func(m *Manager) { + m.netstack = true + m.localForwarding = false + + ip := net.ParseIP("1.1.1.1") + proto := fw.ProtocolTCP + port := &fw.Port{Values: []uint16{80}} + action := fw.ActionAccept + _, err := m.AddPeerFiltering(nil, ip, proto, nil, port, action, "") + require.NoError(t, err) + }, + packetBuilder: func() *PacketBuilder { + return createPacketBuilder("1.1.1.1", "100.10.0.100", "tcp", 12345, 80, fw.RuleDirectionIN) + }, + expectedStages: []PacketStage{ + StageReceived, + StageConntrack, + StageRouting, + StagePeerACL, + StageCompleted, + }, + expectedAllow: true, + }, + { + name: "RoutedTraffic_ACLAllowed", + setup: func(m *Manager) { + m.routingEnabled.Store(true) + m.nativeRouter.Store(false) + + m.forwarder.Store(&forwarder.Forwarder{}) + + src := netip.PrefixFrom(netip.AddrFrom4([4]byte{1, 1, 1, 1}), 32) + dst := netip.PrefixFrom(netip.AddrFrom4([4]byte{172, 17, 0, 2}), 32) + _, err := m.AddRouteFiltering(nil, []netip.Prefix{src}, dst, fw.ProtocolTCP, nil, &fw.Port{Values: []uint16{80}}, fw.ActionAccept) + require.NoError(t, err) + }, + packetBuilder: func() *PacketBuilder { + return createPacketBuilder("1.1.1.1", "172.17.0.2", "tcp", 12345, 80, fw.RuleDirectionIN) + }, + expectedStages: []PacketStage{ + StageReceived, + StageConntrack, + StageRouting, + StageRouteACL, + StageForwarding, + StageCompleted, + }, + expectedAllow: true, + }, + { + name: "RoutedTraffic_ACLDenied", + setup: func(m *Manager) { + m.routingEnabled.Store(true) + m.nativeRouter.Store(false) + + src := netip.PrefixFrom(netip.AddrFrom4([4]byte{1, 1, 1, 1}), 32) + dst := netip.PrefixFrom(netip.AddrFrom4([4]byte{172, 17, 0, 2}), 32) + _, err := m.AddRouteFiltering(nil, []netip.Prefix{src}, dst, fw.ProtocolTCP, nil, &fw.Port{Values: []uint16{80}}, fw.ActionDrop) + require.NoError(t, err) + }, + packetBuilder: func() *PacketBuilder { + return createPacketBuilder("1.1.1.1", "172.17.0.2", "tcp", 12345, 80, fw.RuleDirectionIN) + }, + expectedStages: []PacketStage{ + StageReceived, + StageConntrack, + StageRouting, + StageRouteACL, + StageCompleted, + }, + expectedAllow: false, + }, + { + name: "RoutedTraffic_NativeRouter", + setup: func(m *Manager) { + m.routingEnabled.Store(true) + m.nativeRouter.Store(true) + }, + packetBuilder: func() *PacketBuilder { + return createPacketBuilder("1.1.1.1", "172.17.0.2", "tcp", 12345, 80, fw.RuleDirectionIN) + }, + expectedStages: []PacketStage{ + StageReceived, + StageConntrack, + StageRouting, + StageRouteACL, + StageForwarding, + StageCompleted, + }, + expectedAllow: true, + }, + { + name: "RoutedTraffic_RoutingDisabled", + setup: func(m *Manager) { + m.routingEnabled.Store(false) + }, + packetBuilder: func() *PacketBuilder { + return createPacketBuilder("1.1.1.1", "172.17.0.2", "tcp", 12345, 80, fw.RuleDirectionIN) + }, + expectedStages: []PacketStage{ + StageReceived, + StageConntrack, + StageRouting, + StageCompleted, + }, + expectedAllow: false, + }, + { + name: "ConnectionTracking_Hit", + setup: func(m *Manager) { + srcIP := netip.MustParseAddr("100.10.0.100") + dstIP := netip.MustParseAddr("1.1.1.1") + srcPort := uint16(12345) + dstPort := uint16(80) + + m.tcpTracker.TrackOutbound(srcIP, dstIP, srcPort, dstPort, conntrack.TCPSyn, 0) + }, + packetBuilder: func() *PacketBuilder { + pb := createPacketBuilder("1.1.1.1", "100.10.0.100", "tcp", 80, 12345, fw.RuleDirectionIN) + pb.TCPState = &TCPState{SYN: true, ACK: true} + return pb + }, + expectedStages: []PacketStage{ + StageReceived, + StageConntrack, + StageCompleted, + }, + expectedAllow: true, + }, + { + name: "OutboundTraffic", + setup: func(m *Manager) { + }, + packetBuilder: func() *PacketBuilder { + return createPacketBuilder("100.10.0.100", "1.1.1.1", "tcp", 12345, 80, fw.RuleDirectionOUT) + }, + expectedStages: []PacketStage{ + StageReceived, + StageCompleted, + }, + expectedAllow: true, + }, + { + name: "ICMPEchoRequest", + setup: func(m *Manager) { + ip := net.ParseIP("1.1.1.1") + proto := fw.ProtocolICMP + action := fw.ActionAccept + _, err := m.AddPeerFiltering(nil, ip, proto, nil, nil, action, "") + require.NoError(t, err) + }, + packetBuilder: func() *PacketBuilder { + return createICMPPacketBuilder("1.1.1.1", "100.10.0.100", 8, 0, fw.RuleDirectionIN) + }, + expectedStages: []PacketStage{ + StageReceived, + StageConntrack, + StageRouting, + StagePeerACL, + StageCompleted, + }, + expectedAllow: true, + }, + { + name: "ICMPDestinationUnreachable", + setup: func(m *Manager) { + ip := net.ParseIP("1.1.1.1") + proto := fw.ProtocolICMP + action := fw.ActionDrop + _, err := m.AddPeerFiltering(nil, ip, proto, nil, nil, action, "") + require.NoError(t, err) + }, + packetBuilder: func() *PacketBuilder { + return createICMPPacketBuilder("1.1.1.1", "100.10.0.100", 3, 0, fw.RuleDirectionIN) + }, + expectedStages: []PacketStage{ + StageReceived, + StageConntrack, + StageRouting, + StagePeerACL, + StageCompleted, + }, + expectedAllow: true, + }, + { + name: "UDPTraffic_WithoutHook", + setup: func(m *Manager) { + ip := net.ParseIP("1.1.1.1") + proto := fw.ProtocolUDP + port := &fw.Port{Values: []uint16{53}} + action := fw.ActionAccept + _, err := m.AddPeerFiltering(nil, ip, proto, nil, port, action, "") + require.NoError(t, err) + }, + packetBuilder: func() *PacketBuilder { + return createPacketBuilder("1.1.1.1", "100.10.0.100", "udp", 12345, 53, fw.RuleDirectionIN) + }, + expectedStages: []PacketStage{ + StageReceived, + StageConntrack, + StageRouting, + StagePeerACL, + StageCompleted, + }, + expectedAllow: true, + }, + { + name: "UDPTraffic_WithHook", + setup: func(m *Manager) { + hookFunc := func([]byte) bool { + return true + } + m.AddUDPPacketHook(true, netip.MustParseAddr("1.1.1.1"), 53, hookFunc) + }, + packetBuilder: func() *PacketBuilder { + return createPacketBuilder("1.1.1.1", "100.10.0.100", "udp", 12345, 53, fw.RuleDirectionIN) + }, + expectedStages: []PacketStage{ + StageReceived, + StageConntrack, + StageRouting, + StagePeerACL, + StageCompleted, + }, + expectedAllow: false, + }, + { + name: "StatefulDisabled_NoTracking", + setup: func(m *Manager) { + m.stateful = false + + ip := net.ParseIP("1.1.1.1") + proto := fw.ProtocolTCP + port := &fw.Port{Values: []uint16{80}} + action := fw.ActionDrop + _, err := m.AddPeerFiltering(nil, ip, proto, nil, port, action, "") + require.NoError(t, err) + }, + packetBuilder: func() *PacketBuilder { + return createPacketBuilder("1.1.1.1", "100.10.0.100", "tcp", 12345, 80, fw.RuleDirectionIN) + }, + expectedStages: []PacketStage{ + StageReceived, + StageRouting, + StagePeerACL, + StageCompleted, + }, + expectedAllow: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + m := setupTracerTest(true) + + tc.setup(m) + + require.True(t, m.localipmanager.IsLocalIP(netip.MustParseAddr("100.10.0.100")), + "100.10.0.100 should be recognized as a local IP") + require.False(t, m.localipmanager.IsLocalIP(netip.MustParseAddr("172.17.0.2")), + "172.17.0.2 should not be recognized as a local IP") + + pb := tc.packetBuilder() + + trace, err := m.TracePacketFromBuilder(pb) + require.NoError(t, err) + + verifyTraceStages(t, trace, tc.expectedStages) + verifyFinalDisposition(t, trace, tc.expectedAllow) + }) + } +} diff --git a/client/firewall/uspfilter/uspfilter.go b/client/firewall/uspfilter/uspfilter.go index 193526a52..92da1b240 100644 --- a/client/firewall/uspfilter/uspfilter.go +++ b/client/firewall/uspfilter/uspfilter.go @@ -10,6 +10,7 @@ import ( "strconv" "strings" "sync" + "sync/atomic" "github.com/google/gopacket" "github.com/google/gopacket/layers" @@ -22,6 +23,7 @@ import ( "github.com/netbirdio/netbird/client/firewall/uspfilter/forwarder" nblog "github.com/netbirdio/netbird/client/firewall/uspfilter/log" "github.com/netbirdio/netbird/client/iface/netstack" + nftypes "github.com/netbirdio/netbird/client/internal/netflow/types" "github.com/netbirdio/netbird/client/internal/statemanager" ) @@ -65,9 +67,9 @@ func (r RouteRules) Sort() { // Manager userspace firewall manager type Manager struct { // outgoingRules is used for hooks only - outgoingRules map[string]RuleSet + outgoingRules map[netip.Addr]RuleSet // incomingRules is used for filtering and hooks - incomingRules map[string]RuleSet + incomingRules map[netip.Addr]RuleSet routeRules RouteRules wgNetwork *net.IPNet decoders sync.Pool @@ -79,9 +81,9 @@ type Manager struct { // indicates whether server routes are disabled disableServerRoutes bool // indicates whether we forward packets not destined for ourselves - routingEnabled bool + routingEnabled atomic.Bool // indicates whether we leave forwarding and filtering to the native firewall - nativeRouter bool + nativeRouter atomic.Bool // indicates whether we track outbound connections stateful bool // indicates whether wireguards runs in netstack mode @@ -94,8 +96,9 @@ type Manager struct { udpTracker *conntrack.UDPTracker icmpTracker *conntrack.ICMPTracker tcpTracker *conntrack.TCPTracker - forwarder *forwarder.Forwarder + forwarder atomic.Pointer[forwarder.Forwarder] logger *nblog.Logger + flowLogger nftypes.FlowLogger } // decoder for packages @@ -112,16 +115,16 @@ type decoder struct { } // Create userspace firewall manager constructor -func Create(iface common.IFaceMapper, disableServerRoutes bool) (*Manager, error) { - return create(iface, nil, disableServerRoutes) +func Create(iface common.IFaceMapper, disableServerRoutes bool, flowLogger nftypes.FlowLogger) (*Manager, error) { + return create(iface, nil, disableServerRoutes, flowLogger) } -func CreateWithNativeFirewall(iface common.IFaceMapper, nativeFirewall firewall.Manager, disableServerRoutes bool) (*Manager, error) { +func CreateWithNativeFirewall(iface common.IFaceMapper, nativeFirewall firewall.Manager, disableServerRoutes bool, flowLogger nftypes.FlowLogger) (*Manager, error) { if nativeFirewall == nil { return nil, errors.New("native firewall is nil") } - mgr, err := create(iface, nativeFirewall, disableServerRoutes) + mgr, err := create(iface, nativeFirewall, disableServerRoutes, flowLogger) if err != nil { return nil, err } @@ -148,7 +151,7 @@ func parseCreateEnv() (bool, bool) { return disableConntrack, enableLocalForwarding } -func create(iface common.IFaceMapper, nativeFirewall firewall.Manager, disableServerRoutes bool) (*Manager, error) { +func create(iface common.IFaceMapper, nativeFirewall firewall.Manager, disableServerRoutes bool, flowLogger nftypes.FlowLogger) (*Manager, error) { disableConntrack, enableLocalForwarding := parseCreateEnv() m := &Manager{ @@ -166,17 +169,18 @@ func create(iface common.IFaceMapper, nativeFirewall firewall.Manager, disableSe }, }, nativeFirewall: nativeFirewall, - outgoingRules: make(map[string]RuleSet), - incomingRules: make(map[string]RuleSet), + outgoingRules: make(map[netip.Addr]RuleSet), + incomingRules: make(map[netip.Addr]RuleSet), wgIface: iface, localipmanager: newLocalIPManager(), disableServerRoutes: disableServerRoutes, - routingEnabled: false, stateful: !disableConntrack, logger: nblog.NewFromLogrus(log.StandardLogger()), + flowLogger: flowLogger, netstack: netstack.IsEnabled(), localForwarding: enableLocalForwarding, } + m.routingEnabled.Store(false) if err := m.localipmanager.UpdateLocalIPs(iface); err != nil { return nil, fmt.Errorf("update local IPs: %w", err) @@ -185,9 +189,9 @@ func create(iface common.IFaceMapper, nativeFirewall firewall.Manager, disableSe if disableConntrack { log.Info("conntrack is disabled") } else { - m.udpTracker = conntrack.NewUDPTracker(conntrack.DefaultUDPTimeout, m.logger) - m.icmpTracker = conntrack.NewICMPTracker(conntrack.DefaultICMPTimeout, m.logger) - m.tcpTracker = conntrack.NewTCPTracker(conntrack.DefaultTCPTimeout, m.logger) + m.udpTracker = conntrack.NewUDPTracker(conntrack.DefaultUDPTimeout, m.logger, flowLogger) + m.icmpTracker = conntrack.NewICMPTracker(conntrack.DefaultICMPTimeout, m.logger, flowLogger) + m.tcpTracker = conntrack.NewTCPTracker(conntrack.DefaultTCPTimeout, m.logger, flowLogger) } // netstack needs the forwarder for local traffic @@ -208,7 +212,7 @@ func create(iface common.IFaceMapper, nativeFirewall firewall.Manager, disableSe } func (m *Manager) blockInvalidRouted(iface common.IFaceMapper) error { - if m.forwarder == nil { + if m.forwarder.Load() == nil { return nil } wgPrefix, err := netip.ParsePrefix(iface.Address().Network.String()) @@ -218,6 +222,7 @@ func (m *Manager) blockInvalidRouted(iface common.IFaceMapper) error { log.Debugf("blocking invalid routed traffic for %s", wgPrefix) if _, err := m.AddRouteFiltering( + nil, []netip.Prefix{netip.PrefixFrom(netip.IPv4Unspecified(), 0)}, wgPrefix, firewall.ProtocolALL, @@ -251,20 +256,20 @@ func (m *Manager) determineRouting() error { switch { case disableUspRouting: - m.routingEnabled = false - m.nativeRouter = false + m.routingEnabled.Store(false) + m.nativeRouter.Store(false) log.Info("userspace routing is disabled") case m.disableServerRoutes: // if server routes are disabled we will let packets pass to the native stack - m.routingEnabled = true - m.nativeRouter = true + m.routingEnabled.Store(true) + m.nativeRouter.Store(true) log.Info("server routes are disabled") case forceUserspaceRouter: - m.routingEnabled = true - m.nativeRouter = false + m.routingEnabled.Store(true) + m.nativeRouter.Store(false) log.Info("userspace routing is forced") @@ -272,19 +277,19 @@ func (m *Manager) determineRouting() error { // if the OS supports routing natively, then we don't need to filter/route ourselves // netstack mode won't support native routing as there is no interface - m.routingEnabled = true - m.nativeRouter = true + m.routingEnabled.Store(true) + m.nativeRouter.Store(true) log.Info("native routing is enabled") default: - m.routingEnabled = true - m.nativeRouter = false + m.routingEnabled.Store(true) + m.nativeRouter.Store(false) log.Info("userspace routing enabled by default") } - if m.routingEnabled && !m.nativeRouter { + if m.routingEnabled.Load() && !m.nativeRouter.Load() { return m.initForwarder() } @@ -293,24 +298,24 @@ func (m *Manager) determineRouting() error { // initForwarder initializes the forwarder, it disables routing on errors func (m *Manager) initForwarder() error { - if m.forwarder != nil { + if m.forwarder.Load() != nil { return nil } // Only supported in userspace mode as we need to inject packets back into wireguard directly intf := m.wgIface.GetWGDevice() if intf == nil { - m.routingEnabled = false + m.routingEnabled.Store(false) return errors.New("forwarding not supported") } - forwarder, err := forwarder.New(m.wgIface, m.logger, m.netstack) + forwarder, err := forwarder.New(m.wgIface, m.logger, m.flowLogger, m.netstack) if err != nil { - m.routingEnabled = false + m.routingEnabled.Store(false) return fmt.Errorf("create forwarder: %w", err) } - m.forwarder = forwarder + m.forwarder.Store(forwarder) log.Debug("forwarder initialized") @@ -326,7 +331,7 @@ func (m *Manager) IsServerRouteSupported() bool { } func (m *Manager) AddNatRule(pair firewall.RouterPair) error { - if m.nativeRouter && m.nativeFirewall != nil { + if m.nativeRouter.Load() && m.nativeFirewall != nil { return m.nativeFirewall.AddNatRule(pair) } @@ -337,7 +342,7 @@ func (m *Manager) AddNatRule(pair firewall.RouterPair) error { // RemoveNatRule removes a routing firewall rule func (m *Manager) RemoveNatRule(pair firewall.RouterPair) error { - if m.nativeRouter && m.nativeFirewall != nil { + if m.nativeRouter.Load() && m.nativeFirewall != nil { return m.nativeFirewall.RemoveNatRule(pair) } return nil @@ -348,25 +353,31 @@ func (m *Manager) RemoveNatRule(pair firewall.RouterPair) error { // If comment argument is empty firewall manager should set // rule ID as comment for the rule func (m *Manager) AddPeerFiltering( + id []byte, ip net.IP, proto firewall.Protocol, sPort *firewall.Port, dPort *firewall.Port, action firewall.Action, _ string, - comment string, ) ([]firewall.Rule, error) { + // TODO: fix in upper layers + i, ok := netip.AddrFromSlice(ip) + if !ok { + return nil, fmt.Errorf("invalid IP: %s", ip) + } + + i = i.Unmap() r := PeerRule{ id: uuid.New().String(), - ip: ip, + mgmtId: id, + ip: i, ipLayer: layers.LayerTypeIPv6, matchByIP: true, drop: action == firewall.ActionDrop, - comment: comment, } - if ipNormalized := ip.To4(); ipNormalized != nil { + if i.Is4() { r.ipLayer = layers.LayerTypeIPv4 - r.ip = ipNormalized } if s := r.ip.String(); s == "0.0.0.0" || s == "::" { @@ -391,15 +402,16 @@ func (m *Manager) AddPeerFiltering( } m.mutex.Lock() - if _, ok := m.incomingRules[r.ip.String()]; !ok { - m.incomingRules[r.ip.String()] = make(RuleSet) + if _, ok := m.incomingRules[r.ip]; !ok { + m.incomingRules[r.ip] = make(RuleSet) } - m.incomingRules[r.ip.String()][r.id] = r + m.incomingRules[r.ip][r.id] = r m.mutex.Unlock() return []firewall.Rule{&r}, nil } func (m *Manager) AddRouteFiltering( + id []byte, sources []netip.Prefix, destination netip.Prefix, proto firewall.Protocol, @@ -407,16 +419,15 @@ func (m *Manager) AddRouteFiltering( dPort *firewall.Port, action firewall.Action, ) (firewall.Rule, error) { - if m.nativeRouter && m.nativeFirewall != nil { - return m.nativeFirewall.AddRouteFiltering(sources, destination, proto, sPort, dPort, action) + if m.nativeRouter.Load() && m.nativeFirewall != nil { + return m.nativeFirewall.AddRouteFiltering(id, sources, destination, proto, sPort, dPort, action) } - m.mutex.Lock() - defer m.mutex.Unlock() - ruleID := uuid.New().String() rule := RouteRule{ + // TODO: consolidate these IDs id: ruleID, + mgmtId: id, sources: sources, destination: destination, proto: proto, @@ -425,14 +436,16 @@ func (m *Manager) AddRouteFiltering( action: action, } + m.mutex.Lock() m.routeRules = append(m.routeRules, rule) m.routeRules.Sort() + m.mutex.Unlock() return &rule, nil } func (m *Manager) DeleteRouteRule(rule firewall.Rule) error { - if m.nativeRouter && m.nativeFirewall != nil { + if m.nativeRouter.Load() && m.nativeFirewall != nil { return m.nativeFirewall.DeleteRouteRule(rule) } @@ -461,10 +474,10 @@ func (m *Manager) DeletePeerRule(rule firewall.Rule) error { return fmt.Errorf("delete rule: invalid rule type: %T", rule) } - if _, ok := m.incomingRules[r.ip.String()][r.id]; !ok { + if _, ok := m.incomingRules[r.ip][r.id]; !ok { return fmt.Errorf("delete rule: no rule with such id: %v", r.id) } - delete(m.incomingRules[r.ip.String()], r.id) + delete(m.incomingRules[r.ip], r.id) return nil } @@ -497,13 +510,13 @@ func (m *Manager) DeleteDNATRule(rule firewall.Rule) error { } // DropOutgoing filter outgoing packets -func (m *Manager) DropOutgoing(packetData []byte) bool { - return m.processOutgoingHooks(packetData) +func (m *Manager) DropOutgoing(packetData []byte, size int) bool { + return m.processOutgoingHooks(packetData, size) } // DropIncoming filter incoming packets -func (m *Manager) DropIncoming(packetData []byte) bool { - return m.dropFilter(packetData) +func (m *Manager) DropIncoming(packetData []byte, size int) bool { + return m.dropFilter(packetData, size) } // UpdateLocalIPs updates the list of local IPs @@ -511,10 +524,7 @@ func (m *Manager) UpdateLocalIPs() error { return m.localipmanager.UpdateLocalIPs(m.wgIface) } -func (m *Manager) processOutgoingHooks(packetData []byte) bool { - m.mutex.RLock() - defer m.mutex.RUnlock() - +func (m *Manager) processOutgoingHooks(packetData []byte, size int) bool { d := m.decoders.Get().(*decoder) defer m.decoders.Put(d) @@ -527,52 +537,37 @@ func (m *Manager) processOutgoingHooks(packetData []byte) bool { } srcIP, dstIP := m.extractIPs(d) - if srcIP == nil { + if !srcIP.IsValid() { + m.logger.Error("Unknown network layer: %v", d.decoded[0]) return false } - // Track all protocols if stateful mode is enabled - if m.stateful { - switch d.decoded[1] { - case layers.LayerTypeUDP: - m.trackUDPOutbound(d, srcIP, dstIP) - case layers.LayerTypeTCP: - m.trackTCPOutbound(d, srcIP, dstIP) - case layers.LayerTypeICMPv4: - m.trackICMPOutbound(d, srcIP, dstIP) - } + if d.decoded[1] == layers.LayerTypeUDP && m.udpHooksDrop(uint16(d.udp.DstPort), dstIP, packetData) { + return true } - // Process UDP hooks even if stateful mode is disabled - if d.decoded[1] == layers.LayerTypeUDP { - return m.checkUDPHooks(d, dstIP, packetData) + if m.stateful { + m.trackOutbound(d, srcIP, dstIP, size) } return false } -func (m *Manager) extractIPs(d *decoder) (srcIP, dstIP net.IP) { +func (m *Manager) extractIPs(d *decoder) (srcIP, dstIP netip.Addr) { switch d.decoded[0] { case layers.LayerTypeIPv4: - return d.ip4.SrcIP, d.ip4.DstIP + src, _ := netip.AddrFromSlice(d.ip4.SrcIP) + dst, _ := netip.AddrFromSlice(d.ip4.DstIP) + return src, dst case layers.LayerTypeIPv6: - return d.ip6.SrcIP, d.ip6.DstIP + src, _ := netip.AddrFromSlice(d.ip6.SrcIP) + dst, _ := netip.AddrFromSlice(d.ip6.DstIP) + return src, dst default: - return nil, nil + return netip.Addr{}, netip.Addr{} } } -func (m *Manager) trackTCPOutbound(d *decoder, srcIP, dstIP net.IP) { - flags := getTCPFlags(&d.tcp) - m.tcpTracker.TrackOutbound( - srcIP, - dstIP, - uint16(d.tcp.SrcPort), - uint16(d.tcp.DstPort), - flags, - ) -} - func getTCPFlags(tcp *layers.TCP) uint8 { var flags uint8 if tcp.SYN { @@ -596,45 +591,70 @@ func getTCPFlags(tcp *layers.TCP) uint8 { return flags } -func (m *Manager) trackUDPOutbound(d *decoder, srcIP, dstIP net.IP) { - m.udpTracker.TrackOutbound( - srcIP, - dstIP, - uint16(d.udp.SrcPort), - uint16(d.udp.DstPort), - ) +func (m *Manager) trackOutbound(d *decoder, srcIP, dstIP netip.Addr, size int) { + transport := d.decoded[1] + switch transport { + case layers.LayerTypeUDP: + m.udpTracker.TrackOutbound(srcIP, dstIP, uint16(d.udp.SrcPort), uint16(d.udp.DstPort), size) + case layers.LayerTypeTCP: + flags := getTCPFlags(&d.tcp) + m.tcpTracker.TrackOutbound(srcIP, dstIP, uint16(d.tcp.SrcPort), uint16(d.tcp.DstPort), flags, size) + case layers.LayerTypeICMPv4: + m.icmpTracker.TrackOutbound(srcIP, dstIP, d.icmp4.Id, d.icmp4.TypeCode, size) + } } -func (m *Manager) checkUDPHooks(d *decoder, dstIP net.IP, packetData []byte) bool { - for _, ipKey := range []string{dstIP.String(), "0.0.0.0", "::"} { - if rules, exists := m.outgoingRules[ipKey]; exists { - for _, rule := range rules { - if rule.udpHook != nil && portsMatch(rule.dPort, uint16(d.udp.DstPort)) { - return rule.udpHook(packetData) - } +func (m *Manager) trackInbound(d *decoder, srcIP, dstIP netip.Addr, ruleID []byte, size int) { + transport := d.decoded[1] + switch transport { + case layers.LayerTypeUDP: + m.udpTracker.TrackInbound(srcIP, dstIP, uint16(d.udp.SrcPort), uint16(d.udp.DstPort), ruleID, size) + case layers.LayerTypeTCP: + flags := getTCPFlags(&d.tcp) + m.tcpTracker.TrackInbound(srcIP, dstIP, uint16(d.tcp.SrcPort), uint16(d.tcp.DstPort), flags, ruleID, size) + case layers.LayerTypeICMPv4: + m.icmpTracker.TrackInbound(srcIP, dstIP, d.icmp4.Id, d.icmp4.TypeCode, ruleID, size) + } +} + +// udpHooksDrop checks if any UDP hooks should drop the packet +func (m *Manager) udpHooksDrop(dport uint16, dstIP netip.Addr, packetData []byte) bool { + m.mutex.RLock() + defer m.mutex.RUnlock() + + // Check specific destination IP first + if rules, exists := m.outgoingRules[dstIP]; exists { + for _, rule := range rules { + if rule.udpHook != nil && portsMatch(rule.dPort, dport) { + return rule.udpHook(packetData) } } } - return false -} -func (m *Manager) trackICMPOutbound(d *decoder, srcIP, dstIP net.IP) { - if d.icmp4.TypeCode.Type() == layers.ICMPv4TypeEchoRequest { - m.icmpTracker.TrackOutbound( - srcIP, - dstIP, - d.icmp4.Id, - d.icmp4.Seq, - ) + // Check IPv4 unspecified address + if rules, exists := m.outgoingRules[netip.IPv4Unspecified()]; exists { + for _, rule := range rules { + if rule.udpHook != nil && portsMatch(rule.dPort, dport) { + return rule.udpHook(packetData) + } + } } + + // Check IPv6 unspecified address + if rules, exists := m.outgoingRules[netip.IPv6Unspecified()]; exists { + for _, rule := range rules { + if rule.udpHook != nil && portsMatch(rule.dPort, dport) { + return rule.udpHook(packetData) + } + } + } + + return false } // dropFilter implements filtering logic for incoming packets. // If it returns true, the packet should be dropped. -func (m *Manager) dropFilter(packetData []byte) bool { - m.mutex.RLock() - defer m.mutex.RUnlock() - +func (m *Manager) dropFilter(packetData []byte, size int) bool { d := m.decoders.Get().(*decoder) defer m.decoders.Put(d) @@ -643,19 +663,19 @@ func (m *Manager) dropFilter(packetData []byte) bool { } srcIP, dstIP := m.extractIPs(d) - if srcIP == nil { + if !srcIP.IsValid() { m.logger.Error("Unknown network layer: %v", d.decoded[0]) return true } // For all inbound traffic, first check if it matches a tracked connection. // This must happen before any other filtering because the packets are statefully tracked. - if m.stateful && m.isValidTrackedConnection(d, srcIP, dstIP) { + if m.stateful && m.isValidTrackedConnection(d, srcIP, dstIP, size) { return false } if m.localipmanager.IsLocalIP(dstIP) { - return m.handleLocalTraffic(d, srcIP, dstIP, packetData) + return m.handleLocalTraffic(d, srcIP, dstIP, packetData, size) } return m.handleRoutedTraffic(d, srcIP, dstIP, packetData) @@ -663,10 +683,29 @@ func (m *Manager) dropFilter(packetData []byte) bool { // handleLocalTraffic handles local traffic. // If it returns true, the packet should be dropped. -func (m *Manager) handleLocalTraffic(d *decoder, srcIP, dstIP net.IP, packetData []byte) bool { - if m.peerACLsBlock(srcIP, packetData, m.incomingRules, d) { - m.logger.Trace("Dropping local packet (ACL denied): src=%s dst=%s", - srcIP, dstIP) +func (m *Manager) handleLocalTraffic(d *decoder, srcIP, dstIP netip.Addr, packetData []byte, size int) bool { + ruleID, blocked := m.peerACLsBlock(srcIP, packetData, m.incomingRules, d) + if blocked { + _, pnum := getProtocolFromPacket(d) + srcPort, dstPort := getPortsFromPacket(d) + + m.logger.Trace("Dropping local packet (ACL denied): rule_id=%s proto=%v src=%s:%d dst=%s:%d", + ruleID, pnum, srcIP, srcPort, dstIP, dstPort) + + m.flowLogger.StoreEvent(nftypes.EventFields{ + FlowID: uuid.New(), + Type: nftypes.TypeDrop, + RuleID: ruleID, + Direction: nftypes.Ingress, + Protocol: pnum, + SourceIP: srcIP, + DestIP: dstIP, + SourcePort: srcPort, + DestPort: dstPort, + // TODO: icmp type/code + RxPackets: 1, + RxBytes: uint64(size), + }) return true } @@ -675,6 +714,9 @@ func (m *Manager) handleLocalTraffic(d *decoder, srcIP, dstIP net.IP, packetData return m.handleNetstackLocalTraffic(packetData) } + // track inbound packets to get the correct direction and session id for flows + m.trackInbound(d, srcIP, dstIP, ruleID, size) + return false } @@ -684,12 +726,13 @@ func (m *Manager) handleNetstackLocalTraffic(packetData []byte) bool { return false } - if m.forwarder == nil { + fwd := m.forwarder.Load() + if fwd == nil { m.logger.Trace("Dropping local packet (forwarder not initialized)") return true } - if err := m.forwarder.InjectIncomingPacket(packetData); err != nil { + if err := fwd.InjectIncomingPacket(packetData); err != nil { m.logger.Error("Failed to inject local packet: %v", err) } @@ -699,47 +742,65 @@ func (m *Manager) handleNetstackLocalTraffic(packetData []byte) bool { // handleRoutedTraffic handles routed traffic. // If it returns true, the packet should be dropped. -func (m *Manager) handleRoutedTraffic(d *decoder, srcIP, dstIP net.IP, packetData []byte) bool { +func (m *Manager) handleRoutedTraffic(d *decoder, srcIP, dstIP netip.Addr, packetData []byte) bool { // Drop if routing is disabled - if !m.routingEnabled { + if !m.routingEnabled.Load() { m.logger.Trace("Dropping routed packet (routing disabled): src=%s dst=%s", srcIP, dstIP) return true } // Pass to native stack if native router is enabled or forced - if m.nativeRouter { + if m.nativeRouter.Load() { return false } - proto := getProtocolFromPacket(d) + proto, pnum := getProtocolFromPacket(d) srcPort, dstPort := getPortsFromPacket(d) - if !m.routeACLsPass(srcIP, dstIP, proto, srcPort, dstPort) { - m.logger.Trace("Dropping routed packet (ACL denied): src=%s:%d dst=%s:%d proto=%v", - srcIP, srcPort, dstIP, dstPort, proto) + if ruleID, pass := m.routeACLsPass(srcIP, dstIP, proto, srcPort, dstPort); !pass { + m.logger.Trace("Dropping routed packet (ACL denied): rule_id=%s proto=%v src=%s:%d dst=%s:%d", + ruleID, pnum, srcIP, srcPort, dstIP, dstPort) + + m.flowLogger.StoreEvent(nftypes.EventFields{ + FlowID: uuid.New(), + Type: nftypes.TypeDrop, + RuleID: ruleID, + Direction: nftypes.Ingress, + Protocol: pnum, + SourceIP: srcIP, + DestIP: dstIP, + SourcePort: srcPort, + DestPort: dstPort, + // TODO: icmp type/code + }) return true } // Let forwarder handle the packet if it passed route ACLs - if err := m.forwarder.InjectIncomingPacket(packetData); err != nil { - m.logger.Error("Failed to inject incoming packet: %v", err) + fwd := m.forwarder.Load() + if fwd == nil { + m.logger.Trace("failed to forward routed packet (forwarder not initialized)") + } else { + if err := fwd.InjectIncomingPacket(packetData); err != nil { + m.logger.Error("Failed to inject routed packet: %v", err) + } } // Forwarded packets shouldn't reach the native stack, hence they won't be visible in a packet capture return true } -func getProtocolFromPacket(d *decoder) firewall.Protocol { +func getProtocolFromPacket(d *decoder) (firewall.Protocol, nftypes.Protocol) { switch d.decoded[1] { case layers.LayerTypeTCP: - return firewall.ProtocolTCP + return firewall.ProtocolTCP, nftypes.TCP case layers.LayerTypeUDP: - return firewall.ProtocolUDP + return firewall.ProtocolUDP, nftypes.UDP case layers.LayerTypeICMPv4, layers.LayerTypeICMPv6: - return firewall.ProtocolICMP + return firewall.ProtocolICMP, nftypes.ICMP default: - return firewall.ProtocolALL + return firewall.ProtocolALL, nftypes.ProtocolUnknown } } @@ -767,7 +828,7 @@ func (m *Manager) isValidPacket(d *decoder, packetData []byte) bool { return true } -func (m *Manager) isValidTrackedConnection(d *decoder, srcIP, dstIP net.IP) bool { +func (m *Manager) isValidTrackedConnection(d *decoder, srcIP, dstIP netip.Addr, size int) bool { switch d.decoded[1] { case layers.LayerTypeTCP: return m.tcpTracker.IsValidInbound( @@ -776,6 +837,7 @@ func (m *Manager) isValidTrackedConnection(d *decoder, srcIP, dstIP net.IP) bool uint16(d.tcp.SrcPort), uint16(d.tcp.DstPort), getTCPFlags(&d.tcp), + size, ) case layers.LayerTypeUDP: @@ -784,6 +846,7 @@ func (m *Manager) isValidTrackedConnection(d *decoder, srcIP, dstIP net.IP) bool dstIP, uint16(d.udp.SrcPort), uint16(d.udp.DstPort), + size, ) case layers.LayerTypeICMPv4: @@ -791,8 +854,8 @@ func (m *Manager) isValidTrackedConnection(d *decoder, srcIP, dstIP net.IP) bool srcIP, dstIP, d.icmp4.Id, - d.icmp4.Seq, d.icmp4.TypeCode.Type(), + size, ) // TODO: ICMPv6 @@ -812,25 +875,27 @@ func (m *Manager) isSpecialICMP(d *decoder) bool { icmpType == layers.ICMPv4TypeTimeExceeded } -func (m *Manager) peerACLsBlock(srcIP net.IP, packetData []byte, rules map[string]RuleSet, d *decoder) bool { +func (m *Manager) peerACLsBlock(srcIP netip.Addr, packetData []byte, rules map[netip.Addr]RuleSet, d *decoder) ([]byte, bool) { + m.mutex.RLock() + defer m.mutex.RUnlock() if m.isSpecialICMP(d) { - return false + return nil, false } - if filter, ok := validateRule(srcIP, packetData, rules[srcIP.String()], d); ok { - return filter + if mgmtId, filter, ok := validateRule(srcIP, packetData, rules[srcIP], d); ok { + return mgmtId, filter } - if filter, ok := validateRule(srcIP, packetData, rules["0.0.0.0"], d); ok { - return filter + if mgmtId, filter, ok := validateRule(srcIP, packetData, rules[netip.IPv4Unspecified()], d); ok { + return mgmtId, filter } - if filter, ok := validateRule(srcIP, packetData, rules["::"], d); ok { - return filter + if mgmtId, filter, ok := validateRule(srcIP, packetData, rules[netip.IPv6Unspecified()], d); ok { + return mgmtId, filter } // Default policy: DROP ALL - return true + return nil, true } func portsMatch(rulePort *firewall.Port, packetPort uint16) bool { @@ -850,15 +915,15 @@ func portsMatch(rulePort *firewall.Port, packetPort uint16) bool { return false } -func validateRule(ip net.IP, packetData []byte, rules map[string]PeerRule, d *decoder) (bool, bool) { +func validateRule(ip netip.Addr, packetData []byte, rules map[string]PeerRule, d *decoder) ([]byte, bool, bool) { payloadLayer := d.decoded[1] for _, rule := range rules { - if rule.matchByIP && !ip.Equal(rule.ip) { + if rule.matchByIP && ip.Compare(rule.ip) != 0 { continue } if rule.protoLayer == layerTypeAll { - return rule.drop, true + return rule.mgmtId, rule.drop, true } if payloadLayer != rule.protoLayer { @@ -868,39 +933,36 @@ func validateRule(ip net.IP, packetData []byte, rules map[string]PeerRule, d *de switch payloadLayer { case layers.LayerTypeTCP: if portsMatch(rule.sPort, uint16(d.tcp.SrcPort)) && portsMatch(rule.dPort, uint16(d.tcp.DstPort)) { - return rule.drop, true + return rule.mgmtId, rule.drop, true } case layers.LayerTypeUDP: // if rule has UDP hook (and if we are here we match this rule) // we ignore rule.drop and call this hook if rule.udpHook != nil { - return rule.udpHook(packetData), true + return rule.mgmtId, rule.udpHook(packetData), true } if portsMatch(rule.sPort, uint16(d.udp.SrcPort)) && portsMatch(rule.dPort, uint16(d.udp.DstPort)) { - return rule.drop, true + return rule.mgmtId, rule.drop, true } case layers.LayerTypeICMPv4, layers.LayerTypeICMPv6: - return rule.drop, true + return rule.mgmtId, rule.drop, true } } - return false, false + return nil, false, false } -// routeACLsPass returns treu if the packet is allowed by the route ACLs -func (m *Manager) routeACLsPass(srcIP, dstIP net.IP, proto firewall.Protocol, srcPort, dstPort uint16) bool { +// routeACLsPass returns true if the packet is allowed by the route ACLs +func (m *Manager) routeACLsPass(srcIP, dstIP netip.Addr, proto firewall.Protocol, srcPort, dstPort uint16) ([]byte, bool) { m.mutex.RLock() defer m.mutex.RUnlock() - srcAddr := netip.AddrFrom4([4]byte(srcIP.To4())) - dstAddr := netip.AddrFrom4([4]byte(dstIP.To4())) - for _, rule := range m.routeRules { - if m.ruleMatches(rule, srcAddr, dstAddr, proto, srcPort, dstPort) { - return rule.action == firewall.ActionAccept + if matches := m.ruleMatches(rule, srcIP, dstIP, proto, srcPort, dstPort); matches { + return rule.mgmtId, rule.action == firewall.ActionAccept } } - return false + return nil, false } func (m *Manager) ruleMatches(rule RouteRule, srcAddr, dstAddr netip.Addr, proto firewall.Protocol, srcPort, dstPort uint16) bool { @@ -940,36 +1002,32 @@ func (m *Manager) SetNetwork(network *net.IPNet) { // AddUDPPacketHook calls hook when UDP packet from given direction matched // // Hook function returns flag which indicates should be the matched package dropped or not -func (m *Manager) AddUDPPacketHook( - in bool, ip net.IP, dPort uint16, hook func([]byte) bool, -) string { +func (m *Manager) AddUDPPacketHook(in bool, ip netip.Addr, dPort uint16, hook func(packet []byte) bool) string { r := PeerRule{ id: uuid.New().String(), ip: ip, protoLayer: layers.LayerTypeUDP, dPort: &firewall.Port{Values: []uint16{dPort}}, ipLayer: layers.LayerTypeIPv6, - comment: fmt.Sprintf("UDP Hook direction: %v, ip:%v, dport:%d", in, ip, dPort), udpHook: hook, } - if ip.To4() != nil { + if ip.Is4() { r.ipLayer = layers.LayerTypeIPv4 } m.mutex.Lock() if in { - if _, ok := m.incomingRules[r.ip.String()]; !ok { - m.incomingRules[r.ip.String()] = make(map[string]PeerRule) + if _, ok := m.incomingRules[r.ip]; !ok { + m.incomingRules[r.ip] = make(map[string]PeerRule) } - m.incomingRules[r.ip.String()][r.id] = r + m.incomingRules[r.ip][r.id] = r } else { - if _, ok := m.outgoingRules[r.ip.String()]; !ok { - m.outgoingRules[r.ip.String()] = make(map[string]PeerRule) + if _, ok := m.outgoingRules[r.ip]; !ok { + m.outgoingRules[r.ip] = make(map[string]PeerRule) } - m.outgoingRules[r.ip.String()][r.id] = r + m.outgoingRules[r.ip][r.id] = r } - m.mutex.Unlock() return r.id @@ -1017,20 +1075,21 @@ func (m *Manager) DisableRouting() error { m.mutex.Lock() defer m.mutex.Unlock() - if m.forwarder == nil { + fwder := m.forwarder.Load() + if fwder == nil { return nil } - m.routingEnabled = false - m.nativeRouter = false + m.routingEnabled.Store(false) + m.nativeRouter.Store(false) // don't stop forwarder if in use by netstack if m.netstack && m.localForwarding { return nil } - m.forwarder.Stop() - m.forwarder = nil + fwder.Stop() + m.forwarder.Store(nil) log.Debug("forwarder stopped") diff --git a/client/firewall/uspfilter/uspfilter_bench_test.go b/client/firewall/uspfilter/uspfilter_bench_test.go index bb42a8052..beb5b9336 100644 --- a/client/firewall/uspfilter/uspfilter_bench_test.go +++ b/client/firewall/uspfilter/uspfilter_bench_test.go @@ -93,8 +93,7 @@ func BenchmarkCoreFiltering(b *testing.B) { stateful: false, setupFunc: func(m *Manager) { // Single rule allowing all traffic - _, err := m.AddPeerFiltering(net.ParseIP("0.0.0.0"), fw.ProtocolALL, nil, nil, - fw.ActionAccept, "", "allow all") + _, err := m.AddPeerFiltering(nil, net.ParseIP("0.0.0.0"), fw.ProtocolALL, nil, nil, fw.ActionAccept, "") require.NoError(b, err) }, desc: "Baseline: Single 'allow all' rule without connection tracking", @@ -114,10 +113,15 @@ func BenchmarkCoreFiltering(b *testing.B) { // Add explicit rules matching return traffic pattern for i := 0; i < 1000; i++ { // Simulate realistic ruleset size ip := generateRandomIPs(1)[0] - _, err := m.AddPeerFiltering(ip, fw.ProtocolTCP, + _, err := m.AddPeerFiltering( + nil, + ip, + fw.ProtocolTCP, &fw.Port{Values: []uint16{uint16(1024 + i)}}, &fw.Port{Values: []uint16{80}}, - fw.ActionAccept, "", "explicit return") + fw.ActionAccept, + "", + ) require.NoError(b, err) } }, @@ -128,8 +132,15 @@ func BenchmarkCoreFiltering(b *testing.B) { stateful: true, setupFunc: func(m *Manager) { // Add some basic rules but rely on state for established connections - _, err := m.AddPeerFiltering(net.ParseIP("0.0.0.0"), fw.ProtocolTCP, nil, nil, - fw.ActionDrop, "", "default drop") + _, err := m.AddPeerFiltering( + nil, + net.ParseIP("0.0.0.0"), + fw.ProtocolTCP, + nil, + nil, + fw.ActionDrop, + "", + ) require.NoError(b, err) }, desc: "Connection tracking with established connections", @@ -158,7 +169,7 @@ func BenchmarkCoreFiltering(b *testing.B) { // Create manager and basic setup manager, _ := Create(&IFaceMock{ SetFilterFunc: func(device.PacketFilter) error { return nil }, - }, false) + }, false, flowLogger) defer b.Cleanup(func() { require.NoError(b, manager.Close(nil)) }) @@ -182,13 +193,13 @@ func BenchmarkCoreFiltering(b *testing.B) { // For stateful scenarios, establish the connection if sc.stateful { - manager.processOutgoingHooks(outbound) + manager.processOutgoingHooks(outbound, 0) } // Measure inbound packet processing b.ResetTimer() for i := 0; i < b.N; i++ { - manager.dropFilter(inbound) + manager.dropFilter(inbound, 0) } }) } @@ -203,7 +214,7 @@ func BenchmarkStateScaling(b *testing.B) { b.Run(fmt.Sprintf("conns_%d", count), func(b *testing.B) { manager, _ := Create(&IFaceMock{ SetFilterFunc: func(device.PacketFilter) error { return nil }, - }, false) + }, false, flowLogger) b.Cleanup(func() { require.NoError(b, manager.Close(nil)) }) @@ -219,7 +230,7 @@ func BenchmarkStateScaling(b *testing.B) { for i := 0; i < count; i++ { outbound := generatePacket(b, srcIPs[i], dstIPs[i], uint16(1024+i), 80, layers.IPProtocolTCP) - manager.processOutgoingHooks(outbound) + manager.processOutgoingHooks(outbound, 0) } // Test packet @@ -227,11 +238,11 @@ func BenchmarkStateScaling(b *testing.B) { testIn := generatePacket(b, dstIPs[0], srcIPs[0], 80, 1024, layers.IPProtocolTCP) // First establish our test connection - manager.processOutgoingHooks(testOut) + manager.processOutgoingHooks(testOut, 0) b.ResetTimer() for i := 0; i < b.N; i++ { - manager.dropFilter(testIn) + manager.dropFilter(testIn, 0) } }) } @@ -251,7 +262,7 @@ func BenchmarkEstablishmentOverhead(b *testing.B) { b.Run(sc.name, func(b *testing.B) { manager, _ := Create(&IFaceMock{ SetFilterFunc: func(device.PacketFilter) error { return nil }, - }, false) + }, false, flowLogger) b.Cleanup(func() { require.NoError(b, manager.Close(nil)) }) @@ -267,12 +278,12 @@ func BenchmarkEstablishmentOverhead(b *testing.B) { inbound := generatePacket(b, dstIP, srcIP, 80, 1024, layers.IPProtocolTCP) if sc.established { - manager.processOutgoingHooks(outbound) + manager.processOutgoingHooks(outbound, 0) } b.ResetTimer() for i := 0; i < b.N; i++ { - manager.dropFilter(inbound) + manager.dropFilter(inbound, 0) } }) } @@ -450,7 +461,7 @@ func BenchmarkRoutedNetworkReturn(b *testing.B) { b.Run(sc.name, func(b *testing.B) { manager, _ := Create(&IFaceMock{ SetFilterFunc: func(device.PacketFilter) error { return nil }, - }, false) + }, false, flowLogger) b.Cleanup(func() { require.NoError(b, manager.Close(nil)) }) @@ -466,25 +477,25 @@ func BenchmarkRoutedNetworkReturn(b *testing.B) { // For stateful cases and established connections if !strings.Contains(sc.name, "allow_non_wg") || (strings.Contains(sc.state, "established") || sc.state == "post_handshake") { - manager.processOutgoingHooks(outbound) + manager.processOutgoingHooks(outbound, 0) // For TCP post-handshake, simulate full handshake if sc.state == "post_handshake" { // SYN syn := generateTCPPacketWithFlags(b, srcIP, dstIP, 1024, 80, uint16(conntrack.TCPSyn)) - manager.processOutgoingHooks(syn) + manager.processOutgoingHooks(syn, 0) // SYN-ACK synack := generateTCPPacketWithFlags(b, dstIP, srcIP, 80, 1024, uint16(conntrack.TCPSyn|conntrack.TCPAck)) - manager.dropFilter(synack) + manager.dropFilter(synack, 0) // ACK ack := generateTCPPacketWithFlags(b, srcIP, dstIP, 1024, 80, uint16(conntrack.TCPAck)) - manager.processOutgoingHooks(ack) + manager.processOutgoingHooks(ack, 0) } } b.ResetTimer() for i := 0; i < b.N; i++ { - manager.dropFilter(inbound) + manager.dropFilter(inbound, 0) } }) } @@ -577,7 +588,7 @@ func BenchmarkLongLivedConnections(b *testing.B) { manager, _ := Create(&IFaceMock{ SetFilterFunc: func(device.PacketFilter) error { return nil }, - }, false) + }, false, flowLogger) defer b.Cleanup(func() { require.NoError(b, manager.Close(nil)) }) @@ -590,10 +601,7 @@ func BenchmarkLongLivedConnections(b *testing.B) { // Setup initial state based on scenario if sc.rules { // Single rule to allow all return traffic from port 80 - _, err := manager.AddPeerFiltering(net.ParseIP("0.0.0.0"), fw.ProtocolTCP, - &fw.Port{Values: []uint16{80}}, - nil, - fw.ActionAccept, "", "return traffic") + _, err := manager.AddPeerFiltering(nil, net.ParseIP("0.0.0.0"), fw.ProtocolTCP, &fw.Port{Values: []uint16{80}}, nil, fw.ActionAccept, "") require.NoError(b, err) } @@ -616,17 +624,17 @@ func BenchmarkLongLivedConnections(b *testing.B) { // Initial SYN syn := generateTCPPacketWithFlags(b, srcIPs[i], dstIPs[i], uint16(1024+i), 80, uint16(conntrack.TCPSyn)) - manager.processOutgoingHooks(syn) + manager.processOutgoingHooks(syn, 0) // SYN-ACK synack := generateTCPPacketWithFlags(b, dstIPs[i], srcIPs[i], 80, uint16(1024+i), uint16(conntrack.TCPSyn|conntrack.TCPAck)) - manager.dropFilter(synack) + manager.dropFilter(synack, 0) // ACK ack := generateTCPPacketWithFlags(b, srcIPs[i], dstIPs[i], uint16(1024+i), 80, uint16(conntrack.TCPAck)) - manager.processOutgoingHooks(ack) + manager.processOutgoingHooks(ack, 0) } // Prepare test packets simulating bidirectional traffic @@ -647,9 +655,9 @@ func BenchmarkLongLivedConnections(b *testing.B) { // Simulate bidirectional traffic // First outbound data - manager.processOutgoingHooks(outPackets[connIdx]) + manager.processOutgoingHooks(outPackets[connIdx], 0) // Then inbound response - this is what we're actually measuring - manager.dropFilter(inPackets[connIdx]) + manager.dropFilter(inPackets[connIdx], 0) } }) } @@ -668,7 +676,7 @@ func BenchmarkShortLivedConnections(b *testing.B) { manager, _ := Create(&IFaceMock{ SetFilterFunc: func(device.PacketFilter) error { return nil }, - }, false) + }, false, flowLogger) defer b.Cleanup(func() { require.NoError(b, manager.Close(nil)) }) @@ -681,10 +689,7 @@ func BenchmarkShortLivedConnections(b *testing.B) { // Setup initial state based on scenario if sc.rules { // Single rule to allow all return traffic from port 80 - _, err := manager.AddPeerFiltering(net.ParseIP("0.0.0.0"), fw.ProtocolTCP, - &fw.Port{Values: []uint16{80}}, - nil, - fw.ActionAccept, "", "return traffic") + _, err := manager.AddPeerFiltering(nil, net.ParseIP("0.0.0.0"), fw.ProtocolTCP, &fw.Port{Values: []uint16{80}}, nil, fw.ActionAccept, "") require.NoError(b, err) } @@ -756,19 +761,19 @@ func BenchmarkShortLivedConnections(b *testing.B) { p := patterns[connIdx] // Connection establishment - manager.processOutgoingHooks(p.syn) - manager.dropFilter(p.synAck) - manager.processOutgoingHooks(p.ack) + manager.processOutgoingHooks(p.syn, 0) + manager.dropFilter(p.synAck, 0) + manager.processOutgoingHooks(p.ack, 0) // Data transfer - manager.processOutgoingHooks(p.request) - manager.dropFilter(p.response) + manager.processOutgoingHooks(p.request, 0) + manager.dropFilter(p.response, 0) // Connection teardown - manager.processOutgoingHooks(p.finClient) - manager.dropFilter(p.ackServer) - manager.dropFilter(p.finServer) - manager.processOutgoingHooks(p.ackClient) + manager.processOutgoingHooks(p.finClient, 0) + manager.dropFilter(p.ackServer, 0) + manager.dropFilter(p.finServer, 0) + manager.processOutgoingHooks(p.ackClient, 0) } }) } @@ -787,7 +792,7 @@ func BenchmarkParallelLongLivedConnections(b *testing.B) { manager, _ := Create(&IFaceMock{ SetFilterFunc: func(device.PacketFilter) error { return nil }, - }, false) + }, false, flowLogger) defer b.Cleanup(func() { require.NoError(b, manager.Close(nil)) }) @@ -799,10 +804,7 @@ func BenchmarkParallelLongLivedConnections(b *testing.B) { // Setup initial state based on scenario if sc.rules { - _, err := manager.AddPeerFiltering(net.ParseIP("0.0.0.0"), fw.ProtocolTCP, - &fw.Port{Values: []uint16{80}}, - nil, - fw.ActionAccept, "", "return traffic") + _, err := manager.AddPeerFiltering(nil, net.ParseIP("0.0.0.0"), fw.ProtocolTCP, &fw.Port{Values: []uint16{80}}, nil, fw.ActionAccept, "") require.NoError(b, err) } @@ -824,15 +826,15 @@ func BenchmarkParallelLongLivedConnections(b *testing.B) { for i := 0; i < sc.connCount; i++ { syn := generateTCPPacketWithFlags(b, srcIPs[i], dstIPs[i], uint16(1024+i), 80, uint16(conntrack.TCPSyn)) - manager.processOutgoingHooks(syn) + manager.processOutgoingHooks(syn, 0) synack := generateTCPPacketWithFlags(b, dstIPs[i], srcIPs[i], 80, uint16(1024+i), uint16(conntrack.TCPSyn|conntrack.TCPAck)) - manager.dropFilter(synack) + manager.dropFilter(synack, 0) ack := generateTCPPacketWithFlags(b, srcIPs[i], dstIPs[i], uint16(1024+i), 80, uint16(conntrack.TCPAck)) - manager.processOutgoingHooks(ack) + manager.processOutgoingHooks(ack, 0) } // Pre-generate test packets @@ -854,8 +856,8 @@ func BenchmarkParallelLongLivedConnections(b *testing.B) { counter++ // Simulate bidirectional traffic - manager.processOutgoingHooks(outPackets[connIdx]) - manager.dropFilter(inPackets[connIdx]) + manager.processOutgoingHooks(outPackets[connIdx], 0) + manager.dropFilter(inPackets[connIdx], 0) } }) }) @@ -875,7 +877,7 @@ func BenchmarkParallelShortLivedConnections(b *testing.B) { manager, _ := Create(&IFaceMock{ SetFilterFunc: func(device.PacketFilter) error { return nil }, - }, false) + }, false, flowLogger) defer b.Cleanup(func() { require.NoError(b, manager.Close(nil)) }) @@ -886,10 +888,7 @@ func BenchmarkParallelShortLivedConnections(b *testing.B) { }) if sc.rules { - _, err := manager.AddPeerFiltering(net.ParseIP("0.0.0.0"), fw.ProtocolTCP, - &fw.Port{Values: []uint16{80}}, - nil, - fw.ActionAccept, "", "return traffic") + _, err := manager.AddPeerFiltering(nil, net.ParseIP("0.0.0.0"), fw.ProtocolTCP, &fw.Port{Values: []uint16{80}}, nil, fw.ActionAccept, "") require.NoError(b, err) } @@ -951,17 +950,17 @@ func BenchmarkParallelShortLivedConnections(b *testing.B) { p := patterns[connIdx] // Full connection lifecycle - manager.processOutgoingHooks(p.syn) - manager.dropFilter(p.synAck) - manager.processOutgoingHooks(p.ack) + manager.processOutgoingHooks(p.syn, 0) + manager.dropFilter(p.synAck, 0) + manager.processOutgoingHooks(p.ack, 0) - manager.processOutgoingHooks(p.request) - manager.dropFilter(p.response) + manager.processOutgoingHooks(p.request, 0) + manager.dropFilter(p.response, 0) - manager.processOutgoingHooks(p.finClient) - manager.dropFilter(p.ackServer) - manager.dropFilter(p.finServer) - manager.processOutgoingHooks(p.ackClient) + manager.processOutgoingHooks(p.finClient, 0) + manager.dropFilter(p.ackServer, 0) + manager.dropFilter(p.finServer, 0) + manager.processOutgoingHooks(p.ackClient, 0) } }) }) @@ -1033,14 +1032,7 @@ func BenchmarkRouteACLs(b *testing.B) { } for _, r := range rules { - _, err := manager.AddRouteFiltering( - r.sources, - r.dest, - r.proto, - nil, - r.port, - fw.ActionAccept, - ) + _, err := manager.AddRouteFiltering(nil, r.sources, r.dest, r.proto, nil, r.port, fw.ActionAccept) if err != nil { b.Fatal(err) } @@ -1062,8 +1054,8 @@ func BenchmarkRouteACLs(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { for _, tc := range cases { - srcIP := net.ParseIP(tc.srcIP) - dstIP := net.ParseIP(tc.dstIP) + srcIP := netip.MustParseAddr(tc.srcIP) + dstIP := netip.MustParseAddr(tc.dstIP) manager.routeACLsPass(srcIP, dstIP, tc.proto, 0, tc.dstPort) } } diff --git a/client/firewall/uspfilter/uspfilter_filter_test.go b/client/firewall/uspfilter/uspfilter_filter_test.go index 1497a5ba7..ba97c2643 100644 --- a/client/firewall/uspfilter/uspfilter_filter_test.go +++ b/client/firewall/uspfilter/uspfilter_filter_test.go @@ -34,7 +34,7 @@ func TestPeerACLFiltering(t *testing.T) { }, } - manager, err := Create(ifaceMock, false) + manager, err := Create(ifaceMock, false, flowLogger) require.NoError(t, err) require.NotNil(t, manager) @@ -192,20 +192,20 @@ func TestPeerACLFiltering(t *testing.T) { t.Run("Implicit DROP (no rules)", func(t *testing.T) { packet := createTestPacket(t, "100.10.0.1", "100.10.0.100", fw.ProtocolTCP, 12345, 443) - isDropped := manager.DropIncoming(packet) + isDropped := manager.DropIncoming(packet, 0) require.True(t, isDropped, "Packet should be dropped when no rules exist") }) for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { rules, err := manager.AddPeerFiltering( + nil, net.ParseIP(tc.ruleIP), tc.ruleProto, tc.ruleSrcPort, tc.ruleDstPort, tc.ruleAction, "", - tc.name, ) require.NoError(t, err) require.NotEmpty(t, rules) @@ -217,7 +217,7 @@ func TestPeerACLFiltering(t *testing.T) { }) packet := createTestPacket(t, tc.srcIP, tc.dstIP, tc.proto, tc.srcPort, tc.dstPort) - isDropped := manager.DropIncoming(packet) + isDropped := manager.DropIncoming(packet, 0) require.Equal(t, tc.shouldBeBlocked, isDropped) }) } @@ -302,12 +302,12 @@ func setupRoutedManager(tb testing.TB, network string) *Manager { }, } - manager, err := Create(ifaceMock, false) + manager, err := Create(ifaceMock, false, flowLogger) require.NoError(tb, manager.EnableRouting()) require.NoError(tb, err) require.NotNil(tb, manager) - require.True(tb, manager.routingEnabled) - require.False(tb, manager.nativeRouter) + require.True(tb, manager.routingEnabled.Load()) + require.False(tb, manager.nativeRouter.Load()) tb.Cleanup(func() { require.NoError(tb, manager.Close(nil)) @@ -803,6 +803,7 @@ func TestRouteACLFiltering(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { rule, err := manager.AddRouteFiltering( + nil, tc.rule.sources, tc.rule.dest, tc.rule.proto, @@ -817,12 +818,12 @@ func TestRouteACLFiltering(t *testing.T) { require.NoError(t, manager.DeleteRouteRule(rule)) }) - srcIP := net.ParseIP(tc.srcIP) - dstIP := net.ParseIP(tc.dstIP) + srcIP := netip.MustParseAddr(tc.srcIP) + dstIP := netip.MustParseAddr(tc.dstIP) // testing routeACLsPass only and not DropIncoming, as routed packets are dropped after being passed // to the forwarder - isAllowed := manager.routeACLsPass(srcIP, dstIP, tc.proto, tc.srcPort, tc.dstPort) + _, isAllowed := manager.routeACLsPass(srcIP, dstIP, tc.proto, tc.srcPort, tc.dstPort) require.Equal(t, tc.shouldPass, isAllowed) }) } @@ -985,6 +986,7 @@ func TestRouteACLOrder(t *testing.T) { var rules []fw.Rule for _, r := range tc.rules { rule, err := manager.AddRouteFiltering( + nil, r.sources, r.dest, r.proto, @@ -1004,10 +1006,10 @@ func TestRouteACLOrder(t *testing.T) { }) for i, p := range tc.packets { - srcIP := net.ParseIP(p.srcIP) - dstIP := net.ParseIP(p.dstIP) + srcIP := netip.MustParseAddr(p.srcIP) + dstIP := netip.MustParseAddr(p.dstIP) - isAllowed := manager.routeACLsPass(srcIP, dstIP, p.proto, p.srcPort, p.dstPort) + _, isAllowed := manager.routeACLsPass(srcIP, dstIP, p.proto, p.srcPort, p.dstPort) require.Equal(t, p.shouldPass, isAllowed, "packet %d failed", i) } }) diff --git a/client/firewall/uspfilter/uspfilter_test.go b/client/firewall/uspfilter/uspfilter_test.go index bcb9624ee..a095a5e39 100644 --- a/client/firewall/uspfilter/uspfilter_test.go +++ b/client/firewall/uspfilter/uspfilter_test.go @@ -1,8 +1,10 @@ package uspfilter import ( + "context" "fmt" "net" + "net/netip" "sync" "testing" "time" @@ -18,9 +20,11 @@ import ( "github.com/netbirdio/netbird/client/firewall/uspfilter/log" "github.com/netbirdio/netbird/client/iface/device" "github.com/netbirdio/netbird/client/iface/wgaddr" + "github.com/netbirdio/netbird/client/internal/netflow" ) var logger = log.NewFromLogrus(logrus.StandardLogger()) +var flowLogger = netflow.NewManager(context.Background(), nil, []byte{}, nil).GetLogger() type IFaceMock struct { SetFilterFunc func(device.PacketFilter) error @@ -62,7 +66,7 @@ func TestManagerCreate(t *testing.T) { SetFilterFunc: func(device.PacketFilter) error { return nil }, } - m, err := Create(ifaceMock, false) + m, err := Create(ifaceMock, false, flowLogger) if err != nil { t.Errorf("failed to create Manager: %v", err) return @@ -82,7 +86,7 @@ func TestManagerAddPeerFiltering(t *testing.T) { }, } - m, err := Create(ifaceMock, false) + m, err := Create(ifaceMock, false, flowLogger) if err != nil { t.Errorf("failed to create Manager: %v", err) return @@ -92,9 +96,8 @@ func TestManagerAddPeerFiltering(t *testing.T) { proto := fw.ProtocolTCP port := &fw.Port{Values: []uint16{80}} action := fw.ActionDrop - comment := "Test rule" - rule, err := m.AddPeerFiltering(ip, proto, nil, port, action, "", comment) + rule, err := m.AddPeerFiltering(nil, ip, proto, nil, port, action, "") if err != nil { t.Errorf("failed to add filtering: %v", err) return @@ -116,26 +119,25 @@ func TestManagerDeleteRule(t *testing.T) { SetFilterFunc: func(device.PacketFilter) error { return nil }, } - m, err := Create(ifaceMock, false) + m, err := Create(ifaceMock, false, flowLogger) if err != nil { t.Errorf("failed to create Manager: %v", err) return } - ip := net.ParseIP("192.168.1.1") + ip := netip.MustParseAddr("192.168.1.1") proto := fw.ProtocolTCP port := &fw.Port{Values: []uint16{80}} action := fw.ActionDrop - comment := "Test rule 2" - rule2, err := m.AddPeerFiltering(ip, proto, nil, port, action, "", comment) + rule2, err := m.AddPeerFiltering(nil, ip.AsSlice(), proto, nil, port, action, "") if err != nil { t.Errorf("failed to add filtering: %v", err) return } for _, r := range rule2 { - if _, ok := m.incomingRules[ip.String()][r.ID()]; !ok { + if _, ok := m.incomingRules[ip][r.ID()]; !ok { t.Errorf("rule2 is not in the incomingRules") } } @@ -149,7 +151,7 @@ func TestManagerDeleteRule(t *testing.T) { } for _, r := range rule2 { - if _, ok := m.incomingRules[ip.String()][r.ID()]; ok { + if _, ok := m.incomingRules[ip][r.ID()]; ok { t.Errorf("rule2 is not in the incomingRules") } } @@ -160,7 +162,7 @@ func TestAddUDPPacketHook(t *testing.T) { name string in bool expDir fw.RuleDirection - ip net.IP + ip netip.Addr dPort uint16 hook func([]byte) bool expectedID string @@ -169,7 +171,7 @@ func TestAddUDPPacketHook(t *testing.T) { name: "Test Outgoing UDP Packet Hook", in: false, expDir: fw.RuleDirectionOUT, - ip: net.IPv4(10, 168, 0, 1), + ip: netip.MustParseAddr("10.168.0.1"), dPort: 8000, hook: func([]byte) bool { return true }, }, @@ -177,7 +179,7 @@ func TestAddUDPPacketHook(t *testing.T) { name: "Test Incoming UDP Packet Hook", in: true, expDir: fw.RuleDirectionIN, - ip: net.IPv6loopback, + ip: netip.MustParseAddr("::1"), dPort: 9000, hook: func([]byte) bool { return false }, }, @@ -187,18 +189,18 @@ func TestAddUDPPacketHook(t *testing.T) { t.Run(tt.name, func(t *testing.T) { manager, err := Create(&IFaceMock{ SetFilterFunc: func(device.PacketFilter) error { return nil }, - }, false) + }, false, flowLogger) require.NoError(t, err) manager.AddUDPPacketHook(tt.in, tt.ip, tt.dPort, tt.hook) var addedRule PeerRule if tt.in { - if len(manager.incomingRules[tt.ip.String()]) != 1 { + if len(manager.incomingRules[tt.ip]) != 1 { t.Errorf("expected 1 incoming rule, got %d", len(manager.incomingRules)) return } - for _, rule := range manager.incomingRules[tt.ip.String()] { + for _, rule := range manager.incomingRules[tt.ip] { addedRule = rule } } else { @@ -206,12 +208,12 @@ func TestAddUDPPacketHook(t *testing.T) { t.Errorf("expected 1 outgoing rule, got %d", len(manager.outgoingRules)) return } - for _, rule := range manager.outgoingRules[tt.ip.String()] { + for _, rule := range manager.outgoingRules[tt.ip] { addedRule = rule } } - if !tt.ip.Equal(addedRule.ip) { + if tt.ip.Compare(addedRule.ip) != 0 { t.Errorf("expected ip %s, got %s", tt.ip, addedRule.ip) return } @@ -236,7 +238,7 @@ func TestManagerReset(t *testing.T) { SetFilterFunc: func(device.PacketFilter) error { return nil }, } - m, err := Create(ifaceMock, false) + m, err := Create(ifaceMock, false, flowLogger) if err != nil { t.Errorf("failed to create Manager: %v", err) return @@ -246,9 +248,8 @@ func TestManagerReset(t *testing.T) { proto := fw.ProtocolTCP port := &fw.Port{Values: []uint16{80}} action := fw.ActionDrop - comment := "Test rule" - _, err = m.AddPeerFiltering(ip, proto, nil, port, action, "", comment) + _, err = m.AddPeerFiltering(nil, ip, proto, nil, port, action, "") if err != nil { t.Errorf("failed to add filtering: %v", err) return @@ -279,7 +280,7 @@ func TestNotMatchByIP(t *testing.T) { }, } - m, err := Create(ifaceMock, false) + m, err := Create(ifaceMock, false, flowLogger) if err != nil { t.Errorf("failed to create Manager: %v", err) return @@ -292,9 +293,8 @@ func TestNotMatchByIP(t *testing.T) { ip := net.ParseIP("0.0.0.0") proto := fw.ProtocolUDP action := fw.ActionAccept - comment := "Test rule" - _, err = m.AddPeerFiltering(ip, proto, nil, nil, action, "", comment) + _, err = m.AddPeerFiltering(nil, ip, proto, nil, nil, action, "") if err != nil { t.Errorf("failed to add filtering: %v", err) return @@ -328,7 +328,7 @@ func TestNotMatchByIP(t *testing.T) { return } - if m.dropFilter(buf.Bytes()) { + if m.dropFilter(buf.Bytes(), 0) { t.Errorf("expected packet to be accepted") return } @@ -347,7 +347,7 @@ func TestRemovePacketHook(t *testing.T) { } // creating manager instance - manager, err := Create(iface, false) + manager, err := Create(iface, false, flowLogger) if err != nil { t.Fatalf("Failed to create Manager: %s", err) } @@ -357,7 +357,7 @@ func TestRemovePacketHook(t *testing.T) { // Add a UDP packet hook hookFunc := func(data []byte) bool { return true } - hookID := manager.AddUDPPacketHook(false, net.IPv4(192, 168, 0, 1), 8080, hookFunc) + hookID := manager.AddUDPPacketHook(false, netip.MustParseAddr("192.168.0.1"), 8080, hookFunc) // Assert the hook is added by finding it in the manager's outgoing rules found := false @@ -393,7 +393,7 @@ func TestRemovePacketHook(t *testing.T) { func TestProcessOutgoingHooks(t *testing.T) { manager, err := Create(&IFaceMock{ SetFilterFunc: func(device.PacketFilter) error { return nil }, - }, false) + }, false, flowLogger) require.NoError(t, err) manager.wgNetwork = &net.IPNet{ @@ -401,7 +401,7 @@ func TestProcessOutgoingHooks(t *testing.T) { Mask: net.CIDRMask(16, 32), } manager.udpTracker.Close() - manager.udpTracker = conntrack.NewUDPTracker(100*time.Millisecond, logger) + manager.udpTracker = conntrack.NewUDPTracker(100*time.Millisecond, logger, flowLogger) defer func() { require.NoError(t, manager.Close(nil)) }() @@ -423,7 +423,7 @@ func TestProcessOutgoingHooks(t *testing.T) { hookCalled := false hookID := manager.AddUDPPacketHook( false, - net.ParseIP("100.10.0.100"), + netip.MustParseAddr("100.10.0.100"), 53, func([]byte) bool { hookCalled = true @@ -458,7 +458,7 @@ func TestProcessOutgoingHooks(t *testing.T) { require.NoError(t, err) // Test hook gets called - result := manager.processOutgoingHooks(buf.Bytes()) + result := manager.processOutgoingHooks(buf.Bytes(), 0) require.True(t, result) require.True(t, hookCalled) @@ -468,7 +468,7 @@ func TestProcessOutgoingHooks(t *testing.T) { err = gopacket.SerializeLayers(buf, opts, ipv4) require.NoError(t, err) - result = manager.processOutgoingHooks(buf.Bytes()) + result = manager.processOutgoingHooks(buf.Bytes(), 0) require.False(t, result) } @@ -479,7 +479,7 @@ func TestUSPFilterCreatePerformance(t *testing.T) { ifaceMock := &IFaceMock{ SetFilterFunc: func(device.PacketFilter) error { return nil }, } - manager, err := Create(ifaceMock, false) + manager, err := Create(ifaceMock, false, flowLogger) require.NoError(t, err) time.Sleep(time.Second) @@ -494,7 +494,7 @@ func TestUSPFilterCreatePerformance(t *testing.T) { start := time.Now() for i := 0; i < testMax; i++ { port := &fw.Port{Values: []uint16{uint16(1000 + i)}} - _, err = manager.AddPeerFiltering(ip, "tcp", nil, port, fw.ActionAccept, "", "accept HTTP traffic") + _, err = manager.AddPeerFiltering(nil, ip, "tcp", nil, port, fw.ActionAccept, "") require.NoError(t, err, "failed to add rule") } @@ -506,7 +506,7 @@ func TestUSPFilterCreatePerformance(t *testing.T) { func TestStatefulFirewall_UDPTracking(t *testing.T) { manager, err := Create(&IFaceMock{ SetFilterFunc: func(device.PacketFilter) error { return nil }, - }, false) + }, false, flowLogger) require.NoError(t, err) manager.wgNetwork = &net.IPNet{ @@ -515,7 +515,7 @@ func TestStatefulFirewall_UDPTracking(t *testing.T) { } manager.udpTracker.Close() // Close the existing tracker - manager.udpTracker = conntrack.NewUDPTracker(200*time.Millisecond, logger) + manager.udpTracker = conntrack.NewUDPTracker(200*time.Millisecond, logger, flowLogger) manager.decoders = sync.Pool{ New: func() any { d := &decoder{ @@ -534,8 +534,8 @@ func TestStatefulFirewall_UDPTracking(t *testing.T) { }() // Set up packet parameters - srcIP := net.ParseIP("100.10.0.1") - dstIP := net.ParseIP("100.10.0.100") + srcIP := netip.MustParseAddr("100.10.0.1") + dstIP := netip.MustParseAddr("100.10.0.100") srcPort := uint16(51334) dstPort := uint16(53) @@ -543,8 +543,8 @@ func TestStatefulFirewall_UDPTracking(t *testing.T) { outboundIPv4 := &layers.IPv4{ TTL: 64, Version: 4, - SrcIP: srcIP, - DstIP: dstIP, + SrcIP: srcIP.AsSlice(), + DstIP: dstIP.AsSlice(), Protocol: layers.IPProtocolUDP, } outboundUDP := &layers.UDP{ @@ -569,15 +569,15 @@ func TestStatefulFirewall_UDPTracking(t *testing.T) { require.NoError(t, err) // Process outbound packet and verify connection tracking - drop := manager.DropOutgoing(outboundBuf.Bytes()) + drop := manager.DropOutgoing(outboundBuf.Bytes(), 0) require.False(t, drop, "Initial outbound packet should not be dropped") // Verify connection was tracked conn, exists := manager.udpTracker.GetConnection(srcIP, srcPort, dstIP, dstPort) require.True(t, exists, "Connection should be tracked after outbound packet") - require.True(t, conntrack.ValidateIPs(conntrack.MakeIPAddr(srcIP), conn.SourceIP), "Source IP should match") - require.True(t, conntrack.ValidateIPs(conntrack.MakeIPAddr(dstIP), conn.DestIP), "Destination IP should match") + require.True(t, srcIP.Compare(conn.SourceIP) == 0, "Source IP should match") + require.True(t, dstIP.Compare(conn.DestIP) == 0, "Destination IP should match") require.Equal(t, srcPort, conn.SourcePort, "Source port should match") require.Equal(t, dstPort, conn.DestPort, "Destination port should match") @@ -585,8 +585,8 @@ func TestStatefulFirewall_UDPTracking(t *testing.T) { inboundIPv4 := &layers.IPv4{ TTL: 64, Version: 4, - SrcIP: dstIP, // Original destination is now source - DstIP: srcIP, // Original source is now destination + SrcIP: dstIP.AsSlice(), // Original destination is now source + DstIP: srcIP.AsSlice(), // Original source is now destination Protocol: layers.IPProtocolUDP, } inboundUDP := &layers.UDP{ @@ -636,7 +636,7 @@ func TestStatefulFirewall_UDPTracking(t *testing.T) { for _, cp := range checkPoints { time.Sleep(cp.sleep) - drop = manager.dropFilter(inboundBuf.Bytes()) + drop = manager.dropFilter(inboundBuf.Bytes(), 0) require.Equal(t, cp.shouldAllow, !drop, cp.description) // If the connection should still be valid, verify it exists @@ -685,7 +685,7 @@ func TestStatefulFirewall_UDPTracking(t *testing.T) { } // Create a new outbound connection for invalid tests - drop = manager.processOutgoingHooks(outboundBuf.Bytes()) + drop = manager.processOutgoingHooks(outboundBuf.Bytes(), 0) require.False(t, drop, "Second outbound packet should not be dropped") for _, tc := range invalidCases { @@ -707,7 +707,7 @@ func TestStatefulFirewall_UDPTracking(t *testing.T) { require.NoError(t, err) // Verify the invalid packet is dropped - drop = manager.dropFilter(testBuf.Bytes()) + drop = manager.dropFilter(testBuf.Bytes(), 0) require.True(t, drop, tc.description) }) } diff --git a/client/iface/device/device_filter.go b/client/iface/device/device_filter.go index f87f10429..c9b7e2448 100644 --- a/client/iface/device/device_filter.go +++ b/client/iface/device/device_filter.go @@ -2,6 +2,7 @@ package device import ( "net" + "net/netip" "sync" "golang.zx2c4.com/wireguard/tun" @@ -10,16 +11,16 @@ import ( // PacketFilter interface for firewall abilities type PacketFilter interface { // DropOutgoing filter outgoing packets from host to external destinations - DropOutgoing(packetData []byte) bool + DropOutgoing(packetData []byte, size int) bool // DropIncoming filter incoming packets from external sources to host - DropIncoming(packetData []byte) bool + DropIncoming(packetData []byte, size int) bool // AddUDPPacketHook calls hook when UDP packet from given direction matched // // Hook function returns flag which indicates should be the matched package dropped or not. // Hook function receives raw network packet data as argument. - AddUDPPacketHook(in bool, ip net.IP, dPort uint16, hook func(packet []byte) bool) string + AddUDPPacketHook(in bool, ip netip.Addr, dPort uint16, hook func(packet []byte) bool) string // RemovePacketHook removes hook by ID RemovePacketHook(hookID string) error @@ -57,7 +58,7 @@ func (d *FilteredDevice) Read(bufs [][]byte, sizes []int, offset int) (n int, er } for i := 0; i < n; i++ { - if filter.DropOutgoing(bufs[i][offset : offset+sizes[i]]) { + if filter.DropOutgoing(bufs[i][offset:offset+sizes[i]], sizes[i]) { bufs = append(bufs[:i], bufs[i+1:]...) sizes = append(sizes[:i], sizes[i+1:]...) n-- @@ -81,7 +82,7 @@ func (d *FilteredDevice) Write(bufs [][]byte, offset int) (int, error) { filteredBufs := make([][]byte, 0, len(bufs)) dropped := 0 for _, buf := range bufs { - if !filter.DropIncoming(buf[offset:]) { + if !filter.DropIncoming(buf[offset:], len(buf)) { filteredBufs = append(filteredBufs, buf) dropped++ } diff --git a/client/iface/device/device_filter_test.go b/client/iface/device/device_filter_test.go index d3278b918..c90269e82 100644 --- a/client/iface/device/device_filter_test.go +++ b/client/iface/device/device_filter_test.go @@ -146,7 +146,7 @@ func TestDeviceWrapperRead(t *testing.T) { tun.EXPECT().Write(mockBufs, 0).Return(0, nil) filter := mocks.NewMockPacketFilter(ctrl) - filter.EXPECT().DropIncoming(gomock.Any()).Return(true) + filter.EXPECT().DropIncoming(gomock.Any(), gomock.Any()).Return(true) wrapped := newDeviceFilter(tun) wrapped.filter = filter @@ -201,7 +201,7 @@ func TestDeviceWrapperRead(t *testing.T) { return 1, nil }) filter := mocks.NewMockPacketFilter(ctrl) - filter.EXPECT().DropOutgoing(gomock.Any()).Return(true) + filter.EXPECT().DropOutgoing(gomock.Any(), gomock.Any()).Return(true) wrapped := newDeviceFilter(tun) wrapped.filter = filter diff --git a/client/iface/mocks/filter.go b/client/iface/mocks/filter.go index 6348e0e77..faac55d68 100644 --- a/client/iface/mocks/filter.go +++ b/client/iface/mocks/filter.go @@ -6,6 +6,7 @@ package mocks import ( net "net" + "net/netip" reflect "reflect" gomock "github.com/golang/mock/gomock" @@ -35,7 +36,7 @@ func (m *MockPacketFilter) EXPECT() *MockPacketFilterMockRecorder { } // AddUDPPacketHook mocks base method. -func (m *MockPacketFilter) AddUDPPacketHook(arg0 bool, arg1 net.IP, arg2 uint16, arg3 func([]byte) bool) string { +func (m *MockPacketFilter) AddUDPPacketHook(arg0 bool, arg1 netip.Addr, arg2 uint16, arg3 func([]byte) bool) string { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "AddUDPPacketHook", arg0, arg1, arg2, arg3) ret0, _ := ret[0].(string) @@ -49,31 +50,31 @@ func (mr *MockPacketFilterMockRecorder) AddUDPPacketHook(arg0, arg1, arg2, arg3 } // DropIncoming mocks base method. -func (m *MockPacketFilter) DropIncoming(arg0 []byte) bool { +func (m *MockPacketFilter) DropIncoming(arg0 []byte, arg1 int) bool { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DropIncoming", arg0) + ret := m.ctrl.Call(m, "DropIncoming", arg0, arg1) ret0, _ := ret[0].(bool) return ret0 } // DropIncoming indicates an expected call of DropIncoming. -func (mr *MockPacketFilterMockRecorder) DropIncoming(arg0 interface{}) *gomock.Call { +func (mr *MockPacketFilterMockRecorder) DropIncoming(arg0 interface{}, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DropIncoming", reflect.TypeOf((*MockPacketFilter)(nil).DropIncoming), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DropIncoming", reflect.TypeOf((*MockPacketFilter)(nil).DropIncoming), arg0, arg1) } // DropOutgoing mocks base method. -func (m *MockPacketFilter) DropOutgoing(arg0 []byte) bool { +func (m *MockPacketFilter) DropOutgoing(arg0 []byte, arg1 int) bool { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DropOutgoing", arg0) + ret := m.ctrl.Call(m, "DropOutgoing", arg0, arg1) ret0, _ := ret[0].(bool) return ret0 } // DropOutgoing indicates an expected call of DropOutgoing. -func (mr *MockPacketFilterMockRecorder) DropOutgoing(arg0 interface{}) *gomock.Call { +func (mr *MockPacketFilterMockRecorder) DropOutgoing(arg0 interface{}, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DropOutgoing", reflect.TypeOf((*MockPacketFilter)(nil).DropOutgoing), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DropOutgoing", reflect.TypeOf((*MockPacketFilter)(nil).DropOutgoing), arg0, arg1) } // RemovePacketHook mocks base method. diff --git a/client/internal/acl/manager.go b/client/internal/acl/manager.go index 7cce7402f..61fbb10ca 100644 --- a/client/internal/acl/manager.go +++ b/client/internal/acl/manager.go @@ -28,6 +28,11 @@ type Manager interface { ApplyFiltering(networkMap *mgmProto.NetworkMap) } +type protoMatch struct { + ips map[string]int + policyID []byte +} + // DefaultManager uses firewall manager to handle type DefaultManager struct { firewall firewall.Manager @@ -240,7 +245,7 @@ func (d *DefaultManager) applyRouteACL(rule *mgmProto.RouteFirewallRule) (id.Rul dPorts := convertPortInfo(rule.PortInfo) - addedRule, err := d.firewall.AddRouteFiltering(sources, destination, protocol, nil, dPorts, action) + addedRule, err := d.firewall.AddRouteFiltering(rule.PolicyID, sources, destination, protocol, nil, dPorts, action) if err != nil { return "", fmt.Errorf("add route rule: %w", err) } @@ -281,7 +286,7 @@ func (d *DefaultManager) protoRuleToFirewallRule( } } - ruleID := d.getPeerRuleID(ip, protocol, int(r.Direction), port, action, "") + ruleID := d.getPeerRuleID(ip, protocol, int(r.Direction), port, action) if rulesPair, ok := d.peerRulesPairs[ruleID]; ok { return ruleID, rulesPair, nil } @@ -289,11 +294,11 @@ func (d *DefaultManager) protoRuleToFirewallRule( var rules []firewall.Rule switch r.Direction { case mgmProto.RuleDirection_IN: - rules, err = d.addInRules(ip, protocol, port, action, ipsetName, "") + rules, err = d.addInRules(r.PolicyID, ip, protocol, port, action, ipsetName) case mgmProto.RuleDirection_OUT: // TODO: Remove this soon. Outbound rules are obsolete. // We only maintain this for return traffic (inbound dir) which is now handled by the stateful firewall already - rules, err = d.addOutRules(ip, protocol, port, action, ipsetName, "") + rules, err = d.addOutRules(r.PolicyID, ip, protocol, port, action, ipsetName) default: return "", nil, fmt.Errorf("invalid direction, skipping firewall rule") } @@ -322,14 +327,14 @@ func portInfoEmpty(portInfo *mgmProto.PortInfo) bool { } func (d *DefaultManager) addInRules( + id []byte, ip net.IP, protocol firewall.Protocol, port *firewall.Port, action firewall.Action, ipsetName string, - comment string, ) ([]firewall.Rule, error) { - rule, err := d.firewall.AddPeerFiltering(ip, protocol, nil, port, action, ipsetName, comment) + rule, err := d.firewall.AddPeerFiltering(id, ip, protocol, nil, port, action, ipsetName) if err != nil { return nil, fmt.Errorf("add firewall rule: %w", err) } @@ -338,18 +343,18 @@ func (d *DefaultManager) addInRules( } func (d *DefaultManager) addOutRules( + id []byte, ip net.IP, protocol firewall.Protocol, port *firewall.Port, action firewall.Action, ipsetName string, - comment string, ) ([]firewall.Rule, error) { if shouldSkipInvertedRule(protocol, port) { return nil, nil } - rule, err := d.firewall.AddPeerFiltering(ip, protocol, port, nil, action, ipsetName, comment) + rule, err := d.firewall.AddPeerFiltering(id, ip, protocol, port, nil, action, ipsetName) if err != nil { return nil, fmt.Errorf("add firewall rule: %w", err) } @@ -364,9 +369,8 @@ func (d *DefaultManager) getPeerRuleID( direction int, port *firewall.Port, action firewall.Action, - comment string, ) id.RuleID { - idStr := ip.String() + string(proto) + strconv.Itoa(direction) + strconv.Itoa(int(action)) + comment + idStr := ip.String() + string(proto) + strconv.Itoa(direction) + strconv.Itoa(int(action)) if port != nil { idStr += port.String() } @@ -389,10 +393,8 @@ func (d *DefaultManager) squashAcceptRules( } } - type protoMatch map[mgmProto.RuleProtocol]map[string]int - - in := protoMatch{} - out := protoMatch{} + in := map[mgmProto.RuleProtocol]*protoMatch{} + out := map[mgmProto.RuleProtocol]*protoMatch{} // trace which type of protocols was squashed squashedRules := []*mgmProto.FirewallRule{} @@ -405,14 +407,18 @@ func (d *DefaultManager) squashAcceptRules( // 2. Any of rule contains Port. // // We zeroed this to notify squash function that this protocol can't be squashed. - addRuleToCalculationMap := func(i int, r *mgmProto.FirewallRule, protocols protoMatch) { + addRuleToCalculationMap := func(i int, r *mgmProto.FirewallRule, protocols map[mgmProto.RuleProtocol]*protoMatch) { drop := r.Action == mgmProto.RuleAction_DROP || r.Port != "" if drop { - protocols[r.Protocol] = map[string]int{} + protocols[r.Protocol] = &protoMatch{ips: map[string]int{}} return } if _, ok := protocols[r.Protocol]; !ok { - protocols[r.Protocol] = map[string]int{} + protocols[r.Protocol] = &protoMatch{ + ips: map[string]int{}, + // store the first encountered PolicyID for this protocol + policyID: r.PolicyID, + } } // special case, when we receive this all network IP address @@ -424,7 +430,7 @@ func (d *DefaultManager) squashAcceptRules( return } - ipset := protocols[r.Protocol] + ipset := protocols[r.Protocol].ips if _, ok := ipset[r.PeerIP]; ok { return @@ -450,9 +456,10 @@ func (d *DefaultManager) squashAcceptRules( mgmProto.RuleProtocol_UDP, } - squash := func(matches protoMatch, direction mgmProto.RuleDirection) { + squash := func(matches map[mgmProto.RuleProtocol]*protoMatch, direction mgmProto.RuleDirection) { for _, protocol := range protocolOrders { - if ipset, ok := matches[protocol]; !ok || len(ipset) != totalIPs || len(ipset) < 2 { + match, ok := matches[protocol] + if !ok || len(match.ips) != totalIPs || len(match.ips) < 2 { // don't squash if : // 1. Rules not cover all peers in the network // 2. Rules cover only one peer in the network. @@ -465,6 +472,7 @@ func (d *DefaultManager) squashAcceptRules( Direction: direction, Action: mgmProto.RuleAction_ACCEPT, Protocol: protocol, + PolicyID: match.policyID, }) squashedProtocols[protocol] = struct{}{} @@ -493,9 +501,9 @@ func (d *DefaultManager) squashAcceptRules( // if we also have other not squashed rules. for i, r := range networkMap.FirewallRules { if _, ok := squashedProtocols[r.Protocol]; ok { - if m, ok := in[r.Protocol]; ok && m[r.PeerIP] == i { + if m, ok := in[r.Protocol]; ok && m.ips[r.PeerIP] == i { continue - } else if m, ok := out[r.Protocol]; ok && m[r.PeerIP] == i { + } else if m, ok := out[r.Protocol]; ok && m.ips[r.PeerIP] == i { continue } } diff --git a/client/internal/acl/manager_test.go b/client/internal/acl/manager_test.go index e054d69e9..ca79111ef 100644 --- a/client/internal/acl/manager_test.go +++ b/client/internal/acl/manager_test.go @@ -1,6 +1,7 @@ package acl import ( + "context" "net" "testing" @@ -10,9 +11,12 @@ import ( "github.com/netbirdio/netbird/client/firewall/manager" "github.com/netbirdio/netbird/client/iface/wgaddr" "github.com/netbirdio/netbird/client/internal/acl/mocks" + "github.com/netbirdio/netbird/client/internal/netflow" mgmProto "github.com/netbirdio/netbird/management/proto" ) +var flowLogger = netflow.NewManager(context.Background(), nil, []byte{}, nil).GetLogger() + func TestDefaultManager(t *testing.T) { networkMap := &mgmProto.NetworkMap{ FirewallRules: []*mgmProto.FirewallRule{ @@ -52,7 +56,7 @@ func TestDefaultManager(t *testing.T) { ifaceMock.EXPECT().GetWGDevice().Return(nil).AnyTimes() // we receive one rule from the management so for testing purposes ignore it - fw, err := firewall.NewFirewall(ifaceMock, nil, false) + fw, err := firewall.NewFirewall(ifaceMock, nil, flowLogger, false) if err != nil { t.Errorf("create firewall: %v", err) return @@ -346,7 +350,7 @@ func TestDefaultManagerEnableSSHRules(t *testing.T) { ifaceMock.EXPECT().GetWGDevice().Return(nil).AnyTimes() // we receive one rule from the management so for testing purposes ignore it - fw, err := firewall.NewFirewall(ifaceMock, nil, false) + fw, err := firewall.NewFirewall(ifaceMock, nil, flowLogger, false) if err != nil { t.Errorf("create firewall: %v", err) return diff --git a/client/internal/dns/server_test.go b/client/internal/dns/server_test.go index a14942c34..c7eeb7870 100644 --- a/client/internal/dns/server_test.go +++ b/client/internal/dns/server_test.go @@ -23,6 +23,7 @@ import ( "github.com/netbirdio/netbird/client/iface/device" pfmock "github.com/netbirdio/netbird/client/iface/mocks" "github.com/netbirdio/netbird/client/iface/wgaddr" + "github.com/netbirdio/netbird/client/internal/netflow" "github.com/netbirdio/netbird/client/internal/peer" "github.com/netbirdio/netbird/client/internal/statemanager" "github.com/netbirdio/netbird/client/internal/stdnet" @@ -30,6 +31,8 @@ import ( "github.com/netbirdio/netbird/formatter" ) +var flowLogger = netflow.NewManager(context.Background(), nil, []byte{}, nil).GetLogger() + type mocWGIface struct { filter device.PacketFilter } @@ -456,7 +459,7 @@ func TestDNSFakeResolverHandleUpdates(t *testing.T) { } packetfilter := pfmock.NewMockPacketFilter(ctrl) - packetfilter.EXPECT().DropOutgoing(gomock.Any()).AnyTimes() + packetfilter.EXPECT().DropOutgoing(gomock.Any(), gomock.Any()).AnyTimes() packetfilter.EXPECT().AddUDPPacketHook(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()) packetfilter.EXPECT().RemovePacketHook(gomock.Any()) packetfilter.EXPECT().SetNetwork(ipNet) @@ -917,7 +920,7 @@ func createWgInterfaceWithBind(t *testing.T) (*iface.WGIface, error) { return nil, err } - pf, err := uspfilter.Create(wgIface, false) + pf, err := uspfilter.Create(wgIface, false, flowLogger) if err != nil { t.Fatalf("failed to create uspfilter: %v", err) return nil, err diff --git a/client/internal/dns/service_memory.go b/client/internal/dns/service_memory.go index 250f3ab2e..34c563757 100644 --- a/client/internal/dns/service_memory.go +++ b/client/internal/dns/service_memory.go @@ -2,7 +2,7 @@ package dns import ( "fmt" - "net" + "net/netip" "sync" "github.com/google/gopacket" @@ -117,5 +117,10 @@ func (s *ServiceViaMemory) filterDNSTraffic() (string, error) { return true } - return filter.AddUDPPacketHook(false, net.ParseIP(s.runtimeIP), uint16(s.runtimePort), hook), nil + ip, err := netip.ParseAddr(s.runtimeIP) + if err != nil { + return "", fmt.Errorf("parse runtime ip: %w", err) + } + + return filter.AddUDPPacketHook(false, ip, uint16(s.runtimePort), hook), nil } diff --git a/client/internal/dnsfwd/manager.go b/client/internal/dnsfwd/manager.go index 5d3036dde..8dae06aec 100644 --- a/client/internal/dnsfwd/manager.go +++ b/client/internal/dnsfwd/manager.go @@ -88,7 +88,7 @@ func (h *Manager) allowDNSFirewall() error { return nil } - dnsRules, err := h.firewall.AddPeerFiltering(net.IP{0, 0, 0, 0}, firewall.ProtocolUDP, nil, dport, firewall.ActionAccept, "", "") + dnsRules, err := h.firewall.AddPeerFiltering(nil, net.IP{0, 0, 0, 0}, firewall.ProtocolUDP, nil, dport, firewall.ActionAccept, "") if err != nil { log.Errorf("failed to add allow DNS router rules, err: %v", err) return err diff --git a/client/internal/engine.go b/client/internal/engine.go index 2693976dd..babea2131 100644 --- a/client/internal/engine.go +++ b/client/internal/engine.go @@ -34,6 +34,8 @@ import ( "github.com/netbirdio/netbird/client/internal/dns" "github.com/netbirdio/netbird/client/internal/dnsfwd" "github.com/netbirdio/netbird/client/internal/ingressgw" + "github.com/netbirdio/netbird/client/internal/netflow" + nftypes "github.com/netbirdio/netbird/client/internal/netflow/types" "github.com/netbirdio/netbird/client/internal/networkmonitor" "github.com/netbirdio/netbird/client/internal/peer" "github.com/netbirdio/netbird/client/internal/peer/guard" @@ -189,6 +191,7 @@ type Engine struct { persistNetworkMap bool latestNetworkMap *mgmProto.NetworkMap connSemaphore *semaphoregroup.SemaphoreGroup + flowManager nftypes.FlowManager } // Peer is an instance of the Connection Peer @@ -308,6 +311,12 @@ func (e *Engine) Stop() error { time.Sleep(500 * time.Millisecond) e.close() + + // stop flow manager after wg interface is gone + if e.flowManager != nil { + e.flowManager.Close() + } + log.Infof("stopped Netbird Engine") ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) @@ -342,6 +351,10 @@ func (e *Engine) Start() error { } e.wgInterface = wgIface + // start flow manager right after interface creation + publicKey := e.config.WgPrivateKey.PublicKey() + e.flowManager = netflow.NewManager(e.ctx, e.wgInterface, publicKey[:], e.statusRecorder) + if e.config.RosenpassEnabled { log.Infof("rosenpass is enabled") if e.config.RosenpassPermissive { @@ -448,7 +461,7 @@ func (e *Engine) createFirewall() error { } var err error - e.firewall, err = firewall.NewFirewall(e.wgInterface, e.stateManager, e.config.DisableServerRoutes) + e.firewall, err = firewall.NewFirewall(e.wgInterface, e.stateManager, e.flowManager.GetLogger(), e.config.DisableServerRoutes) if err != nil || e.firewall == nil { log.Errorf("failed creating firewall manager: %s", err) return nil @@ -482,13 +495,13 @@ func (e *Engine) initFirewall() error { // this rule is static and will be torn down on engine down by the firewall manager if _, err := e.firewall.AddPeerFiltering( + nil, net.IP{0, 0, 0, 0}, firewallManager.ProtocolUDP, nil, &port, firewallManager.ActionAccept, "", - "", ); err != nil { log.Errorf("failed to allow rosenpass interface traffic: %v", err) return nil @@ -512,6 +525,7 @@ func (e *Engine) blockLanAccess() { v4 := netip.PrefixFrom(netip.IPv4Unspecified(), 0) for _, network := range toBlock { if _, err := e.firewall.AddRouteFiltering( + nil, []netip.Prefix{v4}, network, firewallManager.ProtocolALL, @@ -642,25 +656,14 @@ func (e *Engine) handleSync(update *mgmProto.SyncResponse) error { stunTurn = append(stunTurn, e.TURNs...) e.stunTurn.Store(stunTurn) - relayMsg := wCfg.GetRelay() - if relayMsg != nil { - // when we receive token we expect valid address list too - c := &auth.Token{ - Payload: relayMsg.GetTokenPayload(), - Signature: relayMsg.GetTokenSignature(), - } - if err := e.relayManager.UpdateToken(c); err != nil { - log.Errorf("failed to update relay token: %v", err) - return fmt.Errorf("update relay token: %w", err) - } + err = e.handleRelayUpdate(wCfg.GetRelay()) + if err != nil { + return err + } - e.relayManager.UpdateServerURLs(relayMsg.Urls) - - // Just in case the agent started with an MGM server where the relay was disabled but was later enabled. - // We can ignore all errors because the guard will manage the reconnection retries. - _ = e.relayManager.Serve() - } else { - e.relayManager.UpdateServerURLs(nil) + err = e.handleFlowUpdate(wCfg.GetFlow()) + if err != nil { + return fmt.Errorf("handle the flow configuration: %w", err) } // todo update signal @@ -691,6 +694,57 @@ func (e *Engine) handleSync(update *mgmProto.SyncResponse) error { return nil } +func (e *Engine) handleRelayUpdate(update *mgmProto.RelayConfig) error { + if update != nil { + // when we receive token we expect valid address list too + c := &auth.Token{ + Payload: update.GetTokenPayload(), + Signature: update.GetTokenSignature(), + } + if err := e.relayManager.UpdateToken(c); err != nil { + return fmt.Errorf("update relay token: %w", err) + } + + e.relayManager.UpdateServerURLs(update.Urls) + + // Just in case the agent started with an MGM server where the relay was disabled but was later enabled. + // We can ignore all errors because the guard will manage the reconnection retries. + _ = e.relayManager.Serve() + } else { + e.relayManager.UpdateServerURLs(nil) + } + + return nil +} + +func (e *Engine) handleFlowUpdate(config *mgmProto.FlowConfig) error { + if config == nil { + return nil + } + + flowConfig, err := toFlowLoggerConfig(config) + if err != nil { + return err + } + return e.flowManager.Update(flowConfig) +} + +func toFlowLoggerConfig(config *mgmProto.FlowConfig) (*nftypes.FlowConfig, error) { + if config.GetInterval() == nil { + return nil, errors.New("flow interval is nil") + } + return &nftypes.FlowConfig{ + Enabled: config.GetEnabled(), + Counters: config.GetCounters(), + URL: config.GetUrl(), + TokenPayload: config.GetTokenPayload(), + TokenSignature: config.GetTokenSignature(), + Interval: config.GetInterval().AsDuration(), + DNSCollection: config.GetDnsCollection(), + ExitNodeCollection: config.GetExitNodeCollection(), + }, nil +} + // updateChecksIfNew updates checks if there are changes and sync new meta with management func (e *Engine) updateChecksIfNew(checks []*mgmProto.Checks) error { // if checks are equal, we skip the update diff --git a/client/internal/engine_test.go b/client/internal/engine_test.go index 164880e2f..56fef43e1 100644 --- a/client/internal/engine_test.go +++ b/client/internal/engine_test.go @@ -12,6 +12,7 @@ import ( "testing" "time" + "github.com/golang/mock/gomock" "github.com/google/uuid" "github.com/pion/transport/v3/stdnet" log "github.com/sirupsen/logrus" @@ -27,6 +28,8 @@ import ( "github.com/netbirdio/management-integrations/integrations" + "github.com/netbirdio/netbird/management/server/types" + "github.com/netbirdio/netbird/client/iface" "github.com/netbirdio/netbird/client/iface/bind" "github.com/netbirdio/netbird/client/iface/configurer" @@ -1435,13 +1438,21 @@ func startManagement(t *testing.T, dataDir, testFile string) (*grpc.Server, stri metrics, err := telemetry.NewDefaultAppMetrics(context.Background()) require.NoError(t, err) - accountManager, err := server.BuildManager(context.Background(), store, peersUpdateManager, nil, "", "netbird.selfhosted", eventStore, nil, false, ia, metrics, port_forwarding.NewControllerMock()) + ctrl := gomock.NewController(t) + t.Cleanup(ctrl.Finish) + settingsMockManager := settings.NewMockManager(ctrl) + settingsMockManager.EXPECT(). + GetSettings(gomock.Any(), gomock.Any(), gomock.Any()). + Return(&types.Settings{}, nil). + AnyTimes() + + accountManager, err := server.BuildManager(context.Background(), store, peersUpdateManager, nil, "", "netbird.selfhosted", eventStore, nil, false, ia, metrics, port_forwarding.NewControllerMock(), settingsMockManager) if err != nil { return nil, "", err } - secretsManager := server.NewTimeBasedAuthSecretsManager(peersUpdateManager, config.TURNConfig, config.Relay) - mgmtServer, err := server.NewServer(context.Background(), config, accountManager, settings.NewManager(store), peersUpdateManager, secretsManager, nil, nil, nil) + secretsManager := server.NewTimeBasedAuthSecretsManager(peersUpdateManager, config.TURNConfig, config.Relay, settingsMockManager) + mgmtServer, err := server.NewServer(context.Background(), config, accountManager, settingsMockManager, peersUpdateManager, secretsManager, nil, nil, nil) if err != nil { return nil, "", err } diff --git a/client/internal/netflow/conntrack/conntrack.go b/client/internal/netflow/conntrack/conntrack.go new file mode 100644 index 000000000..c0614406d --- /dev/null +++ b/client/internal/netflow/conntrack/conntrack.go @@ -0,0 +1,306 @@ +//go:build linux && !android + +package conntrack + +import ( + "encoding/binary" + "fmt" + "net/netip" + "sync" + + "github.com/google/uuid" + log "github.com/sirupsen/logrus" + nfct "github.com/ti-mo/conntrack" + "github.com/ti-mo/netfilter" + + nftypes "github.com/netbirdio/netbird/client/internal/netflow/types" +) + +const defaultChannelSize = 100 + +// ConnTrack manages kernel-based conntrack events +type ConnTrack struct { + flowLogger nftypes.FlowLogger + iface nftypes.IFaceMapper + + conn *nfct.Conn + mux sync.Mutex + + instanceID uuid.UUID + started bool + done chan struct{} + sysctlModified bool +} + +// New creates a new connection tracker that interfaces with the kernel's conntrack system +func New(flowLogger nftypes.FlowLogger, iface nftypes.IFaceMapper) *ConnTrack { + return &ConnTrack{ + flowLogger: flowLogger, + iface: iface, + instanceID: uuid.New(), + started: false, + done: make(chan struct{}, 1), + } +} + +// Start begins tracking connections by listening for conntrack events. This method is idempotent. +func (c *ConnTrack) Start(enableCounters bool) error { + c.mux.Lock() + defer c.mux.Unlock() + + if c.started { + return nil + } + + log.Info("Starting conntrack event listening") + + if enableCounters { + c.EnableAccounting() + } + + conn, err := nfct.Dial(nil) + if err != nil { + return fmt.Errorf("dial conntrack: %w", err) + } + c.conn = conn + + events := make(chan nfct.Event, defaultChannelSize) + errChan, err := conn.Listen(events, 1, []netfilter.NetlinkGroup{ + netfilter.GroupCTNew, + netfilter.GroupCTDestroy, + }) + + if err != nil { + if err := c.conn.Close(); err != nil { + log.Errorf("Error closing conntrack connection: %v", err) + } + c.conn = nil + return fmt.Errorf("start conntrack listener: %w", err) + } + + c.started = true + + go c.receiverRoutine(events, errChan) + + return nil +} + +func (c *ConnTrack) receiverRoutine(events chan nfct.Event, errChan chan error) { + for { + select { + case event := <-events: + c.handleEvent(event) + case err := <-errChan: + log.Errorf("Error from conntrack event listener: %v", err) + if err := c.conn.Close(); err != nil { + log.Errorf("Error closing conntrack connection: %v", err) + } + return + case <-c.done: + return + } + } +} + +// Stop stops the connection tracking. This method is idempotent. +func (c *ConnTrack) Stop() { + c.mux.Lock() + defer c.mux.Unlock() + + if !c.started { + return + } + + log.Info("Stopping conntrack event listening") + + select { + case c.done <- struct{}{}: + default: + } + + if c.conn != nil { + if err := c.conn.Close(); err != nil { + log.Errorf("Error closing conntrack connection: %v", err) + } + c.conn = nil + } + + c.started = false + + c.RestoreAccounting() +} + +// Close stops listening for events and cleans up resources +func (c *ConnTrack) Close() error { + c.mux.Lock() + defer c.mux.Unlock() + + if c.started { + select { + case c.done <- struct{}{}: + default: + } + } + + if c.conn != nil { + err := c.conn.Close() + c.conn = nil + c.started = false + + c.RestoreAccounting() + + if err != nil { + return fmt.Errorf("close conntrack: %w", err) + } + } + + return nil +} + +// handleEvent processes incoming conntrack events +func (c *ConnTrack) handleEvent(event nfct.Event) { + if event.Flow == nil { + return + } + + if event.Type != nfct.EventNew && event.Type != nfct.EventDestroy { + return + } + + flow := *event.Flow + + proto := nftypes.Protocol(flow.TupleOrig.Proto.Protocol) + if proto == nftypes.ProtocolUnknown { + return + } + srcIP := flow.TupleOrig.IP.SourceAddress + dstIP := flow.TupleOrig.IP.DestinationAddress + + if !c.relevantFlow(srcIP, dstIP) { + return + } + + var srcPort, dstPort uint16 + var icmpType, icmpCode uint8 + + switch proto { + case nftypes.TCP, nftypes.UDP, nftypes.SCTP: + srcPort = flow.TupleOrig.Proto.SourcePort + dstPort = flow.TupleOrig.Proto.DestinationPort + case nftypes.ICMP: + icmpType = flow.TupleOrig.Proto.ICMPType + icmpCode = flow.TupleOrig.Proto.ICMPCode + } + + flowID := c.getFlowID(flow.ID) + direction := c.inferDirection(srcIP, dstIP) + + eventType := nftypes.TypeStart + eventStr := "New" + + if event.Type == nfct.EventDestroy { + eventType = nftypes.TypeEnd + eventStr = "Ended" + } + + log.Tracef("%s %s %s connection: %s:%d -> %s:%d", eventStr, direction, proto, srcIP, srcPort, dstIP, dstPort) + + c.flowLogger.StoreEvent(nftypes.EventFields{ + FlowID: flowID, + Type: eventType, + Direction: direction, + Protocol: proto, + SourceIP: srcIP, + DestIP: dstIP, + SourcePort: srcPort, + DestPort: dstPort, + ICMPType: icmpType, + ICMPCode: icmpCode, + RxPackets: c.mapRxPackets(flow, direction), + TxPackets: c.mapTxPackets(flow, direction), + RxBytes: c.mapRxBytes(flow, direction), + TxBytes: c.mapTxBytes(flow, direction), + }) +} + +// relevantFlow checks if the flow is related to the specified interface +func (c *ConnTrack) relevantFlow(srcIP, dstIP netip.Addr) bool { + // TODO: filter traffic by interface + + wgnet := c.iface.Address().Network + if !wgnet.Contains(srcIP.AsSlice()) && !wgnet.Contains(dstIP.AsSlice()) { + return false + } + + return true +} + +// mapRxPackets maps packet counts to RX based on flow direction +func (c *ConnTrack) mapRxPackets(flow nfct.Flow, direction nftypes.Direction) uint64 { + // For Ingress: CountersOrig is from external to us (RX) + // For Egress: CountersReply is from external to us (RX) + if direction == nftypes.Ingress { + return flow.CountersOrig.Packets + } + return flow.CountersReply.Packets +} + +// mapTxPackets maps packet counts to TX based on flow direction +func (c *ConnTrack) mapTxPackets(flow nfct.Flow, direction nftypes.Direction) uint64 { + // For Ingress: CountersReply is from us to external (TX) + // For Egress: CountersOrig is from us to external (TX) + if direction == nftypes.Ingress { + return flow.CountersReply.Packets + } + return flow.CountersOrig.Packets +} + +// mapRxBytes maps byte counts to RX based on flow direction +func (c *ConnTrack) mapRxBytes(flow nfct.Flow, direction nftypes.Direction) uint64 { + // For Ingress: CountersOrig is from external to us (RX) + // For Egress: CountersReply is from external to us (RX) + if direction == nftypes.Ingress { + return flow.CountersOrig.Bytes + } + return flow.CountersReply.Bytes +} + +// mapTxBytes maps byte counts to TX based on flow direction +func (c *ConnTrack) mapTxBytes(flow nfct.Flow, direction nftypes.Direction) uint64 { + // For Ingress: CountersReply is from us to external (TX) + // For Egress: CountersOrig is from us to external (TX) + if direction == nftypes.Ingress { + return flow.CountersReply.Bytes + } + return flow.CountersOrig.Bytes +} + +// getFlowID creates a unique UUID based on the conntrack ID and instance ID +func (c *ConnTrack) getFlowID(conntrackID uint32) uuid.UUID { + var buf [4]byte + binary.BigEndian.PutUint32(buf[:], conntrackID) + return uuid.NewSHA1(c.instanceID, buf[:]) +} + +func (c *ConnTrack) inferDirection(srcIP, dstIP netip.Addr) nftypes.Direction { + wgaddr := c.iface.Address().IP + wgnetwork := c.iface.Address().Network + src, dst := srcIP.AsSlice(), dstIP.AsSlice() + + switch { + case wgaddr.Equal(src): + return nftypes.Egress + case wgaddr.Equal(dst): + return nftypes.Ingress + case wgnetwork.Contains(src): + // netbird network -> resource network + return nftypes.Ingress + case wgnetwork.Contains(dst): + // resource network -> netbird network + return nftypes.Egress + + // TODO: handle site2site traffic + } + + return nftypes.DirectionUnknown +} diff --git a/client/internal/netflow/conntrack/conntrack_nonlinux.go b/client/internal/netflow/conntrack/conntrack_nonlinux.go new file mode 100644 index 000000000..9044fd76c --- /dev/null +++ b/client/internal/netflow/conntrack/conntrack_nonlinux.go @@ -0,0 +1,9 @@ +//go:build !linux || android + +package conntrack + +import nftypes "github.com/netbirdio/netbird/client/internal/netflow/types" + +func New(flowLogger nftypes.FlowLogger, iface nftypes.IFaceMapper) nftypes.ConnTracker { + return nil +} diff --git a/client/internal/netflow/conntrack/sysctl.go b/client/internal/netflow/conntrack/sysctl.go new file mode 100644 index 000000000..c05a49691 --- /dev/null +++ b/client/internal/netflow/conntrack/sysctl.go @@ -0,0 +1,73 @@ +//go:build linux && !android + +package conntrack + +import ( + "fmt" + "os" + "strconv" + "strings" + + log "github.com/sirupsen/logrus" +) + +const ( + // conntrackAcctPath is the sysctl path for conntrack accounting + conntrackAcctPath = "net.netfilter.nf_conntrack_acct" +) + +// EnableAccounting ensures that connection tracking accounting is enabled in the kernel. +func (c *ConnTrack) EnableAccounting() { + // haven't restored yet + if c.sysctlModified { + return + } + + modified, err := setSysctl(conntrackAcctPath, 1) + if err != nil { + log.Warnf("Failed to enable conntrack accounting: %v", err) + return + } + c.sysctlModified = modified +} + +// RestoreAccounting restores the connection tracking accounting setting to its original value. +func (c *ConnTrack) RestoreAccounting() { + if !c.sysctlModified { + return + } + + if _, err := setSysctl(conntrackAcctPath, 0); err != nil { + log.Warnf("Failed to restore conntrack accounting: %v", err) + return + } + + c.sysctlModified = false +} + +// setSysctl sets a sysctl configuration and returns whether it was modified. +func setSysctl(key string, desiredValue int) (bool, error) { + path := fmt.Sprintf("/proc/sys/%s", strings.ReplaceAll(key, ".", "/")) + + currentValue, err := os.ReadFile(path) + if err != nil { + return false, fmt.Errorf("read sysctl %s: %w", key, err) + } + + currentV, err := strconv.Atoi(strings.TrimSpace(string(currentValue))) + if err != nil && len(currentValue) > 0 { + return false, fmt.Errorf("convert current value to int: %w", err) + } + + if currentV == desiredValue { + return false, nil + } + + // nolint:gosec + if err := os.WriteFile(path, []byte(strconv.Itoa(desiredValue)), 0644); err != nil { + return false, fmt.Errorf("write sysctl %s: %w", key, err) + } + + log.Debugf("Set sysctl %s from %d to %d", key, currentV, desiredValue) + return true, nil +} diff --git a/client/internal/netflow/logger/logger.go b/client/internal/netflow/logger/logger.go new file mode 100644 index 000000000..b2cf070fa --- /dev/null +++ b/client/internal/netflow/logger/logger.go @@ -0,0 +1,162 @@ +package logger + +import ( + "context" + "net" + "sync" + "sync/atomic" + "time" + + "github.com/google/uuid" + log "github.com/sirupsen/logrus" + + "github.com/netbirdio/netbird/client/internal/dnsfwd" + "github.com/netbirdio/netbird/client/internal/netflow/store" + "github.com/netbirdio/netbird/client/internal/netflow/types" + "github.com/netbirdio/netbird/client/internal/peer" +) + +type rcvChan chan *types.EventFields +type Logger struct { + mux sync.Mutex + ctx context.Context + cancel context.CancelFunc + enabled atomic.Bool + rcvChan atomic.Pointer[rcvChan] + cancelReceiver context.CancelFunc + statusRecorder *peer.Status + wgIfaceIPNet net.IPNet + dnsCollection atomic.Bool + exitNodeCollection atomic.Bool + Store types.Store +} + +func New(ctx context.Context, statusRecorder *peer.Status, wgIfaceIPNet net.IPNet) *Logger { + + ctx, cancel := context.WithCancel(ctx) + return &Logger{ + ctx: ctx, + cancel: cancel, + statusRecorder: statusRecorder, + wgIfaceIPNet: wgIfaceIPNet, + Store: store.NewMemoryStore(), + } +} + +func (l *Logger) StoreEvent(flowEvent types.EventFields) { + if !l.enabled.Load() { + return + } + + c := l.rcvChan.Load() + if c == nil { + return + } + + select { + case *c <- &flowEvent: + default: + // todo: we should collect or log on this + } +} + +func (l *Logger) Enable() { + go l.startReceiver() +} + +func (l *Logger) startReceiver() { + if l.enabled.Load() { + return + } + + l.mux.Lock() + ctx, cancel := context.WithCancel(l.ctx) + l.cancelReceiver = cancel + l.mux.Unlock() + + c := make(rcvChan, 100) + l.rcvChan.Store(&c) + l.enabled.Store(true) + + for { + select { + case <-ctx.Done(): + log.Info("flow Memory store receiver stopped") + return + case eventFields := <-c: + id := uuid.New() + event := types.Event{ + ID: id, + EventFields: *eventFields, + Timestamp: time.Now(), + } + + var isExitNode bool + if event.Direction == types.Ingress { + if !l.wgIfaceIPNet.Contains(net.IP(event.SourceIP.AsSlice())) { + event.SourceResourceID, isExitNode = l.statusRecorder.CheckRoutes(event.SourceIP) + } + } else if event.Direction == types.Egress { + if !l.wgIfaceIPNet.Contains(net.IP(event.DestIP.AsSlice())) { + event.DestResourceID, isExitNode = l.statusRecorder.CheckRoutes(event.DestIP) + } + } + + if l.shouldStore(eventFields, isExitNode) { + l.Store.StoreEvent(&event) + } + } + } +} + +func (l *Logger) Disable() { + l.stop() + l.Store.Close() +} + +func (l *Logger) stop() { + if !l.enabled.Load() { + return + } + + l.enabled.Store(false) + l.mux.Lock() + if l.cancelReceiver != nil { + l.cancelReceiver() + l.cancelReceiver = nil + } + l.rcvChan.Store(nil) + l.mux.Unlock() +} + +func (l *Logger) GetEvents() []*types.Event { + return l.Store.GetEvents() +} + +func (l *Logger) DeleteEvents(ids []uuid.UUID) { + l.Store.DeleteEvents(ids) +} + +func (l *Logger) UpdateConfig(dnsCollection, exitNodeCollection bool) { + l.dnsCollection.Store(dnsCollection) + l.exitNodeCollection.Store(exitNodeCollection) +} + +func (l *Logger) Close() { + l.stop() + l.cancel() +} + +func (l *Logger) shouldStore(event *types.EventFields, isExitNode bool) bool { + // check dns collection + if !l.dnsCollection.Load() && event.Protocol == types.UDP && (event.DestPort == 53 || event.DestPort == dnsfwd.ListenPort) { + return false + } + + // check exit node collection + if !l.exitNodeCollection.Load() && isExitNode { + return false + } + + return true +} diff --git a/client/internal/netflow/logger/logger_test.go b/client/internal/netflow/logger/logger_test.go new file mode 100644 index 000000000..3ce9d8fd8 --- /dev/null +++ b/client/internal/netflow/logger/logger_test.go @@ -0,0 +1,68 @@ +package logger_test + +import ( + "context" + "net" + "testing" + "time" + + "github.com/google/uuid" + + "github.com/netbirdio/netbird/client/internal/netflow/logger" + "github.com/netbirdio/netbird/client/internal/netflow/types" +) + +func TestStore(t *testing.T) { + logger := logger.New(context.Background(), nil, net.IPNet{}) + logger.Enable() + + event := types.EventFields{ + FlowID: uuid.New(), + Type: types.TypeStart, + Direction: types.Ingress, + Protocol: 6, + } + + wait := func() { time.Sleep(time.Millisecond) } + wait() + logger.StoreEvent(event) + wait() + + allEvents := logger.GetEvents() + matched := false + for _, e := range allEvents { + if e.EventFields.FlowID == event.FlowID { + matched = true + } + } + if !matched { + t.Errorf("didn't match any event") + } + + // test disable + logger.Disable() + wait() + logger.StoreEvent(event) + wait() + allEvents = logger.GetEvents() + if len(allEvents) != 0 { + t.Errorf("expected 0 events, got %d", len(allEvents)) + } + + // test re-enable + logger.Enable() + wait() + logger.StoreEvent(event) + wait() + + allEvents = logger.GetEvents() + matched = false + for _, e := range allEvents { + if e.EventFields.FlowID == event.FlowID { + matched = true + } + } + if !matched { + t.Errorf("didn't match any event") + } +} diff --git a/client/internal/netflow/manager.go b/client/internal/netflow/manager.go new file mode 100644 index 000000000..1e4f44f06 --- /dev/null +++ b/client/internal/netflow/manager.go @@ -0,0 +1,262 @@ +package netflow + +import ( + "context" + "errors" + "fmt" + "net" + "runtime" + "sync" + "time" + + "github.com/google/uuid" + log "github.com/sirupsen/logrus" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/netbirdio/netbird/client/internal/netflow/conntrack" + "github.com/netbirdio/netbird/client/internal/netflow/logger" + nftypes "github.com/netbirdio/netbird/client/internal/netflow/types" + "github.com/netbirdio/netbird/client/internal/peer" + "github.com/netbirdio/netbird/flow/client" + "github.com/netbirdio/netbird/flow/proto" +) + +// Manager handles netflow tracking and logging +type Manager struct { + mux sync.Mutex + logger nftypes.FlowLogger + flowConfig *nftypes.FlowConfig + conntrack nftypes.ConnTracker + ctx context.Context + receiverClient *client.GRPCClient + publicKey []byte +} + +// NewManager creates a new netflow manager +func NewManager(ctx context.Context, iface nftypes.IFaceMapper, publicKey []byte, statusRecorder *peer.Status) *Manager { + var ipNet net.IPNet + if iface != nil { + ipNet = *iface.Address().Network + } + flowLogger := logger.New(ctx, statusRecorder, ipNet) + + var ct nftypes.ConnTracker + if runtime.GOOS == "linux" && iface != nil && !iface.IsUserspaceBind() { + ct = conntrack.New(flowLogger, iface) + } + + return &Manager{ + logger: flowLogger, + conntrack: ct, + ctx: ctx, + publicKey: publicKey, + } +} + +// Update applies new flow configuration settings +// needsNewClient checks if a new client needs to be created +func (m *Manager) needsNewClient(previous *nftypes.FlowConfig) bool { + current := m.flowConfig + return previous == nil || + !previous.Enabled || + previous.TokenPayload != current.TokenPayload || + previous.TokenSignature != current.TokenSignature || + previous.URL != current.URL +} + +// enableFlow starts components for flow tracking +func (m *Manager) enableFlow(previous *nftypes.FlowConfig) error { + // first make sender ready so events don't pile up + if m.needsNewClient(previous) { + if m.receiverClient != nil { + if err := m.receiverClient.Close(); err != nil { + log.Warnf("error closing previous flow client: %v", err) + } + } + + flowClient, err := client.NewClient(m.flowConfig.URL, m.flowConfig.TokenPayload, m.flowConfig.TokenSignature, m.flowConfig.Interval) + if err != nil { + return fmt.Errorf("create client: %w", err) + } + log.Infof("flow client configured to connect to %s", m.flowConfig.URL) + + m.receiverClient = flowClient + go m.receiveACKs(flowClient) + go m.startSender() + } + + m.logger.Enable() + + if m.conntrack != nil { + if err := m.conntrack.Start(m.flowConfig.Counters); err != nil { + return fmt.Errorf("start conntrack: %w", err) + } + } + + return nil +} + +// disableFlow stops components for flow tracking +func (m *Manager) disableFlow() error { + if m.conntrack != nil { + m.conntrack.Stop() + } + + m.logger.Disable() + + if m.receiverClient != nil { + return m.receiverClient.Close() + } + return nil +} + +// Update applies new flow configuration settings +func (m *Manager) Update(update *nftypes.FlowConfig) error { + if update == nil { + log.Debug("no update provided; skipping update") + return nil + } + + log.Tracef("updating flow configuration with new settings: %+v", update) + + m.mux.Lock() + defer m.mux.Unlock() + + previous := m.flowConfig + m.flowConfig = update + + // Preserve TokenPayload and TokenSignature if they were set previously + if previous != nil && previous.TokenPayload != "" && m.flowConfig != nil && m.flowConfig.TokenPayload == "" { + m.flowConfig.TokenPayload = previous.TokenPayload + m.flowConfig.TokenSignature = previous.TokenSignature + } + + m.logger.UpdateConfig(update.DNSCollection, update.ExitNodeCollection) + + if update.Enabled { + log.Infof("netflow manager enabled; starting netflow manager") + return m.enableFlow(previous) + } + + log.Infof("netflow manager disabled; stopping netflow manager") + err := m.disableFlow() + if err != nil { + log.Errorf("failed to disable netflow manager: %v", err) + } + return err +} + +// Close cleans up all resources +func (m *Manager) Close() { + m.mux.Lock() + defer m.mux.Unlock() + + if m.conntrack != nil { + m.conntrack.Close() + } + + if m.receiverClient != nil { + if err := m.receiverClient.Close(); err != nil { + log.Warnf("failed to close receiver client: %v", err) + } + } + + m.logger.Close() +} + +// GetLogger returns the flow logger +func (m *Manager) GetLogger() nftypes.FlowLogger { + return m.logger +} + +func (m *Manager) startSender() { + ticker := time.NewTicker(m.flowConfig.Interval) + defer ticker.Stop() + + for { + select { + case <-m.ctx.Done(): + return + case <-ticker.C: + events := m.logger.GetEvents() + for _, event := range events { + if err := m.send(event); err != nil { + log.Errorf("failed to send flow event to server: %v", err) + continue + } + log.Tracef("sent flow event: %s", event.ID) + } + } + } +} + +func (m *Manager) receiveACKs(client *client.GRPCClient) { + err := client.Receive(m.ctx, m.flowConfig.Interval, func(ack *proto.FlowEventAck) error { + id, err := uuid.FromBytes(ack.EventId) + if err != nil { + log.Warnf("failed to convert ack event id to uuid: %v", err) + return nil + } + log.Tracef("received flow event ack: %s", id) + m.logger.DeleteEvents([]uuid.UUID{uuid.UUID(ack.EventId)}) + return nil + }) + + if err != nil && !errors.Is(err, context.Canceled) { + log.Errorf("failed to receive flow event ack: %v", err) + } +} + +func (m *Manager) send(event *nftypes.Event) error { + m.mux.Lock() + client := m.receiverClient + m.mux.Unlock() + + if client == nil { + return nil + } + + return client.Send(toProtoEvent(m.publicKey, event)) +} + +func toProtoEvent(publicKey []byte, event *nftypes.Event) *proto.FlowEvent { + protoEvent := &proto.FlowEvent{ + EventId: event.ID[:], + Timestamp: timestamppb.New(event.Timestamp), + PublicKey: publicKey, + FlowFields: &proto.FlowFields{ + FlowId: event.FlowID[:], + RuleId: event.RuleID, + Type: proto.Type(event.Type), + Direction: proto.Direction(event.Direction), + Protocol: uint32(event.Protocol), + SourceIp: event.SourceIP.AsSlice(), + DestIp: event.DestIP.AsSlice(), + RxPackets: event.RxPackets, + TxPackets: event.TxPackets, + RxBytes: event.RxBytes, + TxBytes: event.TxBytes, + SourceResourceId: event.SourceResourceID, + DestResourceId: event.DestResourceID, + }, + } + + if event.Protocol == nftypes.ICMP { + protoEvent.FlowFields.ConnectionInfo = &proto.FlowFields_IcmpInfo{ + IcmpInfo: &proto.ICMPInfo{ + IcmpType: uint32(event.ICMPType), + IcmpCode: uint32(event.ICMPCode), + }, + } + return protoEvent + } + + protoEvent.FlowFields.ConnectionInfo = &proto.FlowFields_PortInfo{ + PortInfo: &proto.PortInfo{ + SourcePort: uint32(event.SourcePort), + DestPort: uint32(event.DestPort), + }, + } + + return protoEvent +} diff --git a/client/internal/netflow/store/memory.go b/client/internal/netflow/store/memory.go new file mode 100644 index 000000000..b695a0a12 --- /dev/null +++ b/client/internal/netflow/store/memory.go @@ -0,0 +1,52 @@ +package store + +import ( + "sync" + + "golang.org/x/exp/maps" + + "github.com/google/uuid" + + "github.com/netbirdio/netbird/client/internal/netflow/types" +) + +func NewMemoryStore() *Memory { + return &Memory{ + events: make(map[uuid.UUID]*types.Event), + } +} + +type Memory struct { + mux sync.Mutex + events map[uuid.UUID]*types.Event +} + +func (m *Memory) StoreEvent(event *types.Event) { + m.mux.Lock() + defer m.mux.Unlock() + m.events[event.ID] = event +} + +func (m *Memory) Close() { + m.mux.Lock() + defer m.mux.Unlock() + maps.Clear(m.events) +} + +func (m *Memory) GetEvents() []*types.Event { + m.mux.Lock() + defer m.mux.Unlock() + events := make([]*types.Event, 0, len(m.events)) + for _, event := range m.events { + events = append(events, event) + } + return events +} + +func (m *Memory) DeleteEvents(ids []uuid.UUID) { + m.mux.Lock() + defer m.mux.Unlock() + for _, id := range ids { + delete(m.events, id) + } +} diff --git a/client/internal/netflow/types/types.go b/client/internal/netflow/types/types.go new file mode 100644 index 000000000..881f30bd8 --- /dev/null +++ b/client/internal/netflow/types/types.go @@ -0,0 +1,156 @@ +package types + +import ( + "net/netip" + "strconv" + "time" + + "github.com/google/uuid" + + "github.com/netbirdio/netbird/client/iface/wgaddr" +) + +type Protocol uint8 + +const ( + ProtocolUnknown = Protocol(0) + ICMP = Protocol(1) + TCP = Protocol(6) + UDP = Protocol(17) + SCTP = Protocol(132) +) + +func (p Protocol) String() string { + switch p { + case 1: + return "ICMP" + case 6: + return "TCP" + case 17: + return "UDP" + case 132: + return "SCTP" + default: + return strconv.FormatUint(uint64(p), 10) + } +} + +type Type int + +const ( + TypeUnknown = Type(iota) + TypeStart + TypeEnd + TypeDrop +) + +type Direction int + +func (d Direction) String() string { + switch d { + case Ingress: + return "ingress" + case Egress: + return "egress" + default: + return "unknown" + } +} + +const ( + DirectionUnknown = Direction(iota) + Ingress + Egress +) + +type Event struct { + ID uuid.UUID + Timestamp time.Time + EventFields +} + +type EventFields struct { + FlowID uuid.UUID + Type Type + RuleID []byte + Direction Direction + Protocol Protocol + SourceIP netip.Addr + DestIP netip.Addr + SourceResourceID []byte + DestResourceID []byte + SourcePort uint16 + DestPort uint16 + ICMPType uint8 + ICMPCode uint8 + RxPackets uint64 + TxPackets uint64 + RxBytes uint64 + TxBytes uint64 +} + +type FlowConfig struct { + URL string + Interval time.Duration + Enabled bool + Counters bool + TokenPayload string + TokenSignature string + DNSCollection bool + ExitNodeCollection bool +} + +type FlowManager interface { + // FlowConfig handles network map updates + Update(update *FlowConfig) error + // Close closes the manager + Close() + // GetLogger returns a flow logger + GetLogger() FlowLogger +} + +type FlowLogger interface { + // StoreEvent stores a flow event + StoreEvent(flowEvent EventFields) + // GetEvents returns all stored events + GetEvents() []*Event + // DeleteEvents deletes events from the store + DeleteEvents([]uuid.UUID) + // Close closes the logger + Close() + // Enable enables the flow logger receiver + Enable() + // Disable disables the flow logger receiver + Disable() + + // UpdateConfig updates the flow manager configuration + UpdateConfig(dnsCollection, exitNodeCollection bool) +} + +type Store interface { + // StoreEvent stores a flow event + StoreEvent(event *Event) + // GetEvents returns all stored events + GetEvents() []*Event + // DeleteEvents deletes events from the store + DeleteEvents([]uuid.UUID) + // Close closes the store + Close() +} + +// ConnTracker defines the interface for connection tracking functionality +type ConnTracker interface { + // Start begins tracking connections by listening for conntrack events. + Start(bool) error + // Stop stops the connection tracking. + Stop() + // Close stops listening for events and cleans up resources + Close() error +} + +// IFaceMapper provides interface to check if we're using userspace WireGuard +type IFaceMapper interface { + IsUserspaceBind() bool + Name() string + Address() wgaddr.Address +} diff --git a/client/internal/peer/route.go b/client/internal/peer/route.go new file mode 100644 index 000000000..ff9aafcb2 --- /dev/null +++ b/client/internal/peer/route.go @@ -0,0 +1,81 @@ +package peer + +import ( + "net/netip" + "sync" + + log "github.com/sirupsen/logrus" +) + +type routeIDLookup struct { + localMap sync.Map + remoteMap sync.Map + resolvedIPs sync.Map +} + +func (r *routeIDLookup) AddLocalRouteID(resourceID string, route netip.Prefix) { + _, exists := r.localMap.LoadOrStore(route, resourceID) + if exists { + log.Tracef("resourceID %s already exists in local map", resourceID) + } +} + +func (r *routeIDLookup) RemoveLocalRouteID(route netip.Prefix) { + r.localMap.Delete(route) +} + +func (r *routeIDLookup) AddRemoteRouteID(resourceID string, route netip.Prefix) { + _, exists := r.remoteMap.LoadOrStore(route, resourceID) + if exists { + log.Tracef("resourceID %s already exists in remote map", resourceID) + } +} + +func (r *routeIDLookup) RemoveRemoteRouteID(route netip.Prefix) { + r.remoteMap.Delete(route) +} + +func (r *routeIDLookup) AddResolvedIP(resourceID string, route netip.Prefix) { + r.resolvedIPs.Store(route.Addr(), resourceID) +} + +func (r *routeIDLookup) RemoveResolvedIP(route netip.Prefix) { + r.resolvedIPs.Delete(route.Addr()) +} + +// Lookup returns the resource ID for the given IP address +// and a bool indicating if the IP is an exit node +func (r *routeIDLookup) Lookup(ip netip.Addr) (string, bool) { + var isExitNode bool + + resId, ok := r.resolvedIPs.Load(ip) + if ok { + return resId.(string), false + } + + var resourceID string + r.localMap.Range(func(key, value interface{}) bool { + pref := key.(netip.Prefix) + if pref.Contains(ip) { + resourceID = value.(string) + isExitNode = pref.Bits() == 0 + return false + + } + return true + }) + + if resourceID == "" { + r.remoteMap.Range(func(key, value interface{}) bool { + pref := key.(netip.Prefix) + if pref.Contains(ip) { + resourceID = value.(string) + isExitNode = pref.Bits() == 0 + return false + } + return true + }) + } + + return resourceID, isExitNode +} diff --git a/client/internal/peer/status.go b/client/internal/peer/status.go index ee884a76e..adf1fdd18 100644 --- a/client/internal/peer/status.go +++ b/client/internal/peer/status.go @@ -176,6 +176,8 @@ type Status struct { eventQueue *EventQueue ingressGwMgr *ingressgw.Manager + + routeIDLookup routeIDLookup } // NewRecorder returns a new Status instance @@ -311,7 +313,7 @@ func (d *Status) UpdatePeerState(receivedState State) error { return nil } -func (d *Status) AddPeerStateRoute(peer string, route string) error { +func (d *Status) AddPeerStateRoute(peer string, route string, resourceId string) error { d.mux.Lock() defer d.mux.Unlock() @@ -323,6 +325,11 @@ func (d *Status) AddPeerStateRoute(peer string, route string) error { peerState.AddRoute(route) d.peers[peer] = peerState + pref, err := netip.ParsePrefix(route) + if err == nil { + d.routeIDLookup.AddRemoteRouteID(resourceId, pref) + } + // todo: consider to make sense of this notification or not d.notifyPeerListChanged() return nil @@ -340,11 +347,26 @@ func (d *Status) RemovePeerStateRoute(peer string, route string) error { peerState.DeleteRoute(route) d.peers[peer] = peerState + pref, err := netip.ParsePrefix(route) + if err == nil { + d.routeIDLookup.RemoveRemoteRouteID(pref) + } + // todo: consider to make sense of this notification or not d.notifyPeerListChanged() return nil } +// CheckRoutes checks if the source and destination addresses are within the same route +// and returns the resource ID of the route that contains the addresses +func (d *Status) CheckRoutes(ip netip.Addr) ([]byte, bool) { + if d == nil { + return nil, false + } + resId, isExitNode := d.routeIDLookup.Lookup(ip) + return []byte(resId), isExitNode +} + func (d *Status) UpdatePeerICEState(receivedState State) error { d.mux.Lock() defer d.mux.Unlock() @@ -558,6 +580,50 @@ func (d *Status) UpdateLocalPeerState(localPeerState LocalPeerState) { d.notifyAddressChanged() } +// AddLocalPeerStateRoute adds a route to the local peer state +func (d *Status) AddLocalPeerStateRoute(route, resourceId string) { + d.mux.Lock() + defer d.mux.Unlock() + + pref, err := netip.ParsePrefix(route) + if err != nil { + log.Errorf("failed to parse prefix %s: %v", route, err) + return + } + + if d.localPeer.Routes == nil { + d.localPeer.Routes = map[string]struct{}{} + } + + d.localPeer.Routes[route] = struct{}{} + + d.routeIDLookup.AddLocalRouteID(resourceId, pref) +} + +// RemoveLocalPeerStateRoute removes a route from the local peer state +func (d *Status) RemoveLocalPeerStateRoute(route string) { + d.mux.Lock() + defer d.mux.Unlock() + + pref, err := netip.ParsePrefix(route) + if err != nil { + log.Errorf("failed to parse prefix %s: %v", route, err) + return + } + + delete(d.localPeer.Routes, route) + + d.routeIDLookup.RemoveLocalRouteID(pref) +} + +// CleanLocalPeerStateRoutes cleans all routes from the local peer state +func (d *Status) CleanLocalPeerStateRoutes() { + d.mux.Lock() + defer d.mux.Unlock() + + d.localPeer.Routes = map[string]struct{}{} +} + // CleanLocalPeerState cleans local peer status func (d *Status) CleanLocalPeerState() { d.mux.Lock() @@ -641,7 +707,7 @@ func (d *Status) UpdateDNSStates(dnsStates []NSGroupState) { d.nsGroupStates = dnsStates } -func (d *Status) UpdateResolvedDomainsStates(originalDomain domain.Domain, resolvedDomain domain.Domain, prefixes []netip.Prefix) { +func (d *Status) UpdateResolvedDomainsStates(originalDomain domain.Domain, resolvedDomain domain.Domain, prefixes []netip.Prefix, resourceId string) { d.mux.Lock() defer d.mux.Unlock() @@ -650,6 +716,10 @@ func (d *Status) UpdateResolvedDomainsStates(originalDomain domain.Domain, resol Prefixes: prefixes, ParentDomain: originalDomain, } + + for _, prefix := range prefixes { + d.routeIDLookup.AddResolvedIP(resourceId, prefix) + } } func (d *Status) DeleteResolvedDomainsStates(domain domain.Domain) { @@ -660,6 +730,10 @@ func (d *Status) DeleteResolvedDomainsStates(domain domain.Domain) { for k, v := range d.resolvedDomainsStates { if v.ParentDomain == domain { delete(d.resolvedDomainsStates, k) + + for _, prefix := range v.Prefixes { + d.routeIDLookup.RemoveResolvedIP(prefix) + } } } } diff --git a/client/internal/pkce_auth.go b/client/internal/pkce_auth.go index 6f714889f..ac6734b0c 100644 --- a/client/internal/pkce_auth.go +++ b/client/internal/pkce_auth.go @@ -37,7 +37,7 @@ type PKCEAuthProviderConfig struct { RedirectURLs []string // UseIDToken indicates if the id token should be used for authentication UseIDToken bool - //ClientCertPair is used for mTLS authentication to the IDP + // ClientCertPair is used for mTLS authentication to the IDP ClientCertPair *tls.Certificate } diff --git a/client/internal/routemanager/client.go b/client/internal/routemanager/client.go index 6680f727a..847949a53 100644 --- a/client/internal/routemanager/client.go +++ b/client/internal/routemanager/client.go @@ -330,7 +330,7 @@ func (c *clientNetwork) recalculateRouteAndUpdatePeerAndSystem(rsn reason) error c.connectEvent() } - err := c.statusRecorder.AddPeerStateRoute(c.currentChosen.Peer, c.handler.String()) + err := c.statusRecorder.AddPeerStateRoute(c.currentChosen.Peer, c.handler.String(), c.currentChosen.GetResourceID()) if err != nil { return fmt.Errorf("add peer state route: %w", err) } diff --git a/client/internal/routemanager/dnsinterceptor/handler.go b/client/internal/routemanager/dnsinterceptor/handler.go index da1056e2d..2e6e4fede 100644 --- a/client/internal/routemanager/dnsinterceptor/handler.go +++ b/client/internal/routemanager/dnsinterceptor/handler.go @@ -321,7 +321,7 @@ func (d *DnsInterceptor) updateDomainPrefixes(resolvedDomain, originalDomain dom if len(toAdd) > 0 || len(toRemove) > 0 { d.interceptedDomains[resolvedDomain] = newPrefixes originalDomain = domain.Domain(strings.TrimSuffix(string(originalDomain), ".")) - d.statusRecorder.UpdateResolvedDomainsStates(originalDomain, resolvedDomain, newPrefixes) + d.statusRecorder.UpdateResolvedDomainsStates(originalDomain, resolvedDomain, newPrefixes, d.route.GetResourceID()) if len(toAdd) > 0 { log.Debugf("added dynamic route(s) for domain=%s (pattern: domain=%s): %s", diff --git a/client/internal/routemanager/dynamic/route.go b/client/internal/routemanager/dynamic/route.go index 5ef18a47e..079134701 100644 --- a/client/internal/routemanager/dynamic/route.go +++ b/client/internal/routemanager/dynamic/route.go @@ -288,7 +288,7 @@ func (r *Route) updateDynamicRoutes(ctx context.Context, newDomains domainMap) e updatedPrefixes := combinePrefixes(oldPrefixes, removedPrefixes, addedPrefixes) r.dynamicDomains[domain] = updatedPrefixes - r.statusRecorder.UpdateResolvedDomainsStates(domain, domain, updatedPrefixes) + r.statusRecorder.UpdateResolvedDomainsStates(domain, domain, updatedPrefixes, r.route.GetResourceID()) } return nberrors.FormatErrorOrNil(merr) diff --git a/client/internal/routemanager/server_nonandroid.go b/client/internal/routemanager/server_nonandroid.go index 5b6a788f8..ac2233d4e 100644 --- a/client/internal/routemanager/server_nonandroid.go +++ b/client/internal/routemanager/server_nonandroid.go @@ -103,9 +103,7 @@ func (m *serverRouter) removeFromServerNetwork(route *route.Route) error { delete(m.routes, route.ID) - state := m.statusRecorder.GetLocalPeerState() - delete(state.Routes, route.Network.String()) - m.statusRecorder.UpdateLocalPeerState(state) + m.statusRecorder.RemoveLocalPeerStateRoute(route.Network.String()) return nil } @@ -131,18 +129,12 @@ func (m *serverRouter) addToServerNetwork(route *route.Route) error { m.routes[route.ID] = route - state := m.statusRecorder.GetLocalPeerState() - if state.Routes == nil { - state.Routes = map[string]struct{}{} - } - routeStr := route.Network.String() if route.IsDynamic() { routeStr = route.Domains.SafeString() } - state.Routes[routeStr] = struct{}{} - m.statusRecorder.UpdateLocalPeerState(state) + m.statusRecorder.AddLocalPeerStateRoute(routeStr, route.GetResourceID()) return nil } @@ -164,9 +156,7 @@ func (m *serverRouter) cleanUp() { } - state := m.statusRecorder.GetLocalPeerState() - state.Routes = nil - m.statusRecorder.UpdateLocalPeerState(state) + m.statusRecorder.CleanLocalPeerStateRoutes() } func routeToRouterPair(route *route.Route) (firewall.RouterPair, error) { diff --git a/client/server/server_test.go b/client/server/server_test.go index 0c0f32fec..1dd5fa3c9 100644 --- a/client/server/server_test.go +++ b/client/server/server_test.go @@ -6,11 +6,11 @@ import ( "testing" "time" + "github.com/golang/mock/gomock" "github.com/stretchr/testify/require" "go.opentelemetry.io/otel" "github.com/netbirdio/management-integrations/integrations" - log "github.com/sirupsen/logrus" "google.golang.org/grpc" "google.golang.org/grpc/keepalive" @@ -129,13 +129,17 @@ func startManagement(t *testing.T, signalAddr string, counter *int) (*grpc.Serve metrics, err := telemetry.NewDefaultAppMetrics(context.Background()) require.NoError(t, err) - accountManager, err := server.BuildManager(context.Background(), store, peersUpdateManager, nil, "", "netbird.selfhosted", eventStore, nil, false, ia, metrics, port_forwarding.NewControllerMock()) + ctrl := gomock.NewController(t) + t.Cleanup(ctrl.Finish) + settingsMockManager := settings.NewMockManager(ctrl) + + accountManager, err := server.BuildManager(context.Background(), store, peersUpdateManager, nil, "", "netbird.selfhosted", eventStore, nil, false, ia, metrics, port_forwarding.NewControllerMock(), settingsMockManager) if err != nil { return nil, "", err } - secretsManager := server.NewTimeBasedAuthSecretsManager(peersUpdateManager, config.TURNConfig, config.Relay) - mgmtServer, err := server.NewServer(context.Background(), config, accountManager, settings.NewManager(store), peersUpdateManager, secretsManager, nil, nil, nil) + secretsManager := server.NewTimeBasedAuthSecretsManager(peersUpdateManager, config.TURNConfig, config.Relay, settingsMockManager) + mgmtServer, err := server.NewServer(context.Background(), config, accountManager, settingsMockManager, peersUpdateManager, secretsManager, nil, nil, nil) if err != nil { return nil, "", err } diff --git a/client/server/trace.go b/client/server/trace.go index 66b83d8cf..8b9d375f3 100644 --- a/client/server/trace.go +++ b/client/server/trace.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "net" + "net/netip" fw "github.com/netbirdio/netbird/client/firewall/manager" "github.com/netbirdio/netbird/client/firewall/uspfilter" @@ -41,11 +42,21 @@ func (s *Server) TracePacket(_ context.Context, req *proto.TracePacketRequest) ( srcIP = engine.GetWgAddr() } + srcAddr, ok := netip.AddrFromSlice(srcIP) + if !ok { + return nil, fmt.Errorf("invalid source IP address") + } + dstIP := net.ParseIP(req.GetDestinationIp()) if req.GetDestinationIp() == "self" { dstIP = engine.GetWgAddr() } + dstAddr, ok := netip.AddrFromSlice(dstIP) + if !ok { + return nil, fmt.Errorf("invalid source IP address") + } + if srcIP == nil || dstIP == nil { return nil, fmt.Errorf("invalid IP address") } @@ -85,8 +96,8 @@ func (s *Server) TracePacket(_ context.Context, req *proto.TracePacketRequest) ( } builder := &uspfilter.PacketBuilder{ - SrcIP: srcIP, - DstIP: dstIP, + SrcIP: srcAddr, + DstIP: dstAddr, Protocol: protocol, SrcPort: uint16(req.GetSourcePort()), DstPort: uint16(req.GetDestinationPort()), diff --git a/flow/client/auth.go b/flow/client/auth.go new file mode 100644 index 000000000..de9e9cece --- /dev/null +++ b/flow/client/auth.go @@ -0,0 +1,32 @@ +package client + +import ( + "context" + "fmt" + + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" +) + +var _ credentials.PerRPCCredentials = (*authToken)(nil) + +type authToken struct { + metaMap map[string]string +} + +func (t authToken) GetRequestMetadata(context.Context, ...string) (map[string]string, error) { + return t.metaMap, nil +} + +func (authToken) RequireTransportSecurity() bool { + return false // Set to true if you want to require a secure connection +} + +// WithAuthToken returns a DialOption which sets the receiver flow credentials and places auth state on each outbound RPC +func withAuthToken(payload, signature string) grpc.DialOption { + value := fmt.Sprintf("%s.%s", signature, payload) + authMap := map[string]string{ + "authorization": "Bearer " + value, + } + return grpc.WithPerRPCCredentials(authToken{metaMap: authMap}) +} diff --git a/flow/client/client.go b/flow/client/client.go new file mode 100644 index 000000000..b16b28c64 --- /dev/null +++ b/flow/client/client.go @@ -0,0 +1,167 @@ +package client + +import ( + "context" + "crypto/tls" + "crypto/x509" + "errors" + "fmt" + "strings" + "sync" + "time" + + "github.com/cenkalti/backoff/v4" + log "github.com/sirupsen/logrus" + "google.golang.org/grpc" + "google.golang.org/grpc/connectivity" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/keepalive" + + "github.com/netbirdio/netbird/flow/proto" + "github.com/netbirdio/netbird/util/embeddedroots" + nbgrpc "github.com/netbirdio/netbird/util/grpc" +) + +type GRPCClient struct { + realClient proto.FlowServiceClient + clientConn *grpc.ClientConn + stream proto.FlowService_EventsClient + streamMu sync.Mutex +} + +func NewClient(addr, payload, signature string, interval time.Duration) (*GRPCClient, error) { + var opts []grpc.DialOption + + if strings.Contains(addr, "443") { + certPool, err := x509.SystemCertPool() + if err != nil || certPool == nil { + log.Debugf("System cert pool not available; falling back to embedded cert, error: %v", err) + certPool = embeddedroots.Get() + } + + opts = append(opts, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{ + RootCAs: certPool, + }))) + } else { + opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials())) + } + + opts = append(opts, + nbgrpc.WithCustomDialer(), + grpc.WithIdleTimeout(interval*2), + grpc.WithKeepaliveParams(keepalive.ClientParameters{ + Time: 30 * time.Second, + Timeout: 10 * time.Second, + }), + withAuthToken(payload, signature), + grpc.WithDefaultServiceConfig(`{"healthCheckConfig": {"serviceName": ""}}`), + ) + + conn, err := grpc.NewClient(addr, opts...) + if err != nil { + return nil, fmt.Errorf("creating new grpc client: %w", err) + } + + return &GRPCClient{ + realClient: proto.NewFlowServiceClient(conn), + clientConn: conn, + }, nil +} + +func (c *GRPCClient) Close() error { + c.streamMu.Lock() + defer c.streamMu.Unlock() + + c.stream = nil + return c.clientConn.Close() +} + +func (c *GRPCClient) Receive(ctx context.Context, interval time.Duration, msgHandler func(msg *proto.FlowEventAck) error) error { + backOff := defaultBackoff(ctx, interval) + operation := func() error { + return c.establishStreamAndReceive(ctx, msgHandler) + } + + if err := backoff.Retry(operation, backOff); err != nil { + return fmt.Errorf("receive failed permanently: %w", err) + } + + return nil +} + +func (c *GRPCClient) establishStreamAndReceive(ctx context.Context, msgHandler func(msg *proto.FlowEventAck) error) error { + if c.clientConn.GetState() == connectivity.Shutdown { + return backoff.Permanent(errors.New("connection to flow receiver has been shut down")) + } + + stream, err := c.realClient.Events(ctx, grpc.WaitForReady(true)) + if err != nil { + return fmt.Errorf("create event stream: %w", err) + } + + if err = checkHeader(stream); err != nil { + return fmt.Errorf("check header: %w", err) + } + + c.streamMu.Lock() + c.stream = stream + c.streamMu.Unlock() + + return c.receive(stream, msgHandler) +} + +func (c *GRPCClient) receive(stream proto.FlowService_EventsClient, msgHandler func(msg *proto.FlowEventAck) error) error { + for { + msg, err := stream.Recv() + if err != nil { + return fmt.Errorf("receive from stream: %w", err) + } + + if err := msgHandler(msg); err != nil { + return fmt.Errorf("handle message: %w", err) + } + } +} + +func checkHeader(stream proto.FlowService_EventsClient) error { + header, err := stream.Header() + if err != nil { + log.Errorf("waiting for flow receiver header: %s", err) + return fmt.Errorf("wait for header: %w", err) + } + + if len(header) == 0 { + log.Error("flow receiver sent no headers") + return fmt.Errorf("should have headers") + } + return nil +} + +func defaultBackoff(ctx context.Context, interval time.Duration) backoff.BackOff { + return backoff.WithContext(&backoff.ExponentialBackOff{ + InitialInterval: 800 * time.Millisecond, + RandomizationFactor: 1, + Multiplier: 1.7, + MaxInterval: interval / 2, + MaxElapsedTime: 3 * 30 * 24 * time.Hour, // 3 months + Stop: backoff.Stop, + Clock: backoff.SystemClock, + }, ctx) +} + +func (c *GRPCClient) Send(event *proto.FlowEvent) error { + c.streamMu.Lock() + stream := c.stream + c.streamMu.Unlock() + + if stream == nil { + return errors.New("stream not initialized") + } + + if err := stream.Send(event); err != nil { + return fmt.Errorf("send flow event: %w", err) + } + + return nil +} diff --git a/flow/proto/flow.pb.go b/flow/proto/flow.pb.go new file mode 100644 index 000000000..8b34b0f62 --- /dev/null +++ b/flow/proto/flow.pb.go @@ -0,0 +1,769 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.26.0 +// protoc v4.24.3 +// source: flow.proto + +package proto + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// Flow event types +type Type int32 + +const ( + Type_TYPE_UNKNOWN Type = 0 + Type_TYPE_START Type = 1 + Type_TYPE_END Type = 2 + Type_TYPE_DROP Type = 3 +) + +// Enum value maps for Type. +var ( + Type_name = map[int32]string{ + 0: "TYPE_UNKNOWN", + 1: "TYPE_START", + 2: "TYPE_END", + 3: "TYPE_DROP", + } + Type_value = map[string]int32{ + "TYPE_UNKNOWN": 0, + "TYPE_START": 1, + "TYPE_END": 2, + "TYPE_DROP": 3, + } +) + +func (x Type) Enum() *Type { + p := new(Type) + *p = x + return p +} + +func (x Type) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (Type) Descriptor() protoreflect.EnumDescriptor { + return file_flow_proto_enumTypes[0].Descriptor() +} + +func (Type) Type() protoreflect.EnumType { + return &file_flow_proto_enumTypes[0] +} + +func (x Type) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use Type.Descriptor instead. +func (Type) EnumDescriptor() ([]byte, []int) { + return file_flow_proto_rawDescGZIP(), []int{0} +} + +// Flow direction +type Direction int32 + +const ( + Direction_DIRECTION_UNKNOWN Direction = 0 + Direction_INGRESS Direction = 1 + Direction_EGRESS Direction = 2 +) + +// Enum value maps for Direction. +var ( + Direction_name = map[int32]string{ + 0: "DIRECTION_UNKNOWN", + 1: "INGRESS", + 2: "EGRESS", + } + Direction_value = map[string]int32{ + "DIRECTION_UNKNOWN": 0, + "INGRESS": 1, + "EGRESS": 2, + } +) + +func (x Direction) Enum() *Direction { + p := new(Direction) + *p = x + return p +} + +func (x Direction) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (Direction) Descriptor() protoreflect.EnumDescriptor { + return file_flow_proto_enumTypes[1].Descriptor() +} + +func (Direction) Type() protoreflect.EnumType { + return &file_flow_proto_enumTypes[1] +} + +func (x Direction) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use Direction.Descriptor instead. +func (Direction) EnumDescriptor() ([]byte, []int) { + return file_flow_proto_rawDescGZIP(), []int{1} +} + +type FlowEvent struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Unique client event identifier + EventId []byte `protobuf:"bytes,1,opt,name=event_id,json=eventId,proto3" json:"event_id,omitempty"` + // When the event occurred + Timestamp *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=timestamp,proto3" json:"timestamp,omitempty"` + // Public key of the sending peer + PublicKey []byte `protobuf:"bytes,3,opt,name=public_key,json=publicKey,proto3" json:"public_key,omitempty"` + FlowFields *FlowFields `protobuf:"bytes,4,opt,name=flow_fields,json=flowFields,proto3" json:"flow_fields,omitempty"` +} + +func (x *FlowEvent) Reset() { + *x = FlowEvent{} + if protoimpl.UnsafeEnabled { + mi := &file_flow_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *FlowEvent) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FlowEvent) ProtoMessage() {} + +func (x *FlowEvent) ProtoReflect() protoreflect.Message { + mi := &file_flow_proto_msgTypes[0] + 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 FlowEvent.ProtoReflect.Descriptor instead. +func (*FlowEvent) Descriptor() ([]byte, []int) { + return file_flow_proto_rawDescGZIP(), []int{0} +} + +func (x *FlowEvent) GetEventId() []byte { + if x != nil { + return x.EventId + } + return nil +} + +func (x *FlowEvent) GetTimestamp() *timestamppb.Timestamp { + if x != nil { + return x.Timestamp + } + return nil +} + +func (x *FlowEvent) GetPublicKey() []byte { + if x != nil { + return x.PublicKey + } + return nil +} + +func (x *FlowEvent) GetFlowFields() *FlowFields { + if x != nil { + return x.FlowFields + } + return nil +} + +type FlowEventAck struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Unique client event identifier that has been ack'ed + EventId []byte `protobuf:"bytes,1,opt,name=event_id,json=eventId,proto3" json:"event_id,omitempty"` +} + +func (x *FlowEventAck) Reset() { + *x = FlowEventAck{} + if protoimpl.UnsafeEnabled { + mi := &file_flow_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *FlowEventAck) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FlowEventAck) ProtoMessage() {} + +func (x *FlowEventAck) ProtoReflect() protoreflect.Message { + mi := &file_flow_proto_msgTypes[1] + 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 FlowEventAck.ProtoReflect.Descriptor instead. +func (*FlowEventAck) Descriptor() ([]byte, []int) { + return file_flow_proto_rawDescGZIP(), []int{1} +} + +func (x *FlowEventAck) GetEventId() []byte { + if x != nil { + return x.EventId + } + return nil +} + +type FlowFields struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Unique client flow session identifier + FlowId []byte `protobuf:"bytes,1,opt,name=flow_id,json=flowId,proto3" json:"flow_id,omitempty"` + // Flow type + Type Type `protobuf:"varint,2,opt,name=type,proto3,enum=flow.Type" json:"type,omitempty"` + // RuleId identifies the rule that allowed or denied the connection + RuleId []byte `protobuf:"bytes,3,opt,name=rule_id,json=ruleId,proto3" json:"rule_id,omitempty"` + // Initiating traffic direction + Direction Direction `protobuf:"varint,4,opt,name=direction,proto3,enum=flow.Direction" json:"direction,omitempty"` + // IP protocol number + Protocol uint32 `protobuf:"varint,5,opt,name=protocol,proto3" json:"protocol,omitempty"` + // Source IP address + SourceIp []byte `protobuf:"bytes,6,opt,name=source_ip,json=sourceIp,proto3" json:"source_ip,omitempty"` + // Destination IP address + DestIp []byte `protobuf:"bytes,7,opt,name=dest_ip,json=destIp,proto3" json:"dest_ip,omitempty"` + // Layer 4 -specific information + // + // Types that are assignable to ConnectionInfo: + // + // *FlowFields_PortInfo + // *FlowFields_IcmpInfo + ConnectionInfo isFlowFields_ConnectionInfo `protobuf_oneof:"connection_info"` + // Number of packets + RxPackets uint64 `protobuf:"varint,10,opt,name=rx_packets,json=rxPackets,proto3" json:"rx_packets,omitempty"` + TxPackets uint64 `protobuf:"varint,11,opt,name=tx_packets,json=txPackets,proto3" json:"tx_packets,omitempty"` + // Number of bytes + RxBytes uint64 `protobuf:"varint,12,opt,name=rx_bytes,json=rxBytes,proto3" json:"rx_bytes,omitempty"` + TxBytes uint64 `protobuf:"varint,13,opt,name=tx_bytes,json=txBytes,proto3" json:"tx_bytes,omitempty"` + // Resource ID + SourceResourceId []byte `protobuf:"bytes,14,opt,name=source_resource_id,json=sourceResourceId,proto3" json:"source_resource_id,omitempty"` + DestResourceId []byte `protobuf:"bytes,15,opt,name=dest_resource_id,json=destResourceId,proto3" json:"dest_resource_id,omitempty"` +} + +func (x *FlowFields) Reset() { + *x = FlowFields{} + if protoimpl.UnsafeEnabled { + mi := &file_flow_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *FlowFields) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FlowFields) ProtoMessage() {} + +func (x *FlowFields) ProtoReflect() protoreflect.Message { + mi := &file_flow_proto_msgTypes[2] + 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 FlowFields.ProtoReflect.Descriptor instead. +func (*FlowFields) Descriptor() ([]byte, []int) { + return file_flow_proto_rawDescGZIP(), []int{2} +} + +func (x *FlowFields) GetFlowId() []byte { + if x != nil { + return x.FlowId + } + return nil +} + +func (x *FlowFields) GetType() Type { + if x != nil { + return x.Type + } + return Type_TYPE_UNKNOWN +} + +func (x *FlowFields) GetRuleId() []byte { + if x != nil { + return x.RuleId + } + return nil +} + +func (x *FlowFields) GetDirection() Direction { + if x != nil { + return x.Direction + } + return Direction_DIRECTION_UNKNOWN +} + +func (x *FlowFields) GetProtocol() uint32 { + if x != nil { + return x.Protocol + } + return 0 +} + +func (x *FlowFields) GetSourceIp() []byte { + if x != nil { + return x.SourceIp + } + return nil +} + +func (x *FlowFields) GetDestIp() []byte { + if x != nil { + return x.DestIp + } + return nil +} + +func (m *FlowFields) GetConnectionInfo() isFlowFields_ConnectionInfo { + if m != nil { + return m.ConnectionInfo + } + return nil +} + +func (x *FlowFields) GetPortInfo() *PortInfo { + if x, ok := x.GetConnectionInfo().(*FlowFields_PortInfo); ok { + return x.PortInfo + } + return nil +} + +func (x *FlowFields) GetIcmpInfo() *ICMPInfo { + if x, ok := x.GetConnectionInfo().(*FlowFields_IcmpInfo); ok { + return x.IcmpInfo + } + return nil +} + +func (x *FlowFields) GetRxPackets() uint64 { + if x != nil { + return x.RxPackets + } + return 0 +} + +func (x *FlowFields) GetTxPackets() uint64 { + if x != nil { + return x.TxPackets + } + return 0 +} + +func (x *FlowFields) GetRxBytes() uint64 { + if x != nil { + return x.RxBytes + } + return 0 +} + +func (x *FlowFields) GetTxBytes() uint64 { + if x != nil { + return x.TxBytes + } + return 0 +} + +func (x *FlowFields) GetSourceResourceId() []byte { + if x != nil { + return x.SourceResourceId + } + return nil +} + +func (x *FlowFields) GetDestResourceId() []byte { + if x != nil { + return x.DestResourceId + } + return nil +} + +type isFlowFields_ConnectionInfo interface { + isFlowFields_ConnectionInfo() +} + +type FlowFields_PortInfo struct { + // TCP/UDP port information + PortInfo *PortInfo `protobuf:"bytes,8,opt,name=port_info,json=portInfo,proto3,oneof"` +} + +type FlowFields_IcmpInfo struct { + // ICMP type and code + IcmpInfo *ICMPInfo `protobuf:"bytes,9,opt,name=icmp_info,json=icmpInfo,proto3,oneof"` +} + +func (*FlowFields_PortInfo) isFlowFields_ConnectionInfo() {} + +func (*FlowFields_IcmpInfo) isFlowFields_ConnectionInfo() {} + +// TCP/UDP port information +type PortInfo struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + SourcePort uint32 `protobuf:"varint,1,opt,name=source_port,json=sourcePort,proto3" json:"source_port,omitempty"` + DestPort uint32 `protobuf:"varint,2,opt,name=dest_port,json=destPort,proto3" json:"dest_port,omitempty"` +} + +func (x *PortInfo) Reset() { + *x = PortInfo{} + if protoimpl.UnsafeEnabled { + mi := &file_flow_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *PortInfo) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PortInfo) ProtoMessage() {} + +func (x *PortInfo) ProtoReflect() protoreflect.Message { + mi := &file_flow_proto_msgTypes[3] + 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 PortInfo.ProtoReflect.Descriptor instead. +func (*PortInfo) Descriptor() ([]byte, []int) { + return file_flow_proto_rawDescGZIP(), []int{3} +} + +func (x *PortInfo) GetSourcePort() uint32 { + if x != nil { + return x.SourcePort + } + return 0 +} + +func (x *PortInfo) GetDestPort() uint32 { + if x != nil { + return x.DestPort + } + return 0 +} + +// ICMP message information +type ICMPInfo struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + IcmpType uint32 `protobuf:"varint,1,opt,name=icmp_type,json=icmpType,proto3" json:"icmp_type,omitempty"` + IcmpCode uint32 `protobuf:"varint,2,opt,name=icmp_code,json=icmpCode,proto3" json:"icmp_code,omitempty"` +} + +func (x *ICMPInfo) Reset() { + *x = ICMPInfo{} + if protoimpl.UnsafeEnabled { + mi := &file_flow_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ICMPInfo) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ICMPInfo) ProtoMessage() {} + +func (x *ICMPInfo) ProtoReflect() protoreflect.Message { + mi := &file_flow_proto_msgTypes[4] + 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 ICMPInfo.ProtoReflect.Descriptor instead. +func (*ICMPInfo) Descriptor() ([]byte, []int) { + return file_flow_proto_rawDescGZIP(), []int{4} +} + +func (x *ICMPInfo) GetIcmpType() uint32 { + if x != nil { + return x.IcmpType + } + return 0 +} + +func (x *ICMPInfo) GetIcmpCode() uint32 { + if x != nil { + return x.IcmpCode + } + return 0 +} + +var File_flow_proto protoreflect.FileDescriptor + +var file_flow_proto_rawDesc = []byte{ + 0x0a, 0x0a, 0x66, 0x6c, 0x6f, 0x77, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x04, 0x66, 0x6c, + 0x6f, 0x77, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x22, 0xb2, 0x01, 0x0a, 0x09, 0x46, 0x6c, 0x6f, 0x77, 0x45, 0x76, 0x65, 0x6e, + 0x74, 0x12, 0x19, 0x0a, 0x08, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x0c, 0x52, 0x07, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x38, 0x0a, 0x09, + 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, + 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x74, 0x69, 0x6d, + 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, + 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x70, 0x75, 0x62, 0x6c, + 0x69, 0x63, 0x4b, 0x65, 0x79, 0x12, 0x31, 0x0a, 0x0b, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x66, 0x69, + 0x65, 0x6c, 0x64, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x66, 0x6c, 0x6f, + 0x77, 0x2e, 0x46, 0x6c, 0x6f, 0x77, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x52, 0x0a, 0x66, 0x6c, + 0x6f, 0x77, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x22, 0x29, 0x0a, 0x0c, 0x46, 0x6c, 0x6f, 0x77, + 0x45, 0x76, 0x65, 0x6e, 0x74, 0x41, 0x63, 0x6b, 0x12, 0x19, 0x0a, 0x08, 0x65, 0x76, 0x65, 0x6e, + 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x65, 0x76, 0x65, 0x6e, + 0x74, 0x49, 0x64, 0x22, 0x9c, 0x04, 0x0a, 0x0a, 0x46, 0x6c, 0x6f, 0x77, 0x46, 0x69, 0x65, 0x6c, + 0x64, 0x73, 0x12, 0x17, 0x0a, 0x07, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x0c, 0x52, 0x06, 0x66, 0x6c, 0x6f, 0x77, 0x49, 0x64, 0x12, 0x1e, 0x0a, 0x04, 0x74, + 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x0a, 0x2e, 0x66, 0x6c, 0x6f, 0x77, + 0x2e, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x17, 0x0a, 0x07, 0x72, + 0x75, 0x6c, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x72, 0x75, + 0x6c, 0x65, 0x49, 0x64, 0x12, 0x2d, 0x0a, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, + 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x0f, 0x2e, 0x66, 0x6c, 0x6f, 0x77, 0x2e, 0x44, + 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, + 0x05, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, + 0x1b, 0x0a, 0x09, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x69, 0x70, 0x18, 0x06, 0x20, 0x01, + 0x28, 0x0c, 0x52, 0x08, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x70, 0x12, 0x17, 0x0a, 0x07, + 0x64, 0x65, 0x73, 0x74, 0x5f, 0x69, 0x70, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x64, + 0x65, 0x73, 0x74, 0x49, 0x70, 0x12, 0x2d, 0x0a, 0x09, 0x70, 0x6f, 0x72, 0x74, 0x5f, 0x69, 0x6e, + 0x66, 0x6f, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x66, 0x6c, 0x6f, 0x77, 0x2e, + 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x48, 0x00, 0x52, 0x08, 0x70, 0x6f, 0x72, 0x74, + 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x2d, 0x0a, 0x09, 0x69, 0x63, 0x6d, 0x70, 0x5f, 0x69, 0x6e, 0x66, + 0x6f, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x66, 0x6c, 0x6f, 0x77, 0x2e, 0x49, + 0x43, 0x4d, 0x50, 0x49, 0x6e, 0x66, 0x6f, 0x48, 0x00, 0x52, 0x08, 0x69, 0x63, 0x6d, 0x70, 0x49, + 0x6e, 0x66, 0x6f, 0x12, 0x1d, 0x0a, 0x0a, 0x72, 0x78, 0x5f, 0x70, 0x61, 0x63, 0x6b, 0x65, 0x74, + 0x73, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x72, 0x78, 0x50, 0x61, 0x63, 0x6b, 0x65, + 0x74, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x74, 0x78, 0x5f, 0x70, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x73, + 0x18, 0x0b, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x74, 0x78, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, + 0x73, 0x12, 0x19, 0x0a, 0x08, 0x72, 0x78, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, 0x0c, 0x20, + 0x01, 0x28, 0x04, 0x52, 0x07, 0x72, 0x78, 0x42, 0x79, 0x74, 0x65, 0x73, 0x12, 0x19, 0x0a, 0x08, + 0x74, 0x78, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x04, 0x52, 0x07, + 0x74, 0x78, 0x42, 0x79, 0x74, 0x65, 0x73, 0x12, 0x2c, 0x0a, 0x12, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x5f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x0e, 0x20, + 0x01, 0x28, 0x0c, 0x52, 0x10, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x49, 0x64, 0x12, 0x28, 0x0a, 0x10, 0x64, 0x65, 0x73, 0x74, 0x5f, 0x72, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x0c, 0x52, + 0x0e, 0x64, 0x65, 0x73, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x42, + 0x11, 0x0a, 0x0f, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x6e, + 0x66, 0x6f, 0x22, 0x48, 0x0a, 0x08, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x1f, + 0x0a, 0x0b, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x0d, 0x52, 0x0a, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x50, 0x6f, 0x72, 0x74, 0x12, + 0x1b, 0x0a, 0x09, 0x64, 0x65, 0x73, 0x74, 0x5f, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x0d, 0x52, 0x08, 0x64, 0x65, 0x73, 0x74, 0x50, 0x6f, 0x72, 0x74, 0x22, 0x44, 0x0a, 0x08, + 0x49, 0x43, 0x4d, 0x50, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x1b, 0x0a, 0x09, 0x69, 0x63, 0x6d, 0x70, + 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x08, 0x69, 0x63, 0x6d, + 0x70, 0x54, 0x79, 0x70, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x69, 0x63, 0x6d, 0x70, 0x5f, 0x63, 0x6f, + 0x64, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x08, 0x69, 0x63, 0x6d, 0x70, 0x43, 0x6f, + 0x64, 0x65, 0x2a, 0x45, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12, 0x10, 0x0a, 0x0c, 0x54, 0x59, + 0x50, 0x45, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x0e, 0x0a, 0x0a, + 0x54, 0x59, 0x50, 0x45, 0x5f, 0x53, 0x54, 0x41, 0x52, 0x54, 0x10, 0x01, 0x12, 0x0c, 0x0a, 0x08, + 0x54, 0x59, 0x50, 0x45, 0x5f, 0x45, 0x4e, 0x44, 0x10, 0x02, 0x12, 0x0d, 0x0a, 0x09, 0x54, 0x59, + 0x50, 0x45, 0x5f, 0x44, 0x52, 0x4f, 0x50, 0x10, 0x03, 0x2a, 0x3b, 0x0a, 0x09, 0x44, 0x69, 0x72, + 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x15, 0x0a, 0x11, 0x44, 0x49, 0x52, 0x45, 0x43, 0x54, + 0x49, 0x4f, 0x4e, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x0b, 0x0a, + 0x07, 0x49, 0x4e, 0x47, 0x52, 0x45, 0x53, 0x53, 0x10, 0x01, 0x12, 0x0a, 0x0a, 0x06, 0x45, 0x47, + 0x52, 0x45, 0x53, 0x53, 0x10, 0x02, 0x32, 0x42, 0x0a, 0x0b, 0x46, 0x6c, 0x6f, 0x77, 0x53, 0x65, + 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x33, 0x0a, 0x06, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x12, + 0x0f, 0x2e, 0x66, 0x6c, 0x6f, 0x77, 0x2e, 0x46, 0x6c, 0x6f, 0x77, 0x45, 0x76, 0x65, 0x6e, 0x74, + 0x1a, 0x12, 0x2e, 0x66, 0x6c, 0x6f, 0x77, 0x2e, 0x46, 0x6c, 0x6f, 0x77, 0x45, 0x76, 0x65, 0x6e, + 0x74, 0x41, 0x63, 0x6b, 0x22, 0x00, 0x28, 0x01, 0x30, 0x01, 0x42, 0x08, 0x5a, 0x06, 0x2f, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_flow_proto_rawDescOnce sync.Once + file_flow_proto_rawDescData = file_flow_proto_rawDesc +) + +func file_flow_proto_rawDescGZIP() []byte { + file_flow_proto_rawDescOnce.Do(func() { + file_flow_proto_rawDescData = protoimpl.X.CompressGZIP(file_flow_proto_rawDescData) + }) + return file_flow_proto_rawDescData +} + +var file_flow_proto_enumTypes = make([]protoimpl.EnumInfo, 2) +var file_flow_proto_msgTypes = make([]protoimpl.MessageInfo, 5) +var file_flow_proto_goTypes = []interface{}{ + (Type)(0), // 0: flow.Type + (Direction)(0), // 1: flow.Direction + (*FlowEvent)(nil), // 2: flow.FlowEvent + (*FlowEventAck)(nil), // 3: flow.FlowEventAck + (*FlowFields)(nil), // 4: flow.FlowFields + (*PortInfo)(nil), // 5: flow.PortInfo + (*ICMPInfo)(nil), // 6: flow.ICMPInfo + (*timestamppb.Timestamp)(nil), // 7: google.protobuf.Timestamp +} +var file_flow_proto_depIdxs = []int32{ + 7, // 0: flow.FlowEvent.timestamp:type_name -> google.protobuf.Timestamp + 4, // 1: flow.FlowEvent.flow_fields:type_name -> flow.FlowFields + 0, // 2: flow.FlowFields.type:type_name -> flow.Type + 1, // 3: flow.FlowFields.direction:type_name -> flow.Direction + 5, // 4: flow.FlowFields.port_info:type_name -> flow.PortInfo + 6, // 5: flow.FlowFields.icmp_info:type_name -> flow.ICMPInfo + 2, // 6: flow.FlowService.Events:input_type -> flow.FlowEvent + 3, // 7: flow.FlowService.Events:output_type -> flow.FlowEventAck + 7, // [7:8] is the sub-list for method output_type + 6, // [6:7] is the sub-list for method input_type + 6, // [6:6] is the sub-list for extension type_name + 6, // [6:6] is the sub-list for extension extendee + 0, // [0:6] is the sub-list for field type_name +} + +func init() { file_flow_proto_init() } +func file_flow_proto_init() { + if File_flow_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_flow_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*FlowEvent); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_flow_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*FlowEventAck); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_flow_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*FlowFields); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_flow_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*PortInfo); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_flow_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ICMPInfo); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + file_flow_proto_msgTypes[2].OneofWrappers = []interface{}{ + (*FlowFields_PortInfo)(nil), + (*FlowFields_IcmpInfo)(nil), + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_flow_proto_rawDesc, + NumEnums: 2, + NumMessages: 5, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_flow_proto_goTypes, + DependencyIndexes: file_flow_proto_depIdxs, + EnumInfos: file_flow_proto_enumTypes, + MessageInfos: file_flow_proto_msgTypes, + }.Build() + File_flow_proto = out.File + file_flow_proto_rawDesc = nil + file_flow_proto_goTypes = nil + file_flow_proto_depIdxs = nil +} diff --git a/flow/proto/flow.proto b/flow/proto/flow.proto new file mode 100644 index 000000000..d11af623a --- /dev/null +++ b/flow/proto/flow.proto @@ -0,0 +1,102 @@ +syntax = "proto3"; + +import "google/protobuf/timestamp.proto"; + +option go_package = "/proto"; + +package flow; + +service FlowService { + // Client to receiver streams of events and acknowledgements + rpc Events(stream FlowEvent) returns (stream FlowEventAck) {} +} + +message FlowEvent { + // Unique client event identifier + bytes event_id = 1; + + // When the event occurred + google.protobuf.Timestamp timestamp = 2; + + // Public key of the sending peer + bytes public_key = 3; + + FlowFields flow_fields = 4; +} + +message FlowEventAck { + // Unique client event identifier that has been ack'ed + bytes event_id = 1; +} + +message FlowFields { + // Unique client flow session identifier + bytes flow_id = 1; + + // Flow type + Type type = 2; + + // RuleId identifies the rule that allowed or denied the connection + bytes rule_id = 3; + + // Initiating traffic direction + Direction direction = 4; + + // IP protocol number + uint32 protocol = 5; + + // Source IP address + bytes source_ip = 6; + + // Destination IP address + bytes dest_ip = 7; + + // Layer 4 -specific information + oneof connection_info { + // TCP/UDP port information + PortInfo port_info = 8; + + // ICMP type and code + ICMPInfo icmp_info = 9; + } + + // Number of packets + uint64 rx_packets = 10; + uint64 tx_packets = 11; + + // Number of bytes + uint64 rx_bytes = 12; + uint64 tx_bytes = 13; + + // Resource ID + bytes source_resource_id = 14; + bytes dest_resource_id = 15; + +} + +// Flow event types +enum Type { + TYPE_UNKNOWN = 0; + TYPE_START = 1; + TYPE_END = 2; + TYPE_DROP = 3; +} + +// Flow direction +enum Direction { + DIRECTION_UNKNOWN = 0; + INGRESS = 1; + EGRESS = 2; +} + +// TCP/UDP port information +message PortInfo { + uint32 source_port = 1; + uint32 dest_port = 2; +} + +// ICMP message information +message ICMPInfo { + uint32 icmp_type = 1; + uint32 icmp_code = 2; +} diff --git a/flow/proto/flow_grpc.pb.go b/flow/proto/flow_grpc.pb.go new file mode 100644 index 000000000..b790f86a2 --- /dev/null +++ b/flow/proto/flow_grpc.pb.go @@ -0,0 +1,135 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. + +package proto + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.32.0 or later. +const _ = grpc.SupportPackageIsVersion7 + +// FlowServiceClient is the client API for FlowService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type FlowServiceClient interface { + // Client to receiver streams of events and acknowledgements + Events(ctx context.Context, opts ...grpc.CallOption) (FlowService_EventsClient, error) +} + +type flowServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewFlowServiceClient(cc grpc.ClientConnInterface) FlowServiceClient { + return &flowServiceClient{cc} +} + +func (c *flowServiceClient) Events(ctx context.Context, opts ...grpc.CallOption) (FlowService_EventsClient, error) { + stream, err := c.cc.NewStream(ctx, &FlowService_ServiceDesc.Streams[0], "/flow.FlowService/Events", opts...) + if err != nil { + return nil, err + } + x := &flowServiceEventsClient{stream} + return x, nil +} + +type FlowService_EventsClient interface { + Send(*FlowEvent) error + Recv() (*FlowEventAck, error) + grpc.ClientStream +} + +type flowServiceEventsClient struct { + grpc.ClientStream +} + +func (x *flowServiceEventsClient) Send(m *FlowEvent) error { + return x.ClientStream.SendMsg(m) +} + +func (x *flowServiceEventsClient) Recv() (*FlowEventAck, error) { + m := new(FlowEventAck) + if err := x.ClientStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil +} + +// FlowServiceServer is the server API for FlowService service. +// All implementations must embed UnimplementedFlowServiceServer +// for forward compatibility +type FlowServiceServer interface { + // Client to receiver streams of events and acknowledgements + Events(FlowService_EventsServer) error + mustEmbedUnimplementedFlowServiceServer() +} + +// UnimplementedFlowServiceServer must be embedded to have forward compatible implementations. +type UnimplementedFlowServiceServer struct { +} + +func (UnimplementedFlowServiceServer) Events(FlowService_EventsServer) error { + return status.Errorf(codes.Unimplemented, "method Events not implemented") +} +func (UnimplementedFlowServiceServer) mustEmbedUnimplementedFlowServiceServer() {} + +// UnsafeFlowServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to FlowServiceServer will +// result in compilation errors. +type UnsafeFlowServiceServer interface { + mustEmbedUnimplementedFlowServiceServer() +} + +func RegisterFlowServiceServer(s grpc.ServiceRegistrar, srv FlowServiceServer) { + s.RegisterService(&FlowService_ServiceDesc, srv) +} + +func _FlowService_Events_Handler(srv interface{}, stream grpc.ServerStream) error { + return srv.(FlowServiceServer).Events(&flowServiceEventsServer{stream}) +} + +type FlowService_EventsServer interface { + Send(*FlowEventAck) error + Recv() (*FlowEvent, error) + grpc.ServerStream +} + +type flowServiceEventsServer struct { + grpc.ServerStream +} + +func (x *flowServiceEventsServer) Send(m *FlowEventAck) error { + return x.ServerStream.SendMsg(m) +} + +func (x *flowServiceEventsServer) Recv() (*FlowEvent, error) { + m := new(FlowEvent) + if err := x.ServerStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil +} + +// FlowService_ServiceDesc is the grpc.ServiceDesc for FlowService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var FlowService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "flow.FlowService", + HandlerType: (*FlowServiceServer)(nil), + Methods: []grpc.MethodDesc{}, + Streams: []grpc.StreamDesc{ + { + StreamName: "Events", + Handler: _FlowService_Events_Handler, + ServerStreams: true, + ClientStreams: true, + }, + }, + Metadata: "flow.proto", +} diff --git a/flow/proto/generate.sh b/flow/proto/generate.sh new file mode 100755 index 000000000..6bbf78e61 --- /dev/null +++ b/flow/proto/generate.sh @@ -0,0 +1,17 @@ +#!/bin/bash +set -e + +if ! which realpath > /dev/null 2>&1 +then + echo realpath is not installed + echo run: brew install coreutils + exit 1 +fi + +old_pwd=$(pwd) +script_path=$(dirname $(realpath "$0")) +cd "$script_path" +go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.26 +go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.1 +protoc -I ./ ./flow.proto --go_out=../ --go-grpc_out=../ +cd "$old_pwd" diff --git a/go.mod b/go.mod index 05a155889..f1c514f9f 100644 --- a/go.mod +++ b/go.mod @@ -25,7 +25,7 @@ require ( golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 golang.zx2c4.com/wireguard/windows v0.5.3 google.golang.org/grpc v1.64.1 - google.golang.org/protobuf v1.34.2 + google.golang.org/protobuf v1.35.2 gopkg.in/natefinch/lumberjack.v2 v2.0.0 ) @@ -40,7 +40,9 @@ require ( github.com/coreos/go-iptables v0.7.0 github.com/creack/pty v1.1.18 github.com/davecgh/go-spew v1.1.1 - github.com/eko/gocache/v3 v3.1.1 + github.com/eko/gocache/lib/v4 v4.2.0 + github.com/eko/gocache/store/go_cache/v4 v4.2.2 + github.com/eko/gocache/store/redis/v4 v4.2.2 github.com/fsnotify/fsnotify v1.7.0 github.com/gliderlabs/ssh v0.3.8 github.com/godbus/dbus/v5 v5.1.0 @@ -60,7 +62,7 @@ require ( github.com/miekg/dns v1.1.59 github.com/mitchellh/hashstructure/v2 v2.0.2 github.com/nadoo/ipset v0.5.0 - github.com/netbirdio/management-integrations/integrations v0.0.0-20250307154727-58660ea9a141 + github.com/netbirdio/management-integrations/integrations v0.0.0-20250320152138-69b93e4ef939 github.com/netbirdio/signal-dispatcher/dispatcher v0.0.0-20241010133937-e0df50df217d github.com/okta/okta-sdk-golang/v2 v2.18.0 github.com/oschwald/maxminddb-golang v1.12.0 @@ -73,15 +75,20 @@ require ( github.com/pion/turn/v3 v3.0.1 github.com/prometheus/client_golang v1.19.1 github.com/quic-go/quic-go v0.48.2 + github.com/redis/go-redis/v9 v9.7.1 github.com/rs/xid v1.3.0 github.com/shirou/gopsutil/v3 v3.24.4 github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 - github.com/stretchr/testify v1.9.0 + github.com/stretchr/testify v1.10.0 github.com/testcontainers/testcontainers-go v0.31.0 github.com/testcontainers/testcontainers-go/modules/mysql v0.31.0 github.com/testcontainers/testcontainers-go/modules/postgres v0.31.0 + github.com/testcontainers/testcontainers-go/modules/redis v0.31.0 github.com/things-go/go-socks5 v0.0.4 + github.com/ti-mo/conntrack v0.5.1 + github.com/ti-mo/netfilter v0.5.2 + github.com/vmihailenco/msgpack/v5 v5.4.1 github.com/yusufpapurcu/wmi v1.2.4 github.com/zcalusic/sysinfo v1.1.3 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 @@ -116,7 +123,6 @@ require ( github.com/BurntSushi/toml v1.4.0 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/Microsoft/hcsshim v0.12.3 // indirect - github.com/XiaoMi/pegasus-go-client v0.0.0-20210427083443-f3b6b08bc4c2 // indirect github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect github.com/aws/aws-sdk-go-v2 v1.30.3 // indirect github.com/aws/aws-sdk-go-v2/config v1.27.27 // indirect @@ -133,13 +139,12 @@ require ( github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 // indirect github.com/aws/smithy-go v1.20.3 // indirect github.com/beorn7/perks v1.0.1 // indirect - github.com/bradfitz/gomemcache v0.0.0-20220106215444-fb4bf637b56d // indirect github.com/caddyserver/zerossl v0.1.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/containerd/containerd v1.7.16 // indirect + github.com/containerd/containerd v1.7.26 // indirect github.com/containerd/log v0.1.0 // indirect - github.com/cpuguy83/dockercfg v0.3.1 // indirect - github.com/dgraph-io/ristretto v0.1.1 // indirect + github.com/containerd/platforms v0.2.1 // indirect + github.com/cpuguy83/dockercfg v0.3.2 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/docker v26.1.5+incompatible // indirect @@ -152,10 +157,9 @@ require ( github.com/fyne-io/image v0.0.0-20220602074514-4956b0afb3d2 // indirect github.com/go-gl/gl v0.0.0-20211210172815-726fda9656d6 // indirect github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a // indirect - github.com/go-logr/logr v1.4.1 // indirect + github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.3.0 // indirect - github.com/go-redis/redis/v8 v8.11.5 // indirect github.com/go-sql-driver/mysql v1.8.1 // indirect github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect github.com/go-text/render v0.2.0 // indirect @@ -193,7 +197,8 @@ require ( github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/patternmatcher v0.6.0 // indirect github.com/moby/sys/sequential v0.5.0 // indirect - github.com/moby/sys/user v0.1.0 // indirect + github.com/moby/sys/user v0.3.0 // indirect + github.com/moby/sys/userns v0.1.0 // indirect github.com/moby/term v0.5.0 // indirect github.com/morikuni/aec v1.0.0 // indirect github.com/nicksnyder/go-i18n/v2 v2.4.0 // indirect @@ -201,7 +206,6 @@ require ( github.com/onsi/ginkgo/v2 v2.9.5 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0 // indirect - github.com/pegasus-kv/thrift v0.13.0 // indirect github.com/pion/dtls/v2 v2.2.10 // indirect github.com/pion/mdns v0.0.12 // indirect github.com/pion/transport/v2 v2.2.4 // indirect @@ -213,13 +217,13 @@ require ( github.com/prometheus/procfs v0.15.0 // indirect github.com/rymdport/portal v0.3.0 // indirect github.com/shoenig/go-m1cpu v0.1.6 // indirect - github.com/spf13/cast v1.5.0 // indirect github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c // indirect github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/tklauser/go-sysconf v0.3.14 // indirect github.com/tklauser/numcpus v0.8.0 // indirect github.com/vishvananda/netns v0.0.4 // indirect + github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/yuin/goldmark v1.7.1 // indirect github.com/zeebo/blake3 v0.2.3 // indirect go.opencensus.io v0.24.0 // indirect @@ -238,8 +242,6 @@ require ( google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // 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 - k8s.io/apimachinery v0.26.2 // indirect ) replace github.com/kardianos/service => github.com/netbirdio/service v0.0.0-20240911161631-f62744f42502 diff --git a/go.sum b/go.sum index af7012f13..fb0189709 100644 --- a/go.sum +++ b/go.sum @@ -66,15 +66,8 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERo github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/Microsoft/hcsshim v0.12.3 h1:LS9NXqXhMoqNCplK1ApmVSfB4UnVLRDWRapB6EIlxE0= github.com/Microsoft/hcsshim v0.12.3/go.mod h1:Iyl1WVpZzr+UkzjekHZbV8o5Z9ZkxNGx6CtY2Qg/JVQ= -github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= -github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= -github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/TheJumpCloud/jcapi-go v3.0.0+incompatible h1:hqcTK6ZISdip65SR792lwYJTa/axESA0889D3UlZbLo= github.com/TheJumpCloud/jcapi-go v3.0.0+incompatible/go.mod h1:6B1nuc1MUs6c62ODZDl7hVE5Pv7O2XGSkgg2olnq34I= -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/allegro/bigcache/v3 v3.0.2 h1:AKZCw+5eAaVyNTBmI2fgyPVJhHkdWder3O9IrprcQfI= -github.com/allegro/bigcache/v3 v3.0.2/go.mod h1:aPyh7jEvrog9zAwx5N7+JUQX5dZTSGpxF1LAR4dr35I= 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= @@ -113,19 +106,19 @@ 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/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM= -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/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= +github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= 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/caddyserver/certmagic v0.21.3 h1:pqRRry3yuB4CWBVq9+cUqu+Y6E2z8TswbhNx1AZeYm0= github.com/caddyserver/certmagic v0.21.3/go.mod h1:Zq6pklO9nVRl3DIFUw9gVUfXKdpc/0qwTUAQMBlfgtI= github.com/caddyserver/zerossl v0.1.3 h1:onS+pxp3M8HnHpN5MMbOMyNjmTheJyWRaZYwn+YTAyA= github.com/caddyserver/zerossl v0.1.3/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4= -github.com/cenkalti/backoff/v4 v4.1.0/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= @@ -139,31 +132,27 @@ github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnht github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo= github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= -github.com/containerd/containerd v1.7.16 h1:7Zsfe8Fkj4Wi2My6DXGQ87hiqIrmOXolm72ZEkFU5Mg= -github.com/containerd/containerd v1.7.16/go.mod h1:NL49g7A/Fui7ccmxV6zkBWwqMgmMxFWzujYCc+JLt7k= +github.com/containerd/containerd v1.7.26 h1:3cs8K2RHlMQaPifLqgRyI4VBkoldNdEw62cb7qQga7k= +github.com/containerd/containerd v1.7.26/go.mod h1:m4JU0E+h0ebbo9yXD7Hyt+sWnc8tChm7MudCjj4jRvQ= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= -github.com/coocood/freecache v1.2.1 h1:/v1CqMq45NFH9mp/Pt142reundeBM0dVUD3osQBeu/U= -github.com/coocood/freecache v1.2.1/go.mod h1:RBUWa/Cy+OHdfTGFEhEuE1pMCMX51Ncizj7rthiQ3vk= +github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= +github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= github.com/coreos/go-iptables v0.7.0 h1:XWM3V+MPRr5/q51NuWSgU0fqMad64Zyxs8ZUoMsamr8= github.com/coreos/go-iptables v0.7.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/cpuguy83/dockercfg v0.3.1 h1:/FpZ+JaygUR/lZP2NlFI2DVfrOEMAIKP5wWEJdoYe9E= -github.com/cpuguy83/dockercfg v0.3.1/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/cunicu/circl v0.0.0-20230801113412-fec58fc7b5f6 h1:/DS5cDX3FJdl+XaN2D7XAwFpuanTxnp52DBLZAaJKx0= github.com/cunicu/circl v0.0.0-20230801113412-fec58fc7b5f6/go.mod h1:+CauBF6R70Jqcyl8N2hC8pAXYbWkGIezuSbuGLtRhnw= -github.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8= -github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA= -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/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= @@ -174,13 +163,12 @@ github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= -github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= -github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= -github.com/eko/gocache/v3 v3.1.1 h1:r3CBwLnqPkcK56h9Do2CWw1kZ4TeKK0wDE1Oo/YZnhs= -github.com/eko/gocache/v3 v3.1.1/go.mod h1:UpP/LyHAioP/a/dizgl0MpgZ3A3CkS4NbG/mWkGTQ9M= -github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= -github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/eko/gocache/lib/v4 v4.2.0 h1:MNykyi5Xw+5Wu3+PUrvtOCaKSZM1nUSVftbzmeC7Yuw= +github.com/eko/gocache/lib/v4 v4.2.0/go.mod h1:7ViVmbU+CzDHzRpmB4SXKyyzyuJ8A3UW3/cszpcqB4M= +github.com/eko/gocache/store/go_cache/v4 v4.2.2 h1:tAI9nl6TLoJyKG1ujF0CS0n/IgTEMl+NivxtR5R3/hw= +github.com/eko/gocache/store/go_cache/v4 v4.2.2/go.mod h1:T9zkHokzr8K9EiC7RfMbDg6HSwaV6rv3UdcNu13SGcA= +github.com/eko/gocache/store/redis/v4 v4.2.2 h1:Thw31fzGuH3WzJywsdbMivOmP550D6JS7GDHhvCJPA0= +github.com/eko/gocache/store/redis/v4 v4.2.2/go.mod h1:LaTxLKx9TG/YUEybQvPMij++D7PBTIJ4+pzvk0ykz0w= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= @@ -188,16 +176,11 @@ github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5y github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g= github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= -github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= -github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= -github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= github.com/fredbi/uri v1.1.0 h1:OqLpTXtyRg9ABReqvDGdJPqZUxs8cyBDOMXBbskCaB8= github.com/fredbi/uri v1.1.0/go.mod h1:aYTUoAXBOq7BLfVJ8GnKmfcuURosB1xyHDIfWeC/iW4= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= @@ -210,7 +193,6 @@ github.com/fyne-io/glfw-js v0.0.0-20241126112943-313d8a0fe1d0 h1:/1YRWFv9bAWkoo3 github.com/fyne-io/glfw-js v0.0.0-20241126112943-313d8a0fe1d0/go.mod h1:gsGA2dotD4v0SR6PmPCYvS9JuOeMwAtmfvDE7mbYXMY= github.com/fyne-io/image v0.0.0-20220602074514-4956b0afb3d2 h1:hnLq+55b7Zh7/2IRzWCpiTcAvjv/P8ERF+N7+xXbZhk= github.com/fyne-io/image v0.0.0-20220602074514-4956b0afb3d2/go.mod h1:eO7W361vmlPOrykIg+Rsh1SZ3tQBaOsfzZhsIOb/Lm0= -github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 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= @@ -223,19 +205,14 @@ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a h1:vxnBhFDDT+xzxf1jTJKMKZw3H0swfWk9RpWbBbDK5+0= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= -github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= -github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0= -github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg= -github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc= -github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I= github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM= github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY= github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= @@ -257,15 +234,11 @@ github.com/go-text/typesetting-utils v0.0.0-20240317173224-1986cbe96c66/go.mod h github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/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/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/glog v1.2.0 h1:uCdmnmatrKCgMBlM4rMuJZWOkPDqdbZPnrMXDY4gI68= -github.com/golang/glog v1.2.0/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= -github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 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= @@ -281,7 +254,6 @@ github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71 github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= 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= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -322,7 +294,6 @@ github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/gofuzz v0.0.0-20161122191042-44d81051d367/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= @@ -347,7 +318,6 @@ github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8I github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= -github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -357,7 +327,6 @@ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gax-go/v2 v2.12.3 h1:5/zPPDvw8Q1SuXjrqrZslrqT7dL/uJT2CQii/cLCKqA= github.com/googleapis/gax-go/v2 v2.12.3/go.mod h1:AKloxT6GtNbaLm8QTNSidHUVsHYcBHwWRvkNFJUQcS4= -github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= github.com/gopacket/gopacket v1.1.1 h1:zbx9F9d6A7sWNkFKrvMBZTfGgxFoY4NgUudFVVHMfcw= github.com/gopacket/gopacket v1.1.1/go.mod h1:HavMeONEl7W9036of9LbSWoonqhH7HA1+ZRO+rMIvFs= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= @@ -431,19 +400,15 @@ github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGw github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA= github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= -github.com/json-iterator/go v0.0.0-20180612202835-f2b4162afba3/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/jsummers/gobmp v0.0.0-20151104160322-e2ba15ffa76e h1:LvL4XsI70QxOGHed6yhQtAU34Kx3Qq2wwBzGFKY8zKk= github.com/jsummers/gobmp v0.0.0-20151104160322-e2ba15ffa76e/go.mod h1:kLgvv7o6UM+0QSf0QjAse3wReFDsb9qbZJdfexWlrQw= -github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= -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.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU= @@ -451,7 +416,6 @@ github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ib github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -473,7 +437,6 @@ github.com/lufia/plan9stats v0.0.0-20240513124658-fba389f38bae/go.mod h1:ilwx/Dt github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= -github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= @@ -508,19 +471,17 @@ github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkV github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc= github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo= -github.com/moby/sys/user v0.1.0 h1:WmZ93f5Ux6het5iituh9x2zAG7NFY9Aqi49jjE1PaQg= -github.com/moby/sys/user v0.1.0/go.mod h1:fKJhFOnsCN6xZ5gSfbM6zaHGgDJMrqt9/reuj4T7MmU= +github.com/moby/sys/user v0.3.0 h1:9ni5DlcW5an3SvRSx4MouotOygvzaXbaSrc/wGDFWPo= +github.com/moby/sys/user v0.3.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= +github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= +github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v0.0.0-20180320133207-05fbef0ca5da/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= -github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/nadoo/ipset v0.5.0 h1:5GJUAuZ7ITQQQGne5J96AmFjRtI8Avlbk6CabzYWVUc= github.com/nadoo/ipset v0.5.0/go.mod h1:rYF5DQLRGGoQ8ZSWeK+6eX5amAuPqwFkWjhQlEITGJQ= github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo= @@ -529,8 +490,8 @@ github.com/netbirdio/go-netroute v0.0.0-20240611143515-f59b0e1d3944 h1:TDtJKmM6S github.com/netbirdio/go-netroute v0.0.0-20240611143515-f59b0e1d3944/go.mod h1:sHA6TRxjQ6RLbnI+3R4DZo2Eseg/iKiPRfNmcuNySVQ= github.com/netbirdio/ice/v3 v3.0.0-20240315174635-e72a50fcb64e h1:PURA50S8u4mF6RrkYYCAvvPCixhqqEiEy3Ej6avh04c= github.com/netbirdio/ice/v3 v3.0.0-20240315174635-e72a50fcb64e/go.mod h1:YMLU7qbKfVjmEv7EoZPIVEI+kNYxWCdPK3VS0BU+U4Q= -github.com/netbirdio/management-integrations/integrations v0.0.0-20250307154727-58660ea9a141 h1:GZUkZd9ZMBGahNt+AbYYvZrSMpOnaBLjHiBbloOE7sc= -github.com/netbirdio/management-integrations/integrations v0.0.0-20250307154727-58660ea9a141/go.mod h1:A5QUfEZb5J3tw8EUB9e3q7Bgd/JtC0WlFT1onf3HPCY= +github.com/netbirdio/management-integrations/integrations v0.0.0-20250320152138-69b93e4ef939 h1:OsLDdb6ekNaCVSyD+omhio2DECfEqLjCA1zo4HrgGqU= +github.com/netbirdio/management-integrations/integrations v0.0.0-20250320152138-69b93e4ef939/go.mod h1:3LvBPnW+i06K9fQr1SYwsbhvnxQHtIC8vvO4PjLmmy0= github.com/netbirdio/service v0.0.0-20240911161631-f62744f42502 h1:3tHlFmhTdX9axERMVN63dqyFqnvuD+EMJHzM7mNGON8= github.com/netbirdio/service v0.0.0-20240911161631-f62744f42502/go.mod h1:CIMRFEJVL+0DS1a3Nx06NaMn4Dz63Ng6O7dl0qH0zVM= github.com/netbirdio/signal-dispatcher/dispatcher v0.0.0-20241010133937-e0df50df217d h1:bRq5TKgC7Iq20pDiuC54yXaWnAVeS5PdGpSokFTlR28= @@ -544,16 +505,12 @@ github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/okta/okta-sdk-golang/v2 v2.18.0 h1:cfDasMb7CShbZvOrF6n+DnLevWwiHgedWMGJ8M8xKDc= github.com/okta/okta-sdk-golang/v2 v2.18.0/go.mod h1:dz30v3ctAiMb7jpsCngGfQUAEGm1/NsWT92uTbNDQIs= -github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q= github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k= -github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= -github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE= @@ -567,8 +524,6 @@ github.com/oschwald/maxminddb-golang v1.12.0/go.mod h1:q0Nob5lTCqyQ8WT6FYgS1L7PX github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= -github.com/pegasus-kv/thrift v0.13.0 h1:4ESwaNoHImfbHa9RUGJiJZ4hrxorihZHk5aarYwY8d4= -github.com/pegasus-kv/thrift v0.13.0/go.mod h1:Gl9NT/WHG6ABm6NsrbfE8LiJN0sAyneCrvB4qN4NPqQ= github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml/v2 v2.0.9 h1:uH2qQXheeefCCkuBBSLi7jCiSmj3VRh2+Goq2N7Xxu0= @@ -599,7 +554,6 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA= github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo= github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= -github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= @@ -617,6 +571,8 @@ github.com/prometheus/procfs v0.15.0 h1:A82kmvXJq2jTu5YUhSGNlYoxh85zLnKgPz4bMZgI github.com/prometheus/procfs v0.15.0/go.mod h1:Y0RJ/Y5g5wJpkTisOtqwDSo4HwhGmLB4VQSw2sQJLHk= github.com/quic-go/quic-go v0.48.2 h1:wsKXZPeGWpMpCGSWqOcqpW2wZYic/8T3aqiOID0/KWE= github.com/quic-go/quic-go v0.48.2/go.mod h1:yBgs3rWBOADpga7F+jJsb6Ybg1LSYiQvwWlLX+/6HMs= +github.com/redis/go-redis/v9 v9.7.1 h1:4LhKRCIduqXqtvCUlaq9c8bdHOkICjDMrr1+Zb3osAc= +github.com/redis/go-redis/v9 v9.7.1/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw= 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= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= @@ -641,28 +597,20 @@ github.com/shurcooL/go v0.0.0-20200502201357-93f07166e636/go.mod h1:TDJrrUr11Vxr github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/shurcooL/vfsgen v0.0.0-20200824052919-0d455de96546/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw= -github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA= github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= -github.com/smartystreets/assertions v1.13.0 h1:Dx1kYM01xsSqKPno3aqLnrwac2LetPvN23diwyr69Qs= -github.com/smartystreets/assertions v1.13.0/go.mod h1:wDmR7qL282YbGsPy6H/yAsesrxfxaaSlJazyFLYVFx8= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= -github.com/smartystreets/goconvey v1.7.2 h1:9RBaZCeXEQ3UselpuwUQHltGVXvdwm6cv1hgR6gDIPg= -github.com/smartystreets/goconvey v1.7.2/go.mod h1:Vw0tHAZW6lzCRk3xgdin6fKYcG+G3Pg9vgXWeJpQFMM= github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 h1:TG/diQgUe0pntT/2D9tmUCz4VNwm9MfrtPr0SU2qSX8= github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8/go.mod h1:P5HUIBuIWKbyjl083/loAegFkfbFNx5i2qEP4CNbm7E= github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= -github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk= github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= -github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns= @@ -671,12 +619,10 @@ github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c/go.mod h1:cNQ3dwVJtS github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef h1:Ch6Q+AZUxDBCVqdkI8FSpFyZDtCVBc2VmejdNrm5rRQ= github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef/go.mod h1:nXTWP6+gD5+LUJ8krVhhoeHjvHTutPxMYl5SvkcnJNE= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= -github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -688,8 +634,9 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/testcontainers/testcontainers-go v0.31.0 h1:W0VwIhcEVhRflwL9as3dhY6jXjVCA27AkmbnZ+UTh3U= github.com/testcontainers/testcontainers-go v0.31.0/go.mod h1:D2lAoA0zUFiSY+eAflqK5mcUx/A5hrrORaEQrd0SefI= @@ -697,8 +644,14 @@ github.com/testcontainers/testcontainers-go/modules/mysql v0.31.0 h1:790+S8ewZYC github.com/testcontainers/testcontainers-go/modules/mysql v0.31.0/go.mod h1:REFmO+lSG9S6uSBEwIMZCxeI36uhScjTwChYADeO3JA= github.com/testcontainers/testcontainers-go/modules/postgres v0.31.0 h1:isAwFS3KNKRbJMbWv+wolWqOFUECmjYZ+sIRZCIBc/E= github.com/testcontainers/testcontainers-go/modules/postgres v0.31.0/go.mod h1:ZNYY8vumNCEG9YI59A9d6/YaMY49uwRhmeU563EzFGw= +github.com/testcontainers/testcontainers-go/modules/redis v0.31.0 h1:5X6GhOdLwV86zcW8sxppJAMtsDC9u+r9tb3biBc9GKs= +github.com/testcontainers/testcontainers-go/modules/redis v0.31.0/go.mod h1:dKi5xBwy1k4u8yb3saQHu7hMEJwewHXxzbcMAuLiA6o= github.com/things-go/go-socks5 v0.0.4 h1:jMQjIc+qhD4z9cITOMnBiwo9dDmpGuXmBlkRFrl/qD0= github.com/things-go/go-socks5 v0.0.4/go.mod h1:sh4K6WHrmHZpjxLTCHyYtXYH8OUuD+yZun41NomR1IQ= +github.com/ti-mo/conntrack v0.5.1 h1:opEwkFICnDbQc0BUXl73PHBK0h23jEIFVjXsqvF4GY0= +github.com/ti-mo/conntrack v0.5.1/go.mod h1:T6NCbkMdVU4qEIgwL0njA6lw/iCAbzchlnwm1Sa314o= +github.com/ti-mo/netfilter v0.5.2 h1:CTjOwFuNNeZ9QPdRXt1MZFLFUf84cKtiQutNauHWd40= +github.com/ti-mo/netfilter v0.5.2/go.mod h1:Btx3AtFiOVdHReTDmP9AE+hlkOcvIy403u7BXXbWZKo= github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU= github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY= @@ -712,6 +665,10 @@ github.com/vishvananda/netlink v1.2.1-beta.2/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhg github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8= github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= +github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= +github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= +github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= +github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= 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= @@ -839,7 +796,6 @@ golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91 golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -855,8 +811,6 @@ golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191105084925-a882066a44e0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -919,7 +873,6 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= -golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -927,19 +880,16 @@ golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -983,7 +933,6 @@ golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1002,7 +951,6 @@ golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= -golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1024,8 +972,6 @@ golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxb golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 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= -golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= @@ -1206,8 +1152,8 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= -google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io= +google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -1217,7 +1163,6 @@ 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= gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ= -gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= @@ -1227,9 +1172,6 @@ gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637 h1:yiW+nvdHb9LVqSHQBXfZCieqV4fzYhNBql77zY0ykqs= -gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637/go.mod h1:BHsqpu/nsuzkT5BpiH1EMZPLyqSMM8JbIavyFACoFNk= -gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= @@ -1261,15 +1203,6 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -k8s.io/apimachinery v0.0.0-20191123233150-4c4803ed55e3/go.mod h1:b9qmWdKlLuU9EBh+06BtLcSf/Mu89rWL33naRxs1uZg= -k8s.io/apimachinery v0.26.2 h1:da1u3D5wfR5u2RpLhE/ZtZS2P7QvDgLZTi9wrNZl/tQ= -k8s.io/apimachinery v0.26.2/go.mod h1:ats7nN1LExKHvJ9TmwootT00Yz05MuYqPXEXaVeOy5I= -k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= -k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= -k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= -k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= -sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI= -sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= diff --git a/management/client/client_test.go b/management/client/client_test.go index 73427b38a..65237754c 100644 --- a/management/client/client_test.go +++ b/management/client/client_test.go @@ -8,15 +8,16 @@ import ( "testing" "time" + "github.com/golang/mock/gomock" "github.com/stretchr/testify/require" + "github.com/netbirdio/netbird/client/system" "github.com/netbirdio/netbird/management/server/activity" "github.com/netbirdio/netbird/management/server/integrations/port_forwarding" "github.com/netbirdio/netbird/management/server/settings" "github.com/netbirdio/netbird/management/server/store" "github.com/netbirdio/netbird/management/server/telemetry" - - "github.com/netbirdio/netbird/client/system" + "github.com/netbirdio/netbird/management/server/types" log "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" @@ -73,13 +74,26 @@ func startManagement(t *testing.T) (*grpc.Server, net.Listener) { metrics, err := telemetry.NewDefaultAppMetrics(context.Background()) require.NoError(t, err) - accountManager, err := mgmt.BuildManager(context.Background(), store, peersUpdateManager, nil, "", "netbird.selfhosted", eventStore, nil, false, ia, metrics, port_forwarding.NewControllerMock()) + ctrl := gomock.NewController(t) + t.Cleanup(ctrl.Finish) + settingsMockManager := settings.NewMockManager(ctrl) + settingsMockManager. + EXPECT(). + GetSettings( + gomock.Any(), + gomock.Any(), + gomock.Any(), + ). + Return(&types.Settings{}, nil). + AnyTimes() + + accountManager, err := mgmt.BuildManager(context.Background(), store, peersUpdateManager, nil, "", "netbird.selfhosted", eventStore, nil, false, ia, metrics, port_forwarding.NewControllerMock(), settingsMockManager) if err != nil { t.Fatal(err) } - secretsManager := mgmt.NewTimeBasedAuthSecretsManager(peersUpdateManager, config.TURNConfig, config.Relay) - mgmtServer, err := mgmt.NewServer(context.Background(), config, accountManager, settings.NewManager(store), peersUpdateManager, secretsManager, nil, nil, nil) + secretsManager := mgmt.NewTimeBasedAuthSecretsManager(peersUpdateManager, config.TURNConfig, config.Relay, settingsMockManager) + mgmtServer, err := mgmt.NewServer(context.Background(), config, accountManager, settingsMockManager, peersUpdateManager, secretsManager, nil, nil, nil) if err != nil { t.Fatal(err) } diff --git a/management/client/rest/accounts_test.go b/management/client/rest/accounts_test.go index 621228261..f6d48d874 100644 --- a/management/client/rest/accounts_test.go +++ b/management/client/rest/accounts_test.go @@ -23,7 +23,7 @@ var ( Id: "Test", Settings: api.AccountSettings{ Extra: &api.AccountExtraSettings{ - PeerApprovalEnabled: ptr(false), + PeerApprovalEnabled: false, }, GroupsPropagationEnabled: ptr(true), JwtGroupsEnabled: ptr(false), @@ -141,7 +141,7 @@ func TestAccounts_Integration_List(t *testing.T) { require.NoError(t, err) assert.Len(t, accounts, 1) assert.Equal(t, "bf1c8084-ba50-4ce7-9439-34653001fc3b", accounts[0].Id) - assert.Equal(t, false, *accounts[0].Settings.Extra.PeerApprovalEnabled) + assert.Equal(t, false, accounts[0].Settings.Extra.PeerApprovalEnabled) }) } diff --git a/management/cmd/management.go b/management/cmd/management.go index b8f5f4233..1b2216932 100644 --- a/management/cmd/management.go +++ b/management/cmd/management.go @@ -34,7 +34,6 @@ import ( "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/realip" "github.com/netbirdio/management-integrations/integrations" - "github.com/netbirdio/netbird/management/server/peers" "github.com/netbirdio/netbird/encryption" @@ -203,18 +202,19 @@ var ( } userManager := users.NewManager(store) - settingsManager := settings.NewManager(store) + extraSettingsManager := integrations.NewManager(eventStore) + settingsManager := settings.NewManager(store, userManager, extraSettingsManager) permissionsManager := permissions.NewManager(userManager, settingsManager) peersManager := peers.NewManager(store, permissionsManager) proxyController := integrations.NewController(store) accountManager, err := server.BuildManager(ctx, store, peersUpdateManager, idpManager, mgmtSingleAccModeDomain, - dnsDomain, eventStore, geo, userDeleteFromIDPEnabled, integratedPeerValidator, appMetrics, proxyController) + dnsDomain, eventStore, geo, userDeleteFromIDPEnabled, integratedPeerValidator, appMetrics, proxyController, settingsManager) if err != nil { return fmt.Errorf("failed to build default manager: %v", err) } - secretsManager := server.NewTimeBasedAuthSecretsManager(peersUpdateManager, config.TURNConfig, config.Relay) + secretsManager := server.NewTimeBasedAuthSecretsManager(peersUpdateManager, config.TURNConfig, config.Relay, settingsManager) trustedPeers := config.ReverseProxy.TrustedPeers defaultTrustedPeers := []netip.Prefix{netip.MustParsePrefix("0.0.0.0/0"), netip.MustParsePrefix("::/0")} @@ -276,7 +276,7 @@ var ( routersManager := routers.NewManager(store, permissionsManager, accountManager) networksManager := networks.NewManager(store, permissionsManager, resourcesManager, routersManager, accountManager) - httpAPIHandler, err := nbhttp.NewAPIHandler(ctx, accountManager, networksManager, resourcesManager, routersManager, groupsManager, geo, authManager, appMetrics, integratedPeerValidator, proxyController, permissionsManager, peersManager) + httpAPIHandler, err := nbhttp.NewAPIHandler(ctx, accountManager, networksManager, resourcesManager, routersManager, groupsManager, geo, authManager, appMetrics, integratedPeerValidator, proxyController, permissionsManager, peersManager, settingsManager) if err != nil { return fmt.Errorf("failed creating HTTP API handler: %v", err) diff --git a/management/proto/generate.sh b/management/proto/generate.sh index 64aef891e..207630ae7 100755 --- a/management/proto/generate.sh +++ b/management/proto/generate.sh @@ -14,4 +14,4 @@ cd "$script_path" go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.26 go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.1 protoc -I ./ ./management.proto --go_out=../ --go-grpc_out=../ -cd "$old_pwd" \ No newline at end of file +cd "$old_pwd" diff --git a/management/proto/management.pb.go b/management/proto/management.pb.go index cd69d0565..83780762b 100644 --- a/management/proto/management.pb.go +++ b/management/proto/management.pb.go @@ -9,6 +9,7 @@ package proto import ( protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" + durationpb "google.golang.org/protobuf/types/known/durationpb" timestamppb "google.golang.org/protobuf/types/known/timestamppb" reflect "reflect" sync "sync" @@ -266,7 +267,7 @@ func (x DeviceAuthorizationFlowProvider) Number() protoreflect.EnumNumber { // Deprecated: Use DeviceAuthorizationFlowProvider.Descriptor instead. func (DeviceAuthorizationFlowProvider) EnumDescriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{22, 0} + return file_management_proto_rawDescGZIP(), []int{23, 0} } type EncryptedMessage struct { @@ -1246,6 +1247,7 @@ type NetbirdConfig struct { // a Signal server config Signal *HostConfig `protobuf:"bytes,3,opt,name=signal,proto3" json:"signal,omitempty"` Relay *RelayConfig `protobuf:"bytes,4,opt,name=relay,proto3" json:"relay,omitempty"` + Flow *FlowConfig `protobuf:"bytes,5,opt,name=flow,proto3" json:"flow,omitempty"` } func (x *NetbirdConfig) Reset() { @@ -1308,6 +1310,13 @@ func (x *NetbirdConfig) GetRelay() *RelayConfig { return nil } +func (x *NetbirdConfig) GetFlow() *FlowConfig { + if x != nil { + return x.Flow + } + return nil +} + // HostConfig describes connection properties of some server (e.g. STUN, Signal, Management) type HostConfig struct { state protoimpl.MessageState @@ -1428,6 +1437,112 @@ func (x *RelayConfig) GetTokenSignature() string { return "" } +type FlowConfig struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Url string `protobuf:"bytes,1,opt,name=url,proto3" json:"url,omitempty"` + TokenPayload string `protobuf:"bytes,2,opt,name=tokenPayload,proto3" json:"tokenPayload,omitempty"` + TokenSignature string `protobuf:"bytes,3,opt,name=tokenSignature,proto3" json:"tokenSignature,omitempty"` + Interval *durationpb.Duration `protobuf:"bytes,4,opt,name=interval,proto3" json:"interval,omitempty"` + Enabled bool `protobuf:"varint,5,opt,name=enabled,proto3" json:"enabled,omitempty"` + // Counters determines if flow packets and bytes counters should be sent + Counters bool `protobuf:"varint,6,opt,name=counters,proto3" json:"counters,omitempty"` + // ExitNodeCollection determines if event collection on exit nodes should be enabled + ExitNodeCollection bool `protobuf:"varint,7,opt,name=exitNodeCollection,proto3" json:"exitNodeCollection,omitempty"` + // DnsCollection determines if DNS event collection should be enabled + DnsCollection bool `protobuf:"varint,8,opt,name=dnsCollection,proto3" json:"dnsCollection,omitempty"` +} + +func (x *FlowConfig) Reset() { + *x = FlowConfig{} + if protoimpl.UnsafeEnabled { + mi := &file_management_proto_msgTypes[16] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *FlowConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FlowConfig) ProtoMessage() {} + +func (x *FlowConfig) ProtoReflect() protoreflect.Message { + mi := &file_management_proto_msgTypes[16] + 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 FlowConfig.ProtoReflect.Descriptor instead. +func (*FlowConfig) Descriptor() ([]byte, []int) { + return file_management_proto_rawDescGZIP(), []int{16} +} + +func (x *FlowConfig) GetUrl() string { + if x != nil { + return x.Url + } + return "" +} + +func (x *FlowConfig) GetTokenPayload() string { + if x != nil { + return x.TokenPayload + } + return "" +} + +func (x *FlowConfig) GetTokenSignature() string { + if x != nil { + return x.TokenSignature + } + return "" +} + +func (x *FlowConfig) GetInterval() *durationpb.Duration { + if x != nil { + return x.Interval + } + return nil +} + +func (x *FlowConfig) GetEnabled() bool { + if x != nil { + return x.Enabled + } + return false +} + +func (x *FlowConfig) GetCounters() bool { + if x != nil { + return x.Counters + } + return false +} + +func (x *FlowConfig) GetExitNodeCollection() bool { + if x != nil { + return x.ExitNodeCollection + } + return false +} + +func (x *FlowConfig) GetDnsCollection() bool { + if x != nil { + return x.DnsCollection + } + return false +} + // ProtectedHostConfig is similar to HostConfig but has additional user and password // Mostly used for TURN servers type ProtectedHostConfig struct { @@ -1443,7 +1558,7 @@ type ProtectedHostConfig struct { func (x *ProtectedHostConfig) Reset() { *x = ProtectedHostConfig{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[16] + mi := &file_management_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1456,7 +1571,7 @@ func (x *ProtectedHostConfig) String() string { func (*ProtectedHostConfig) ProtoMessage() {} func (x *ProtectedHostConfig) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[16] + mi := &file_management_proto_msgTypes[17] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1469,7 +1584,7 @@ func (x *ProtectedHostConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use ProtectedHostConfig.ProtoReflect.Descriptor instead. func (*ProtectedHostConfig) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{16} + return file_management_proto_rawDescGZIP(), []int{17} } func (x *ProtectedHostConfig) GetHostConfig() *HostConfig { @@ -1514,7 +1629,7 @@ type PeerConfig struct { func (x *PeerConfig) Reset() { *x = PeerConfig{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[17] + mi := &file_management_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1527,7 +1642,7 @@ func (x *PeerConfig) String() string { func (*PeerConfig) ProtoMessage() {} func (x *PeerConfig) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[17] + mi := &file_management_proto_msgTypes[18] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1540,7 +1655,7 @@ func (x *PeerConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use PeerConfig.ProtoReflect.Descriptor instead. func (*PeerConfig) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{17} + return file_management_proto_rawDescGZIP(), []int{18} } func (x *PeerConfig) GetAddress() string { @@ -1614,7 +1729,7 @@ type NetworkMap struct { func (x *NetworkMap) Reset() { *x = NetworkMap{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[18] + mi := &file_management_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1627,7 +1742,7 @@ func (x *NetworkMap) String() string { func (*NetworkMap) ProtoMessage() {} func (x *NetworkMap) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[18] + mi := &file_management_proto_msgTypes[19] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1640,7 +1755,7 @@ func (x *NetworkMap) ProtoReflect() protoreflect.Message { // Deprecated: Use NetworkMap.ProtoReflect.Descriptor instead. func (*NetworkMap) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{18} + return file_management_proto_rawDescGZIP(), []int{19} } func (x *NetworkMap) GetSerial() uint64 { @@ -1747,7 +1862,7 @@ type RemotePeerConfig struct { func (x *RemotePeerConfig) Reset() { *x = RemotePeerConfig{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[19] + mi := &file_management_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1760,7 +1875,7 @@ func (x *RemotePeerConfig) String() string { func (*RemotePeerConfig) ProtoMessage() {} func (x *RemotePeerConfig) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[19] + mi := &file_management_proto_msgTypes[20] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1773,7 +1888,7 @@ func (x *RemotePeerConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use RemotePeerConfig.ProtoReflect.Descriptor instead. func (*RemotePeerConfig) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{19} + return file_management_proto_rawDescGZIP(), []int{20} } func (x *RemotePeerConfig) GetWgPubKey() string { @@ -1820,7 +1935,7 @@ type SSHConfig struct { func (x *SSHConfig) Reset() { *x = SSHConfig{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[20] + mi := &file_management_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1833,7 +1948,7 @@ func (x *SSHConfig) String() string { func (*SSHConfig) ProtoMessage() {} func (x *SSHConfig) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[20] + mi := &file_management_proto_msgTypes[21] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1846,7 +1961,7 @@ func (x *SSHConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use SSHConfig.ProtoReflect.Descriptor instead. func (*SSHConfig) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{20} + return file_management_proto_rawDescGZIP(), []int{21} } func (x *SSHConfig) GetSshEnabled() bool { @@ -1873,7 +1988,7 @@ type DeviceAuthorizationFlowRequest struct { func (x *DeviceAuthorizationFlowRequest) Reset() { *x = DeviceAuthorizationFlowRequest{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[21] + mi := &file_management_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1886,7 +2001,7 @@ func (x *DeviceAuthorizationFlowRequest) String() string { func (*DeviceAuthorizationFlowRequest) ProtoMessage() {} func (x *DeviceAuthorizationFlowRequest) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[21] + mi := &file_management_proto_msgTypes[22] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1899,7 +2014,7 @@ func (x *DeviceAuthorizationFlowRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DeviceAuthorizationFlowRequest.ProtoReflect.Descriptor instead. func (*DeviceAuthorizationFlowRequest) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{21} + return file_management_proto_rawDescGZIP(), []int{22} } // DeviceAuthorizationFlow represents Device Authorization Flow information @@ -1918,7 +2033,7 @@ type DeviceAuthorizationFlow struct { func (x *DeviceAuthorizationFlow) Reset() { *x = DeviceAuthorizationFlow{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[22] + mi := &file_management_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1931,7 +2046,7 @@ func (x *DeviceAuthorizationFlow) String() string { func (*DeviceAuthorizationFlow) ProtoMessage() {} func (x *DeviceAuthorizationFlow) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[22] + mi := &file_management_proto_msgTypes[23] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1944,7 +2059,7 @@ func (x *DeviceAuthorizationFlow) ProtoReflect() protoreflect.Message { // Deprecated: Use DeviceAuthorizationFlow.ProtoReflect.Descriptor instead. func (*DeviceAuthorizationFlow) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{22} + return file_management_proto_rawDescGZIP(), []int{23} } func (x *DeviceAuthorizationFlow) GetProvider() DeviceAuthorizationFlowProvider { @@ -1971,7 +2086,7 @@ type PKCEAuthorizationFlowRequest struct { func (x *PKCEAuthorizationFlowRequest) Reset() { *x = PKCEAuthorizationFlowRequest{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[23] + mi := &file_management_proto_msgTypes[24] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1984,7 +2099,7 @@ func (x *PKCEAuthorizationFlowRequest) String() string { func (*PKCEAuthorizationFlowRequest) ProtoMessage() {} func (x *PKCEAuthorizationFlowRequest) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[23] + mi := &file_management_proto_msgTypes[24] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1997,7 +2112,7 @@ func (x *PKCEAuthorizationFlowRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use PKCEAuthorizationFlowRequest.ProtoReflect.Descriptor instead. func (*PKCEAuthorizationFlowRequest) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{23} + return file_management_proto_rawDescGZIP(), []int{24} } // PKCEAuthorizationFlow represents Authorization Code Flow information @@ -2014,7 +2129,7 @@ type PKCEAuthorizationFlow struct { func (x *PKCEAuthorizationFlow) Reset() { *x = PKCEAuthorizationFlow{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[24] + mi := &file_management_proto_msgTypes[25] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2027,7 +2142,7 @@ func (x *PKCEAuthorizationFlow) String() string { func (*PKCEAuthorizationFlow) ProtoMessage() {} func (x *PKCEAuthorizationFlow) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[24] + mi := &file_management_proto_msgTypes[25] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2040,7 +2155,7 @@ func (x *PKCEAuthorizationFlow) ProtoReflect() protoreflect.Message { // Deprecated: Use PKCEAuthorizationFlow.ProtoReflect.Descriptor instead. func (*PKCEAuthorizationFlow) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{24} + return file_management_proto_rawDescGZIP(), []int{25} } func (x *PKCEAuthorizationFlow) GetProviderConfig() *ProviderConfig { @@ -2082,7 +2197,7 @@ type ProviderConfig struct { func (x *ProviderConfig) Reset() { *x = ProviderConfig{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[25] + mi := &file_management_proto_msgTypes[26] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2095,7 +2210,7 @@ func (x *ProviderConfig) String() string { func (*ProviderConfig) ProtoMessage() {} func (x *ProviderConfig) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[25] + mi := &file_management_proto_msgTypes[26] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2108,7 +2223,7 @@ func (x *ProviderConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use ProviderConfig.ProtoReflect.Descriptor instead. func (*ProviderConfig) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{25} + return file_management_proto_rawDescGZIP(), []int{26} } func (x *ProviderConfig) GetClientID() string { @@ -2201,7 +2316,7 @@ type Route struct { func (x *Route) Reset() { *x = Route{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[26] + mi := &file_management_proto_msgTypes[27] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2214,7 +2329,7 @@ func (x *Route) String() string { func (*Route) ProtoMessage() {} func (x *Route) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[26] + mi := &file_management_proto_msgTypes[27] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2227,7 +2342,7 @@ func (x *Route) ProtoReflect() protoreflect.Message { // Deprecated: Use Route.ProtoReflect.Descriptor instead. func (*Route) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{26} + return file_management_proto_rawDescGZIP(), []int{27} } func (x *Route) GetID() string { @@ -2307,7 +2422,7 @@ type DNSConfig struct { func (x *DNSConfig) Reset() { *x = DNSConfig{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[27] + mi := &file_management_proto_msgTypes[28] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2320,7 +2435,7 @@ func (x *DNSConfig) String() string { func (*DNSConfig) ProtoMessage() {} func (x *DNSConfig) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[27] + mi := &file_management_proto_msgTypes[28] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2333,7 +2448,7 @@ func (x *DNSConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use DNSConfig.ProtoReflect.Descriptor instead. func (*DNSConfig) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{27} + return file_management_proto_rawDescGZIP(), []int{28} } func (x *DNSConfig) GetServiceEnable() bool { @@ -2370,7 +2485,7 @@ type CustomZone struct { func (x *CustomZone) Reset() { *x = CustomZone{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[28] + mi := &file_management_proto_msgTypes[29] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2383,7 +2498,7 @@ func (x *CustomZone) String() string { func (*CustomZone) ProtoMessage() {} func (x *CustomZone) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[28] + mi := &file_management_proto_msgTypes[29] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2396,7 +2511,7 @@ func (x *CustomZone) ProtoReflect() protoreflect.Message { // Deprecated: Use CustomZone.ProtoReflect.Descriptor instead. func (*CustomZone) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{28} + return file_management_proto_rawDescGZIP(), []int{29} } func (x *CustomZone) GetDomain() string { @@ -2429,7 +2544,7 @@ type SimpleRecord struct { func (x *SimpleRecord) Reset() { *x = SimpleRecord{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[29] + mi := &file_management_proto_msgTypes[30] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2442,7 +2557,7 @@ func (x *SimpleRecord) String() string { func (*SimpleRecord) ProtoMessage() {} func (x *SimpleRecord) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[29] + mi := &file_management_proto_msgTypes[30] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2455,7 +2570,7 @@ func (x *SimpleRecord) ProtoReflect() protoreflect.Message { // Deprecated: Use SimpleRecord.ProtoReflect.Descriptor instead. func (*SimpleRecord) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{29} + return file_management_proto_rawDescGZIP(), []int{30} } func (x *SimpleRecord) GetName() string { @@ -2508,7 +2623,7 @@ type NameServerGroup struct { func (x *NameServerGroup) Reset() { *x = NameServerGroup{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[30] + mi := &file_management_proto_msgTypes[31] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2521,7 +2636,7 @@ func (x *NameServerGroup) String() string { func (*NameServerGroup) ProtoMessage() {} func (x *NameServerGroup) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[30] + mi := &file_management_proto_msgTypes[31] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2534,7 +2649,7 @@ func (x *NameServerGroup) ProtoReflect() protoreflect.Message { // Deprecated: Use NameServerGroup.ProtoReflect.Descriptor instead. func (*NameServerGroup) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{30} + return file_management_proto_rawDescGZIP(), []int{31} } func (x *NameServerGroup) GetNameServers() []*NameServer { @@ -2579,7 +2694,7 @@ type NameServer struct { func (x *NameServer) Reset() { *x = NameServer{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[31] + mi := &file_management_proto_msgTypes[32] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2592,7 +2707,7 @@ func (x *NameServer) String() string { func (*NameServer) ProtoMessage() {} func (x *NameServer) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[31] + mi := &file_management_proto_msgTypes[32] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2605,7 +2720,7 @@ func (x *NameServer) ProtoReflect() protoreflect.Message { // Deprecated: Use NameServer.ProtoReflect.Descriptor instead. func (*NameServer) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{31} + return file_management_proto_rawDescGZIP(), []int{32} } func (x *NameServer) GetIP() string { @@ -2641,12 +2756,14 @@ type FirewallRule struct { Protocol RuleProtocol `protobuf:"varint,4,opt,name=Protocol,proto3,enum=management.RuleProtocol" json:"Protocol,omitempty"` Port string `protobuf:"bytes,5,opt,name=Port,proto3" json:"Port,omitempty"` PortInfo *PortInfo `protobuf:"bytes,6,opt,name=PortInfo,proto3" json:"PortInfo,omitempty"` + // PolicyID is the ID of the policy that this rule belongs to + PolicyID []byte `protobuf:"bytes,7,opt,name=PolicyID,proto3" json:"PolicyID,omitempty"` } func (x *FirewallRule) Reset() { *x = FirewallRule{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[32] + mi := &file_management_proto_msgTypes[33] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2659,7 +2776,7 @@ func (x *FirewallRule) String() string { func (*FirewallRule) ProtoMessage() {} func (x *FirewallRule) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[32] + mi := &file_management_proto_msgTypes[33] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2672,7 +2789,7 @@ func (x *FirewallRule) ProtoReflect() protoreflect.Message { // Deprecated: Use FirewallRule.ProtoReflect.Descriptor instead. func (*FirewallRule) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{32} + return file_management_proto_rawDescGZIP(), []int{33} } func (x *FirewallRule) GetPeerIP() string { @@ -2717,6 +2834,13 @@ func (x *FirewallRule) GetPortInfo() *PortInfo { return nil } +func (x *FirewallRule) GetPolicyID() []byte { + if x != nil { + return x.PolicyID + } + return nil +} + type NetworkAddress struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -2729,7 +2853,7 @@ type NetworkAddress struct { func (x *NetworkAddress) Reset() { *x = NetworkAddress{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[33] + mi := &file_management_proto_msgTypes[34] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2742,7 +2866,7 @@ func (x *NetworkAddress) String() string { func (*NetworkAddress) ProtoMessage() {} func (x *NetworkAddress) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[33] + mi := &file_management_proto_msgTypes[34] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2755,7 +2879,7 @@ func (x *NetworkAddress) ProtoReflect() protoreflect.Message { // Deprecated: Use NetworkAddress.ProtoReflect.Descriptor instead. func (*NetworkAddress) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{33} + return file_management_proto_rawDescGZIP(), []int{34} } func (x *NetworkAddress) GetNetIP() string { @@ -2783,7 +2907,7 @@ type Checks struct { func (x *Checks) Reset() { *x = Checks{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[34] + mi := &file_management_proto_msgTypes[35] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2796,7 +2920,7 @@ func (x *Checks) String() string { func (*Checks) ProtoMessage() {} func (x *Checks) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[34] + mi := &file_management_proto_msgTypes[35] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2809,7 +2933,7 @@ func (x *Checks) ProtoReflect() protoreflect.Message { // Deprecated: Use Checks.ProtoReflect.Descriptor instead. func (*Checks) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{34} + return file_management_proto_rawDescGZIP(), []int{35} } func (x *Checks) GetFiles() []string { @@ -2834,7 +2958,7 @@ type PortInfo struct { func (x *PortInfo) Reset() { *x = PortInfo{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[35] + mi := &file_management_proto_msgTypes[36] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2847,7 +2971,7 @@ func (x *PortInfo) String() string { func (*PortInfo) ProtoMessage() {} func (x *PortInfo) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[35] + mi := &file_management_proto_msgTypes[36] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2860,7 +2984,7 @@ func (x *PortInfo) ProtoReflect() protoreflect.Message { // Deprecated: Use PortInfo.ProtoReflect.Descriptor instead. func (*PortInfo) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{35} + return file_management_proto_rawDescGZIP(), []int{36} } func (m *PortInfo) GetPortSelection() isPortInfo_PortSelection { @@ -2922,12 +3046,14 @@ type RouteFirewallRule struct { Domains []string `protobuf:"bytes,7,rep,name=domains,proto3" json:"domains,omitempty"` // CustomProtocol is a custom protocol ID. CustomProtocol uint32 `protobuf:"varint,8,opt,name=customProtocol,proto3" json:"customProtocol,omitempty"` + // PolicyID is the ID of the policy that this rule belongs to + PolicyID []byte `protobuf:"bytes,9,opt,name=PolicyID,proto3" json:"PolicyID,omitempty"` } func (x *RouteFirewallRule) Reset() { *x = RouteFirewallRule{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[36] + mi := &file_management_proto_msgTypes[37] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2940,7 +3066,7 @@ func (x *RouteFirewallRule) String() string { func (*RouteFirewallRule) ProtoMessage() {} func (x *RouteFirewallRule) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[36] + mi := &file_management_proto_msgTypes[37] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2953,7 +3079,7 @@ func (x *RouteFirewallRule) ProtoReflect() protoreflect.Message { // Deprecated: Use RouteFirewallRule.ProtoReflect.Descriptor instead. func (*RouteFirewallRule) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{36} + return file_management_proto_rawDescGZIP(), []int{37} } func (x *RouteFirewallRule) GetSourceRanges() []string { @@ -3012,6 +3138,13 @@ func (x *RouteFirewallRule) GetCustomProtocol() uint32 { return 0 } +func (x *RouteFirewallRule) GetPolicyID() []byte { + if x != nil { + return x.PolicyID + } + return nil +} + type ForwardingRule struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -3022,7 +3155,6 @@ type ForwardingRule struct { // portInfo is the ingress destination port information, where the traffic arrives in the gateway node DestinationPort *PortInfo `protobuf:"bytes,2,opt,name=destinationPort,proto3" json:"destinationPort,omitempty"` // IP address of the translated address (remote peer) to send traffic to - // todo type pending TranslatedAddress []byte `protobuf:"bytes,3,opt,name=translatedAddress,proto3" json:"translatedAddress,omitempty"` // Translated port information, where the traffic should be forwarded to TranslatedPort *PortInfo `protobuf:"bytes,4,opt,name=translatedPort,proto3" json:"translatedPort,omitempty"` @@ -3031,7 +3163,7 @@ type ForwardingRule struct { func (x *ForwardingRule) Reset() { *x = ForwardingRule{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[37] + mi := &file_management_proto_msgTypes[38] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3044,7 +3176,7 @@ func (x *ForwardingRule) String() string { func (*ForwardingRule) ProtoMessage() {} func (x *ForwardingRule) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[37] + mi := &file_management_proto_msgTypes[38] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3057,7 +3189,7 @@ func (x *ForwardingRule) ProtoReflect() protoreflect.Message { // Deprecated: Use ForwardingRule.ProtoReflect.Descriptor instead. func (*ForwardingRule) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{37} + return file_management_proto_rawDescGZIP(), []int{38} } func (x *ForwardingRule) GetProtocol() RuleProtocol { @@ -3100,7 +3232,7 @@ type PortInfo_Range struct { func (x *PortInfo_Range) Reset() { *x = PortInfo_Range{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[38] + mi := &file_management_proto_msgTypes[39] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3113,7 +3245,7 @@ func (x *PortInfo_Range) String() string { func (*PortInfo_Range) ProtoMessage() {} func (x *PortInfo_Range) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[38] + mi := &file_management_proto_msgTypes[39] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3126,7 +3258,7 @@ func (x *PortInfo_Range) ProtoReflect() protoreflect.Message { // Deprecated: Use PortInfo_Range.ProtoReflect.Descriptor instead. func (*PortInfo_Range) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{35, 0} + return file_management_proto_rawDescGZIP(), []int{36, 0} } func (x *PortInfo_Range) GetStart() uint32 { @@ -3149,7 +3281,9 @@ var file_management_proto_rawDesc = []byte{ 0x0a, 0x10, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0a, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, - 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, + 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, + 0x1e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, + 0x2f, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x5c, 0x0a, 0x10, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 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, @@ -3291,7 +3425,7 @@ var file_management_proto_rawDesc = []byte{ 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x41, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x07, 0x76, 0x65, 0x72, - 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x07, 0x0a, 0x05, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0xd3, 0x01, + 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x07, 0x0a, 0x05, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0xff, 0x01, 0x0a, 0x0d, 0x4e, 0x65, 0x74, 0x62, 0x69, 0x72, 0x64, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x2c, 0x0a, 0x05, 0x73, 0x74, 0x75, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x48, 0x6f, 0x73, 0x74, @@ -3305,315 +3439,340 @@ var file_management_proto_rawDesc = []byte{ 0x67, 0x6e, 0x61, 0x6c, 0x12, 0x2d, 0x0a, 0x05, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x65, 0x6c, 0x61, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x05, 0x72, 0x65, - 0x6c, 0x61, 0x79, 0x22, 0x98, 0x01, 0x0a, 0x0a, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, - 0x69, 0x67, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x69, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x03, 0x75, 0x72, 0x69, 0x12, 0x3b, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1f, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, - 0x65, 0x6e, 0x74, 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x50, - 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, - 0x6c, 0x22, 0x3b, 0x0a, 0x08, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x07, 0x0a, - 0x03, 0x55, 0x44, 0x50, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x54, 0x43, 0x50, 0x10, 0x01, 0x12, - 0x08, 0x0a, 0x04, 0x48, 0x54, 0x54, 0x50, 0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, 0x48, 0x54, 0x54, - 0x50, 0x53, 0x10, 0x03, 0x12, 0x08, 0x0a, 0x04, 0x44, 0x54, 0x4c, 0x53, 0x10, 0x04, 0x22, 0x6d, - 0x0a, 0x0b, 0x52, 0x65, 0x6c, 0x61, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, 0x0a, - 0x04, 0x75, 0x72, 0x6c, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x04, 0x75, 0x72, 0x6c, - 0x73, 0x12, 0x22, 0x0a, 0x0c, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, - 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x50, 0x61, - 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x12, 0x26, 0x0a, 0x0e, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x53, 0x69, - 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x74, - 0x6f, 0x6b, 0x65, 0x6e, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x22, 0x7d, 0x0a, - 0x13, 0x50, 0x72, 0x6f, 0x74, 0x65, 0x63, 0x74, 0x65, 0x64, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x12, 0x36, 0x0a, 0x0a, 0x68, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, - 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, - 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, - 0x52, 0x0a, 0x68, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, 0x0a, 0x04, - 0x75, 0x73, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, - 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, 0xcb, 0x01, 0x0a, - 0x0a, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x18, 0x0a, 0x07, 0x61, - 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x64, - 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x64, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x03, 0x64, 0x6e, 0x73, 0x12, 0x33, 0x0a, 0x09, 0x73, 0x73, 0x68, 0x43, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x18, 0x03, 0x20, 0x01, 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, - 0x12, 0x48, 0x0a, 0x1f, 0x52, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, 0x50, 0x65, 0x65, 0x72, 0x44, - 0x6e, 0x73, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x61, 0x62, - 0x6c, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x1f, 0x52, 0x6f, 0x75, 0x74, 0x69, - 0x6e, 0x67, 0x50, 0x65, 0x65, 0x72, 0x44, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x75, 0x74, - 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x22, 0xb9, 0x05, 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, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, - 0x6e, 0x74, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0a, 0x70, - 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x3e, 0x0a, 0x0b, 0x72, 0x65, 0x6d, - 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x18, 0x03, 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, 0x0b, 0x72, 0x65, - 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x12, 0x2e, 0x0a, 0x12, 0x72, 0x65, 0x6d, - 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x18, - 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x12, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, - 0x72, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x29, 0x0a, 0x06, 0x52, 0x6f, 0x75, - 0x74, 0x65, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, - 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x52, 0x06, 0x52, 0x6f, - 0x75, 0x74, 0x65, 0x73, 0x12, 0x33, 0x0a, 0x09, 0x44, 0x4e, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, - 0x67, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, - 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x44, 0x4e, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x09, - 0x44, 0x4e, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x40, 0x0a, 0x0c, 0x6f, 0x66, 0x66, - 0x6c, 0x69, 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, 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, 0x12, 0x32, 0x0a, 0x14, 0x66, - 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x49, 0x73, 0x45, 0x6d, - 0x70, 0x74, 0x79, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x14, 0x66, 0x69, 0x72, 0x65, 0x77, - 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, - 0x4f, 0x0a, 0x13, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, - 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x6d, - 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x46, - 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x52, 0x13, 0x72, 0x6f, 0x75, - 0x74, 0x65, 0x73, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, - 0x12, 0x3e, 0x0a, 0x1a, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, - 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x18, 0x0b, - 0x20, 0x01, 0x28, 0x08, 0x52, 0x1a, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x46, 0x69, 0x72, 0x65, - 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, - 0x12, 0x44, 0x0a, 0x0f, 0x66, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x75, - 0x6c, 0x65, 0x73, 0x18, 0x0c, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e, 0x61, - 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, - 0x67, 0x52, 0x75, 0x6c, 0x65, 0x52, 0x0f, 0x66, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, - 0x67, 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, 0x6c, 0x6f, 0x77, - 0x65, 0x64, 0x49, 0x70, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, 0x61, 0x6c, 0x6c, - 0x6f, 0x77, 0x65, 0x64, 0x49, 0x70, 0x73, 0x12, 0x33, 0x0a, 0x09, 0x73, 0x73, 0x68, 0x43, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x18, 0x03, 0x20, 0x01, 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, 0x49, 0x0a, 0x09, 0x53, 0x53, 0x48, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1e, 0x0a, - 0x0a, 0x73, 0x73, 0x68, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x08, 0x52, 0x0a, 0x73, 0x73, 0x68, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x1c, 0x0a, - 0x09, 0x73, 0x73, 0x68, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, - 0x52, 0x09, 0x73, 0x73, 0x68, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x22, 0x20, 0x0a, 0x1e, 0x44, - 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xbf, 0x01, - 0x0a, 0x17, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x12, 0x48, 0x0a, 0x08, 0x50, 0x72, 0x6f, - 0x76, 0x69, 0x64, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x2c, 0x2e, 0x6d, 0x61, - 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, - 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, - 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x52, 0x08, 0x50, 0x72, 0x6f, 0x76, 0x69, - 0x64, 0x65, 0x72, 0x12, 0x42, 0x0a, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61, - 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, - 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, - 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0x16, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x76, 0x69, - 0x64, 0x65, 0x72, 0x12, 0x0a, 0x0a, 0x06, 0x48, 0x4f, 0x53, 0x54, 0x45, 0x44, 0x10, 0x00, 0x22, - 0x1e, 0x0a, 0x1c, 0x50, 0x4b, 0x43, 0x45, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, - 0x5b, 0x0a, 0x15, 0x50, 0x4b, 0x43, 0x45, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x12, 0x42, 0x0a, 0x0e, 0x50, 0x72, 0x6f, 0x76, - 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x72, - 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0e, 0x50, 0x72, - 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0xea, 0x02, 0x0a, - 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, - 0x1a, 0x0a, 0x08, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x08, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x44, 0x12, 0x22, 0x0a, 0x0c, 0x43, - 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x0c, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x12, - 0x16, 0x0a, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x41, 0x75, 0x64, 0x69, 0x65, - 0x6e, 0x63, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x41, 0x75, 0x64, 0x69, 0x65, - 0x6e, 0x63, 0x65, 0x12, 0x2e, 0x0a, 0x12, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, - 0x68, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x12, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x45, 0x6e, 0x64, 0x70, 0x6f, - 0x69, 0x6e, 0x74, 0x12, 0x24, 0x0a, 0x0d, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x45, 0x6e, 0x64, 0x70, - 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x54, 0x6f, 0x6b, 0x65, - 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x53, 0x63, 0x6f, - 0x70, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x12, - 0x1e, 0x0a, 0x0a, 0x55, 0x73, 0x65, 0x49, 0x44, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x08, 0x20, - 0x01, 0x28, 0x08, 0x52, 0x0a, 0x55, 0x73, 0x65, 0x49, 0x44, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, - 0x34, 0x0a, 0x15, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x15, - 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x64, - 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x22, 0x0a, 0x0c, 0x52, 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, - 0x74, 0x55, 0x52, 0x4c, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0c, 0x52, 0x65, 0x64, - 0x69, 0x72, 0x65, 0x63, 0x74, 0x55, 0x52, 0x4c, 0x73, 0x22, 0xed, 0x01, 0x0a, 0x05, 0x52, 0x6f, - 0x75, 0x74, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x02, 0x49, 0x44, 0x12, 0x18, 0x0a, 0x07, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x12, 0x20, 0x0a, - 0x0b, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x54, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x03, 0x52, 0x0b, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x54, 0x79, 0x70, 0x65, 0x12, - 0x12, 0x0a, 0x04, 0x50, 0x65, 0x65, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x50, - 0x65, 0x65, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x18, 0x05, 0x20, - 0x01, 0x28, 0x03, 0x52, 0x06, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x12, 0x1e, 0x0a, 0x0a, 0x4d, - 0x61, 0x73, 0x71, 0x75, 0x65, 0x72, 0x61, 0x64, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, - 0x0a, 0x4d, 0x61, 0x73, 0x71, 0x75, 0x65, 0x72, 0x61, 0x64, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x4e, - 0x65, 0x74, 0x49, 0x44, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x4e, 0x65, 0x74, 0x49, - 0x44, 0x12, 0x18, 0x0a, 0x07, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x08, 0x20, 0x03, - 0x28, 0x09, 0x52, 0x07, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x6b, - 0x65, 0x65, 0x70, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, - 0x6b, 0x65, 0x65, 0x70, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x22, 0xb4, 0x01, 0x0a, 0x09, 0x44, 0x4e, - 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x24, 0x0a, 0x0d, 0x53, 0x65, 0x72, 0x76, 0x69, - 0x63, 0x65, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, - 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x47, 0x0a, - 0x10, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, - 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, - 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, - 0x72, 0x6f, 0x75, 0x70, 0x52, 0x10, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, - 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x12, 0x38, 0x0a, 0x0b, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, - 0x5a, 0x6f, 0x6e, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, - 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a, - 0x6f, 0x6e, 0x65, 0x52, 0x0b, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, 0x73, - 0x22, 0x58, 0x0a, 0x0a, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, 0x12, 0x16, - 0x0a, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, - 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x32, 0x0a, 0x07, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, - 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, - 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x52, 0x65, 0x63, 0x6f, 0x72, - 0x64, 0x52, 0x07, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x22, 0x74, 0x0a, 0x0c, 0x53, 0x69, - 0x6d, 0x70, 0x6c, 0x65, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, - 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x12, - 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x54, 0x79, - 0x70, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x05, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x54, 0x54, 0x4c, 0x18, - 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x03, 0x54, 0x54, 0x4c, 0x12, 0x14, 0x0a, 0x05, 0x52, 0x44, - 0x61, 0x74, 0x61, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x52, 0x44, 0x61, 0x74, 0x61, - 0x22, 0xb3, 0x01, 0x0a, 0x0f, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, - 0x72, 0x6f, 0x75, 0x70, 0x12, 0x38, 0x0a, 0x0b, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, - 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, - 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, - 0x72, 0x52, 0x0b, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x12, 0x18, - 0x0a, 0x07, 0x50, 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, - 0x07, 0x50, 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x44, 0x6f, 0x6d, 0x61, - 0x69, 0x6e, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x44, 0x6f, 0x6d, 0x61, 0x69, - 0x6e, 0x73, 0x12, 0x32, 0x0a, 0x14, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x44, 0x6f, 0x6d, 0x61, - 0x69, 0x6e, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, - 0x52, 0x14, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x45, - 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x22, 0x48, 0x0a, 0x0a, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, - 0x72, 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, - 0x22, 0x8b, 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, 0x37, 0x0a, 0x09, 0x44, 0x69, 0x72, - 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x19, 0x2e, 0x6d, - 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x44, 0x69, - 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x09, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, - 0x6f, 0x6e, 0x12, 0x2e, 0x0a, 0x06, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x0e, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, - 0x52, 0x75, 0x6c, 0x65, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x41, 0x63, 0x74, 0x69, - 0x6f, 0x6e, 0x12, 0x34, 0x0a, 0x08, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x04, - 0x20, 0x01, 0x28, 0x0e, 0x32, 0x18, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, - 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x50, 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, 0x12, 0x30, 0x0a, 0x08, - 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, - 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x6f, 0x72, 0x74, - 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x08, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x22, 0x38, - 0x0a, 0x0e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, - 0x12, 0x14, 0x0a, 0x05, 0x6e, 0x65, 0x74, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x05, 0x6e, 0x65, 0x74, 0x49, 0x50, 0x12, 0x10, 0x0a, 0x03, 0x6d, 0x61, 0x63, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x03, 0x6d, 0x61, 0x63, 0x22, 0x1e, 0x0a, 0x06, 0x43, 0x68, 0x65, 0x63, - 0x6b, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, - 0x09, 0x52, 0x05, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x22, 0x96, 0x01, 0x0a, 0x08, 0x50, 0x6f, 0x72, - 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x14, 0x0a, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x0d, 0x48, 0x00, 0x52, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x12, 0x32, 0x0a, 0x05, 0x72, - 0x61, 0x6e, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e, - 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, - 0x2e, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x48, 0x00, 0x52, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x1a, - 0x2f, 0x0a, 0x05, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x72, - 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x12, 0x10, - 0x0a, 0x03, 0x65, 0x6e, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x03, 0x65, 0x6e, 0x64, - 0x42, 0x0f, 0x0a, 0x0d, 0x70, 0x6f, 0x72, 0x74, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, - 0x6e, 0x22, 0xd1, 0x02, 0x0a, 0x11, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x46, 0x69, 0x72, 0x65, 0x77, - 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x22, 0x0a, 0x0c, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0c, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x12, 0x2e, 0x0a, 0x06, 0x61, - 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x16, 0x2e, 0x6d, 0x61, - 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x41, 0x63, 0x74, - 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x20, 0x0a, 0x0b, 0x64, - 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x0b, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x34, 0x0a, - 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, - 0x18, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, - 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x63, 0x6f, 0x6c, 0x12, 0x30, 0x0a, 0x08, 0x70, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x18, - 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, - 0x6e, 0x74, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x08, 0x70, 0x6f, 0x72, - 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x1c, 0x0a, 0x09, 0x69, 0x73, 0x44, 0x79, 0x6e, 0x61, 0x6d, - 0x69, 0x63, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x69, 0x73, 0x44, 0x79, 0x6e, 0x61, - 0x6d, 0x69, 0x63, 0x12, 0x18, 0x0a, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x07, - 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x26, 0x0a, - 0x0e, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, - 0x08, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0e, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x50, 0x72, 0x6f, - 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x22, 0xf2, 0x01, 0x0a, 0x0e, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, - 0x64, 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x34, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x18, 0x2e, 0x6d, 0x61, 0x6e, - 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x50, 0x72, 0x6f, 0x74, - 0x6f, 0x63, 0x6f, 0x6c, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x3e, - 0x0a, 0x0f, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x6f, 0x72, - 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, - 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x0f, 0x64, - 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x2c, - 0x0a, 0x11, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x41, 0x64, 0x64, 0x72, - 0x65, 0x73, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x11, 0x74, 0x72, 0x61, 0x6e, 0x73, - 0x6c, 0x61, 0x74, 0x65, 0x64, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x3c, 0x0a, 0x0e, - 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x04, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, - 0x74, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x0e, 0x74, 0x72, 0x61, 0x6e, - 0x73, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x50, 0x6f, 0x72, 0x74, 0x2a, 0x4c, 0x0a, 0x0c, 0x52, 0x75, - 0x6c, 0x65, 0x50, 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, 0x12, 0x0a, 0x0a, 0x06, - 0x43, 0x55, 0x53, 0x54, 0x4f, 0x4d, 0x10, 0x05, 0x2a, 0x20, 0x0a, 0x0d, 0x52, 0x75, 0x6c, 0x65, - 0x44, 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, 0x2a, 0x22, 0x0a, 0x0a, 0x52, 0x75, - 0x6c, 0x65, 0x41, 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, 0x32, 0x90, - 0x04, 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, 0x58, 0x0a, 0x18, 0x47, 0x65, 0x74, 0x50, + 0x6c, 0x61, 0x79, 0x12, 0x2a, 0x0a, 0x04, 0x66, 0x6c, 0x6f, 0x77, 0x18, 0x05, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x46, + 0x6c, 0x6f, 0x77, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x04, 0x66, 0x6c, 0x6f, 0x77, 0x22, + 0x98, 0x01, 0x0a, 0x0a, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x10, + 0x0a, 0x03, 0x75, 0x72, 0x69, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x69, + 0x12, 0x3b, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x0e, 0x32, 0x1f, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, + 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x50, 0x72, 0x6f, 0x74, 0x6f, + 0x63, 0x6f, 0x6c, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x22, 0x3b, 0x0a, + 0x08, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x07, 0x0a, 0x03, 0x55, 0x44, 0x50, + 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x54, 0x43, 0x50, 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x48, + 0x54, 0x54, 0x50, 0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, 0x48, 0x54, 0x54, 0x50, 0x53, 0x10, 0x03, + 0x12, 0x08, 0x0a, 0x04, 0x44, 0x54, 0x4c, 0x53, 0x10, 0x04, 0x22, 0x6d, 0x0a, 0x0b, 0x52, 0x65, + 0x6c, 0x61, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x72, 0x6c, + 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x04, 0x75, 0x72, 0x6c, 0x73, 0x12, 0x22, 0x0a, + 0x0c, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0c, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, + 0x64, 0x12, 0x26, 0x0a, 0x0e, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, + 0x75, 0x72, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x74, 0x6f, 0x6b, 0x65, 0x6e, + 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x22, 0xad, 0x02, 0x0a, 0x0a, 0x46, 0x6c, + 0x6f, 0x77, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x6c, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x6c, 0x12, 0x22, 0x0a, 0x0c, 0x74, 0x6f, + 0x6b, 0x65, 0x6e, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0c, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x12, 0x26, + 0x0a, 0x0e, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x53, 0x69, 0x67, + 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x12, 0x35, 0x0a, 0x08, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x76, + 0x61, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, + 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x52, 0x08, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x12, 0x18, 0x0a, + 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, + 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x63, 0x6f, 0x75, 0x6e, 0x74, + 0x65, 0x72, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x63, 0x6f, 0x75, 0x6e, 0x74, + 0x65, 0x72, 0x73, 0x12, 0x2e, 0x0a, 0x12, 0x65, 0x78, 0x69, 0x74, 0x4e, 0x6f, 0x64, 0x65, 0x43, + 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x07, 0x20, 0x01, 0x28, 0x08, 0x52, + 0x12, 0x65, 0x78, 0x69, 0x74, 0x4e, 0x6f, 0x64, 0x65, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x12, 0x24, 0x0a, 0x0d, 0x64, 0x6e, 0x73, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, + 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x64, 0x6e, 0x73, 0x43, + 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x7d, 0x0a, 0x13, 0x50, 0x72, 0x6f, + 0x74, 0x65, 0x63, 0x74, 0x65, 0x64, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, + 0x12, 0x36, 0x0a, 0x0a, 0x68, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, + 0x74, 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0a, 0x68, 0x6f, + 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x12, 0x1a, 0x0a, 0x08, + 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, + 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, 0xcb, 0x01, 0x0a, 0x0a, 0x50, 0x65, 0x65, + 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, + 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, + 0x73, 0x12, 0x10, 0x0a, 0x03, 0x64, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, + 0x64, 0x6e, 0x73, 0x12, 0x33, 0x0a, 0x09, 0x73, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, + 0x18, 0x03, 0x20, 0x01, 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, 0x12, 0x48, 0x0a, 0x1f, + 0x52, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, 0x50, 0x65, 0x65, 0x72, 0x44, 0x6e, 0x73, 0x52, 0x65, + 0x73, 0x6f, 0x6c, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, + 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x1f, 0x52, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, 0x50, 0x65, + 0x65, 0x72, 0x44, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x45, + 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x22, 0xb9, 0x05, 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, 0x01, 0x28, + 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, + 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0a, 0x70, 0x65, 0x65, 0x72, 0x43, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x3e, 0x0a, 0x0b, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, + 0x65, 0x65, 0x72, 0x73, 0x18, 0x03, 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, 0x0b, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, + 0x50, 0x65, 0x65, 0x72, 0x73, 0x12, 0x2e, 0x0a, 0x12, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, + 0x65, 0x65, 0x72, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x08, 0x52, 0x12, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x49, 0x73, + 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x29, 0x0a, 0x06, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x18, + 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, + 0x6e, 0x74, 0x2e, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x52, 0x06, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, + 0x12, 0x33, 0x0a, 0x09, 0x44, 0x4e, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x06, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, + 0x2e, 0x44, 0x4e, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x09, 0x44, 0x4e, 0x53, 0x43, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x40, 0x0a, 0x0c, 0x6f, 0x66, 0x66, 0x6c, 0x69, 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, 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, 0x12, 0x32, 0x0a, 0x14, 0x66, 0x69, 0x72, 0x65, 0x77, + 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x18, + 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x14, 0x66, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, + 0x75, 0x6c, 0x65, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x4f, 0x0a, 0x13, 0x72, + 0x6f, 0x75, 0x74, 0x65, 0x73, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, + 0x65, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, + 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x46, 0x69, 0x72, 0x65, 0x77, + 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x52, 0x13, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x46, + 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x12, 0x3e, 0x0a, 0x1a, + 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, + 0x6c, 0x65, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x08, + 0x52, 0x1a, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, + 0x52, 0x75, 0x6c, 0x65, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x44, 0x0a, 0x0f, + 0x66, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x18, + 0x0c, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, + 0x6e, 0x74, 0x2e, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, + 0x65, 0x52, 0x0f, 0x66, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 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, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x49, 0x70, + 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, + 0x49, 0x70, 0x73, 0x12, 0x33, 0x0a, 0x09, 0x73, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, + 0x18, 0x03, 0x20, 0x01, 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, 0x49, 0x0a, 0x09, + 0x53, 0x53, 0x48, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1e, 0x0a, 0x0a, 0x73, 0x73, 0x68, + 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x73, + 0x73, 0x68, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x73, 0x68, + 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x73, 0x73, + 0x68, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x22, 0x20, 0x0a, 0x1e, 0x44, 0x65, 0x76, 0x69, 0x63, + 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, + 0x6f, 0x77, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xbf, 0x01, 0x0a, 0x17, 0x44, 0x65, + 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x12, 0x48, 0x0a, 0x08, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, + 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x2c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, + 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, + 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x2e, 0x70, 0x72, 0x6f, + 0x76, 0x69, 0x64, 0x65, 0x72, 0x52, 0x08, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, + 0x42, 0x0a, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, + 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x52, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x22, 0x16, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, + 0x0a, 0x0a, 0x06, 0x48, 0x4f, 0x53, 0x54, 0x45, 0x44, 0x10, 0x00, 0x22, 0x1e, 0x0a, 0x1c, 0x50, 0x4b, 0x43, 0x45, 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, 0x3d, 0x0a, 0x08, 0x53, 0x79, 0x6e, 0x63, 0x4d, 0x65, 0x74, 0x61, 0x12, 0x1c, + 0x46, 0x6c, 0x6f, 0x77, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x5b, 0x0a, 0x15, 0x50, + 0x4b, 0x43, 0x45, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x46, 0x6c, 0x6f, 0x77, 0x12, 0x42, 0x0a, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, + 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, + 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, + 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, + 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0xea, 0x02, 0x0a, 0x0e, 0x50, 0x72, 0x6f, + 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1a, 0x0a, 0x08, 0x43, + 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x43, + 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x44, 0x12, 0x22, 0x0a, 0x0c, 0x43, 0x6c, 0x69, 0x65, 0x6e, + 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x43, + 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x44, + 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x44, 0x6f, 0x6d, + 0x61, 0x69, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x41, 0x75, 0x64, 0x69, 0x65, 0x6e, 0x63, 0x65, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x41, 0x75, 0x64, 0x69, 0x65, 0x6e, 0x63, 0x65, 0x12, + 0x2e, 0x0a, 0x12, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x45, 0x6e, 0x64, + 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x44, 0x65, 0x76, + 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, + 0x24, 0x0a, 0x0d, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, + 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x45, 0x6e, 0x64, + 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x18, 0x07, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x55, + 0x73, 0x65, 0x49, 0x44, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, + 0x0a, 0x55, 0x73, 0x65, 0x49, 0x44, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x34, 0x0a, 0x15, 0x41, + 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x64, 0x70, + 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x15, 0x41, 0x75, 0x74, 0x68, + 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, + 0x74, 0x12, 0x22, 0x0a, 0x0c, 0x52, 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x55, 0x52, 0x4c, + 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0c, 0x52, 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, + 0x74, 0x55, 0x52, 0x4c, 0x73, 0x22, 0xed, 0x01, 0x0a, 0x05, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x12, + 0x0e, 0x0a, 0x02, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x44, 0x12, + 0x18, 0x0a, 0x07, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x07, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x12, 0x20, 0x0a, 0x0b, 0x4e, 0x65, 0x74, + 0x77, 0x6f, 0x72, 0x6b, 0x54, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, + 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x50, + 0x65, 0x65, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x50, 0x65, 0x65, 0x72, 0x12, + 0x16, 0x0a, 0x06, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, + 0x06, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x12, 0x1e, 0x0a, 0x0a, 0x4d, 0x61, 0x73, 0x71, 0x75, + 0x65, 0x72, 0x61, 0x64, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x4d, 0x61, 0x73, + 0x71, 0x75, 0x65, 0x72, 0x61, 0x64, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x4e, 0x65, 0x74, 0x49, 0x44, + 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x4e, 0x65, 0x74, 0x49, 0x44, 0x12, 0x18, 0x0a, + 0x07, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, + 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x6b, 0x65, 0x65, 0x70, 0x52, + 0x6f, 0x75, 0x74, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x6b, 0x65, 0x65, 0x70, + 0x52, 0x6f, 0x75, 0x74, 0x65, 0x22, 0xb4, 0x01, 0x0a, 0x09, 0x44, 0x4e, 0x53, 0x43, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x12, 0x24, 0x0a, 0x0d, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x45, 0x6e, + 0x61, 0x62, 0x6c, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x53, 0x65, 0x72, 0x76, + 0x69, 0x63, 0x65, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x47, 0x0a, 0x10, 0x4e, 0x61, 0x6d, + 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x18, 0x02, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, + 0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, + 0x52, 0x10, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, + 0x70, 0x73, 0x12, 0x38, 0x0a, 0x0b, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, + 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, + 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, 0x52, + 0x0b, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, 0x73, 0x22, 0x58, 0x0a, 0x0a, + 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x44, 0x6f, + 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x44, 0x6f, 0x6d, 0x61, + 0x69, 0x6e, 0x12, 0x32, 0x0a, 0x07, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x18, 0x02, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, + 0x2e, 0x53, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x52, 0x07, 0x52, + 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x22, 0x74, 0x0a, 0x0c, 0x53, 0x69, 0x6d, 0x70, 0x6c, 0x65, + 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x54, 0x79, + 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12, 0x14, + 0x0a, 0x05, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x43, + 0x6c, 0x61, 0x73, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x54, 0x54, 0x4c, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x03, 0x52, 0x03, 0x54, 0x54, 0x4c, 0x12, 0x14, 0x0a, 0x05, 0x52, 0x44, 0x61, 0x74, 0x61, 0x18, + 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x52, 0x44, 0x61, 0x74, 0x61, 0x22, 0xb3, 0x01, 0x0a, + 0x0f, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, + 0x12, 0x38, 0x0a, 0x0b, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x18, + 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, + 0x6e, 0x74, 0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x0b, 0x4e, + 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x50, 0x72, + 0x69, 0x6d, 0x61, 0x72, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x50, 0x72, 0x69, + 0x6d, 0x61, 0x72, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, + 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x32, + 0x0a, 0x14, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x45, + 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x14, 0x53, 0x65, + 0x61, 0x72, 0x63, 0x68, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, + 0x65, 0x64, 0x22, 0x48, 0x0a, 0x0a, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 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, 0x22, 0xa7, 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, 0x37, 0x0a, 0x09, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, + 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x19, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, + 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x52, 0x09, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x2e, + 0x0a, 0x06, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x16, + 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, + 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x34, + 0x0a, 0x08, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, + 0x32, 0x18, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, + 0x6c, 0x65, 0x50, 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, 0x12, 0x30, 0x0a, 0x08, 0x50, 0x6f, 0x72, 0x74, + 0x49, 0x6e, 0x66, 0x6f, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x6d, 0x61, 0x6e, + 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, + 0x52, 0x08, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x1a, 0x0a, 0x08, 0x50, 0x6f, + 0x6c, 0x69, 0x63, 0x79, 0x49, 0x44, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x50, 0x6f, + 0x6c, 0x69, 0x63, 0x79, 0x49, 0x44, 0x22, 0x38, 0x0a, 0x0e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, + 0x6b, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x6e, 0x65, 0x74, 0x49, + 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6e, 0x65, 0x74, 0x49, 0x50, 0x12, 0x10, + 0x0a, 0x03, 0x6d, 0x61, 0x63, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6d, 0x61, 0x63, + 0x22, 0x1e, 0x0a, 0x06, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x46, 0x69, + 0x6c, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x46, 0x69, 0x6c, 0x65, 0x73, + 0x22, 0x96, 0x01, 0x0a, 0x08, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x14, 0x0a, + 0x04, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x48, 0x00, 0x52, 0x04, 0x70, + 0x6f, 0x72, 0x74, 0x12, 0x32, 0x0a, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, + 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x2e, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x48, 0x00, + 0x52, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x1a, 0x2f, 0x0a, 0x05, 0x52, 0x61, 0x6e, 0x67, 0x65, + 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, + 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x65, 0x6e, 0x64, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x0d, 0x52, 0x03, 0x65, 0x6e, 0x64, 0x42, 0x0f, 0x0a, 0x0d, 0x70, 0x6f, 0x72, 0x74, + 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0xed, 0x02, 0x0a, 0x11, 0x52, 0x6f, + 0x75, 0x74, 0x65, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x12, + 0x22, 0x0a, 0x0c, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x18, + 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0c, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x61, 0x6e, + 0x67, 0x65, 0x73, 0x12, 0x2e, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x0e, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, + 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x61, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x34, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, + 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x18, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, + 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, + 0x6c, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x30, 0x0a, 0x08, 0x70, + 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, + 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x49, + 0x6e, 0x66, 0x6f, 0x52, 0x08, 0x70, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x1c, 0x0a, + 0x09, 0x69, 0x73, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, + 0x52, 0x09, 0x69, 0x73, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x12, 0x18, 0x0a, 0x07, 0x64, + 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x64, 0x6f, + 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x26, 0x0a, 0x0e, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x50, + 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0e, 0x63, + 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x1a, 0x0a, + 0x08, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x49, 0x44, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0c, 0x52, + 0x08, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x49, 0x44, 0x22, 0xf2, 0x01, 0x0a, 0x0e, 0x46, 0x6f, + 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x34, 0x0a, 0x08, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x18, + 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, + 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, + 0x6f, 0x6c, 0x12, 0x3e, 0x0a, 0x0f, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x6d, 0x61, + 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, + 0x6f, 0x52, 0x0f, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x6f, + 0x72, 0x74, 0x12, 0x2c, 0x0a, 0x11, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, 0x64, + 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x11, 0x74, + 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, + 0x12, 0x3c, 0x0a, 0x0e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x50, 0x6f, + 0x72, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, + 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x0e, + 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x50, 0x6f, 0x72, 0x74, 0x2a, 0x4c, + 0x0a, 0x0c, 0x52, 0x75, 0x6c, 0x65, 0x50, 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, + 0x12, 0x0a, 0x0a, 0x06, 0x43, 0x55, 0x53, 0x54, 0x4f, 0x4d, 0x10, 0x05, 0x2a, 0x20, 0x0a, 0x0d, + 0x52, 0x75, 0x6c, 0x65, 0x44, 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, 0x2a, 0x22, + 0x0a, 0x0a, 0x52, 0x75, 0x6c, 0x65, 0x41, 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, 0x32, 0x90, 0x04, 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, 0x11, 0x2e, 0x6d, - 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, - 0x00, 0x42, 0x08, 0x5a, 0x06, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x33, + 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, 0x58, 0x0a, 0x18, + 0x47, 0x65, 0x74, 0x50, 0x4b, 0x43, 0x45, 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, 0x3d, 0x0a, 0x08, 0x53, 0x79, 0x6e, 0x63, 0x4d, 0x65, + 0x74, 0x61, 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, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, + 0x70, 0x74, 0x79, 0x22, 0x00, 0x42, 0x08, 0x5a, 0x06, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, + 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -3629,7 +3788,7 @@ func file_management_proto_rawDescGZIP() []byte { } var file_management_proto_enumTypes = make([]protoimpl.EnumInfo, 5) -var file_management_proto_msgTypes = make([]protoimpl.MessageInfo, 39) +var file_management_proto_msgTypes = make([]protoimpl.MessageInfo, 40) var file_management_proto_goTypes = []interface{}{ (RuleProtocol)(0), // 0: management.RuleProtocol (RuleDirection)(0), // 1: management.RuleDirection @@ -3652,102 +3811,106 @@ var file_management_proto_goTypes = []interface{}{ (*NetbirdConfig)(nil), // 18: management.NetbirdConfig (*HostConfig)(nil), // 19: management.HostConfig (*RelayConfig)(nil), // 20: management.RelayConfig - (*ProtectedHostConfig)(nil), // 21: management.ProtectedHostConfig - (*PeerConfig)(nil), // 22: management.PeerConfig - (*NetworkMap)(nil), // 23: management.NetworkMap - (*RemotePeerConfig)(nil), // 24: management.RemotePeerConfig - (*SSHConfig)(nil), // 25: management.SSHConfig - (*DeviceAuthorizationFlowRequest)(nil), // 26: management.DeviceAuthorizationFlowRequest - (*DeviceAuthorizationFlow)(nil), // 27: management.DeviceAuthorizationFlow - (*PKCEAuthorizationFlowRequest)(nil), // 28: management.PKCEAuthorizationFlowRequest - (*PKCEAuthorizationFlow)(nil), // 29: management.PKCEAuthorizationFlow - (*ProviderConfig)(nil), // 30: management.ProviderConfig - (*Route)(nil), // 31: management.Route - (*DNSConfig)(nil), // 32: management.DNSConfig - (*CustomZone)(nil), // 33: management.CustomZone - (*SimpleRecord)(nil), // 34: management.SimpleRecord - (*NameServerGroup)(nil), // 35: management.NameServerGroup - (*NameServer)(nil), // 36: management.NameServer - (*FirewallRule)(nil), // 37: management.FirewallRule - (*NetworkAddress)(nil), // 38: management.NetworkAddress - (*Checks)(nil), // 39: management.Checks - (*PortInfo)(nil), // 40: management.PortInfo - (*RouteFirewallRule)(nil), // 41: management.RouteFirewallRule - (*ForwardingRule)(nil), // 42: management.ForwardingRule - (*PortInfo_Range)(nil), // 43: management.PortInfo.Range - (*timestamppb.Timestamp)(nil), // 44: google.protobuf.Timestamp + (*FlowConfig)(nil), // 21: management.FlowConfig + (*ProtectedHostConfig)(nil), // 22: management.ProtectedHostConfig + (*PeerConfig)(nil), // 23: management.PeerConfig + (*NetworkMap)(nil), // 24: management.NetworkMap + (*RemotePeerConfig)(nil), // 25: management.RemotePeerConfig + (*SSHConfig)(nil), // 26: management.SSHConfig + (*DeviceAuthorizationFlowRequest)(nil), // 27: management.DeviceAuthorizationFlowRequest + (*DeviceAuthorizationFlow)(nil), // 28: management.DeviceAuthorizationFlow + (*PKCEAuthorizationFlowRequest)(nil), // 29: management.PKCEAuthorizationFlowRequest + (*PKCEAuthorizationFlow)(nil), // 30: management.PKCEAuthorizationFlow + (*ProviderConfig)(nil), // 31: management.ProviderConfig + (*Route)(nil), // 32: management.Route + (*DNSConfig)(nil), // 33: management.DNSConfig + (*CustomZone)(nil), // 34: management.CustomZone + (*SimpleRecord)(nil), // 35: management.SimpleRecord + (*NameServerGroup)(nil), // 36: management.NameServerGroup + (*NameServer)(nil), // 37: management.NameServer + (*FirewallRule)(nil), // 38: management.FirewallRule + (*NetworkAddress)(nil), // 39: management.NetworkAddress + (*Checks)(nil), // 40: management.Checks + (*PortInfo)(nil), // 41: management.PortInfo + (*RouteFirewallRule)(nil), // 42: management.RouteFirewallRule + (*ForwardingRule)(nil), // 43: management.ForwardingRule + (*PortInfo_Range)(nil), // 44: management.PortInfo.Range + (*timestamppb.Timestamp)(nil), // 45: google.protobuf.Timestamp + (*durationpb.Duration)(nil), // 46: google.protobuf.Duration } var file_management_proto_depIdxs = []int32{ 14, // 0: management.SyncRequest.meta:type_name -> management.PeerSystemMeta 18, // 1: management.SyncResponse.netbirdConfig:type_name -> management.NetbirdConfig - 22, // 2: management.SyncResponse.peerConfig:type_name -> management.PeerConfig - 24, // 3: management.SyncResponse.remotePeers:type_name -> management.RemotePeerConfig - 23, // 4: management.SyncResponse.NetworkMap:type_name -> management.NetworkMap - 39, // 5: management.SyncResponse.Checks:type_name -> management.Checks + 23, // 2: management.SyncResponse.peerConfig:type_name -> management.PeerConfig + 25, // 3: management.SyncResponse.remotePeers:type_name -> management.RemotePeerConfig + 24, // 4: management.SyncResponse.NetworkMap:type_name -> management.NetworkMap + 40, // 5: management.SyncResponse.Checks:type_name -> management.Checks 14, // 6: management.SyncMetaRequest.meta:type_name -> management.PeerSystemMeta 14, // 7: management.LoginRequest.meta:type_name -> management.PeerSystemMeta 10, // 8: management.LoginRequest.peerKeys:type_name -> management.PeerKeys - 38, // 9: management.PeerSystemMeta.networkAddresses:type_name -> management.NetworkAddress + 39, // 9: management.PeerSystemMeta.networkAddresses:type_name -> management.NetworkAddress 11, // 10: management.PeerSystemMeta.environment:type_name -> management.Environment 12, // 11: management.PeerSystemMeta.files:type_name -> management.File 13, // 12: management.PeerSystemMeta.flags:type_name -> management.Flags 18, // 13: management.LoginResponse.netbirdConfig:type_name -> management.NetbirdConfig - 22, // 14: management.LoginResponse.peerConfig:type_name -> management.PeerConfig - 39, // 15: management.LoginResponse.Checks:type_name -> management.Checks - 44, // 16: management.ServerKeyResponse.expiresAt:type_name -> google.protobuf.Timestamp + 23, // 14: management.LoginResponse.peerConfig:type_name -> management.PeerConfig + 40, // 15: management.LoginResponse.Checks:type_name -> management.Checks + 45, // 16: management.ServerKeyResponse.expiresAt:type_name -> google.protobuf.Timestamp 19, // 17: management.NetbirdConfig.stuns:type_name -> management.HostConfig - 21, // 18: management.NetbirdConfig.turns:type_name -> management.ProtectedHostConfig + 22, // 18: management.NetbirdConfig.turns:type_name -> management.ProtectedHostConfig 19, // 19: management.NetbirdConfig.signal:type_name -> management.HostConfig 20, // 20: management.NetbirdConfig.relay:type_name -> management.RelayConfig - 3, // 21: management.HostConfig.protocol:type_name -> management.HostConfig.Protocol - 19, // 22: management.ProtectedHostConfig.hostConfig:type_name -> management.HostConfig - 25, // 23: management.PeerConfig.sshConfig:type_name -> management.SSHConfig - 22, // 24: management.NetworkMap.peerConfig:type_name -> management.PeerConfig - 24, // 25: management.NetworkMap.remotePeers:type_name -> management.RemotePeerConfig - 31, // 26: management.NetworkMap.Routes:type_name -> management.Route - 32, // 27: management.NetworkMap.DNSConfig:type_name -> management.DNSConfig - 24, // 28: management.NetworkMap.offlinePeers:type_name -> management.RemotePeerConfig - 37, // 29: management.NetworkMap.FirewallRules:type_name -> management.FirewallRule - 41, // 30: management.NetworkMap.routesFirewallRules:type_name -> management.RouteFirewallRule - 42, // 31: management.NetworkMap.forwardingRules:type_name -> management.ForwardingRule - 25, // 32: management.RemotePeerConfig.sshConfig:type_name -> management.SSHConfig - 4, // 33: management.DeviceAuthorizationFlow.Provider:type_name -> management.DeviceAuthorizationFlow.provider - 30, // 34: management.DeviceAuthorizationFlow.ProviderConfig:type_name -> management.ProviderConfig - 30, // 35: management.PKCEAuthorizationFlow.ProviderConfig:type_name -> management.ProviderConfig - 35, // 36: management.DNSConfig.NameServerGroups:type_name -> management.NameServerGroup - 33, // 37: management.DNSConfig.CustomZones:type_name -> management.CustomZone - 34, // 38: management.CustomZone.Records:type_name -> management.SimpleRecord - 36, // 39: management.NameServerGroup.NameServers:type_name -> management.NameServer - 1, // 40: management.FirewallRule.Direction:type_name -> management.RuleDirection - 2, // 41: management.FirewallRule.Action:type_name -> management.RuleAction - 0, // 42: management.FirewallRule.Protocol:type_name -> management.RuleProtocol - 40, // 43: management.FirewallRule.PortInfo:type_name -> management.PortInfo - 43, // 44: management.PortInfo.range:type_name -> management.PortInfo.Range - 2, // 45: management.RouteFirewallRule.action:type_name -> management.RuleAction - 0, // 46: management.RouteFirewallRule.protocol:type_name -> management.RuleProtocol - 40, // 47: management.RouteFirewallRule.portInfo:type_name -> management.PortInfo - 0, // 48: management.ForwardingRule.protocol:type_name -> management.RuleProtocol - 40, // 49: management.ForwardingRule.destinationPort:type_name -> management.PortInfo - 40, // 50: management.ForwardingRule.translatedPort:type_name -> management.PortInfo - 5, // 51: management.ManagementService.Login:input_type -> management.EncryptedMessage - 5, // 52: management.ManagementService.Sync:input_type -> management.EncryptedMessage - 17, // 53: management.ManagementService.GetServerKey:input_type -> management.Empty - 17, // 54: management.ManagementService.isHealthy:input_type -> management.Empty - 5, // 55: management.ManagementService.GetDeviceAuthorizationFlow:input_type -> management.EncryptedMessage - 5, // 56: management.ManagementService.GetPKCEAuthorizationFlow:input_type -> management.EncryptedMessage - 5, // 57: management.ManagementService.SyncMeta:input_type -> management.EncryptedMessage - 5, // 58: management.ManagementService.Login:output_type -> management.EncryptedMessage - 5, // 59: management.ManagementService.Sync:output_type -> management.EncryptedMessage - 16, // 60: management.ManagementService.GetServerKey:output_type -> management.ServerKeyResponse - 17, // 61: management.ManagementService.isHealthy:output_type -> management.Empty - 5, // 62: management.ManagementService.GetDeviceAuthorizationFlow:output_type -> management.EncryptedMessage - 5, // 63: management.ManagementService.GetPKCEAuthorizationFlow:output_type -> management.EncryptedMessage - 17, // 64: management.ManagementService.SyncMeta:output_type -> management.Empty - 58, // [58:65] is the sub-list for method output_type - 51, // [51:58] is the sub-list for method input_type - 51, // [51:51] is the sub-list for extension type_name - 51, // [51:51] is the sub-list for extension extendee - 0, // [0:51] is the sub-list for field type_name + 21, // 21: management.NetbirdConfig.flow:type_name -> management.FlowConfig + 3, // 22: management.HostConfig.protocol:type_name -> management.HostConfig.Protocol + 46, // 23: management.FlowConfig.interval:type_name -> google.protobuf.Duration + 19, // 24: management.ProtectedHostConfig.hostConfig:type_name -> management.HostConfig + 26, // 25: management.PeerConfig.sshConfig:type_name -> management.SSHConfig + 23, // 26: management.NetworkMap.peerConfig:type_name -> management.PeerConfig + 25, // 27: management.NetworkMap.remotePeers:type_name -> management.RemotePeerConfig + 32, // 28: management.NetworkMap.Routes:type_name -> management.Route + 33, // 29: management.NetworkMap.DNSConfig:type_name -> management.DNSConfig + 25, // 30: management.NetworkMap.offlinePeers:type_name -> management.RemotePeerConfig + 38, // 31: management.NetworkMap.FirewallRules:type_name -> management.FirewallRule + 42, // 32: management.NetworkMap.routesFirewallRules:type_name -> management.RouteFirewallRule + 43, // 33: management.NetworkMap.forwardingRules:type_name -> management.ForwardingRule + 26, // 34: management.RemotePeerConfig.sshConfig:type_name -> management.SSHConfig + 4, // 35: management.DeviceAuthorizationFlow.Provider:type_name -> management.DeviceAuthorizationFlow.provider + 31, // 36: management.DeviceAuthorizationFlow.ProviderConfig:type_name -> management.ProviderConfig + 31, // 37: management.PKCEAuthorizationFlow.ProviderConfig:type_name -> management.ProviderConfig + 36, // 38: management.DNSConfig.NameServerGroups:type_name -> management.NameServerGroup + 34, // 39: management.DNSConfig.CustomZones:type_name -> management.CustomZone + 35, // 40: management.CustomZone.Records:type_name -> management.SimpleRecord + 37, // 41: management.NameServerGroup.NameServers:type_name -> management.NameServer + 1, // 42: management.FirewallRule.Direction:type_name -> management.RuleDirection + 2, // 43: management.FirewallRule.Action:type_name -> management.RuleAction + 0, // 44: management.FirewallRule.Protocol:type_name -> management.RuleProtocol + 41, // 45: management.FirewallRule.PortInfo:type_name -> management.PortInfo + 44, // 46: management.PortInfo.range:type_name -> management.PortInfo.Range + 2, // 47: management.RouteFirewallRule.action:type_name -> management.RuleAction + 0, // 48: management.RouteFirewallRule.protocol:type_name -> management.RuleProtocol + 41, // 49: management.RouteFirewallRule.portInfo:type_name -> management.PortInfo + 0, // 50: management.ForwardingRule.protocol:type_name -> management.RuleProtocol + 41, // 51: management.ForwardingRule.destinationPort:type_name -> management.PortInfo + 41, // 52: management.ForwardingRule.translatedPort:type_name -> management.PortInfo + 5, // 53: management.ManagementService.Login:input_type -> management.EncryptedMessage + 5, // 54: management.ManagementService.Sync:input_type -> management.EncryptedMessage + 17, // 55: management.ManagementService.GetServerKey:input_type -> management.Empty + 17, // 56: management.ManagementService.isHealthy:input_type -> management.Empty + 5, // 57: management.ManagementService.GetDeviceAuthorizationFlow:input_type -> management.EncryptedMessage + 5, // 58: management.ManagementService.GetPKCEAuthorizationFlow:input_type -> management.EncryptedMessage + 5, // 59: management.ManagementService.SyncMeta:input_type -> management.EncryptedMessage + 5, // 60: management.ManagementService.Login:output_type -> management.EncryptedMessage + 5, // 61: management.ManagementService.Sync:output_type -> management.EncryptedMessage + 16, // 62: management.ManagementService.GetServerKey:output_type -> management.ServerKeyResponse + 17, // 63: management.ManagementService.isHealthy:output_type -> management.Empty + 5, // 64: management.ManagementService.GetDeviceAuthorizationFlow:output_type -> management.EncryptedMessage + 5, // 65: management.ManagementService.GetPKCEAuthorizationFlow:output_type -> management.EncryptedMessage + 17, // 66: management.ManagementService.SyncMeta:output_type -> management.Empty + 60, // [60:67] is the sub-list for method output_type + 53, // [53:60] is the sub-list for method input_type + 53, // [53:53] is the sub-list for extension type_name + 53, // [53:53] is the sub-list for extension extendee + 0, // [0:53] is the sub-list for field type_name } func init() { file_management_proto_init() } @@ -3949,7 +4112,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[16].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ProtectedHostConfig); i { + switch v := v.(*FlowConfig); i { case 0: return &v.state case 1: @@ -3961,7 +4124,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[17].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*PeerConfig); i { + switch v := v.(*ProtectedHostConfig); i { case 0: return &v.state case 1: @@ -3973,7 +4136,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[18].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*NetworkMap); i { + switch v := v.(*PeerConfig); i { case 0: return &v.state case 1: @@ -3985,7 +4148,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[19].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*RemotePeerConfig); i { + switch v := v.(*NetworkMap); i { case 0: return &v.state case 1: @@ -3997,7 +4160,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[20].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*SSHConfig); i { + switch v := v.(*RemotePeerConfig); i { case 0: return &v.state case 1: @@ -4009,7 +4172,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[21].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DeviceAuthorizationFlowRequest); i { + switch v := v.(*SSHConfig); i { case 0: return &v.state case 1: @@ -4021,7 +4184,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[22].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DeviceAuthorizationFlow); i { + switch v := v.(*DeviceAuthorizationFlowRequest); i { case 0: return &v.state case 1: @@ -4033,7 +4196,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[23].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*PKCEAuthorizationFlowRequest); i { + switch v := v.(*DeviceAuthorizationFlow); i { case 0: return &v.state case 1: @@ -4045,7 +4208,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[24].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*PKCEAuthorizationFlow); i { + switch v := v.(*PKCEAuthorizationFlowRequest); i { case 0: return &v.state case 1: @@ -4057,7 +4220,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[25].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ProviderConfig); i { + switch v := v.(*PKCEAuthorizationFlow); i { case 0: return &v.state case 1: @@ -4069,7 +4232,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[26].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Route); i { + switch v := v.(*ProviderConfig); i { case 0: return &v.state case 1: @@ -4081,7 +4244,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[27].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DNSConfig); i { + switch v := v.(*Route); i { case 0: return &v.state case 1: @@ -4093,7 +4256,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[28].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*CustomZone); i { + switch v := v.(*DNSConfig); i { case 0: return &v.state case 1: @@ -4105,7 +4268,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[29].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*SimpleRecord); i { + switch v := v.(*CustomZone); i { case 0: return &v.state case 1: @@ -4117,7 +4280,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[30].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*NameServerGroup); i { + switch v := v.(*SimpleRecord); i { case 0: return &v.state case 1: @@ -4129,7 +4292,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[31].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*NameServer); i { + switch v := v.(*NameServerGroup); i { case 0: return &v.state case 1: @@ -4141,7 +4304,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[32].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*FirewallRule); i { + switch v := v.(*NameServer); i { case 0: return &v.state case 1: @@ -4153,7 +4316,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[33].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*NetworkAddress); i { + switch v := v.(*FirewallRule); i { case 0: return &v.state case 1: @@ -4165,7 +4328,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[34].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Checks); i { + switch v := v.(*NetworkAddress); i { case 0: return &v.state case 1: @@ -4177,7 +4340,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[35].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*PortInfo); i { + switch v := v.(*Checks); i { case 0: return &v.state case 1: @@ -4189,7 +4352,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[36].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*RouteFirewallRule); i { + switch v := v.(*PortInfo); i { case 0: return &v.state case 1: @@ -4201,7 +4364,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[37].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ForwardingRule); i { + switch v := v.(*RouteFirewallRule); i { case 0: return &v.state case 1: @@ -4213,6 +4376,18 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[38].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ForwardingRule); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_management_proto_msgTypes[39].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*PortInfo_Range); i { case 0: return &v.state @@ -4225,7 +4400,7 @@ func file_management_proto_init() { } } } - file_management_proto_msgTypes[35].OneofWrappers = []interface{}{ + file_management_proto_msgTypes[36].OneofWrappers = []interface{}{ (*PortInfo_Port)(nil), (*PortInfo_Range_)(nil), } @@ -4235,7 +4410,7 @@ func file_management_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_management_proto_rawDesc, NumEnums: 5, - NumMessages: 39, + NumMessages: 40, NumExtensions: 0, NumServices: 1, }, diff --git a/management/proto/management.proto b/management/proto/management.proto index 863fa2c48..f7d11fdac 100644 --- a/management/proto/management.proto +++ b/management/proto/management.proto @@ -1,6 +1,7 @@ syntax = "proto3"; import "google/protobuf/timestamp.proto"; +import "google/protobuf/duration.proto"; option go_package = "/proto"; @@ -97,7 +98,7 @@ message LoginRequest { string jwtToken = 3; // Can be absent for now. PeerKeys peerKeys = 4; - + repeated string dnsLabels = 5; } @@ -191,6 +192,8 @@ message NetbirdConfig { HostConfig signal = 3; RelayConfig relay = 4; + + FlowConfig flow = 5; } // HostConfig describes connection properties of some server (e.g. STUN, Signal, Management) @@ -214,6 +217,21 @@ message RelayConfig { string tokenSignature = 3; } +message FlowConfig { + string url = 1; + string tokenPayload = 2; + string tokenSignature = 3; + google.protobuf.Duration interval = 4; + bool enabled = 5; + + // counters determines if flow packets and bytes counters should be sent + bool counters = 6; + // exitNodeCollection determines if event collection on exit nodes should be enabled + bool exitNodeCollection = 7; + // dnsCollection determines if DNS event collection should be enabled + bool dnsCollection = 8; +} + // ProtectedHostConfig is similar to HostConfig but has additional user and password // Mostly used for TURN servers message ProtectedHostConfig { @@ -434,6 +452,9 @@ message FirewallRule { RuleProtocol Protocol = 4; string Port = 5; PortInfo PortInfo = 6; + + // PolicyID is the ID of the policy that this rule belongs to + bytes PolicyID = 7; } message NetworkAddress { @@ -483,6 +504,9 @@ message RouteFirewallRule { // CustomProtocol is a custom protocol ID. uint32 customProtocol = 8; + + // PolicyID is the ID of the policy that this rule belongs to + bytes PolicyID = 9; } message ForwardingRule { @@ -493,7 +517,6 @@ message ForwardingRule { PortInfo destinationPort = 2; // IP address of the translated address (remote peer) to send traffic to - // todo type pending bytes translatedAddress = 3; // Translated port information, where the traffic should be forwarded to diff --git a/management/server/account.go b/management/server/account.go index 80821ce91..8aa4e5dd0 100644 --- a/management/server/account.go +++ b/management/server/account.go @@ -6,7 +6,6 @@ import ( "fmt" "math/rand" "net" - "net/netip" "reflect" "regexp" "slices" @@ -14,16 +13,16 @@ import ( "sync" "time" - "github.com/eko/gocache/v3/cache" - cacheStore "github.com/eko/gocache/v3/store" - gocache "github.com/patrickmn/go-cache" + cacheStore "github.com/eko/gocache/lib/v4/store" "github.com/rs/xid" log "github.com/sirupsen/logrus" + "github.com/vmihailenco/msgpack/v5" "golang.org/x/exp/maps" nbdns "github.com/netbirdio/netbird/dns" - "github.com/netbirdio/netbird/management/domain" + "github.com/netbirdio/netbird/management/server/account" "github.com/netbirdio/netbird/management/server/activity" + nbcache "github.com/netbirdio/netbird/management/server/cache" nbcontext "github.com/netbirdio/netbird/management/server/context" "github.com/netbirdio/netbird/management/server/geolocation" "github.com/netbirdio/netbird/management/server/idp" @@ -31,6 +30,7 @@ import ( "github.com/netbirdio/netbird/management/server/integrations/port_forwarding" nbpeer "github.com/netbirdio/netbird/management/server/peer" "github.com/netbirdio/netbird/management/server/posture" + "github.com/netbirdio/netbird/management/server/settings" "github.com/netbirdio/netbird/management/server/status" "github.com/netbirdio/netbird/management/server/store" "github.com/netbirdio/netbird/management/server/telemetry" @@ -40,8 +40,6 @@ import ( ) const ( - CacheExpirationMax = 7 * 24 * 3600 * time.Second // 7 days - CacheExpirationMin = 3 * 24 * 3600 * time.Second // 3 days peerSchedulerRetryInterval = 3 * time.Second emptyUserID = "empty user ID in claims" errorGettingDomainAccIDFmt = "error getting account ID by private domain: %v" @@ -49,104 +47,11 @@ const ( type userLoggedInOnce bool -type ExternalCacheManager cache.CacheInterface[*idp.UserData] - func cacheEntryExpiration() time.Duration { - r := rand.Intn(int(CacheExpirationMax.Milliseconds()-CacheExpirationMin.Milliseconds())) + int(CacheExpirationMin.Milliseconds()) + r := rand.Intn(int(nbcache.DefaultIDPCacheExpirationMax.Milliseconds()-nbcache.DefaultIDPCacheExpirationMin.Milliseconds())) + int(nbcache.DefaultIDPCacheExpirationMin.Milliseconds()) return time.Duration(r) * time.Millisecond } -type AccountManager interface { - GetOrCreateAccountByUser(ctx context.Context, userId, domain string) (*types.Account, error) - GetAccount(ctx context.Context, accountID string) (*types.Account, error) - CreateSetupKey(ctx context.Context, accountID string, keyName string, keyType types.SetupKeyType, expiresIn time.Duration, - autoGroups []string, usageLimit int, userID string, ephemeral bool, allowExtraDNSLabels bool) (*types.SetupKey, error) - SaveSetupKey(ctx context.Context, accountID string, key *types.SetupKey, userID string) (*types.SetupKey, error) - CreateUser(ctx context.Context, accountID, initiatorUserID string, key *types.UserInfo) (*types.UserInfo, error) - DeleteUser(ctx context.Context, accountID, initiatorUserID string, targetUserID string) error - DeleteRegularUsers(ctx context.Context, accountID, initiatorUserID string, targetUserIDs []string, userInfos map[string]*types.UserInfo) error - InviteUser(ctx context.Context, accountID string, initiatorUserID string, targetUserID string) error - ListSetupKeys(ctx context.Context, accountID, userID string) ([]*types.SetupKey, error) - SaveUser(ctx context.Context, accountID, initiatorUserID string, update *types.User) (*types.UserInfo, error) - SaveOrAddUser(ctx context.Context, accountID, initiatorUserID string, update *types.User, addIfNotExists bool) (*types.UserInfo, error) - SaveOrAddUsers(ctx context.Context, accountID, initiatorUserID string, updates []*types.User, addIfNotExists bool) ([]*types.UserInfo, error) - GetSetupKey(ctx context.Context, accountID, userID, keyID string) (*types.SetupKey, error) - GetAccountByID(ctx context.Context, accountID string, userID string) (*types.Account, error) - AccountExists(ctx context.Context, accountID string) (bool, error) - GetAccountIDByUserID(ctx context.Context, userID, domain string) (string, error) - GetAccountIDFromUserAuth(ctx context.Context, userAuth nbcontext.UserAuth) (string, string, error) - DeleteAccount(ctx context.Context, accountID, userID string) error - GetUserByID(ctx context.Context, id string) (*types.User, error) - GetUserFromUserAuth(ctx context.Context, userAuth nbcontext.UserAuth) (*types.User, error) - ListUsers(ctx context.Context, accountID string) ([]*types.User, error) - GetPeers(ctx context.Context, accountID, userID, nameFilter, ipFilter string) ([]*nbpeer.Peer, error) - MarkPeerConnected(ctx context.Context, peerKey string, connected bool, realIP net.IP, accountID string) error - DeletePeer(ctx context.Context, accountID, peerID, userID string) error - UpdatePeer(ctx context.Context, accountID, userID string, peer *nbpeer.Peer) (*nbpeer.Peer, error) - GetNetworkMap(ctx context.Context, peerID string) (*types.NetworkMap, error) - GetPeerNetwork(ctx context.Context, peerID string) (*types.Network, error) - AddPeer(ctx context.Context, setupKey, userID string, peer *nbpeer.Peer) (*nbpeer.Peer, *types.NetworkMap, []*posture.Checks, error) - CreatePAT(ctx context.Context, accountID string, initiatorUserID string, targetUserID string, tokenName string, expiresIn int) (*types.PersonalAccessTokenGenerated, error) - DeletePAT(ctx context.Context, accountID string, initiatorUserID string, targetUserID string, tokenID string) error - GetPAT(ctx context.Context, accountID string, initiatorUserID string, targetUserID string, tokenID string) (*types.PersonalAccessToken, error) - GetAllPATs(ctx context.Context, accountID string, initiatorUserID string, targetUserID string) ([]*types.PersonalAccessToken, error) - GetUsersFromAccount(ctx context.Context, accountID, userID string) (map[string]*types.UserInfo, error) - GetGroup(ctx context.Context, accountId, groupID, userID string) (*types.Group, error) - GetAllGroups(ctx context.Context, accountID, userID string) ([]*types.Group, error) - GetGroupByName(ctx context.Context, groupName, accountID string) (*types.Group, error) - SaveGroup(ctx context.Context, accountID, userID string, group *types.Group) error - SaveGroups(ctx context.Context, accountID, userID string, newGroups []*types.Group) error - DeleteGroup(ctx context.Context, accountId, userId, groupID string) error - DeleteGroups(ctx context.Context, accountId, userId string, groupIDs []string) error - GroupAddPeer(ctx context.Context, accountId, groupID, peerID string) error - GroupDeletePeer(ctx context.Context, accountId, groupID, peerID string) error - GetPeerGroups(ctx context.Context, accountID, peerID string) ([]*types.Group, error) - GetPolicy(ctx context.Context, accountID, policyID, userID string) (*types.Policy, error) - SavePolicy(ctx context.Context, accountID, userID string, policy *types.Policy) (*types.Policy, error) - DeletePolicy(ctx context.Context, accountID, policyID, userID string) error - ListPolicies(ctx context.Context, accountID, userID string) ([]*types.Policy, error) - GetRoute(ctx context.Context, accountID string, routeID route.ID, userID string) (*route.Route, error) - CreateRoute(ctx context.Context, accountID string, prefix netip.Prefix, networkType route.NetworkType, domains domain.List, peerID string, peerGroupIDs []string, description string, netID route.NetID, masquerade bool, metric int, groups, accessControlGroupIDs []string, enabled bool, userID string, keepRoute bool) (*route.Route, error) - SaveRoute(ctx context.Context, accountID, userID string, route *route.Route) error - DeleteRoute(ctx context.Context, accountID string, routeID route.ID, userID string) error - ListRoutes(ctx context.Context, accountID, userID string) ([]*route.Route, error) - GetNameServerGroup(ctx context.Context, accountID, userID, nsGroupID string) (*nbdns.NameServerGroup, error) - CreateNameServerGroup(ctx context.Context, accountID string, name, description string, nameServerList []nbdns.NameServer, groups []string, primary bool, domains []string, enabled bool, userID string, searchDomainsEnabled bool) (*nbdns.NameServerGroup, error) - SaveNameServerGroup(ctx context.Context, accountID, userID string, nsGroupToSave *nbdns.NameServerGroup) error - DeleteNameServerGroup(ctx context.Context, accountID, nsGroupID, userID string) error - ListNameServerGroups(ctx context.Context, accountID string, userID string) ([]*nbdns.NameServerGroup, error) - GetDNSDomain() string - StoreEvent(ctx context.Context, initiatorID, targetID, accountID string, activityID activity.ActivityDescriber, meta map[string]any) - GetEvents(ctx context.Context, accountID, userID string) ([]*activity.Event, error) - GetDNSSettings(ctx context.Context, accountID string, userID string) (*types.DNSSettings, error) - SaveDNSSettings(ctx context.Context, accountID string, userID string, dnsSettingsToSave *types.DNSSettings) error - GetPeer(ctx context.Context, accountID, peerID, userID string) (*nbpeer.Peer, error) - UpdateAccountSettings(ctx context.Context, accountID, userID string, newSettings *types.Settings) (*types.Account, error) - LoginPeer(ctx context.Context, login PeerLogin) (*nbpeer.Peer, *types.NetworkMap, []*posture.Checks, error) // used by peer gRPC API - SyncPeer(ctx context.Context, sync PeerSync, accountID string) (*nbpeer.Peer, *types.NetworkMap, []*posture.Checks, error) // used by peer gRPC API - GetAllConnectedPeers() (map[string]struct{}, error) - HasConnectedChannel(peerID string) bool - GetExternalCacheManager() ExternalCacheManager - GetPostureChecks(ctx context.Context, accountID, postureChecksID, userID string) (*posture.Checks, error) - SavePostureChecks(ctx context.Context, accountID, userID string, postureChecks *posture.Checks) (*posture.Checks, error) - DeletePostureChecks(ctx context.Context, accountID, postureChecksID, userID string) error - ListPostureChecks(ctx context.Context, accountID, userID string) ([]*posture.Checks, error) - GetIdpManager() idp.Manager - UpdateIntegratedValidatorGroups(ctx context.Context, accountID string, userID string, groups []string) error - GroupValidation(ctx context.Context, accountId string, groups []string) (bool, error) - GetValidatedPeers(ctx context.Context, accountID string) (map[string]struct{}, error) - SyncAndMarkPeer(ctx context.Context, accountID string, peerPubKey string, meta nbpeer.PeerSystemMeta, realIP net.IP) (*nbpeer.Peer, *types.NetworkMap, []*posture.Checks, error) - OnPeerDisconnected(ctx context.Context, accountID string, peerPubKey string) error - SyncPeerMeta(ctx context.Context, peerPubKey string, meta nbpeer.PeerSystemMeta) error - FindExistingPostureCheck(accountID string, checks *posture.ChecksDefinition) (*posture.Checks, error) - GetAccountIDForPeerKey(ctx context.Context, peerKey string) (string, error) - GetAccountSettings(ctx context.Context, accountID string, userID string) (*types.Settings, error) - DeleteSetupKey(ctx context.Context, accountID, userID, keyID string) error - UpdateAccountPeers(ctx context.Context, accountID string) - BuildUserInfosForAccount(ctx context.Context, accountID, initiatorUserID string, accountUsers []*types.User) (map[string]*types.UserInfo, error) - SyncUserJWTGroups(ctx context.Context, userAuth nbcontext.UserAuth) error -} - type DefaultAccountManager struct { Store store.Store // cacheMux and cacheLoading helps to make sure that only a single cache reload runs at a time per accountID @@ -155,8 +60,8 @@ type DefaultAccountManager struct { cacheLoading map[string]chan struct{} peersUpdateManager *PeersUpdateManager idpManager idp.Manager - cacheManager cache.CacheInterface[[]*idp.UserData] - externalCacheManager ExternalCacheManager + cacheManager *nbcache.AccountUserDataCache + externalCacheManager nbcache.UserDataCache ctx context.Context eventStore activity.Store geo geolocation.Geolocation @@ -164,6 +69,7 @@ type DefaultAccountManager struct { requestBuffer *AccountRequestBuffer proxyController port_forwarding.Controller + settingsManager settings.Manager // singleAccountMode indicates whether the instance has a single account. // If true, then every new user will end up under the same account. @@ -249,6 +155,7 @@ func BuildManager( integratedPeerValidator integrated_validator.IntegratedValidator, metrics telemetry.AppMetrics, proxyController port_forwarding.Controller, + settingsManager settings.Manager, ) (*DefaultAccountManager, error) { start := time.Now() defer func() { @@ -272,6 +179,7 @@ func BuildManager( metrics: metrics, requestBuffer: NewAccountRequestBuffer(ctx, store), proxyController: proxyController, + settingsManager: settingsManager, } accountsCounter, err := store.GetAccountsCounter(ctx) if err != nil { @@ -290,14 +198,12 @@ func BuildManager( log.WithContext(ctx).Infof("single account mode disabled, accounts number %d", accountsCounter) } - goCacheClient := gocache.New(CacheExpirationMax, 30*time.Minute) - goCacheStore := cacheStore.NewGoCache(goCacheClient) - am.cacheManager = cache.NewLoadable[[]*idp.UserData](am.loadAccount, cache.New[[]*idp.UserData](goCacheStore)) - - // TODO: what is max expiration time? Should be quite long - am.externalCacheManager = cache.New[*idp.UserData]( - cacheStore.NewGoCache(goCacheClient), - ) + cacheStore, err := nbcache.NewStore(nbcache.DefaultIDPCacheExpirationMax, nbcache.DefaultIDPCacheCleanupInterval) + if err != nil { + return nil, fmt.Errorf("getting cache store: %s", err) + } + am.externalCacheManager = nbcache.NewUserDataCache(cacheStore) + am.cacheManager = nbcache.NewAccountUserDataCache(am.loadAccount, cacheStore) if !isNil(am.idpManager) { go func() { @@ -317,7 +223,7 @@ func BuildManager( return am, nil } -func (am *DefaultAccountManager) GetExternalCacheManager() ExternalCacheManager { +func (am *DefaultAccountManager) GetExternalCacheManager() account.ExternalCacheManager { return am.externalCacheManager } @@ -406,7 +312,12 @@ func (am *DefaultAccountManager) UpdateAccountSettings(ctx context.Context, acco return nil, err } - if updateAccountPeers { + extraSettingsChanged, err := am.settingsManager.UpdateExtraSettings(ctx, accountID, userID, newSettings.Extra) + if err != nil { + return nil, err + } + + if updateAccountPeers || extraSettingsChanged { go am.UpdateAccountPeers(ctx, accountID) } @@ -574,7 +485,7 @@ func (am *DefaultAccountManager) warmupIDPCache(ctx context.Context) error { rcvdUsers := 0 for accountID, users := range userData { rcvdUsers += len(users) - err = am.cacheManager.Set(am.ctx, accountID, users, cacheStore.WithExpiration(cacheEntryExpiration())) + err = am.cacheManager.Set(am.ctx, accountID, users, cacheEntryExpiration()) if err != nil { return err } @@ -718,18 +629,18 @@ func (am *DefaultAccountManager) addAccountIDToIDPAppMeta(ctx context.Context, u return nil } -func (am *DefaultAccountManager) loadAccount(ctx context.Context, accountID interface{}) ([]*idp.UserData, error) { +func (am *DefaultAccountManager) loadAccount(ctx context.Context, accountID any) (any, []cacheStore.Option, error) { log.WithContext(ctx).Debugf("account %s not found in cache, reloading", accountID) accountIDString := fmt.Sprintf("%v", accountID) account, err := am.Store.GetAccount(ctx, accountIDString) if err != nil { - return nil, err + return nil, nil, err } userData, err := am.idpManager.GetAccount(ctx, accountIDString) if err != nil { - return nil, err + return nil, nil, err } log.WithContext(ctx).Debugf("%d entries received from IdP management", len(userData)) @@ -750,7 +661,13 @@ func (am *DefaultAccountManager) loadAccount(ctx context.Context, accountID inte } matchedUserData = append(matchedUserData, datum) } - return matchedUserData, nil + + data, err := msgpack.Marshal(matchedUserData) + if err != nil { + return nil, nil, err + } + + return data, []cacheStore.Option{cacheStore.WithExpiration(cacheEntryExpiration())}, nil } func (am *DefaultAccountManager) lookupUserInCacheByEmail(ctx context.Context, email string, accountID string) (*idp.UserData, error) { @@ -936,7 +853,7 @@ func (am *DefaultAccountManager) removeUserFromCache(ctx context.Context, accoun } } - return am.cacheManager.Set(am.ctx, accountID, data, cacheStore.WithExpiration(cacheEntryExpiration())) + return am.cacheManager.Set(am.ctx, accountID, data, cacheEntryExpiration()) } // updateAccountDomainAttributesIfNotUpToDate updates the account domain attributes if they are not up to date and then, saves the account changes @@ -1476,7 +1393,7 @@ func (am *DefaultAccountManager) SyncAndMarkPeer(ctx context.Context, accountID peerUnlock := am.Store.AcquireWriteLockByUID(ctx, peerPubKey) defer peerUnlock() - peer, netMap, postureChecks, err := am.SyncPeer(ctx, PeerSync{WireGuardPubKey: peerPubKey, Meta: meta}, accountID) + peer, netMap, postureChecks, err := am.SyncPeer(ctx, types.PeerSync{WireGuardPubKey: peerPubKey, Meta: meta}, accountID) if err != nil { return nil, nil, nil, fmt.Errorf("error syncing peer: %w", err) } @@ -1516,7 +1433,7 @@ func (am *DefaultAccountManager) SyncPeerMeta(ctx context.Context, peerPubKey st unlockPeer := am.Store.AcquireWriteLockByUID(ctx, peerPubKey) defer unlockPeer() - _, _, _, err = am.SyncPeer(ctx, PeerSync{WireGuardPubKey: peerPubKey, Meta: meta, UpdateAccountPeers: true}, accountID) + _, _, _, err = am.SyncPeer(ctx, types.PeerSync{WireGuardPubKey: peerPubKey, Meta: meta, UpdateAccountPeers: true}, accountID) if err != nil { return mapError(ctx, err) } @@ -1685,3 +1602,7 @@ func separateGroups(autoGroups []string, allGroups []*types.Group) ([]string, ma return newAutoGroups, jwtAutoGroups } + +func (am *DefaultAccountManager) GetStore() store.Store { + return am.Store +} diff --git a/management/server/account/account.go b/management/server/account/account.go deleted file mode 100644 index 40f032fbe..000000000 --- a/management/server/account/account.go +++ /dev/null @@ -1,19 +0,0 @@ -package account - -type ExtraSettings struct { - // PeerApprovalEnabled enables or disables the need for peers bo be approved by an administrator - PeerApprovalEnabled bool - - // IntegratedValidatorGroups list of group IDs to be used with integrated approval configurations - IntegratedValidatorGroups []string `gorm:"serializer:json"` -} - -// Copy copies the ExtraSettings struct -func (e *ExtraSettings) Copy() *ExtraSettings { - var cpGroup []string - - return &ExtraSettings{ - PeerApprovalEnabled: e.PeerApprovalEnabled, - IntegratedValidatorGroups: append(cpGroup, e.IntegratedValidatorGroups...), - } -} diff --git a/management/server/account/manager.go b/management/server/account/manager.go new file mode 100644 index 000000000..37c50267b --- /dev/null +++ b/management/server/account/manager.go @@ -0,0 +1,114 @@ +package account + +import ( + "context" + "net" + "net/netip" + "time" + + nbdns "github.com/netbirdio/netbird/dns" + "github.com/netbirdio/netbird/management/domain" + "github.com/netbirdio/netbird/management/server/activity" + nbcache "github.com/netbirdio/netbird/management/server/cache" + nbcontext "github.com/netbirdio/netbird/management/server/context" + "github.com/netbirdio/netbird/management/server/idp" + nbpeer "github.com/netbirdio/netbird/management/server/peer" + "github.com/netbirdio/netbird/management/server/posture" + "github.com/netbirdio/netbird/management/server/store" + "github.com/netbirdio/netbird/management/server/types" + "github.com/netbirdio/netbird/route" +) + +type ExternalCacheManager nbcache.UserDataCache + +type Manager interface { + GetOrCreateAccountByUser(ctx context.Context, userId, domain string) (*types.Account, error) + GetAccount(ctx context.Context, accountID string) (*types.Account, error) + CreateSetupKey(ctx context.Context, accountID string, keyName string, keyType types.SetupKeyType, expiresIn time.Duration, + autoGroups []string, usageLimit int, userID string, ephemeral bool, allowExtraDNSLabels bool) (*types.SetupKey, error) + SaveSetupKey(ctx context.Context, accountID string, key *types.SetupKey, userID string) (*types.SetupKey, error) + CreateUser(ctx context.Context, accountID, initiatorUserID string, key *types.UserInfo) (*types.UserInfo, error) + DeleteUser(ctx context.Context, accountID, initiatorUserID string, targetUserID string) error + DeleteRegularUsers(ctx context.Context, accountID, initiatorUserID string, targetUserIDs []string, userInfos map[string]*types.UserInfo) error + InviteUser(ctx context.Context, accountID string, initiatorUserID string, targetUserID string) error + ListSetupKeys(ctx context.Context, accountID, userID string) ([]*types.SetupKey, error) + SaveUser(ctx context.Context, accountID, initiatorUserID string, update *types.User) (*types.UserInfo, error) + SaveOrAddUser(ctx context.Context, accountID, initiatorUserID string, update *types.User, addIfNotExists bool) (*types.UserInfo, error) + SaveOrAddUsers(ctx context.Context, accountID, initiatorUserID string, updates []*types.User, addIfNotExists bool) ([]*types.UserInfo, error) + GetSetupKey(ctx context.Context, accountID, userID, keyID string) (*types.SetupKey, error) + GetAccountByID(ctx context.Context, accountID string, userID string) (*types.Account, error) + AccountExists(ctx context.Context, accountID string) (bool, error) + GetAccountIDByUserID(ctx context.Context, userID, domain string) (string, error) + GetAccountIDFromUserAuth(ctx context.Context, userAuth nbcontext.UserAuth) (string, string, error) + DeleteAccount(ctx context.Context, accountID, userID string) error + GetUserByID(ctx context.Context, id string) (*types.User, error) + GetUserFromUserAuth(ctx context.Context, userAuth nbcontext.UserAuth) (*types.User, error) + ListUsers(ctx context.Context, accountID string) ([]*types.User, error) + GetPeers(ctx context.Context, accountID, userID, nameFilter, ipFilter string) ([]*nbpeer.Peer, error) + MarkPeerConnected(ctx context.Context, peerKey string, connected bool, realIP net.IP, accountID string) error + DeletePeer(ctx context.Context, accountID, peerID, userID string) error + UpdatePeer(ctx context.Context, accountID, userID string, peer *nbpeer.Peer) (*nbpeer.Peer, error) + GetNetworkMap(ctx context.Context, peerID string) (*types.NetworkMap, error) + GetPeerNetwork(ctx context.Context, peerID string) (*types.Network, error) + AddPeer(ctx context.Context, setupKey, userID string, peer *nbpeer.Peer) (*nbpeer.Peer, *types.NetworkMap, []*posture.Checks, error) + CreatePAT(ctx context.Context, accountID string, initiatorUserID string, targetUserID string, tokenName string, expiresIn int) (*types.PersonalAccessTokenGenerated, error) + DeletePAT(ctx context.Context, accountID string, initiatorUserID string, targetUserID string, tokenID string) error + GetPAT(ctx context.Context, accountID string, initiatorUserID string, targetUserID string, tokenID string) (*types.PersonalAccessToken, error) + GetAllPATs(ctx context.Context, accountID string, initiatorUserID string, targetUserID string) ([]*types.PersonalAccessToken, error) + GetUsersFromAccount(ctx context.Context, accountID, userID string) (map[string]*types.UserInfo, error) + GetGroup(ctx context.Context, accountId, groupID, userID string) (*types.Group, error) + GetAllGroups(ctx context.Context, accountID, userID string) ([]*types.Group, error) + GetGroupByName(ctx context.Context, groupName, accountID string) (*types.Group, error) + SaveGroup(ctx context.Context, accountID, userID string, group *types.Group) error + SaveGroups(ctx context.Context, accountID, userID string, newGroups []*types.Group) error + DeleteGroup(ctx context.Context, accountId, userId, groupID string) error + DeleteGroups(ctx context.Context, accountId, userId string, groupIDs []string) error + GroupAddPeer(ctx context.Context, accountId, groupID, peerID string) error + GroupDeletePeer(ctx context.Context, accountId, groupID, peerID string) error + GetPeerGroups(ctx context.Context, accountID, peerID string) ([]*types.Group, error) + GetPolicy(ctx context.Context, accountID, policyID, userID string) (*types.Policy, error) + SavePolicy(ctx context.Context, accountID, userID string, policy *types.Policy) (*types.Policy, error) + DeletePolicy(ctx context.Context, accountID, policyID, userID string) error + ListPolicies(ctx context.Context, accountID, userID string) ([]*types.Policy, error) + GetRoute(ctx context.Context, accountID string, routeID route.ID, userID string) (*route.Route, error) + CreateRoute(ctx context.Context, accountID string, prefix netip.Prefix, networkType route.NetworkType, domains domain.List, peerID string, peerGroupIDs []string, description string, netID route.NetID, masquerade bool, metric int, groups, accessControlGroupIDs []string, enabled bool, userID string, keepRoute bool) (*route.Route, error) + SaveRoute(ctx context.Context, accountID, userID string, route *route.Route) error + DeleteRoute(ctx context.Context, accountID string, routeID route.ID, userID string) error + ListRoutes(ctx context.Context, accountID, userID string) ([]*route.Route, error) + GetNameServerGroup(ctx context.Context, accountID, userID, nsGroupID string) (*nbdns.NameServerGroup, error) + CreateNameServerGroup(ctx context.Context, accountID string, name, description string, nameServerList []nbdns.NameServer, groups []string, primary bool, domains []string, enabled bool, userID string, searchDomainsEnabled bool) (*nbdns.NameServerGroup, error) + SaveNameServerGroup(ctx context.Context, accountID, userID string, nsGroupToSave *nbdns.NameServerGroup) error + DeleteNameServerGroup(ctx context.Context, accountID, nsGroupID, userID string) error + ListNameServerGroups(ctx context.Context, accountID string, userID string) ([]*nbdns.NameServerGroup, error) + GetDNSDomain() string + StoreEvent(ctx context.Context, initiatorID, targetID, accountID string, activityID activity.ActivityDescriber, meta map[string]any) + GetEvents(ctx context.Context, accountID, userID string) ([]*activity.Event, error) + GetDNSSettings(ctx context.Context, accountID string, userID string) (*types.DNSSettings, error) + SaveDNSSettings(ctx context.Context, accountID string, userID string, dnsSettingsToSave *types.DNSSettings) error + GetPeer(ctx context.Context, accountID, peerID, userID string) (*nbpeer.Peer, error) + UpdateAccountSettings(ctx context.Context, accountID, userID string, newSettings *types.Settings) (*types.Account, error) + LoginPeer(ctx context.Context, login types.PeerLogin) (*nbpeer.Peer, *types.NetworkMap, []*posture.Checks, error) // used by peer gRPC API + SyncPeer(ctx context.Context, sync types.PeerSync, accountID string) (*nbpeer.Peer, *types.NetworkMap, []*posture.Checks, error) // used by peer gRPC API + GetAllConnectedPeers() (map[string]struct{}, error) + HasConnectedChannel(peerID string) bool + GetExternalCacheManager() ExternalCacheManager + GetPostureChecks(ctx context.Context, accountID, postureChecksID, userID string) (*posture.Checks, error) + SavePostureChecks(ctx context.Context, accountID, userID string, postureChecks *posture.Checks) (*posture.Checks, error) + DeletePostureChecks(ctx context.Context, accountID, postureChecksID, userID string) error + ListPostureChecks(ctx context.Context, accountID, userID string) ([]*posture.Checks, error) + GetIdpManager() idp.Manager + UpdateIntegratedValidatorGroups(ctx context.Context, accountID string, userID string, groups []string) error + GroupValidation(ctx context.Context, accountId string, groups []string) (bool, error) + GetValidatedPeers(ctx context.Context, accountID string) (map[string]struct{}, error) + SyncAndMarkPeer(ctx context.Context, accountID string, peerPubKey string, meta nbpeer.PeerSystemMeta, realIP net.IP) (*nbpeer.Peer, *types.NetworkMap, []*posture.Checks, error) + OnPeerDisconnected(ctx context.Context, accountID string, peerPubKey string) error + SyncPeerMeta(ctx context.Context, peerPubKey string, meta nbpeer.PeerSystemMeta) error + FindExistingPostureCheck(accountID string, checks *posture.ChecksDefinition) (*posture.Checks, error) + GetAccountIDForPeerKey(ctx context.Context, peerKey string) (string, error) + GetAccountSettings(ctx context.Context, accountID string, userID string) (*types.Settings, error) + DeleteSetupKey(ctx context.Context, accountID, userID, keyID string) error + UpdateAccountPeers(ctx context.Context, accountID string) + BuildUserInfosForAccount(ctx context.Context, accountID, initiatorUserID string, accountUsers []*types.User) (map[string]*types.UserInfo, error) + SyncUserJWTGroups(ctx context.Context, userAuth nbcontext.UserAuth) error + GetStore() store.Store +} diff --git a/management/server/account_test.go b/management/server/account_test.go index 9a6828940..1cfcf127c 100644 --- a/management/server/account_test.go +++ b/management/server/account_test.go @@ -13,7 +13,11 @@ import ( "testing" "time" + "github.com/golang/mock/gomock" + + nbAccount "github.com/netbirdio/netbird/management/server/account" "github.com/netbirdio/netbird/management/server/integrations/port_forwarding" + "github.com/netbirdio/netbird/management/server/settings" "github.com/netbirdio/netbird/management/server/util" resourceTypes "github.com/netbirdio/netbird/management/server/networks/resources/types" @@ -36,7 +40,7 @@ import ( "github.com/netbirdio/netbird/route" ) -func verifyCanAddPeerToAccount(t *testing.T, manager AccountManager, account *types.Account, userID string) { +func verifyCanAddPeerToAccount(t *testing.T, manager nbAccount.Manager, account *types.Account, userID string) { t.Helper() peer := &nbpeer.Peer{ Key: "BhRPtynAAYRDy08+q4HTMsos8fs4plTP4NOSh7C1ry8=", @@ -1403,7 +1407,7 @@ func TestAccountManager_DeletePeer(t *testing.T) { assert.Equal(t, peer.IP.String(), fmt.Sprint(ev.Meta["ip"])) } -func getEvent(t *testing.T, accountID string, manager AccountManager, eventType activity.Activity) *activity.Event { +func getEvent(t *testing.T, accountID string, manager nbAccount.Manager, eventType activity.Activity) *activity.Event { t.Helper() for { select { @@ -2793,6 +2797,8 @@ type TB interface { Cleanup(func()) Helper() TempDir() string + Errorf(format string, args ...interface{}) + Fatalf(format string, args ...interface{}) } func createManager(t TB) (*DefaultAccountManager, error) { @@ -2809,7 +2815,20 @@ func createManager(t TB) (*DefaultAccountManager, error) { return nil, err } - manager, err := BuildManager(context.Background(), store, NewPeersUpdateManager(nil), nil, "", "netbird.cloud", eventStore, nil, false, MocIntegratedValidator{}, metrics, port_forwarding.NewControllerMock()) + ctrl := gomock.NewController(t) + t.Cleanup(ctrl.Finish) + + settingsMockManager := settings.NewMockManager(ctrl) + settingsMockManager.EXPECT(). + GetExtraSettings(gomock.Any(), gomock.Any()). + Return(&types.ExtraSettings{}, nil). + AnyTimes() + settingsMockManager.EXPECT(). + UpdateExtraSettings(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + Return(false, nil). + AnyTimes() + + manager, err := BuildManager(context.Background(), store, NewPeersUpdateManager(nil), nil, "", "netbird.cloud", eventStore, nil, false, MocIntegratedValidator{}, metrics, port_forwarding.NewControllerMock(), settingsMockManager) if err != nil { return nil, err } @@ -3024,7 +3043,7 @@ func BenchmarkLoginPeer_ExistingPeer(b *testing.B) { b.ResetTimer() start := time.Now() for i := 0; i < b.N; i++ { - _, _, _, err := manager.LoginPeer(context.Background(), PeerLogin{ + _, _, _, err := manager.LoginPeer(context.Background(), types.PeerLogin{ WireGuardPubKey: account.Peers["peer-1"].Key, SSHKey: "someKey", Meta: nbpeer.PeerSystemMeta{Hostname: strconv.Itoa(i)}, @@ -3099,7 +3118,7 @@ func BenchmarkLoginPeer_NewPeer(b *testing.B) { b.ResetTimer() start := time.Now() for i := 0; i < b.N; i++ { - _, _, _, err := manager.LoginPeer(context.Background(), PeerLogin{ + _, _, _, err := manager.LoginPeer(context.Background(), types.PeerLogin{ WireGuardPubKey: "some-new-key" + strconv.Itoa(i), SSHKey: "someKey", Meta: nbpeer.PeerSystemMeta{Hostname: strconv.Itoa(i)}, diff --git a/management/server/activity/codes.go b/management/server/activity/codes.go index 5379a8dd8..46ae754cf 100644 --- a/management/server/activity/codes.go +++ b/management/server/activity/codes.go @@ -172,8 +172,8 @@ const ( ) var activityMap = map[Activity]Code{ - PeerAddedByUser: {"Peer added", "user.peer.add"}, - PeerAddedWithSetupKey: {"Peer added", "setupkey.peer.add"}, + PeerAddedByUser: {"Peer added", "peer.user.add"}, + PeerAddedWithSetupKey: {"Peer added", "peer.setupkey.add"}, UserJoined: {"User joined", "user.join"}, UserInvited: {"User invited", "user.invite"}, AccountCreated: {"Account created", "account.create"}, @@ -232,9 +232,9 @@ var activityMap = map[Activity]Code{ PeerApproved: {"Peer approved", "peer.approve"}, PeerApprovalRevoked: {"Peer approval revoked", "peer.approval.revoke"}, TransferredOwnerRole: {"Transferred owner role", "transferred.owner.role"}, - PostureCheckCreated: {"Posture check created", "posture.check.created"}, - PostureCheckUpdated: {"Posture check updated", "posture.check.updated"}, - PostureCheckDeleted: {"Posture check deleted", "posture.check.deleted"}, + PostureCheckCreated: {"Posture check created", "posture.check.create"}, + PostureCheckUpdated: {"Posture check updated", "posture.check.update"}, + PostureCheckDeleted: {"Posture check deleted", "posture.check.delete"}, PeerInactivityExpirationEnabled: {"Peer inactivity expiration enabled", "peer.inactivity.expiration.enable"}, PeerInactivityExpirationDisabled: {"Peer inactivity expiration disabled", "peer.inactivity.expiration.disable"}, diff --git a/management/server/cache/idp.go b/management/server/cache/idp.go new file mode 100644 index 000000000..1b31ff82a --- /dev/null +++ b/management/server/cache/idp.go @@ -0,0 +1,113 @@ +package cache + +import ( + "context" + "fmt" + "time" + + "github.com/eko/gocache/lib/v4/cache" + "github.com/eko/gocache/lib/v4/marshaler" + "github.com/eko/gocache/lib/v4/store" + "github.com/eko/gocache/store/redis/v4" + "github.com/vmihailenco/msgpack/v5" + + "github.com/netbirdio/netbird/management/server/idp" +) + +const ( + DefaultIDPCacheExpirationMax = 7 * 24 * time.Hour // 7 days + DefaultIDPCacheExpirationMin = 3 * 24 * time.Hour // 3 days + DefaultIDPCacheCleanupInterval = 30 * time.Minute +) + +// UserDataCache is an interface that wraps the basic Get, Set and Delete methods for idp.UserData objects. +type UserDataCache interface { + Get(ctx context.Context, key string) (*idp.UserData, error) + Set(ctx context.Context, key string, value *idp.UserData, expiration time.Duration) error + Delete(ctx context.Context, key string) error +} + +// UserDataCacheImpl is a struct that implements the UserDataCache interface. +type UserDataCacheImpl struct { + cache Marshaler +} + +func (u *UserDataCacheImpl) Get(ctx context.Context, key string) (*idp.UserData, error) { + v, err := u.cache.Get(ctx, key, new(idp.UserData)) + if err != nil { + return nil, err + } + + data := v.(*idp.UserData) + return data, nil +} + +func (u *UserDataCacheImpl) Set(ctx context.Context, key string, value *idp.UserData, expiration time.Duration) error { + return u.cache.Set(ctx, key, value, store.WithExpiration(expiration)) +} + +func (u *UserDataCacheImpl) Delete(ctx context.Context, key string) error { + return u.cache.Delete(ctx, key) +} + +// NewUserDataCache creates a new UserDataCacheImpl object. +func NewUserDataCache(store store.StoreInterface) *UserDataCacheImpl { + simpleCache := cache.New[any](store) + if store.GetType() == redis.RedisType { + m := marshaler.New(simpleCache) + return &UserDataCacheImpl{cache: m} + } + return &UserDataCacheImpl{cache: &marshalerWraper{simpleCache}} +} + +// AccountUserDataCache wraps the basic Get, Set and Delete methods for []*idp.UserData objects. +type AccountUserDataCache struct { + cache Marshaler +} + +func (a *AccountUserDataCache) Get(ctx context.Context, key string) ([]*idp.UserData, error) { + var m []*idp.UserData + v, err := a.cache.Get(ctx, key, &m) + if err != nil { + return nil, err + } + + switch v := v.(type) { + case []*idp.UserData: + return v, nil + case *[]*idp.UserData: + return *v, nil + case []byte: + return unmarshalUserData(v) + } + + return nil, fmt.Errorf("unexpected type: %T", v) +} + +func unmarshalUserData(data []byte) ([]*idp.UserData, error) { + returnObj := &[]*idp.UserData{} + err := msgpack.Unmarshal(data, returnObj) + if err != nil { + return nil, err + } + return *returnObj, nil +} + +func (a *AccountUserDataCache) Set(ctx context.Context, key string, value []*idp.UserData, expiration time.Duration) error { + return a.cache.Set(ctx, key, value, store.WithExpiration(expiration)) +} + +func (a *AccountUserDataCache) Delete(ctx context.Context, key string) error { + return a.cache.Delete(ctx, key) +} + +// NewAccountUserDataCache creates a new AccountUserDataCache object. +func NewAccountUserDataCache(loadableFunc cache.LoadFunction[any], store store.StoreInterface) *AccountUserDataCache { + simpleCache := cache.New[any](store) + loadable := cache.NewLoadable[any](loadableFunc, simpleCache) + if store.GetType() == redis.RedisType { + m := marshaler.New(loadable) + return &AccountUserDataCache{cache: m} + } + return &AccountUserDataCache{cache: &marshalerWraper{loadable}} +} diff --git a/management/server/cache/idp_test.go b/management/server/cache/idp_test.go new file mode 100644 index 000000000..a824bff42 --- /dev/null +++ b/management/server/cache/idp_test.go @@ -0,0 +1,135 @@ +package cache_test + +import ( + "context" + "os" + "testing" + "time" + + "github.com/eko/gocache/lib/v4/store" + "github.com/redis/go-redis/v9" + "github.com/testcontainers/testcontainers-go" + testcontainersredis "github.com/testcontainers/testcontainers-go/modules/redis" + "github.com/vmihailenco/msgpack/v5" + + "github.com/netbirdio/netbird/management/server/cache" + "github.com/netbirdio/netbird/management/server/idp" +) + +func TestNewIDPCacheManagers(t *testing.T) { + tt := []struct { + name string + redis bool + }{ + {"memory", false}, + {"redis", true}, + } + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + if tc.redis { + ctx := context.Background() + redisContainer, err := testcontainersredis.RunContainer(ctx, testcontainers.WithImage("redis:7")) + if err != nil { + t.Fatalf("couldn't start redis container: %s", err) + } + defer func() { + if err := redisContainer.Terminate(ctx); err != nil { + t.Logf("failed to terminate container: %s", err) + } + }() + redisURL, err := redisContainer.ConnectionString(ctx) + if err != nil { + t.Fatalf("couldn't get connection string: %s", err) + } + + t.Setenv(cache.RedisStoreEnvVar, redisURL) + } + cacheStore, err := cache.NewStore(cache.DefaultIDPCacheExpirationMax, cache.DefaultIDPCacheCleanupInterval) + if err != nil { + t.Fatalf("couldn't create cache store: %s", err) + } + + simple := cache.NewUserDataCache(cacheStore) + loadable := cache.NewAccountUserDataCache(loader, cacheStore) + + ctx := context.Background() + value := &idp.UserData{ID: "v", Name: "vv"} + err = simple.Set(ctx, "key1", value, time.Minute) + if err != nil { + t.Errorf("couldn't set testing data: %s", err) + } + + result, err := simple.Get(ctx, "key1") + if err != nil { + t.Errorf("couldn't get testing data: %s", err) + } + if value.ID != result.ID || value.Name != result.Name { + t.Errorf("value returned doesn't match testing data, got %v, expected %v", result, "value1") + } + values := []*idp.UserData{ + {ID: "v2", Name: "v2v2"}, + {ID: "v3", Name: "v3v3"}, + {ID: "v4", Name: "v4v4"}, + } + err = loadable.Set(ctx, "key2", values, time.Minute) + + if err != nil { + t.Errorf("couldn't set testing data: %s", err) + } + result2, err := loadable.Get(ctx, "key2") + if err != nil { + t.Errorf("couldn't get testing data: %s", err) + } + + if values[0].ID != result2[0].ID || values[0].Name != result2[0].Name { + t.Errorf("value returned doesn't match testing data, got %v, expected %v", result2[0], values[0]) + } + if values[1].ID != result2[1].ID || values[1].Name != result2[1].Name { + t.Errorf("value returned doesn't match testing data, got %v, expected %v", result2[1], values[1]) + } + + // checking with direct store client + if tc.redis { + // wait for redis to sync + options, err := redis.ParseURL(os.Getenv(cache.RedisStoreEnvVar)) + if err != nil { + t.Fatalf("parsing redis cache url: %s", err) + } + + redisClient := redis.NewClient(options) + _, err = redisClient.Get(ctx, "loadKey").Result() + if err == nil { + t.Errorf("shouldn't find testing data from redis") + } + } + + // testing loadable capability + result2, err = loadable.Get(ctx, "loadKey") + if err != nil { + t.Errorf("couldn't get testing data: %s", err) + } + + if loadData[0].ID != result2[0].ID || loadData[0].Name != result2[0].Name { + t.Errorf("value returned doesn't match testing data, got %v, expected %v", result2[0], loadData[0]) + } + if loadData[1].ID != result2[1].ID || loadData[1].Name != result2[1].Name { + t.Errorf("value returned doesn't match testing data, got %v, expected %v", result2[1], loadData[1]) + } + }) + } + +} + +var loadData = []*idp.UserData{ + {ID: "a", Name: "aa"}, + {ID: "b", Name: "bb"}, + {ID: "c", Name: "cc"}, +} + +func loader(ctx context.Context, key any) (any, []store.Option, error) { + bytes, err := msgpack.Marshal(loadData) + if err != nil { + return nil, nil, err + } + return bytes, nil, nil +} diff --git a/management/server/cache/marshaler.go b/management/server/cache/marshaler.go new file mode 100644 index 000000000..12035b904 --- /dev/null +++ b/management/server/cache/marshaler.go @@ -0,0 +1,35 @@ +package cache + +import ( + "context" + + "github.com/eko/gocache/lib/v4/store" +) + +type Marshaler interface { + Get(ctx context.Context, key any, returnObj any) (any, error) + Set(ctx context.Context, key, object any, options ...store.Option) error + Delete(ctx context.Context, key any) error +} + +type cacher[T any] interface { + Get(ctx context.Context, key any) (T, error) + Set(ctx context.Context, key any, object T, options ...store.Option) error + Delete(ctx context.Context, key any) error +} + +type marshalerWraper struct { + cache cacher[any] +} + +func (m marshalerWraper) Get(ctx context.Context, key any, _ any) (any, error) { + return m.cache.Get(ctx, key) +} + +func (m marshalerWraper) Set(ctx context.Context, key, object any, options ...store.Option) error { + return m.cache.Set(ctx, key, object, options...) +} + +func (m marshalerWraper) Delete(ctx context.Context, key any) error { + return m.cache.Delete(ctx, key) +} diff --git a/management/server/cache/store.go b/management/server/cache/store.go new file mode 100644 index 000000000..e4d4cc648 --- /dev/null +++ b/management/server/cache/store.go @@ -0,0 +1,50 @@ +package cache + +import ( + "context" + "fmt" + "os" + "time" + + "github.com/eko/gocache/lib/v4/store" + gocache_store "github.com/eko/gocache/store/go_cache/v4" + redis_store "github.com/eko/gocache/store/redis/v4" + gocache "github.com/patrickmn/go-cache" + "github.com/redis/go-redis/v9" +) + +// RedisStoreEnvVar is the environment variable that determines if a redis store should be used. +// The value should follow redis URL format. https://github.com/redis/redis-specifications/blob/master/uri/redis.txt +const RedisStoreEnvVar = "NB_IDP_CACHE_REDIS_ADDRESS" + +// NewStore creates a new cache store with the given max timeout and cleanup interval. It checks for the environment Variable RedisStoreEnvVar +// to determine if a redis store should be used. If the environment variable is set, it will attempt to connect to the redis store. +func NewStore(maxTimeout, cleanupInterval time.Duration) (store.StoreInterface, error) { + redisAddr := os.Getenv(RedisStoreEnvVar) + if redisAddr != "" { + return getRedisStore(redisAddr) + } + goc := gocache.New(maxTimeout, cleanupInterval) + return gocache_store.NewGoCache(goc), nil +} + +func getRedisStore(redisEnvAddr string) (store.StoreInterface, error) { + options, err := redis.ParseURL(redisEnvAddr) + if err != nil { + return nil, fmt.Errorf("parsing redis cache url: %s", err) + } + + options.MaxIdleConns = 6 + options.MinIdleConns = 3 + options.MaxActiveConns = 100 + redisClient := redis.NewClient(options) + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + + _, err = redisClient.Ping(ctx).Result() + if err != nil { + return nil, err + } + + return redis_store.NewRedis(redisClient), nil +} diff --git a/management/server/cache/store_test.go b/management/server/cache/store_test.go new file mode 100644 index 000000000..1f1bf5ec6 --- /dev/null +++ b/management/server/cache/store_test.go @@ -0,0 +1,105 @@ +package cache_test + +import ( + "context" + "testing" + "time" + + "github.com/eko/gocache/lib/v4/store" + "github.com/redis/go-redis/v9" + "github.com/testcontainers/testcontainers-go" + + testcontainersredis "github.com/testcontainers/testcontainers-go/modules/redis" + + "github.com/netbirdio/netbird/management/server/cache" +) + +func TestMemoryStore(t *testing.T) { + memStore, err := cache.NewStore(100*time.Millisecond, 300*time.Millisecond) + if err != nil { + t.Fatalf("couldn't create memory store: %s", err) + } + ctx := context.Background() + key, value := "testing", "tested" + err = memStore.Set(ctx, key, value) + if err != nil { + t.Errorf("couldn't set testing data: %s", err) + } + result, err := memStore.Get(ctx, key) + if err != nil { + t.Errorf("couldn't get testing data: %s", err) + } + if value != result.(string) { + t.Errorf("value returned doesn't match testing data, got %s, expected %s", result, value) + } + // test expiration + time.Sleep(300 * time.Millisecond) + _, err = memStore.Get(ctx, key) + if err == nil { + t.Error("value should not be found") + } +} + +func TestRedisStoreConnectionFailure(t *testing.T) { + t.Setenv(cache.RedisStoreEnvVar, "redis://127.0.0.1:6379") + _, err := cache.NewStore(10*time.Millisecond, 30*time.Millisecond) + if err == nil { + t.Fatal("getting redis cache store should return error") + } +} + +func TestRedisStoreConnectionSuccess(t *testing.T) { + ctx := context.Background() + redisContainer, err := testcontainersredis.RunContainer(ctx, testcontainers.WithImage("redis:7")) + if err != nil { + t.Fatalf("couldn't start redis container: %s", err) + } + defer func() { + if err := redisContainer.Terminate(ctx); err != nil { + t.Logf("failed to terminate container: %s", err) + } + }() + redisURL, err := redisContainer.ConnectionString(ctx) + if err != nil { + t.Fatalf("couldn't get connection string: %s", err) + } + + t.Setenv(cache.RedisStoreEnvVar, redisURL) + redisStore, err := cache.NewStore(100*time.Millisecond, 300*time.Millisecond) + if err != nil { + t.Fatalf("couldn't create redis store: %s", err) + } + + key, value := "testing", "tested" + err = redisStore.Set(ctx, key, value, store.WithExpiration(100*time.Millisecond)) + if err != nil { + t.Errorf("couldn't set testing data: %s", err) + } + result, err := redisStore.Get(ctx, key) + if err != nil { + t.Errorf("couldn't get testing data: %s", err) + } + if value != result.(string) { + t.Errorf("value returned doesn't match testing data, got %s, expected %s", result, value) + } + + options, err := redis.ParseURL(redisURL) + if err != nil { + t.Errorf("parsing redis cache url: %s", err) + } + + redisClient := redis.NewClient(options) + r, e := redisClient.Get(ctx, key).Result() + if e != nil { + t.Errorf("couldn't get testing data from redis: %s", e) + } + if value != r { + t.Errorf("value returned from redis doesn't match testing data, got %s, expected %s", r, value) + } + // test expiration + time.Sleep(300 * time.Millisecond) + _, err = redisStore.Get(ctx, key) + if err == nil { + t.Error("value should not be found") + } +} diff --git a/management/server/dns_test.go b/management/server/dns_test.go index 3318dbaed..824557356 100644 --- a/management/server/dns_test.go +++ b/management/server/dns_test.go @@ -8,10 +8,12 @@ import ( "testing" "time" + "github.com/golang/mock/gomock" "github.com/stretchr/testify/assert" nbdns "github.com/netbirdio/netbird/dns" "github.com/netbirdio/netbird/management/server/integrations/port_forwarding" + "github.com/netbirdio/netbird/management/server/settings" "github.com/netbirdio/netbird/management/server/store" "github.com/netbirdio/netbird/management/server/telemetry" "github.com/netbirdio/netbird/management/server/types" @@ -209,7 +211,12 @@ func createDNSManager(t *testing.T) (*DefaultAccountManager, error) { metrics, err := telemetry.NewDefaultAppMetrics(context.Background()) require.NoError(t, err) - return BuildManager(context.Background(), store, NewPeersUpdateManager(nil), nil, "", "netbird.test", eventStore, nil, false, MocIntegratedValidator{}, metrics, port_forwarding.NewControllerMock()) + ctrl := gomock.NewController(t) + t.Cleanup(ctrl.Finish) + + settingsMockManager := settings.NewMockManager(ctrl) + + return BuildManager(context.Background(), store, NewPeersUpdateManager(nil), nil, "", "netbird.test", eventStore, nil, false, MocIntegratedValidator{}, metrics, port_forwarding.NewControllerMock(), settingsMockManager) } func createDNSStore(t *testing.T) (store.Store, error) { diff --git a/management/server/ephemeral.go b/management/server/ephemeral.go index 3d6d01434..3cb9b7536 100644 --- a/management/server/ephemeral.go +++ b/management/server/ephemeral.go @@ -7,6 +7,7 @@ import ( log "github.com/sirupsen/logrus" + nbAccount "github.com/netbirdio/netbird/management/server/account" "github.com/netbirdio/netbird/management/server/activity" nbpeer "github.com/netbirdio/netbird/management/server/peer" "github.com/netbirdio/netbird/management/server/store" @@ -34,7 +35,7 @@ type ephemeralPeer struct { // automatically. Inactivity means the peer disconnected from the Management server. type EphemeralManager struct { store store.Store - accountManager AccountManager + accountManager nbAccount.Manager headPeer *ephemeralPeer tailPeer *ephemeralPeer @@ -43,7 +44,7 @@ type EphemeralManager struct { } // NewEphemeralManager instantiate new EphemeralManager -func NewEphemeralManager(store store.Store, accountManager AccountManager) *EphemeralManager { +func NewEphemeralManager(store store.Store, accountManager nbAccount.Manager) *EphemeralManager { return &EphemeralManager{ store: store, accountManager: accountManager, diff --git a/management/server/ephemeral_test.go b/management/server/ephemeral_test.go index df8fe98c3..38477f7a8 100644 --- a/management/server/ephemeral_test.go +++ b/management/server/ephemeral_test.go @@ -6,6 +6,7 @@ import ( "testing" "time" + nbAccount "github.com/netbirdio/netbird/management/server/account" nbpeer "github.com/netbirdio/netbird/management/server/peer" "github.com/netbirdio/netbird/management/server/store" "github.com/netbirdio/netbird/management/server/types" @@ -27,7 +28,7 @@ func (s *MockStore) GetAllEphemeralPeers(_ context.Context, _ store.LockingStren } type MocAccountManager struct { - AccountManager + nbAccount.Manager store *MockStore } @@ -36,6 +37,10 @@ func (a MocAccountManager) DeletePeer(_ context.Context, accountID, peerID, user return nil //nolint:nil } +func (a MocAccountManager) GetStore() store.Store { + return a.store +} + func TestNewManager(t *testing.T) { startTime := time.Now() timeNow = func() time.Time { diff --git a/management/server/groups/manager.go b/management/server/groups/manager.go index cfc7ee57b..27698a085 100644 --- a/management/server/groups/manager.go +++ b/management/server/groups/manager.go @@ -4,7 +4,7 @@ import ( "context" "fmt" - s "github.com/netbirdio/netbird/management/server" + "github.com/netbirdio/netbird/management/server/account" "github.com/netbirdio/netbird/management/server/activity" "github.com/netbirdio/netbird/management/server/http/api" "github.com/netbirdio/netbird/management/server/permissions" @@ -24,13 +24,13 @@ type Manager interface { type managerImpl struct { store store.Store permissionsManager permissions.Manager - accountManager s.AccountManager + accountManager account.Manager } type mockManager struct { } -func NewManager(store store.Store, permissionsManager permissions.Manager, accountManager s.AccountManager) Manager { +func NewManager(store store.Store, permissionsManager permissions.Manager, accountManager account.Manager) Manager { return &managerImpl{ store: store, permissionsManager: permissionsManager, diff --git a/management/server/grpcserver.go b/management/server/grpcserver.go index 9f77fd242..49b7b4a33 100644 --- a/management/server/grpcserver.go +++ b/management/server/grpcserver.go @@ -18,8 +18,11 @@ import ( "google.golang.org/grpc/peer" "google.golang.org/grpc/status" + integrationsConfig "github.com/netbirdio/management-integrations/integrations/config" "github.com/netbirdio/netbird/encryption" "github.com/netbirdio/netbird/management/proto" + "github.com/netbirdio/netbird/management/server/account" + "github.com/netbirdio/netbird/management/server/activity" "github.com/netbirdio/netbird/management/server/auth" nbContext "github.com/netbirdio/netbird/management/server/context" nbpeer "github.com/netbirdio/netbird/management/server/peer" @@ -32,7 +35,7 @@ import ( // GRPCServer an instance of a Management gRPC API server type GRPCServer struct { - accountManager AccountManager + accountManager account.Manager settingsManager settings.Manager wgKey wgtypes.Key proto.UnimplementedManagementServiceServer @@ -49,7 +52,7 @@ type GRPCServer struct { func NewServer( ctx context.Context, config *Config, - accountManager AccountManager, + accountManager account.Manager, settingsManager settings.Manager, peersUpdateManager *PeersUpdateManager, secretsManager SecretsManager, @@ -184,7 +187,7 @@ func (s *GRPCServer) Sync(req *proto.EncryptedMessage, srv proto.ManagementServi s.ephemeralManager.OnPeerConnected(ctx, peer) - s.secretsManager.SetupRefresh(ctx, peer.ID) + s.secretsManager.SetupRefresh(ctx, accountID, peer.ID) if s.appMetrics != nil { s.appMetrics.GRPCMetrics().CountSyncRequestDuration(time.Since(reqStart)) @@ -457,7 +460,7 @@ func (s *GRPCServer) Login(ctx context.Context, req *proto.EncryptedMessage) (*p sshKey = loginReq.GetPeerKeys().GetSshPubKey() } - peer, netMap, postureChecks, err := s.accountManager.LoginPeer(ctx, PeerLogin{ + peer, netMap, postureChecks, err := s.accountManager.LoginPeer(ctx, types.PeerLogin{ WireGuardPubKey: peerKey.String(), SSHKey: string(sshKey), Meta: extractPeerMeta(ctx, loginReq.GetMeta()), @@ -486,7 +489,7 @@ func (s *GRPCServer) Login(ctx context.Context, req *proto.EncryptedMessage) (*p // if peer has reached this point then it has logged in loginResp := &proto.LoginResponse{ - NetbirdConfig: toNetbirdConfig(s.config, nil, relayToken), + NetbirdConfig: toNetbirdConfig(s.config, nil, relayToken, nil), PeerConfig: toPeerConfig(peer, netMap.Network, s.accountManager.GetDNSDomain(), false), Checks: toProtocolChecks(ctx, postureChecks), } @@ -544,7 +547,7 @@ func ToResponseProto(configProto Protocol) proto.HostConfig_Protocol { } } -func toNetbirdConfig(config *Config, turnCredentials *Token, relayToken *Token) *proto.NetbirdConfig { +func toNetbirdConfig(config *Config, turnCredentials *Token, relayToken *Token, extraSettings *types.ExtraSettings) *proto.NetbirdConfig { if config == nil { return nil } @@ -592,15 +595,24 @@ func toNetbirdConfig(config *Config, turnCredentials *Token, relayToken *Token) } } - return &proto.NetbirdConfig{ - Stuns: stuns, - Turns: turns, - Signal: &proto.HostConfig{ + var signalCfg *proto.HostConfig + if config.Signal != nil { + signalCfg = &proto.HostConfig{ Uri: config.Signal.URI, Protocol: ToResponseProto(config.Signal.Proto), - }, - Relay: relayCfg, + } } + + nbConfig := &proto.NetbirdConfig{ + Stuns: stuns, + Turns: turns, + Signal: signalCfg, + Relay: relayCfg, + } + + integrationsConfig.ExtendNetBirdConfig(nbConfig, extraSettings) + + return nbConfig } func toPeerConfig(peer *nbpeer.Peer, network *types.Network, dnsName string, dnsResolutionOnRoutingPeerEnabled bool) *proto.PeerConfig { @@ -614,10 +626,10 @@ func toPeerConfig(peer *nbpeer.Peer, network *types.Network, dnsName string, dns } } -func toSyncResponse(ctx context.Context, config *Config, peer *nbpeer.Peer, turnCredentials *Token, relayCredentials *Token, networkMap *types.NetworkMap, dnsName string, checks []*posture.Checks, dnsCache *DNSConfigCache, dnsResolutionOnRoutingPeerEnbled bool) *proto.SyncResponse { +func toSyncResponse(ctx context.Context, config *Config, peer *nbpeer.Peer, turnCredentials *Token, relayCredentials *Token, networkMap *types.NetworkMap, dnsName string, checks []*posture.Checks, dnsCache *DNSConfigCache, dnsResolutionOnRoutingPeerEnabled bool, extraSettings *types.ExtraSettings) *proto.SyncResponse { response := &proto.SyncResponse{ - NetbirdConfig: toNetbirdConfig(config, turnCredentials, relayCredentials), - PeerConfig: toPeerConfig(peer, networkMap.Network, dnsName, dnsResolutionOnRoutingPeerEnbled), + NetbirdConfig: toNetbirdConfig(config, turnCredentials, relayCredentials, extraSettings), + PeerConfig: toPeerConfig(peer, networkMap.Network, dnsName, dnsResolutionOnRoutingPeerEnabled), NetworkMap: &proto.NetworkMap{ Serial: networkMap.Network.CurrentSerial(), Routes: toProtocolRoutes(networkMap.Routes), @@ -693,12 +705,12 @@ func (s *GRPCServer) sendInitialSync(ctx context.Context, peerKey wgtypes.Key, p } } - settings, err := s.settingsManager.GetSettings(ctx, peer.AccountID, peer.UserID) + settings, err := s.settingsManager.GetSettings(ctx, peer.AccountID, activity.SystemInitiator) if err != nil { return status.Errorf(codes.Internal, "error handling request") } - plainResp := toSyncResponse(ctx, s.config, peer, turnToken, relayToken, networkMap, s.accountManager.GetDNSDomain(), postureChecks, nil, settings.RoutingPeerDNSResolutionEnabled) + plainResp := toSyncResponse(ctx, s.config, peer, turnToken, relayToken, networkMap, s.accountManager.GetDNSDomain(), postureChecks, nil, settings.RoutingPeerDNSResolutionEnabled, settings.Extra) encryptedResp, err := encryption.EncryptMessage(peerKey, s.wgKey, plainResp) if err != nil { diff --git a/management/server/http/api/openapi.yml b/management/server/http/api/openapi.yml index ab4f7c0ba..31d485161 100644 --- a/management/server/http/api/openapi.yml +++ b/management/server/http/api/openapi.yml @@ -106,6 +106,18 @@ components: description: (Cloud only) Enables or disables peer approval globally. If enabled, all peers added will be in pending state until approved by an admin. type: boolean example: true + network_traffic_logs_enabled: + description: Enables or disables network traffic logs. If enabled, all network traffic logs from peers will be stored. + type: boolean + example: true + network_traffic_packet_counter_enabled: + description: Enables or disables network traffic packet counter. If enabled, network packets and their size will be counted and reported. (This can have an slight impact on performance) + type: boolean + example: true + required: + - peer_approval_enabled + - network_traffic_logs_enabled + - network_traffic_packet_counter_enabled AccountRequest: type: object properties: @@ -1817,6 +1829,137 @@ components: - ingress_start - ingress_end - protocol + NetworkTrafficLocation: + type: object + properties: + city_name: + type: string + description: "Name of the city (if known)." + country_code: + type: string + description: "ISO country code (if known)." + required: + - city_name + - country_code + NetworkTrafficEndpoint: + type: object + properties: + id: + type: string + description: "ID of this endpoint (e.g., peer ID or resource ID)." + type: + type: string + description: "Type of the endpoint object (e.g., UNKNOWN, PEER, HOST_RESOURCE)." + name: + type: string + description: "Name is the name of the endpoint object (e.g., a peer name)." + geo_location: + $ref: '#/components/schemas/NetworkTrafficLocation' + os: + type: string + nullable: true + description: "Operating system of the peer, if applicable." + address: + type: string + description: "IP address (and possibly port) in string form." + example: "100.64.0.10:51820" + dns_label: + type: string + nullable: true + description: "DNS label/name if available." + required: + - id + - type + - name + - geo_location + - os + - address + - dns_label + NetworkTrafficEvent: + type: object + properties: + id: + type: string + description: "ID of the event. Unique." + flow_id: + type: string + description: "FlowID is the ID of the connection flow. Not unique because it can be the same for multiple events (e.g., start and end of the connection)." + reporter_id: + type: string + description: "ID of the reporter of the event (e.g., the peer that reported the event)." + timestamp: + type: string + format: date-time + description: "Timestamp of the event." + source: + $ref: '#/components/schemas/NetworkTrafficEndpoint' + user_id: + type: string + nullable: true + description: "UserID is the ID of the user that initiated the event (can be empty as not every event is user-initiated)." + user_email: + type: string + nullable: true + description: "Email of the user who initiated the event (if any)." + user_name: + type: string + nullable: true + description: "Name of the user who initiated the event (if any)." + destination: + $ref: '#/components/schemas/NetworkTrafficEndpoint' + protocol: + type: integer + description: "Protocol is the protocol of the traffic (e.g. 1 = ICMP, 6 = TCP, 17 = UDP, etc.)." + type: + type: string + description: "Type of the event (e.g. TYPE_UNKNOWN, TYPE_START, TYPE_END, TYPE_DROP)." + direction: + type: string + description: "Direction of the traffic (e.g. DIRECTION_UNKNOWN, INGRESS, EGRESS)." + rx_bytes: + type: integer + description: "Number of bytes received." + rx_packets: + type: integer + description: "Number of packets received." + tx_bytes: + type: integer + description: "Number of bytes transmitted." + tx_packets: + type: integer + description: "Number of packets transmitted." + policy_id: + type: string + description: "ID of the policy that allowed this event." + policy_name: + type: string + description: "Name of the policy that allowed this event." + icmp_type: + type: integer + description: "ICMP type (if applicable)." + icmp_code: + type: integer + description: "ICMP code (if applicable)." + required: + - id + - flow_id + - reporter_id + - timestamp + - source + - user_id + - user_email + - destination + - protocol + - type + - direction + - rx_bytes + - rx_packets + - tx_bytes + - tx_packets + - policy_id + - policy_name + - icmp_type + - icmp_code responses: not_found: description: Resource not found @@ -3972,10 +4115,10 @@ paths: "$ref": "#/components/responses/forbidden" '500': "$ref": "#/components/responses/internal_error" - /api/events: + /api/events/audit: get: - summary: List all Events - description: Returns a list of all events + summary: List all Audit Events + description: Returns a list of all audit events tags: [ Events ] security: - BearerAuth: [ ] @@ -3997,6 +4140,26 @@ paths: "$ref": "#/components/responses/forbidden" '500': "$ref": "#/components/responses/internal_error" + /api/events/network-traffic: + get: + summary: List all Network Traffic Events + description: Returns a list of all network traffic events + tags: [ Events ] + x-cloud-only: true + x-experimental: true + responses: + "200": + description: List of network traffic events + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/NetworkTrafficEvent" + "401": + $ref: "#/components/responses/requires_authentication" + "500": + $ref: "#/components/responses/internal_error" /api/posture-checks: get: summary: List all Posture Checks diff --git a/management/server/http/api/types.gen.go b/management/server/http/api/types.gen.go index fc5d3d707..8838efe31 100644 --- a/management/server/http/api/types.gen.go +++ b/management/server/http/api/types.gen.go @@ -230,8 +230,14 @@ type Account struct { // AccountExtraSettings defines model for AccountExtraSettings. type AccountExtraSettings struct { + // NetworkTrafficLogsEnabled Enables or disables network traffic logs. If enabled, all network traffic logs from peers will be stored. + NetworkTrafficLogsEnabled bool `json:"network_traffic_logs_enabled"` + + // NetworkTrafficPacketCounterEnabled Enables or disables network traffic packet counter. If enabled, network packets and their size will be counted and reported. (This can have an slight impact on performance) + NetworkTrafficPacketCounterEnabled bool `json:"network_traffic_packet_counter_enabled"` + // PeerApprovalEnabled (Cloud only) Enables or disables peer approval globally. If enabled, all peers added will be in pending state until approved by an admin. - PeerApprovalEnabled *bool `json:"peer_approval_enabled,omitempty"` + PeerApprovalEnabled bool `json:"peer_approval_enabled"` } // AccountRequest defines model for AccountRequest. @@ -817,6 +823,97 @@ type NetworkRouterRequest struct { PeerGroups *[]string `json:"peer_groups,omitempty"` } +// NetworkTrafficEndpoint defines model for NetworkTrafficEndpoint. +type NetworkTrafficEndpoint struct { + // Address IP address (and possibly port) in string form. + Address string `json:"address"` + + // DnsLabel DNS label/name if available. + DnsLabel *string `json:"dns_label"` + GeoLocation NetworkTrafficLocation `json:"geo_location"` + + // Id ID of this endpoint (e.g., peer ID or resource ID). + Id string `json:"id"` + + // Name Name is the name of the endpoint object (e.g., a peer name). + Name string `json:"name"` + + // Os Operating system of the peer, if applicable. + Os *string `json:"os"` + + // Type Type of the endpoint object (e.g., UNKNOWN, PEER, HOST_RESOURCE). + Type string `json:"type"` +} + +// NetworkTrafficEvent defines model for NetworkTrafficEvent. +type NetworkTrafficEvent struct { + Destination NetworkTrafficEndpoint `json:"destination"` + + // Direction Direction of the traffic (e.g. DIRECTION_UNKNOWN, INGRESS, EGRESS). + Direction string `json:"direction"` + + // FlowId FlowID is the ID of the connection flow. Not unique because it can be the same for multiple events (e.g., start and end of the connection). + FlowId string `json:"flow_id"` + + // IcmpCode ICMP code (if applicable). + IcmpCode int `json:"icmp_code"` + + // IcmpType ICMP type (if applicable). + IcmpType int `json:"icmp_type"` + + // Id ID of the event. Unique. + Id string `json:"id"` + + // PolicyId ID of the policy that allowed this event. + PolicyId string `json:"policy_id"` + + // PolicyName Name of the policy that allowed this event. + PolicyName string `json:"policy_name"` + + // Protocol Protocol is the protocol of the traffic (e.g. 1 = ICMP, 6 = TCP, 17 = UDP, etc.). + Protocol int `json:"protocol"` + + // ReporterId ID of the reporter of the event (e.g., the peer that reported the event). + ReporterId string `json:"reporter_id"` + + // RxBytes Number of bytes received. + RxBytes int `json:"rx_bytes"` + + // RxPackets Number of packets received. + RxPackets int `json:"rx_packets"` + Source NetworkTrafficEndpoint `json:"source"` + + // Timestamp Timestamp of the event. + Timestamp time.Time `json:"timestamp"` + + // TxBytes Number of bytes transmitted. + TxBytes int `json:"tx_bytes"` + + // TxPackets Number of packets transmitted. + TxPackets int `json:"tx_packets"` + + // Type Type of the event (e.g. TYPE_UNKNOWN, TYPE_START, TYPE_END, TYPE_DROP). + Type string `json:"type"` + + // UserEmail Email of the user who initiated the event (if any). + UserEmail *string `json:"user_email"` + + // UserId UserID is the ID of the user that initiated the event (can be empty as not every event is user-initiated). + UserId *string `json:"user_id"` + + // UserName Name of the user who initiated the event (if any). + UserName *string `json:"user_name"` +} + +// NetworkTrafficLocation defines model for NetworkTrafficLocation. +type NetworkTrafficLocation struct { + // CityName Name of the city (if known). + CityName string `json:"city_name"` + + // CountryCode ISO country code (if known). + CountryCode string `json:"country_code"` +} + // OSVersionCheck Posture check for the version of operating system type OSVersionCheck struct { // Android Posture check for the version of operating system diff --git a/management/server/http/handler.go b/management/server/http/handler.go index f4c4bc763..e4cc8585a 100644 --- a/management/server/http/handler.go +++ b/management/server/http/handler.go @@ -10,10 +10,12 @@ import ( "github.com/netbirdio/management-integrations/integrations" + "github.com/netbirdio/netbird/management/server/account" + "github.com/netbirdio/netbird/management/server/settings" + "github.com/netbirdio/netbird/management/server/integrations/port_forwarding" "github.com/netbirdio/netbird/management/server/permissions" - s "github.com/netbirdio/netbird/management/server" "github.com/netbirdio/netbird/management/server/auth" "github.com/netbirdio/netbird/management/server/geolocation" nbgroups "github.com/netbirdio/netbird/management/server/groups" @@ -41,7 +43,7 @@ const apiPrefix = "/api" // NewAPIHandler creates the Management service HTTP API handler registering all the available endpoints. func NewAPIHandler( ctx context.Context, - accountManager s.AccountManager, + accountManager account.Manager, networksManager nbnetworks.Manager, resourceManager resources.Manager, routerManager routers.Manager, @@ -53,6 +55,7 @@ func NewAPIHandler( proxyController port_forwarding.Controller, permissionsManager permissions.Manager, peersManager nbpeers.Manager, + settingsManager settings.Manager, ) (http.Handler, error) { authMiddleware := middleware.NewAuthMiddleware( @@ -73,11 +76,11 @@ func NewAPIHandler( router.Use(metricsMiddleware.Handler, corsMiddleware.Handler, authMiddleware.Handler, acMiddleware.Handler) - if _, err := integrations.RegisterHandlers(ctx, prefix, router, accountManager, integratedValidator, appMetrics.GetMeter(), permissionsManager, peersManager, proxyController); err != nil { + if _, err := integrations.RegisterHandlers(ctx, prefix, router, accountManager, integratedValidator, appMetrics.GetMeter(), permissionsManager, peersManager, proxyController, settingsManager); err != nil { return nil, fmt.Errorf("register integrations endpoints: %w", err) } - accounts.AddEndpoints(accountManager, router) + accounts.AddEndpoints(accountManager, settingsManager, router) peers.AddEndpoints(accountManager, router) users.AddEndpoints(accountManager, router) setup_keys.AddEndpoints(accountManager, router) diff --git a/management/server/http/handlers/accounts/accounts_handler.go b/management/server/http/handlers/accounts/accounts_handler.go index bc0054a7f..6c8f8028a 100644 --- a/management/server/http/handlers/accounts/accounts_handler.go +++ b/management/server/http/handlers/accounts/accounts_handler.go @@ -7,31 +7,33 @@ import ( "github.com/gorilla/mux" - "github.com/netbirdio/netbird/management/server" "github.com/netbirdio/netbird/management/server/account" nbcontext "github.com/netbirdio/netbird/management/server/context" "github.com/netbirdio/netbird/management/server/http/api" "github.com/netbirdio/netbird/management/server/http/util" + "github.com/netbirdio/netbird/management/server/settings" "github.com/netbirdio/netbird/management/server/status" "github.com/netbirdio/netbird/management/server/types" ) // handler is a handler that handles the server.Account HTTP endpoints type handler struct { - accountManager server.AccountManager + accountManager account.Manager + settingsManager settings.Manager } -func AddEndpoints(accountManager server.AccountManager, router *mux.Router) { - accountsHandler := newHandler(accountManager) +func AddEndpoints(accountManager account.Manager, settingsManager settings.Manager, router *mux.Router) { + accountsHandler := newHandler(accountManager, settingsManager) router.HandleFunc("/accounts/{accountId}", accountsHandler.updateAccount).Methods("PUT", "OPTIONS") router.HandleFunc("/accounts/{accountId}", accountsHandler.deleteAccount).Methods("DELETE", "OPTIONS") router.HandleFunc("/accounts", accountsHandler.getAllAccounts).Methods("GET", "OPTIONS") } // newHandler creates a new handler HTTP handler -func newHandler(accountManager server.AccountManager) *handler { +func newHandler(accountManager account.Manager, settingsManager settings.Manager) *handler { return &handler{ - accountManager: accountManager, + accountManager: accountManager, + settingsManager: settingsManager, } } @@ -45,7 +47,7 @@ func (h *handler) getAllAccounts(w http.ResponseWriter, r *http.Request) { accountID, userID := userAuth.AccountId, userAuth.UserId - settings, err := h.accountManager.GetAccountSettings(r.Context(), accountID, userID) + settings, err := h.settingsManager.GetSettings(r.Context(), accountID, userID) if err != nil { util.WriteError(r.Context(), err, w) return @@ -89,7 +91,11 @@ func (h *handler) updateAccount(w http.ResponseWriter, r *http.Request) { } if req.Settings.Extra != nil { - settings.Extra = &account.ExtraSettings{PeerApprovalEnabled: *req.Settings.Extra.PeerApprovalEnabled} + settings.Extra = &types.ExtraSettings{ + PeerApprovalEnabled: req.Settings.Extra.PeerApprovalEnabled, + FlowEnabled: req.Settings.Extra.NetworkTrafficLogsEnabled, + FlowPacketCounterEnabled: req.Settings.Extra.NetworkTrafficPacketCounterEnabled, + } } if req.Settings.JwtGroupsEnabled != nil { @@ -163,7 +169,11 @@ func toAccountResponse(accountID string, settings *types.Settings) *api.Account } if settings.Extra != nil { - apiSettings.Extra = &api.AccountExtraSettings{PeerApprovalEnabled: &settings.Extra.PeerApprovalEnabled} + apiSettings.Extra = &api.AccountExtraSettings{ + PeerApprovalEnabled: settings.Extra.PeerApprovalEnabled, + NetworkTrafficLogsEnabled: settings.Extra.FlowEnabled, + NetworkTrafficPacketCounterEnabled: settings.Extra.FlowPacketCounterEnabled, + } } return &api.Account{ diff --git a/management/server/http/handlers/accounts/accounts_handler_test.go b/management/server/http/handlers/accounts/accounts_handler_test.go index a8d57a13f..e971a6514 100644 --- a/management/server/http/handlers/accounts/accounts_handler_test.go +++ b/management/server/http/handlers/accounts/accounts_handler_test.go @@ -10,17 +10,27 @@ import ( "testing" "time" + "github.com/golang/mock/gomock" "github.com/gorilla/mux" "github.com/stretchr/testify/assert" nbcontext "github.com/netbirdio/netbird/management/server/context" "github.com/netbirdio/netbird/management/server/http/api" "github.com/netbirdio/netbird/management/server/mock_server" + "github.com/netbirdio/netbird/management/server/settings" "github.com/netbirdio/netbird/management/server/status" "github.com/netbirdio/netbird/management/server/types" ) -func initAccountsTestData(account *types.Account) *handler { +func initAccountsTestData(t *testing.T, account *types.Account) *handler { + ctrl := gomock.NewController(t) + t.Cleanup(ctrl.Finish) + settingsMockManager := settings.NewMockManager(ctrl) + settingsMockManager.EXPECT(). + GetSettings(gomock.Any(), account.Id, "test_user"). + Return(account.Settings, nil). + AnyTimes() + return &handler{ accountManager: &mock_server.MockAccountManager{ GetAccountSettingsFunc: func(ctx context.Context, accountID string, userID string) (*types.Settings, error) { @@ -41,6 +51,7 @@ func initAccountsTestData(account *types.Account) *handler { return accCopy, nil }, }, + settingsManager: settingsMockManager, } } @@ -51,7 +62,7 @@ func TestAccounts_AccountsHandler(t *testing.T) { sr := func(v string) *string { return &v } br := func(v bool) *bool { return &v } - handler := initAccountsTestData(&types.Account{ + handler := initAccountsTestData(t, &types.Account{ Id: accountID, Domain: "hotmail.com", Network: types.NewNetwork(), diff --git a/management/server/http/handlers/dns/dns_settings_handler.go b/management/server/http/handlers/dns/dns_settings_handler.go index 6ff938369..60822c883 100644 --- a/management/server/http/handlers/dns/dns_settings_handler.go +++ b/management/server/http/handlers/dns/dns_settings_handler.go @@ -7,7 +7,7 @@ import ( "github.com/gorilla/mux" log "github.com/sirupsen/logrus" - "github.com/netbirdio/netbird/management/server" + "github.com/netbirdio/netbird/management/server/account" nbcontext "github.com/netbirdio/netbird/management/server/context" "github.com/netbirdio/netbird/management/server/http/api" "github.com/netbirdio/netbird/management/server/http/util" @@ -16,22 +16,22 @@ import ( // dnsSettingsHandler is a handler that returns the DNS settings of the account type dnsSettingsHandler struct { - accountManager server.AccountManager + accountManager account.Manager } -func AddEndpoints(accountManager server.AccountManager, router *mux.Router) { +func AddEndpoints(accountManager account.Manager, router *mux.Router) { addDNSSettingEndpoint(accountManager, router) addDNSNameserversEndpoint(accountManager, router) } -func addDNSSettingEndpoint(accountManager server.AccountManager, router *mux.Router) { +func addDNSSettingEndpoint(accountManager account.Manager, router *mux.Router) { dnsSettingsHandler := newDNSSettingsHandler(accountManager) router.HandleFunc("/dns/settings", dnsSettingsHandler.getDNSSettings).Methods("GET", "OPTIONS") router.HandleFunc("/dns/settings", dnsSettingsHandler.updateDNSSettings).Methods("PUT", "OPTIONS") } // newDNSSettingsHandler returns a new instance of dnsSettingsHandler handler -func newDNSSettingsHandler(accountManager server.AccountManager) *dnsSettingsHandler { +func newDNSSettingsHandler(accountManager account.Manager) *dnsSettingsHandler { return &dnsSettingsHandler{accountManager: accountManager} } diff --git a/management/server/http/handlers/dns/nameservers_handler.go b/management/server/http/handlers/dns/nameservers_handler.go index 33d070477..970be6d8a 100644 --- a/management/server/http/handlers/dns/nameservers_handler.go +++ b/management/server/http/handlers/dns/nameservers_handler.go @@ -9,7 +9,7 @@ import ( log "github.com/sirupsen/logrus" nbdns "github.com/netbirdio/netbird/dns" - "github.com/netbirdio/netbird/management/server" + "github.com/netbirdio/netbird/management/server/account" nbcontext "github.com/netbirdio/netbird/management/server/context" "github.com/netbirdio/netbird/management/server/http/api" "github.com/netbirdio/netbird/management/server/http/util" @@ -18,10 +18,10 @@ import ( // nameserversHandler is the nameserver group handler of the account type nameserversHandler struct { - accountManager server.AccountManager + accountManager account.Manager } -func addDNSNameserversEndpoint(accountManager server.AccountManager, router *mux.Router) { +func addDNSNameserversEndpoint(accountManager account.Manager, router *mux.Router) { nameserversHandler := newNameserversHandler(accountManager) router.HandleFunc("/dns/nameservers", nameserversHandler.getAllNameservers).Methods("GET", "OPTIONS") router.HandleFunc("/dns/nameservers", nameserversHandler.createNameserverGroup).Methods("POST", "OPTIONS") @@ -31,7 +31,7 @@ func addDNSNameserversEndpoint(accountManager server.AccountManager, router *mux } // newNameserversHandler returns a new instance of nameserversHandler handler -func newNameserversHandler(accountManager server.AccountManager) *nameserversHandler { +func newNameserversHandler(accountManager account.Manager) *nameserversHandler { return &nameserversHandler{accountManager: accountManager} } diff --git a/management/server/http/handlers/events/events_handler.go b/management/server/http/handlers/events/events_handler.go index 0fb2295a8..7ebdef78f 100644 --- a/management/server/http/handlers/events/events_handler.go +++ b/management/server/http/handlers/events/events_handler.go @@ -8,7 +8,7 @@ import ( "github.com/gorilla/mux" log "github.com/sirupsen/logrus" - "github.com/netbirdio/netbird/management/server" + "github.com/netbirdio/netbird/management/server/account" "github.com/netbirdio/netbird/management/server/activity" nbcontext "github.com/netbirdio/netbird/management/server/context" "github.com/netbirdio/netbird/management/server/http/api" @@ -17,16 +17,17 @@ import ( // handler HTTP handler type handler struct { - accountManager server.AccountManager + accountManager account.Manager } -func AddEndpoints(accountManager server.AccountManager, router *mux.Router) { +func AddEndpoints(accountManager account.Manager, router *mux.Router) { eventsHandler := newHandler(accountManager) router.HandleFunc("/events", eventsHandler.getAllEvents).Methods("GET", "OPTIONS") + router.HandleFunc("/events/audit", eventsHandler.getAllEvents).Methods("GET", "OPTIONS") } // newHandler creates a new events handler -func newHandler(accountManager server.AccountManager) *handler { +func newHandler(accountManager account.Manager) *handler { return &handler{accountManager: accountManager} } diff --git a/management/server/http/handlers/groups/groups_handler.go b/management/server/http/handlers/groups/groups_handler.go index 2d0b8bdbd..667095018 100644 --- a/management/server/http/handlers/groups/groups_handler.go +++ b/management/server/http/handlers/groups/groups_handler.go @@ -7,10 +7,10 @@ import ( "github.com/gorilla/mux" log "github.com/sirupsen/logrus" + "github.com/netbirdio/netbird/management/server/account" nbcontext "github.com/netbirdio/netbird/management/server/context" nbpeer "github.com/netbirdio/netbird/management/server/peer" - "github.com/netbirdio/netbird/management/server" "github.com/netbirdio/netbird/management/server/http/api" "github.com/netbirdio/netbird/management/server/http/util" "github.com/netbirdio/netbird/management/server/status" @@ -19,10 +19,10 @@ import ( // handler is a handler that returns groups of the account type handler struct { - accountManager server.AccountManager + accountManager account.Manager } -func AddEndpoints(accountManager server.AccountManager, router *mux.Router) { +func AddEndpoints(accountManager account.Manager, router *mux.Router) { groupsHandler := newHandler(accountManager) router.HandleFunc("/groups", groupsHandler.getAllGroups).Methods("GET", "OPTIONS") router.HandleFunc("/groups", groupsHandler.createGroup).Methods("POST", "OPTIONS") @@ -32,7 +32,7 @@ func AddEndpoints(accountManager server.AccountManager, router *mux.Router) { } // newHandler creates a new groups handler -func newHandler(accountManager server.AccountManager) *handler { +func newHandler(accountManager account.Manager) *handler { return &handler{ accountManager: accountManager, } diff --git a/management/server/http/handlers/networks/handler.go b/management/server/http/handlers/networks/handler.go index e52f885d5..1809019a6 100644 --- a/management/server/http/handlers/networks/handler.go +++ b/management/server/http/handlers/networks/handler.go @@ -9,7 +9,7 @@ import ( "github.com/gorilla/mux" log "github.com/sirupsen/logrus" - s "github.com/netbirdio/netbird/management/server" + "github.com/netbirdio/netbird/management/server/account" nbcontext "github.com/netbirdio/netbird/management/server/context" "github.com/netbirdio/netbird/management/server/groups" "github.com/netbirdio/netbird/management/server/http/api" @@ -28,12 +28,12 @@ type handler struct { networksManager networks.Manager resourceManager resources.Manager routerManager routers.Manager - accountManager s.AccountManager + accountManager account.Manager groupsManager groups.Manager } -func AddEndpoints(networksManager networks.Manager, resourceManager resources.Manager, routerManager routers.Manager, groupsManager groups.Manager, accountManager s.AccountManager, router *mux.Router) { +func AddEndpoints(networksManager networks.Manager, resourceManager resources.Manager, routerManager routers.Manager, groupsManager groups.Manager, accountManager account.Manager, router *mux.Router) { addRouterEndpoints(routerManager, router) addResourceEndpoints(resourceManager, groupsManager, router) @@ -45,7 +45,7 @@ func AddEndpoints(networksManager networks.Manager, resourceManager resources.Ma router.HandleFunc("/networks/{networkId}", networksHandler.deleteNetwork).Methods("DELETE", "OPTIONS") } -func newHandler(networksManager networks.Manager, resourceManager resources.Manager, routerManager routers.Manager, groupsManager groups.Manager, accountManager s.AccountManager) *handler { +func newHandler(networksManager networks.Manager, resourceManager resources.Manager, routerManager routers.Manager, groupsManager groups.Manager, accountManager account.Manager) *handler { return &handler{ networksManager: networksManager, resourceManager: resourceManager, diff --git a/management/server/http/handlers/peers/peers_handler.go b/management/server/http/handlers/peers/peers_handler.go index 2336d16cf..9342d84a3 100644 --- a/management/server/http/handlers/peers/peers_handler.go +++ b/management/server/http/handlers/peers/peers_handler.go @@ -9,7 +9,7 @@ import ( "github.com/gorilla/mux" log "github.com/sirupsen/logrus" - "github.com/netbirdio/netbird/management/server" + "github.com/netbirdio/netbird/management/server/account" nbcontext "github.com/netbirdio/netbird/management/server/context" "github.com/netbirdio/netbird/management/server/groups" "github.com/netbirdio/netbird/management/server/http/api" @@ -21,10 +21,10 @@ import ( // Handler is a handler that returns peers of the account type Handler struct { - accountManager server.AccountManager + accountManager account.Manager } -func AddEndpoints(accountManager server.AccountManager, router *mux.Router) { +func AddEndpoints(accountManager account.Manager, router *mux.Router) { peersHandler := NewHandler(accountManager) router.HandleFunc("/peers", peersHandler.GetAllPeers).Methods("GET", "OPTIONS") router.HandleFunc("/peers/{peerId}", peersHandler.HandlePeer). @@ -33,7 +33,7 @@ func AddEndpoints(accountManager server.AccountManager, router *mux.Router) { } // NewHandler creates a new peers Handler -func NewHandler(accountManager server.AccountManager) *Handler { +func NewHandler(accountManager account.Manager) *Handler { return &Handler{ accountManager: accountManager, } diff --git a/management/server/http/handlers/policies/geolocations_handler.go b/management/server/http/handlers/policies/geolocations_handler.go index c4868f879..fb19887dc 100644 --- a/management/server/http/handlers/policies/geolocations_handler.go +++ b/management/server/http/handlers/policies/geolocations_handler.go @@ -6,7 +6,7 @@ import ( "github.com/gorilla/mux" - "github.com/netbirdio/netbird/management/server" + "github.com/netbirdio/netbird/management/server/account" nbcontext "github.com/netbirdio/netbird/management/server/context" "github.com/netbirdio/netbird/management/server/geolocation" "github.com/netbirdio/netbird/management/server/http/api" @@ -20,18 +20,18 @@ var ( // geolocationsHandler is a handler that returns locations. type geolocationsHandler struct { - accountManager server.AccountManager + accountManager account.Manager geolocationManager geolocation.Geolocation } -func addLocationsEndpoint(accountManager server.AccountManager, locationManager geolocation.Geolocation, router *mux.Router) { +func addLocationsEndpoint(accountManager account.Manager, locationManager geolocation.Geolocation, router *mux.Router) { locationHandler := newGeolocationsHandlerHandler(accountManager, locationManager) router.HandleFunc("/locations/countries", locationHandler.getAllCountries).Methods("GET", "OPTIONS") router.HandleFunc("/locations/countries/{country}/cities", locationHandler.getCitiesByCountry).Methods("GET", "OPTIONS") } // newGeolocationsHandlerHandler creates a new Geolocations handler -func newGeolocationsHandlerHandler(accountManager server.AccountManager, geolocationManager geolocation.Geolocation) *geolocationsHandler { +func newGeolocationsHandlerHandler(accountManager account.Manager, geolocationManager geolocation.Geolocation) *geolocationsHandler { return &geolocationsHandler{ accountManager: accountManager, geolocationManager: geolocationManager, diff --git a/management/server/http/handlers/policies/policies_handler.go b/management/server/http/handlers/policies/policies_handler.go index 63fc8a03b..01a09842a 100644 --- a/management/server/http/handlers/policies/policies_handler.go +++ b/management/server/http/handlers/policies/policies_handler.go @@ -7,7 +7,7 @@ import ( "github.com/gorilla/mux" - "github.com/netbirdio/netbird/management/server" + "github.com/netbirdio/netbird/management/server/account" nbcontext "github.com/netbirdio/netbird/management/server/context" "github.com/netbirdio/netbird/management/server/geolocation" "github.com/netbirdio/netbird/management/server/http/api" @@ -18,10 +18,10 @@ import ( // handler is a handler that returns policy of the account type handler struct { - accountManager server.AccountManager + accountManager account.Manager } -func AddEndpoints(accountManager server.AccountManager, locationManager geolocation.Geolocation, router *mux.Router) { +func AddEndpoints(accountManager account.Manager, locationManager geolocation.Geolocation, router *mux.Router) { policiesHandler := newHandler(accountManager) router.HandleFunc("/policies", policiesHandler.getAllPolicies).Methods("GET", "OPTIONS") router.HandleFunc("/policies", policiesHandler.createPolicy).Methods("POST", "OPTIONS") @@ -32,7 +32,7 @@ func AddEndpoints(accountManager server.AccountManager, locationManager geolocat } // newHandler creates a new policies handler -func newHandler(accountManager server.AccountManager) *handler { +func newHandler(accountManager account.Manager) *handler { return &handler{ accountManager: accountManager, } diff --git a/management/server/http/handlers/policies/posture_checks_handler.go b/management/server/http/handlers/policies/posture_checks_handler.go index e6e58da58..b99649dbc 100644 --- a/management/server/http/handlers/policies/posture_checks_handler.go +++ b/management/server/http/handlers/policies/posture_checks_handler.go @@ -6,7 +6,7 @@ import ( "github.com/gorilla/mux" - "github.com/netbirdio/netbird/management/server" + "github.com/netbirdio/netbird/management/server/account" nbcontext "github.com/netbirdio/netbird/management/server/context" "github.com/netbirdio/netbird/management/server/geolocation" "github.com/netbirdio/netbird/management/server/http/api" @@ -17,11 +17,11 @@ import ( // postureChecksHandler is a handler that returns posture checks of the account. type postureChecksHandler struct { - accountManager server.AccountManager + accountManager account.Manager geolocationManager geolocation.Geolocation } -func addPostureCheckEndpoint(accountManager server.AccountManager, locationManager geolocation.Geolocation, router *mux.Router) { +func addPostureCheckEndpoint(accountManager account.Manager, locationManager geolocation.Geolocation, router *mux.Router) { postureCheckHandler := newPostureChecksHandler(accountManager, locationManager) router.HandleFunc("/posture-checks", postureCheckHandler.getAllPostureChecks).Methods("GET", "OPTIONS") router.HandleFunc("/posture-checks", postureCheckHandler.createPostureCheck).Methods("POST", "OPTIONS") @@ -32,7 +32,7 @@ func addPostureCheckEndpoint(accountManager server.AccountManager, locationManag } // newPostureChecksHandler creates a new PostureChecks handler -func newPostureChecksHandler(accountManager server.AccountManager, geolocationManager geolocation.Geolocation) *postureChecksHandler { +func newPostureChecksHandler(accountManager account.Manager, geolocationManager geolocation.Geolocation) *postureChecksHandler { return &postureChecksHandler{ accountManager: accountManager, geolocationManager: geolocationManager, diff --git a/management/server/http/handlers/routes/routes_handler.go b/management/server/http/handlers/routes/routes_handler.go index 0f0d24780..0f1c37eb7 100644 --- a/management/server/http/handlers/routes/routes_handler.go +++ b/management/server/http/handlers/routes/routes_handler.go @@ -9,7 +9,7 @@ import ( "github.com/gorilla/mux" "github.com/netbirdio/netbird/management/domain" - "github.com/netbirdio/netbird/management/server" + "github.com/netbirdio/netbird/management/server/account" nbcontext "github.com/netbirdio/netbird/management/server/context" "github.com/netbirdio/netbird/management/server/http/api" "github.com/netbirdio/netbird/management/server/http/util" @@ -21,10 +21,10 @@ const failedToConvertRoute = "failed to convert route to response: %v" // handler is the routes handler of the account type handler struct { - accountManager server.AccountManager + accountManager account.Manager } -func AddEndpoints(accountManager server.AccountManager, router *mux.Router) { +func AddEndpoints(accountManager account.Manager, router *mux.Router) { routesHandler := newHandler(accountManager) router.HandleFunc("/routes", routesHandler.getAllRoutes).Methods("GET", "OPTIONS") router.HandleFunc("/routes", routesHandler.createRoute).Methods("POST", "OPTIONS") @@ -34,7 +34,7 @@ func AddEndpoints(accountManager server.AccountManager, router *mux.Router) { } // newHandler returns a new instance of routes handler -func newHandler(accountManager server.AccountManager) *handler { +func newHandler(accountManager account.Manager) *handler { return &handler{ accountManager: accountManager, } diff --git a/management/server/http/handlers/setup_keys/setupkeys_handler.go b/management/server/http/handlers/setup_keys/setupkeys_handler.go index 8095f43b0..38ba86fb1 100644 --- a/management/server/http/handlers/setup_keys/setupkeys_handler.go +++ b/management/server/http/handlers/setup_keys/setupkeys_handler.go @@ -3,13 +3,12 @@ package setup_keys import ( "context" "encoding/json" - "net/http" "time" "github.com/gorilla/mux" - "github.com/netbirdio/netbird/management/server" + "github.com/netbirdio/netbird/management/server/account" nbcontext "github.com/netbirdio/netbird/management/server/context" "github.com/netbirdio/netbird/management/server/http/api" "github.com/netbirdio/netbird/management/server/http/util" @@ -19,10 +18,10 @@ import ( // handler is a handler that returns a list of setup keys of the account type handler struct { - accountManager server.AccountManager + accountManager account.Manager } -func AddEndpoints(accountManager server.AccountManager, router *mux.Router) { +func AddEndpoints(accountManager account.Manager, router *mux.Router) { keysHandler := newHandler(accountManager) router.HandleFunc("/setup-keys", keysHandler.getAllSetupKeys).Methods("GET", "OPTIONS") router.HandleFunc("/setup-keys", keysHandler.createSetupKey).Methods("POST", "OPTIONS") @@ -32,7 +31,7 @@ func AddEndpoints(accountManager server.AccountManager, router *mux.Router) { } // newHandler creates a new setup key handler -func newHandler(accountManager server.AccountManager) *handler { +func newHandler(accountManager account.Manager) *handler { return &handler{ accountManager: accountManager, } diff --git a/management/server/http/handlers/users/pat_handler.go b/management/server/http/handlers/users/pat_handler.go index 84fbef93e..90913eac1 100644 --- a/management/server/http/handlers/users/pat_handler.go +++ b/management/server/http/handlers/users/pat_handler.go @@ -6,7 +6,7 @@ import ( "github.com/gorilla/mux" - "github.com/netbirdio/netbird/management/server" + "github.com/netbirdio/netbird/management/server/account" nbcontext "github.com/netbirdio/netbird/management/server/context" "github.com/netbirdio/netbird/management/server/http/api" "github.com/netbirdio/netbird/management/server/http/util" @@ -16,10 +16,10 @@ import ( // patHandler is the nameserver group handler of the account type patHandler struct { - accountManager server.AccountManager + accountManager account.Manager } -func addUsersTokensEndpoint(accountManager server.AccountManager, router *mux.Router) { +func addUsersTokensEndpoint(accountManager account.Manager, router *mux.Router) { tokenHandler := newPATsHandler(accountManager) router.HandleFunc("/users/{userId}/tokens", tokenHandler.getAllTokens).Methods("GET", "OPTIONS") router.HandleFunc("/users/{userId}/tokens", tokenHandler.createToken).Methods("POST", "OPTIONS") @@ -28,7 +28,7 @@ func addUsersTokensEndpoint(accountManager server.AccountManager, router *mux.Ro } // newPATsHandler creates a new patHandler HTTP handler -func newPATsHandler(accountManager server.AccountManager) *patHandler { +func newPATsHandler(accountManager account.Manager) *patHandler { return &patHandler{ accountManager: accountManager, } diff --git a/management/server/http/handlers/users/users_handler.go b/management/server/http/handlers/users/users_handler.go index 3869f21f0..19f56c464 100644 --- a/management/server/http/handlers/users/users_handler.go +++ b/management/server/http/handlers/users/users_handler.go @@ -8,21 +8,21 @@ import ( "github.com/gorilla/mux" log "github.com/sirupsen/logrus" + "github.com/netbirdio/netbird/management/server/account" "github.com/netbirdio/netbird/management/server/http/api" "github.com/netbirdio/netbird/management/server/http/util" "github.com/netbirdio/netbird/management/server/status" "github.com/netbirdio/netbird/management/server/types" - "github.com/netbirdio/netbird/management/server" nbcontext "github.com/netbirdio/netbird/management/server/context" ) // handler is a handler that returns users of the account type handler struct { - accountManager server.AccountManager + accountManager account.Manager } -func AddEndpoints(accountManager server.AccountManager, router *mux.Router) { +func AddEndpoints(accountManager account.Manager, router *mux.Router) { userHandler := newHandler(accountManager) router.HandleFunc("/users", userHandler.getAllUsers).Methods("GET", "OPTIONS") router.HandleFunc("/users/{userId}", userHandler.updateUser).Methods("PUT", "OPTIONS") @@ -33,7 +33,7 @@ func AddEndpoints(accountManager server.AccountManager, router *mux.Router) { } // newHandler creates a new UsersHandler HTTP handler -func newHandler(accountManager server.AccountManager) *handler { +func newHandler(accountManager account.Manager) *handler { return &handler{ accountManager: accountManager, } diff --git a/management/server/http/testing/testing_tools/tools.go b/management/server/http/testing/testing_tools/tools.go index a74299fa0..01c4adcf3 100644 --- a/management/server/http/testing/testing_tools/tools.go +++ b/management/server/http/testing/testing_tools/tools.go @@ -15,7 +15,13 @@ import ( "time" "github.com/golang-jwt/jwt" + "github.com/netbirdio/management-integrations/integrations" + + "github.com/netbirdio/netbird/management/server/account" + "github.com/netbirdio/netbird/management/server/settings" + "github.com/netbirdio/netbird/management/server/users" + "github.com/stretchr/testify/assert" "golang.zx2c4.com/wireguard/wgctrl/wgtypes" @@ -88,7 +94,7 @@ type PerformanceMetrics struct { MaxMsPerOpCICD float64 } -func BuildApiBlackBoxWithDBState(t TB, sqlFile string, expectedPeerUpdate *server.UpdateMessage, validateUpdate bool) (http.Handler, server.AccountManager, chan struct{}) { +func BuildApiBlackBoxWithDBState(t TB, sqlFile string, expectedPeerUpdate *server.UpdateMessage, validateUpdate bool) (http.Handler, account.Manager, chan struct{}) { store, cleanup, err := store.NewTestStoreFromSQL(context.Background(), sqlFile, t.TempDir()) if err != nil { t.Fatalf("Failed to create test store: %v", err) @@ -117,7 +123,9 @@ func BuildApiBlackBoxWithDBState(t TB, sqlFile string, expectedPeerUpdate *serve geoMock := &geolocation.Mock{} validatorMock := server.MocIntegratedValidator{} proxyController := integrations.NewController(store) - am, err := server.BuildManager(context.Background(), store, peersUpdateManager, nil, "", "", &activity.InMemoryEventStore{}, geoMock, false, validatorMock, metrics, proxyController) + userManager := users.NewManager(store) + settingsManager := settings.NewManager(store, userManager, integrations.NewManager(&activity.InMemoryEventStore{})) + am, err := server.BuildManager(context.Background(), store, peersUpdateManager, nil, "", "", &activity.InMemoryEventStore{}, geoMock, false, validatorMock, metrics, proxyController, settingsManager) if err != nil { t.Fatalf("Failed to create manager: %v", err) } @@ -138,7 +146,7 @@ func BuildApiBlackBoxWithDBState(t TB, sqlFile string, expectedPeerUpdate *serve permissionsManagerMock := permissions.NewManagerMock() peersManager := peers.NewManager(store, permissionsManagerMock) - apiHandler, err := nbhttp.NewAPIHandler(context.Background(), am, networksManagerMock, resourcesManagerMock, routersManagerMock, groupsManagerMock, geoMock, authManagerMock, metrics, validatorMock, proxyController, permissionsManagerMock, peersManager) + apiHandler, err := nbhttp.NewAPIHandler(context.Background(), am, networksManagerMock, resourcesManagerMock, routersManagerMock, groupsManagerMock, geoMock, authManagerMock, metrics, validatorMock, proxyController, permissionsManagerMock, peersManager, settingsManager) if err != nil { t.Fatalf("Failed to create API handler: %v", err) } diff --git a/management/server/idp/idp.go b/management/server/idp/idp.go index 0f1ff0f1f..51f99b3b7 100644 --- a/management/server/idp/idp.go +++ b/management/server/idp/idp.go @@ -2,6 +2,7 @@ package idp import ( "context" + "encoding/json" "fmt" "net/http" "strings" @@ -73,6 +74,23 @@ type UserData struct { AppMetadata AppMetadata `json:"app_metadata"` } +func (u *UserData) MarshalBinary() (data []byte, err error) { + return json.Marshal(u) +} + +func (u *UserData) UnmarshalBinary(data []byte) (err error) { + return json.Unmarshal(data, &u) +} + +func (u *UserData) Marshal() (data string, err error) { + d, err := json.Marshal(u) + return string(d), err +} + +func (u *UserData) Unmarshal(data []byte) (err error) { + return json.Unmarshal(data, &u) +} + // AppMetadata user app metadata to associate with a profile type AppMetadata struct { // WTAccountID is a NetBird (previously Wiretrustee) account id to update in the IDP diff --git a/management/server/integrated_validator.go b/management/server/integrated_validator.go index b95ea1699..ef77bf10c 100644 --- a/management/server/integrated_validator.go +++ b/management/server/integrated_validator.go @@ -6,7 +6,6 @@ import ( log "github.com/sirupsen/logrus" - "github.com/netbirdio/netbird/management/server/account" nbpeer "github.com/netbirdio/netbird/management/server/peer" "github.com/netbirdio/netbird/management/server/store" "github.com/netbirdio/netbird/management/server/types" @@ -43,12 +42,12 @@ func (am *DefaultAccountManager) UpdateIntegratedValidatorGroups(ctx context.Con return err } - var extra *account.ExtraSettings + var extra *types.ExtraSettings if a.Settings.Extra != nil { extra = a.Settings.Extra } else { - extra = &account.ExtraSettings{} + extra = &types.ExtraSettings{} a.Settings.Extra = extra } extra.IntegratedValidatorGroups = groups @@ -104,21 +103,21 @@ func (am *DefaultAccountManager) GetValidatedPeers(ctx context.Context, accountI } type MocIntegratedValidator struct { - ValidatePeerFunc func(_ context.Context, update *nbpeer.Peer, peer *nbpeer.Peer, userID string, accountID string, dnsDomain string, peersGroup []string, extraSettings *account.ExtraSettings) (*nbpeer.Peer, bool, error) + ValidatePeerFunc func(_ context.Context, update *nbpeer.Peer, peer *nbpeer.Peer, userID string, accountID string, dnsDomain string, peersGroup []string, extraSettings *types.ExtraSettings) (*nbpeer.Peer, bool, error) } -func (a MocIntegratedValidator) ValidateExtraSettings(_ context.Context, newExtraSettings *account.ExtraSettings, oldExtraSettings *account.ExtraSettings, peers map[string]*nbpeer.Peer, userID string, accountID string) error { +func (a MocIntegratedValidator) ValidateExtraSettings(_ context.Context, newExtraSettings *types.ExtraSettings, oldExtraSettings *types.ExtraSettings, peers map[string]*nbpeer.Peer, userID string, accountID string) error { return nil } -func (a MocIntegratedValidator) ValidatePeer(_ context.Context, update *nbpeer.Peer, peer *nbpeer.Peer, userID string, accountID string, dnsDomain string, peersGroup []string, extraSettings *account.ExtraSettings) (*nbpeer.Peer, bool, error) { +func (a MocIntegratedValidator) ValidatePeer(_ context.Context, update *nbpeer.Peer, peer *nbpeer.Peer, userID string, accountID string, dnsDomain string, peersGroup []string, extraSettings *types.ExtraSettings) (*nbpeer.Peer, bool, error) { if a.ValidatePeerFunc != nil { return a.ValidatePeerFunc(context.Background(), update, peer, userID, accountID, dnsDomain, peersGroup, extraSettings) } return update, false, nil } -func (a MocIntegratedValidator) GetValidatedPeers(accountID string, groups []*types.Group, peers []*nbpeer.Peer, extraSettings *account.ExtraSettings) (map[string]struct{}, error) { +func (a MocIntegratedValidator) GetValidatedPeers(accountID string, groups []*types.Group, peers []*nbpeer.Peer, extraSettings *types.ExtraSettings) (map[string]struct{}, error) { validatedPeers := make(map[string]struct{}) for _, peer := range peers { validatedPeers[peer.ID] = struct{}{} @@ -126,11 +125,11 @@ func (a MocIntegratedValidator) GetValidatedPeers(accountID string, groups []*ty return validatedPeers, nil } -func (MocIntegratedValidator) PreparePeer(_ context.Context, accountID string, peer *nbpeer.Peer, peersGroup []string, extraSettings *account.ExtraSettings) *nbpeer.Peer { +func (MocIntegratedValidator) PreparePeer(_ context.Context, accountID string, peer *nbpeer.Peer, peersGroup []string, extraSettings *types.ExtraSettings) *nbpeer.Peer { return peer } -func (MocIntegratedValidator) IsNotValidPeer(_ context.Context, accountID string, peer *nbpeer.Peer, peersGroup []string, extraSettings *account.ExtraSettings) (bool, bool, error) { +func (MocIntegratedValidator) IsNotValidPeer(_ context.Context, accountID string, peer *nbpeer.Peer, peersGroup []string, extraSettings *types.ExtraSettings) (bool, bool, error) { return false, false, nil } diff --git a/management/server/integrations/extra_settings/manager.go b/management/server/integrations/extra_settings/manager.go new file mode 100644 index 000000000..34763e3dd --- /dev/null +++ b/management/server/integrations/extra_settings/manager.go @@ -0,0 +1,12 @@ +package extra_settings + +import ( + "context" + + "github.com/netbirdio/netbird/management/server/types" +) + +type Manager interface { + GetExtraSettings(ctx context.Context, accountID string) (*types.ExtraSettings, error) + UpdateExtraSettings(ctx context.Context, accountID, userID string, extraSettings *types.ExtraSettings) (bool, error) +} diff --git a/management/server/integrations/integrated_validator/interface.go b/management/server/integrations/integrated_validator/interface.go index ff179e3c0..083baa65e 100644 --- a/management/server/integrations/integrated_validator/interface.go +++ b/management/server/integrations/integrated_validator/interface.go @@ -3,18 +3,17 @@ package integrated_validator import ( "context" - "github.com/netbirdio/netbird/management/server/account" nbpeer "github.com/netbirdio/netbird/management/server/peer" "github.com/netbirdio/netbird/management/server/types" ) // IntegratedValidator interface exists to avoid the circle dependencies type IntegratedValidator interface { - ValidateExtraSettings(ctx context.Context, newExtraSettings *account.ExtraSettings, oldExtraSettings *account.ExtraSettings, peers map[string]*nbpeer.Peer, userID string, accountID string) error - ValidatePeer(ctx context.Context, update *nbpeer.Peer, peer *nbpeer.Peer, userID string, accountID string, dnsDomain string, peersGroup []string, extraSettings *account.ExtraSettings) (*nbpeer.Peer, bool, error) - PreparePeer(ctx context.Context, accountID string, peer *nbpeer.Peer, peersGroup []string, extraSettings *account.ExtraSettings) *nbpeer.Peer - IsNotValidPeer(ctx context.Context, accountID string, peer *nbpeer.Peer, peersGroup []string, extraSettings *account.ExtraSettings) (bool, bool, error) - GetValidatedPeers(accountID string, groups []*types.Group, peers []*nbpeer.Peer, extraSettings *account.ExtraSettings) (map[string]struct{}, error) + ValidateExtraSettings(ctx context.Context, newExtraSettings *types.ExtraSettings, oldExtraSettings *types.ExtraSettings, peers map[string]*nbpeer.Peer, userID string, accountID string) error + ValidatePeer(ctx context.Context, update *nbpeer.Peer, peer *nbpeer.Peer, userID string, accountID string, dnsDomain string, peersGroup []string, extraSettings *types.ExtraSettings) (*nbpeer.Peer, bool, error) + PreparePeer(ctx context.Context, accountID string, peer *nbpeer.Peer, peersGroup []string, extraSettings *types.ExtraSettings) *nbpeer.Peer + IsNotValidPeer(ctx context.Context, accountID string, peer *nbpeer.Peer, peersGroup []string, extraSettings *types.ExtraSettings) (bool, bool, error) + GetValidatedPeers(accountID string, groups []*types.Group, peers []*nbpeer.Peer, extraSettings *types.ExtraSettings) (map[string]struct{}, error) PeerDeleted(ctx context.Context, accountID, peerID string) error SetPeerInvalidationListener(fn func(accountID string)) Stop(ctx context.Context) diff --git a/management/server/management_proto_test.go b/management/server/management_proto_test.go index 74ec20cf4..04fd88359 100644 --- a/management/server/management_proto_test.go +++ b/management/server/management_proto_test.go @@ -12,6 +12,7 @@ import ( "testing" "time" + "github.com/golang/mock/gomock" log "github.com/sirupsen/logrus" "github.com/stretchr/testify/require" "golang.zx2c4.com/wireguard/wgctrl/wgtypes" @@ -430,18 +431,27 @@ func startManagementForTest(t *testing.T, testFile string, config *Config) (*grp metrics, err := telemetry.NewDefaultAppMetrics(context.Background()) require.NoError(t, err) + ctrl := gomock.NewController(t) + t.Cleanup(ctrl.Finish) + settingsMockManager := settings.NewMockManager(ctrl) + settingsMockManager. + EXPECT(). + GetSettings(gomock.Any(), gomock.Any(), gomock.Any()). + AnyTimes(). + Return(&types.Settings{}, nil) + accountManager, err := BuildManager(ctx, store, peersUpdateManager, nil, "", "netbird.selfhosted", - eventStore, nil, false, MocIntegratedValidator{}, metrics, port_forwarding.NewControllerMock()) + eventStore, nil, false, MocIntegratedValidator{}, metrics, port_forwarding.NewControllerMock(), settingsMockManager) if err != nil { cleanup() return nil, nil, "", cleanup, err } - secretsManager := NewTimeBasedAuthSecretsManager(peersUpdateManager, config.TURNConfig, config.Relay) + secretsManager := NewTimeBasedAuthSecretsManager(peersUpdateManager, config.TURNConfig, config.Relay, settingsMockManager) ephemeralMgr := NewEphemeralManager(store, accountManager) - mgmtServer, err := NewServer(context.Background(), config, accountManager, settings.NewManager(store), peersUpdateManager, secretsManager, nil, ephemeralMgr, nil) + mgmtServer, err := NewServer(context.Background(), config, accountManager, settingsMockManager, peersUpdateManager, secretsManager, nil, ephemeralMgr, nil) if err != nil { return nil, nil, "", cleanup, err } @@ -740,7 +750,7 @@ func Test_LoginPerformance(t *testing.T) { NetbirdVersion: "", } - peerLogin := PeerLogin{ + peerLogin := types.PeerLogin{ WireGuardPubKey: key.String(), SSHKey: "random", Meta: extractPeerMeta(context.Background(), meta), @@ -765,7 +775,7 @@ func Test_LoginPerformance(t *testing.T) { messageCalls = append(messageCalls, login) mu.Unlock() - go func(peerLogin PeerLogin, counterStart *int32) { + go func(peerLogin types.PeerLogin, counterStart *int32) { defer wgPeer.Done() _, _, _, err = am.LoginPeer(context.Background(), peerLogin) if err != nil { diff --git a/management/server/management_test.go b/management/server/management_test.go index 838065e49..9cad3ab9d 100644 --- a/management/server/management_test.go +++ b/management/server/management_test.go @@ -10,6 +10,7 @@ import ( "testing" "time" + "github.com/golang/mock/gomock" pb "github.com/golang/protobuf/proto" //nolint log "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" @@ -26,6 +27,7 @@ import ( "github.com/netbirdio/netbird/management/server/settings" "github.com/netbirdio/netbird/management/server/store" "github.com/netbirdio/netbird/management/server/telemetry" + "github.com/netbirdio/netbird/management/server/types" "github.com/netbirdio/netbird/util" ) @@ -178,6 +180,20 @@ func startServer( t.Fatalf("failed creating metrics: %v", err) } + ctrl := gomock.NewController(t) + t.Cleanup(ctrl.Finish) + settingsMockManager := settings.NewMockManager(ctrl) + settingsMockManager. + EXPECT(). + GetExtraSettings(gomock.Any(), gomock.Any()). + Return(&types.ExtraSettings{}, nil). + AnyTimes() + settingsMockManager. + EXPECT(). + GetSettings(gomock.Any(), gomock.Any(), gomock.Any()). + Return(&types.Settings{}, nil). + AnyTimes() + accountManager, err := server.BuildManager( context.Background(), str, @@ -191,17 +207,18 @@ func startServer( server.MocIntegratedValidator{}, metrics, port_forwarding.NewControllerMock(), + settingsMockManager, ) if err != nil { t.Fatalf("failed creating an account manager: %v", err) } - secretsManager := server.NewTimeBasedAuthSecretsManager(peersUpdateManager, config.TURNConfig, config.Relay) + secretsManager := server.NewTimeBasedAuthSecretsManager(peersUpdateManager, config.TURNConfig, config.Relay, settingsMockManager) mgmtServer, err := server.NewServer( context.Background(), config, accountManager, - settings.NewManager(str), + settingsMockManager, peersUpdateManager, secretsManager, nil, diff --git a/management/server/mock_server/account_mock.go b/management/server/mock_server/account_mock.go index 5564aab01..cb8d598f8 100644 --- a/management/server/mock_server/account_mock.go +++ b/management/server/mock_server/account_mock.go @@ -11,17 +11,18 @@ import ( nbdns "github.com/netbirdio/netbird/dns" "github.com/netbirdio/netbird/management/domain" - "github.com/netbirdio/netbird/management/server" + "github.com/netbirdio/netbird/management/server/account" "github.com/netbirdio/netbird/management/server/activity" nbcontext "github.com/netbirdio/netbird/management/server/context" "github.com/netbirdio/netbird/management/server/idp" nbpeer "github.com/netbirdio/netbird/management/server/peer" "github.com/netbirdio/netbird/management/server/posture" + "github.com/netbirdio/netbird/management/server/store" "github.com/netbirdio/netbird/management/server/types" "github.com/netbirdio/netbird/route" ) -var _ server.AccountManager = (*MockAccountManager)(nil) +var _ account.Manager = (*MockAccountManager)(nil) type MockAccountManager struct { GetOrCreateAccountByUserFunc func(ctx context.Context, userId, domain string) (*types.Account, error) @@ -89,12 +90,12 @@ type MockAccountManager struct { SaveDNSSettingsFunc func(ctx context.Context, accountID, userID string, dnsSettingsToSave *types.DNSSettings) error GetPeerFunc func(ctx context.Context, accountID, peerID, userID string) (*nbpeer.Peer, error) UpdateAccountSettingsFunc func(ctx context.Context, accountID, userID string, newSettings *types.Settings) (*types.Account, error) - LoginPeerFunc func(ctx context.Context, login server.PeerLogin) (*nbpeer.Peer, *types.NetworkMap, []*posture.Checks, error) - SyncPeerFunc func(ctx context.Context, sync server.PeerSync, accountID string) (*nbpeer.Peer, *types.NetworkMap, []*posture.Checks, error) + LoginPeerFunc func(ctx context.Context, login types.PeerLogin) (*nbpeer.Peer, *types.NetworkMap, []*posture.Checks, error) + SyncPeerFunc func(ctx context.Context, sync types.PeerSync, accountID string) (*nbpeer.Peer, *types.NetworkMap, []*posture.Checks, error) InviteUserFunc func(ctx context.Context, accountID string, initiatorUserID string, targetUserEmail string) error GetAllConnectedPeersFunc func() (map[string]struct{}, error) HasConnectedChannelFunc func(peerID string) bool - GetExternalCacheManagerFunc func() server.ExternalCacheManager + GetExternalCacheManagerFunc func() account.ExternalCacheManager GetPostureChecksFunc func(ctx context.Context, accountID, postureChecksID, userID string) (*posture.Checks, error) SavePostureChecksFunc func(ctx context.Context, accountID, userID string, postureChecks *posture.Checks) (*posture.Checks, error) DeletePostureChecksFunc func(ctx context.Context, accountID, postureChecksID, userID string) error @@ -110,6 +111,7 @@ type MockAccountManager struct { GetAccountSettingsFunc func(ctx context.Context, accountID string, userID string) (*types.Settings, error) DeleteSetupKeyFunc func(ctx context.Context, accountID, userID, keyID string) error BuildUserInfosForAccountFunc func(ctx context.Context, accountID, initiatorUserID string, accountUsers []*types.User) (map[string]*types.UserInfo, error) + GetStoreFunc func() store.Store } func (am *MockAccountManager) UpdateAccountPeers(ctx context.Context, accountID string) { @@ -661,7 +663,7 @@ func (am *MockAccountManager) UpdateAccountSettings(ctx context.Context, account } // LoginPeer mocks LoginPeer of the AccountManager interface -func (am *MockAccountManager) LoginPeer(ctx context.Context, login server.PeerLogin) (*nbpeer.Peer, *types.NetworkMap, []*posture.Checks, error) { +func (am *MockAccountManager) LoginPeer(ctx context.Context, login types.PeerLogin) (*nbpeer.Peer, *types.NetworkMap, []*posture.Checks, error) { if am.LoginPeerFunc != nil { return am.LoginPeerFunc(ctx, login) } @@ -669,7 +671,7 @@ func (am *MockAccountManager) LoginPeer(ctx context.Context, login server.PeerLo } // SyncPeer mocks SyncPeer of the AccountManager interface -func (am *MockAccountManager) SyncPeer(ctx context.Context, sync server.PeerSync, accountID string) (*nbpeer.Peer, *types.NetworkMap, []*posture.Checks, error) { +func (am *MockAccountManager) SyncPeer(ctx context.Context, sync types.PeerSync, accountID string) (*nbpeer.Peer, *types.NetworkMap, []*posture.Checks, error) { if am.SyncPeerFunc != nil { return am.SyncPeerFunc(ctx, sync, accountID) } @@ -700,7 +702,7 @@ func (am *MockAccountManager) StoreEvent(ctx context.Context, initiatorID, targe } // GetExternalCacheManager mocks GetExternalCacheManager of the AccountManager interface -func (am *MockAccountManager) GetExternalCacheManager() server.ExternalCacheManager { +func (am *MockAccountManager) GetExternalCacheManager() account.ExternalCacheManager { if am.GetExternalCacheManagerFunc() != nil { return am.GetExternalCacheManagerFunc() } @@ -838,3 +840,10 @@ func (am *MockAccountManager) BuildUserInfosForAccount(ctx context.Context, acco func (am *MockAccountManager) SyncUserJWTGroups(ctx context.Context, userAuth nbcontext.UserAuth) error { return status.Errorf(codes.Unimplemented, "method SyncUserJWTGroups is not implemented") } + +func (am *MockAccountManager) GetStore() store.Store { + if am.GetStoreFunc != nil { + return am.GetStoreFunc() + } + return nil +} diff --git a/management/server/nameserver_test.go b/management/server/nameserver_test.go index 064a645d7..9b260d237 100644 --- a/management/server/nameserver_test.go +++ b/management/server/nameserver_test.go @@ -6,6 +6,7 @@ import ( "testing" "time" + "github.com/golang/mock/gomock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -13,6 +14,7 @@ import ( "github.com/netbirdio/netbird/management/server/activity" "github.com/netbirdio/netbird/management/server/integrations/port_forwarding" nbpeer "github.com/netbirdio/netbird/management/server/peer" + "github.com/netbirdio/netbird/management/server/settings" "github.com/netbirdio/netbird/management/server/store" "github.com/netbirdio/netbird/management/server/telemetry" "github.com/netbirdio/netbird/management/server/types" @@ -772,7 +774,11 @@ func createNSManager(t *testing.T) (*DefaultAccountManager, error) { metrics, err := telemetry.NewDefaultAppMetrics(context.Background()) require.NoError(t, err) - return BuildManager(context.Background(), store, NewPeersUpdateManager(nil), nil, "", "netbird.selfhosted", eventStore, nil, false, MocIntegratedValidator{}, metrics, port_forwarding.NewControllerMock()) + ctrl := gomock.NewController(t) + t.Cleanup(ctrl.Finish) + settingsMockManager := settings.NewMockManager(ctrl) + + return BuildManager(context.Background(), store, NewPeersUpdateManager(nil), nil, "", "netbird.selfhosted", eventStore, nil, false, MocIntegratedValidator{}, metrics, port_forwarding.NewControllerMock(), settingsMockManager) } func createNSStore(t *testing.T) (store.Store, error) { diff --git a/management/server/networks/manager.go b/management/server/networks/manager.go index 51205f1e9..609b68918 100644 --- a/management/server/networks/manager.go +++ b/management/server/networks/manager.go @@ -6,7 +6,7 @@ import ( "github.com/rs/xid" - s "github.com/netbirdio/netbird/management/server" + "github.com/netbirdio/netbird/management/server/account" "github.com/netbirdio/netbird/management/server/activity" "github.com/netbirdio/netbird/management/server/networks/resources" "github.com/netbirdio/netbird/management/server/networks/routers" @@ -26,7 +26,7 @@ type Manager interface { type managerImpl struct { store store.Store - accountManager s.AccountManager + accountManager account.Manager permissionsManager permissions.Manager resourcesManager resources.Manager routersManager routers.Manager @@ -35,7 +35,7 @@ type managerImpl struct { type mockManager struct { } -func NewManager(store store.Store, permissionsManager permissions.Manager, resourceManager resources.Manager, routersManager routers.Manager, accountManager s.AccountManager) Manager { +func NewManager(store store.Store, permissionsManager permissions.Manager, resourceManager resources.Manager, routersManager routers.Manager, accountManager account.Manager) Manager { return &managerImpl{ store: store, permissionsManager: permissionsManager, diff --git a/management/server/networks/resources/manager.go b/management/server/networks/resources/manager.go index 5b542d886..acaacbfb9 100644 --- a/management/server/networks/resources/manager.go +++ b/management/server/networks/resources/manager.go @@ -5,7 +5,7 @@ import ( "errors" "fmt" - s "github.com/netbirdio/netbird/management/server" + "github.com/netbirdio/netbird/management/server/account" "github.com/netbirdio/netbird/management/server/activity" "github.com/netbirdio/netbird/management/server/groups" "github.com/netbirdio/netbird/management/server/networks/resources/types" @@ -31,13 +31,13 @@ type managerImpl struct { store store.Store permissionsManager permissions.Manager groupsManager groups.Manager - accountManager s.AccountManager + accountManager account.Manager } type mockManager struct { } -func NewManager(store store.Store, permissionsManager permissions.Manager, groupsManager groups.Manager, accountManager s.AccountManager) Manager { +func NewManager(store store.Store, permissionsManager permissions.Manager, groupsManager groups.Manager, accountManager account.Manager) Manager { return &managerImpl{ store: store, permissionsManager: permissionsManager, diff --git a/management/server/networks/resources/types/resource.go b/management/server/networks/resources/types/resource.go index 0df6727c3..ecac0a724 100644 --- a/management/server/networks/resources/types/resource.go +++ b/management/server/networks/resources/types/resource.go @@ -20,9 +20,9 @@ import ( type NetworkResourceType string const ( - host NetworkResourceType = "host" - subnet NetworkResourceType = "subnet" - domain NetworkResourceType = "domain" + Host NetworkResourceType = "host" + Subnet NetworkResourceType = "subnet" + Domain NetworkResourceType = "domain" ) func (p NetworkResourceType) String() string { @@ -66,7 +66,7 @@ func NewNetworkResource(accountID, networkID, name, description, address string, func (n *NetworkResource) ToAPIResponse(groups []api.GroupMinimum) *api.NetworkResource { addr := n.Prefix.String() - if n.Type == domain { + if n.Type == Domain { addr = n.Domain } @@ -125,7 +125,7 @@ func (n *NetworkResource) ToRoute(peer *nbpeer.Peer, router *routerTypes.Network AccessControlGroups: nil, } - if n.Type == host || n.Type == subnet { + if n.Type == Host || n.Type == Subnet { r.Network = n.Prefix r.NetworkType = route.IPv4Network @@ -134,7 +134,7 @@ func (n *NetworkResource) ToRoute(peer *nbpeer.Peer, router *routerTypes.Network } } - if n.Type == domain { + if n.Type == Domain { domainList, err := nbDomain.FromStringList([]string{n.Domain}) if err != nil { return nil @@ -157,18 +157,18 @@ func (n *NetworkResource) EventMeta(network *networkTypes.Network) map[string]an func GetResourceType(address string) (NetworkResourceType, string, netip.Prefix, error) { if prefix, err := netip.ParsePrefix(address); err == nil { if prefix.Bits() == 32 || prefix.Bits() == 128 { - return host, "", prefix, nil + return Host, "", prefix, nil } - return subnet, "", prefix, nil + return Subnet, "", prefix, nil } if ip, err := netip.ParseAddr(address); err == nil { - return host, "", netip.PrefixFrom(ip, ip.BitLen()), nil + return Host, "", netip.PrefixFrom(ip, ip.BitLen()), nil } domainRegex := regexp.MustCompile(`^(\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$`) if domainRegex.MatchString(address) { - return domain, address, netip.Prefix{}, nil + return Domain, address, netip.Prefix{}, nil } return "", "", netip.Prefix{}, errors.New("not a valid host, subnet, or domain") diff --git a/management/server/networks/resources/types/resource_test.go b/management/server/networks/resources/types/resource_test.go index 6af384cce..02e802300 100644 --- a/management/server/networks/resources/types/resource_test.go +++ b/management/server/networks/resources/types/resource_test.go @@ -14,15 +14,15 @@ func TestGetResourceType(t *testing.T) { expectedPrefix netip.Prefix }{ // Valid host IPs - {"1.1.1.1", host, false, "", netip.MustParsePrefix("1.1.1.1/32")}, - {"1.1.1.1/32", host, false, "", netip.MustParsePrefix("1.1.1.1/32")}, + {"1.1.1.1", Host, false, "", netip.MustParsePrefix("1.1.1.1/32")}, + {"1.1.1.1/32", Host, false, "", netip.MustParsePrefix("1.1.1.1/32")}, // Valid subnets - {"192.168.1.0/24", subnet, false, "", netip.MustParsePrefix("192.168.1.0/24")}, - {"10.0.0.0/16", subnet, false, "", netip.MustParsePrefix("10.0.0.0/16")}, + {"192.168.1.0/24", Subnet, false, "", netip.MustParsePrefix("192.168.1.0/24")}, + {"10.0.0.0/16", Subnet, false, "", netip.MustParsePrefix("10.0.0.0/16")}, // Valid domains - {"example.com", domain, false, "example.com", netip.Prefix{}}, - {"*.example.com", domain, false, "*.example.com", netip.Prefix{}}, - {"sub.example.com", domain, false, "sub.example.com", netip.Prefix{}}, + {"example.com", Domain, false, "example.com", netip.Prefix{}}, + {"*.example.com", Domain, false, "*.example.com", netip.Prefix{}}, + {"sub.example.com", Domain, false, "sub.example.com", netip.Prefix{}}, // Invalid inputs {"invalid", "", true, "", netip.Prefix{}}, {"1.1.1.1/abc", "", true, "", netip.Prefix{}}, @@ -32,7 +32,7 @@ func TestGetResourceType(t *testing.T) { for _, tt := range tests { t.Run(tt.input, func(t *testing.T) { result, domain, prefix, err := GetResourceType(tt.input) - + if result != tt.expectedType { t.Errorf("Expected type %v, got %v", tt.expectedType, result) } diff --git a/management/server/networks/routers/manager.go b/management/server/networks/routers/manager.go index 3b32810a2..595fffd97 100644 --- a/management/server/networks/routers/manager.go +++ b/management/server/networks/routers/manager.go @@ -7,7 +7,7 @@ import ( "github.com/rs/xid" - s "github.com/netbirdio/netbird/management/server" + "github.com/netbirdio/netbird/management/server/account" "github.com/netbirdio/netbird/management/server/activity" "github.com/netbirdio/netbird/management/server/networks/routers/types" networkTypes "github.com/netbirdio/netbird/management/server/networks/types" @@ -29,13 +29,13 @@ type Manager interface { type managerImpl struct { store store.Store permissionsManager permissions.Manager - accountManager s.AccountManager + accountManager account.Manager } type mockManager struct { } -func NewManager(store store.Store, permissionsManager permissions.Manager, accountManager s.AccountManager) Manager { +func NewManager(store store.Store, permissionsManager permissions.Manager, accountManager account.Manager) Manager { return &managerImpl{ store: store, permissionsManager: permissionsManager, diff --git a/management/server/peer.go b/management/server/peer.go index 60d3fee38..d976ce68e 100644 --- a/management/server/peer.go +++ b/management/server/peer.go @@ -29,36 +29,6 @@ import ( "github.com/netbirdio/netbird/management/server/status" ) -// PeerSync used as a data object between the gRPC API and AccountManager on Sync request. -type PeerSync struct { - // WireGuardPubKey is a peers WireGuard public key - WireGuardPubKey string - // Meta is the system information passed by peer, must be always present - Meta nbpeer.PeerSystemMeta - // UpdateAccountPeers indicate updating account peers, - // which occurs when the peer's metadata is updated - UpdateAccountPeers bool -} - -// PeerLogin used as a data object between the gRPC API and AccountManager on Login request. -type PeerLogin struct { - // WireGuardPubKey is a peers WireGuard public key - WireGuardPubKey string - // SSHKey is a peer's ssh key. Can be empty (e.g., old version do not provide it, or this feature is disabled) - SSHKey string - // Meta is the system information passed by peer, must be always present. - Meta nbpeer.PeerSystemMeta - // UserID indicates that JWT was used to log in, and it was valid. Can be empty when SetupKey is used or auth is not required. - UserID string - // SetupKey references to a server.SetupKey to log in. Can be empty when UserID is used or auth is not required. - SetupKey string - // ConnectionIP is the real IP of the peer - ConnectionIP net.IP - - // ExtraDNSLabels is a list of extra DNS labels that the peer wants to use - ExtraDNSLabels []string -} - // GetPeers returns a list of peers under the given account filtering out peers that do not belong to a user if // the current user is not an admin. func (am *DefaultAccountManager) GetPeers(ctx context.Context, accountID, userID, nameFilter, ipFilter string) ([]*nbpeer.Peer, error) { @@ -709,7 +679,7 @@ func getFreeIP(ctx context.Context, transaction store.Store, accountID string) ( } // SyncPeer checks whether peer is eligible for receiving NetworkMap (authenticated) and returns its NetworkMap if eligible -func (am *DefaultAccountManager) SyncPeer(ctx context.Context, sync PeerSync, accountID string) (*nbpeer.Peer, *types.NetworkMap, []*posture.Checks, error) { +func (am *DefaultAccountManager) SyncPeer(ctx context.Context, sync types.PeerSync, accountID string) (*nbpeer.Peer, *types.NetworkMap, []*posture.Checks, error) { start := time.Now() defer func() { log.WithContext(ctx).Debugf("SyncPeer: took %v", time.Since(start)) @@ -784,7 +754,7 @@ func (am *DefaultAccountManager) SyncPeer(ctx context.Context, sync PeerSync, ac return am.getValidatedPeerWithMap(ctx, peerNotValid, accountID, peer) } -func (am *DefaultAccountManager) handlePeerLoginNotFound(ctx context.Context, login PeerLogin, err error) (*nbpeer.Peer, *types.NetworkMap, []*posture.Checks, error) { +func (am *DefaultAccountManager) handlePeerLoginNotFound(ctx context.Context, login types.PeerLogin, err error) (*nbpeer.Peer, *types.NetworkMap, []*posture.Checks, error) { if errStatus, ok := status.FromError(err); ok && errStatus.Type() == status.NotFound { // we couldn't find this peer by its public key which can mean that peer hasn't been registered yet. // Try registering it. @@ -804,7 +774,7 @@ func (am *DefaultAccountManager) handlePeerLoginNotFound(ctx context.Context, lo // LoginPeer logs in or registers a peer. // If peer doesn't exist the function checks whether a setup key or a user is present and registers a new peer if so. -func (am *DefaultAccountManager) LoginPeer(ctx context.Context, login PeerLogin) (*nbpeer.Peer, *types.NetworkMap, []*posture.Checks, error) { +func (am *DefaultAccountManager) LoginPeer(ctx context.Context, login types.PeerLogin) (*nbpeer.Peer, *types.NetworkMap, []*posture.Checks, error) { accountID, err := am.Store.GetAccountIDByPeerPubKey(ctx, login.WireGuardPubKey) if err != nil { return am.handlePeerLoginNotFound(ctx, login, err) @@ -994,7 +964,7 @@ func processPeerPostureChecks(ctx context.Context, transaction store.Store, poli // The NetBird client doesn't have a way to check if the peer needs login besides sending a login request // with no JWT token and usually no setup-key. As the client can send up to two login request to check if it is expired // and before starting the engine, we do the checks without an account lock to avoid piling up requests. -func (am *DefaultAccountManager) checkIFPeerNeedsLoginWithoutLock(ctx context.Context, accountID string, login PeerLogin) error { +func (am *DefaultAccountManager) checkIFPeerNeedsLoginWithoutLock(ctx context.Context, accountID string, login types.PeerLogin) error { peer, err := am.Store.GetPeerByPeerPubKey(ctx, store.LockingStrengthShare, login.WireGuardPubKey) if err != nil { return err @@ -1237,7 +1207,13 @@ func (am *DefaultAccountManager) UpdateAccountPeers(ctx context.Context, account remotePeerNetworkMap.Merge(proxyNetworkMap) } - update := toSyncResponse(ctx, nil, p, nil, nil, remotePeerNetworkMap, am.GetDNSDomain(), postureChecks, dnsCache, account.Settings.RoutingPeerDNSResolutionEnabled) + extraSetting, err := am.settingsManager.GetExtraSettings(ctx, accountID) + if err != nil { + log.WithContext(ctx).Errorf("failed to get flow enabled status: %v", err) + return + } + + update := toSyncResponse(ctx, &Config{}, p, nil, nil, remotePeerNetworkMap, am.GetDNSDomain(), postureChecks, dnsCache, account.Settings.RoutingPeerDNSResolutionEnabled, extraSetting) am.peersUpdateManager.SendUpdate(ctx, p.ID, &UpdateMessage{Update: update, NetworkMap: remotePeerNetworkMap}) }(peer) } @@ -1300,7 +1276,13 @@ func (am *DefaultAccountManager) UpdateAccountPeer(ctx context.Context, accountI remotePeerNetworkMap.Merge(proxyNetworkMap) } - update := toSyncResponse(ctx, nil, peer, nil, nil, remotePeerNetworkMap, am.GetDNSDomain(), postureChecks, dnsCache, account.Settings.RoutingPeerDNSResolutionEnabled) + extraSettings, err := am.settingsManager.GetExtraSettings(ctx, peer.AccountID) + if err != nil { + log.WithContext(ctx).Errorf("failed to get extra settings: %v", err) + return + } + + update := toSyncResponse(ctx, &Config{}, peer, nil, nil, remotePeerNetworkMap, am.GetDNSDomain(), postureChecks, dnsCache, account.Settings.RoutingPeerDNSResolutionEnabled, extraSettings) am.peersUpdateManager.SendUpdate(ctx, peer.ID, &UpdateMessage{Update: update, NetworkMap: remotePeerNetworkMap}) } diff --git a/management/server/peer_test.go b/management/server/peer_test.go index 843910597..64bf5a73b 100644 --- a/management/server/peer_test.go +++ b/management/server/peer_test.go @@ -13,13 +13,15 @@ import ( "testing" "time" - nbAccount "github.com/netbirdio/netbird/management/server/account" + "github.com/golang/mock/gomock" "github.com/rs/xid" log "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.zx2c4.com/wireguard/wgctrl/wgtypes" + "github.com/netbirdio/netbird/management/server/settings" + "github.com/netbirdio/netbird/management/server/integrations/port_forwarding" "github.com/netbirdio/netbird/management/server/util" @@ -1107,7 +1109,7 @@ func TestToSyncResponse(t *testing.T) { } dnsCache := &DNSConfigCache{} - response := toSyncResponse(context.Background(), config, peer, turnRelayToken, turnRelayToken, networkMap, dnsName, checks, dnsCache, true) + response := toSyncResponse(context.Background(), config, peer, turnRelayToken, turnRelayToken, networkMap, dnsName, checks, dnsCache, true, nil) assert.NotNil(t, response) // assert peer config @@ -1211,7 +1213,11 @@ func Test_RegisterPeerByUser(t *testing.T) { metrics, err := telemetry.NewDefaultAppMetrics(context.Background()) assert.NoError(t, err) - am, err := BuildManager(context.Background(), s, NewPeersUpdateManager(nil), nil, "", "netbird.cloud", eventStore, nil, false, MocIntegratedValidator{}, metrics, port_forwarding.NewControllerMock()) + ctrl := gomock.NewController(t) + t.Cleanup(ctrl.Finish) + settingsMockManager := settings.NewMockManager(ctrl) + + am, err := BuildManager(context.Background(), s, NewPeersUpdateManager(nil), nil, "", "netbird.cloud", eventStore, nil, false, MocIntegratedValidator{}, metrics, port_forwarding.NewControllerMock(), settingsMockManager) assert.NoError(t, err) existingAccountID := "bf1c8084-ba50-4ce7-9439-34653001fc3b" @@ -1275,7 +1281,11 @@ func Test_RegisterPeerBySetupKey(t *testing.T) { metrics, err := telemetry.NewDefaultAppMetrics(context.Background()) assert.NoError(t, err) - am, err := BuildManager(context.Background(), s, NewPeersUpdateManager(nil), nil, "", "netbird.cloud", eventStore, nil, false, MocIntegratedValidator{}, metrics, port_forwarding.NewControllerMock()) + ctrl := gomock.NewController(t) + t.Cleanup(ctrl.Finish) + settingsMockManager := settings.NewMockManager(ctrl) + + am, err := BuildManager(context.Background(), s, NewPeersUpdateManager(nil), nil, "", "netbird.cloud", eventStore, nil, false, MocIntegratedValidator{}, metrics, port_forwarding.NewControllerMock(), settingsMockManager) assert.NoError(t, err) existingAccountID := "bf1c8084-ba50-4ce7-9439-34653001fc3b" @@ -1342,7 +1352,11 @@ func Test_RegisterPeerRollbackOnFailure(t *testing.T) { metrics, err := telemetry.NewDefaultAppMetrics(context.Background()) assert.NoError(t, err) - am, err := BuildManager(context.Background(), s, NewPeersUpdateManager(nil), nil, "", "netbird.cloud", eventStore, nil, false, MocIntegratedValidator{}, metrics, port_forwarding.NewControllerMock()) + ctrl := gomock.NewController(t) + t.Cleanup(ctrl.Finish) + settingsMockManager := settings.NewMockManager(ctrl) + + am, err := BuildManager(context.Background(), s, NewPeersUpdateManager(nil), nil, "", "netbird.cloud", eventStore, nil, false, MocIntegratedValidator{}, metrics, port_forwarding.NewControllerMock(), settingsMockManager) assert.NoError(t, err) existingAccountID := "bf1c8084-ba50-4ce7-9439-34653001fc3b" @@ -1531,7 +1545,7 @@ func TestPeerAccountPeersUpdate(t *testing.T) { }) t.Run("validator requires update", func(t *testing.T) { - requireUpdateFunc := func(_ context.Context, update *nbpeer.Peer, peer *nbpeer.Peer, userID string, accountID string, dnsDomain string, peersGroup []string, extraSettings *nbAccount.ExtraSettings) (*nbpeer.Peer, bool, error) { + requireUpdateFunc := func(_ context.Context, update *nbpeer.Peer, peer *nbpeer.Peer, userID string, accountID string, dnsDomain string, peersGroup []string, extraSettings *types.ExtraSettings) (*nbpeer.Peer, bool, error) { return update, true, nil } @@ -1553,7 +1567,7 @@ func TestPeerAccountPeersUpdate(t *testing.T) { }) t.Run("validator requires no update", func(t *testing.T) { - requireNoUpdateFunc := func(_ context.Context, update *nbpeer.Peer, peer *nbpeer.Peer, userID string, accountID string, dnsDomain string, peersGroup []string, extraSettings *nbAccount.ExtraSettings) (*nbpeer.Peer, bool, error) { + requireNoUpdateFunc := func(_ context.Context, update *nbpeer.Peer, peer *nbpeer.Peer, userID string, accountID string, dnsDomain string, peersGroup []string, extraSettings *types.ExtraSettings) (*nbpeer.Peer, bool, error) { return update, false, nil } diff --git a/management/server/peers/manager.go b/management/server/peers/manager.go index 1e067cef5..b00c1761b 100644 --- a/management/server/peers/manager.go +++ b/management/server/peers/manager.go @@ -1,5 +1,7 @@ package peers +//go:generate go run github.com/golang/mock/mockgen -package peers -destination=manager_mock.go -source=./manager.go -build_flags=-mod=mod + import ( "context" "fmt" @@ -12,6 +14,7 @@ import ( type Manager interface { GetPeer(ctx context.Context, accountID, userID, peerID string) (*peer.Peer, error) + GetPeerAccountID(ctx context.Context, peerID string) (string, error) GetAllPeers(ctx context.Context, accountID, userID string) ([]*peer.Peer, error) } @@ -52,3 +55,7 @@ func (m *managerImpl) GetAllPeers(ctx context.Context, accountID, userID string) return m.store.GetAccountPeers(ctx, store.LockingStrengthShare, accountID, "", "") } + +func (m *managerImpl) GetPeerAccountID(ctx context.Context, peerID string) (string, error) { + return m.store.GetAccountIDByPeerID(ctx, store.LockingStrengthShare, peerID) +} diff --git a/management/server/peers/manager_mock.go b/management/server/peers/manager_mock.go new file mode 100644 index 000000000..b247a1752 --- /dev/null +++ b/management/server/peers/manager_mock.go @@ -0,0 +1,81 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: ./manager.go + +// Package peers is a generated GoMock package. +package peers + +import ( + context "context" + reflect "reflect" + + gomock "github.com/golang/mock/gomock" + peer "github.com/netbirdio/netbird/management/server/peer" +) + +// MockManager is a mock of Manager interface. +type MockManager struct { + ctrl *gomock.Controller + recorder *MockManagerMockRecorder +} + +// MockManagerMockRecorder is the mock recorder for MockManager. +type MockManagerMockRecorder struct { + mock *MockManager +} + +// NewMockManager creates a new mock instance. +func NewMockManager(ctrl *gomock.Controller) *MockManager { + mock := &MockManager{ctrl: ctrl} + mock.recorder = &MockManagerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockManager) EXPECT() *MockManagerMockRecorder { + return m.recorder +} + +// GetAllPeers mocks base method. +func (m *MockManager) GetAllPeers(ctx context.Context, accountID, userID string) ([]*peer.Peer, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetAllPeers", ctx, accountID, userID) + ret0, _ := ret[0].([]*peer.Peer) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetAllPeers indicates an expected call of GetAllPeers. +func (mr *MockManagerMockRecorder) GetAllPeers(ctx, accountID, userID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAllPeers", reflect.TypeOf((*MockManager)(nil).GetAllPeers), ctx, accountID, userID) +} + +// GetPeer mocks base method. +func (m *MockManager) GetPeer(ctx context.Context, accountID, userID, peerID string) (*peer.Peer, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetPeer", ctx, accountID, userID, peerID) + ret0, _ := ret[0].(*peer.Peer) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetPeer indicates an expected call of GetPeer. +func (mr *MockManagerMockRecorder) GetPeer(ctx, accountID, userID, peerID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPeer", reflect.TypeOf((*MockManager)(nil).GetPeer), ctx, accountID, userID, peerID) +} + +// GetPeerAccountID mocks base method. +func (m *MockManager) GetPeerAccountID(ctx context.Context, peerID string) (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetPeerAccountID", ctx, peerID) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetPeerAccountID indicates an expected call of GetPeerAccountID. +func (mr *MockManagerMockRecorder) GetPeerAccountID(ctx, peerID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPeerAccountID", reflect.TypeOf((*MockManager)(nil).GetPeerAccountID), ctx, peerID) +} diff --git a/management/server/permissions/manager.go b/management/server/permissions/manager.go index 320aad027..0345405fe 100644 --- a/management/server/permissions/manager.go +++ b/management/server/permissions/manager.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" + "github.com/netbirdio/netbird/management/server/activity" "github.com/netbirdio/netbird/management/server/settings" "github.com/netbirdio/netbird/management/server/types" "github.com/netbirdio/netbird/management/server/users" @@ -71,7 +72,7 @@ func (m *managerImpl) ValidateUserPermissions(ctx context.Context, accountID, us } func (m *managerImpl) validateRegularUserPermissions(ctx context.Context, accountID, userID string, module Module, operation Operation) (bool, error) { - settings, err := m.settingsManager.GetSettings(ctx, accountID, userID) + settings, err := m.settingsManager.GetSettings(ctx, accountID, activity.SystemInitiator) if err != nil { return false, fmt.Errorf("failed to get settings: %w", err) } diff --git a/management/server/policy.go b/management/server/policy.go index d9f04f3a9..bbc85f6ae 100644 --- a/management/server/policy.go +++ b/management/server/policy.go @@ -256,6 +256,7 @@ func toProtocolFirewallRules(rules []*types.FirewallRule) []*proto.FirewallRule rule := rules[i] result[i] = &proto.FirewallRule{ + PolicyID: []byte(rule.PolicyID), PeerIP: rule.PeerIP, Direction: getProtoDirection(rule.Direction), Action: getProtoAction(rule.Action), diff --git a/management/server/policy_test.go b/management/server/policy_test.go index 90f9670d1..10b7fc2d1 100644 --- a/management/server/policy_test.go +++ b/management/server/policy_test.go @@ -182,6 +182,7 @@ func TestAccount_getPeersByPolicy(t *testing.T) { Action: "accept", Protocol: "all", Port: "", + PolicyID: "RuleDefault", }, { PeerIP: "0.0.0.0", @@ -189,6 +190,7 @@ func TestAccount_getPeersByPolicy(t *testing.T) { Action: "accept", Protocol: "all", Port: "", + PolicyID: "RuleDefault", }, { PeerIP: "100.65.14.88", @@ -196,6 +198,7 @@ func TestAccount_getPeersByPolicy(t *testing.T) { Action: "accept", Protocol: "all", Port: "", + PolicyID: "RuleSwarm", }, { PeerIP: "100.65.14.88", @@ -203,6 +206,7 @@ func TestAccount_getPeersByPolicy(t *testing.T) { Action: "accept", Protocol: "all", Port: "", + PolicyID: "RuleSwarm", }, { PeerIP: "100.65.62.5", @@ -210,6 +214,7 @@ func TestAccount_getPeersByPolicy(t *testing.T) { Action: "accept", Protocol: "all", Port: "", + PolicyID: "RuleSwarm", }, { PeerIP: "100.65.62.5", @@ -217,6 +222,7 @@ func TestAccount_getPeersByPolicy(t *testing.T) { Action: "accept", Protocol: "all", Port: "", + PolicyID: "RuleSwarm", }, { @@ -225,6 +231,7 @@ func TestAccount_getPeersByPolicy(t *testing.T) { Action: "accept", Protocol: "all", Port: "", + PolicyID: "RuleSwarm", }, { PeerIP: "100.65.32.206", @@ -232,6 +239,7 @@ func TestAccount_getPeersByPolicy(t *testing.T) { Action: "accept", Protocol: "all", Port: "", + PolicyID: "RuleSwarm", }, { @@ -240,6 +248,7 @@ func TestAccount_getPeersByPolicy(t *testing.T) { Action: "accept", Protocol: "all", Port: "", + PolicyID: "RuleSwarm", }, { PeerIP: "100.65.250.202", @@ -247,6 +256,7 @@ func TestAccount_getPeersByPolicy(t *testing.T) { Action: "accept", Protocol: "all", Port: "", + PolicyID: "RuleSwarm", }, { @@ -255,6 +265,7 @@ func TestAccount_getPeersByPolicy(t *testing.T) { Action: "accept", Protocol: "all", Port: "", + PolicyID: "RuleSwarm", }, { PeerIP: "100.65.13.186", @@ -262,6 +273,7 @@ func TestAccount_getPeersByPolicy(t *testing.T) { Action: "accept", Protocol: "all", Port: "", + PolicyID: "RuleSwarm", }, { @@ -270,6 +282,7 @@ func TestAccount_getPeersByPolicy(t *testing.T) { Action: "accept", Protocol: "all", Port: "", + PolicyID: "RuleSwarm", }, { PeerIP: "100.65.29.55", @@ -277,6 +290,7 @@ func TestAccount_getPeersByPolicy(t *testing.T) { Action: "accept", Protocol: "all", Port: "", + PolicyID: "RuleSwarm", }, } assert.Len(t, firewallRules, len(epectedFirewallRules)) @@ -404,6 +418,7 @@ func TestAccount_getPeersByPolicyDirect(t *testing.T) { Action: "accept", Protocol: "all", Port: "", + PolicyID: "RuleSwarm", }, { PeerIP: "100.65.254.139", @@ -411,6 +426,7 @@ func TestAccount_getPeersByPolicyDirect(t *testing.T) { Action: "accept", Protocol: "all", Port: "", + PolicyID: "RuleSwarm", }, } assert.Len(t, firewallRules, len(epectedFirewallRules)) @@ -432,6 +448,7 @@ func TestAccount_getPeersByPolicyDirect(t *testing.T) { Action: "accept", Protocol: "all", Port: "", + PolicyID: "RuleSwarm", }, { PeerIP: "100.65.80.39", @@ -439,6 +456,7 @@ func TestAccount_getPeersByPolicyDirect(t *testing.T) { Action: "accept", Protocol: "all", Port: "", + PolicyID: "RuleSwarm", }, } assert.Len(t, firewallRules, len(epectedFirewallRules)) @@ -462,6 +480,7 @@ func TestAccount_getPeersByPolicyDirect(t *testing.T) { Action: "accept", Protocol: "all", Port: "", + PolicyID: "RuleSwarm", }, } assert.Len(t, firewallRules, len(epectedFirewallRules)) @@ -483,6 +502,7 @@ func TestAccount_getPeersByPolicyDirect(t *testing.T) { Action: "accept", Protocol: "all", Port: "", + PolicyID: "RuleSwarm", }, } assert.Len(t, firewallRules, len(epectedFirewallRules)) @@ -690,6 +710,7 @@ func TestAccount_getPeersByPolicyPostureChecks(t *testing.T) { Action: "accept", Protocol: "tcp", Port: "80", + PolicyID: "RuleSwarm", }, } assert.ElementsMatch(t, firewallRules, expectedFirewallRules) @@ -773,6 +794,7 @@ func TestAccount_getPeersByPolicyPostureChecks(t *testing.T) { Action: "accept", Protocol: "tcp", Port: "80", + PolicyID: "RuleSwarm", }, { PeerIP: "100.65.32.206", @@ -780,6 +802,7 @@ func TestAccount_getPeersByPolicyPostureChecks(t *testing.T) { Action: "accept", Protocol: "tcp", Port: "80", + PolicyID: "RuleSwarm", }, { PeerIP: "100.65.13.186", @@ -787,6 +810,7 @@ func TestAccount_getPeersByPolicyPostureChecks(t *testing.T) { Action: "accept", Protocol: "tcp", Port: "80", + PolicyID: "RuleSwarm", }, { PeerIP: "100.65.29.55", @@ -794,6 +818,7 @@ func TestAccount_getPeersByPolicyPostureChecks(t *testing.T) { Action: "accept", Protocol: "tcp", Port: "80", + PolicyID: "RuleSwarm", }, { PeerIP: "100.65.254.139", @@ -801,6 +826,7 @@ func TestAccount_getPeersByPolicyPostureChecks(t *testing.T) { Action: "accept", Protocol: "tcp", Port: "80", + PolicyID: "RuleSwarm", }, { PeerIP: "100.65.62.5", @@ -808,6 +834,7 @@ func TestAccount_getPeersByPolicyPostureChecks(t *testing.T) { Action: "accept", Protocol: "tcp", Port: "80", + PolicyID: "RuleSwarm", }, } assert.Len(t, firewallRules, len(expectedFirewallRules)) diff --git a/management/server/route.go b/management/server/route.go index b6b44fbbd..94663dc80 100644 --- a/management/server/route.go +++ b/management/server/route.go @@ -388,6 +388,7 @@ func toProtocolRoutesFirewallRules(rules []*types.RouteFirewallRule) []*proto.Ro Protocol: getProtoProtocol(rule.Protocol), PortInfo: getProtoPortInfo(rule), IsDynamic: rule.IsDynamic, + PolicyID: []byte(rule.PolicyID), } } diff --git a/management/server/route_test.go b/management/server/route_test.go index c5a5f2040..473fbd862 100644 --- a/management/server/route_test.go +++ b/management/server/route_test.go @@ -9,6 +9,7 @@ import ( "testing" "time" + "github.com/golang/mock/gomock" "github.com/rs/xid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -20,6 +21,7 @@ import ( routerTypes "github.com/netbirdio/netbird/management/server/networks/routers/types" networkTypes "github.com/netbirdio/netbird/management/server/networks/types" nbpeer "github.com/netbirdio/netbird/management/server/peer" + "github.com/netbirdio/netbird/management/server/settings" "github.com/netbirdio/netbird/management/server/store" "github.com/netbirdio/netbird/management/server/telemetry" "github.com/netbirdio/netbird/management/server/types" @@ -1257,7 +1259,29 @@ func createRouterManager(t *testing.T) (*DefaultAccountManager, error) { metrics, err := telemetry.NewDefaultAppMetrics(context.Background()) require.NoError(t, err) - return BuildManager(context.Background(), store, NewPeersUpdateManager(nil), nil, "", "netbird.selfhosted", eventStore, nil, false, MocIntegratedValidator{}, metrics, port_forwarding.NewControllerMock()) + ctrl := gomock.NewController(t) + t.Cleanup(ctrl.Finish) + + settingsMockManager := settings.NewMockManager(ctrl) + settingsMockManager. + EXPECT(). + GetSettings( + gomock.Any(), + gomock.Any(), + gomock.Any(), + ). + Return(nil, nil). + AnyTimes() + settingsMockManager. + EXPECT(). + GetExtraSettings( + gomock.Any(), + gomock.Any(), + ). + AnyTimes(). + Return(&types.ExtraSettings{}, nil) + + return BuildManager(context.Background(), store, NewPeersUpdateManager(nil), nil, "", "netbird.selfhosted", eventStore, nil, false, MocIntegratedValidator{}, metrics, port_forwarding.NewControllerMock(), settingsMockManager) } func createRouterStore(t *testing.T) (store.Store, error) { diff --git a/management/server/settings/manager.go b/management/server/settings/manager.go index 37bc9f549..28a984875 100644 --- a/management/server/settings/manager.go +++ b/management/server/settings/manager.go @@ -1,37 +1,98 @@ package settings +//go:generate go run github.com/golang/mock/mockgen -package settings -destination=manager_mock.go -source=./manager.go -build_flags=-mod=mod + import ( "context" + "fmt" + "github.com/netbirdio/netbird/management/server/activity" + "github.com/netbirdio/netbird/management/server/integrations/extra_settings" + "github.com/netbirdio/netbird/management/server/status" "github.com/netbirdio/netbird/management/server/store" "github.com/netbirdio/netbird/management/server/types" + "github.com/netbirdio/netbird/management/server/users" ) type Manager interface { + GetExtraSettingsManager() extra_settings.Manager GetSettings(ctx context.Context, accountID string, userID string) (*types.Settings, error) + GetExtraSettings(ctx context.Context, accountID string) (*types.ExtraSettings, error) + UpdateExtraSettings(ctx context.Context, accountID, userID string, extraSettings *types.ExtraSettings) (bool, error) } type managerImpl struct { - store store.Store + store store.Store + extraSettingsManager extra_settings.Manager + userManager users.Manager } -type managerMock struct { -} - -func NewManager(store store.Store) Manager { +func NewManager(store store.Store, userManager users.Manager, extraSettingsManager extra_settings.Manager) Manager { return &managerImpl{ - store: store, + store: store, + extraSettingsManager: extraSettingsManager, + userManager: userManager, } } -func (m *managerImpl) GetSettings(ctx context.Context, accountID string, userID string) (*types.Settings, error) { - return m.store.GetAccountSettings(ctx, store.LockingStrengthShare, accountID) +func (m *managerImpl) GetExtraSettingsManager() extra_settings.Manager { + return m.extraSettingsManager } -func NewManagerMock() Manager { - return &managerMock{} +func (m *managerImpl) GetSettings(ctx context.Context, accountID, userID string) (*types.Settings, error) { + if userID != activity.SystemInitiator { + user, err := m.userManager.GetUser(ctx, userID) + if err != nil { + return nil, fmt.Errorf("get user: %w", err) + } + + if user.AccountID != accountID || (!user.HasAdminPower() && !user.IsServiceUser) { + return nil, status.Errorf(status.PermissionDenied, "the user has no permission to access account data") + } + } + + extraSettings, err := m.extraSettingsManager.GetExtraSettings(ctx, accountID) + if err != nil { + return nil, fmt.Errorf("get extra settings: %w", err) + } + + settings, err := m.store.GetAccountSettings(ctx, store.LockingStrengthShare, accountID) + if err != nil { + return nil, fmt.Errorf("get account settings: %w", err) + } + + // Once we migrate the peer approval to settings manager this merging is obsolete + if settings.Extra != nil { + settings.Extra.FlowEnabled = extraSettings.FlowEnabled + settings.Extra.FlowPacketCounterEnabled = extraSettings.FlowPacketCounterEnabled + settings.Extra.FlowENCollectionEnabled = extraSettings.FlowENCollectionEnabled + settings.Extra.FlowDnsCollectionEnabled = extraSettings.FlowDnsCollectionEnabled + } + + return settings, nil } -func (m *managerMock) GetSettings(ctx context.Context, accountID string, userID string) (*types.Settings, error) { - return &types.Settings{}, nil +func (m *managerImpl) GetExtraSettings(ctx context.Context, accountID string) (*types.ExtraSettings, error) { + extraSettings, err := m.extraSettingsManager.GetExtraSettings(ctx, accountID) + if err != nil { + return nil, fmt.Errorf("get extra settings: %w", err) + } + + settings, err := m.store.GetAccountSettings(ctx, store.LockingStrengthShare, accountID) + if err != nil { + return nil, fmt.Errorf("get account settings: %w", err) + } + + // Once we migrate the peer approval to settings manager this merging is obsolete + if settings.Extra == nil { + settings.Extra = &types.ExtraSettings{} + } + + settings.Extra.FlowEnabled = extraSettings.FlowEnabled + + return settings.Extra, nil +} + +func (m *managerImpl) UpdateExtraSettings(ctx context.Context, accountID, userID string, extraSettings *types.ExtraSettings) (bool, error) { + return m.extraSettingsManager.UpdateExtraSettings(ctx, accountID, userID, extraSettings) } diff --git a/management/server/settings/manager_mock.go b/management/server/settings/manager_mock.go new file mode 100644 index 000000000..dc2f2ebfe --- /dev/null +++ b/management/server/settings/manager_mock.go @@ -0,0 +1,96 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: ./manager.go + +// Package settings is a generated GoMock package. +package settings + +import ( + context "context" + reflect "reflect" + + gomock "github.com/golang/mock/gomock" + extra_settings "github.com/netbirdio/netbird/management/server/integrations/extra_settings" + types "github.com/netbirdio/netbird/management/server/types" +) + +// MockManager is a mock of Manager interface. +type MockManager struct { + ctrl *gomock.Controller + recorder *MockManagerMockRecorder +} + +// MockManagerMockRecorder is the mock recorder for MockManager. +type MockManagerMockRecorder struct { + mock *MockManager +} + +// NewMockManager creates a new mock instance. +func NewMockManager(ctrl *gomock.Controller) *MockManager { + mock := &MockManager{ctrl: ctrl} + mock.recorder = &MockManagerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockManager) EXPECT() *MockManagerMockRecorder { + return m.recorder +} + +// GetExtraSettings mocks base method. +func (m *MockManager) GetExtraSettings(ctx context.Context, accountID string) (*types.ExtraSettings, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetExtraSettings", ctx, accountID) + ret0, _ := ret[0].(*types.ExtraSettings) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetExtraSettings indicates an expected call of GetExtraSettings. +func (mr *MockManagerMockRecorder) GetExtraSettings(ctx, accountID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetExtraSettings", reflect.TypeOf((*MockManager)(nil).GetExtraSettings), ctx, accountID) +} + +// GetExtraSettingsManager mocks base method. +func (m *MockManager) GetExtraSettingsManager() extra_settings.Manager { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetExtraSettingsManager") + ret0, _ := ret[0].(extra_settings.Manager) + return ret0 +} + +// GetExtraSettingsManager indicates an expected call of GetExtraSettingsManager. +func (mr *MockManagerMockRecorder) GetExtraSettingsManager() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetExtraSettingsManager", reflect.TypeOf((*MockManager)(nil).GetExtraSettingsManager)) +} + +// GetSettings mocks base method. +func (m *MockManager) GetSettings(ctx context.Context, accountID, userID string) (*types.Settings, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetSettings", ctx, accountID, userID) + ret0, _ := ret[0].(*types.Settings) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetSettings indicates an expected call of GetSettings. +func (mr *MockManagerMockRecorder) GetSettings(ctx, accountID, userID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSettings", reflect.TypeOf((*MockManager)(nil).GetSettings), ctx, accountID, userID) +} + +// UpdateExtraSettings mocks base method. +func (m *MockManager) UpdateExtraSettings(ctx context.Context, accountID, userID string, extraSettings *types.ExtraSettings) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateExtraSettings", ctx, accountID, userID, extraSettings) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// UpdateExtraSettings indicates an expected call of UpdateExtraSettings. +func (mr *MockManagerMockRecorder) UpdateExtraSettings(ctx, accountID, userID, extraSettings interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateExtraSettings", reflect.TypeOf((*MockManager)(nil).UpdateExtraSettings), ctx, accountID, userID, extraSettings) +} diff --git a/management/server/status/error.go b/management/server/status/error.go index 96b103183..adf7e060c 100644 --- a/management/server/status/error.go +++ b/management/server/status/error.go @@ -40,6 +40,8 @@ const ( // Type is a type of the Error type Type int32 +var ErrExtraSettingsNotFound = fmt.Errorf("extra settings not found") + // Error is an internal error type Error struct { ErrorType Type @@ -206,3 +208,7 @@ func NewOwnerDeletePermissionError() error { func NewPATNotFoundError(patID string) error { return Errorf(NotFound, "PAT: %s not found", patID) } + +func NewExtraSettingsNotFoundError() error { + return ErrExtraSettingsNotFound +} diff --git a/management/server/store/sql_store.go b/management/server/store/sql_store.go index 1dae3999b..cf6665665 100644 --- a/management/server/store/sql_store.go +++ b/management/server/store/sql_store.go @@ -26,7 +26,6 @@ import ( "github.com/netbirdio/netbird/management/server/util" nbdns "github.com/netbirdio/netbird/dns" - "github.com/netbirdio/netbird/management/server/account" resourceTypes "github.com/netbirdio/netbird/management/server/networks/resources/types" routerTypes "github.com/netbirdio/netbird/management/server/networks/routers/types" networkTypes "github.com/netbirdio/netbird/management/server/networks/types" @@ -95,7 +94,7 @@ func NewSqlStore(ctx context.Context, db *gorm.DB, storeEngine Engine, metrics t err = db.AutoMigrate( &types.SetupKey{}, &nbpeer.Peer{}, &types.User{}, &types.PersonalAccessToken{}, &types.Group{}, &types.Account{}, &types.Policy{}, &types.PolicyRule{}, &route.Route{}, &nbdns.NameServerGroup{}, - &installation{}, &account.ExtraSettings{}, &posture.Checks{}, &nbpeer.NetworkAddress{}, + &installation{}, &types.ExtraSettings{}, &posture.Checks{}, &nbpeer.NetworkAddress{}, &networkTypes.Network{}, &routerTypes.NetworkRouter{}, &resourceTypes.NetworkResource{}, ) if err != nil { @@ -1317,7 +1316,6 @@ func (s *SqlStore) GetPeerByID(ctx context.Context, lockStrength LockingStrength if errors.Is(result.Error, gorm.ErrRecordNotFound) { return nil, status.NewPeerNotFoundError(peerID) } - log.WithContext(ctx).Errorf("failed to get peer from store: %s", result.Error) return nil, status.Errorf(status.Internal, "failed to get peer from store") } @@ -2178,3 +2176,17 @@ func (s *SqlStore) DeletePAT(ctx context.Context, lockStrength LockingStrength, return nil } + +func (s *SqlStore) GetPeerByIP(ctx context.Context, lockStrength LockingStrength, accountID string, ip net.IP) (*nbpeer.Peer, error) { + jsonValue := fmt.Sprintf(`"%s"`, ip.String()) + + var peer nbpeer.Peer + result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}). + First(&peer, "account_id = ? AND ip = ?", accountID, jsonValue) + if result.Error != nil { + // no logging here + return nil, status.Errorf(status.Internal, "failed to get peer from store") + } + + return &peer, nil +} diff --git a/management/server/store/store.go b/management/server/store/store.go index d84d699bb..9ff0c5636 100644 --- a/management/server/store/store.go +++ b/management/server/store/store.go @@ -23,10 +23,9 @@ import ( "gorm.io/gorm" "github.com/netbirdio/netbird/dns" + "github.com/netbirdio/netbird/management/server/telemetry" "github.com/netbirdio/netbird/management/server/testutil" "github.com/netbirdio/netbird/management/server/types" - - "github.com/netbirdio/netbird/management/server/telemetry" "github.com/netbirdio/netbird/util" "github.com/netbirdio/netbird/management/server/migration" @@ -185,6 +184,7 @@ type Store interface { GetNetworkResourceByName(ctx context.Context, lockStrength LockingStrength, accountID, resourceName string) (*resourceTypes.NetworkResource, error) SaveNetworkResource(ctx context.Context, lockStrength LockingStrength, resource *resourceTypes.NetworkResource) error DeleteNetworkResource(ctx context.Context, lockStrength LockingStrength, accountID, resourceID string) error + GetPeerByIP(ctx context.Context, lockStrength LockingStrength, accountID string, ip net.IP) (*nbpeer.Peer, error) } type Engine string @@ -342,7 +342,7 @@ func NewTestStoreFromSQL(ctx context.Context, filename string, dataDir string) ( } if filename != "" { - err = loadSQL(db, filename) + err = LoadSQL(db, filename) if err != nil { return nil, nil, fmt.Errorf("failed to load SQL file: %v", err) } @@ -353,12 +353,11 @@ func NewTestStoreFromSQL(ctx context.Context, filename string, dataDir string) ( return nil, nil, fmt.Errorf("failed to create test store: %v", err) } - err = addAllGroupToAccount(ctx, store) + err = addAllGroupToAccount(ctx, store) if err != nil { return nil, nil, fmt.Errorf("failed to add all group to account: %v", err) } - maxRetries := 2 for i := 0; i < maxRetries; i++ { sqlStore, cleanUp, err := getSqlStoreEngine(ctx, store, kind) @@ -516,7 +515,7 @@ func replaceDBName(dsn, newDBName string) string { return re.ReplaceAllString(dsn, `${pre}`+newDBName+`${post}`) } -func loadSQL(db *gorm.DB, filepath string) error { +func LoadSQL(db *gorm.DB, filepath string) error { sqlContent, err := os.ReadFile(filepath) if err != nil { return err diff --git a/management/server/token_mgr.go b/management/server/token_mgr.go index ec8aae47e..f8238aa16 100644 --- a/management/server/token_mgr.go +++ b/management/server/token_mgr.go @@ -12,8 +12,11 @@ import ( log "github.com/sirupsen/logrus" "github.com/netbirdio/netbird/management/proto" + "github.com/netbirdio/netbird/management/server/settings" auth "github.com/netbirdio/netbird/relay/auth/hmac" authv2 "github.com/netbirdio/netbird/relay/auth/hmac/v2" + + integrationsConfig "github.com/netbirdio/management-integrations/integrations/config" ) const defaultDuration = 12 * time.Hour @@ -22,31 +25,33 @@ const defaultDuration = 12 * time.Hour type SecretsManager interface { GenerateTurnToken() (*Token, error) GenerateRelayToken() (*Token, error) - SetupRefresh(ctx context.Context, peerKey string) + SetupRefresh(ctx context.Context, accountID, peerKey string) CancelRefresh(peerKey string) } // TimeBasedAuthSecretsManager generates credentials with TTL and using pre-shared secret known to TURN server type TimeBasedAuthSecretsManager struct { - mux sync.Mutex - turnCfg *TURNConfig - relayCfg *Relay - turnHmacToken *auth.TimedHMAC - relayHmacToken *authv2.Generator - updateManager *PeersUpdateManager - turnCancelMap map[string]chan struct{} - relayCancelMap map[string]chan struct{} + mux sync.Mutex + turnCfg *TURNConfig + relayCfg *Relay + turnHmacToken *auth.TimedHMAC + relayHmacToken *authv2.Generator + updateManager *PeersUpdateManager + settingsManager settings.Manager + turnCancelMap map[string]chan struct{} + relayCancelMap map[string]chan struct{} } type Token auth.Token -func NewTimeBasedAuthSecretsManager(updateManager *PeersUpdateManager, turnCfg *TURNConfig, relayCfg *Relay) *TimeBasedAuthSecretsManager { +func NewTimeBasedAuthSecretsManager(updateManager *PeersUpdateManager, turnCfg *TURNConfig, relayCfg *Relay, settingsManager settings.Manager) *TimeBasedAuthSecretsManager { mgr := &TimeBasedAuthSecretsManager{ - updateManager: updateManager, - turnCfg: turnCfg, - relayCfg: relayCfg, - turnCancelMap: make(map[string]chan struct{}), - relayCancelMap: make(map[string]chan struct{}), + updateManager: updateManager, + turnCfg: turnCfg, + relayCfg: relayCfg, + turnCancelMap: make(map[string]chan struct{}), + relayCancelMap: make(map[string]chan struct{}), + settingsManager: settingsManager, } if turnCfg != nil { @@ -126,7 +131,7 @@ func (m *TimeBasedAuthSecretsManager) CancelRefresh(peerID string) { } // SetupRefresh starts peer credentials refresh -func (m *TimeBasedAuthSecretsManager) SetupRefresh(ctx context.Context, peerID string) { +func (m *TimeBasedAuthSecretsManager) SetupRefresh(ctx context.Context, accountID, peerID string) { m.mux.Lock() defer m.mux.Unlock() @@ -136,19 +141,19 @@ func (m *TimeBasedAuthSecretsManager) SetupRefresh(ctx context.Context, peerID s if m.turnCfg != nil && m.turnCfg.TimeBasedCredentials { turnCancel := make(chan struct{}, 1) m.turnCancelMap[peerID] = turnCancel - go m.refreshTURNTokens(ctx, peerID, turnCancel) + go m.refreshTURNTokens(ctx, accountID, peerID, turnCancel) log.WithContext(ctx).Debugf("starting TURN refresh for %s", peerID) } if m.relayCfg != nil { relayCancel := make(chan struct{}, 1) m.relayCancelMap[peerID] = relayCancel - go m.refreshRelayTokens(ctx, peerID, relayCancel) + go m.refreshRelayTokens(ctx, accountID, peerID, relayCancel) log.WithContext(ctx).Debugf("starting relay refresh for %s", peerID) } } -func (m *TimeBasedAuthSecretsManager) refreshTURNTokens(ctx context.Context, peerID string, cancel chan struct{}) { +func (m *TimeBasedAuthSecretsManager) refreshTURNTokens(ctx context.Context, accountID, peerID string, cancel chan struct{}) { ticker := time.NewTicker(m.turnCfg.CredentialsTTL.Duration / 4 * 3) defer ticker.Stop() @@ -158,12 +163,12 @@ func (m *TimeBasedAuthSecretsManager) refreshTURNTokens(ctx context.Context, pee log.WithContext(ctx).Debugf("stopping TURN refresh for %s", peerID) return case <-ticker.C: - m.pushNewTURNAndRelayTokens(ctx, peerID) + m.pushNewTURNAndRelayTokens(ctx, accountID, peerID) } } } -func (m *TimeBasedAuthSecretsManager) refreshRelayTokens(ctx context.Context, peerID string, cancel chan struct{}) { +func (m *TimeBasedAuthSecretsManager) refreshRelayTokens(ctx context.Context, accountID, peerID string, cancel chan struct{}) { ticker := time.NewTicker(m.relayCfg.CredentialsTTL.Duration / 4 * 3) defer ticker.Stop() @@ -173,15 +178,15 @@ func (m *TimeBasedAuthSecretsManager) refreshRelayTokens(ctx context.Context, pe log.WithContext(ctx).Debugf("stopping relay refresh for %s", peerID) return case <-ticker.C: - m.pushNewRelayTokens(ctx, peerID) + m.pushNewRelayTokens(ctx, accountID, peerID) } } } -func (m *TimeBasedAuthSecretsManager) pushNewTURNAndRelayTokens(ctx context.Context, peerID string) { +func (m *TimeBasedAuthSecretsManager) pushNewTURNAndRelayTokens(ctx context.Context, accountID, peerID string) { turnToken, err := m.turnHmacToken.GenerateToken(sha1.New) if err != nil { - log.Errorf("failed to generate token for peer '%s': %s", peerID, err) + log.WithContext(ctx).Errorf("failed to generate token for peer '%s': %s", peerID, err) return } @@ -216,11 +221,13 @@ func (m *TimeBasedAuthSecretsManager) pushNewTURNAndRelayTokens(ctx context.Cont } } + m.extendNetbirdConfig(ctx, accountID, update) + log.WithContext(ctx).Debugf("sending new TURN credentials to peer %s", peerID) m.updateManager.SendUpdate(ctx, peerID, &UpdateMessage{Update: update}) } -func (m *TimeBasedAuthSecretsManager) pushNewRelayTokens(ctx context.Context, peerID string) { +func (m *TimeBasedAuthSecretsManager) pushNewRelayTokens(ctx context.Context, accountID, peerID string) { relayToken, err := m.relayHmacToken.GenerateToken() if err != nil { log.Errorf("failed to generate relay token for peer '%s': %s", peerID, err) @@ -238,6 +245,17 @@ func (m *TimeBasedAuthSecretsManager) pushNewRelayTokens(ctx context.Context, pe }, } + m.extendNetbirdConfig(ctx, accountID, update) + log.WithContext(ctx).Debugf("sending new relay credentials to peer %s", peerID) m.updateManager.SendUpdate(ctx, peerID, &UpdateMessage{Update: update}) } + +func (m *TimeBasedAuthSecretsManager) extendNetbirdConfig(ctx context.Context, accountID string, update *proto.SyncResponse) { + extraSettings, err := m.settingsManager.GetExtraSettings(ctx, accountID) + if err != nil { + log.WithContext(ctx).Errorf("failed to get extra settings: %v", err) + } + + integrationsConfig.ExtendNetBirdConfig(update.NetbirdConfig, extraSettings) +} diff --git a/management/server/token_mgr_test.go b/management/server/token_mgr_test.go index f2b056d8f..c07e40418 100644 --- a/management/server/token_mgr_test.go +++ b/management/server/token_mgr_test.go @@ -10,9 +10,12 @@ import ( "testing" "time" + "github.com/golang/mock/gomock" "github.com/stretchr/testify/require" "github.com/netbirdio/netbird/management/proto" + "github.com/netbirdio/netbird/management/server/settings" + "github.com/netbirdio/netbird/management/server/types" "github.com/netbirdio/netbird/util" ) @@ -34,12 +37,16 @@ func TestTimeBasedAuthSecretsManager_GenerateCredentials(t *testing.T) { Secret: secret, } + ctrl := gomock.NewController(t) + t.Cleanup(ctrl.Finish) + settingsMockManager := settings.NewMockManager(ctrl) + tested := NewTimeBasedAuthSecretsManager(peersManager, &TURNConfig{ CredentialsTTL: ttl, Secret: secret, Turns: []*Host{TurnTestHost}, TimeBasedCredentials: true, - }, rc) + }, rc, settingsMockManager) turnCredentials, err := tested.GenerateTurnToken() require.NoError(t, err) @@ -79,17 +86,23 @@ func TestTimeBasedAuthSecretsManager_SetupRefresh(t *testing.T) { CredentialsTTL: ttl, Secret: secret, } + + ctrl := gomock.NewController(t) + t.Cleanup(ctrl.Finish) + settingsMockManager := settings.NewMockManager(ctrl) + settingsMockManager.EXPECT().GetExtraSettings(gomock.Any(), "someAccountID").Return(&types.ExtraSettings{}, nil).AnyTimes() + tested := NewTimeBasedAuthSecretsManager(peersManager, &TURNConfig{ CredentialsTTL: ttl, Secret: secret, Turns: []*Host{TurnTestHost}, TimeBasedCredentials: true, - }, rc) + }, rc, settingsMockManager) ctx, cancel := context.WithCancel(context.Background()) defer cancel() - tested.SetupRefresh(ctx, peer) + tested.SetupRefresh(ctx, "someAccountID", peer) if _, ok := tested.turnCancelMap[peer]; !ok { t.Errorf("expecting peer to be present in the turn cancel map, got not present") @@ -176,14 +189,19 @@ func TestTimeBasedAuthSecretsManager_CancelRefresh(t *testing.T) { CredentialsTTL: ttl, Secret: secret, } + + ctrl := gomock.NewController(t) + t.Cleanup(ctrl.Finish) + settingsMockManager := settings.NewMockManager(ctrl) + tested := NewTimeBasedAuthSecretsManager(peersManager, &TURNConfig{ CredentialsTTL: ttl, Secret: secret, Turns: []*Host{TurnTestHost}, TimeBasedCredentials: true, - }, rc) + }, rc, settingsMockManager) - tested.SetupRefresh(context.Background(), peer) + tested.SetupRefresh(context.Background(), "someAccountID", peer) if _, ok := tested.turnCancelMap[peer]; !ok { t.Errorf("expecting peer to be present in turn cancel map, got not present") } diff --git a/management/server/types/account.go b/management/server/types/account.go index c890a7730..ce5abfd32 100644 --- a/management/server/types/account.go +++ b/management/server/types/account.go @@ -1012,6 +1012,7 @@ func (a *Account) connResourcesGenerator(ctx context.Context) (func(*PolicyRule, } fr := FirewallRule{ + PolicyID: rule.ID, PeerIP: peer.IP.String(), Direction: direction, Action: string(rule.Action), diff --git a/management/server/types/firewall_rule.go b/management/server/types/firewall_rule.go index 10923828d..d98a56871 100644 --- a/management/server/types/firewall_rule.go +++ b/management/server/types/firewall_rule.go @@ -20,6 +20,9 @@ const ( // FirewallRule is a rule of the firewall. type FirewallRule struct { + // PolicyID is the ID of the policy this rule is derived from + PolicyID string + // PeerIP of the peer PeerIP string @@ -58,6 +61,7 @@ func generateRouteFirewallRules(ctx context.Context, route *nbroute.Route, rule } baseRule := RouteFirewallRule{ + PolicyID: rule.PolicyID, SourceRanges: sourceRanges, Action: string(rule.Action), Destination: route.Network.String(), diff --git a/management/server/types/peer.go b/management/server/types/peer.go new file mode 100644 index 000000000..15d343793 --- /dev/null +++ b/management/server/types/peer.go @@ -0,0 +1,37 @@ +package types + +import ( + "net" + + nbpeer "github.com/netbirdio/netbird/management/server/peer" +) + +// PeerSync used as a data object between the gRPC API and Manager on Sync request. +type PeerSync struct { + // WireGuardPubKey is a peers WireGuard public key + WireGuardPubKey string + // Meta is the system information passed by peer, must be always present + Meta nbpeer.PeerSystemMeta + // UpdateAccountPeers indicate updating account peers, + // which occurs when the peer's metadata is updated + UpdateAccountPeers bool +} + +// PeerLogin used as a data object between the gRPC API and Manager on Login request. +type PeerLogin struct { + // WireGuardPubKey is a peers WireGuard public key + WireGuardPubKey string + // SSHKey is a peer's ssh key. Can be empty (e.g., old version do not provide it, or this feature is disabled) + SSHKey string + // Meta is the system information passed by peer, must be always present. + Meta nbpeer.PeerSystemMeta + // UserID indicates that JWT was used to log in, and it was valid. Can be empty when SetupKey is used or auth is not required. + UserID string + // SetupKey references to a server.SetupKey to log in. Can be empty when UserID is used or auth is not required. + SetupKey string + // ConnectionIP is the real IP of the peer + ConnectionIP net.IP + + // ExtraDNSLabels is a list of extra DNS labels that the peer wants to use + ExtraDNSLabels []string +} diff --git a/management/server/types/route_firewall_rule.go b/management/server/types/route_firewall_rule.go index 18eda7eda..5b752bc36 100644 --- a/management/server/types/route_firewall_rule.go +++ b/management/server/types/route_firewall_rule.go @@ -6,6 +6,9 @@ import ( // RouteFirewallRule a firewall rule applicable for a routed network. type RouteFirewallRule struct { + // PolicyID is the ID of the policy this rule is derived from + PolicyID string + // SourceRanges IP ranges of the routing peers. SourceRanges []string diff --git a/management/server/types/settings.go b/management/server/types/settings.go index 0ce5a6133..7054ede8c 100644 --- a/management/server/types/settings.go +++ b/management/server/types/settings.go @@ -2,8 +2,6 @@ package types import ( "time" - - "github.com/netbirdio/netbird/management/server/account" ) // Settings represents Account settings structure that can be modified via API and Dashboard @@ -42,7 +40,7 @@ type Settings struct { RoutingPeerDNSResolutionEnabled bool // Extra is a dictionary of Account settings - Extra *account.ExtraSettings `gorm:"embedded;embeddedPrefix:extra_"` + Extra *ExtraSettings `gorm:"embedded;embeddedPrefix:extra_"` } // Copy copies the Settings struct @@ -66,3 +64,26 @@ func (s *Settings) Copy() *Settings { } return settings } + +type ExtraSettings struct { + // PeerApprovalEnabled enables or disables the need for peers bo be approved by an administrator + PeerApprovalEnabled bool + + // IntegratedValidatorGroups list of group IDs to be used with integrated approval configurations + IntegratedValidatorGroups []string `gorm:"serializer:json"` + + FlowEnabled bool `gorm:"-"` + FlowPacketCounterEnabled bool `gorm:"-"` + FlowENCollectionEnabled bool `gorm:"-"` + FlowDnsCollectionEnabled bool `gorm:"-"` +} + +// Copy copies the ExtraSettings struct +func (e *ExtraSettings) Copy() *ExtraSettings { + var cpGroup []string + + return &ExtraSettings{ + PeerApprovalEnabled: e.PeerApprovalEnabled, + IntegratedValidatorGroups: append(cpGroup, e.IntegratedValidatorGroups...), + } +} diff --git a/management/server/user_test.go b/management/server/user_test.go index a180a761a..5a400f005 100644 --- a/management/server/user_test.go +++ b/management/server/user_test.go @@ -7,19 +7,18 @@ import ( "testing" "time" - "github.com/eko/gocache/v3/cache" - cacheStore "github.com/eko/gocache/v3/store" "github.com/google/go-cmp/cmp" + "golang.org/x/exp/maps" + + nbcache "github.com/netbirdio/netbird/management/server/cache" nbcontext "github.com/netbirdio/netbird/management/server/context" "github.com/netbirdio/netbird/management/server/util" - "golang.org/x/exp/maps" nbpeer "github.com/netbirdio/netbird/management/server/peer" "github.com/netbirdio/netbird/management/server/store" "github.com/netbirdio/netbird/management/server/types" - gocache "github.com/patrickmn/go-cache" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.zx2c4.com/wireguard/wgctrl/wgtypes" @@ -517,9 +516,10 @@ func TestUser_InviteNewUser(t *testing.T) { cacheLoading: map[string]chan struct{}{}, } - goCacheClient := gocache.New(CacheExpirationMax, 30*time.Minute) - goCacheStore := cacheStore.NewGoCache(goCacheClient) - am.cacheManager = cache.NewLoadable[[]*idp.UserData](am.loadAccount, cache.New[[]*idp.UserData](goCacheStore)) + cs, err := nbcache.NewStore(nbcache.DefaultIDPCacheExpirationMax, nbcache.DefaultIDPCacheCleanupInterval) + require.NoError(t, err) + + am.cacheManager = nbcache.NewAccountUserDataCache(am.loadAccount, cs) mockData := []*idp.UserData{ { @@ -1092,21 +1092,19 @@ func TestDefaultAccountManager_ExternalCache(t *testing.T) { eventStore: &activity.InMemoryEventStore{}, idpManager: &idp.GoogleWorkspaceManager{}, // empty manager cacheLoading: map[string]chan struct{}{}, - cacheManager: cache.New[[]*idp.UserData]( - cacheStore.NewGoCache(gocache.New(CacheExpirationMax, 30*time.Minute)), - ), - externalCacheManager: cache.New[*idp.UserData]( - cacheStore.NewGoCache(gocache.New(CacheExpirationMax, 30*time.Minute)), - ), } + cacheStore, err := nbcache.NewStore(nbcache.DefaultIDPCacheExpirationMax, nbcache.DefaultIDPCacheCleanupInterval) + assert.NoError(t, err) + am.externalCacheManager = nbcache.NewUserDataCache(cacheStore) + am.cacheManager = nbcache.NewAccountUserDataCache(am.loadAccount, cacheStore) // pretend that we receive mockUserID from IDP - err = am.cacheManager.Set(am.ctx, mockAccountID, []*idp.UserData{{Name: mockUserID, ID: mockUserID}}) + err = am.cacheManager.Set(am.ctx, mockAccountID, []*idp.UserData{{Name: mockUserID, ID: mockUserID}}, time.Minute) assert.NoError(t, err) cacheManager := am.GetExternalCacheManager() cacheKey := externalUser.IntegrationReference.CacheKey(mockAccountID, externalUser.Id) - err = cacheManager.Set(context.Background(), cacheKey, &idp.UserData{ID: externalUser.Id, Name: "Test User", Email: "user@example.com"}) + err = cacheManager.Set(context.Background(), cacheKey, &idp.UserData{ID: externalUser.Id, Name: "Test User", Email: "user@example.com"}, time.Minute) assert.NoError(t, err) infos, err := am.GetUsersFromAccount(context.Background(), mockAccountID, mockUserID)