diff --git a/.github/workflows/golang-test-windows.yml b/.github/workflows/golang-test-windows.yml index a50c81918..6027d3626 100644 --- a/.github/workflows/golang-test-windows.yml +++ b/.github/workflows/golang-test-windows.yml @@ -44,7 +44,6 @@ jobs: - run: PsExec64 -s -w ${{ github.workspace }} C:\hostedtoolcache\windows\go\${{ steps.go.outputs.go-version }}\x64\bin\go.exe env -w GOMODCACHE=C:\Users\runneradmin\go\pkg\mod - run: PsExec64 -s -w ${{ github.workspace }} C:\hostedtoolcache\windows\go\${{ steps.go.outputs.go-version }}\x64\bin\go.exe env -w GOCACHE=C:\Users\runneradmin\AppData\Local\go-build - - run: "[Environment]::SetEnvironmentVariable('NETBIRD_STORE_ENGINE', 'jsonfile', 'Machine')" - name: test run: PsExec64 -s -w ${{ github.workspace }} cmd.exe /c "C:\hostedtoolcache\windows\go\${{ steps.go.outputs.go-version }}\x64\bin\go.exe test -timeout 5m -p 1 ./... > test-out.txt 2>&1" diff --git a/.github/workflows/mobile-build-validation.yml b/.github/workflows/mobile-build-validation.yml index 8cb0eed74..852964842 100644 --- a/.github/workflows/mobile-build-validation.yml +++ b/.github/workflows/mobile-build-validation.yml @@ -11,7 +11,7 @@ concurrency: cancel-in-progress: true jobs: - andrloid_build: + android_build: runs-on: ubuntu-latest steps: - name: Checkout repository @@ -41,7 +41,7 @@ jobs: run: go install golang.org/x/mobile/cmd/gomobile@v0.0.0-20230531173138-3c911d8e3eda - name: gomobile init run: gomobile init - - name: build android nebtird lib + - name: build android netbird lib run: PATH=$PATH:$(go env GOPATH) gomobile bind -o $GITHUB_WORKSPACE/netbird.aar -javapkg=io.netbird.gomobile -ldflags="-X golang.zx2c4.com/wireguard/ipc.socketDirectory=/data/data/io.netbird.client/cache/wireguard -X github.com/netbirdio/netbird/version.version=buildtest" $GITHUB_WORKSPACE/client/android env: CGO_ENABLED: 0 @@ -59,7 +59,7 @@ jobs: run: go install golang.org/x/mobile/cmd/gomobile@v0.0.0-20230531173138-3c911d8e3eda - name: gomobile init run: gomobile init - - name: build iOS nebtird lib + - name: build iOS netbird lib run: PATH=$PATH:$(go env GOPATH) gomobile bind -target=ios -bundleid=io.netbird.framework -ldflags="-X github.com/netbirdio/netbird/version.version=buildtest" -o $GITHUB_WORKSPACE/NetBirdSDK.xcframework $GITHUB_WORKSPACE/client/ios/NetBirdSDK env: CGO_ENABLED: 0 \ No newline at end of file diff --git a/client/cmd/root.go b/client/cmd/root.go index b3a924016..c3ff0a3c8 100644 --- a/client/cmd/root.go +++ b/client/cmd/root.go @@ -61,6 +61,7 @@ var ( serverSSHAllowed bool interfaceName string wireguardPort uint16 + serviceName string autoConnectDisabled bool rootCmd = &cobra.Command{ Use: "netbird", @@ -100,9 +101,16 @@ func init() { if runtime.GOOS == "windows" { defaultDaemonAddr = "tcp://127.0.0.1:41731" } + + defaultServiceName := "netbird" + if runtime.GOOS == "windows" { + defaultServiceName = "Netbird" + } + rootCmd.PersistentFlags().StringVar(&daemonAddr, "daemon-addr", defaultDaemonAddr, "Daemon service address to serve CLI requests [unix|tcp]://[path|host:port]") rootCmd.PersistentFlags().StringVarP(&managementURL, "management-url", "m", "", fmt.Sprintf("Management Service URL [http|https]://[host]:[port] (default \"%s\")", internal.DefaultManagementURL)) rootCmd.PersistentFlags().StringVar(&adminURL, "admin-url", "", fmt.Sprintf("Admin Panel URL [http|https]://[host]:[port] (default \"%s\")", internal.DefaultAdminURL)) + rootCmd.PersistentFlags().StringVarP(&serviceName, "service", "s", defaultServiceName, "Netbird system service name") rootCmd.PersistentFlags().StringVarP(&configPath, "config", "c", defaultConfigPath, "Netbird config file location") rootCmd.PersistentFlags().StringVarP(&logLevel, "log-level", "l", "info", "sets Netbird log level") rootCmd.PersistentFlags().StringVar(&logFile, "log-file", defaultLogFile, "sets Netbird log path. If console is specified the log will be output to stdout") diff --git a/client/cmd/service.go b/client/cmd/service.go index 18fe5d621..5c60744f9 100644 --- a/client/cmd/service.go +++ b/client/cmd/service.go @@ -2,8 +2,6 @@ package cmd import ( "context" - "runtime" - "github.com/kardianos/service" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" @@ -24,12 +22,8 @@ func newProgram(ctx context.Context, cancel context.CancelFunc) *program { } func newSVCConfig() *service.Config { - name := "netbird" - if runtime.GOOS == "windows" { - name = "Netbird" - } return &service.Config{ - Name: name, + Name: serviceName, DisplayName: "Netbird", Description: "A WireGuard-based mesh network that connects your devices into a single private network.", Option: make(service.KeyValue), diff --git a/client/cmd/status.go b/client/cmd/status.go index fded7dff8..4c7218fde 100644 --- a/client/cmd/status.go +++ b/client/cmd/status.go @@ -35,6 +35,7 @@ type peerStateDetailOutput struct { TransferReceived int64 `json:"transferReceived" yaml:"transferReceived"` TransferSent int64 `json:"transferSent" yaml:"transferSent"` RosenpassEnabled bool `json:"quantumResistance" yaml:"quantumResistance"` + Routes []string `json:"routes" yaml:"routes"` } type peersStateOutput struct { @@ -72,19 +73,28 @@ type iceCandidateType struct { Remote string `json:"remote" yaml:"remote"` } +type nsServerGroupStateOutput struct { + Servers []string `json:"servers" yaml:"servers"` + Domains []string `json:"domains" yaml:"domains"` + Enabled bool `json:"enabled" yaml:"enabled"` + Error string `json:"error" yaml:"error"` +} + type statusOutputOverview struct { - Peers peersStateOutput `json:"peers" yaml:"peers"` - CliVersion string `json:"cliVersion" yaml:"cliVersion"` - DaemonVersion string `json:"daemonVersion" yaml:"daemonVersion"` - ManagementState managementStateOutput `json:"management" yaml:"management"` - SignalState signalStateOutput `json:"signal" yaml:"signal"` - Relays relayStateOutput `json:"relays" yaml:"relays"` - IP string `json:"netbirdIp" yaml:"netbirdIp"` - PubKey string `json:"publicKey" yaml:"publicKey"` - KernelInterface bool `json:"usesKernelInterface" yaml:"usesKernelInterface"` - FQDN string `json:"fqdn" yaml:"fqdn"` - RosenpassEnabled bool `json:"quantumResistance" yaml:"quantumResistance"` - RosenpassPermissive bool `json:"quantumResistancePermissive" yaml:"quantumResistancePermissive"` + Peers peersStateOutput `json:"peers" yaml:"peers"` + CliVersion string `json:"cliVersion" yaml:"cliVersion"` + DaemonVersion string `json:"daemonVersion" yaml:"daemonVersion"` + ManagementState managementStateOutput `json:"management" yaml:"management"` + SignalState signalStateOutput `json:"signal" yaml:"signal"` + Relays relayStateOutput `json:"relays" yaml:"relays"` + IP string `json:"netbirdIp" yaml:"netbirdIp"` + PubKey string `json:"publicKey" yaml:"publicKey"` + KernelInterface bool `json:"usesKernelInterface" yaml:"usesKernelInterface"` + FQDN string `json:"fqdn" yaml:"fqdn"` + RosenpassEnabled bool `json:"quantumResistance" yaml:"quantumResistance"` + RosenpassPermissive bool `json:"quantumResistancePermissive" yaml:"quantumResistancePermissive"` + Routes []string `json:"routes" yaml:"routes"` + NSServerGroups []nsServerGroupStateOutput `json:"dnsServers" yaml:"dnsServers"` } var ( @@ -168,7 +178,7 @@ func statusFunc(cmd *cobra.Command, args []string) error { case yamlFlag: statusOutputString, err = parseToYAML(outputInformationHolder) default: - statusOutputString = parseGeneralSummary(outputInformationHolder, false, false) + statusOutputString = parseGeneralSummary(outputInformationHolder, false, false, false) } if err != nil { @@ -268,6 +278,8 @@ func convertToStatusOutputOverview(resp *proto.StatusResponse) statusOutputOverv FQDN: pbFullStatus.GetLocalPeerState().GetFqdn(), RosenpassEnabled: pbFullStatus.GetLocalPeerState().GetRosenpassEnabled(), RosenpassPermissive: pbFullStatus.GetLocalPeerState().GetRosenpassPermissive(), + Routes: pbFullStatus.GetLocalPeerState().GetRoutes(), + NSServerGroups: mapNSGroups(pbFullStatus.GetDnsServers()), } return overview @@ -299,6 +311,19 @@ func mapRelays(relays []*proto.RelayState) relayStateOutput { } } +func mapNSGroups(servers []*proto.NSGroupState) []nsServerGroupStateOutput { + mappedNSGroups := make([]nsServerGroupStateOutput, 0, len(servers)) + for _, pbNsGroupServer := range servers { + mappedNSGroups = append(mappedNSGroups, nsServerGroupStateOutput{ + Servers: pbNsGroupServer.GetServers(), + Domains: pbNsGroupServer.GetDomains(), + Enabled: pbNsGroupServer.GetEnabled(), + Error: pbNsGroupServer.GetError(), + }) + } + return mappedNSGroups +} + func mapPeers(peers []*proto.PeerState) peersStateOutput { var peersStateDetail []peerStateDetailOutput localICE := "" @@ -352,6 +377,7 @@ func mapPeers(peers []*proto.PeerState) peersStateOutput { TransferReceived: transferReceived, TransferSent: transferSent, RosenpassEnabled: pbPeerState.GetRosenpassEnabled(), + Routes: pbPeerState.GetRoutes(), } peersStateDetail = append(peersStateDetail, peerState) @@ -401,8 +427,7 @@ func parseToYAML(overview statusOutputOverview) (string, error) { return string(yamlBytes), nil } -func parseGeneralSummary(overview statusOutputOverview, showURL bool, showRelays bool) string { - +func parseGeneralSummary(overview statusOutputOverview, showURL bool, showRelays bool, showNameServers bool) string { var managementConnString string if overview.ManagementState.Connected { managementConnString = "Connected" @@ -438,7 +463,7 @@ func parseGeneralSummary(overview statusOutputOverview, showURL bool, showRelays interfaceIP = "N/A" } - var relayAvailableString string + var relaysString string if showRelays { for _, relay := range overview.Relays.Details { available := "Available" @@ -447,15 +472,46 @@ func parseGeneralSummary(overview statusOutputOverview, showURL bool, showRelays available = "Unavailable" reason = fmt.Sprintf(", reason: %s", relay.Error) } - relayAvailableString += fmt.Sprintf("\n [%s] is %s%s", relay.URI, available, reason) - + relaysString += fmt.Sprintf("\n [%s] is %s%s", relay.URI, available, reason) } } else { - - relayAvailableString = fmt.Sprintf("%d/%d Available", overview.Relays.Available, overview.Relays.Total) + relaysString = fmt.Sprintf("%d/%d Available", overview.Relays.Available, overview.Relays.Total) } - peersCountString := fmt.Sprintf("%d/%d Connected", overview.Peers.Connected, overview.Peers.Total) + routes := "-" + if len(overview.Routes) > 0 { + sort.Strings(overview.Routes) + routes = strings.Join(overview.Routes, ", ") + } + + var dnsServersString string + if showNameServers { + for _, nsServerGroup := range overview.NSServerGroups { + enabled := "Available" + if !nsServerGroup.Enabled { + enabled = "Unavailable" + } + errorString := "" + if nsServerGroup.Error != "" { + errorString = fmt.Sprintf(", reason: %s", nsServerGroup.Error) + errorString = strings.TrimSpace(errorString) + } + + domainsString := strings.Join(nsServerGroup.Domains, ", ") + if domainsString == "" { + domainsString = "." // Show "." for the default zone + } + dnsServersString += fmt.Sprintf( + "\n [%s] for [%s] is %s%s", + strings.Join(nsServerGroup.Servers, ", "), + domainsString, + enabled, + errorString, + ) + } + } else { + dnsServersString = fmt.Sprintf("%d/%d Available", countEnabled(overview.NSServerGroups), len(overview.NSServerGroups)) + } rosenpassEnabledStatus := "false" if overview.RosenpassEnabled { @@ -465,26 +521,32 @@ func parseGeneralSummary(overview statusOutputOverview, showURL bool, showRelays } } + peersCountString := fmt.Sprintf("%d/%d Connected", overview.Peers.Connected, overview.Peers.Total) + summary := fmt.Sprintf( "Daemon version: %s\n"+ "CLI version: %s\n"+ "Management: %s\n"+ "Signal: %s\n"+ "Relays: %s\n"+ + "Nameservers: %s\n"+ "FQDN: %s\n"+ "NetBird IP: %s\n"+ "Interface type: %s\n"+ "Quantum resistance: %s\n"+ + "Routes: %s\n"+ "Peers count: %s\n", overview.DaemonVersion, version.NetbirdVersion(), managementConnString, signalConnString, - relayAvailableString, + relaysString, + dnsServersString, overview.FQDN, interfaceIP, interfaceTypeString, rosenpassEnabledStatus, + routes, peersCountString, ) return summary @@ -492,7 +554,7 @@ func parseGeneralSummary(overview statusOutputOverview, showURL bool, showRelays func parseToFullDetailSummary(overview statusOutputOverview) string { parsedPeersString := parsePeers(overview.Peers, overview.RosenpassEnabled, overview.RosenpassPermissive) - summary := parseGeneralSummary(overview, true, true) + summary := parseGeneralSummary(overview, true, true, true) return fmt.Sprintf( "Peers detail:"+ @@ -556,6 +618,12 @@ func parsePeers(peers peersStateOutput, rosenpassEnabled, rosenpassPermissive bo } } + routes := "-" + if len(peerState.Routes) > 0 { + sort.Strings(peerState.Routes) + routes = strings.Join(peerState.Routes, ", ") + } + peerString := fmt.Sprintf( "\n %s:\n"+ " NetBird IP: %s\n"+ @@ -569,7 +637,8 @@ func parsePeers(peers peersStateOutput, rosenpassEnabled, rosenpassPermissive bo " Last connection update: %s\n"+ " Last WireGuard handshake: %s\n"+ " Transfer status (received/sent) %s/%s\n"+ - " Quantum resistance: %s\n", + " Quantum resistance: %s\n"+ + " Routes: %s\n", peerState.FQDN, peerState.IP, peerState.PubKey, @@ -585,6 +654,7 @@ func parsePeers(peers peersStateOutput, rosenpassEnabled, rosenpassPermissive bo toIEC(peerState.TransferReceived), toIEC(peerState.TransferSent), rosenpassEnabledStatus, + routes, ) peersString += peerString @@ -638,3 +708,13 @@ func toIEC(b int64) string { return fmt.Sprintf("%.1f %ciB", float64(b)/float64(div), "KMGTPE"[exp]) } + +func countEnabled(dnsServers []nsServerGroupStateOutput) int { + count := 0 + for _, server := range dnsServers { + if server.Enabled { + count++ + } + } + return count +} diff --git a/client/cmd/status_test.go b/client/cmd/status_test.go index b5db576e4..ea6980c3d 100644 --- a/client/cmd/status_test.go +++ b/client/cmd/status_test.go @@ -42,6 +42,9 @@ var resp = &proto.StatusResponse{ LastWireguardHandshake: timestamppb.New(time.Date(2001, time.Month(1), 1, 1, 1, 2, 0, time.UTC)), BytesRx: 200, BytesTx: 100, + Routes: []string{ + "10.1.0.0/24", + }, }, { IP: "192.168.178.102", @@ -87,6 +90,31 @@ var resp = &proto.StatusResponse{ PubKey: "Some-Pub-Key", KernelInterface: true, Fqdn: "some-localhost.awesome-domain.com", + Routes: []string{ + "10.10.0.0/24", + }, + }, + DnsServers: []*proto.NSGroupState{ + { + Servers: []string{ + "8.8.8.8:53", + }, + Domains: nil, + Enabled: true, + Error: "", + }, + { + Servers: []string{ + "1.1.1.1:53", + "2.2.2.2:53", + }, + Domains: []string{ + "example.com", + "example.net", + }, + Enabled: false, + Error: "timeout", + }, }, }, DaemonVersion: "0.14.1", @@ -116,6 +144,9 @@ var overview = statusOutputOverview{ LastWireguardHandshake: time.Date(2001, 1, 1, 1, 1, 2, 0, time.UTC), TransferReceived: 200, TransferSent: 100, + Routes: []string{ + "10.1.0.0/24", + }, }, { IP: "192.168.178.102", @@ -171,6 +202,31 @@ var overview = statusOutputOverview{ PubKey: "Some-Pub-Key", KernelInterface: true, FQDN: "some-localhost.awesome-domain.com", + NSServerGroups: []nsServerGroupStateOutput{ + { + Servers: []string{ + "8.8.8.8:53", + }, + Domains: nil, + Enabled: true, + Error: "", + }, + { + Servers: []string{ + "1.1.1.1:53", + "2.2.2.2:53", + }, + Domains: []string{ + "example.com", + "example.net", + }, + Enabled: false, + Error: "timeout", + }, + }, + Routes: []string{ + "10.10.0.0/24", + }, } func TestConversionFromFullStatusToOutputOverview(t *testing.T) { @@ -232,7 +288,10 @@ func TestParsingToJSON(t *testing.T) { "lastWireguardHandshake": "2001-01-01T01:01:02Z", "transferReceived": 200, "transferSent": 100, - "quantumResistance":false + "quantumResistance": false, + "routes": [ + "10.1.0.0/24" + ] }, { "fqdn": "peer-2.awesome-domain.com", @@ -253,7 +312,8 @@ func TestParsingToJSON(t *testing.T) { "lastWireguardHandshake": "2002-02-02T02:02:03Z", "transferReceived": 2000, "transferSent": 1000, - "quantumResistance":false + "quantumResistance": false, + "routes": null } ] }, @@ -289,8 +349,33 @@ func TestParsingToJSON(t *testing.T) { "publicKey": "Some-Pub-Key", "usesKernelInterface": true, "fqdn": "some-localhost.awesome-domain.com", - "quantumResistance":false, - "quantumResistancePermissive":false + "quantumResistance": false, + "quantumResistancePermissive": false, + "routes": [ + "10.10.0.0/24" + ], + "dnsServers": [ + { + "servers": [ + "8.8.8.8:53" + ], + "domains": null, + "enabled": true, + "error": "" + }, + { + "servers": [ + "1.1.1.1:53", + "2.2.2.2:53" + ], + "domains": [ + "example.com", + "example.net" + ], + "enabled": false, + "error": "timeout" + } + ] }` // @formatter:on @@ -325,6 +410,8 @@ func TestParsingToYAML(t *testing.T) { transferReceived: 200 transferSent: 100 quantumResistance: false + routes: + - 10.1.0.0/24 - fqdn: peer-2.awesome-domain.com netbirdIp: 192.168.178.102 publicKey: Pubkey2 @@ -342,6 +429,7 @@ func TestParsingToYAML(t *testing.T) { transferReceived: 2000 transferSent: 1000 quantumResistance: false + routes: [] cliVersion: development daemonVersion: 0.14.1 management: @@ -368,6 +456,22 @@ usesKernelInterface: true fqdn: some-localhost.awesome-domain.com quantumResistance: false quantumResistancePermissive: false +routes: + - 10.10.0.0/24 +dnsServers: + - servers: + - 8.8.8.8:53 + domains: [] + enabled: true + error: "" + - servers: + - 1.1.1.1:53 + - 2.2.2.2:53 + domains: + - example.com + - example.net + enabled: false + error: timeout ` assert.Equal(t, expectedYAML, yaml) @@ -391,6 +495,7 @@ func TestParsingToDetail(t *testing.T) { Last WireGuard handshake: 2001-01-01 01:01:02 Transfer status (received/sent) 200 B/100 B Quantum resistance: false + Routes: 10.1.0.0/24 peer-2.awesome-domain.com: NetBird IP: 192.168.178.102 @@ -405,6 +510,7 @@ func TestParsingToDetail(t *testing.T) { Last WireGuard handshake: 2002-02-02 02:02:03 Transfer status (received/sent) 2.0 KiB/1000 B Quantum resistance: false + Routes: - Daemon version: 0.14.1 CLI version: development @@ -413,10 +519,14 @@ Signal: Connected to my-awesome-signal.com:443 Relays: [stun:my-awesome-stun.com:3478] is Available [turns:my-awesome-turn.com:443?transport=tcp] is Unavailable, reason: context: deadline exceeded +Nameservers: + [8.8.8.8:53] for [.] is Available + [1.1.1.1:53, 2.2.2.2:53] for [example.com, example.net] is Unavailable, reason: timeout FQDN: some-localhost.awesome-domain.com NetBird IP: 192.168.178.100/16 Interface type: Kernel Quantum resistance: false +Routes: 10.10.0.0/24 Peers count: 2/2 Connected ` @@ -424,7 +534,7 @@ Peers count: 2/2 Connected } func TestParsingToShortVersion(t *testing.T) { - shortVersion := parseGeneralSummary(overview, false, false) + shortVersion := parseGeneralSummary(overview, false, false, false) expectedString := `Daemon version: 0.14.1 @@ -432,10 +542,12 @@ CLI version: development Management: Connected Signal: Connected Relays: 1/2 Available +Nameservers: 1/2 Available FQDN: some-localhost.awesome-domain.com NetBird IP: 192.168.178.100/16 Interface type: Kernel Quantum resistance: false +Routes: 10.10.0.0/24 Peers count: 2/2 Connected ` diff --git a/client/cmd/testutil.go b/client/cmd/testutil.go index cba47326f..2cfc93415 100644 --- a/client/cmd/testutil.go +++ b/client/cmd/testutil.go @@ -78,7 +78,7 @@ func startManagement(t *testing.T, config *mgmt.Config) (*grpc.Server, net.Liste if err != nil { return nil, nil } - accountManager, err := mgmt.BuildManager(store, peersUpdateManager, nil, "", "", eventStore, nil, false) + accountManager, err := mgmt.BuildManager(store, peersUpdateManager, nil, "", "netbird.selfhosted", eventStore, nil, false) if err != nil { t.Fatal(err) } diff --git a/client/internal/dns/server.go b/client/internal/dns/server.go index 9986f632e..b9608b6f2 100644 --- a/client/internal/dns/server.go +++ b/client/internal/dns/server.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "net/netip" + "strings" "sync" "github.com/miekg/dns" @@ -11,6 +12,7 @@ import ( log "github.com/sirupsen/logrus" "github.com/netbirdio/netbird/client/internal/listener" + "github.com/netbirdio/netbird/client/internal/peer" nbdns "github.com/netbirdio/netbird/dns" ) @@ -59,6 +61,8 @@ type DefaultServer struct { // make sense on mobile only searchDomainNotifier *notifier iosDnsManager IosDnsManager + + statusRecorder *peer.Status } type handlerWithStop interface { @@ -73,7 +77,12 @@ type muxUpdate struct { } // NewDefaultServer returns a new dns server -func NewDefaultServer(ctx context.Context, wgInterface WGIface, customAddress string) (*DefaultServer, error) { +func NewDefaultServer( + ctx context.Context, + wgInterface WGIface, + customAddress string, + statusRecorder *peer.Status, +) (*DefaultServer, error) { var addrPort *netip.AddrPort if customAddress != "" { parsedAddrPort, err := netip.ParseAddrPort(customAddress) @@ -90,13 +99,20 @@ func NewDefaultServer(ctx context.Context, wgInterface WGIface, customAddress st dnsService = newServiceViaListener(wgInterface, addrPort) } - return newDefaultServer(ctx, wgInterface, dnsService), nil + return newDefaultServer(ctx, wgInterface, dnsService, statusRecorder), nil } // NewDefaultServerPermanentUpstream returns a new dns server. It optimized for mobile systems -func NewDefaultServerPermanentUpstream(ctx context.Context, wgInterface WGIface, hostsDnsList []string, config nbdns.Config, listener listener.NetworkChangeListener) *DefaultServer { +func NewDefaultServerPermanentUpstream( + ctx context.Context, + wgInterface WGIface, + hostsDnsList []string, + config nbdns.Config, + listener listener.NetworkChangeListener, + statusRecorder *peer.Status, +) *DefaultServer { log.Debugf("host dns address list is: %v", hostsDnsList) - ds := newDefaultServer(ctx, wgInterface, newServiceViaMemory(wgInterface)) + ds := newDefaultServer(ctx, wgInterface, newServiceViaMemory(wgInterface), statusRecorder) ds.permanent = true ds.hostsDnsList = hostsDnsList ds.addHostRootZone() @@ -108,13 +124,18 @@ func NewDefaultServerPermanentUpstream(ctx context.Context, wgInterface WGIface, } // NewDefaultServerIos returns a new dns server. It optimized for ios -func NewDefaultServerIos(ctx context.Context, wgInterface WGIface, iosDnsManager IosDnsManager) *DefaultServer { - ds := newDefaultServer(ctx, wgInterface, newServiceViaMemory(wgInterface)) +func NewDefaultServerIos( + ctx context.Context, + wgInterface WGIface, + iosDnsManager IosDnsManager, + statusRecorder *peer.Status, +) *DefaultServer { + ds := newDefaultServer(ctx, wgInterface, newServiceViaMemory(wgInterface), statusRecorder) ds.iosDnsManager = iosDnsManager return ds } -func newDefaultServer(ctx context.Context, wgInterface WGIface, dnsService service) *DefaultServer { +func newDefaultServer(ctx context.Context, wgInterface WGIface, dnsService service, statusRecorder *peer.Status) *DefaultServer { ctx, stop := context.WithCancel(ctx) defaultServer := &DefaultServer{ ctx: ctx, @@ -124,7 +145,8 @@ func newDefaultServer(ctx context.Context, wgInterface WGIface, dnsService servi localResolver: &localResolver{ registeredMap: make(registrationMap), }, - wgInterface: wgInterface, + wgInterface: wgInterface, + statusRecorder: statusRecorder, } return defaultServer @@ -256,9 +278,15 @@ func (s *DefaultServer) SearchDomains() []string { // ProbeAvailability tests each upstream group's servers for availability // and deactivates the group if no server responds func (s *DefaultServer) ProbeAvailability() { + var wg sync.WaitGroup for _, mux := range s.dnsMuxMap { - mux.probeAvailability() + wg.Add(1) + go func(mux handlerWithStop) { + defer wg.Done() + mux.probeAvailability() + }(mux) } + wg.Wait() } func (s *DefaultServer) applyConfiguration(update nbdns.Config) error { @@ -299,6 +327,8 @@ func (s *DefaultServer) applyConfiguration(update nbdns.Config) error { s.searchDomainNotifier.onNewSearchDomains(s.SearchDomains()) } + s.updateNSGroupStates(update.NameServerGroups) + return nil } @@ -338,7 +368,13 @@ func (s *DefaultServer) buildUpstreamHandlerUpdate(nameServerGroups []*nbdns.Nam continue } - handler, err := newUpstreamResolver(s.ctx, s.wgInterface.Name(), s.wgInterface.Address().IP, s.wgInterface.Address().Network) + handler, err := newUpstreamResolver( + s.ctx, + s.wgInterface.Name(), + s.wgInterface.Address().IP, + s.wgInterface.Address().Network, + s.statusRecorder, + ) if err != nil { return nil, fmt.Errorf("unable to create a new upstream resolver, error: %v", err) } @@ -460,14 +496,14 @@ func getNSHostPort(ns nbdns.NameServer) string { func (s *DefaultServer) upstreamCallbacks( nsGroup *nbdns.NameServerGroup, handler dns.Handler, -) (deactivate func(), reactivate func()) { +) (deactivate func(error), reactivate func()) { var removeIndex map[string]int - deactivate = func() { + deactivate = func(err error) { s.mux.Lock() defer s.mux.Unlock() l := log.WithField("nameservers", nsGroup.NameServers) - l.Info("temporary deactivate nameservers group due timeout") + l.Info("Temporarily deactivating nameservers group due to timeout") removeIndex = make(map[string]int) for _, domain := range nsGroup.Domains { @@ -486,8 +522,11 @@ func (s *DefaultServer) upstreamCallbacks( } } if err := s.hostManager.applyDNSConfig(s.currentConfig); err != nil { - l.WithError(err).Error("fail to apply nameserver deactivation on the host") + l.Errorf("Failed to apply nameserver deactivation on the host: %v", err) } + + s.updateNSState(nsGroup, err, false) + } reactivate = func() { s.mux.Lock() @@ -510,12 +549,20 @@ func (s *DefaultServer) upstreamCallbacks( if err := s.hostManager.applyDNSConfig(s.currentConfig); err != nil { l.WithError(err).Error("reactivate temporary disabled nameserver group, DNS update apply") } + + s.updateNSState(nsGroup, nil, true) } return } func (s *DefaultServer) addHostRootZone() { - handler, err := newUpstreamResolver(s.ctx, s.wgInterface.Name(), s.wgInterface.Address().IP, s.wgInterface.Address().Network) + handler, err := newUpstreamResolver( + s.ctx, + s.wgInterface.Name(), + s.wgInterface.Address().IP, + s.wgInterface.Address().Network, + s.statusRecorder, + ) if err != nil { log.Errorf("unable to create a new upstream resolver, error: %v", err) return @@ -535,7 +582,50 @@ func (s *DefaultServer) addHostRootZone() { handler.upstreamServers[n] = fmt.Sprintf("%s:53", ipString) } - handler.deactivate = func() {} + handler.deactivate = func(error) {} handler.reactivate = func() {} s.service.RegisterMux(nbdns.RootZone, handler) } + +func (s *DefaultServer) updateNSGroupStates(groups []*nbdns.NameServerGroup) { + var states []peer.NSGroupState + + for _, group := range groups { + var servers []string + for _, ns := range group.NameServers { + servers = append(servers, fmt.Sprintf("%s:%d", ns.IP, ns.Port)) + } + + state := peer.NSGroupState{ + ID: generateGroupKey(group), + Servers: servers, + Domains: group.Domains, + // The probe will determine the state, default enabled + Enabled: true, + Error: nil, + } + states = append(states, state) + } + s.statusRecorder.UpdateDNSStates(states) +} + +func (s *DefaultServer) updateNSState(nsGroup *nbdns.NameServerGroup, err error, enabled bool) { + states := s.statusRecorder.GetDNSStates() + id := generateGroupKey(nsGroup) + for i, state := range states { + if state.ID == id { + states[i].Enabled = enabled + states[i].Error = err + break + } + } + s.statusRecorder.UpdateDNSStates(states) +} + +func generateGroupKey(nsGroup *nbdns.NameServerGroup) string { + var servers []string + for _, ns := range nsGroup.NameServers { + servers = append(servers, fmt.Sprintf("%s:%d", ns.IP, ns.Port)) + } + return fmt.Sprintf("%s_%s_%s", nsGroup.ID, nsGroup.Name, strings.Join(servers, ",")) +} diff --git a/client/internal/dns/server_test.go b/client/internal/dns/server_test.go index 68c4992d8..22966d89c 100644 --- a/client/internal/dns/server_test.go +++ b/client/internal/dns/server_test.go @@ -15,6 +15,7 @@ import ( "golang.zx2c4.com/wireguard/wgctrl/wgtypes" "github.com/netbirdio/netbird/client/firewall/uspfilter" + "github.com/netbirdio/netbird/client/internal/peer" "github.com/netbirdio/netbird/client/internal/stdnet" nbdns "github.com/netbirdio/netbird/dns" "github.com/netbirdio/netbird/formatter" @@ -274,7 +275,7 @@ func TestUpdateDNSServer(t *testing.T) { t.Log(err) } }() - dnsServer, err := NewDefaultServer(context.Background(), wgIface, "") + dnsServer, err := NewDefaultServer(context.Background(), wgIface, "", &peer.Status{}) if err != nil { t.Fatal(err) } @@ -375,7 +376,7 @@ func TestDNSFakeResolverHandleUpdates(t *testing.T) { return } - dnsServer, err := NewDefaultServer(context.Background(), wgIface, "") + dnsServer, err := NewDefaultServer(context.Background(), wgIface, "", &peer.Status{}) if err != nil { t.Errorf("create DNS server: %v", err) return @@ -470,7 +471,7 @@ func TestDNSServerStartStop(t *testing.T) { for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { - dnsServer, err := NewDefaultServer(context.Background(), &mocWGIface{}, testCase.addrPort) + dnsServer, err := NewDefaultServer(context.Background(), &mocWGIface{}, testCase.addrPort, &peer.Status{}) if err != nil { t.Fatalf("%v", err) } @@ -541,6 +542,7 @@ func TestDNSServerUpstreamDeactivateCallback(t *testing.T) { {false, "domain2", false}, }, }, + statusRecorder: &peer.Status{}, } var domainsUpdate string @@ -563,7 +565,7 @@ func TestDNSServerUpstreamDeactivateCallback(t *testing.T) { }, }, nil) - deactivate() + deactivate(nil) expected := "domain0,domain2" domains := []string{} for _, item := range server.currentConfig.Domains { @@ -601,7 +603,7 @@ func TestDNSPermanent_updateHostDNS_emptyUpstream(t *testing.T) { var dnsList []string dnsConfig := nbdns.Config{} - dnsServer := NewDefaultServerPermanentUpstream(context.Background(), wgIFace, dnsList, dnsConfig, nil) + dnsServer := NewDefaultServerPermanentUpstream(context.Background(), wgIFace, dnsList, dnsConfig, nil, &peer.Status{}) err = dnsServer.Initialize() if err != nil { t.Errorf("failed to initialize DNS server: %v", err) @@ -625,7 +627,7 @@ func TestDNSPermanent_updateUpstream(t *testing.T) { } defer wgIFace.Close() dnsConfig := nbdns.Config{} - dnsServer := NewDefaultServerPermanentUpstream(context.Background(), wgIFace, []string{"8.8.8.8"}, dnsConfig, nil) + dnsServer := NewDefaultServerPermanentUpstream(context.Background(), wgIFace, []string{"8.8.8.8"}, dnsConfig, nil, &peer.Status{}) err = dnsServer.Initialize() if err != nil { t.Errorf("failed to initialize DNS server: %v", err) @@ -717,7 +719,7 @@ func TestDNSPermanent_matchOnly(t *testing.T) { } defer wgIFace.Close() dnsConfig := nbdns.Config{} - dnsServer := NewDefaultServerPermanentUpstream(context.Background(), wgIFace, []string{"8.8.8.8"}, dnsConfig, nil) + dnsServer := NewDefaultServerPermanentUpstream(context.Background(), wgIFace, []string{"8.8.8.8"}, dnsConfig, nil, &peer.Status{}) err = dnsServer.Initialize() if err != nil { t.Errorf("failed to initialize DNS server: %v", err) @@ -748,6 +750,11 @@ func TestDNSPermanent_matchOnly(t *testing.T) { NSType: nbdns.UDPNameServerType, Port: 53, }, + { + IP: netip.MustParseAddr("9.9.9.9"), + NSType: nbdns.UDPNameServerType, + Port: 53, + }, }, Domains: []string{"customdomain.com"}, Primary: false, diff --git a/client/internal/dns/upstream.go b/client/internal/dns/upstream.go index 9fd524700..cc31559fa 100644 --- a/client/internal/dns/upstream.go +++ b/client/internal/dns/upstream.go @@ -11,8 +11,11 @@ import ( "time" "github.com/cenkalti/backoff/v4" + "github.com/hashicorp/go-multierror" "github.com/miekg/dns" log "github.com/sirupsen/logrus" + + "github.com/netbirdio/netbird/client/internal/peer" ) const ( @@ -45,12 +48,13 @@ type upstreamResolverBase struct { reactivatePeriod time.Duration upstreamTimeout time.Duration - deactivate func() - reactivate func() + deactivate func(error) + reactivate func() + statusRecorder *peer.Status } -func newUpstreamResolverBase(parentCTX context.Context) *upstreamResolverBase { - ctx, cancel := context.WithCancel(parentCTX) +func newUpstreamResolverBase(ctx context.Context, statusRecorder *peer.Status) *upstreamResolverBase { + ctx, cancel := context.WithCancel(ctx) return &upstreamResolverBase{ ctx: ctx, @@ -58,6 +62,7 @@ func newUpstreamResolverBase(parentCTX context.Context) *upstreamResolverBase { upstreamTimeout: upstreamTimeout, reactivatePeriod: reactivatePeriod, failsTillDeact: failsTillDeact, + statusRecorder: statusRecorder, } } @@ -68,7 +73,10 @@ func (u *upstreamResolverBase) stop() { // ServeDNS handles a DNS request func (u *upstreamResolverBase) ServeDNS(w dns.ResponseWriter, r *dns.Msg) { - defer u.checkUpstreamFails() + var err error + defer func() { + u.checkUpstreamFails(err) + }() log.WithField("question", r.Question[0]).Trace("received an upstream question") @@ -81,7 +89,6 @@ func (u *upstreamResolverBase) ServeDNS(w dns.ResponseWriter, r *dns.Msg) { for _, upstream := range u.upstreamServers { var rm *dns.Msg var t time.Duration - var err error func() { ctx, cancel := context.WithTimeout(u.ctx, u.upstreamTimeout) @@ -132,7 +139,7 @@ func (u *upstreamResolverBase) ServeDNS(w dns.ResponseWriter, r *dns.Msg) { // If fails count is greater that failsTillDeact, upstream resolving // will be disabled for reactivatePeriod, after that time period fails counter // will be reset and upstream will be reactivated. -func (u *upstreamResolverBase) checkUpstreamFails() { +func (u *upstreamResolverBase) checkUpstreamFails(err error) { u.mutex.Lock() defer u.mutex.Unlock() @@ -146,7 +153,7 @@ func (u *upstreamResolverBase) checkUpstreamFails() { default: } - u.disable() + u.disable(err) } // probeAvailability tests all upstream servers simultaneously and @@ -165,13 +172,16 @@ func (u *upstreamResolverBase) probeAvailability() { var mu sync.Mutex var wg sync.WaitGroup + var errors *multierror.Error for _, upstream := range u.upstreamServers { upstream := upstream wg.Add(1) go func() { defer wg.Done() - if err := u.testNameserver(upstream); err != nil { + err := u.testNameserver(upstream) + if err != nil { + errors = multierror.Append(errors, err) log.Warnf("probing upstream nameserver %s: %s", upstream, err) return } @@ -186,7 +196,7 @@ func (u *upstreamResolverBase) probeAvailability() { // didn't find a working upstream server, let's disable and try later if !success { - u.disable() + u.disable(errors.ErrorOrNil()) } } @@ -245,15 +255,15 @@ func isTimeout(err error) bool { return false } -func (u *upstreamResolverBase) disable() { +func (u *upstreamResolverBase) disable(err error) { if u.disabled { return } // todo test the deactivation logic, it seems to affect the client if runtime.GOOS != "ios" { - log.Warnf("upstream resolving is Disabled for %v", reactivatePeriod) - u.deactivate() + log.Warnf("Upstream resolving is Disabled for %v", reactivatePeriod) + u.deactivate(err) u.disabled = true go u.waitUntilResponse() } diff --git a/client/internal/dns/upstream_ios.go b/client/internal/dns/upstream_ios.go index 33937d8d8..c9d3bb942 100644 --- a/client/internal/dns/upstream_ios.go +++ b/client/internal/dns/upstream_ios.go @@ -11,6 +11,8 @@ import ( "github.com/miekg/dns" log "github.com/sirupsen/logrus" "golang.org/x/sys/unix" + + "github.com/netbirdio/netbird/client/internal/peer" ) type upstreamResolverIOS struct { @@ -20,8 +22,14 @@ type upstreamResolverIOS struct { iIndex int } -func newUpstreamResolver(parentCTX context.Context, interfaceName string, ip net.IP, net *net.IPNet) (*upstreamResolverIOS, error) { - upstreamResolverBase := newUpstreamResolverBase(parentCTX) +func newUpstreamResolver( + ctx context.Context, + interfaceName string, + ip net.IP, + net *net.IPNet, + statusRecorder *peer.Status, +) (*upstreamResolverIOS, error) { + upstreamResolverBase := newUpstreamResolverBase(ctx, statusRecorder) index, err := getInterfaceIndex(interfaceName) if err != nil { diff --git a/client/internal/dns/upstream_nonios.go b/client/internal/dns/upstream_nonios.go index 93e523c4e..22bd24ca9 100644 --- a/client/internal/dns/upstream_nonios.go +++ b/client/internal/dns/upstream_nonios.go @@ -8,14 +8,22 @@ import ( "time" "github.com/miekg/dns" + + "github.com/netbirdio/netbird/client/internal/peer" ) type upstreamResolverNonIOS struct { *upstreamResolverBase } -func newUpstreamResolver(parentCTX context.Context, interfaceName string, ip net.IP, net *net.IPNet) (*upstreamResolverNonIOS, error) { - upstreamResolverBase := newUpstreamResolverBase(parentCTX) +func newUpstreamResolver( + ctx context.Context, + _ string, + _ net.IP, + _ *net.IPNet, + statusRecorder *peer.Status, +) (*upstreamResolverNonIOS, error) { + upstreamResolverBase := newUpstreamResolverBase(ctx, statusRecorder) nonIOS := &upstreamResolverNonIOS{ upstreamResolverBase: upstreamResolverBase, } diff --git a/client/internal/dns/upstream_test.go b/client/internal/dns/upstream_test.go index 13610df41..77851dd9d 100644 --- a/client/internal/dns/upstream_test.go +++ b/client/internal/dns/upstream_test.go @@ -58,7 +58,7 @@ func TestUpstreamResolver_ServeDNS(t *testing.T) { for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { ctx, cancel := context.WithCancel(context.TODO()) - resolver, _ := newUpstreamResolver(ctx, "", net.IP{}, &net.IPNet{}) + resolver, _ := newUpstreamResolver(ctx, "", net.IP{}, &net.IPNet{}, nil) resolver.upstreamServers = testCase.InputServers resolver.upstreamTimeout = testCase.timeout if testCase.cancelCTX { @@ -131,7 +131,7 @@ func TestUpstreamResolver_DeactivationReactivation(t *testing.T) { } failed := false - resolver.deactivate = func() { + resolver.deactivate = func(error) { failed = true } diff --git a/client/internal/engine.go b/client/internal/engine.go index e4f0f236d..78d26f0b8 100644 --- a/client/internal/engine.go +++ b/client/internal/engine.go @@ -698,15 +698,16 @@ func (e *Engine) updateNetworkMap(networkMap *mgmProto.NetworkMap) error { log.Errorf("failed to update dns server, err: %v", err) } - // Test received (upstream) servers for availability right away instead of upon usage. - // If no server of a server group responds this will disable the respective handler and retry later. - e.dnsServer.ProbeAvailability() - if e.acl != nil { e.acl.ApplyFiltering(networkMap) } + e.networkSerial = serial + // Test received (upstream) servers for availability right away instead of upon usage. + // If no server of a server group responds this will disable the respective handler and retry later. + e.dnsServer.ProbeAvailability() + return nil } @@ -1188,14 +1189,21 @@ func (e *Engine) newDnsServer() ([]*route.Route, dns.Server, error) { if err != nil { return nil, nil, err } - dnsServer := dns.NewDefaultServerPermanentUpstream(e.ctx, e.wgInterface, e.mobileDep.HostDNSAddresses, *dnsConfig, e.mobileDep.NetworkChangeListener) + dnsServer := dns.NewDefaultServerPermanentUpstream( + e.ctx, + e.wgInterface, + e.mobileDep.HostDNSAddresses, + *dnsConfig, + e.mobileDep.NetworkChangeListener, + e.statusRecorder, + ) go e.mobileDep.DnsReadyListener.OnReady() return routes, dnsServer, nil case "ios": - dnsServer := dns.NewDefaultServerIos(e.ctx, e.wgInterface, e.mobileDep.DnsManager) + dnsServer := dns.NewDefaultServerIos(e.ctx, e.wgInterface, e.mobileDep.DnsManager, e.statusRecorder) return nil, dnsServer, nil default: - dnsServer, err := dns.NewDefaultServer(e.ctx, e.wgInterface, e.config.CustomDNSAddress) + dnsServer, err := dns.NewDefaultServer(e.ctx, e.wgInterface, e.config.CustomDNSAddress, e.statusRecorder) if err != nil { return nil, nil, err } diff --git a/client/internal/engine_test.go b/client/internal/engine_test.go index ee0380db7..952b3c90c 100644 --- a/client/internal/engine_test.go +++ b/client/internal/engine_test.go @@ -70,10 +70,10 @@ func TestEngine_SSH(t *testing.T) { defer cancel() engine := NewEngine(ctx, cancel, &signal.MockClient{}, &mgmt.MockClient{}, &EngineConfig{ - WgIfaceName: "utun101", - WgAddr: "100.64.0.1/24", - WgPrivateKey: key, - WgPort: 33100, + WgIfaceName: "utun101", + WgAddr: "100.64.0.1/24", + WgPrivateKey: key, + WgPort: 33100, ServerSSHAllowed: true, }, MobileDependency{}, peer.NewRecorder("https://mgm")) @@ -1050,7 +1050,7 @@ func startManagement(dataDir string) (*grpc.Server, string, error) { if err != nil { return nil, "", err } - accountManager, err := server.BuildManager(store, peersUpdateManager, nil, "", "", eventStore, nil, false) + accountManager, err := server.BuildManager(store, peersUpdateManager, nil, "", "netbird.selfhosted", eventStore, nil, false) if err != nil { return nil, "", err } diff --git a/client/internal/peer/status.go b/client/internal/peer/status.go index 87338f646..1e252c5dd 100644 --- a/client/internal/peer/status.go +++ b/client/internal/peer/status.go @@ -29,6 +29,7 @@ type State struct { BytesTx int64 BytesRx int64 RosenpassEnabled bool + Routes map[string]struct{} } // LocalPeerState contains the latest state of the local peer @@ -37,6 +38,7 @@ type LocalPeerState struct { PubKey string KernelInterface bool FQDN string + Routes map[string]struct{} } // SignalState contains the latest state of a signal connection @@ -59,6 +61,16 @@ type RosenpassState struct { Permissive bool } +// NSGroupState represents the status of a DNS server group, including associated domains, +// whether it's enabled, and the last error message encountered during probing. +type NSGroupState struct { + ID string + Servers []string + Domains []string + Enabled bool + Error error +} + // FullStatus contains the full state held by the Status instance type FullStatus struct { Peers []State @@ -67,6 +79,7 @@ type FullStatus struct { LocalPeerState LocalPeerState RosenpassState RosenpassState Relays []relay.ProbeResult + NSGroupStates []NSGroupState } // Status holds a state of peers, signal, management connections and relays @@ -86,6 +99,7 @@ type Status struct { notifier *notifier rosenpassEnabled bool rosenpassPermissive bool + nsGroupStates []NSGroupState // To reduce the number of notification invocation this bool will be true when need to call the notification // Some Peer actions mostly used by in a batch when the network map has been synchronized. In these type of events @@ -174,6 +188,10 @@ func (d *Status) UpdatePeerState(receivedState State) error { peerState.IP = receivedState.IP } + if receivedState.Routes != nil { + peerState.Routes = receivedState.Routes + } + skipNotification := shouldSkipNotify(receivedState, peerState) if receivedState.ConnStatus != peerState.ConnStatus { @@ -278,6 +296,13 @@ func (d *Status) GetPeerStateChangeNotifier(peer string) <-chan struct{} { return ch } +// GetLocalPeerState returns the local peer state +func (d *Status) GetLocalPeerState() LocalPeerState { + d.mux.Lock() + defer d.mux.Unlock() + return d.localPeer +} + // UpdateLocalPeerState updates local peer status func (d *Status) UpdateLocalPeerState(localPeerState LocalPeerState) { d.mux.Lock() @@ -364,6 +389,12 @@ func (d *Status) UpdateRelayStates(relayResults []relay.ProbeResult) { d.relayStates = relayResults } +func (d *Status) UpdateDNSStates(dnsStates []NSGroupState) { + d.mux.Lock() + defer d.mux.Unlock() + d.nsGroupStates = dnsStates +} + func (d *Status) GetRosenpassState() RosenpassState { return RosenpassState{ d.rosenpassEnabled, @@ -409,6 +440,10 @@ func (d *Status) GetRelayStates() []relay.ProbeResult { return d.relayStates } +func (d *Status) GetDNSStates() []NSGroupState { + return d.nsGroupStates +} + // GetFullStatus gets full status func (d *Status) GetFullStatus() FullStatus { d.mux.Lock() @@ -420,6 +455,7 @@ func (d *Status) GetFullStatus() FullStatus { LocalPeerState: d.localPeer, Relays: d.GetRelayStates(), RosenpassState: d.GetRosenpassState(), + NSGroupStates: d.GetDNSStates(), } for _, status := range d.peers { diff --git a/client/internal/routemanager/client.go b/client/internal/routemanager/client.go index ee98d503d..f7ead5827 100644 --- a/client/internal/routemanager/client.go +++ b/client/internal/routemanager/client.go @@ -160,6 +160,12 @@ func (c *clientNetwork) removeRouteFromWireguardPeer(peerKey string) error { if err != nil { return err } + + delete(state.Routes, c.network.String()) + if err := c.statusRecorder.UpdatePeerState(state); err != nil { + log.Warnf("Failed to update peer state: %v", err) + } + if state.ConnStatus != peer.StatusConnected { return nil } @@ -225,6 +231,20 @@ func (c *clientNetwork) recalculateRouteAndUpdatePeerAndSystem() error { } c.chosenRoute = c.routes[chosen] + + state, err := c.statusRecorder.GetPeer(c.chosenRoute.Peer) + if err != nil { + log.Errorf("Failed to get peer state: %v", err) + } else { + if state.Routes == nil { + state.Routes = map[string]struct{}{} + } + state.Routes[c.network.String()] = struct{}{} + if err := c.statusRecorder.UpdatePeerState(state); err != nil { + log.Warnf("Failed to update peer state: %v", err) + } + } + err = c.wgInterface.AddAllowedIP(c.chosenRoute.Peer, c.network.String()) if err != nil { log.Errorf("couldn't add allowed IP %s added for peer %s, err: %v", diff --git a/client/internal/routemanager/manager.go b/client/internal/routemanager/manager.go index e8a4bd134..fde943757 100644 --- a/client/internal/routemanager/manager.go +++ b/client/internal/routemanager/manager.go @@ -58,7 +58,7 @@ func NewManager(ctx context.Context, pubKey string, wgInterface *iface.WGIface, func (m *DefaultManager) EnableServerRouter(firewall firewall.Manager) error { var err error - m.serverRouter, err = newServerRouter(m.ctx, m.wgInterface, firewall) + m.serverRouter, err = newServerRouter(m.ctx, m.wgInterface, firewall, m.statusRecorder) if err != nil { return err } diff --git a/client/internal/routemanager/server_android.go b/client/internal/routemanager/server_android.go index 1918c7f6f..b4065bca6 100644 --- a/client/internal/routemanager/server_android.go +++ b/client/internal/routemanager/server_android.go @@ -7,9 +7,10 @@ import ( "fmt" firewall "github.com/netbirdio/netbird/client/firewall/manager" + "github.com/netbirdio/netbird/client/internal/peer" "github.com/netbirdio/netbird/iface" ) -func newServerRouter(context.Context, *iface.WGIface, firewall.Manager) (serverRouter, error) { +func newServerRouter(context.Context, *iface.WGIface, firewall.Manager, *peer.Status) (serverRouter, error) { return nil, fmt.Errorf("server route not supported on this os") } diff --git a/client/internal/routemanager/server_nonandroid.go b/client/internal/routemanager/server_nonandroid.go index 20e500e79..192367877 100644 --- a/client/internal/routemanager/server_nonandroid.go +++ b/client/internal/routemanager/server_nonandroid.go @@ -10,24 +10,27 @@ import ( log "github.com/sirupsen/logrus" firewall "github.com/netbirdio/netbird/client/firewall/manager" + "github.com/netbirdio/netbird/client/internal/peer" "github.com/netbirdio/netbird/iface" "github.com/netbirdio/netbird/route" ) type defaultServerRouter struct { - mux sync.Mutex - ctx context.Context - routes map[string]*route.Route - firewall firewall.Manager - wgInterface *iface.WGIface + mux sync.Mutex + ctx context.Context + routes map[string]*route.Route + firewall firewall.Manager + wgInterface *iface.WGIface + statusRecorder *peer.Status } -func newServerRouter(ctx context.Context, wgInterface *iface.WGIface, firewall firewall.Manager) (serverRouter, error) { +func newServerRouter(ctx context.Context, wgInterface *iface.WGIface, firewall firewall.Manager, statusRecorder *peer.Status) (serverRouter, error) { return &defaultServerRouter{ - ctx: ctx, - routes: make(map[string]*route.Route), - firewall: firewall, - wgInterface: wgInterface, + ctx: ctx, + routes: make(map[string]*route.Route), + firewall: firewall, + wgInterface: wgInterface, + statusRecorder: statusRecorder, }, nil } @@ -88,6 +91,11 @@ func (m *defaultServerRouter) removeFromServerNetwork(route *route.Route) error return err } delete(m.routes, route.ID) + + state := m.statusRecorder.GetLocalPeerState() + delete(state.Routes, route.Network.String()) + m.statusRecorder.UpdateLocalPeerState(state) + return nil } } @@ -105,6 +113,14 @@ func (m *defaultServerRouter) addToServerNetwork(route *route.Route) error { return err } m.routes[route.ID] = route + + state := m.statusRecorder.GetLocalPeerState() + if state.Routes == nil { + state.Routes = map[string]struct{}{} + } + state.Routes[route.Network.String()] = struct{}{} + m.statusRecorder.UpdateLocalPeerState(state) + return nil } } @@ -117,6 +133,10 @@ func (m *defaultServerRouter) cleanUp() { if err != nil { log.Warnf("failed to remove clean up route: %s", r.ID) } + + state := m.statusRecorder.GetLocalPeerState() + state.Routes = nil + m.statusRecorder.UpdateLocalPeerState(state) } } diff --git a/client/proto/daemon.pb.go b/client/proto/daemon.pb.go index a1c3aef11..869eceee5 100644 --- a/client/proto/daemon.pb.go +++ b/client/proto/daemon.pb.go @@ -772,6 +772,7 @@ type PeerState struct { BytesRx int64 `protobuf:"varint,13,opt,name=bytesRx,proto3" json:"bytesRx,omitempty"` BytesTx int64 `protobuf:"varint,14,opt,name=bytesTx,proto3" json:"bytesTx,omitempty"` RosenpassEnabled bool `protobuf:"varint,15,opt,name=rosenpassEnabled,proto3" json:"rosenpassEnabled,omitempty"` + Routes []string `protobuf:"bytes,16,rep,name=routes,proto3" json:"routes,omitempty"` } func (x *PeerState) Reset() { @@ -911,18 +912,26 @@ func (x *PeerState) GetRosenpassEnabled() bool { return false } +func (x *PeerState) GetRoutes() []string { + if x != nil { + return x.Routes + } + return nil +} + // LocalPeerState contains the latest state of the local peer type LocalPeerState struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - IP string `protobuf:"bytes,1,opt,name=IP,proto3" json:"IP,omitempty"` - PubKey string `protobuf:"bytes,2,opt,name=pubKey,proto3" json:"pubKey,omitempty"` - KernelInterface bool `protobuf:"varint,3,opt,name=kernelInterface,proto3" json:"kernelInterface,omitempty"` - Fqdn string `protobuf:"bytes,4,opt,name=fqdn,proto3" json:"fqdn,omitempty"` - RosenpassEnabled bool `protobuf:"varint,5,opt,name=rosenpassEnabled,proto3" json:"rosenpassEnabled,omitempty"` - RosenpassPermissive bool `protobuf:"varint,6,opt,name=rosenpassPermissive,proto3" json:"rosenpassPermissive,omitempty"` + IP string `protobuf:"bytes,1,opt,name=IP,proto3" json:"IP,omitempty"` + PubKey string `protobuf:"bytes,2,opt,name=pubKey,proto3" json:"pubKey,omitempty"` + KernelInterface bool `protobuf:"varint,3,opt,name=kernelInterface,proto3" json:"kernelInterface,omitempty"` + Fqdn string `protobuf:"bytes,4,opt,name=fqdn,proto3" json:"fqdn,omitempty"` + RosenpassEnabled bool `protobuf:"varint,5,opt,name=rosenpassEnabled,proto3" json:"rosenpassEnabled,omitempty"` + RosenpassPermissive bool `protobuf:"varint,6,opt,name=rosenpassPermissive,proto3" json:"rosenpassPermissive,omitempty"` + Routes []string `protobuf:"bytes,7,rep,name=routes,proto3" json:"routes,omitempty"` } func (x *LocalPeerState) Reset() { @@ -999,6 +1008,13 @@ func (x *LocalPeerState) GetRosenpassPermissive() bool { return false } +func (x *LocalPeerState) GetRoutes() []string { + if x != nil { + return x.Routes + } + return nil +} + // SignalState contains the latest state of a signal connection type SignalState struct { state protoimpl.MessageState @@ -1191,6 +1207,77 @@ func (x *RelayState) GetError() string { return "" } +type NSGroupState struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Servers []string `protobuf:"bytes,1,rep,name=servers,proto3" json:"servers,omitempty"` + Domains []string `protobuf:"bytes,2,rep,name=domains,proto3" json:"domains,omitempty"` + Enabled bool `protobuf:"varint,3,opt,name=enabled,proto3" json:"enabled,omitempty"` + Error string `protobuf:"bytes,4,opt,name=error,proto3" json:"error,omitempty"` +} + +func (x *NSGroupState) Reset() { + *x = NSGroupState{} + if protoimpl.UnsafeEnabled { + mi := &file_daemon_proto_msgTypes[17] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *NSGroupState) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*NSGroupState) ProtoMessage() {} + +func (x *NSGroupState) ProtoReflect() protoreflect.Message { + mi := &file_daemon_proto_msgTypes[17] + 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 NSGroupState.ProtoReflect.Descriptor instead. +func (*NSGroupState) Descriptor() ([]byte, []int) { + return file_daemon_proto_rawDescGZIP(), []int{17} +} + +func (x *NSGroupState) GetServers() []string { + if x != nil { + return x.Servers + } + return nil +} + +func (x *NSGroupState) GetDomains() []string { + if x != nil { + return x.Domains + } + return nil +} + +func (x *NSGroupState) GetEnabled() bool { + if x != nil { + return x.Enabled + } + return false +} + +func (x *NSGroupState) GetError() string { + if x != nil { + return x.Error + } + return "" +} + // FullStatus contains the full state held by the Status instance type FullStatus struct { state protoimpl.MessageState @@ -1202,12 +1289,13 @@ type FullStatus struct { LocalPeerState *LocalPeerState `protobuf:"bytes,3,opt,name=localPeerState,proto3" json:"localPeerState,omitempty"` Peers []*PeerState `protobuf:"bytes,4,rep,name=peers,proto3" json:"peers,omitempty"` Relays []*RelayState `protobuf:"bytes,5,rep,name=relays,proto3" json:"relays,omitempty"` + DnsServers []*NSGroupState `protobuf:"bytes,6,rep,name=dns_servers,json=dnsServers,proto3" json:"dns_servers,omitempty"` } func (x *FullStatus) Reset() { *x = FullStatus{} if protoimpl.UnsafeEnabled { - mi := &file_daemon_proto_msgTypes[17] + mi := &file_daemon_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1220,7 +1308,7 @@ func (x *FullStatus) String() string { func (*FullStatus) ProtoMessage() {} func (x *FullStatus) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[17] + mi := &file_daemon_proto_msgTypes[18] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1233,7 +1321,7 @@ func (x *FullStatus) ProtoReflect() protoreflect.Message { // Deprecated: Use FullStatus.ProtoReflect.Descriptor instead. func (*FullStatus) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{17} + return file_daemon_proto_rawDescGZIP(), []int{18} } func (x *FullStatus) GetManagementState() *ManagementState { @@ -1271,6 +1359,13 @@ func (x *FullStatus) GetRelays() []*RelayState { return nil } +func (x *FullStatus) GetDnsServers() []*NSGroupState { + if x != nil { + return x.DnsServers + } + return nil +} + var File_daemon_proto protoreflect.FileDescriptor var file_daemon_proto_rawDesc = []byte{ @@ -1380,7 +1475,7 @@ var file_daemon_proto_rawDesc = []byte{ 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x70, 0x72, 0x65, 0x53, 0x68, 0x61, 0x72, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x12, 0x1a, 0x0a, 0x08, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x55, 0x52, 0x4c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x55, 0x52, 0x4c, - 0x22, 0x81, 0x05, 0x0a, 0x09, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x0e, + 0x22, 0x99, 0x05, 0x0a, 0x09, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x50, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x12, 0x1e, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x6e, 0x53, 0x74, @@ -1420,20 +1515,23 @@ var file_daemon_proto_rawDesc = []byte{ 0x07, 0x62, 0x79, 0x74, 0x65, 0x73, 0x54, 0x78, 0x12, 0x2a, 0x0a, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, - 0x62, 0x6c, 0x65, 0x64, 0x22, 0xd4, 0x01, 0x0a, 0x0e, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x65, - 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x50, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x50, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x75, 0x62, 0x4b, 0x65, - 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x12, - 0x28, 0x0a, 0x0f, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, - 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, - 0x49, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x71, 0x64, - 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x12, 0x2a, 0x0a, + 0x62, 0x6c, 0x65, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x18, 0x10, + 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x22, 0xec, 0x01, 0x0a, + 0x0e, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, + 0x0e, 0x0a, 0x02, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x50, 0x12, + 0x16, 0x0a, 0x06, 0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x06, 0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x12, 0x28, 0x0a, 0x0f, 0x6b, 0x65, 0x72, 0x6e, 0x65, + 0x6c, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, + 0x52, 0x0f, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, + 0x65, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x04, 0x66, 0x71, 0x64, 0x6e, 0x12, 0x2a, 0x0a, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, + 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, - 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, - 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x30, 0x0a, 0x13, 0x72, 0x6f, 0x73, - 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x76, 0x65, - 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x13, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, - 0x73, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x76, 0x65, 0x22, 0x53, 0x0a, 0x0b, 0x53, + 0x64, 0x12, 0x30, 0x0a, 0x13, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, 0x65, + 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x76, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x13, + 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, + 0x69, 0x76, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x18, 0x07, 0x20, + 0x03, 0x28, 0x09, 0x52, 0x06, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x22, 0x53, 0x0a, 0x0b, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x55, 0x52, 0x4c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x55, 0x52, 0x4c, 0x12, 0x1c, 0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, @@ -1449,50 +1547,61 @@ var file_daemon_proto_rawDesc = []byte{ 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x55, 0x52, 0x49, 0x12, 0x1c, 0x0a, 0x09, 0x61, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x61, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, 0x9b, 0x02, - 0x0a, 0x0a, 0x46, 0x75, 0x6c, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x41, 0x0a, 0x0f, - 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, - 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x0f, - 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, - 0x35, 0x0a, 0x0b, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x65, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x69, - 0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x0b, 0x73, 0x69, 0x67, 0x6e, 0x61, - 0x6c, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x3e, 0x0a, 0x0e, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x50, - 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, - 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x65, 0x65, - 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x0e, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x65, 0x65, - 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x27, 0x0a, 0x05, 0x70, 0x65, 0x65, 0x72, 0x73, 0x18, - 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x50, - 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x05, 0x70, 0x65, 0x65, 0x72, 0x73, 0x12, - 0x2a, 0x0a, 0x06, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, - 0x12, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x52, 0x65, 0x6c, 0x61, 0x79, 0x53, 0x74, - 0x61, 0x74, 0x65, 0x52, 0x06, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x73, 0x32, 0xf7, 0x02, 0x0a, 0x0d, - 0x44, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x36, 0x0a, - 0x05, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x14, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, - 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x15, 0x2e, 0x64, - 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x4b, 0x0a, 0x0c, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, - 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x57, - 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x57, 0x61, 0x69, 0x74, - 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x22, 0x00, 0x12, 0x2d, 0x0a, 0x02, 0x55, 0x70, 0x12, 0x11, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, - 0x6e, 0x2e, 0x55, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x64, 0x61, - 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x55, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, - 0x00, 0x12, 0x39, 0x0a, 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x15, 0x2e, 0x64, 0x61, - 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x74, - 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x33, 0x0a, 0x04, - 0x44, 0x6f, 0x77, 0x6e, 0x12, 0x13, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x6f, - 0x77, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x64, 0x61, 0x65, 0x6d, - 0x6f, 0x6e, 0x2e, 0x44, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, - 0x00, 0x12, 0x42, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x18, - 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, - 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, - 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x08, 0x5a, 0x06, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, - 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, 0x72, 0x0a, + 0x0c, 0x4e, 0x53, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x18, 0x0a, + 0x07, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, + 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, + 0x6e, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, + 0x73, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x65, + 0x72, 0x72, 0x6f, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, + 0x72, 0x22, 0xd2, 0x02, 0x0a, 0x0a, 0x46, 0x75, 0x6c, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, + 0x12, 0x41, 0x0a, 0x0f, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, + 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x64, 0x61, 0x65, 0x6d, + 0x6f, 0x6e, 0x2e, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, + 0x74, 0x65, 0x52, 0x0f, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, + 0x61, 0x74, 0x65, 0x12, 0x35, 0x0a, 0x0b, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, + 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, + 0x6e, 0x2e, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x0b, 0x73, + 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x3e, 0x0a, 0x0e, 0x6c, 0x6f, + 0x63, 0x61, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x63, 0x61, + 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x0e, 0x6c, 0x6f, 0x63, 0x61, + 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x27, 0x0a, 0x05, 0x70, 0x65, + 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x64, 0x61, 0x65, 0x6d, + 0x6f, 0x6e, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x05, 0x70, 0x65, + 0x65, 0x72, 0x73, 0x12, 0x2a, 0x0a, 0x06, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x73, 0x18, 0x05, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x52, 0x65, 0x6c, + 0x61, 0x79, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x06, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x73, 0x12, + 0x35, 0x0a, 0x0b, 0x64, 0x6e, 0x73, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x18, 0x06, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4e, 0x53, + 0x47, 0x72, 0x6f, 0x75, 0x70, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x0a, 0x64, 0x6e, 0x73, 0x53, + 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x32, 0xf7, 0x02, 0x0a, 0x0d, 0x44, 0x61, 0x65, 0x6d, 0x6f, + 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x36, 0x0a, 0x05, 0x4c, 0x6f, 0x67, 0x69, + 0x6e, 0x12, 0x14, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x15, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, + 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, + 0x12, 0x4b, 0x0a, 0x0c, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, + 0x12, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, + 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, + 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, + 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x2d, 0x0a, + 0x02, 0x55, 0x70, 0x12, 0x11, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x55, 0x70, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, + 0x55, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x39, 0x0a, 0x06, + 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x15, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, + 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, + 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x33, 0x0a, 0x04, 0x44, 0x6f, 0x77, 0x6e, 0x12, + 0x13, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x6f, + 0x77, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x42, 0x0a, 0x09, + 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x18, 0x2e, 0x64, 0x61, 0x65, 0x6d, + 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, + 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, + 0x42, 0x08, 0x5a, 0x06, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x33, } var ( @@ -1507,7 +1616,7 @@ func file_daemon_proto_rawDescGZIP() []byte { return file_daemon_proto_rawDescData } -var file_daemon_proto_msgTypes = make([]protoimpl.MessageInfo, 18) +var file_daemon_proto_msgTypes = make([]protoimpl.MessageInfo, 19) var file_daemon_proto_goTypes = []interface{}{ (*LoginRequest)(nil), // 0: daemon.LoginRequest (*LoginResponse)(nil), // 1: daemon.LoginResponse @@ -1526,35 +1635,37 @@ var file_daemon_proto_goTypes = []interface{}{ (*SignalState)(nil), // 14: daemon.SignalState (*ManagementState)(nil), // 15: daemon.ManagementState (*RelayState)(nil), // 16: daemon.RelayState - (*FullStatus)(nil), // 17: daemon.FullStatus - (*timestamp.Timestamp)(nil), // 18: google.protobuf.Timestamp + (*NSGroupState)(nil), // 17: daemon.NSGroupState + (*FullStatus)(nil), // 18: daemon.FullStatus + (*timestamp.Timestamp)(nil), // 19: google.protobuf.Timestamp } var file_daemon_proto_depIdxs = []int32{ - 17, // 0: daemon.StatusResponse.fullStatus:type_name -> daemon.FullStatus - 18, // 1: daemon.PeerState.connStatusUpdate:type_name -> google.protobuf.Timestamp - 18, // 2: daemon.PeerState.lastWireguardHandshake:type_name -> google.protobuf.Timestamp + 18, // 0: daemon.StatusResponse.fullStatus:type_name -> daemon.FullStatus + 19, // 1: daemon.PeerState.connStatusUpdate:type_name -> google.protobuf.Timestamp + 19, // 2: daemon.PeerState.lastWireguardHandshake:type_name -> google.protobuf.Timestamp 15, // 3: daemon.FullStatus.managementState:type_name -> daemon.ManagementState 14, // 4: daemon.FullStatus.signalState:type_name -> daemon.SignalState 13, // 5: daemon.FullStatus.localPeerState:type_name -> daemon.LocalPeerState 12, // 6: daemon.FullStatus.peers:type_name -> daemon.PeerState 16, // 7: daemon.FullStatus.relays:type_name -> daemon.RelayState - 0, // 8: daemon.DaemonService.Login:input_type -> daemon.LoginRequest - 2, // 9: daemon.DaemonService.WaitSSOLogin:input_type -> daemon.WaitSSOLoginRequest - 4, // 10: daemon.DaemonService.Up:input_type -> daemon.UpRequest - 6, // 11: daemon.DaemonService.Status:input_type -> daemon.StatusRequest - 8, // 12: daemon.DaemonService.Down:input_type -> daemon.DownRequest - 10, // 13: daemon.DaemonService.GetConfig:input_type -> daemon.GetConfigRequest - 1, // 14: daemon.DaemonService.Login:output_type -> daemon.LoginResponse - 3, // 15: daemon.DaemonService.WaitSSOLogin:output_type -> daemon.WaitSSOLoginResponse - 5, // 16: daemon.DaemonService.Up:output_type -> daemon.UpResponse - 7, // 17: daemon.DaemonService.Status:output_type -> daemon.StatusResponse - 9, // 18: daemon.DaemonService.Down:output_type -> daemon.DownResponse - 11, // 19: daemon.DaemonService.GetConfig:output_type -> daemon.GetConfigResponse - 14, // [14:20] is the sub-list for method output_type - 8, // [8:14] is the sub-list for method input_type - 8, // [8:8] is the sub-list for extension type_name - 8, // [8:8] is the sub-list for extension extendee - 0, // [0:8] is the sub-list for field type_name + 17, // 8: daemon.FullStatus.dns_servers:type_name -> daemon.NSGroupState + 0, // 9: daemon.DaemonService.Login:input_type -> daemon.LoginRequest + 2, // 10: daemon.DaemonService.WaitSSOLogin:input_type -> daemon.WaitSSOLoginRequest + 4, // 11: daemon.DaemonService.Up:input_type -> daemon.UpRequest + 6, // 12: daemon.DaemonService.Status:input_type -> daemon.StatusRequest + 8, // 13: daemon.DaemonService.Down:input_type -> daemon.DownRequest + 10, // 14: daemon.DaemonService.GetConfig:input_type -> daemon.GetConfigRequest + 1, // 15: daemon.DaemonService.Login:output_type -> daemon.LoginResponse + 3, // 16: daemon.DaemonService.WaitSSOLogin:output_type -> daemon.WaitSSOLoginResponse + 5, // 17: daemon.DaemonService.Up:output_type -> daemon.UpResponse + 7, // 18: daemon.DaemonService.Status:output_type -> daemon.StatusResponse + 9, // 19: daemon.DaemonService.Down:output_type -> daemon.DownResponse + 11, // 20: daemon.DaemonService.GetConfig:output_type -> daemon.GetConfigResponse + 15, // [15:21] is the sub-list for method output_type + 9, // [9:15] is the sub-list for method input_type + 9, // [9:9] is the sub-list for extension type_name + 9, // [9:9] is the sub-list for extension extendee + 0, // [0:9] is the sub-list for field type_name } func init() { file_daemon_proto_init() } @@ -1768,6 +1879,18 @@ func file_daemon_proto_init() { } } file_daemon_proto_msgTypes[17].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*NSGroupState); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_daemon_proto_msgTypes[18].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*FullStatus); i { case 0: return &v.state @@ -1787,7 +1910,7 @@ func file_daemon_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_daemon_proto_rawDesc, NumEnums: 0, - NumMessages: 18, + NumMessages: 19, NumExtensions: 0, NumServices: 1, }, diff --git a/client/proto/daemon.proto b/client/proto/daemon.proto index 2858ba2e0..bdb1cb83e 100644 --- a/client/proto/daemon.proto +++ b/client/proto/daemon.proto @@ -141,6 +141,7 @@ message PeerState { int64 bytesRx = 13; int64 bytesTx = 14; bool rosenpassEnabled = 15; + repeated string routes = 16; } // LocalPeerState contains the latest state of the local peer @@ -151,6 +152,7 @@ message LocalPeerState { string fqdn = 4; bool rosenpassEnabled = 5; bool rosenpassPermissive = 6; + repeated string routes = 7; } // SignalState contains the latest state of a signal connection @@ -174,6 +176,13 @@ message RelayState { string error = 3; } +message NSGroupState { + repeated string servers = 1; + repeated string domains = 2; + bool enabled = 3; + string error = 4; +} + // FullStatus contains the full state held by the Status instance message FullStatus { ManagementState managementState = 1; @@ -181,4 +190,5 @@ message FullStatus { LocalPeerState localPeerState = 3; repeated PeerState peers = 4; repeated RelayState relays = 5; + repeated NSGroupState dns_servers = 6; } \ No newline at end of file diff --git a/client/server/server.go b/client/server/server.go index fc1e4cc26..5f1bf0100 100644 --- a/client/server/server.go +++ b/client/server/server.go @@ -3,11 +3,16 @@ package server import ( "context" "fmt" + "os" "os/exec" "runtime" + "strconv" "sync" "time" + "github.com/cenkalti/backoff/v4" + "golang.org/x/exp/maps" + "github.com/netbirdio/netbird/client/internal/auth" "github.com/netbirdio/netbird/client/system" @@ -23,7 +28,17 @@ import ( "github.com/netbirdio/netbird/version" ) -const probeThreshold = time.Second * 5 +const ( + probeThreshold = time.Second * 5 + retryInitialIntervalVar = "NB_CONN_RETRY_INTERVAL_TIME" + maxRetryIntervalVar = "NB_CONN_MAX_RETRY_INTERVAL_TIME" + maxRetryTimeVar = "NB_CONN_MAX_RETRY_TIME_TIME" + retryMultiplierVar = "NB_CONN_RETRY_MULTIPLIER" + defaultInitialRetryTime = 14 * 24 * time.Hour + defaultMaxRetryInterval = 60 * time.Minute + defaultMaxRetryTime = 14 * 24 * time.Hour + defaultRetryMultiplier = 1.7 +) // Server for service control. type Server struct { @@ -125,16 +140,110 @@ func (s *Server) Start() error { } if !config.DisableAutoConnect { - go func() { - if err := internal.RunClientWithProbes(ctx, config, s.statusRecorder, s.mgmProbe, s.signalProbe, s.relayProbe, s.wgProbe); err != nil { - log.Errorf("init connections: %v", err) - } - }() + go s.connectWithRetryRuns(ctx, config, s.statusRecorder, s.mgmProbe, s.signalProbe, s.relayProbe, s.wgProbe) } return nil } +// connectWithRetryRuns runs the client connection with a backoff strategy where we retry the operation as additional +// mechanism to keep the client connected even when the connection is lost. +// we cancel retry if the client receive a stop or down command, or if disable auto connect is configured. +func (s *Server) connectWithRetryRuns(ctx context.Context, config *internal.Config, statusRecorder *peer.Status, + mgmProbe *internal.Probe, signalProbe *internal.Probe, relayProbe *internal.Probe, wgProbe *internal.Probe) { + backOff := getConnectWithBackoff(ctx) + retryStarted := false + + go func() { + t := time.NewTicker(24 * time.Hour) + for { + select { + case <-ctx.Done(): + t.Stop() + return + case <-t.C: + if retryStarted { + + mgmtState := statusRecorder.GetManagementState() + signalState := statusRecorder.GetSignalState() + if mgmtState.Connected && signalState.Connected { + log.Tracef("resetting status") + retryStarted = false + } else { + log.Tracef("not resetting status: mgmt: %v, signal: %v", mgmtState.Connected, signalState.Connected) + } + } + } + } + }() + + runOperation := func() error { + log.Tracef("running client connection") + err := internal.RunClientWithProbes(ctx, config, statusRecorder, mgmProbe, signalProbe, relayProbe, wgProbe) + if err != nil { + log.Debugf("run client connection exited with error: %v. Will retry in the background", err) + } + + if config.DisableAutoConnect { + return backoff.Permanent(err) + } + + if !retryStarted { + retryStarted = true + backOff.Reset() + } + + log.Tracef("client connection exited") + return fmt.Errorf("client connection exited") + } + + err := backoff.Retry(runOperation, backOff) + if s, ok := gstatus.FromError(err); ok && s.Code() != codes.Canceled { + log.Errorf("received an error when trying to connect: %v", err) + } else { + log.Tracef("retry canceled") + } +} + +// getConnectWithBackoff returns a backoff with exponential backoff strategy for connection retries +func getConnectWithBackoff(ctx context.Context) backoff.BackOff { + initialInterval := parseEnvDuration(retryInitialIntervalVar, defaultInitialRetryTime) + maxInterval := parseEnvDuration(maxRetryIntervalVar, defaultMaxRetryInterval) + maxElapsedTime := parseEnvDuration(maxRetryTimeVar, defaultMaxRetryTime) + multiplier := defaultRetryMultiplier + + if envValue := os.Getenv(retryMultiplierVar); envValue != "" { + // parse the multiplier from the environment variable string value to float64 + value, err := strconv.ParseFloat(envValue, 64) + if err != nil { + log.Warnf("unable to parse environment variable %s: %s. using default: %f", retryMultiplierVar, envValue, multiplier) + } else { + multiplier = value + } + } + + return backoff.WithContext(&backoff.ExponentialBackOff{ + InitialInterval: initialInterval, + RandomizationFactor: 1, + Multiplier: multiplier, + MaxInterval: maxInterval, + MaxElapsedTime: maxElapsedTime, // 14 days + Stop: backoff.Stop, + Clock: backoff.SystemClock, + }, ctx) +} + +// parseEnvDuration parses the environment variable and returns the duration +func parseEnvDuration(envVar string, defaultDuration time.Duration) time.Duration { + if envValue := os.Getenv(envVar); envValue != "" { + if duration, err := time.ParseDuration(envValue); err == nil { + return duration + } + log.Warnf("unable to parse environment variable %s: %s. using default: %s", envVar, envValue, defaultDuration) + } + return defaultDuration +} + // loginAttempt attempts to login using the provided information. it returns a status in case something fails func (s *Server) loginAttempt(ctx context.Context, setupKey, jwtToken string) (internal.StatusType, error) { var status internal.StatusType @@ -445,12 +554,7 @@ func (s *Server) Up(callerCtx context.Context, _ *proto.UpRequest) (*proto.UpRes s.statusRecorder.UpdateManagementAddress(s.config.ManagementURL.String()) s.statusRecorder.UpdateRosenpass(s.config.RosenpassEnabled, s.config.RosenpassPermissive) - go func() { - if err := internal.RunClientWithProbes(ctx, s.config, s.statusRecorder, s.mgmProbe, s.signalProbe, s.relayProbe, s.wgProbe); err != nil { - log.Errorf("run client connection: %v", err) - return - } - }() + go s.connectWithRetryRuns(ctx, s.config, s.statusRecorder, s.mgmProbe, s.signalProbe, s.relayProbe, s.wgProbe) return &proto.UpResponse{}, nil } @@ -567,7 +671,6 @@ func toProtoFullStatus(fullStatus peer.FullStatus) *proto.FullStatus { SignalState: &proto.SignalState{}, LocalPeerState: &proto.LocalPeerState{}, Peers: []*proto.PeerState{}, - Relays: []*proto.RelayState{}, } pbFullStatus.ManagementState.URL = fullStatus.ManagementState.URL @@ -588,6 +691,7 @@ func toProtoFullStatus(fullStatus peer.FullStatus) *proto.FullStatus { pbFullStatus.LocalPeerState.Fqdn = fullStatus.LocalPeerState.FQDN pbFullStatus.LocalPeerState.RosenpassPermissive = fullStatus.RosenpassState.Permissive pbFullStatus.LocalPeerState.RosenpassEnabled = fullStatus.RosenpassState.Enabled + pbFullStatus.LocalPeerState.Routes = maps.Keys(fullStatus.LocalPeerState.Routes) for _, peerState := range fullStatus.Peers { pbPeerState := &proto.PeerState{ @@ -606,6 +710,7 @@ func toProtoFullStatus(fullStatus peer.FullStatus) *proto.FullStatus { BytesRx: peerState.BytesRx, BytesTx: peerState.BytesTx, RosenpassEnabled: peerState.RosenpassEnabled, + Routes: maps.Keys(peerState.Routes), } pbFullStatus.Peers = append(pbFullStatus.Peers, pbPeerState) } @@ -621,6 +726,20 @@ func toProtoFullStatus(fullStatus peer.FullStatus) *proto.FullStatus { pbFullStatus.Relays = append(pbFullStatus.Relays, pbRelayState) } + for _, dnsState := range fullStatus.NSGroupStates { + var err string + if dnsState.Error != nil { + err = dnsState.Error.Error() + } + pbDnsState := &proto.NSGroupState{ + Servers: dnsState.Servers, + Domains: dnsState.Domains, + Enabled: dnsState.Enabled, + Error: err, + } + pbFullStatus.DnsServers = append(pbFullStatus.DnsServers, pbDnsState) + } + return &pbFullStatus } diff --git a/client/server/server_test.go b/client/server/server_test.go new file mode 100644 index 000000000..7f8310c90 --- /dev/null +++ b/client/server/server_test.go @@ -0,0 +1,157 @@ +package server + +import ( + "context" + "net" + "testing" + "time" + + log "github.com/sirupsen/logrus" + "google.golang.org/grpc" + "google.golang.org/grpc/keepalive" + + "github.com/netbirdio/netbird/client/internal" + "github.com/netbirdio/netbird/client/internal/peer" + mgmtProto "github.com/netbirdio/netbird/management/proto" + "github.com/netbirdio/netbird/management/server" + "github.com/netbirdio/netbird/management/server/activity" + "github.com/netbirdio/netbird/signal/proto" + signalServer "github.com/netbirdio/netbird/signal/server" +) + +var ( + kaep = keepalive.EnforcementPolicy{ + MinTime: 15 * time.Second, + PermitWithoutStream: true, + } + + kasp = keepalive.ServerParameters{ + MaxConnectionIdle: 15 * time.Second, + MaxConnectionAgeGrace: 5 * time.Second, + Time: 5 * time.Second, + Timeout: 2 * time.Second, + } +) + +// TestConnectWithRetryRuns checks that the connectWithRetry function runs and runs the retries according to the times specified via environment variables +// we will use a management server started via to simulate the server and capture the number of retries +func TestConnectWithRetryRuns(t *testing.T) { + // start the signal server + _, signalAddr, err := startSignal() + if err != nil { + t.Fatalf("failed to start signal server: %v", err) + } + + counter := 0 + // start the management server + _, mgmtAddr, err := startManagement(t, signalAddr, &counter) + if err != nil { + t.Fatalf("failed to start management server: %v", err) + } + + ctx := internal.CtxInitState(context.Background()) + + ctx, cancel := context.WithDeadline(ctx, time.Now().Add(30*time.Second)) + defer cancel() + // create new server + s := New(ctx, t.TempDir()+"/config.json", "debug") + s.latestConfigInput.ManagementURL = "http://" + mgmtAddr + config, err := internal.UpdateOrCreateConfig(s.latestConfigInput) + if err != nil { + t.Fatalf("failed to create config: %v", err) + } + s.config = config + + s.statusRecorder = peer.NewRecorder(config.ManagementURL.String()) + t.Setenv(retryInitialIntervalVar, "1s") + t.Setenv(maxRetryIntervalVar, "2s") + t.Setenv(maxRetryTimeVar, "5s") + t.Setenv(retryMultiplierVar, "1") + + s.connectWithRetryRuns(ctx, config, s.statusRecorder, s.mgmProbe, s.signalProbe, s.relayProbe, s.wgProbe) + if counter < 3 { + t.Fatalf("expected counter > 2, got %d", counter) + } +} + +type mockServer struct { + mgmtProto.ManagementServiceServer + counter *int +} + +func (m *mockServer) Login(ctx context.Context, req *mgmtProto.EncryptedMessage) (*mgmtProto.EncryptedMessage, error) { + *m.counter++ + return m.ManagementServiceServer.Login(ctx, req) +} + +func startManagement(t *testing.T, signalAddr string, counter *int) (*grpc.Server, string, error) { + t.Helper() + dataDir := t.TempDir() + + config := &server.Config{ + Stuns: []*server.Host{}, + TURNConfig: &server.TURNConfig{}, + Signal: &server.Host{ + Proto: "http", + URI: signalAddr, + }, + Datadir: dataDir, + HttpConfig: nil, + } + + lis, err := net.Listen("tcp", "localhost:0") + if err != nil { + return nil, "", err + } + s := grpc.NewServer(grpc.KeepaliveEnforcementPolicy(kaep), grpc.KeepaliveParams(kasp)) + store, err := server.NewStoreFromJson(config.Datadir, nil) + if err != nil { + return nil, "", err + } + + peersUpdateManager := server.NewPeersUpdateManager(nil) + eventStore := &activity.InMemoryEventStore{} + if err != nil { + return nil, "", err + } + accountManager, err := server.BuildManager(store, peersUpdateManager, nil, "", "netbird.selfhosted", eventStore, nil, false) + if err != nil { + return nil, "", err + } + turnManager := server.NewTimeBasedAuthSecretsManager(peersUpdateManager, config.TURNConfig) + mgmtServer, err := server.NewServer(config, accountManager, peersUpdateManager, turnManager, nil, nil) + if err != nil { + return nil, "", err + } + mock := &mockServer{ + ManagementServiceServer: mgmtServer, + counter: counter, + } + mgmtProto.RegisterManagementServiceServer(s, mock) + go func() { + if err = s.Serve(lis); err != nil { + log.Fatalf("failed to serve: %v", err) + } + }() + + return s, lis.Addr().String(), nil +} + +func startSignal() (*grpc.Server, string, error) { + s := grpc.NewServer(grpc.KeepaliveEnforcementPolicy(kaep), grpc.KeepaliveParams(kasp)) + + lis, err := net.Listen("tcp", "localhost:0") + if err != nil { + log.Fatalf("failed to listen: %v", err) + } + + proto.RegisterSignalExchangeServer(s, signalServer.NewServer()) + + go func() { + if err = s.Serve(lis); err != nil { + log.Fatalf("failed to serve: %v", err) + } + }() + + return s, lis.Addr().String(), nil +} diff --git a/go.mod b/go.mod index d435e4eb8..6aba599f8 100644 --- a/go.mod +++ b/go.mod @@ -48,6 +48,7 @@ require ( github.com/google/gopacket v1.1.19 github.com/google/nftables v0.0.0-20220808154552-2eca00135732 github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.0.2-0.20240212192251-757544f21357 + github.com/hashicorp/go-multierror v1.1.0 github.com/hashicorp/go-secure-stdlib/base62 v0.1.2 github.com/hashicorp/go-version v1.6.0 github.com/libp2p/go-netroute v0.2.0 @@ -123,6 +124,7 @@ require ( github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect github.com/googleapis/gax-go/v2 v2.10.0 // indirect github.com/gopacket/gopacket v1.1.1 // indirect + github.com/hashicorp/errwrap v1.0.0 // indirect github.com/hashicorp/go-uuid v1.0.2 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect diff --git a/go.sum b/go.sum index cc7a52ed6..ca10cd553 100644 --- a/go.sum +++ b/go.sum @@ -289,6 +289,10 @@ github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB7 github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.0.2-0.20240212192251-757544f21357 h1:Fkzd8ktnpOR9h47SXHe2AYPwelXLH2GjGsjlAloiWfo= github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.0.2-0.20240212192251-757544f21357/go.mod h1:w9Y7gY31krpLmrVU5ZPG9H7l9fZuRu5/3R3S3FMtVQ4= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.0 h1:B9UzwGQJehnUY1yNrnwREHc3fGbC2xefo8g4TbElacI= +github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= github.com/hashicorp/go-secure-stdlib/base62 v0.1.2 h1:ET4pqyjiGmY09R5y+rSd70J2w45CtbWDNvGqWp/R3Ng= github.com/hashicorp/go-secure-stdlib/base62 v0.1.2/go.mod h1:EdWO6czbmthiwZ3/PUsDV+UD1D5IRU4ActiaWGwt0Yw= github.com/hashicorp/go-uuid v1.0.2 h1:cfejS+Tpcp13yd5nYHWDI6qVCny6wyX2Mt5SGur2IGE= diff --git a/management/client/client_test.go b/management/client/client_test.go index 0a57fda72..f30ae0cfd 100644 --- a/management/client/client_test.go +++ b/management/client/client_test.go @@ -60,7 +60,7 @@ func startManagement(t *testing.T) (*grpc.Server, net.Listener) { peersUpdateManager := mgmt.NewPeersUpdateManager(nil) eventStore := &activity.InMemoryEventStore{} - accountManager, err := mgmt.BuildManager(store, peersUpdateManager, nil, "", "", eventStore, nil, false) + accountManager, err := mgmt.BuildManager(store, peersUpdateManager, nil, "", "netbird.selfhosted", eventStore, nil, false) if err != nil { t.Fatal(err) } diff --git a/management/server/account.go b/management/server/account.go index 9450c95b4..8b326d93a 100644 --- a/management/server/account.go +++ b/management/server/account.go @@ -40,9 +40,6 @@ const ( PublicCategory = "public" PrivateCategory = "private" UnknownCategory = "unknown" - GroupIssuedAPI = "api" - GroupIssuedJWT = "jwt" - GroupIssuedIntegration = "integration" CacheExpirationMax = 7 * 24 * 3600 * time.Second // 7 days CacheExpirationMin = 3 * 24 * 3600 * time.Second // 3 days DefaultPeerLoginExpiration = 24 * time.Hour @@ -227,9 +224,6 @@ type Account struct { PostureChecks []*posture.Checks `gorm:"foreignKey:AccountID;references:id"` // Settings is a dictionary of Account settings Settings *Settings `gorm:"embedded;embeddedPrefix:settings_"` - // deprecated on store and api level - Rules map[string]*Rule `json:"-" gorm:"-"` - RulesG []Rule `json:"-" gorm:"-"` } type UserInfo struct { @@ -559,6 +553,16 @@ func (a *Account) FindUser(userID string) (*User, error) { return user, nil } +// FindGroupByName looks for a given group in the Account by name or returns error if the group wasn't found. +func (a *Account) FindGroupByName(groupName string) (*Group, error) { + for _, group := range a.Groups { + if group.Name == groupName { + return group, nil + } + } + return nil, status.Errorf(status.NotFound, "group %s not found", groupName) +} + // FindSetupKey looks for a given SetupKey in the Account or returns error if it wasn't found. func (a *Account) FindSetupKey(setupKey string) (*SetupKey, error) { key := a.SetupKeys[setupKey] @@ -1361,16 +1365,21 @@ func (am *DefaultAccountManager) removeUserFromCache(accountID, userID string) e func (am *DefaultAccountManager) updateAccountDomainAttributes(account *Account, claims jwtclaims.AuthorizationClaims, primaryDomain bool, ) error { - account.IsDomainPrimaryAccount = primaryDomain - lowerDomain := strings.ToLower(claims.Domain) - userObj := account.Users[claims.UserId] - if account.Domain != lowerDomain && userObj.Role == UserRoleAdmin { - account.Domain = lowerDomain - } - // prevent updating category for different domain until admin logs in - if account.Domain == lowerDomain { - account.DomainCategory = claims.DomainCategory + if claims.Domain != "" { + account.IsDomainPrimaryAccount = primaryDomain + + lowerDomain := strings.ToLower(claims.Domain) + userObj := account.Users[claims.UserId] + if account.Domain != lowerDomain && userObj.Role == UserRoleAdmin { + account.Domain = lowerDomain + } + // prevent updating category for different domain until admin logs in + if account.Domain == lowerDomain { + account.DomainCategory = claims.DomainCategory + } + } else { + log.Errorf("claims don't contain a valid domain, skipping domain attributes update. Received claims: %v", claims) } err := am.Store.SaveAccount(account) diff --git a/management/server/file_store_test.go b/management/server/file_store_test.go index e0868fb49..d8575a3bf 100644 --- a/management/server/file_store_test.go +++ b/management/server/file_store_test.go @@ -258,18 +258,6 @@ func TestStore(t *testing.T) { t.Errorf("failed to restore a FileStore file - missing Group all") } - if restoredAccount.Rules["all"] == nil { - t.Errorf("failed to restore a FileStore file - missing Rule all") - return - } - - if restoredAccount.Rules["dmz"] == nil { - t.Errorf("failed to restore a FileStore file - missing Rule dmz") - return - } - assert.Equal(t, account.Rules["all"], restoredAccount.Rules["all"], "failed to restore a FileStore file - missing Rule all") - assert.Equal(t, account.Rules["dmz"], restoredAccount.Rules["dmz"], "failed to restore a FileStore file - missing Rule dmz") - if len(restoredAccount.Policies) != 2 { t.Errorf("failed to restore a FileStore file - missing Policies") return @@ -411,7 +399,6 @@ func TestFileStore_GetAccount(t *testing.T) { assert.Len(t, account.Peers, len(expected.Peers)) assert.Len(t, account.Users, len(expected.Users)) assert.Len(t, account.SetupKeys, len(expected.SetupKeys)) - assert.Len(t, account.Rules, len(expected.Rules)) assert.Len(t, account.Routes, len(expected.Routes)) assert.Len(t, account.NameServerGroups, len(expected.NameServerGroups)) } diff --git a/management/server/group.go b/management/server/group.go index be8d3fb0e..43d48e622 100644 --- a/management/server/group.go +++ b/management/server/group.go @@ -3,6 +3,7 @@ package server import ( "fmt" + "github.com/rs/xid" log "github.com/sirupsen/logrus" "github.com/netbirdio/netbird/management/server/activity" @@ -18,6 +19,12 @@ func (e *GroupLinkError) Error() string { return fmt.Sprintf("group has been linked to %s: %s", e.Resource, e.Name) } +const ( + GroupIssuedAPI = "api" + GroupIssuedJWT = "jwt" + GroupIssuedIntegration = "integration" +) + // Group of the peers for ACL type Group struct { // ID of the group @@ -29,7 +36,7 @@ type Group struct { // Name visible in the UI Name string - // Issued of the group + // Issued defines how this group was created (enum of "api", "integration" or "jwt") Issued string // Peers list of the group @@ -116,6 +123,29 @@ func (am *DefaultAccountManager) SaveGroup(accountID, userID string, newGroup *G return err } + if newGroup.ID == "" && newGroup.Issued != GroupIssuedAPI { + return status.Errorf(status.InvalidArgument, "%s group without ID set", newGroup.Issued) + } + + if newGroup.ID == "" && newGroup.Issued == GroupIssuedAPI { + + existingGroup, err := account.FindGroupByName(newGroup.Name) + if err != nil { + s, ok := status.FromError(err) + if !ok || s.ErrorType != status.NotFound { + return err + } + } + + // avoid duplicate groups only for the API issued groups. Integration or JWT groups can be duplicated as they are + // coming from the IdP that we don't have control of. + if existingGroup != nil { + return status.Errorf(status.AlreadyExists, "group with name %s already exists", newGroup.Name) + } + + newGroup.ID = xid.New().String() + } + for _, peerID := range newGroup.Peers { if account.Peers[peerID] == nil { return status.Errorf(status.InvalidArgument, "peer with ID \"%s\" not found", peerID) diff --git a/management/server/group_test.go b/management/server/group_test.go index e2051a656..3a2195c88 100644 --- a/management/server/group_test.go +++ b/management/server/group_test.go @@ -13,6 +13,41 @@ const ( groupAdminUserID = "testingAdminUser" ) +func TestDefaultAccountManager_CreateGroup(t *testing.T) { + am, err := createManager(t) + if err != nil { + t.Error("failed to create account manager") + } + + account, err := initTestGroupAccount(am) + if err != nil { + t.Error("failed to init testing account") + } + for _, group := range account.Groups { + group.Issued = GroupIssuedIntegration + err = am.SaveGroup(account.Id, groupAdminUserID, group) + if err != nil { + t.Errorf("should allow to create %s groups", GroupIssuedIntegration) + } + } + + for _, group := range account.Groups { + group.Issued = GroupIssuedJWT + err = am.SaveGroup(account.Id, groupAdminUserID, group) + if err != nil { + t.Errorf("should allow to create %s groups", GroupIssuedJWT) + } + } + for _, group := range account.Groups { + group.Issued = GroupIssuedAPI + group.ID = "" + err = am.SaveGroup(account.Id, groupAdminUserID, group) + if err == nil { + t.Errorf("should not create api group with the same name, %s", group.Name) + } + } +} + func TestDefaultAccountManager_DeleteGroup(t *testing.T) { am, err := createManager(t) if err != nil { @@ -137,7 +172,7 @@ func initTestGroupAccount(am *DefaultAccountManager) (*Account, error) { groupForIntegration := &Group{ ID: "grp-for-integration", AccountID: "account-id", - Name: "Group for users", + Name: "Group for users integration", Issued: GroupIssuedIntegration, Peers: make([]string, 0), } diff --git a/management/server/http/api/openapi.yml b/management/server/http/api/openapi.yml index de49d3073..6c22e52bf 100644 --- a/management/server/http/api/openapi.yml +++ b/management/server/http/api/openapi.yml @@ -17,8 +17,6 @@ tags: description: Interact with and view information about setup keys. - name: Groups description: Interact with and view information about groups. - - name: Rules - description: Interact with and view information about rules. - name: Policies description: Interact with and view information about policies. - name: Posture Checks @@ -587,7 +585,10 @@ components: type: integer example: 2 issued: - description: How group was issued by API or from JWT token + description: How the group was issued (api, integration, jwt) + type: string + enum: ["api", "integration", "jwt"] + example: api type: string example: api required: @@ -621,73 +622,6 @@ components: $ref: '#/components/schemas/PeerMinimum' required: - peers - RuleMinimum: - type: object - properties: - name: - description: Rule name identifier - type: string - example: Default - description: - description: Rule friendly description - type: string - example: This is a default rule that allows connections between all the resources - disabled: - description: Rules status - type: boolean - example: false - flow: - description: Rule flow, currently, only "bidirect" for bi-directional traffic is accepted - type: string - example: bidirect - required: - - name - - description - - disabled - - flow - RuleRequest: - allOf: - - $ref: '#/components/schemas/RuleMinimum' - - type: object - properties: - sources: - type: array - description: List of source group IDs - items: - type: string - example: "ch8i4ug6lnn4g9hqv7m1" - destinations: - type: array - description: List of destination group IDs - items: - type: string - example: "ch8i4ug6lnn4g9hqv7m0" - Rule: - allOf: - - type: object - properties: - id: - description: Rule ID - type: string - example: ch8i4ug6lnn4g9hqv7mg - required: - - id - - $ref: '#/components/schemas/RuleMinimum' - - type: object - properties: - sources: - description: Rule source group IDs - type: array - items: - $ref: '#/components/schemas/GroupMinimum' - destinations: - description: Rule destination group IDs - type: array - items: - $ref: '#/components/schemas/GroupMinimum' - required: - - sources - - destinations PolicyRuleMinimum: type: object properties: @@ -1339,7 +1273,7 @@ paths: /api/accounts/{accountId}: delete: summary: Delete an Account - description: Deletes an account and all its resources. Only administrators and account owners can delete accounts. + description: Deletes an account and all its resources. Only account owners can delete accounts. tags: [ Accounts ] security: - BearerAuth: [ ] @@ -2059,147 +1993,6 @@ paths: "$ref": "#/components/responses/forbidden" '500': "$ref": "#/components/responses/internal_error" - /api/rules: - get: - summary: List all Rules - description: Returns a list of all rules. This will be deprecated in favour of `/api/policies`. - tags: [ Rules ] - deprecated: true - security: - - BearerAuth: [ ] - - TokenAuth: [ ] - responses: - '200': - description: A JSON Array of Rules - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/Rule' - '400': - "$ref": "#/components/responses/bad_request" - '401': - "$ref": "#/components/responses/requires_authentication" - '403': - "$ref": "#/components/responses/forbidden" - '500': - "$ref": "#/components/responses/internal_error" - post: - summary: Create a Rule - description: Creates a rule. This will be deprecated in favour of `/api/policies`. - deprecated: true - tags: [ Rules ] - security: - - BearerAuth: [ ] - - TokenAuth: [ ] - requestBody: - description: New Rule request - content: - 'application/json': - schema: - $ref: '#/components/schemas/RuleRequest' - responses: - '200': - description: A Rule Object - content: - application/json: - schema: - $ref: '#/components/schemas/Rule' - /api/rules/{ruleId}: - get: - summary: Retrieve a Rule - description: Get information about a rules. This will be deprecated in favour of `/api/policies/{policyID}`. - deprecated: true - tags: [ Rules ] - security: - - BearerAuth: [ ] - - TokenAuth: [ ] - parameters: - - in: path - name: ruleId - required: true - schema: - type: string - description: The unique identifier of a rule - responses: - '200': - description: A Rule object - content: - application/json: - schema: - $ref: '#/components/schemas/Rule' - '400': - "$ref": "#/components/responses/bad_request" - '401': - "$ref": "#/components/responses/requires_authentication" - '403': - "$ref": "#/components/responses/forbidden" - '500': - "$ref": "#/components/responses/internal_error" - put: - summary: Update a Rule - description: Update/Replace a rule. This will be deprecated in favour of `/api/policies/{policyID}`. - deprecated: true - tags: [ Rules ] - security: - - BearerAuth: [ ] - - TokenAuth: [ ] - parameters: - - in: path - name: ruleId - required: true - schema: - type: string - description: The unique identifier of a rule - requestBody: - description: Update Rule request - content: - 'application/json': - schema: - $ref: '#/components/schemas/RuleRequest' - responses: - '200': - description: A Rule object - content: - application/json: - schema: - $ref: '#/components/schemas/Rule' - '400': - "$ref": "#/components/responses/bad_request" - '401': - "$ref": "#/components/responses/requires_authentication" - '403': - "$ref": "#/components/responses/forbidden" - '500': - "$ref": "#/components/responses/internal_error" - delete: - summary: Delete a Rule - description: Delete a rule. This will be deprecated in favour of `/api/policies/{policyID}`. - deprecated: true - tags: [ Rules ] - security: - - BearerAuth: [ ] - - TokenAuth: [ ] - parameters: - - in: path - name: ruleId - required: true - schema: - type: string - description: The unique identifier of a rule - responses: - '200': - description: Delete status code - content: { } - '400': - "$ref": "#/components/responses/bad_request" - '401': - "$ref": "#/components/responses/requires_authentication" - '403': - "$ref": "#/components/responses/forbidden" - '500': - "$ref": "#/components/responses/internal_error" /api/policies: get: summary: List all Policies diff --git a/management/server/http/api/types.gen.go b/management/server/http/api/types.gen.go index 02547926b..d10b28c2b 100644 --- a/management/server/http/api/types.gen.go +++ b/management/server/http/api/types.gen.go @@ -993,66 +993,6 @@ type RouteRequest struct { PeerGroups *[]string `json:"peer_groups,omitempty"` } -// Rule defines model for Rule. -type Rule struct { - // Description Rule friendly description - Description string `json:"description"` - - // Destinations Rule destination group IDs - Destinations []GroupMinimum `json:"destinations"` - - // Disabled Rules status - Disabled bool `json:"disabled"` - - // Flow Rule flow, currently, only "bidirect" for bi-directional traffic is accepted - Flow string `json:"flow"` - - // Id Rule ID - Id string `json:"id"` - - // Name Rule name identifier - Name string `json:"name"` - - // Sources Rule source group IDs - Sources []GroupMinimum `json:"sources"` -} - -// RuleMinimum defines model for RuleMinimum. -type RuleMinimum struct { - // Description Rule friendly description - Description string `json:"description"` - - // Disabled Rules status - Disabled bool `json:"disabled"` - - // Flow Rule flow, currently, only "bidirect" for bi-directional traffic is accepted - Flow string `json:"flow"` - - // Name Rule name identifier - Name string `json:"name"` -} - -// RuleRequest defines model for RuleRequest. -type RuleRequest struct { - // Description Rule friendly description - Description string `json:"description"` - - // Destinations List of destination group IDs - Destinations *[]string `json:"destinations,omitempty"` - - // Disabled Rules status - Disabled bool `json:"disabled"` - - // Flow Rule flow, currently, only "bidirect" for bi-directional traffic is accepted - Flow string `json:"flow"` - - // Name Rule name identifier - Name string `json:"name"` - - // Sources List of source group IDs - Sources *[]string `json:"sources,omitempty"` -} - // SetupKey defines model for SetupKey. type SetupKey struct { // AutoGroups List of group IDs to auto-assign to peers registered with this key @@ -1236,12 +1176,6 @@ type PostApiRoutesJSONRequestBody = RouteRequest // PutApiRoutesRouteIdJSONRequestBody defines body for PutApiRoutesRouteId for application/json ContentType. type PutApiRoutesRouteIdJSONRequestBody = RouteRequest -// PostApiRulesJSONRequestBody defines body for PostApiRules for application/json ContentType. -type PostApiRulesJSONRequestBody = RuleRequest - -// PutApiRulesRuleIdJSONRequestBody defines body for PutApiRulesRuleId for application/json ContentType. -type PutApiRulesRuleIdJSONRequestBody = RuleRequest - // PostApiSetupKeysJSONRequestBody defines body for PostApiSetupKeys for application/json ContentType. type PostApiSetupKeysJSONRequestBody = SetupKeyRequest diff --git a/management/server/http/groups_handler.go b/management/server/http/groups_handler.go index c06445690..b37f4fd2f 100644 --- a/management/server/http/groups_handler.go +++ b/management/server/http/groups_handler.go @@ -8,8 +8,6 @@ import ( "github.com/netbirdio/netbird/management/server/http/util" "github.com/netbirdio/netbird/management/server/status" - "github.com/rs/xid" - "github.com/netbirdio/netbird/management/server" "github.com/netbirdio/netbird/management/server/jwtclaims" @@ -151,7 +149,6 @@ func (h *GroupsHandler) CreateGroup(w http.ResponseWriter, r *http.Request) { peers = *req.Peers } group := server.Group{ - ID: xid.New().String(), Name: req.Name, Peers: peers, Issued: server.GroupIssuedAPI, diff --git a/management/server/http/handler.go b/management/server/http/handler.go index 4aab513a7..d035ae0b7 100644 --- a/management/server/http/handler.go +++ b/management/server/http/handler.go @@ -85,7 +85,6 @@ func APIHandler(ctx context.Context, accountManager s.AccountManager, LocationMa api.addUsersEndpoint() api.addUsersTokensEndpoint() api.addSetupKeysEndpoint() - api.addRulesEndpoint() api.addPoliciesEndpoint() api.addGroupsEndpoint() api.addRoutesEndpoint() @@ -158,15 +157,6 @@ func (apiHandler *apiHandler) addSetupKeysEndpoint() { apiHandler.Router.HandleFunc("/setup-keys/{keyId}", keysHandler.UpdateSetupKey).Methods("PUT", "OPTIONS") } -func (apiHandler *apiHandler) addRulesEndpoint() { - rulesHandler := NewRulesHandler(apiHandler.AccountManager, apiHandler.AuthCfg) - apiHandler.Router.HandleFunc("/rules", rulesHandler.GetAllRules).Methods("GET", "OPTIONS") - apiHandler.Router.HandleFunc("/rules", rulesHandler.CreateRule).Methods("POST", "OPTIONS") - apiHandler.Router.HandleFunc("/rules/{ruleId}", rulesHandler.UpdateRule).Methods("PUT", "OPTIONS") - apiHandler.Router.HandleFunc("/rules/{ruleId}", rulesHandler.GetRule).Methods("GET", "OPTIONS") - apiHandler.Router.HandleFunc("/rules/{ruleId}", rulesHandler.DeleteRule).Methods("DELETE", "OPTIONS") -} - func (apiHandler *apiHandler) addPoliciesEndpoint() { policiesHandler := NewPoliciesHandler(apiHandler.AccountManager, apiHandler.AuthCfg) apiHandler.Router.HandleFunc("/policies", policiesHandler.GetAllPolicies).Methods("GET", "OPTIONS") diff --git a/management/server/http/peers_handler_test.go b/management/server/http/peers_handler_test.go index 27978c487..e43c4375e 100644 --- a/management/server/http/peers_handler_test.go +++ b/management/server/http/peers_handler_test.go @@ -55,6 +55,9 @@ func initTestMetaData(peers ...*nbpeer.Peer) *PeersHandler { GetPeersFunc: func(accountID, userID string) ([]*nbpeer.Peer, error) { return peers, nil }, + GetDNSDomainFunc: func() string { + return "netbird.selfhosted" + }, GetAccountFromTokenFunc: func(claims jwtclaims.AuthorizationClaims) (*server.Account, *server.User, error) { user := server.NewAdminUser("test_user") return &server.Account{ diff --git a/management/server/http/policies_handler_test.go b/management/server/http/policies_handler_test.go index 86665848b..e6b858036 100644 --- a/management/server/http/policies_handler_test.go +++ b/management/server/http/policies_handler_test.go @@ -3,7 +3,6 @@ package http import ( "bytes" "encoding/json" - "fmt" "io" "net/http" "net/http/httptest" @@ -44,24 +43,6 @@ func initPoliciesTestData(policies ...*server.Policy) *Policies { } return nil }, - SaveRuleFunc: func(_, _ string, rule *server.Rule) error { - if !strings.HasPrefix(rule.ID, "id-") { - rule.ID = "id-was-set" - } - return nil - }, - GetRuleFunc: func(_, ruleID, _ string) (*server.Rule, error) { - if ruleID != "idoftherule" { - return nil, fmt.Errorf("not found") - } - return &server.Rule{ - ID: "idoftherule", - Name: "Rule", - Source: []string{"idofsrcrule"}, - Destination: []string{"idofdestrule"}, - Flow: server.TrafficFlowBidirect, - }, nil - }, GetAccountFromTokenFunc: func(claims jwtclaims.AuthorizationClaims) (*server.Account, *server.User, error) { user := server.NewAdminUser("test_user") return &server.Account{ diff --git a/management/server/http/rules_handler.go b/management/server/http/rules_handler.go deleted file mode 100644 index bd501acf9..000000000 --- a/management/server/http/rules_handler.go +++ /dev/null @@ -1,305 +0,0 @@ -package http - -import ( - "encoding/json" - "net/http" - - "github.com/gorilla/mux" - "github.com/rs/xid" - - "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/jwtclaims" - "github.com/netbirdio/netbird/management/server/status" -) - -// RulesHandler is a handler that returns rules of the account -type RulesHandler struct { - accountManager server.AccountManager - claimsExtractor *jwtclaims.ClaimsExtractor -} - -// NewRulesHandler creates a new RulesHandler HTTP handler -func NewRulesHandler(accountManager server.AccountManager, authCfg AuthCfg) *RulesHandler { - return &RulesHandler{ - accountManager: accountManager, - claimsExtractor: jwtclaims.NewClaimsExtractor( - jwtclaims.WithAudience(authCfg.Audience), - jwtclaims.WithUserIDClaim(authCfg.UserIDClaim), - ), - } -} - -// GetAllRules list for the account -func (h *RulesHandler) GetAllRules(w http.ResponseWriter, r *http.Request) { - claims := h.claimsExtractor.FromRequestContext(r) - account, user, err := h.accountManager.GetAccountFromToken(claims) - if err != nil { - util.WriteError(err, w) - return - } - - accountPolicies, err := h.accountManager.ListPolicies(account.Id, user.Id) - if err != nil { - util.WriteError(err, w) - return - } - rules := []*api.Rule{} - for _, policy := range accountPolicies { - for _, r := range policy.Rules { - rules = append(rules, toRuleResponse(account, r.ToRule())) - } - } - - util.WriteJSONObject(w, rules) -} - -// UpdateRule handles update to a rule identified by a given ID -func (h *RulesHandler) UpdateRule(w http.ResponseWriter, r *http.Request) { - claims := h.claimsExtractor.FromRequestContext(r) - account, user, err := h.accountManager.GetAccountFromToken(claims) - if err != nil { - util.WriteError(err, w) - return - } - - vars := mux.Vars(r) - ruleID := vars["ruleId"] - if len(ruleID) == 0 { - util.WriteError(status.Errorf(status.InvalidArgument, "invalid rule ID"), w) - return - } - - policy, err := h.accountManager.GetPolicy(account.Id, ruleID, user.Id) - if err != nil { - util.WriteError(err, w) - return - } - - var req api.PutApiRulesRuleIdJSONRequestBody - err = json.NewDecoder(r.Body).Decode(&req) - if err != nil { - util.WriteErrorResponse("couldn't parse JSON request", http.StatusBadRequest, w) - } - - if req.Name == "" { - util.WriteError(status.Errorf(status.InvalidArgument, "rule name shouldn't be empty"), w) - return - } - - var reqSources []string - if req.Sources != nil { - reqSources = *req.Sources - } - - var reqDestinations []string - if req.Destinations != nil { - reqDestinations = *req.Destinations - } - - if len(policy.Rules) != 1 { - util.WriteError(status.Errorf(status.Internal, "policy should contain exactly one rule"), w) - return - } - - policy.Name = req.Name - policy.Description = req.Description - policy.Enabled = !req.Disabled - policy.Rules[0].ID = ruleID - policy.Rules[0].Name = req.Name - policy.Rules[0].Sources = reqSources - policy.Rules[0].Destinations = reqDestinations - policy.Rules[0].Enabled = !req.Disabled - policy.Rules[0].Description = req.Description - - switch req.Flow { - case server.TrafficFlowBidirectString: - policy.Rules[0].Action = server.PolicyTrafficActionAccept - default: - util.WriteError(status.Errorf(status.InvalidArgument, "unknown flow type"), w) - return - } - - err = h.accountManager.SavePolicy(account.Id, user.Id, policy) - if err != nil { - util.WriteError(err, w) - return - } - - resp := toRuleResponse(account, policy.Rules[0].ToRule()) - - util.WriteJSONObject(w, &resp) -} - -// CreateRule handles rule creation request -func (h *RulesHandler) CreateRule(w http.ResponseWriter, r *http.Request) { - claims := h.claimsExtractor.FromRequestContext(r) - account, user, err := h.accountManager.GetAccountFromToken(claims) - if err != nil { - util.WriteError(err, w) - return - } - - var req api.PostApiRulesJSONRequestBody - err = json.NewDecoder(r.Body).Decode(&req) - if err != nil { - util.WriteErrorResponse("couldn't parse JSON request", http.StatusBadRequest, w) - return - } - - if req.Name == "" { - util.WriteError(status.Errorf(status.InvalidArgument, "rule name shouldn't be empty"), w) - return - } - - var reqSources []string - if req.Sources != nil { - reqSources = *req.Sources - } - - var reqDestinations []string - if req.Destinations != nil { - reqDestinations = *req.Destinations - } - - rule := server.Rule{ - ID: xid.New().String(), - Name: req.Name, - Source: reqSources, - Destination: reqDestinations, - Disabled: req.Disabled, - Description: req.Description, - } - - switch req.Flow { - case server.TrafficFlowBidirectString: - rule.Flow = server.TrafficFlowBidirect - default: - util.WriteError(status.Errorf(status.InvalidArgument, "unknown flow type"), w) - return - } - - policy, err := server.RuleToPolicy(&rule) - if err != nil { - util.WriteError(err, w) - return - } - err = h.accountManager.SavePolicy(account.Id, user.Id, policy) - if err != nil { - util.WriteError(err, w) - return - } - - resp := toRuleResponse(account, &rule) - - util.WriteJSONObject(w, &resp) -} - -// DeleteRule handles rule deletion request -func (h *RulesHandler) DeleteRule(w http.ResponseWriter, r *http.Request) { - claims := h.claimsExtractor.FromRequestContext(r) - account, user, err := h.accountManager.GetAccountFromToken(claims) - if err != nil { - util.WriteError(err, w) - return - } - aID := account.Id - - rID := mux.Vars(r)["ruleId"] - if len(rID) == 0 { - util.WriteError(status.Errorf(status.InvalidArgument, "invalid rule ID"), w) - return - } - - err = h.accountManager.DeletePolicy(aID, rID, user.Id) - if err != nil { - util.WriteError(err, w) - return - } - - util.WriteJSONObject(w, emptyObject{}) -} - -// GetRule handles a group Get request identified by ID -func (h *RulesHandler) GetRule(w http.ResponseWriter, r *http.Request) { - claims := h.claimsExtractor.FromRequestContext(r) - account, user, err := h.accountManager.GetAccountFromToken(claims) - if err != nil { - util.WriteError(err, w) - return - } - - switch r.Method { - case http.MethodGet: - ruleID := mux.Vars(r)["ruleId"] - if len(ruleID) == 0 { - util.WriteError(status.Errorf(status.InvalidArgument, "invalid rule ID"), w) - return - } - - policy, err := h.accountManager.GetPolicy(account.Id, ruleID, user.Id) - if err != nil { - util.WriteError(err, w) - return - } - - util.WriteJSONObject(w, toRuleResponse(account, policy.Rules[0].ToRule())) - default: - util.WriteError(status.Errorf(status.NotFound, "method not found"), w) - } -} - -func toRuleResponse(account *server.Account, rule *server.Rule) *api.Rule { - cache := make(map[string]api.GroupMinimum) - gr := api.Rule{ - Id: rule.ID, - Name: rule.Name, - Description: rule.Description, - Disabled: rule.Disabled, - } - - switch rule.Flow { - case server.TrafficFlowBidirect: - gr.Flow = server.TrafficFlowBidirectString - default: - gr.Flow = "unknown" - } - - for _, gid := range rule.Source { - _, ok := cache[gid] - if ok { - continue - } - - if group, ok := account.Groups[gid]; ok { - minimum := api.GroupMinimum{ - Id: group.ID, - Name: group.Name, - PeersCount: len(group.Peers), - } - - gr.Sources = append(gr.Sources, minimum) - cache[gid] = minimum - } - } - - for _, gid := range rule.Destination { - cachedMinimum, ok := cache[gid] - if ok { - gr.Destinations = append(gr.Destinations, cachedMinimum) - continue - } - if group, ok := account.Groups[gid]; ok { - minimum := api.GroupMinimum{ - Id: group.ID, - Name: group.Name, - PeersCount: len(group.Peers), - } - gr.Destinations = append(gr.Destinations, minimum) - cache[gid] = minimum - } - } - - return &gr -} diff --git a/management/server/http/rules_handler_test.go b/management/server/http/rules_handler_test.go deleted file mode 100644 index 27a308a0a..000000000 --- a/management/server/http/rules_handler_test.go +++ /dev/null @@ -1,265 +0,0 @@ -package http - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "net/http" - "net/http/httptest" - "strings" - "testing" - - "github.com/netbirdio/netbird/management/server/http/api" - "github.com/netbirdio/netbird/management/server/status" - - "github.com/gorilla/mux" - - "github.com/netbirdio/netbird/management/server/jwtclaims" - - "github.com/magiconair/properties/assert" - - "github.com/netbirdio/netbird/management/server" - "github.com/netbirdio/netbird/management/server/mock_server" -) - -func initRulesTestData(rules ...*server.Rule) *RulesHandler { - testPolicies := make(map[string]*server.Policy, len(rules)) - for _, rule := range rules { - policy, err := server.RuleToPolicy(rule) - if err != nil { - panic(err) - } - testPolicies[policy.ID] = policy - } - return &RulesHandler{ - accountManager: &mock_server.MockAccountManager{ - GetPolicyFunc: func(_, policyID, _ string) (*server.Policy, error) { - policy, ok := testPolicies[policyID] - if !ok { - return nil, status.Errorf(status.NotFound, "policy not found") - } - return policy, nil - }, - SavePolicyFunc: func(_, _ string, policy *server.Policy) error { - if !strings.HasPrefix(policy.ID, "id-") { - policy.ID = "id-was-set" - } - return nil - }, - SaveRuleFunc: func(_, _ string, rule *server.Rule) error { - if !strings.HasPrefix(rule.ID, "id-") { - rule.ID = "id-was-set" - } - return nil - }, - GetRuleFunc: func(_, ruleID, _ string) (*server.Rule, error) { - if ruleID != "idoftherule" { - return nil, fmt.Errorf("not found") - } - return &server.Rule{ - ID: "idoftherule", - Name: "Rule", - Source: []string{"idofsrcrule"}, - Destination: []string{"idofdestrule"}, - Flow: server.TrafficFlowBidirect, - }, nil - }, - GetAccountFromTokenFunc: func(claims jwtclaims.AuthorizationClaims) (*server.Account, *server.User, error) { - user := server.NewAdminUser("test_user") - return &server.Account{ - Id: claims.AccountId, - Domain: "hotmail.com", - Rules: map[string]*server.Rule{"id-existed": {ID: "id-existed"}}, - Groups: map[string]*server.Group{ - "F": {ID: "F"}, - "G": {ID: "G"}, - }, - Users: map[string]*server.User{ - "test_user": user, - }, - }, user, nil - }, - }, - claimsExtractor: jwtclaims.NewClaimsExtractor( - jwtclaims.WithFromRequestContext(func(r *http.Request) jwtclaims.AuthorizationClaims { - return jwtclaims.AuthorizationClaims{ - UserId: "test_user", - Domain: "hotmail.com", - AccountId: "test_id", - } - }), - ), - } -} - -func TestRulesGetRule(t *testing.T) { - tt := []struct { - name string - expectedStatus int - expectedBody bool - requestType string - requestPath string - requestBody io.Reader - }{ - { - name: "GetRule OK", - expectedBody: true, - requestType: http.MethodGet, - requestPath: "/api/rules/idoftherule", - expectedStatus: http.StatusOK, - }, - { - name: "GetRule not found", - requestType: http.MethodGet, - requestPath: "/api/rules/notexists", - expectedStatus: http.StatusNotFound, - }, - } - - rule := &server.Rule{ - ID: "idoftherule", - Name: "Rule", - } - - p := initRulesTestData(rule) - - for _, tc := range tt { - t.Run(tc.name, func(t *testing.T) { - recorder := httptest.NewRecorder() - req := httptest.NewRequest(tc.requestType, tc.requestPath, tc.requestBody) - - router := mux.NewRouter() - router.HandleFunc("/api/rules/{ruleId}", p.GetRule).Methods("GET") - router.ServeHTTP(recorder, req) - - res := recorder.Result() - defer res.Body.Close() - - if status := recorder.Code; status != tc.expectedStatus { - t.Errorf("handler returned wrong status code: got %v want %v", - status, tc.expectedStatus) - return - } - - if !tc.expectedBody { - return - } - - content, err := io.ReadAll(res.Body) - if err != nil { - t.Fatalf("I don't know what I expected; %v", err) - } - - var got api.Rule - if err = json.Unmarshal(content, &got); err != nil { - t.Fatalf("Sent content is not in correct json format; %v", err) - } - - assert.Equal(t, got.Id, rule.ID) - assert.Equal(t, got.Name, rule.Name) - }) - } -} - -func TestRulesWriteRule(t *testing.T) { - tt := []struct { - name string - expectedStatus int - expectedBody bool - expectedRule *api.Rule - requestType string - requestPath string - requestBody io.Reader - }{ - { - name: "WriteRule POST OK", - requestType: http.MethodPost, - requestPath: "/api/rules", - requestBody: bytes.NewBuffer( - []byte(`{"Name":"Default POSTed Rule","Flow":"bidirect"}`)), - expectedStatus: http.StatusOK, - expectedBody: true, - expectedRule: &api.Rule{ - Id: "id-was-set", - Name: "Default POSTed Rule", - Flow: server.TrafficFlowBidirectString, - }, - }, - { - name: "WriteRule POST Invalid Name", - requestType: http.MethodPost, - requestPath: "/api/rules", - requestBody: bytes.NewBuffer( - []byte(`{"Name":"","Flow":"bidirect"}`)), - expectedStatus: http.StatusUnprocessableEntity, - expectedBody: false, - }, - { - name: "WriteRule PUT OK", - requestType: http.MethodPut, - requestPath: "/api/rules/id-existed", - requestBody: bytes.NewBuffer( - []byte(`{"Name":"Default POSTed Rule","Flow":"bidirect"}`)), - expectedStatus: http.StatusOK, - expectedBody: true, - expectedRule: &api.Rule{ - Id: "id-existed", - Name: "Default POSTed Rule", - Flow: server.TrafficFlowBidirectString, - }, - }, - { - name: "WriteRule PUT Invalid Name", - requestType: http.MethodPut, - requestPath: "/api/rules/id-existed", - requestBody: bytes.NewBuffer( - []byte(`{"Name":"","Flow":"bidirect"}`)), - expectedStatus: http.StatusUnprocessableEntity, - }, - } - - p := initRulesTestData(&server.Rule{ - ID: "id-existed", - Name: "Default POSTed Rule", - Flow: server.TrafficFlowBidirect, - }) - - for _, tc := range tt { - t.Run(tc.name, func(t *testing.T) { - recorder := httptest.NewRecorder() - req := httptest.NewRequest(tc.requestType, tc.requestPath, tc.requestBody) - - router := mux.NewRouter() - router.HandleFunc("/api/rules", p.CreateRule).Methods("POST") - router.HandleFunc("/api/rules/{ruleId}", p.UpdateRule).Methods("PUT") - router.ServeHTTP(recorder, req) - - res := recorder.Result() - defer res.Body.Close() - - content, err := io.ReadAll(res.Body) - if err != nil { - t.Fatalf("I don't know what I expected; %v", err) - } - - if status := recorder.Code; status != tc.expectedStatus { - t.Errorf("handler returned wrong status code: got %v want %v, content: %s", - status, tc.expectedStatus, string(content)) - return - } - - if !tc.expectedBody { - return - } - - got := &api.Rule{} - if err = json.Unmarshal(content, &got); err != nil { - t.Fatalf("Sent content is not in correct json format; %v", err) - } - tc.expectedRule.Id = got.Id - - assert.Equal(t, got, tc.expectedRule) - }) - } -} diff --git a/management/server/idp/authentik.go b/management/server/idp/authentik.go index 4bbf09404..b39f2b5cb 100644 --- a/management/server/idp/authentik.go +++ b/management/server/idp/authentik.go @@ -76,6 +76,10 @@ func NewAuthentikManager(config AuthentikClientConfig, return nil, fmt.Errorf("authentik IdP configuration is incomplete, TokenEndpoint is missing") } + if config.Issuer == "" { + return nil, fmt.Errorf("authentik IdP configuration is incomplete, Issuer is missing") + } + if config.GrantType == "" { return nil, fmt.Errorf("authentik IdP configuration is incomplete, GrantType is missing") } diff --git a/management/server/idp/authentik_test.go b/management/server/idp/authentik_test.go index c70a84efd..342e16384 100644 --- a/management/server/idp/authentik_test.go +++ b/management/server/idp/authentik_test.go @@ -7,9 +7,10 @@ import ( "testing" "time" - "github.com/netbirdio/netbird/management/server/telemetry" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/netbirdio/netbird/management/server/telemetry" ) func TestNewAuthentikManager(t *testing.T) { @@ -25,6 +26,7 @@ func TestNewAuthentikManager(t *testing.T) { Username: "username", Password: "password", TokenEndpoint: "https://localhost:8080/application/o/token/", + Issuer: "https://localhost:8080/application/o/netbird/", GrantType: "client_credentials", } @@ -75,7 +77,17 @@ func TestNewAuthentikManager(t *testing.T) { assertErrFuncMessage: "should return error when field empty", } - for _, testCase := range []test{testCase1, testCase2, testCase3, testCase4, testCase5} { + testCase6Config := defaultTestConfig + testCase6Config.Issuer = "" + + testCase6 := test{ + name: "Missing Issuer Configuration", + inputConfig: testCase6Config, + assertErrFunc: require.Error, + assertErrFuncMessage: "should return error when field empty", + } + + for _, testCase := range []test{testCase1, testCase2, testCase3, testCase4, testCase5, testCase6} { t.Run(testCase.name, func(t *testing.T) { _, err := NewAuthentikManager(testCase.inputConfig, &telemetry.MockAppMetrics{}) testCase.assertErrFunc(t, err, testCase.assertErrFuncMessage) diff --git a/management/server/idp/zitadel.go b/management/server/idp/zitadel.go index 926f078b2..c09d362d8 100644 --- a/management/server/idp/zitadel.go +++ b/management/server/idp/zitadel.go @@ -75,6 +75,27 @@ type zitadelProfile struct { Human *zitadelUser `json:"human"` } +// zitadelUserDetails represents the metadata for the new user that was created +type zitadelUserDetails struct { + Sequence string `json:"sequence"` // uint64 as a string + CreationDate string `json:"creationDate"` // ISO format + ChangeDate string `json:"changeDate"` // ISO format + ResourceOwner string +} + +// zitadelPasswordlessRegistration represents the information for the user to complete signup +type zitadelPasswordlessRegistration struct { + Link string `json:"link"` + Expiration string `json:"expiration"` // ex: 3600s +} + +// zitadelUser represents an zitadel create user response +type zitadelUserResponse struct { + UserId string `json:"userId"` + Details zitadelUserDetails `json:"details"` + PasswordlessRegistration zitadelPasswordlessRegistration `json:"passwordlessRegistration"` +} + // NewZitadelManager creates a new instance of the ZitadelManager. func NewZitadelManager(config ZitadelClientConfig, appMetrics telemetry.AppMetrics) (*ZitadelManager, error) { httpTransport := http.DefaultTransport.(*http.Transport).Clone() @@ -224,9 +245,57 @@ func (zc *ZitadelCredentials) Authenticate() (JWTToken, error) { return zc.jwtToken, nil } -// CreateUser creates a new user in zitadel Idp and sends an invite. -func (zm *ZitadelManager) CreateUser(_, _, _, _ string) (*UserData, error) { - return nil, fmt.Errorf("method CreateUser not implemented") +// CreateUser creates a new user in zitadel Idp and sends an invite via Zitadel. +func (zm *ZitadelManager) CreateUser(email, name, accountID, invitedByEmail string) (*UserData, error) { + firstLast := strings.SplitN(name, " ", 2) + + var addUser = map[string]any{ + "userName": email, + "profile": map[string]string{ + "firstName": firstLast[0], + "lastName": firstLast[0], + "displayName": name, + }, + "email": map[string]any{ + "email": email, + "isEmailVerified": false, + }, + "passwordChangeRequired": true, + "requestPasswordlessRegistration": false, // let Zitadel send the invite for us + } + + payload, err := zm.helper.Marshal(addUser) + if err != nil { + return nil, err + } + + body, err := zm.post("users/human/_import", string(payload)) + if err != nil { + return nil, err + } + + if zm.appMetrics != nil { + zm.appMetrics.IDPMetrics().CountCreateUser() + } + + var newUser zitadelUserResponse + err = zm.helper.Unmarshal(body, &newUser) + if err != nil { + return nil, err + } + + var pending bool = true + ret := &UserData{ + Email: email, + Name: name, + ID: newUser.UserId, + AppMetadata: AppMetadata{ + WTAccountID: accountID, + WTPendingInvite: &pending, + WTInvitedBy: invitedByEmail, + }, + } + return ret, nil } // GetUserByEmail searches users with a given email. @@ -354,10 +423,25 @@ func (zm *ZitadelManager) UpdateUserAppMetadata(_ string, _ AppMetadata) error { return nil } +type inviteUserRequest struct { + Email string `json:"email"` +} + // InviteUserByID resend invitations to users who haven't activated, // their accounts prior to the expiration period. -func (zm *ZitadelManager) InviteUserByID(_ string) error { - return fmt.Errorf("method InviteUserByID not implemented") +func (zm *ZitadelManager) InviteUserByID(userID string) error { + inviteUser := inviteUserRequest{ + Email: userID, + } + + payload, err := zm.helper.Marshal(inviteUser) + if err != nil { + return err + } + + // don't care about the body in the response + _, err = zm.post(fmt.Sprintf("users/%s/_resend_initialization", userID), string(payload)) + return err } // DeleteUser from Zitadel @@ -411,7 +495,38 @@ func (zm *ZitadelManager) post(resource string, body string) ([]byte, error) { } // delete perform Delete requests. -func (zm *ZitadelManager) delete(_ string) error { +func (zm *ZitadelManager) delete(resource string) error { + jwtToken, err := zm.credentials.Authenticate() + if err != nil { + return err + } + + reqURL := fmt.Sprintf("%s/%s", zm.managementEndpoint, resource) + req, err := http.NewRequest(http.MethodDelete, reqURL, nil) + if err != nil { + return err + } + req.Header.Add("authorization", "Bearer "+jwtToken.AccessToken) + req.Header.Add("content-type", "application/json") + + resp, err := zm.httpClient.Do(req) + if err != nil { + if zm.appMetrics != nil { + zm.appMetrics.IDPMetrics().CountRequestError() + } + + return err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + if zm.appMetrics != nil { + zm.appMetrics.IDPMetrics().CountRequestStatusError() + } + + return fmt.Errorf("unable to get %s, statusCode %d", reqURL, resp.StatusCode) + } + return nil } diff --git a/management/server/management_proto_test.go b/management/server/management_proto_test.go index e5457db02..6ea902003 100644 --- a/management/server/management_proto_test.go +++ b/management/server/management_proto_test.go @@ -412,7 +412,7 @@ func startManagement(t *testing.T, config *Config) (*grpc.Server, string, error) } peersUpdateManager := NewPeersUpdateManager(nil) eventStore := &activity.InMemoryEventStore{} - accountManager, err := BuildManager(store, peersUpdateManager, nil, "", "", + accountManager, err := BuildManager(store, peersUpdateManager, nil, "", "netbird.selfhosted", eventStore, nil, false) if err != nil { return nil, "", err diff --git a/management/server/management_test.go b/management/server/management_test.go index f45354877..fb3f74cb9 100644 --- a/management/server/management_test.go +++ b/management/server/management_test.go @@ -503,7 +503,7 @@ func startServer(config *server.Config) (*grpc.Server, net.Listener) { } peersUpdateManager := server.NewPeersUpdateManager(nil) eventStore := &activity.InMemoryEventStore{} - accountManager, err := server.BuildManager(store, peersUpdateManager, nil, "", "", + accountManager, err := server.BuildManager(store, peersUpdateManager, nil, "", "netbird.selfhosted", eventStore, nil, false) if err != nil { log.Fatalf("failed creating a manager: %v", err) diff --git a/management/server/mock_server/account_mock.go b/management/server/mock_server/account_mock.go index 2df5ef086..f518372ed 100644 --- a/management/server/mock_server/account_mock.go +++ b/management/server/mock_server/account_mock.go @@ -38,10 +38,7 @@ type MockAccountManager struct { ListGroupsFunc func(accountID string) ([]*server.Group, error) GroupAddPeerFunc func(accountID, groupID, peerID string) error GroupDeletePeerFunc func(accountID, groupID, peerID string) error - GetRuleFunc func(accountID, ruleID, userID string) (*server.Rule, error) - SaveRuleFunc func(accountID, userID string, rule *server.Rule) error DeleteRuleFunc func(accountID, ruleID, userID string) error - ListRulesFunc func(accountID, userID string) ([]*server.Rule, error) GetPolicyFunc func(accountID, policyID, userID string) (*server.Policy, error) SavePolicyFunc func(accountID, userID string, policy *server.Policy) error DeletePolicyFunc func(accountID, policyID, userID string) error @@ -302,22 +299,6 @@ func (am *MockAccountManager) GroupDeletePeer(accountID, groupID, peerID string) return status.Errorf(codes.Unimplemented, "method GroupDeletePeer is not implemented") } -// GetRule mock implementation of GetRule from server.AccountManager interface -func (am *MockAccountManager) GetRule(accountID, ruleID, userID string) (*server.Rule, error) { - if am.GetRuleFunc != nil { - return am.GetRuleFunc(accountID, ruleID, userID) - } - return nil, status.Errorf(codes.Unimplemented, "method GetRule is not implemented") -} - -// SaveRule mock implementation of SaveRule from server.AccountManager interface -func (am *MockAccountManager) SaveRule(accountID, userID string, rule *server.Rule) error { - if am.SaveRuleFunc != nil { - return am.SaveRuleFunc(accountID, userID, rule) - } - return status.Errorf(codes.Unimplemented, "method SaveRule is not implemented") -} - // DeleteRule mock implementation of DeleteRule from server.AccountManager interface func (am *MockAccountManager) DeleteRule(accountID, ruleID, userID string) error { if am.DeleteRuleFunc != nil { @@ -326,14 +307,6 @@ func (am *MockAccountManager) DeleteRule(accountID, ruleID, userID string) error return status.Errorf(codes.Unimplemented, "method DeleteRule is not implemented") } -// ListRules mock implementation of ListRules from server.AccountManager interface -func (am *MockAccountManager) ListRules(accountID, userID string) ([]*server.Rule, error) { - if am.ListRulesFunc != nil { - return am.ListRulesFunc(accountID, userID) - } - return nil, status.Errorf(codes.Unimplemented, "method ListRules is not implemented") -} - // GetPolicy mock implementation of GetPolicy from server.AccountManager interface func (am *MockAccountManager) GetPolicy(accountID, policyID, userID string) (*server.Policy, error) { if am.GetPolicyFunc != nil { diff --git a/management/server/nameserver_test.go b/management/server/nameserver_test.go index 3327869b4..d04ac1a20 100644 --- a/management/server/nameserver_test.go +++ b/management/server/nameserver_test.go @@ -759,7 +759,7 @@ func createNSManager(t *testing.T) (*DefaultAccountManager, error) { return nil, err } eventStore := &activity.InMemoryEventStore{} - return BuildManager(store, NewPeersUpdateManager(nil), nil, "", "", eventStore, nil, false) + return BuildManager(store, NewPeersUpdateManager(nil), nil, "", "netbird.selfhosted", eventStore, nil, false) } func createNSStore(t *testing.T) (Store, error) { diff --git a/management/server/policy.go b/management/server/policy.go index 291a4f1f7..8265dabb5 100644 --- a/management/server/policy.go +++ b/management/server/policy.go @@ -52,6 +52,17 @@ const ( PolicyRuleFlowBidirect = PolicyRuleDirection("bidirect") ) +const ( + // DefaultRuleName is a name for the Default rule that is created for every account + DefaultRuleName = "Default" + // DefaultRuleDescription is a description for the Default rule that is created for every account + DefaultRuleDescription = "This is a default rule that allows connections between all the resources" + // DefaultPolicyName is a name for the Default policy that is created for every account + DefaultPolicyName = "Default" + // DefaultPolicyDescription is a description for the Default policy that is created for every account + DefaultPolicyDescription = "This is a default policy that allows connections between all the resources" +) + const ( firewallRuleDirectionIN = 0 firewallRuleDirectionOUT = 1 @@ -119,19 +130,6 @@ func (pm *PolicyRule) Copy() *PolicyRule { return rule } -// ToRule converts the PolicyRule to a legacy representation of the Rule (for backwards compatibility) -func (pm *PolicyRule) ToRule() *Rule { - return &Rule{ - ID: pm.ID, - Name: pm.Name, - Description: pm.Description, - Disabled: !pm.Enabled, - Flow: TrafficFlowBidirect, - Destination: pm.Destinations, - Source: pm.Sources, - } -} - // Policy of the Rego query type Policy struct { // ID of the policy' diff --git a/management/server/route_test.go b/management/server/route_test.go index a5db2ca07..5a56eaa8b 100644 --- a/management/server/route_test.go +++ b/management/server/route_test.go @@ -1014,7 +1014,7 @@ func createRouterManager(t *testing.T) (*DefaultAccountManager, error) { return nil, err } eventStore := &activity.InMemoryEventStore{} - return BuildManager(store, NewPeersUpdateManager(nil), nil, "", "", eventStore, nil, false) + return BuildManager(store, NewPeersUpdateManager(nil), nil, "", "netbird.selfhosted", eventStore, nil, false) } func createRouterStore(t *testing.T) (Store, error) { diff --git a/management/server/rule.go b/management/server/rule.go deleted file mode 100644 index 19085840c..000000000 --- a/management/server/rule.go +++ /dev/null @@ -1,100 +0,0 @@ -package server - -import "fmt" - -// TrafficFlowType defines allowed direction of the traffic in the rule -type TrafficFlowType int - -const ( - // TrafficFlowBidirect allows traffic to both direction - TrafficFlowBidirect TrafficFlowType = iota - // TrafficFlowBidirectString allows traffic to both direction - TrafficFlowBidirectString = "bidirect" - // DefaultRuleName is a name for the Default rule that is created for every account - DefaultRuleName = "Default" - // DefaultRuleDescription is a description for the Default rule that is created for every account - DefaultRuleDescription = "This is a default rule that allows connections between all the resources" - // DefaultPolicyName is a name for the Default policy that is created for every account - DefaultPolicyName = "Default" - // DefaultPolicyDescription is a description for the Default policy that is created for every account - DefaultPolicyDescription = "This is a default policy that allows connections between all the resources" -) - -// Rule of ACL for groups -type Rule struct { - // ID of the rule - ID string - - // AccountID is a reference to Account that this object belongs - AccountID string `json:"-" gorm:"index"` - - // Name of the rule visible in the UI - Name string - - // Description of the rule visible in the UI - Description string - - // Disabled status of rule in the system - Disabled bool - - // Source list of groups IDs of peers - Source []string `gorm:"serializer:json"` - - // Destination list of groups IDs of peers - Destination []string `gorm:"serializer:json"` - - // Flow of the traffic allowed by the rule - Flow TrafficFlowType -} - -func (r *Rule) Copy() *Rule { - rule := &Rule{ - ID: r.ID, - Name: r.Name, - Description: r.Description, - Disabled: r.Disabled, - Source: make([]string, len(r.Source)), - Destination: make([]string, len(r.Destination)), - Flow: r.Flow, - } - copy(rule.Source, r.Source) - copy(rule.Destination, r.Destination) - return rule -} - -// EventMeta returns activity event meta related to this rule -func (r *Rule) EventMeta() map[string]any { - return map[string]any{"name": r.Name} -} - -// ToPolicyRule converts a Rule to a PolicyRule object -func (r *Rule) ToPolicyRule() *PolicyRule { - if r == nil { - return nil - } - return &PolicyRule{ - ID: r.ID, - Name: r.Name, - Enabled: !r.Disabled, - Description: r.Description, - Destinations: r.Destination, - Sources: r.Source, - Bidirectional: true, - Protocol: PolicyRuleProtocolALL, - Action: PolicyTrafficActionAccept, - } -} - -// RuleToPolicy converts a Rule to a Policy query object -func RuleToPolicy(rule *Rule) (*Policy, error) { - if rule == nil { - return nil, fmt.Errorf("rule is empty") - } - return &Policy{ - ID: rule.ID, - Name: rule.Name, - Description: rule.Description, - Enabled: !rule.Disabled, - Rules: []*PolicyRule{rule.ToPolicyRule()}, - }, nil -} diff --git a/management/server/scheduler_test.go b/management/server/scheduler_test.go index 4b2c2e30d..9dd73e269 100644 --- a/management/server/scheduler_test.go +++ b/management/server/scheduler_test.go @@ -3,6 +3,7 @@ package server import ( "fmt" "math/rand" + "runtime" "sync" "testing" "time" @@ -25,7 +26,13 @@ func TestScheduler_Performance(t *testing.T) { return 0, false }) } - failed := waitTimeout(wg, 3*time.Second) + timeout := 3 * time.Second + if runtime.GOOS == "windows" { + // sleep and ticker are slower on windows see https://github.com/golang/go/issues/44343 + timeout = 5 * time.Second + } + + failed := waitTimeout(wg, timeout) if failed { t.Fatal("timed out while waiting for test to finish") return @@ -39,22 +46,29 @@ func TestScheduler_Cancel(t *testing.T) { scheduler := NewDefaultScheduler() tChan := make(chan struct{}) p := []string{jobID1, jobID2} - scheduler.Schedule(2*time.Millisecond, jobID1, func() (nextRunIn time.Duration, reschedule bool) { + scheduletime := 2 * time.Millisecond + sleepTime := 4 * time.Millisecond + if runtime.GOOS == "windows" { + // sleep and ticker are slower on windows see https://github.com/golang/go/issues/44343 + sleepTime = 20 * time.Millisecond + } + + scheduler.Schedule(scheduletime, jobID1, func() (nextRunIn time.Duration, reschedule bool) { tt := p[0] <-tChan t.Logf("job %s", tt) - return 2 * time.Millisecond, true + return scheduletime, true }) - scheduler.Schedule(2*time.Millisecond, jobID2, func() (nextRunIn time.Duration, reschedule bool) { - return 2 * time.Millisecond, true + scheduler.Schedule(scheduletime, jobID2, func() (nextRunIn time.Duration, reschedule bool) { + return scheduletime, true }) - time.Sleep(4 * time.Millisecond) + time.Sleep(sleepTime) assert.Len(t, scheduler.jobs, 2) scheduler.Cancel([]string{jobID1}) close(tChan) p = []string{} - time.Sleep(4 * time.Millisecond) + time.Sleep(sleepTime) assert.Len(t, scheduler.jobs, 1) assert.NotNil(t, scheduler.jobs[jobID2]) } diff --git a/management/server/sqlite_store.go b/management/server/sqlite_store.go index eff43a31b..f6a6f92a7 100644 --- a/management/server/sqlite_store.go +++ b/management/server/sqlite_store.go @@ -64,7 +64,7 @@ func NewSqliteStore(dataDir string, metrics telemetry.AppMetrics) (*SqliteStore, sql.SetMaxOpenConns(conns) // TODO: make it configurable err = db.AutoMigrate( - &SetupKey{}, &nbpeer.Peer{}, &User{}, &PersonalAccessToken{}, &Group{}, &Rule{}, + &SetupKey{}, &nbpeer.Peer{}, &User{}, &PersonalAccessToken{}, &Group{}, &Account{}, &Policy{}, &PolicyRule{}, &route.Route{}, &nbdns.NameServerGroup{}, &installation{}, &account.ExtraSettings{}, &posture.Checks{}, &nbpeer.NetworkAddress{}, ) diff --git a/management/server/store.go b/management/server/store.go index 7ef090a67..77b8d0dad 100644 --- a/management/server/store.go +++ b/management/server/store.go @@ -103,7 +103,14 @@ func NewStoreFromJson(dataDir string, metrics telemetry.AppMetrics) (Store, erro return nil, err } - switch kind := getStoreEngineFromEnv(); kind { + // if store engine is not set in the config we first try to evaluate NETBIRD_STORE_ENGINE + kind := getStoreEngineFromEnv() + if kind == "" { + // NETBIRD_STORE_ENGINE is not set we evaluate default based on dataDir + kind = getStoreEngineFromDatadir(dataDir) + } + + switch kind { case FileStoreEngine: return fstore, nil case SqliteStoreEngine: