mirror of
https://github.com/netbirdio/netbird.git
synced 2025-07-15 13:56:08 +02:00
1138 lines
30 KiB
Go
1138 lines
30 KiB
Go
//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
|
|
}
|