From 4f63996ae8118d1017fd221d6f792000fcbc4a98 Mon Sep 17 00:00:00 2001 From: Vlad <4941176+crn4@users.noreply.github.com> Date: Thu, 12 Jun 2025 18:48:54 +0100 Subject: [PATCH 1/6] [management] added events streaming metrics (#3814) --- go.mod | 2 +- go.sum | 4 ++-- management/cmd/management.go | 8 +++++++- management/server/telemetry/app_metrics.go | 20 ++++++++++---------- 4 files changed, 20 insertions(+), 14 deletions(-) diff --git a/go.mod b/go.mod index 11dc88c43..a12058278 100644 --- a/go.mod +++ b/go.mod @@ -63,7 +63,7 @@ require ( github.com/miekg/dns v1.1.59 github.com/mitchellh/hashstructure/v2 v2.0.2 github.com/nadoo/ipset v0.5.0 - github.com/netbirdio/management-integrations/integrations v0.0.0-20250529122842-6700aa91190c + github.com/netbirdio/management-integrations/integrations v0.0.0-20250612164546-6bd7e2338d65 github.com/netbirdio/signal-dispatcher/dispatcher v0.0.0-20250514131221-a464fd5f30cb github.com/okta/okta-sdk-golang/v2 v2.18.0 github.com/oschwald/maxminddb-golang v1.12.0 diff --git a/go.sum b/go.sum index f887cee94..6ce503dd1 100644 --- a/go.sum +++ b/go.sum @@ -503,8 +503,8 @@ github.com/netbirdio/go-netroute v0.0.0-20240611143515-f59b0e1d3944 h1:TDtJKmM6S github.com/netbirdio/go-netroute v0.0.0-20240611143515-f59b0e1d3944/go.mod h1:sHA6TRxjQ6RLbnI+3R4DZo2Eseg/iKiPRfNmcuNySVQ= github.com/netbirdio/ice/v3 v3.0.0-20240315174635-e72a50fcb64e h1:PURA50S8u4mF6RrkYYCAvvPCixhqqEiEy3Ej6avh04c= github.com/netbirdio/ice/v3 v3.0.0-20240315174635-e72a50fcb64e/go.mod h1:YMLU7qbKfVjmEv7EoZPIVEI+kNYxWCdPK3VS0BU+U4Q= -github.com/netbirdio/management-integrations/integrations v0.0.0-20250529122842-6700aa91190c h1:SdZxYjR9XXHLyRsTbS1EHBr6+RI15oie1K9Q8yvi3FY= -github.com/netbirdio/management-integrations/integrations v0.0.0-20250529122842-6700aa91190c/go.mod h1:Gi9raplYzCCyh07Olw/DVfCJTFgpr1WCXJ/Q+8TSA9Q= +github.com/netbirdio/management-integrations/integrations v0.0.0-20250612164546-6bd7e2338d65 h1:5OfYiLjpr4dbQYJI5ouZaylkVdi2KlErLFOwBeBo5Hw= +github.com/netbirdio/management-integrations/integrations v0.0.0-20250612164546-6bd7e2338d65/go.mod h1:Gi9raplYzCCyh07Olw/DVfCJTFgpr1WCXJ/Q+8TSA9Q= github.com/netbirdio/service v0.0.0-20240911161631-f62744f42502 h1:3tHlFmhTdX9axERMVN63dqyFqnvuD+EMJHzM7mNGON8= github.com/netbirdio/service v0.0.0-20240911161631-f62744f42502/go.mod h1:CIMRFEJVL+0DS1a3Nx06NaMn4Dz63Ng6O7dl0qH0zVM= github.com/netbirdio/signal-dispatcher/dispatcher v0.0.0-20250514131221-a464fd5f30cb h1:Cr6age+ePALqlSvtp7wc6lYY97XN7rkD1K4XEDmY+TU= diff --git a/management/cmd/management.go b/management/cmd/management.go index 5fb07890f..bce09efdd 100644 --- a/management/cmd/management.go +++ b/management/cmd/management.go @@ -159,6 +159,12 @@ var ( if err != nil { return err } + + integrationMetrics, err := integrations.InitIntegrationMetrics(ctx, appMetrics) + if err != nil { + return err + } + store, err := store.NewStore(ctx, config.StoreConfig.Engine, config.Datadir, appMetrics, false) if err != nil { return fmt.Errorf("failed creating Store: %s: %v", config.Datadir, err) @@ -176,7 +182,7 @@ var ( if disableSingleAccMode { mgmtSingleAccModeDomain = "" } - eventStore, key, err := integrations.InitEventStore(ctx, config.Datadir, config.DataStoreEncryptionKey) + eventStore, key, err := integrations.InitEventStore(ctx, config.Datadir, config.DataStoreEncryptionKey, integrationMetrics) if err != nil { return fmt.Errorf("failed to initialize database: %s", err) } diff --git a/management/server/telemetry/app_metrics.go b/management/server/telemetry/app_metrics.go index 09deb8127..988f91779 100644 --- a/management/server/telemetry/app_metrics.go +++ b/management/server/telemetry/app_metrics.go @@ -184,10 +184,10 @@ func (appMetrics *defaultAppMetrics) Expose(ctx context.Context, port int, endpo } appMetrics.listener = listener go func() { - err := http.Serve(listener, rootRouter) - if err != nil { - return + if err := http.Serve(listener, rootRouter); err != nil && err != http.ErrServerClosed { + log.WithContext(ctx).Errorf("metrics server error: %v", err) } + log.WithContext(ctx).Info("metrics server stopped") }() log.WithContext(ctx).Infof("enabled application metrics and exposing on http://%s", listener.Addr().String()) @@ -204,7 +204,7 @@ func (appMetrics *defaultAppMetrics) GetMeter() metric2.Meter { func NewDefaultAppMetrics(ctx context.Context) (AppMetrics, error) { exporter, err := prometheus.New() if err != nil { - return nil, err + return nil, fmt.Errorf("failed to create prometheus exporter: %w", err) } provider := metric.NewMeterProvider(metric.WithReader(exporter)) @@ -213,32 +213,32 @@ func NewDefaultAppMetrics(ctx context.Context) (AppMetrics, error) { idpMetrics, err := NewIDPMetrics(ctx, meter) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to initialize IDP metrics: %w", err) } middleware, err := NewMetricsMiddleware(ctx, meter) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to initialize HTTP middleware metrics: %w", err) } grpcMetrics, err := NewGRPCMetrics(ctx, meter) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to initialize gRPC metrics: %w", err) } storeMetrics, err := NewStoreMetrics(ctx, meter) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to initialize store metrics: %w", err) } updateChannelMetrics, err := NewUpdateChannelMetrics(ctx, meter) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to initialize update channel metrics: %w", err) } accountManagerMetrics, err := NewAccountManagerMetrics(ctx, meter) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to initialize account manager metrics: %w", err) } return &defaultAppMetrics{ From d24d8328f95ef3f687302e47992890a6d91729a3 Mon Sep 17 00:00:00 2001 From: Zoltan Papp Date: Fri, 13 Jun 2025 11:04:17 +0200 Subject: [PATCH 2/6] [client] Propagation networks for Android client (#3966) Add networks propagation --- client/android/client.go | 57 ++++++++++++++++++++++++++++++--- client/android/networks.go | 27 ++++++++++++++++ client/android/peer_notifier.go | 15 +++------ 3 files changed, 84 insertions(+), 15 deletions(-) create mode 100644 client/android/networks.go diff --git a/client/android/client.go b/client/android/client.go index 229bcd974..3b8a5bd0f 100644 --- a/client/android/client.go +++ b/client/android/client.go @@ -59,6 +59,8 @@ type Client struct { deviceName string uiVersion string networkChangeListener listener.NetworkChangeListener + + connectClient *internal.ConnectClient } // NewClient instantiate a new Client @@ -106,8 +108,8 @@ func (c *Client) Run(urlOpener URLOpener, dns *DNSList, dnsReadyListener DnsRead // todo do not throw error in case of cancelled context ctx = internal.CtxInitState(ctx) - connectClient := internal.NewConnectClient(ctx, cfg, c.recorder) - return connectClient.RunOnAndroid(c.tunAdapter, c.iFaceDiscover, c.networkChangeListener, dns.items, dnsReadyListener) + c.connectClient = internal.NewConnectClient(ctx, cfg, c.recorder) + return c.connectClient.RunOnAndroid(c.tunAdapter, c.iFaceDiscover, c.networkChangeListener, dns.items, dnsReadyListener) } // RunWithoutLogin we apply this type of run function when the backed has been started without UI (i.e. after reboot). @@ -132,8 +134,8 @@ func (c *Client) RunWithoutLogin(dns *DNSList, dnsReadyListener DnsReadyListener // todo do not throw error in case of cancelled context ctx = internal.CtxInitState(ctx) - connectClient := internal.NewConnectClient(ctx, cfg, c.recorder) - return connectClient.RunOnAndroid(c.tunAdapter, c.iFaceDiscover, c.networkChangeListener, dns.items, dnsReadyListener) + c.connectClient = internal.NewConnectClient(ctx, cfg, c.recorder) + return c.connectClient.RunOnAndroid(c.tunAdapter, c.iFaceDiscover, c.networkChangeListener, dns.items, dnsReadyListener) } // Stop the internal client and free the resources @@ -174,6 +176,53 @@ func (c *Client) PeersList() *PeerInfoArray { return &PeerInfoArray{items: peerInfos} } +func (c *Client) Networks() *NetworkArray { + if c.connectClient == nil { + log.Error("not connected") + return nil + } + + engine := c.connectClient.Engine() + if engine == nil { + log.Error("could not get engine") + return nil + } + + routeManager := engine.GetRouteManager() + if routeManager == nil { + log.Error("could not get route manager") + return nil + } + + networkArray := &NetworkArray{ + items: make([]Network, 0), + } + + for id, routes := range routeManager.GetClientRoutesWithNetID() { + if len(routes) == 0 { + continue + } + + if routes[0].IsDynamic() { + continue + } + + peer, err := c.recorder.GetPeer(routes[0].Peer) + if err != nil { + log.Errorf("could not get peer info for %s: %v", routes[0].Peer, err) + continue + } + network := Network{ + Name: string(id), + Network: routes[0].Network.String(), + Peer: peer.FQDN, + Status: peer.ConnStatus.String(), + } + networkArray.Add(network) + } + return networkArray +} + // OnUpdatedHostDNS update the DNS servers addresses for root zones func (c *Client) OnUpdatedHostDNS(list *DNSList) error { dnsServer, err := dns.GetServerDns() diff --git a/client/android/networks.go b/client/android/networks.go new file mode 100644 index 000000000..aa130420b --- /dev/null +++ b/client/android/networks.go @@ -0,0 +1,27 @@ +//go:build android + +package android + +type Network struct { + Name string + Network string + Peer string + Status string +} + +type NetworkArray struct { + items []Network +} + +func (array *NetworkArray) Add(s Network) *NetworkArray { + array.items = append(array.items, s) + return array +} + +func (array *NetworkArray) Get(i int) *Network { + return &array.items[i] +} + +func (array *NetworkArray) Size() int { + return len(array.items) +} diff --git a/client/android/peer_notifier.go b/client/android/peer_notifier.go index 9f6fcddd6..1f5564c72 100644 --- a/client/android/peer_notifier.go +++ b/client/android/peer_notifier.go @@ -7,30 +7,23 @@ type PeerInfo struct { ConnStatus string // Todo replace to enum } -// PeerInfoCollection made for Java layer to get non default types as collection -type PeerInfoCollection interface { - Add(s string) PeerInfoCollection - Get(i int) string - Size() int -} - -// PeerInfoArray is the implementation of the PeerInfoCollection +// PeerInfoArray is a wrapper of []PeerInfo type PeerInfoArray struct { items []PeerInfo } // Add new PeerInfo to the collection -func (array PeerInfoArray) Add(s PeerInfo) PeerInfoArray { +func (array *PeerInfoArray) Add(s PeerInfo) *PeerInfoArray { array.items = append(array.items, s) return array } // Get return an element of the collection -func (array PeerInfoArray) Get(i int) *PeerInfo { +func (array *PeerInfoArray) Get(i int) *PeerInfo { return &array.items[i] } // Size return with the size of the collection -func (array PeerInfoArray) Size() int { +func (array *PeerInfoArray) Size() int { return len(array.items) } From 04a376539198ad511e17d109d798380b6736897f Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Fri, 13 Jun 2025 20:38:50 +0200 Subject: [PATCH 3/6] [client] Fix unncessary UI updates (#3785) --- client/ui/client_ui.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ui/client_ui.go b/client/ui/client_ui.go index 554cfdc44..ea3349cd1 100644 --- a/client/ui/client_ui.go +++ b/client/ui/client_ui.go @@ -565,7 +565,7 @@ func (s *serviceClient) updateStatus() error { var systrayIconState bool switch { - case status.Status == string(internal.StatusConnected): + case status.Status == string(internal.StatusConnected) && !s.mUp.Disabled(): s.connected = true s.sendNotification = true if s.isUpdateIconActive { From 089d442fb2607d513311492985c0d23df8855442 Mon Sep 17 00:00:00 2001 From: hakansa <43675540+hakansa@users.noreply.github.com> Date: Sat, 14 Jun 2025 00:51:57 +0300 Subject: [PATCH 4/6] [client] Display login popup on session expiration (#3955) This PR implements a feature enhancement to display a login popup when the session expires. Key changes include updating flag handling and client construction to support a new login URL popup, revising login and notification handling logic to use the new popup, and updating status and server-side session state management accordingly. --- client/cmd/status.go | 5 +- client/internal/state.go | 9 +-- client/server/server.go | 13 +++- client/ui/client_ui.go | 131 +++++++++++++++++++++++++++++++-------- client/ui/network.go | 2 - 5 files changed, 124 insertions(+), 36 deletions(-) diff --git a/client/cmd/status.go b/client/cmd/status.go index e466f73ab..a85ee925e 100644 --- a/client/cmd/status.go +++ b/client/cmd/status.go @@ -69,7 +69,10 @@ func statusFunc(cmd *cobra.Command, args []string) error { return err } - if resp.GetStatus() == string(internal.StatusNeedsLogin) || resp.GetStatus() == string(internal.StatusLoginFailed) { + status := resp.GetStatus() + + if status == string(internal.StatusNeedsLogin) || status == string(internal.StatusLoginFailed) || + status == string(internal.StatusSessionExpired) { cmd.Printf("Daemon status: %s\n\n"+ "Run UP command to log in with SSO (interactive login):\n\n"+ " netbird up \n\n"+ diff --git a/client/internal/state.go b/client/internal/state.go index 4ae99d944..041cb73f8 100644 --- a/client/internal/state.go +++ b/client/internal/state.go @@ -10,10 +10,11 @@ type StatusType string const ( StatusIdle StatusType = "Idle" - StatusConnecting StatusType = "Connecting" - StatusConnected StatusType = "Connected" - StatusNeedsLogin StatusType = "NeedsLogin" - StatusLoginFailed StatusType = "LoginFailed" + StatusConnecting StatusType = "Connecting" + StatusConnected StatusType = "Connected" + StatusNeedsLogin StatusType = "NeedsLogin" + StatusLoginFailed StatusType = "LoginFailed" + StatusSessionExpired StatusType = "SessionExpired" ) // CtxInitState setup context state into the context tree. diff --git a/client/server/server.go b/client/server/server.go index 72837b59d..31a437c99 100644 --- a/client/server/server.go +++ b/client/server/server.go @@ -8,6 +8,7 @@ import ( "runtime" "strconv" "sync" + "sync/atomic" "time" "github.com/cenkalti/backoff/v4" @@ -66,6 +67,7 @@ type Server struct { lastProbe time.Time persistNetworkMap bool + isSessionActive atomic.Bool } type oauthAuthFlow struct { @@ -567,9 +569,6 @@ func (s *Server) WaitSSOLogin(callerCtx context.Context, msg *proto.WaitSSOLogin tokenInfo, err := s.oauthAuthFlow.flow.WaitToken(waitCTX, flowInfo) if err != nil { - if err == context.Canceled { - return nil, nil //nolint:nilnil - } s.mutex.Lock() s.oauthAuthFlow.expiresAt = time.Now() s.mutex.Unlock() @@ -640,6 +639,7 @@ func (s *Server) Up(callerCtx context.Context, _ *proto.UpRequest) (*proto.UpRes for { select { case <-runningChan: + s.isSessionActive.Store(true) return &proto.UpResponse{}, nil case <-callerCtx.Done(): log.Debug("context done, stopping the wait for engine to become ready") @@ -668,6 +668,7 @@ func (s *Server) Down(ctx context.Context, _ *proto.DownRequest) (*proto.DownRes log.Errorf("failed to shut down properly: %v", err) return nil, err } + s.isSessionActive.Store(false) state := internal.CtxGetState(s.rootCtx) state.Set(internal.StatusIdle) @@ -694,6 +695,12 @@ func (s *Server) Status( return nil, err } + if status == internal.StatusNeedsLogin && s.isSessionActive.Load() { + log.Debug("status requested while session is active, returning SessionExpired") + status = internal.StatusSessionExpired + s.isSessionActive.Store(false) + } + statusResponse := proto.StatusResponse{Status: string(status), DaemonVersion: version.NetbirdVersion()} s.statusRecorder.UpdateManagementAddress(s.config.ManagementURL.String()) diff --git a/client/ui/client_ui.go b/client/ui/client_ui.go index ea3349cd1..a1ca4120f 100644 --- a/client/ui/client_ui.go +++ b/client/ui/client_ui.go @@ -20,7 +20,10 @@ import ( "fyne.io/fyne/v2" "fyne.io/fyne/v2/app" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/dialog" + "fyne.io/fyne/v2/layout" "fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/widget" "fyne.io/systray" @@ -51,7 +54,7 @@ const ( ) func main() { - daemonAddr, showSettings, showNetworks, showDebug, errorMsg, saveLogsInFile := parseFlags() + daemonAddr, showSettings, showNetworks, showLoginURL, showDebug, errorMsg, saveLogsInFile := parseFlags() // Initialize file logging if needed. var logFile string @@ -77,13 +80,13 @@ func main() { } // Create the service client (this also builds the settings or networks UI if requested). - client := newServiceClient(daemonAddr, logFile, a, showSettings, showNetworks, showDebug) + client := newServiceClient(daemonAddr, logFile, a, showSettings, showNetworks, showLoginURL, showDebug) // Watch for theme/settings changes to update the icon. go watchSettingsChanges(a, client) // Run in window mode if any UI flag was set. - if showSettings || showNetworks || showDebug { + if showSettings || showNetworks || showDebug || showLoginURL { a.Run() return } @@ -104,7 +107,7 @@ func main() { } // parseFlags reads and returns all needed command-line flags. -func parseFlags() (daemonAddr string, showSettings, showNetworks, showDebug bool, errorMsg string, saveLogsInFile bool) { +func parseFlags() (daemonAddr string, showSettings, showNetworks, showLoginURL, showDebug bool, errorMsg string, saveLogsInFile bool) { defaultDaemonAddr := "unix:///var/run/netbird.sock" if runtime.GOOS == "windows" { defaultDaemonAddr = "tcp://127.0.0.1:41731" @@ -112,6 +115,7 @@ func parseFlags() (daemonAddr string, showSettings, showNetworks, showDebug bool flag.StringVar(&daemonAddr, "daemon-addr", defaultDaemonAddr, "Daemon service address to serve CLI requests [unix|tcp]://[path|host:port]") flag.BoolVar(&showSettings, "settings", false, "run settings window") flag.BoolVar(&showNetworks, "networks", false, "run networks window") + flag.BoolVar(&showLoginURL, "login-url", false, "show login URL in a popup window") flag.BoolVar(&showDebug, "debug", false, "run debug window") flag.StringVar(&errorMsg, "error-msg", "", "displays an error message window") flag.BoolVar(&saveLogsInFile, "use-log-file", false, fmt.Sprintf("save logs in a file: %s/netbird-ui-PID.log", os.TempDir())) @@ -253,6 +257,7 @@ type serviceClient struct { exitNodeStates []exitNodeState mExitNodeDeselectAll *systray.MenuItem logFile string + wLoginURL fyne.Window } type menuHandler struct { @@ -263,7 +268,7 @@ type menuHandler struct { // newServiceClient instance constructor // // This constructor also builds the UI elements for the settings window. -func newServiceClient(addr string, logFile string, a fyne.App, showSettings bool, showNetworks bool, showDebug bool) *serviceClient { +func newServiceClient(addr string, logFile string, a fyne.App, showSettings bool, showNetworks bool, showLoginURL bool, showDebug bool) *serviceClient { ctx, cancel := context.WithCancel(context.Background()) s := &serviceClient{ ctx: ctx, @@ -286,6 +291,8 @@ func newServiceClient(addr string, logFile string, a fyne.App, showSettings bool s.showSettingsUI() case showNetworks: s.showNetworksUI() + case showLoginURL: + s.showLoginURL() case showDebug: s.showDebugUI() } @@ -445,11 +452,11 @@ func (s *serviceClient) getSettingsForm() *widget.Form { } } -func (s *serviceClient) login() error { +func (s *serviceClient) login(openURL bool) (*proto.LoginResponse, error) { conn, err := s.getSrvClient(defaultFailTimeout) if err != nil { log.Errorf("get client: %v", err) - return err + return nil, err } loginResp, err := conn.Login(s.ctx, &proto.LoginRequest{ @@ -457,24 +464,24 @@ func (s *serviceClient) login() error { }) if err != nil { log.Errorf("login to management URL with: %v", err) - return err + return nil, err } - if loginResp.NeedsSSOLogin { + if loginResp.NeedsSSOLogin && openURL { err = open.Run(loginResp.VerificationURIComplete) if err != nil { log.Errorf("opening the verification uri in the browser failed: %v", err) - return err + return nil, err } _, err = conn.WaitSSOLogin(s.ctx, &proto.WaitSSOLoginRequest{UserCode: loginResp.UserCode}) if err != nil { log.Errorf("waiting sso login failed with: %v", err) - return err + return nil, err } } - return nil + return loginResp, nil } func (s *serviceClient) menuUpClick() error { @@ -486,7 +493,7 @@ func (s *serviceClient) menuUpClick() error { return err } - err = s.login() + _, err = s.login(true) if err != nil { log.Errorf("login failed with: %v", err) return err @@ -558,7 +565,7 @@ func (s *serviceClient) updateStatus() error { defer s.updateIndicationLock.Unlock() // notify the user when the session has expired - if status.Status == string(internal.StatusNeedsLogin) { + if status.Status == string(internal.StatusSessionExpired) { s.onSessionExpire() } @@ -732,7 +739,6 @@ func (s *serviceClient) onTrayReady() { go s.eventHandler.listen(s.ctx) } - func (s *serviceClient) attachOutput(cmd *exec.Cmd) *os.File { if s.logFile == "" { // attach child's streams to parent's streams @@ -871,17 +877,9 @@ func (s *serviceClient) onUpdateAvailable() { // onSessionExpire sends a notification to the user when the session expires. func (s *serviceClient) onSessionExpire() { + s.sendNotification = true if s.sendNotification { - title := "Connection session expired" - if runtime.GOOS == "darwin" { - title = "NetBird connection session expired" - } - s.app.SendNotification( - fyne.NewNotification( - title, - "Please re-authenticate to connect to the network", - ), - ) + s.eventHandler.runSelfCommand("login-url", "true") s.sendNotification = false } } @@ -955,9 +953,9 @@ func (s *serviceClient) updateConfig() error { ServerSSHAllowed: &sshAllowed, RosenpassEnabled: &rosenpassEnabled, DisableAutoConnect: &disableAutoStart, + DisableNotifications: ¬ificationsDisabled, LazyConnectionEnabled: &lazyConnectionEnabled, BlockInbound: &blockInbound, - DisableNotifications: ¬ificationsDisabled, } if err := s.restartClient(&loginRequest); err != nil { @@ -991,6 +989,87 @@ func (s *serviceClient) restartClient(loginRequest *proto.LoginRequest) error { return nil } +// showLoginURL creates a borderless window styled like a pop-up in the top-right corner using s.wLoginURL. +func (s *serviceClient) showLoginURL() { + + resp, err := s.login(false) + if err != nil { + log.Errorf("failed to fetch login URL: %v", err) + return + } + verificationURL := resp.VerificationURIComplete + if verificationURL == "" { + verificationURL = resp.VerificationURI + } + + if verificationURL == "" { + log.Error("no verification URL provided in the login response") + return + } + + resIcon := fyne.NewStaticResource("netbird.png", iconAbout) + + if s.wLoginURL == nil { + s.wLoginURL = s.app.NewWindow("NetBird Session Expired") + s.wLoginURL.Resize(fyne.NewSize(400, 200)) + s.wLoginURL.SetIcon(resIcon) + } + // add a description label + label := widget.NewLabel("Your NetBird session has expired.\nPlease re-authenticate to continue using NetBird.") + + btn := widget.NewButtonWithIcon("Re-authenticate", theme.ViewRefreshIcon(), func() { + + conn, err := s.getSrvClient(defaultFailTimeout) + if err != nil { + log.Errorf("get client: %v", err) + return + } + + if err := openURL(verificationURL); err != nil { + log.Errorf("failed to open login URL: %v", err) + return + } + + _, err = conn.WaitSSOLogin(s.ctx, &proto.WaitSSOLoginRequest{UserCode: resp.UserCode}) + if err != nil { + log.Errorf("Waiting sso login failed with: %v", err) + label.SetText("Waiting login failed, please create \na debug bundle in the settings and contact support.") + return + } + + label.SetText("Re-authentication successful.\nReconnecting") + time.Sleep(300 * time.Millisecond) + _, err = conn.Up(s.ctx, &proto.UpRequest{}) + if err != nil { + label.SetText("Reconnecting failed, please create \na debug bundle in the settings and contact support.") + log.Errorf("Reconnecting failed with: %v", err) + return + } + + label.SetText("Connection successful.\nClosing this window.") + time.Sleep(time.Second) + + s.wLoginURL.Close() + }) + + img := canvas.NewImageFromResource(resIcon) + img.FillMode = canvas.ImageFillContain + img.SetMinSize(fyne.NewSize(64, 64)) + img.Resize(fyne.NewSize(64, 64)) + + // center the content vertically + content := container.NewVBox( + layout.NewSpacer(), + img, + label, + btn, + layout.NewSpacer(), + ) + s.wLoginURL.SetContent(container.NewCenter(content)) + + s.wLoginURL.Show() +} + func openURL(url string) error { var err error switch runtime.GOOS { diff --git a/client/ui/network.go b/client/ui/network.go index b3748a89d..fb73efd7b 100644 --- a/client/ui/network.go +++ b/client/ui/network.go @@ -358,8 +358,6 @@ func (s *serviceClient) updateExitNodes() { } else { s.mExitNode.Disable() } - - log.Debugf("Exit nodes updated: %d", len(s.mExitNodeItems)) } func (s *serviceClient) recreateExitNodeMenu(exitNodes []*proto.Network) { From f595057a0be7473ab058180b3f11abed6c71b920 Mon Sep 17 00:00:00 2001 From: Philippe Vaucher Date: Sat, 14 Jun 2025 00:08:34 +0200 Subject: [PATCH 5/6] [signal] Set flags from environment variables (#3972) --- signal/cmd/env.go | 35 +++++++++++++++++++++++++++++++++++ signal/cmd/run.go | 1 + 2 files changed, 36 insertions(+) create mode 100644 signal/cmd/env.go diff --git a/signal/cmd/env.go b/signal/cmd/env.go new file mode 100644 index 000000000..3c15ebe1f --- /dev/null +++ b/signal/cmd/env.go @@ -0,0 +1,35 @@ +package cmd + +import ( + "os" + "strings" + + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +// setFlagsFromEnvVars reads and updates flag values from environment variables with prefix NB_ +func setFlagsFromEnvVars(cmd *cobra.Command) { + flags := cmd.PersistentFlags() + flags.VisitAll(func(f *pflag.Flag) { + newEnvVar := flagNameToEnvVar(f.Name, "NB_") + value, present := os.LookupEnv(newEnvVar) + if !present { + return + } + + err := flags.Set(f.Name, value) + if err != nil { + log.Infof("unable to configure flag %s using variable %s, err: %v", f.Name, newEnvVar, err) + } + }) +} + +// flagNameToEnvVar converts flag name to environment var name adding a prefix, +// replacing dashes and making all uppercase (e.g. setup-keys is converted to NB_SETUP_KEYS according to the input prefix) +func flagNameToEnvVar(cmdFlag string, prefix string) string { + parsed := strings.ReplaceAll(cmdFlag, "-", "_") + upper := strings.ToUpper(parsed) + return prefix + upper +} diff --git a/signal/cmd/run.go b/signal/cmd/run.go index 3a671a848..39bc8331f 100644 --- a/signal/cmd/run.go +++ b/signal/cmd/run.go @@ -303,4 +303,5 @@ func init() { runCmd.Flags().StringVar(&signalLetsencryptDomain, "letsencrypt-domain", "", "a domain to issue Let's Encrypt certificate for. Enables TLS using Let's Encrypt. Will fetch and renew certificate, and run the server with TLS") runCmd.Flags().StringVar(&signalCertFile, "cert-file", "", "Location of your SSL certificate. Can be used when you have an existing certificate and don't want a new certificate be generated automatically. If letsencrypt-domain is specified this property has no effect") runCmd.Flags().StringVar(&signalCertKey, "cert-key", "", "Location of your SSL certificate private key. Can be used when you have an existing certificate and don't want a new certificate be generated automatically. If letsencrypt-domain is specified this property has no effect") + setFlagsFromEnvVars(runCmd) } From b1247a14ba93294ee4484cc01476234bfd686894 Mon Sep 17 00:00:00 2001 From: Maycon Santos Date: Sat, 14 Jun 2025 12:24:16 +0100 Subject: [PATCH 6/6] [management] Use xID for setup key IDs to avoid id collisions (#3977) This PR addresses potential ID collisions by switching the setup key ID generation from a hash-based approach to using xid-generated IDs. Replace the hash function with xid.New().String() Remove obsolete imports and the Hash() function --- management/server/setupkey_test.go | 11 +++++------ management/server/types/setupkey.go | 14 ++------------ 2 files changed, 7 insertions(+), 18 deletions(-) diff --git a/management/server/setupkey_test.go b/management/server/setupkey_test.go index a561de40d..cecf55200 100644 --- a/management/server/setupkey_test.go +++ b/management/server/setupkey_test.go @@ -5,7 +5,6 @@ import ( "crypto/sha256" "encoding/base64" "fmt" - "strconv" "strings" "testing" "time" @@ -182,7 +181,7 @@ func TestDefaultAccountManager_CreateSetupKey(t *testing.T) { } assertKey(t, key, tCase.expectedKeyName, false, tCase.expectedType, tCase.expectedUsedTimes, - tCase.expectedCreatedAt, tCase.expectedExpiresAt, strconv.Itoa(int(types.Hash(key.Key))), + tCase.expectedCreatedAt, tCase.expectedExpiresAt, key.Id, tCase.expectedUpdatedAt, tCase.expectedGroups, false) // check the corresponding events that should have been generated @@ -258,10 +257,10 @@ func TestGenerateDefaultSetupKey(t *testing.T) { expectedExpiresAt := time.Now().UTC().Add(24 * 30 * time.Hour) var expectedAutoGroups []string - key, plainKey := types.GenerateDefaultSetupKey() + key, _ := types.GenerateDefaultSetupKey() assertKey(t, key, expectedName, expectedRevoke, expectedType, expectedUsedTimes, expectedCreatedAt, - expectedExpiresAt, strconv.Itoa(int(types.Hash(plainKey))), expectedUpdatedAt, expectedAutoGroups, true) + expectedExpiresAt, key.Id, expectedUpdatedAt, expectedAutoGroups, true) } @@ -275,10 +274,10 @@ func TestGenerateSetupKey(t *testing.T) { expectedUpdatedAt := time.Now().UTC() var expectedAutoGroups []string - key, plain := types.GenerateSetupKey(expectedName, types.SetupKeyOneOff, time.Hour, []string{}, types.SetupKeyUnlimitedUsage, false, false) + key, _ := types.GenerateSetupKey(expectedName, types.SetupKeyOneOff, time.Hour, []string{}, types.SetupKeyUnlimitedUsage, false, false) assertKey(t, key, expectedName, expectedRevoke, expectedType, expectedUsedTimes, expectedCreatedAt, - expectedExpiresAt, strconv.Itoa(int(types.Hash(plain))), expectedUpdatedAt, expectedAutoGroups, true) + expectedExpiresAt, key.Id, expectedUpdatedAt, expectedAutoGroups, true) } diff --git a/management/server/types/setupkey.go b/management/server/types/setupkey.go index ab8e46bea..69b381ae5 100644 --- a/management/server/types/setupkey.go +++ b/management/server/types/setupkey.go @@ -3,13 +3,12 @@ package types import ( "crypto/sha256" b64 "encoding/base64" - "hash/fnv" - "strconv" "strings" "time" "unicode/utf8" "github.com/google/uuid" + "github.com/rs/xid" "github.com/netbirdio/netbird/management/server/util" ) @@ -170,7 +169,7 @@ func GenerateSetupKey(name string, t SetupKeyType, validFor time.Duration, autoG encodedHashedKey := b64.StdEncoding.EncodeToString(hashedKey[:]) return &SetupKey{ - Id: strconv.Itoa(int(Hash(key))), + Id: xid.New().String(), Key: encodedHashedKey, KeySecret: HiddenKey(key, 4), Name: name, @@ -192,12 +191,3 @@ func GenerateDefaultSetupKey() (*SetupKey, string) { return GenerateSetupKey(DefaultSetupKeyName, SetupKeyReusable, DefaultSetupKeyDuration, []string{}, SetupKeyUnlimitedUsage, false, false) } - -func Hash(s string) uint32 { - h := fnv.New32a() - _, err := h.Write([]byte(s)) - if err != nil { - panic(err) - } - return h.Sum32() -}