//go:build !(linux && 386) package main import ( "context" _ "embed" "errors" "flag" "fmt" "os" "os/exec" "path" "runtime" "strconv" "strings" "sync" "time" "unicode" "fyne.io/fyne/v2" "fyne.io/fyne/v2/app" "fyne.io/fyne/v2/dialog" "fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/widget" "fyne.io/systray" "github.com/cenkalti/backoff/v4" log "github.com/sirupsen/logrus" "github.com/skratchdot/open-golang/open" "golang.zx2c4.com/wireguard/wgctrl/wgtypes" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" "github.com/netbirdio/netbird/client/internal" "github.com/netbirdio/netbird/client/proto" "github.com/netbirdio/netbird/client/ui/desktop" "github.com/netbirdio/netbird/client/ui/event" "github.com/netbirdio/netbird/client/ui/process" "github.com/netbirdio/netbird/util" "github.com/netbirdio/netbird/version" ) const ( defaultFailTimeout = 3 * time.Second failFastTimeout = time.Second ) const ( censoredPreSharedKey = "**********" ) func main() { flags := parseFlags() // Initialize file logging if needed. var logFile string if flags.saveLogsInFile { file, err := initLogFile() if err != nil { log.Errorf("error while initializing log: %v", err) return } logFile = file } else { _ = util.InitLog("trace", "console") } // Create the Fyne application. a := app.NewWithID("NetBird") a.SetIcon(fyne.NewStaticResource("netbird", iconDisconnected)) // Show error message window if needed. if flags.errorMsg != "" { showErrorMessage(flags.errorMsg) return } // Create the service client (this also builds the settings or networks UI if requested). client := newServiceClient(&newServiceClientArgs{ addr: flags.daemonAddr, logFile: logFile, app: a, showSettings: flags.showSettings, showNetworks: flags.showNetworks, showDebug: flags.showDebug, showProfiles: flags.showProfiles, }) // Watch for theme/settings changes to update the icon. go watchSettingsChanges(a, client) // Run in window mode if any UI flag was set. if flags.showSettings || flags.showNetworks || flags.showDebug || flags.showProfiles { a.Run() return } // Check for another running process. running, err := process.IsAnotherProcessRunning() if err != nil { log.Errorf("error while checking process: %v", err) return } if running { log.Warn("another process is running") return } client.setDefaultFonts() systray.Run(client.onTrayReady, client.onTrayExit) } type cliFlags struct { daemonAddr string showSettings bool showNetworks bool showProfiles bool showDebug bool errorMsg string saveLogsInFile bool } // parseFlags reads and returns all needed command-line flags. func parseFlags() *cliFlags { var flags cliFlags defaultDaemonAddr := "unix:///var/run/netbird.sock" if runtime.GOOS == "windows" { defaultDaemonAddr = "tcp://127.0.0.1:41731" } flag.StringVar(&flags.daemonAddr, "daemon-addr", defaultDaemonAddr, "Daemon service address to serve CLI requests [unix|tcp]://[path|host:port]") flag.BoolVar(&flags.showSettings, "settings", false, "run settings window") flag.BoolVar(&flags.showNetworks, "networks", false, "run networks window") flag.BoolVar(&flags.showProfiles, "profiles", false, "run profiles window") flag.BoolVar(&flags.showDebug, "debug", false, "run debug window") flag.StringVar(&flags.errorMsg, "error-msg", "", "displays an error message window") flag.BoolVar(&flags.saveLogsInFile, "use-log-file", false, fmt.Sprintf("save logs in a file: %s/netbird-ui-PID.log", os.TempDir())) flag.Parse() return &flags } // initLogFile initializes logging into a file. func initLogFile() (string, error) { logFile := path.Join(os.TempDir(), fmt.Sprintf("netbird-ui-%d.log", os.Getpid())) return logFile, util.InitLog("trace", logFile) } // watchSettingsChanges listens for Fyne theme/settings changes and updates the client icon. func watchSettingsChanges(a fyne.App, client *serviceClient) { settingsChangeChan := make(chan fyne.Settings) a.Settings().AddChangeListener(settingsChangeChan) for range settingsChangeChan { client.updateIcon() } } // showErrorMessage displays an error message in a simple window. func showErrorMessage(msg string) { a := app.New() w := a.NewWindow("NetBird Error") label := widget.NewLabel(msg) label.Wrapping = fyne.TextWrapWord w.SetContent(label) w.Resize(fyne.NewSize(400, 100)) w.Show() a.Run() } //go:embed assets/netbird-systemtray-connected-macos.png var iconConnectedMacOS []byte //go:embed assets/netbird-systemtray-disconnected-macos.png var iconDisconnectedMacOS []byte //go:embed assets/netbird-systemtray-update-disconnected-macos.png var iconUpdateDisconnectedMacOS []byte //go:embed assets/netbird-systemtray-update-connected-macos.png var iconUpdateConnectedMacOS []byte //go:embed assets/netbird-systemtray-connecting-macos.png var iconConnectingMacOS []byte //go:embed assets/netbird-systemtray-error-macos.png var iconErrorMacOS []byte type serviceClient struct { ctx context.Context cancel context.CancelFunc addr string conn proto.DaemonServiceClient icAbout []byte icConnected []byte icDisconnected []byte icUpdateConnected []byte icUpdateDisconnected []byte icConnecting []byte icError []byte // systray menu items mStatus *systray.MenuItem mProfileName *systray.MenuItem mUp *systray.MenuItem mDown *systray.MenuItem mSettings *systray.MenuItem profileMenu *systray.MenuItem mProfiles *systray.MenuItem mAbout *systray.MenuItem mGitHub *systray.MenuItem mVersionUI *systray.MenuItem mVersionDaemon *systray.MenuItem mUpdate *systray.MenuItem mQuit *systray.MenuItem mNetworks *systray.MenuItem mAllowSSH *systray.MenuItem mAutoConnect *systray.MenuItem mEnableRosenpass *systray.MenuItem mLazyConnEnabled *systray.MenuItem mNotifications *systray.MenuItem mAdvancedSettings *systray.MenuItem mCreateDebugBundle *systray.MenuItem mExitNode *systray.MenuItem // application with main windows. app fyne.App wSettings fyne.Window showAdvancedSettings bool sendNotification bool // input elements for settings form iMngURL *widget.Entry iAdminURL *widget.Entry iConfigFile *widget.Entry iLogFile *widget.Entry iPreSharedKey *widget.Entry iInterfaceName *widget.Entry iInterfacePort *widget.Entry // switch elements for settings form sRosenpassPermissive *widget.Check // observable settings over corresponding iMngURL and iPreSharedKey values. managementURL string preSharedKey string adminURL string RosenpassPermissive bool interfaceName string interfacePort int connected bool update *version.Update daemonVersion string updateIndicationLock sync.Mutex isUpdateIconActive bool showNetworks bool wNetworks fyne.Window wProfiles fyne.Window eventManager *event.Manager exitNodeMu sync.Mutex mExitNodeItems []menuHandler logFile string } type menuHandler struct { *systray.MenuItem cancel context.CancelFunc } type newServiceClientArgs struct { addr string logFile string app fyne.App showSettings bool showNetworks bool showDebug bool showProfiles bool } // newServiceClient instance constructor // // This constructor also builds the UI elements for the settings window. func newServiceClient(args *newServiceClientArgs) *serviceClient { ctx, cancel := context.WithCancel(context.Background()) s := &serviceClient{ ctx: ctx, cancel: cancel, addr: args.addr, app: args.app, logFile: args.logFile, sendNotification: false, showAdvancedSettings: args.showSettings, showNetworks: args.showNetworks, update: version.NewUpdate(), } s.setNewIcons() switch { case args.showSettings: s.showSettingsUI() case args.showNetworks: s.showNetworksUI() case args.showDebug: s.showDebugUI() case args.showProfiles: s.showProfilesUI() } return s } func (s *serviceClient) setNewIcons() { s.icAbout = iconAbout if s.app.Settings().ThemeVariant() == theme.VariantDark { s.icConnected = iconConnectedDark s.icDisconnected = iconDisconnected s.icUpdateConnected = iconUpdateConnectedDark s.icUpdateDisconnected = iconUpdateDisconnectedDark s.icConnecting = iconConnectingDark s.icError = iconErrorDark } else { s.icConnected = iconConnected s.icDisconnected = iconDisconnected s.icUpdateConnected = iconUpdateConnected s.icUpdateDisconnected = iconUpdateDisconnected s.icConnecting = iconConnecting s.icError = iconError } } func (s *serviceClient) updateIcon() { s.setNewIcons() s.updateIndicationLock.Lock() if s.connected { if s.isUpdateIconActive { systray.SetTemplateIcon(iconUpdateConnectedMacOS, s.icUpdateConnected) } else { systray.SetTemplateIcon(iconConnectedMacOS, s.icConnected) } } else { if s.isUpdateIconActive { systray.SetTemplateIcon(iconUpdateDisconnectedMacOS, s.icUpdateDisconnected) } else { systray.SetTemplateIcon(iconDisconnectedMacOS, s.icDisconnected) } } s.updateIndicationLock.Unlock() } func (s *serviceClient) showSettingsUI() { // add settings window UI elements. s.wSettings = s.app.NewWindow("NetBird Settings") s.wSettings.SetOnClosed(s.cancel) s.iMngURL = widget.NewEntry() s.iAdminURL = widget.NewEntry() s.iConfigFile = widget.NewEntry() s.iConfigFile.Disable() s.iLogFile = widget.NewEntry() s.iLogFile.Disable() s.iPreSharedKey = widget.NewPasswordEntry() s.iInterfaceName = widget.NewEntry() s.iInterfacePort = widget.NewEntry() s.sRosenpassPermissive = widget.NewCheck("Enable Rosenpass permissive mode", nil) s.wSettings.SetContent(s.getSettingsForm()) s.wSettings.Resize(fyne.NewSize(600, 400)) s.wSettings.SetFixedSize(true) s.getSrvConfig() s.wSettings.Show() } // getSettingsForm to embed it into settings window. func (s *serviceClient) getSettingsForm() *widget.Form { return &widget.Form{ Items: []*widget.FormItem{ {Text: "Quantum-Resistance", Widget: s.sRosenpassPermissive}, {Text: "Interface Name", Widget: s.iInterfaceName}, {Text: "Interface Port", Widget: s.iInterfacePort}, {Text: "Management URL", Widget: s.iMngURL}, {Text: "Admin URL", Widget: s.iAdminURL}, {Text: "Pre-shared Key", Widget: s.iPreSharedKey}, {Text: "Config File", Widget: s.iConfigFile}, {Text: "Log File", Widget: s.iLogFile}, }, SubmitText: "Save", OnSubmit: func() { if s.iPreSharedKey.Text != "" && s.iPreSharedKey.Text != censoredPreSharedKey { // validate preSharedKey if it added if _, err := wgtypes.ParseKey(s.iPreSharedKey.Text); err != nil { dialog.ShowError(fmt.Errorf("Invalid Pre-shared Key Value"), s.wSettings) return } } port, err := strconv.ParseInt(s.iInterfacePort.Text, 10, 64) if err != nil { dialog.ShowError(errors.New("Invalid interface port"), s.wSettings) return } iAdminURL := strings.TrimSpace(s.iAdminURL.Text) iMngURL := strings.TrimSpace(s.iMngURL.Text) defer s.wSettings.Close() // If the management URL, pre-shared key, admin URL, Rosenpass permissive mode, // interface name, or interface port have changed, we attempt to re-login with the new settings. if s.managementURL != iMngURL || s.preSharedKey != s.iPreSharedKey.Text || s.adminURL != iAdminURL || s.RosenpassPermissive != s.sRosenpassPermissive.Checked || s.interfaceName != s.iInterfaceName.Text || s.interfacePort != int(port) { s.managementURL = iMngURL s.preSharedKey = s.iPreSharedKey.Text s.adminURL = iAdminURL loginRequest := proto.LoginRequest{ ManagementUrl: iMngURL, AdminURL: iAdminURL, IsUnixDesktopClient: runtime.GOOS == "linux" || runtime.GOOS == "freebsd", RosenpassPermissive: &s.sRosenpassPermissive.Checked, InterfaceName: &s.iInterfaceName.Text, WireguardPort: &port, } if s.iPreSharedKey.Text != censoredPreSharedKey { loginRequest.OptionalPreSharedKey = &s.iPreSharedKey.Text } if err := s.restartClient(&loginRequest); err != nil { log.Errorf("restarting client connection: %v", err) return } } }, OnCancel: func() { s.wSettings.Close() }, } } func (s *serviceClient) login() error { conn, err := s.getSrvClient(defaultFailTimeout) if err != nil { log.Errorf("get client: %v", err) return err } loginResp, err := conn.Login(s.ctx, &proto.LoginRequest{ IsUnixDesktopClient: runtime.GOOS == "linux" || runtime.GOOS == "freebsd", }) if err != nil { log.Errorf("login to management URL with: %v", err) return err } if loginResp.NeedsSSOLogin { err = open.Run(loginResp.VerificationURIComplete) if err != nil { log.Errorf("opening the verification uri in the browser failed: %v", err) return 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 } func (s *serviceClient) menuUpClick() error { systray.SetTemplateIcon(iconConnectingMacOS, s.icConnecting) conn, err := s.getSrvClient(defaultFailTimeout) if err != nil { systray.SetTemplateIcon(iconErrorMacOS, s.icError) log.Errorf("get client: %v", err) return err } err = s.login() if err != nil { log.Errorf("login failed with: %v", err) return err } status, err := conn.Status(s.ctx, &proto.StatusRequest{}) if err != nil { log.Errorf("get service status: %v", err) return err } if status.Status == string(internal.StatusConnected) { log.Warnf("already connected") return nil } if _, err := s.conn.Up(s.ctx, &proto.UpRequest{}); err != nil { log.Errorf("up service: %v", err) return err } return nil } func (s *serviceClient) menuDownClick() error { systray.SetTemplateIcon(iconConnectingMacOS, s.icConnecting) conn, err := s.getSrvClient(defaultFailTimeout) if err != nil { log.Errorf("get client: %v", err) return err } status, err := conn.Status(s.ctx, &proto.StatusRequest{}) if err != nil { log.Errorf("get service status: %v", err) return err } if status.Status != string(internal.StatusConnected) && status.Status != string(internal.StatusConnecting) { log.Warnf("already down") return nil } if _, err := s.conn.Down(s.ctx, &proto.DownRequest{}); err != nil { log.Errorf("down service: %v", err) return err } return nil } func (s *serviceClient) updateStatus() error { conn, err := s.getSrvClient(defaultFailTimeout) if err != nil { return err } err = backoff.Retry(func() error { status, err := conn.Status(s.ctx, &proto.StatusRequest{}) if err != nil { log.Errorf("get service status: %v", err) if s.connected { s.app.SendNotification(fyne.NewNotification("Error", "Connection to service lost")) } s.setDisconnectedStatus() return err } s.updateIndicationLock.Lock() defer s.updateIndicationLock.Unlock() // notify the user when the session has expired if status.Status == string(internal.StatusNeedsLogin) { s.onSessionExpire() } var systrayIconState bool switch { case status.Status == string(internal.StatusConnected): s.connected = true s.sendNotification = true if s.isUpdateIconActive { systray.SetTemplateIcon(iconUpdateConnectedMacOS, s.icUpdateConnected) } else { systray.SetTemplateIcon(iconConnectedMacOS, s.icConnected) } systray.SetTooltip("NetBird (Connected)") s.mStatus.SetTitle("Connected") s.mUp.Disable() s.mDown.Enable() s.mNetworks.Enable() go s.updateExitNodes() systrayIconState = true case status.Status == string(internal.StatusConnecting): s.setConnectingStatus() case status.Status != string(internal.StatusConnected) && s.mUp.Disabled(): s.setDisconnectedStatus() systrayIconState = false } // the updater struct notify by the upgrades available only, but if meanwhile the daemon has successfully // updated must reset the mUpdate visibility state if s.daemonVersion != status.DaemonVersion { s.mUpdate.Hide() s.daemonVersion = status.DaemonVersion s.isUpdateIconActive = s.update.SetDaemonVersion(status.DaemonVersion) if !s.isUpdateIconActive { if systrayIconState { systray.SetTemplateIcon(iconConnectedMacOS, s.icConnected) } else { systray.SetTemplateIcon(iconDisconnectedMacOS, s.icDisconnected) } } daemonVersionTitle := normalizedVersion(s.daemonVersion) s.mVersionDaemon.SetTitle(fmt.Sprintf("Daemon: %s", daemonVersionTitle)) s.mVersionDaemon.SetTooltip(fmt.Sprintf("Daemon version: %s", daemonVersionTitle)) s.mVersionDaemon.Show() } return nil }, &backoff.ExponentialBackOff{ InitialInterval: time.Second, RandomizationFactor: backoff.DefaultRandomizationFactor, Multiplier: backoff.DefaultMultiplier, MaxInterval: 300 * time.Millisecond, MaxElapsedTime: 2 * time.Second, Stop: backoff.Stop, Clock: backoff.SystemClock, }) if err != nil { return err } return nil } func (s *serviceClient) setDisconnectedStatus() { s.connected = false if s.isUpdateIconActive { systray.SetTemplateIcon(iconUpdateDisconnectedMacOS, s.icUpdateDisconnected) } else { systray.SetTemplateIcon(iconDisconnectedMacOS, s.icDisconnected) } systray.SetTooltip("NetBird (Disconnected)") s.mStatus.SetTitle("Disconnected") s.mDown.Disable() s.mUp.Enable() s.mNetworks.Disable() s.mExitNode.Disable() go s.updateExitNodes() } func (s *serviceClient) setConnectingStatus() { s.connected = false systray.SetTemplateIcon(iconConnectingMacOS, s.icConnecting) systray.SetTooltip("NetBird (Connecting)") s.mStatus.SetTitle("Connecting") s.mUp.Disable() s.mDown.Enable() s.mNetworks.Disable() s.mExitNode.Disable() } func (s *serviceClient) onTrayReady() { systray.SetTemplateIcon(iconDisconnectedMacOS, s.icDisconnected) systray.SetTooltip("NetBird") // setup systray menu items s.mStatus = systray.AddMenuItem("Disconnected", "Disconnected") s.mStatus.Disable() s.mProfileName = systray.AddMenuItem("Profile: Home", "Selected Profile: Home") s.mProfileName.Disable() systray.AddSeparator() s.mUp = systray.AddMenuItem("Connect", "Connect") s.mDown = systray.AddMenuItem("Disconnect", "Disconnect") s.mDown.Disable() systray.AddSeparator() s.mSettings = systray.AddMenuItem("Settings", settingsMenuDescr) s.mAllowSSH = s.mSettings.AddSubMenuItemCheckbox("Allow SSH", allowSSHMenuDescr, false) s.mAutoConnect = s.mSettings.AddSubMenuItemCheckbox("Connect on Startup", autoConnectMenuDescr, false) s.mEnableRosenpass = s.mSettings.AddSubMenuItemCheckbox("Enable Quantum-Resistance", quantumResistanceMenuDescr, false) s.mLazyConnEnabled = s.mSettings.AddSubMenuItemCheckbox("Enable lazy connection", lazyConnMenuDescr, false) s.mNotifications = s.mSettings.AddSubMenuItemCheckbox("Notifications", notificationsMenuDescr, false) s.mAdvancedSettings = s.mSettings.AddSubMenuItem("Advanced Settings", advancedSettingsMenuDescr) s.mCreateDebugBundle = s.mSettings.AddSubMenuItem("Create Debug Bundle", debugBundleMenuDescr) s.loadSettings() s.mProfiles = systray.AddMenuItem("Profiles", profilesMenuDescr) s.exitNodeMu.Lock() s.mExitNode = systray.AddMenuItem("Exit Node", exitNodeMenuDescr) s.mExitNode.Disable() s.exitNodeMu.Unlock() s.mNetworks = systray.AddMenuItem("Networks", networksMenuDescr) s.mNetworks.Disable() systray.AddSeparator() s.mAbout = systray.AddMenuItem("About", "About") s.mAbout.SetIcon(s.icAbout) s.mGitHub = s.mAbout.AddSubMenuItem("GitHub", "GitHub") versionString := normalizedVersion(version.NetbirdVersion()) s.mVersionUI = s.mAbout.AddSubMenuItem(fmt.Sprintf("GUI: %s", versionString), fmt.Sprintf("GUI Version: %s", versionString)) s.mVersionUI.Disable() s.mVersionDaemon = s.mAbout.AddSubMenuItem("", "") s.mVersionDaemon.Disable() s.mVersionDaemon.Hide() s.mUpdate = s.mAbout.AddSubMenuItem("Download latest version", latestVersionMenuDescr) s.mUpdate.Hide() systray.AddSeparator() s.mQuit = systray.AddMenuItem("Quit", quitMenuDescr) // update exit node menu in case service is already connected go s.updateExitNodes() s.update.SetOnUpdateListener(s.onUpdateAvailable) go func() { s.getSrvConfig() time.Sleep(100 * time.Millisecond) // To prevent race condition caused by systray not being fully initialized and ignoring setIcon for { err := s.updateStatus() if err != nil { log.Errorf("error while updating status: %v", err) } time.Sleep(2 * time.Second) } }() s.eventManager = event.NewManager(s.app, s.addr) s.eventManager.SetNotificationsEnabled(s.mNotifications.Checked()) s.eventManager.AddHandler(func(event *proto.SystemEvent) { if event.Category == proto.SystemEvent_SYSTEM { s.updateExitNodes() } }) go s.eventManager.Start(s.ctx) go s.listenEvents() } func (s *serviceClient) listenEvents() { for { select { case <-s.mUp.ClickedCh: s.mUp.Disable() go func() { defer s.mUp.Enable() err := s.menuUpClick() if err != nil { s.app.SendNotification(fyne.NewNotification("Error", "Failed to connect to NetBird service")) return } }() case <-s.mDown.ClickedCh: s.mDown.Disable() go func() { defer s.mDown.Enable() err := s.menuDownClick() if err != nil { s.app.SendNotification(fyne.NewNotification("Error", "Failed to connect to NetBird service")) return } }() case <-s.mAllowSSH.ClickedCh: if s.mAllowSSH.Checked() { s.mAllowSSH.Uncheck() } else { s.mAllowSSH.Check() } if err := s.updateConfig(); err != nil { log.Errorf("failed to update config: %v", err) } case <-s.mAutoConnect.ClickedCh: if s.mAutoConnect.Checked() { s.mAutoConnect.Uncheck() } else { s.mAutoConnect.Check() } if err := s.updateConfig(); err != nil { log.Errorf("failed to update config: %v", err) } case <-s.mEnableRosenpass.ClickedCh: if s.mEnableRosenpass.Checked() { s.mEnableRosenpass.Uncheck() } else { s.mEnableRosenpass.Check() } if err := s.updateConfig(); err != nil { log.Errorf("failed to update config: %v", err) } case <-s.mLazyConnEnabled.ClickedCh: if s.mLazyConnEnabled.Checked() { s.mLazyConnEnabled.Uncheck() } else { s.mLazyConnEnabled.Check() } if err := s.updateConfig(); err != nil { log.Errorf("failed to update config: %v", err) } case <-s.mAdvancedSettings.ClickedCh: s.mAdvancedSettings.Disable() go func() { defer s.mAdvancedSettings.Enable() defer s.getSrvConfig() s.runSelfCommand("settings", "true") }() case <-s.mCreateDebugBundle.ClickedCh: s.mCreateDebugBundle.Disable() go func() { defer s.mCreateDebugBundle.Enable() s.runSelfCommand("debug", "true") }() case <-s.mQuit.ClickedCh: systray.Quit() return case <-s.mGitHub.ClickedCh: err := openURL("https://github.com/netbirdio/netbird") if err != nil { log.Errorf("%s", err) } case <-s.mUpdate.ClickedCh: err := openURL(version.DownloadUrl()) if err != nil { log.Errorf("%s", err) } case <-s.mNetworks.ClickedCh: s.mNetworks.Disable() go func() { defer s.mNetworks.Enable() s.runSelfCommand("networks", "true") }() case <-s.mProfiles.ClickedCh: s.mProfiles.Disable() go func() { defer s.mProfiles.Enable() s.runSelfCommand("profiles", "true") }() case <-s.mNotifications.ClickedCh: if s.mNotifications.Checked() { s.mNotifications.Uncheck() } else { s.mNotifications.Check() } if s.eventManager != nil { s.eventManager.SetNotificationsEnabled(s.mNotifications.Checked()) } if err := s.updateConfig(); err != nil { log.Errorf("failed to update config: %v", err) } } } } func (s *serviceClient) runSelfCommand(command, arg string) { proc, err := os.Executable() if err != nil { log.Errorf("Error getting executable path: %v", err) return } cmd := exec.Command(proc, fmt.Sprintf("--%s=%s", command, arg), fmt.Sprintf("--daemon-addr=%s", s.addr), ) if out := s.attachOutput(cmd); out != nil { defer func() { if err := out.Close(); err != nil { log.Errorf("Error closing log file %s: %v", s.logFile, err) } }() } log.Printf("Running command: %s --%s=%s --daemon-addr=%s", proc, command, arg, s.addr) err = cmd.Run() if err != nil { var exitErr *exec.ExitError if errors.As(err, &exitErr) { log.Printf("Command '%s %s' failed with exit code %d", command, arg, exitErr.ExitCode()) } else { log.Printf("Failed to start/run command '%s %s': %v", command, arg, err) } return } log.Printf("Command '%s %s' completed successfully.", command, arg) } func (s *serviceClient) attachOutput(cmd *exec.Cmd) *os.File { if s.logFile == "" { // attach child's streams to parent's streams cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr return nil } out, err := os.OpenFile(s.logFile, os.O_WRONLY|os.O_APPEND, 0) if err != nil { log.Errorf("Failed to open log file %s: %v", s.logFile, err) return nil } cmd.Stdout = out cmd.Stderr = out return out } func normalizedVersion(version string) string { versionString := version if unicode.IsDigit(rune(versionString[0])) { versionString = fmt.Sprintf("v%s", versionString) } return versionString } // onTrayExit is called when the tray icon is closed. func (s *serviceClient) onTrayExit() { s.cancel() } // getSrvClient connection to the service. func (s *serviceClient) getSrvClient(timeout time.Duration) (proto.DaemonServiceClient, error) { if s.conn != nil { return s.conn, nil } ctx, cancel := context.WithTimeout(s.ctx, timeout) defer cancel() conn, err := grpc.DialContext( ctx, strings.TrimPrefix(s.addr, "tcp://"), grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithBlock(), grpc.WithUserAgent(desktop.GetUIUserAgent()), ) if err != nil { return nil, fmt.Errorf("dial service: %w", err) } s.conn = proto.NewDaemonServiceClient(conn) return s.conn, nil } // getSrvConfig from the service to show it in the settings window. func (s *serviceClient) getSrvConfig() { s.managementURL = internal.DefaultManagementURL s.adminURL = internal.DefaultAdminURL conn, err := s.getSrvClient(failFastTimeout) if err != nil { log.Errorf("get client: %v", err) return } cfg, err := conn.GetConfig(s.ctx, &proto.GetConfigRequest{}) if err != nil { log.Errorf("get config settings from server: %v", err) return } if cfg.ManagementUrl != "" { s.managementURL = cfg.ManagementUrl } if cfg.AdminURL != "" { s.adminURL = cfg.AdminURL } s.preSharedKey = cfg.PreSharedKey s.RosenpassPermissive = cfg.RosenpassPermissive s.interfaceName = cfg.InterfaceName s.interfacePort = int(cfg.WireguardPort) if s.showAdvancedSettings { s.iMngURL.SetText(s.managementURL) s.iAdminURL.SetText(s.adminURL) s.iConfigFile.SetText(cfg.ConfigFile) s.iLogFile.SetText(cfg.LogFile) s.iPreSharedKey.SetText(cfg.PreSharedKey) s.iInterfaceName.SetText(cfg.InterfaceName) s.iInterfacePort.SetText(strconv.Itoa(int(cfg.WireguardPort))) s.sRosenpassPermissive.SetChecked(cfg.RosenpassPermissive) if !cfg.RosenpassEnabled { s.sRosenpassPermissive.Disable() } } if s.mNotifications == nil { return } if cfg.DisableNotifications { s.mNotifications.Uncheck() } else { s.mNotifications.Check() } if s.eventManager != nil { s.eventManager.SetNotificationsEnabled(s.mNotifications.Checked()) } } func (s *serviceClient) onUpdateAvailable() { s.updateIndicationLock.Lock() defer s.updateIndicationLock.Unlock() s.mUpdate.Show() s.isUpdateIconActive = true if s.connected { systray.SetTemplateIcon(iconUpdateConnectedMacOS, s.icUpdateConnected) } else { systray.SetTemplateIcon(iconUpdateDisconnectedMacOS, s.icUpdateDisconnected) } } // onSessionExpire sends a notification to the user when the session expires. func (s *serviceClient) onSessionExpire() { 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.sendNotification = false } } // loadSettings loads the settings from the config file and updates the UI elements accordingly. func (s *serviceClient) loadSettings() { conn, err := s.getSrvClient(failFastTimeout) if err != nil { log.Errorf("get client: %v", err) return } cfg, err := conn.GetConfig(s.ctx, &proto.GetConfigRequest{}) if err != nil { log.Errorf("get config settings from server: %v", err) return } if cfg.ServerSSHAllowed { s.mAllowSSH.Check() } else { s.mAllowSSH.Uncheck() } if cfg.DisableAutoConnect { s.mAutoConnect.Uncheck() } else { s.mAutoConnect.Check() } if cfg.RosenpassEnabled { s.mEnableRosenpass.Check() } else { s.mEnableRosenpass.Uncheck() } if cfg.DisableNotifications { s.mNotifications.Uncheck() } else { s.mNotifications.Check() } if s.eventManager != nil { s.eventManager.SetNotificationsEnabled(s.mNotifications.Checked()) } } // updateConfig updates the configuration parameters // based on the values selected in the settings window. func (s *serviceClient) updateConfig() error { disableAutoStart := !s.mAutoConnect.Checked() sshAllowed := s.mAllowSSH.Checked() rosenpassEnabled := s.mEnableRosenpass.Checked() notificationsDisabled := !s.mNotifications.Checked() lazyConnectionEnabled := s.mLazyConnEnabled.Checked() loginRequest := proto.LoginRequest{ IsUnixDesktopClient: runtime.GOOS == "linux" || runtime.GOOS == "freebsd", ServerSSHAllowed: &sshAllowed, RosenpassEnabled: &rosenpassEnabled, DisableAutoConnect: &disableAutoStart, DisableNotifications: ¬ificationsDisabled, LazyConnectionEnabled: &lazyConnectionEnabled, } if err := s.restartClient(&loginRequest); err != nil { log.Errorf("restarting client connection: %v", err) return err } return nil } // restartClient restarts the client connection. func (s *serviceClient) restartClient(loginRequest *proto.LoginRequest) error { ctx, cancel := context.WithTimeout(s.ctx, defaultFailTimeout) defer cancel() client, err := s.getSrvClient(failFastTimeout) if err != nil { return err } _, err = client.Login(ctx, loginRequest) if err != nil { return err } _, err = client.Up(ctx, &proto.UpRequest{}) if err != nil { return err } return nil } func openURL(url string) error { var err error switch runtime.GOOS { case "windows": err = exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start() case "darwin": err = exec.Command("open", url).Start() case "linux", "freebsd": err = exec.Command("xdg-open", url).Start() default: err = fmt.Errorf("unsupported platform") } return err }