From 951e011a9c0959676e540bf422302f967292f6a0 Mon Sep 17 00:00:00 2001 From: Givi Khojanashvili Date: Fri, 15 Apr 2022 19:30:12 +0400 Subject: [PATCH] Add Settings window to Agent UI Agent systray UI has been extended with a setting window that allows configuring management URL, admin URL and supports pre-shared key. While for the Netbird managed version the Settings are not necessary, it helps to properly configure the self-hosted version. --- .github/workflows/golang-test-linux.yml | 2 +- .github/workflows/golangci-lint.yml | 5 +- .gitignore | 3 +- .goreleaser.yaml | 26 ++- Makefile | 42 ---- client/cmd/login.go | 5 +- client/cmd/root.go | 2 + client/cmd/service_controller.go | 2 +- client/cmd/testutil.go | 2 +- client/cmd/up.go | 4 +- client/internal/config.go | 83 ++++--- client/internal/config_test.go | 16 +- client/internal/login.go | 62 ++---- client/proto/daemon.pb.go | 284 +++++++++++++++++++----- client/proto/daemon.proto | 29 ++- client/proto/daemon_grpc.pb.go | 38 ++++ client/server/server.go | 63 +++++- client/ui/client_ui.go | 281 +++++++++++++++++++---- client/ui/connected.png | Bin 0 -> 10290 bytes client/ui/disconnected.png | Bin 0 -> 6461 bytes client/ui/netbird.desktop | 8 + client/ui/wiretrustee.desktop | 9 - client/ui/wiretrustee.png | Bin 12868 -> 0 bytes go.mod | 10 + go.sum | 35 +++ 25 files changed, 767 insertions(+), 244 deletions(-) delete mode 100644 Makefile create mode 100644 client/ui/connected.png create mode 100644 client/ui/disconnected.png create mode 100644 client/ui/netbird.desktop delete mode 100644 client/ui/wiretrustee.desktop delete mode 100644 client/ui/wiretrustee.png diff --git a/.github/workflows/golang-test-linux.yml b/.github/workflows/golang-test-linux.yml index e26528c26..3a85d4aba 100644 --- a/.github/workflows/golang-test-linux.yml +++ b/.github/workflows/golang-test-linux.yml @@ -25,7 +25,7 @@ jobs: uses: actions/checkout@v2 - name: Install dependencies - run: sudo apt update && sudo apt install -y -q libgtk-3-dev libappindicator3-dev libayatana-appindicator3-dev + run: sudo apt update && sudo apt install -y -q libgtk-3-dev libappindicator3-dev libayatana-appindicator3-dev libgl1-mesa-dev xorg-dev - name: Install modules run: go mod tidy diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index e0bc9fbc5..513b2d3c0 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -8,9 +8,10 @@ jobs: - uses: actions/checkout@v2 - name: Install dependencies - run: sudo apt update && sudo apt install -y -q libgtk-3-dev libappindicator3-dev libayatana-appindicator3-dev - + run: sudo apt update && sudo apt install -y -q libgtk-3-dev libappindicator3-dev libayatana-appindicator3-dev libgl1-mesa-dev xorg-dev - name: golangci-lint uses: golangci/golangci-lint-action@v2 + with: + args: --timeout=6m diff --git a/.gitignore b/.gitignore index 327eb3076..499205fbd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,11 @@ .idea *.iml dist/ +bin/ .env conf.json http-cmds.sh infrastructure_files/management.json infrastructure_files/docker-compose.yml *.syso -client/.distfiles/ \ No newline at end of file +client/.distfiles/ diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 24c4ef652..b6e71c3ac 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -58,10 +58,24 @@ builds: - id: wiretrustee-ui dir: client/ui binary: wiretrustee-ui - env: [CGO_ENABLED=1] + env: + - CGO_ENABLED=1 + goos: + - linux + goarch: + - amd64 + ldflags: + - -s -w -X github.com/netbirdio/netbird/client/ui/system.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.CommitDate}} -X main.builtBy=goreleaser + mod_timestamp: '{{ .CommitTimestamp }}' + + - id: wiretrustee-ui-windows + dir: client/ui + binary: wiretrustee-ui-windows + env: + - CGO_ENABLED=1 + - CC=x86_64-w64-mingw32-gcc goos: - windows - - linux goarch: - amd64 ldflags: @@ -85,10 +99,10 @@ nfpms: - deb - rpm contents: - - src: client/ui/wiretrustee.desktop - dst: /usr/share/applications/wiretrustee.desktop - - src: client/ui/wiretrustee.png - dst: /usr/share/icons/hicolor/256x256/wiretrustee.png + - src: client/ui/netbird.desktop + dst: /usr/share/applications/netbird.desktop + - src: client/ui/disconnected.png + dst: /usr/share/pixmaps/netbird.png dependencies: - libayatana-appindicator3-1 - libgtk-3-dev diff --git a/Makefile b/Makefile deleted file mode 100644 index 7d7828384..000000000 --- a/Makefile +++ /dev/null @@ -1,42 +0,0 @@ -PACKAGE_NAME := github.com/wiretrustee/wiretrustee/ -GOLANG_CROSS_VERSION ?= v1.17.6 - -SYSROOT_DIR ?= sysroots -SYSROOT_ARCHIVE ?= sysroots.tar.bz2 - -.PHONY: sysroot-pack -sysroot-pack: - @tar cf - $(SYSROOT_DIR) -P | pv -s $[$(du -sk $(SYSROOT_DIR) | awk '{print $1}') * 1024] | pbzip2 > $(SYSROOT_ARCHIVE) - -.PHONY: sysroot-unpack -sysroot-unpack: - @pv $(SYSROOT_ARCHIVE) | pbzip2 -cd | tar -xf - - -.PHONY: release-dry-run -release-dry-run: - @docker run \ - --rm \ - --privileged \ - -e CGO_ENABLED=1 \ - -v /var/run/docker.sock:/var/run/docker.sock \ - -v `pwd`:/go/src/$(PACKAGE_NAME) \ - -w /go/src/$(PACKAGE_NAME) \ - goreleaser/goreleaser-cross:${GOLANG_CROSS_VERSION} \ - --rm-dist --skip-validate --skip-publish - -.PHONY: release -release: - @if [ ! -f ".release-env" ]; then \ - echo "\033[91m.release-env is required for release\033[0m";\ - exit 1;\ - fi - docker run \ - --rm \ - --privileged \ - -e CGO_ENABLED=1 \ - --env-file .release-env \ - -v /var/run/docker.sock:/var/run/docker.sock \ - -v `pwd`:/go/src/$(PACKAGE_NAME) \ - -w /go/src/$(PACKAGE_NAME) \ - goreleaser/goreleaser-cross:${GOLANG_CROSS_VERSION} \ - release --rm-dist diff --git a/client/cmd/login.go b/client/cmd/login.go index 261bc7d6b..a5d2e5248 100644 --- a/client/cmd/login.go +++ b/client/cmd/login.go @@ -3,6 +3,7 @@ package cmd import ( "context" "fmt" + "github.com/netbirdio/netbird/util" log "github.com/sirupsen/logrus" @@ -28,7 +29,7 @@ var loginCmd = &cobra.Command{ // workaround to run without service if logFile == "console" { - config, err := internal.GetConfig(managementURL, configPath, preSharedKey) + config, err := internal.GetConfig(managementURL, adminURL, configPath, preSharedKey) if err != nil { log.Errorf("get config file: %v", err) return err @@ -56,7 +57,7 @@ var loginCmd = &cobra.Command{ request := proto.LoginRequest{ SetupKey: setupKey, - PresharedKey: preSharedKey, + PreSharedKey: preSharedKey, ManagementUrl: managementURL, } client := proto.NewDaemonServiceClient(conn) diff --git a/client/cmd/root.go b/client/cmd/root.go index 320aa7da7..c20da54ca 100644 --- a/client/cmd/root.go +++ b/client/cmd/root.go @@ -28,6 +28,7 @@ var ( logFile string daemonAddr string managementURL string + adminURL string setupKey string preSharedKey string rootCmd = &cobra.Command{ @@ -56,6 +57,7 @@ func init() { } rootCmd.PersistentFlags().StringVar(&daemonAddr, "daemon-addr", defaultDaemonAddr, "Daemon service address to serve CLI requests [unix|tcp]://[path|host:port]") rootCmd.PersistentFlags().StringVar(&managementURL, "management-url", "", fmt.Sprintf("Management Service URL [http|https]://[host]:[port] (default \"%s\")", internal.ManagementURLDefault().String())) + rootCmd.PersistentFlags().StringVar(&adminURL, "admin-url", "https://app.netbird.io", "Admin Panel URL [http|https]://[host]:[port]") rootCmd.PersistentFlags().StringVar(&configPath, "config", defaultConfigPath, "Wiretrustee config file location") rootCmd.PersistentFlags().StringVar(&logLevel, "log-level", "info", "sets Wiretrustee log level") rootCmd.PersistentFlags().StringVar(&logFile, "log-file", defaultLogFile, "sets Wiretrustee log path. If console is specified the the log will be output to stdout") diff --git a/client/cmd/service_controller.go b/client/cmd/service_controller.go index 2a5315ea1..205a16c4b 100644 --- a/client/cmd/service_controller.go +++ b/client/cmd/service_controller.go @@ -54,7 +54,7 @@ func (p *program) Start(svc service.Service) error { } } - serverInstance := server.New(p.ctx, managementURL, configPath) + serverInstance := server.New(p.ctx, managementURL, adminURL, configPath, logFile) if err := serverInstance.Start(); err != nil { log.Fatalf("failed start daemon: %v", err) } diff --git a/client/cmd/testutil.go b/client/cmd/testutil.go index a57814fa1..9a766b7d1 100644 --- a/client/cmd/testutil.go +++ b/client/cmd/testutil.go @@ -93,7 +93,7 @@ func startClientDaemon( } s := grpc.NewServer() - server := client.New(ctx, managementURL, configPath) + server := client.New(ctx, managementURL, adminURL, configPath, "") if err := server.Start(); err != nil { t.Fatal(err) } diff --git a/client/cmd/up.go b/client/cmd/up.go index f6f80bf3a..c951ac080 100644 --- a/client/cmd/up.go +++ b/client/cmd/up.go @@ -27,7 +27,7 @@ var upCmd = &cobra.Command{ // workaround to run without service if logFile == "console" { - config, err := internal.GetConfig(managementURL, configPath, preSharedKey) + config, err := internal.GetConfig(managementURL, adminURL, configPath, preSharedKey) if err != nil { log.Errorf("get config file: %v", err) return err @@ -57,7 +57,7 @@ var upCmd = &cobra.Command{ loginRequest := proto.LoginRequest{ SetupKey: setupKey, - PresharedKey: preSharedKey, + PreSharedKey: preSharedKey, ManagementUrl: managementURL, } err = WithBackOff(func() error { diff --git a/client/internal/config.go b/client/internal/config.go index 84406b1a5..877435ce4 100644 --- a/client/internal/config.go +++ b/client/internal/config.go @@ -2,12 +2,13 @@ package internal import ( "fmt" + "net/url" + "os" + "github.com/netbirdio/netbird/iface" "github.com/netbirdio/netbird/util" log "github.com/sirupsen/logrus" "golang.zx2c4.com/wireguard/wgctrl/wgtypes" - "net/url" - "os" ) var managementURLDefault *url.URL @@ -17,7 +18,7 @@ func ManagementURLDefault() *url.URL { } func init() { - managementURL, err := parseManagementURL("https://api.wiretrustee.com:33073") + managementURL, err := parseURL("Management URL", "https://api.wiretrustee.com:33073") if err != nil { panic(err) } @@ -30,16 +31,17 @@ type Config struct { PrivateKey string PreSharedKey string ManagementURL *url.URL + AdminURL *url.URL WgIface string IFaceBlackList []string } -//createNewConfig creates a new config generating a new Wireguard key and saving to file -func createNewConfig(managementURL string, configPath string, preSharedKey string) (*Config, error) { +// createNewConfig creates a new config generating a new Wireguard key and saving to file +func createNewConfig(managementURL, adminURL, configPath, preSharedKey string) (*Config, error) { wgKey := generateKey() config := &Config{PrivateKey: wgKey, WgIface: iface.WgInterfaceDefault, IFaceBlackList: []string{}} if managementURL != "" { - URL, err := parseManagementURL(managementURL) + URL, err := parseURL("Management URL", managementURL) if err != nil { return nil, err } @@ -62,55 +64,82 @@ func createNewConfig(managementURL string, configPath string, preSharedKey strin return config, nil } -func parseManagementURL(managementURL string) (*url.URL, error) { - +func parseURL(serviceName, managementURL string) (*url.URL, error) { parsedMgmtURL, err := url.ParseRequestURI(managementURL) if err != nil { log.Errorf("failed parsing management URL %s: [%s]", managementURL, err.Error()) return nil, err } - if !(parsedMgmtURL.Scheme == "https" || parsedMgmtURL.Scheme == "http") { - return nil, fmt.Errorf("invalid Management Service URL provided %s. Supported format [http|https]://[host]:[port]", managementURL) + if parsedMgmtURL.Scheme != "https" && parsedMgmtURL.Scheme != "http" { + return nil, fmt.Errorf( + "invalid %s URL provided %s. Supported format [http|https]://[host]:[port]", + serviceName, managementURL) } return parsedMgmtURL, err - } // ReadConfig reads existing config. In case provided managementURL is not empty overrides the read property -func ReadConfig(managementURL string, configPath string) (*Config, error) { +func ReadConfig(managementURL, adminURL, configPath string, preSharedKey *string) (*Config, error) { config := &Config{} - _, err := util.ReadJson(configPath, config) - if err != nil { + if _, err := util.ReadJson(configPath, config); err != nil { return nil, err } + refresh := false + if managementURL != "" && config.ManagementURL.String() != managementURL { - URL, err := parseManagementURL(managementURL) + log.Infof("new Management URL provided, updated to %s (old value %s)", + managementURL, config.ManagementURL) + newURL, err := parseURL("Management URL", managementURL) if err != nil { return nil, err } - config.ManagementURL = URL - // since we have new management URL, we need to update config file - err = util.WriteJson(configPath, config) - if err != nil { - return nil, err - } - log.Infof("new Management URL provided, updated to %s (old value %s)", managementURL, config.ManagementURL) + config.ManagementURL = newURL + refresh = true } - return config, err + if adminURL != "" && (config.AdminURL == nil || config.AdminURL.String() != adminURL) { + log.Infof("new Admin Panel URL provided, updated to %s (old value %s)", + adminURL, config.AdminURL) + newURL, err := parseURL("Admin Panel URL", adminURL) + if err != nil { + return nil, err + } + config.AdminURL = newURL + refresh = true + } + + if preSharedKey != nil && config.PreSharedKey != *preSharedKey { + log.Infof("new pre-shared key provided, updated to %s (old value %s)", + *preSharedKey, config.PreSharedKey) + config.PreSharedKey = *preSharedKey + refresh = true + } + + if refresh { + // since we have new management URL, we need to update config file + if err := util.WriteJson(configPath, config); err != nil { + return nil, err + } + } + + return config, nil } // GetConfig reads existing config or generates a new one -func GetConfig(managementURL string, configPath string, preSharedKey string) (*Config, error) { - +func GetConfig(managementURL, adminURL, configPath, preSharedKey string) (*Config, error) { if _, err := os.Stat(configPath); os.IsNotExist(err) { log.Infof("generating new config %s", configPath) - return createNewConfig(managementURL, configPath, preSharedKey) + return createNewConfig(managementURL, adminURL, configPath, preSharedKey) } else { - return ReadConfig(managementURL, configPath) + // don't overwrite pre-shared key if we receive asterisks from UI + pk := &preSharedKey + if preSharedKey == "**********" { + pk = nil + } + return ReadConfig(managementURL, adminURL, configPath, pk) } } diff --git a/client/internal/config_test.go b/client/internal/config_test.go index 06e9c1bfc..a9cd3e88b 100644 --- a/client/internal/config_test.go +++ b/client/internal/config_test.go @@ -2,24 +2,25 @@ package internal import ( "errors" - "github.com/netbirdio/netbird/util" - "github.com/stretchr/testify/assert" "os" "path/filepath" "testing" + + "github.com/netbirdio/netbird/util" + "github.com/stretchr/testify/assert" ) func TestReadConfig(t *testing.T) { - } -func TestGetConfig(t *testing.T) { +func TestGetConfig(t *testing.T) { managementURL := "https://test.management.url:33071" + adminURL := "https://app.admin.url" path := filepath.Join(t.TempDir(), "config.json") preSharedKey := "preSharedKey" // case 1: new config has to be generated - config, err := GetConfig(managementURL, path, preSharedKey) + config, err := GetConfig(managementURL, adminURL, path, preSharedKey) if err != nil { return } @@ -32,7 +33,7 @@ func TestGetConfig(t *testing.T) { } // case 2: existing config -> fetch it - config, err = GetConfig(managementURL, path, preSharedKey) + config, err = GetConfig(managementURL, adminURL, path, preSharedKey) if err != nil { return } @@ -42,7 +43,7 @@ func TestGetConfig(t *testing.T) { // case 3: existing config, but new managementURL has been provided -> update config newManagementURL := "https://test.newManagement.url:33071" - config, err = GetConfig(newManagementURL, path, preSharedKey) + config, err = GetConfig(newManagementURL, adminURL, path, preSharedKey) if err != nil { return } @@ -56,5 +57,4 @@ func TestGetConfig(t *testing.T) { return } assert.Equal(t, readConf.(*Config).ManagementURL.String(), newManagementURL) - } diff --git a/client/internal/login.go b/client/internal/login.go index 117810438..221f24de4 100644 --- a/client/internal/login.go +++ b/client/internal/login.go @@ -2,9 +2,7 @@ package internal import ( "context" - "time" - "github.com/cenkalti/backoff/v4" "github.com/google/uuid" "github.com/netbirdio/netbird/client/system" mgm "github.com/netbirdio/netbird/management/client" @@ -16,16 +14,6 @@ import ( ) func Login(ctx context.Context, config *Config, setupKey string) error { - backOff := &backoff.ExponentialBackOff{ - InitialInterval: time.Second, - RandomizationFactor: backoff.DefaultRandomizationFactor, - Multiplier: backoff.DefaultMultiplier, - MaxInterval: 2 * time.Second, - MaxElapsedTime: time.Second * 10, - Stop: backoff.Stop, - Clock: backoff.SystemClock, - } - // validate our peer's Wireguard PRIVATE key myPrivateKey, err := wgtypes.ParseKey(config.PrivateKey) if err != nil { @@ -38,41 +26,29 @@ func Login(ctx context.Context, config *Config, setupKey string) error { mgmTlsEnabled = true } - loginOp := func() error { - log.Debugf("connecting to Management Service %s", config.ManagementURL.String()) - mgmClient, err := mgm.NewClient(ctx, config.ManagementURL.Host, myPrivateKey, mgmTlsEnabled) - if err != nil { - log.Errorf("failed connecting to Management Service %s %v", config.ManagementURL.String(), err) - return err - } - log.Debugf("connected to management Service %s", config.ManagementURL.String()) + log.Debugf("connecting to Management Service %s", config.ManagementURL.String()) + mgmClient, err := mgm.NewClient(ctx, config.ManagementURL.Host, myPrivateKey, mgmTlsEnabled) + if err != nil { + log.Errorf("failed connecting to Management Service %s %v", config.ManagementURL.String(), err) + return err + } + log.Debugf("connected to management Service %s", config.ManagementURL.String()) - serverKey, err := mgmClient.GetServerPublicKey() - if err != nil { - log.Errorf("failed while getting Management Service public key: %v", err) - return err - } - - _, err = loginPeer(*serverKey, mgmClient, setupKey) - if err != nil { - log.Errorf("failed logging-in peer on Management Service : %v", err) - return err - } - - err = mgmClient.Close() - if err != nil { - log.Errorf("failed closing Management Service client: %v", err) - return err - } - - return nil + serverKey, err := mgmClient.GetServerPublicKey() + if err != nil { + log.Errorf("failed while getting Management Service public key: %v", err) + return err } - err = backoff.RetryNotify(loginOp, backOff, func(err error, duration time.Duration) { - log.Warnf("retrying Login to the Management service in %v due to error %v", duration, err) - }) + _, err = loginPeer(*serverKey, mgmClient, setupKey) if err != nil { - log.Errorf("exiting login retry loop due to unrecoverable error: %v", err) + log.Errorf("failed logging-in peer on Management Service : %v", err) + return err + } + + err = mgmClient.Close() + if err != nil { + log.Errorf("failed closing Management Service client: %v", err) return err } diff --git a/client/proto/daemon.pb.go b/client/proto/daemon.pb.go index 8c603bebf..50f9aff7a 100644 --- a/client/proto/daemon.pb.go +++ b/client/proto/daemon.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.26.0 -// protoc v3.17.3 +// protoc-gen-go v1.25.0-devel +// protoc v3.14.0 // source: daemon.proto package proto @@ -28,10 +28,12 @@ type LoginRequest struct { // setupKey wiretrustee setup key. SetupKey string `protobuf:"bytes,1,opt,name=setupKey,proto3" json:"setupKey,omitempty"` - // presharedKey for wireguard setup. - PresharedKey string `protobuf:"bytes,2,opt,name=presharedKey,proto3" json:"presharedKey,omitempty"` + // preSharedKey for wireguard setup. + PreSharedKey string `protobuf:"bytes,2,opt,name=preSharedKey,proto3" json:"preSharedKey,omitempty"` // managementUrl to authenticate. ManagementUrl string `protobuf:"bytes,3,opt,name=managementUrl,proto3" json:"managementUrl,omitempty"` + // adminUrl to manage keys. + AdminURL string `protobuf:"bytes,4,opt,name=adminURL,proto3" json:"adminURL,omitempty"` } func (x *LoginRequest) Reset() { @@ -73,9 +75,9 @@ func (x *LoginRequest) GetSetupKey() string { return "" } -func (x *LoginRequest) GetPresharedKey() string { +func (x *LoginRequest) GetPreSharedKey() string { if x != nil { - return x.PresharedKey + return x.PreSharedKey } return "" } @@ -87,6 +89,13 @@ func (x *LoginRequest) GetManagementUrl() string { return "" } +func (x *LoginRequest) GetAdminURL() string { + if x != nil { + return x.AdminURL + } + return "" +} + type LoginResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -363,44 +372,185 @@ func (*DownResponse) Descriptor() ([]byte, []int) { return file_daemon_proto_rawDescGZIP(), []int{7} } +type GetConfigRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *GetConfigRequest) Reset() { + *x = GetConfigRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_daemon_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GetConfigRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetConfigRequest) ProtoMessage() {} + +func (x *GetConfigRequest) ProtoReflect() protoreflect.Message { + mi := &file_daemon_proto_msgTypes[8] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetConfigRequest.ProtoReflect.Descriptor instead. +func (*GetConfigRequest) Descriptor() ([]byte, []int) { + return file_daemon_proto_rawDescGZIP(), []int{8} +} + +type GetConfigResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // managementUrl settings value. + ManagementUrl string `protobuf:"bytes,1,opt,name=managementUrl,proto3" json:"managementUrl,omitempty"` + // configFile settings value. + ConfigFile string `protobuf:"bytes,2,opt,name=configFile,proto3" json:"configFile,omitempty"` + // logFile settings value. + LogFile string `protobuf:"bytes,3,opt,name=logFile,proto3" json:"logFile,omitempty"` + // preSharedKey settings value. + PreSharedKey string `protobuf:"bytes,4,opt,name=preSharedKey,proto3" json:"preSharedKey,omitempty"` + // adminURL settings value. + AdminURL string `protobuf:"bytes,5,opt,name=adminURL,proto3" json:"adminURL,omitempty"` +} + +func (x *GetConfigResponse) Reset() { + *x = GetConfigResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_daemon_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GetConfigResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetConfigResponse) ProtoMessage() {} + +func (x *GetConfigResponse) ProtoReflect() protoreflect.Message { + mi := &file_daemon_proto_msgTypes[9] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetConfigResponse.ProtoReflect.Descriptor instead. +func (*GetConfigResponse) Descriptor() ([]byte, []int) { + return file_daemon_proto_rawDescGZIP(), []int{9} +} + +func (x *GetConfigResponse) GetManagementUrl() string { + if x != nil { + return x.ManagementUrl + } + return "" +} + +func (x *GetConfigResponse) GetConfigFile() string { + if x != nil { + return x.ConfigFile + } + return "" +} + +func (x *GetConfigResponse) GetLogFile() string { + if x != nil { + return x.LogFile + } + return "" +} + +func (x *GetConfigResponse) GetPreSharedKey() string { + if x != nil { + return x.PreSharedKey + } + return "" +} + +func (x *GetConfigResponse) GetAdminURL() string { + if x != nil { + return x.AdminURL + } + return "" +} + var File_daemon_proto protoreflect.FileDescriptor var file_daemon_proto_rawDesc = []byte{ 0x0a, 0x0c, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x06, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x1a, 0x20, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, - 0x6f, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x74, 0x0a, 0x0c, 0x4c, 0x6f, 0x67, 0x69, - 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x65, 0x74, 0x75, - 0x70, 0x4b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x73, 0x65, 0x74, 0x75, - 0x70, 0x4b, 0x65, 0x79, 0x12, 0x22, 0x0a, 0x0c, 0x70, 0x72, 0x65, 0x73, 0x68, 0x61, 0x72, 0x65, - 0x64, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x70, 0x72, 0x65, 0x73, - 0x68, 0x61, 0x72, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x12, 0x24, 0x0a, 0x0d, 0x6d, 0x61, 0x6e, 0x61, - 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x55, 0x72, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x0d, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x55, 0x72, 0x6c, 0x22, 0x0f, - 0x0a, 0x0d, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, - 0x0b, 0x0a, 0x09, 0x55, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x0c, 0x0a, 0x0a, - 0x55, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x0f, 0x0a, 0x0d, 0x53, 0x74, - 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x28, 0x0a, 0x0e, 0x53, - 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x16, 0x0a, - 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, - 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x0d, 0x0a, 0x0b, 0x44, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x22, 0x0e, 0x0a, 0x0c, 0x44, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x32, 0xe6, 0x01, 0x0a, 0x0d, 0x44, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x53, - 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x36, 0x0a, 0x05, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, - 0x14, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x15, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, - 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x2d, - 0x0a, 0x02, 0x55, 0x70, 0x12, 0x11, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x55, 0x70, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, - 0x2e, 0x55, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x39, 0x0a, - 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x15, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, - 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, + 0x6f, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x90, 0x01, 0x0a, 0x0c, 0x4c, 0x6f, 0x67, + 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x65, 0x74, + 0x75, 0x70, 0x4b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x73, 0x65, 0x74, + 0x75, 0x70, 0x4b, 0x65, 0x79, 0x12, 0x22, 0x0a, 0x0c, 0x70, 0x72, 0x65, 0x53, 0x68, 0x61, 0x72, + 0x65, 0x64, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x70, 0x72, 0x65, + 0x53, 0x68, 0x61, 0x72, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x12, 0x24, 0x0a, 0x0d, 0x6d, 0x61, 0x6e, + 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x55, 0x72, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0d, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x55, 0x72, 0x6c, 0x12, + 0x1a, 0x0a, 0x08, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x55, 0x52, 0x4c, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x08, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x55, 0x52, 0x4c, 0x22, 0x0f, 0x0a, 0x0d, 0x4c, + 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x0b, 0x0a, 0x09, + 0x55, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x0c, 0x0a, 0x0a, 0x55, 0x70, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x0f, 0x0a, 0x0d, 0x53, 0x74, 0x61, 0x74, 0x75, + 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x28, 0x0a, 0x0e, 0x53, 0x74, 0x61, 0x74, + 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, + 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, + 0x75, 0x73, 0x22, 0x0d, 0x0a, 0x0b, 0x44, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x22, 0x0e, 0x0a, 0x0c, 0x44, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x22, 0x12, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xb3, 0x01, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x24, 0x0a, 0x0d, 0x6d, + 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x55, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0d, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x55, 0x72, + 0x6c, 0x12, 0x1e, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x46, 0x69, 0x6c, 0x65, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x46, 0x69, 0x6c, + 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6c, 0x6f, 0x67, 0x46, 0x69, 0x6c, 0x65, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x07, 0x6c, 0x6f, 0x67, 0x46, 0x69, 0x6c, 0x65, 0x12, 0x22, 0x0a, 0x0c, 0x70, + 0x72, 0x65, 0x53, 0x68, 0x61, 0x72, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0c, 0x70, 0x72, 0x65, 0x53, 0x68, 0x61, 0x72, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x12, + 0x1a, 0x0a, 0x08, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x55, 0x52, 0x4c, 0x18, 0x05, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x08, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x55, 0x52, 0x4c, 0x32, 0xaa, 0x02, 0x0a, 0x0d, + 0x44, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x36, 0x0a, + 0x05, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x14, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, + 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x15, 0x2e, 0x64, + 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x2d, 0x0a, 0x02, 0x55, 0x70, 0x12, 0x11, 0x2e, 0x64, 0x61, + 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x55, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, + 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x55, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x22, 0x00, 0x12, 0x39, 0x0a, 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x15, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x33, 0x0a, 0x04, 0x44, 0x6f, 0x77, 0x6e, - 0x12, 0x13, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x6f, 0x77, 0x6e, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, - 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x08, 0x5a, - 0x06, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, + 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, + 0x33, 0x0a, 0x04, 0x44, 0x6f, 0x77, 0x6e, 0x12, 0x13, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, + 0x2e, 0x44, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x64, + 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x22, 0x00, 0x12, 0x42, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x12, 0x18, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x64, 0x61, + 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x08, 0x5a, 0x06, 0x2f, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -415,28 +565,32 @@ func file_daemon_proto_rawDescGZIP() []byte { return file_daemon_proto_rawDescData } -var file_daemon_proto_msgTypes = make([]protoimpl.MessageInfo, 8) +var file_daemon_proto_msgTypes = make([]protoimpl.MessageInfo, 10) var file_daemon_proto_goTypes = []interface{}{ - (*LoginRequest)(nil), // 0: daemon.LoginRequest - (*LoginResponse)(nil), // 1: daemon.LoginResponse - (*UpRequest)(nil), // 2: daemon.UpRequest - (*UpResponse)(nil), // 3: daemon.UpResponse - (*StatusRequest)(nil), // 4: daemon.StatusRequest - (*StatusResponse)(nil), // 5: daemon.StatusResponse - (*DownRequest)(nil), // 6: daemon.DownRequest - (*DownResponse)(nil), // 7: daemon.DownResponse + (*LoginRequest)(nil), // 0: daemon.LoginRequest + (*LoginResponse)(nil), // 1: daemon.LoginResponse + (*UpRequest)(nil), // 2: daemon.UpRequest + (*UpResponse)(nil), // 3: daemon.UpResponse + (*StatusRequest)(nil), // 4: daemon.StatusRequest + (*StatusResponse)(nil), // 5: daemon.StatusResponse + (*DownRequest)(nil), // 6: daemon.DownRequest + (*DownResponse)(nil), // 7: daemon.DownResponse + (*GetConfigRequest)(nil), // 8: daemon.GetConfigRequest + (*GetConfigResponse)(nil), // 9: daemon.GetConfigResponse } var file_daemon_proto_depIdxs = []int32{ 0, // 0: daemon.DaemonService.Login:input_type -> daemon.LoginRequest 2, // 1: daemon.DaemonService.Up:input_type -> daemon.UpRequest 4, // 2: daemon.DaemonService.Status:input_type -> daemon.StatusRequest 6, // 3: daemon.DaemonService.Down:input_type -> daemon.DownRequest - 1, // 4: daemon.DaemonService.Login:output_type -> daemon.LoginResponse - 3, // 5: daemon.DaemonService.Up:output_type -> daemon.UpResponse - 5, // 6: daemon.DaemonService.Status:output_type -> daemon.StatusResponse - 7, // 7: daemon.DaemonService.Down:output_type -> daemon.DownResponse - 4, // [4:8] is the sub-list for method output_type - 0, // [0:4] is the sub-list for method input_type + 8, // 4: daemon.DaemonService.GetConfig:input_type -> daemon.GetConfigRequest + 1, // 5: daemon.DaemonService.Login:output_type -> daemon.LoginResponse + 3, // 6: daemon.DaemonService.Up:output_type -> daemon.UpResponse + 5, // 7: daemon.DaemonService.Status:output_type -> daemon.StatusResponse + 7, // 8: daemon.DaemonService.Down:output_type -> daemon.DownResponse + 9, // 9: daemon.DaemonService.GetConfig:output_type -> daemon.GetConfigResponse + 5, // [5:10] is the sub-list for method output_type + 0, // [0:5] is the sub-list for method input_type 0, // [0:0] is the sub-list for extension type_name 0, // [0:0] is the sub-list for extension extendee 0, // [0:0] is the sub-list for field type_name @@ -544,6 +698,30 @@ func file_daemon_proto_init() { return nil } } + file_daemon_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GetConfigRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_daemon_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GetConfigResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } } type x struct{} out := protoimpl.TypeBuilder{ @@ -551,7 +729,7 @@ func file_daemon_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_daemon_proto_rawDesc, NumEnums: 0, - NumMessages: 8, + NumMessages: 10, NumExtensions: 0, NumServices: 1, }, diff --git a/client/proto/daemon.proto b/client/proto/daemon.proto index 53b74f1fa..4eb8180e0 100644 --- a/client/proto/daemon.proto +++ b/client/proto/daemon.proto @@ -18,17 +18,23 @@ service DaemonService { // Down engine work in the daemon. rpc Down(DownRequest) returns (DownResponse) {} + + // GetConfig of the daemon. + rpc GetConfig(GetConfigRequest) returns (GetConfigResponse) {} }; message LoginRequest { // setupKey wiretrustee setup key. string setupKey = 1; - // presharedKey for wireguard setup. - string presharedKey = 2; + // preSharedKey for wireguard setup. + string preSharedKey = 2; // managementUrl to authenticate. string managementUrl = 3; + + // adminUrl to manage keys. + string adminURL = 4; } message LoginResponse {} @@ -47,3 +53,22 @@ message StatusResponse{ message DownRequest {} message DownResponse {} + +message GetConfigRequest {} + +message GetConfigResponse { + // managementUrl settings value. + string managementUrl = 1; + + // configFile settings value. + string configFile = 2; + + // logFile settings value. + string logFile = 3; + + // preSharedKey settings value. + string preSharedKey = 4; + + // adminURL settings value. + string adminURL = 5; +} diff --git a/client/proto/daemon_grpc.pb.go b/client/proto/daemon_grpc.pb.go index 8ef4c0e8d..1e28015d5 100644 --- a/client/proto/daemon_grpc.pb.go +++ b/client/proto/daemon_grpc.pb.go @@ -26,6 +26,8 @@ type DaemonServiceClient interface { Status(ctx context.Context, in *StatusRequest, opts ...grpc.CallOption) (*StatusResponse, error) // Down engine work in the daemon. Down(ctx context.Context, in *DownRequest, opts ...grpc.CallOption) (*DownResponse, error) + // GetConfig of the daemon. + GetConfig(ctx context.Context, in *GetConfigRequest, opts ...grpc.CallOption) (*GetConfigResponse, error) } type daemonServiceClient struct { @@ -72,6 +74,15 @@ func (c *daemonServiceClient) Down(ctx context.Context, in *DownRequest, opts .. return out, nil } +func (c *daemonServiceClient) GetConfig(ctx context.Context, in *GetConfigRequest, opts ...grpc.CallOption) (*GetConfigResponse, error) { + out := new(GetConfigResponse) + err := c.cc.Invoke(ctx, "/daemon.DaemonService/GetConfig", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + // DaemonServiceServer is the server API for DaemonService service. // All implementations must embed UnimplementedDaemonServiceServer // for forward compatibility @@ -84,6 +95,8 @@ type DaemonServiceServer interface { Status(context.Context, *StatusRequest) (*StatusResponse, error) // Down engine work in the daemon. Down(context.Context, *DownRequest) (*DownResponse, error) + // GetConfig of the daemon. + GetConfig(context.Context, *GetConfigRequest) (*GetConfigResponse, error) mustEmbedUnimplementedDaemonServiceServer() } @@ -103,6 +116,9 @@ func (UnimplementedDaemonServiceServer) Status(context.Context, *StatusRequest) func (UnimplementedDaemonServiceServer) Down(context.Context, *DownRequest) (*DownResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method Down not implemented") } +func (UnimplementedDaemonServiceServer) GetConfig(context.Context, *GetConfigRequest) (*GetConfigResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetConfig not implemented") +} func (UnimplementedDaemonServiceServer) mustEmbedUnimplementedDaemonServiceServer() {} // UnsafeDaemonServiceServer may be embedded to opt out of forward compatibility for this service. @@ -188,6 +204,24 @@ func _DaemonService_Down_Handler(srv interface{}, ctx context.Context, dec func( return interceptor(ctx, in, info, handler) } +func _DaemonService_GetConfig_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetConfigRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DaemonServiceServer).GetConfig(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/daemon.DaemonService/GetConfig", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DaemonServiceServer).GetConfig(ctx, req.(*GetConfigRequest)) + } + return interceptor(ctx, in, info, handler) +} + // DaemonService_ServiceDesc is the grpc.ServiceDesc for DaemonService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -211,6 +245,10 @@ var DaemonService_ServiceDesc = grpc.ServiceDesc{ MethodName: "Down", Handler: _DaemonService_Down_Handler, }, + { + MethodName: "GetConfig", + Handler: _DaemonService_GetConfig_Handler, + }, }, Streams: []grpc.StreamDesc{}, Metadata: "daemon.proto", diff --git a/client/server/server.go b/client/server/server.go index 5b9c934e3..2715c7a47 100644 --- a/client/server/server.go +++ b/client/server/server.go @@ -17,7 +17,9 @@ type Server struct { actCancel context.CancelFunc managementURL string + adminURL string configPath string + logFile string mutex sync.Mutex config *internal.Config @@ -25,11 +27,13 @@ type Server struct { } // New server instance constructor. -func New(ctx context.Context, managementURL, configPath string) *Server { +func New(ctx context.Context, managementURL, adminURL, configPath, logFile string) *Server { return &Server{ rootCtx: ctx, managementURL: managementURL, + adminURL: adminURL, configPath: configPath, + logFile: logFile, } } @@ -52,7 +56,7 @@ func (s *Server) Start() error { s.actCancel = cancel // if configuration exists, we just start connections. - config, err := internal.ReadConfig(s.managementURL, s.configPath) + config, err := internal.ReadConfig(s.managementURL, s.adminURL, s.configPath, nil) if err != nil { log.Warnf("no config file, skip connection stage: %v", err) return nil @@ -71,22 +75,42 @@ func (s *Server) Start() error { // Login uses setup key to prepare configuration for the daemon. func (s *Server) Login(_ context.Context, msg *proto.LoginRequest) (*proto.LoginResponse, error) { s.mutex.Lock() - defer s.mutex.Unlock() + if s.actCancel != nil { + s.actCancel() + } + ctx, cancel := context.WithCancel(s.rootCtx) + s.actCancel = cancel + s.mutex.Unlock() + state := internal.CtxGetState(ctx) + defer state.Set(internal.StatusIdle) + + state.Set(internal.StatusConnecting) + + s.mutex.Lock() managementURL := s.managementURL if msg.ManagementUrl != "" { managementURL = msg.ManagementUrl } - config, err := internal.GetConfig(managementURL, s.configPath, msg.PresharedKey) + adminURL := s.adminURL + if msg.AdminURL != "" { + adminURL = msg.AdminURL + } + s.mutex.Unlock() + + config, err := internal.GetConfig(managementURL, adminURL, s.configPath, msg.PreSharedKey) if err != nil { return nil, err } + + s.mutex.Lock() s.config = config + s.mutex.Unlock() // login operation uses backoff scheme to connect to management API // we don't wait for result and return response immediately. - if err := internal.Login(s.rootCtx, s.config, msg.SetupKey); err != nil { + if err := internal.Login(ctx, s.config, msg.SetupKey); err != nil { log.Errorf("failed login: %v", err) return nil, err } @@ -158,3 +182,32 @@ func (s *Server) Status(ctx context.Context, msg *proto.StatusRequest) (*proto.S return &proto.StatusResponse{Status: string(status)}, nil } + +// GetConfig of the daemon. +func (s *Server) GetConfig(ctx context.Context, msg *proto.GetConfigRequest) (*proto.GetConfigResponse, error) { + s.mutex.Lock() + defer s.mutex.Unlock() + + managementURL := s.managementURL + if managementURL == "" && s.config.ManagementURL != nil { + managementURL = s.config.ManagementURL.String() + } + + adminURL := s.adminURL + if s.config.AdminURL != nil { + adminURL = s.config.AdminURL.String() + } + + preSharedKey := s.config.PreSharedKey + if preSharedKey != "" { + preSharedKey = "**********" + } + + return &proto.GetConfigResponse{ + ManagementUrl: managementURL, + AdminURL: adminURL, + ConfigFile: s.configPath, + LogFile: s.logFile, + PreSharedKey: preSharedKey, + }, nil +} diff --git a/client/ui/client_ui.go b/client/ui/client_ui.go index 3b0e7281b..555f12aa0 100644 --- a/client/ui/client_ui.go +++ b/client/ui/client_ui.go @@ -6,6 +6,7 @@ import ( "fmt" "io/ioutil" "os" + "os/exec" "path" "runtime" "strconv" @@ -20,18 +21,24 @@ import ( "github.com/netbirdio/netbird/client/proto" 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" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/app" + "fyne.io/fyne/v2/dialog" + "fyne.io/fyne/v2/widget" +) + +const ( + defaulFailTimeout = time.Duration(3 * time.Second) + fastFailTimeout = time.Second ) func main() { var daemonAddr string - if err := checkPIDFile(); err != nil { - fmt.Println(err) - return - } - defaultDaemonAddr := "unix:///var/run/wiretrustee.sock" if runtime.GOOS == "windows" { defaultDaemonAddr = "tcp://127.0.0.1:41731" @@ -42,37 +49,177 @@ func main() { defaultDaemonAddr, "Daemon service address to serve CLI requests [unix|tcp]://[path|host:port]") + var showSettings bool + flag.BoolVar(&showSettings, "settings", false, "run settings windows") + flag.Parse() - client := newServiceClient(daemonAddr) - systray.Run(client.onTrayReady, client.onTrayExit) + a := app.New() + client := newServiceClient(daemonAddr, a, showSettings) + if showSettings { + a.Run() + } else { + if err := checkPIDFile(); err != nil { + fmt.Println(err) + return + } + systray.Run(client.onTrayReady, client.onTrayExit) + } } //go:embed connected.ico -var iconConnected []byte +var iconConnectedICO []byte + +//go:embed connected.png +var iconConnectedPNG []byte //go:embed disconnected.ico -var iconDisconnected []byte +var iconDisconnectedICO []byte + +//go:embed disconnected.png +var iconDisconnectedPNG []byte type serviceClient struct { - ctx context.Context - addr string - conn proto.DaemonServiceClient - mStatus *systray.MenuItem - mUp *systray.MenuItem - mDown *systray.MenuItem + ctx context.Context + addr string + conn proto.DaemonServiceClient + + icConnected []byte + icDisconnected []byte + + // systray menu itmes + mStatus *systray.MenuItem + mUp *systray.MenuItem + mDown *systray.MenuItem + mAdminPanel *systray.MenuItem + mSettings *systray.MenuItem + mQuit *systray.MenuItem + + // application with main windows. + app fyne.App + wSettings fyne.Window + showSettings bool + + // input elements for settings form + iMngURL *widget.Entry + iAdminURL *widget.Entry + iConfigFile *widget.Entry + iLogFile *widget.Entry + iPreSharedKey *widget.Entry + + // observable settings over correspondign iMngURL and iPreSharedKey values. + managementURL string + preSharedKey string + adminURL string } -func newServiceClient(addr string) *serviceClient { +// newServiceClient instance constructor +// +// This constructor olso build UI elements for settings window. +func newServiceClient(addr string, a fyne.App, showSettings bool) *serviceClient { s := &serviceClient{ ctx: context.Background(), addr: addr, + app: a, + + showSettings: showSettings, } + + if runtime.GOOS == "windows" { + s.icConnected = iconConnectedICO + s.icDisconnected = iconDisconnectedICO + } else { + s.icConnected = iconConnectedPNG + s.icDisconnected = iconDisconnectedPNG + } + + if showSettings { + s.showUIElements() + return s + } + return s } -func (s *serviceClient) up() error { - conn, err := s.client() +func (s *serviceClient) showUIElements() { + // add settings window UI elements. + s.wSettings = s.app.NewWindow("Settings") + 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.wSettings.SetContent(s.getSettingsForm()) + s.wSettings.Resize(fyne.NewSize(600, 100)) + + 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: "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 != "**********" { + // 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 + } + } + + defer s.wSettings.Close() + // if management URL or Pre-shared key changed, we try to re-login with new settings. + if s.managementURL != s.iMngURL.Text || s.preSharedKey != s.iPreSharedKey.Text || + s.adminURL != s.iAdminURL.Text { + + s.managementURL = s.iMngURL.Text + s.preSharedKey = s.iPreSharedKey.Text + s.adminURL = s.iAdminURL.Text + + client, err := s.getSrvClient(fastFailTimeout) + if err != nil { + log.Errorf("get daemon client: %v", err) + return + } + + _, err = client.Login(s.ctx, &proto.LoginRequest{ + ManagementUrl: s.iMngURL.Text, + AdminURL: s.iAdminURL.Text, + PreSharedKey: s.iPreSharedKey.Text, + }) + if err != nil { + log.Errorf("login to management URL: %v", err) + return + } + + _, err = client.Up(s.ctx, &proto.UpRequest{}) + if err != nil { + log.Errorf("login to management URL: %v", err) + return + } + } + s.wSettings.Close() + }, + OnCancel: func() { + s.wSettings.Close() + }, + } +} + +func (s *serviceClient) menuUpClick() error { + conn, err := s.getSrvClient(defaulFailTimeout) if err != nil { log.Errorf("get client: %v", err) return err @@ -97,8 +244,8 @@ func (s *serviceClient) up() error { return nil } -func (s *serviceClient) down() error { - conn, err := s.client() +func (s *serviceClient) menuDownClick() error { + conn, err := s.getSrvClient(defaulFailTimeout) if err != nil { log.Errorf("get client: %v", err) return err @@ -124,7 +271,7 @@ func (s *serviceClient) down() error { } func (s *serviceClient) updateStatus() { - conn, err := s.client() + conn, err := s.getSrvClient(defaulFailTimeout) if err != nil { log.Errorf("get client: %v", err) return @@ -137,12 +284,12 @@ func (s *serviceClient) updateStatus() { } if status.Status == string(internal.StatusConnected) { - systray.SetTemplateIcon(iconConnected, iconConnected) + systray.SetIcon(s.icConnected) s.mStatus.SetTitle("Connected") s.mUp.Disable() s.mDown.Enable() } else { - systray.SetTemplateIcon(iconDisconnected, iconDisconnected) + systray.SetIcon(s.icDisconnected) s.mStatus.SetTitle("Disconnected") s.mDown.Disable() s.mUp.Enable() @@ -150,25 +297,23 @@ func (s *serviceClient) updateStatus() { } func (s *serviceClient) onTrayReady() { - systray.SetTemplateIcon(iconDisconnected, iconDisconnected) + systray.SetIcon(s.icDisconnected) + // setup systray menu items s.mStatus = systray.AddMenuItem("Disconnected", "Disconnected") s.mStatus.Disable() - systray.AddSeparator() - s.mUp = systray.AddMenuItem("Connect", "Connect") - s.mDown = systray.AddMenuItem("Disconnect", "Disconnect") s.mDown.Disable() - - mURL := systray.AddMenuItem("Admin Panel", "Wiretrustee Admin Panel") - + s.mAdminPanel = systray.AddMenuItem("Admin Panel", "Wiretrustee Admin Panel") systray.AddSeparator() - - mQuit := systray.AddMenuItem("Quit", "Quit the client app") + s.mSettings = systray.AddMenuItem("Settings", "Settings of the application") + systray.AddSeparator() + s.mQuit = systray.AddMenuItem("Quit", "Quit the client app") go func() { + s.getSrvConfig() for { s.updateStatus() time.Sleep(time.Second * 3) @@ -179,19 +324,42 @@ func (s *serviceClient) onTrayReady() { var err error for { select { - case <-mURL.ClickedCh: - err = open.Run("https://app.wiretrustee.com") + case <-s.mAdminPanel.ClickedCh: + err = open.Run(s.adminURL) case <-s.mUp.ClickedCh: s.mUp.Disable() - if err = s.up(); err != nil { + if err = s.menuUpClick(); err != nil { s.mUp.Enable() } case <-s.mDown.ClickedCh: s.mDown.Disable() - if err = s.down(); err != nil { + if err = s.menuDownClick(); err != nil { s.mDown.Enable() } - case <-mQuit.ClickedCh: + case <-s.mSettings.ClickedCh: + s.mSettings.Disable() + go func() { + defer s.mSettings.Enable() + proc, err := os.Executable() + if err != nil { + log.Errorf("show settings: %v", err) + return + } + + cmd := exec.Command(proc, "--settings=true") + out, err := cmd.CombinedOutput() + if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() == 1 { + log.Errorf("start settings UI: %v, %s", err, string(out)) + return + } + if len(out) != 0 { + log.Info("settings change:", string(out)) + } + + // update config in systray when settings windows closed + s.getSrvConfig() + }() + case <-s.mQuit.ClickedCh: systray.Quit() return } @@ -204,12 +372,13 @@ func (s *serviceClient) onTrayReady() { func (s *serviceClient) onTrayExit() {} -func (s *serviceClient) client() (proto.DaemonServiceClient, error) { +// 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(context.Background(), time.Second*3) + ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() conn, err := grpc.DialContext( @@ -226,6 +395,40 @@ func (s *serviceClient) client() (proto.DaemonServiceClient, error) { return s.conn, nil } +// getSrvConfig from the service to show it in the settings window. +func (s *serviceClient) getSrvConfig() { + s.managementURL = "https://api.wiretrustee.com:33073" + s.adminURL = "https://app.netbird.io" + + conn, err := s.getSrvClient(fastFailTimeout) + 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 + + if s.showSettings { + 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) + } +} + // checkPIDFile exists and return error, or write new. func checkPIDFile() error { pidFile := path.Join(os.TempDir(), "wiretrustee-ui.pid") diff --git a/client/ui/connected.png b/client/ui/connected.png new file mode 100644 index 0000000000000000000000000000000000000000..64b25ade12e36b1b1f8ca695f4fac45c38cf162a GIT binary patch literal 10290 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 QuOTLsqB09ee8q`qPM+}8QBI! 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 G=KlcAE9s5^ literal 0 HcmV?d00001 diff --git a/client/ui/netbird.desktop b/client/ui/netbird.desktop new file mode 100644 index 000000000..c4ac46765 --- /dev/null +++ b/client/ui/netbird.desktop @@ -0,0 +1,8 @@ +[Desktop Entry] +Name=Netbird Agent +Exec=/usr/bin/wiretrustee-ui +Icon=netbird +Type=Application +Terminal=false +Categories=Utility; +Keywords=netbird; diff --git a/client/ui/wiretrustee.desktop b/client/ui/wiretrustee.desktop deleted file mode 100644 index 0f39a9a3c..000000000 --- a/client/ui/wiretrustee.desktop +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/env xdg-open -[Desktop Entry] -Version=1.0 -Terminal=false -Type=Application -Name=Wiretrustee UI -Exec=/usr/bin/wiretrustee-ui -Icon=/usr/share/icons/hicolor/256x256/wiretrustee.png -Comment=Wiretrustee client UI. diff --git a/client/ui/wiretrustee.png b/client/ui/wiretrustee.png deleted file mode 100644 index 296be93b0d253c321456781a1ced5697a600c3cf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12868 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