From 0c2809971249808b18c315d05e461ce8af2e8854 Mon Sep 17 00:00:00 2001 From: adasauce <60991921+adasauce@users.noreply.github.com> Date: Tue, 14 Jan 2025 05:38:08 -0400 Subject: [PATCH 01/92] [management] enable optional zitadel configuration of a PAT (#3159) * [management] enable optional zitadel configuration of a PAT for service user via the ExtraConfig fields * [management] validate both PAT and JWT configurations for zitadel --- management/server/idp/idp.go | 1 + management/server/idp/zitadel.go | 61 ++++++++++++++++++++++++-------- 2 files changed, 48 insertions(+), 14 deletions(-) diff --git a/management/server/idp/idp.go b/management/server/idp/idp.go index 419220942..0f1ff0f1f 100644 --- a/management/server/idp/idp.go +++ b/management/server/idp/idp.go @@ -149,6 +149,7 @@ func NewManager(ctx context.Context, config Config, appMetrics telemetry.AppMetr GrantType: config.ClientConfig.GrantType, TokenEndpoint: config.ClientConfig.TokenEndpoint, ManagementEndpoint: config.ExtraConfig["ManagementEndpoint"], + PAT: config.ExtraConfig["PAT"], } } diff --git a/management/server/idp/zitadel.go b/management/server/idp/zitadel.go index 9d7626844..343357927 100644 --- a/management/server/idp/zitadel.go +++ b/management/server/idp/zitadel.go @@ -34,6 +34,7 @@ type ZitadelClientConfig struct { GrantType string TokenEndpoint string ManagementEndpoint string + PAT string } // ZitadelCredentials zitadel authentication information. @@ -135,6 +136,28 @@ func readZitadelError(body io.ReadCloser) error { return errors.New(strings.Join(errsOut, " ")) } +// verifyJWTConfig ensures necessary values are set in the ZitadelClientConfig for JWTs to be generated. +func verifyJWTConfig(config ZitadelClientConfig) error { + + if config.ClientID == "" { + return fmt.Errorf("zitadel IdP configuration is incomplete, clientID is missing") + } + + if config.ClientSecret == "" { + return fmt.Errorf("zitadel IdP configuration is incomplete, ClientSecret is missing") + } + + if config.TokenEndpoint == "" { + return fmt.Errorf("zitadel IdP configuration is incomplete, TokenEndpoint is missing") + } + + if config.GrantType == "" { + return fmt.Errorf("zitadel IdP configuration is incomplete, GrantType is missing") + } + + return nil +} + // NewZitadelManager creates a new instance of the ZitadelManager. func NewZitadelManager(config ZitadelClientConfig, appMetrics telemetry.AppMetrics) (*ZitadelManager, error) { httpTransport := http.DefaultTransport.(*http.Transport).Clone() @@ -146,26 +169,18 @@ func NewZitadelManager(config ZitadelClientConfig, appMetrics telemetry.AppMetri } helper := JsonParser{} - if config.ClientID == "" { - return nil, fmt.Errorf("zitadel IdP configuration is incomplete, clientID is missing") - } - - if config.ClientSecret == "" { - return nil, fmt.Errorf("zitadel IdP configuration is incomplete, ClientSecret is missing") - } - - if config.TokenEndpoint == "" { - return nil, fmt.Errorf("zitadel IdP configuration is incomplete, TokenEndpoint is missing") + hasPAT := config.PAT != "" + if !hasPAT { + jwtErr := verifyJWTConfig(config) + if jwtErr != nil { + return nil, jwtErr + } } if config.ManagementEndpoint == "" { return nil, fmt.Errorf("zitadel IdP configuration is incomplete, ManagementEndpoint is missing") } - if config.GrantType == "" { - return nil, fmt.Errorf("zitadel IdP configuration is incomplete, GrantType is missing") - } - credentials := &ZitadelCredentials{ clientConfig: config, httpClient: httpClient, @@ -254,6 +269,20 @@ func (zc *ZitadelCredentials) parseRequestJWTResponse(rawBody io.ReadCloser) (JW return jwtToken, nil } +// generatePATToken creates a functional JWTToken instance which will pass the +// PAT to the API directly and skip requesting a token. +func (zc *ZitadelCredentials) generatePATToken() (JWTToken, error) { + tok := JWTToken{ + AccessToken: zc.clientConfig.PAT, + Scope: "openid", + ExpiresIn: 9999, + TokenType: "PAT", + } + tok.expiresInTime = time.Now().Add(time.Duration(tok.ExpiresIn) * time.Second) + zc.jwtToken = tok + return tok, nil +} + // Authenticate retrieves access token to use the Zitadel Management API. func (zc *ZitadelCredentials) Authenticate(ctx context.Context) (JWTToken, error) { zc.mux.Lock() @@ -269,6 +298,10 @@ func (zc *ZitadelCredentials) Authenticate(ctx context.Context) (JWTToken, error return zc.jwtToken, nil } + if zc.clientConfig.PAT != "" { + return zc.generatePATToken() + } + resp, err := zc.requestJWTToken(ctx) if err != nil { return zc.jwtToken, err From 9b5b632ff9d4705c74f10af4df2a2e2f6efff9bc Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Tue, 14 Jan 2025 10:39:37 +0100 Subject: [PATCH 02/92] [client] Support non-openresolv for DNS on Linux (#3176) --- client/internal/dns/resolvconf_unix.go | 72 +++++++++++++++++++++++--- 1 file changed, 66 insertions(+), 6 deletions(-) diff --git a/client/internal/dns/resolvconf_unix.go b/client/internal/dns/resolvconf_unix.go index a5d1cc8a2..6b5fdaf86 100644 --- a/client/internal/dns/resolvconf_unix.go +++ b/client/internal/dns/resolvconf_unix.go @@ -7,6 +7,7 @@ import ( "fmt" "net/netip" "os/exec" + "strings" log "github.com/sirupsen/logrus" @@ -15,23 +16,64 @@ import ( const resolvconfCommand = "resolvconf" +// resolvconfType represents the type of resolvconf implementation +type resolvconfType int + +func (r resolvconfType) String() string { + switch r { + case typeOpenresolv: + return "openresolv" + case typeResolvconf: + return "resolvconf" + default: + return "unknown" + } +} + +const ( + typeOpenresolv resolvconfType = iota + typeResolvconf +) + type resolvconf struct { ifaceName string + implType resolvconfType originalSearchDomains []string originalNameServers []string othersConfigs []string } -// supported "openresolv" only +func detectResolvconfType() (resolvconfType, error) { + cmd := exec.Command(resolvconfCommand, "--version") + out, err := cmd.Output() + if err != nil { + return typeOpenresolv, fmt.Errorf("failed to determine resolvconf type: %w", err) + } + + if strings.Contains(string(out), "openresolv") { + return typeOpenresolv, nil + } + return typeResolvconf, nil +} + func newResolvConfConfigurator(wgInterface string) (*resolvconf, error) { resolvConfEntries, err := parseDefaultResolvConf() if err != nil { log.Errorf("could not read original search domains from %s: %s", defaultResolvConfPath, err) } + implType, err := detectResolvconfType() + if err != nil { + log.Warnf("failed to detect resolvconf type, defaulting to openresolv: %v", err) + implType = typeOpenresolv + } else { + log.Infof("detected resolvconf type: %v", implType) + } + return &resolvconf{ ifaceName: wgInterface, + implType: implType, originalSearchDomains: resolvConfEntries.searchDomains, originalNameServers: resolvConfEntries.nameServers, othersConfigs: resolvConfEntries.others, @@ -80,8 +122,15 @@ func (r *resolvconf) applyDNSConfig(config HostDNSConfig, stateManager *stateman } func (r *resolvconf) restoreHostDNS() error { - // openresolv only, debian resolvconf doesn't support "-f" - cmd := exec.Command(resolvconfCommand, "-f", "-d", r.ifaceName) + var cmd *exec.Cmd + + switch r.implType { + case typeOpenresolv: + cmd = exec.Command(resolvconfCommand, "-f", "-d", r.ifaceName) + case typeResolvconf: + cmd = exec.Command(resolvconfCommand, "-d", r.ifaceName) + } + _, err := cmd.Output() if err != nil { return fmt.Errorf("removing resolvconf configuration for %s interface: %w", r.ifaceName, err) @@ -91,10 +140,21 @@ func (r *resolvconf) restoreHostDNS() error { } func (r *resolvconf) applyConfig(content bytes.Buffer) error { - // openresolv only, debian resolvconf doesn't support "-x" - cmd := exec.Command(resolvconfCommand, "-x", "-a", r.ifaceName) + var cmd *exec.Cmd + + switch r.implType { + case typeOpenresolv: + // OpenResolv supports exclusive mode with -x + cmd = exec.Command(resolvconfCommand, "-x", "-a", r.ifaceName) + case typeResolvconf: + cmd = exec.Command(resolvconfCommand, "-a", r.ifaceName) + default: + return fmt.Errorf("unsupported resolvconf type: %v", r.implType) + } + cmd.Stdin = &content - _, err := cmd.Output() + out, err := cmd.Output() + log.Tracef("resolvconf output: %s", out) if err != nil { return fmt.Errorf("applying resolvconf configuration for %s interface: %w", r.ifaceName, err) } From 15f0a665f816d50bfed08ce356832b2be79c972e Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Tue, 14 Jan 2025 12:43:13 +0100 Subject: [PATCH 03/92] [client] Allow ssh server on freebsd (#3170) * Enable ssh server on freebsd * Fix listening in netstack mode * Fix panic if login cmd fails * Tidy up go mod --- client/cmd/ssh.go | 3 +-- client/internal/engine.go | 15 ++++++++++----- client/internal/engine_test.go | 3 +-- client/ssh/login.go | 31 +++++++++++++++++++++++-------- client/ssh/lookup.go | 4 ++++ client/ssh/lookup_darwin.go | 4 ++++ client/ssh/server.go | 6 +++++- go.mod | 8 ++++---- go.sum | 17 ++++++++--------- 9 files changed, 60 insertions(+), 31 deletions(-) diff --git a/client/cmd/ssh.go b/client/cmd/ssh.go index 81e6c255a..f9dbc26fc 100644 --- a/client/cmd/ssh.go +++ b/client/cmd/ssh.go @@ -9,7 +9,6 @@ import ( "strings" "syscall" - log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/netbirdio/netbird/client/internal" @@ -73,7 +72,7 @@ var sshCmd = &cobra.Command{ go func() { // blocking if err := runSSH(sshctx, host, []byte(config.SSHKey), cmd); err != nil { - log.Debug(err) + cmd.Printf("Error: %v\n", err) os.Exit(1) } cancel() diff --git a/client/internal/engine.go b/client/internal/engine.go index b50532b7d..a5247bc27 100644 --- a/client/internal/engine.go +++ b/client/internal/engine.go @@ -27,6 +27,7 @@ import ( "github.com/netbirdio/netbird/client/iface" "github.com/netbirdio/netbird/client/iface/bind" "github.com/netbirdio/netbird/client/iface/device" + "github.com/netbirdio/netbird/client/iface/netstack" "github.com/netbirdio/netbird/client/internal/acl" "github.com/netbirdio/netbird/client/internal/dns" "github.com/netbirdio/netbird/client/internal/dnsfwd" @@ -699,18 +700,22 @@ func (e *Engine) updateSSH(sshConf *mgmProto.SSHConfig) error { } else { if sshConf.GetSshEnabled() { - if runtime.GOOS == "windows" || runtime.GOOS == "freebsd" { + if runtime.GOOS == "windows" { log.Warnf("running SSH server on %s is not supported", runtime.GOOS) return nil } // start SSH server if it wasn't running if isNil(e.sshServer) { + listenAddr := fmt.Sprintf("%s:%d", e.wgInterface.Address().IP.String(), nbssh.DefaultSSHPort) + if netstack.IsEnabled() { + listenAddr = fmt.Sprintf("127.0.0.1:%d", nbssh.DefaultSSHPort) + } // nil sshServer means it has not yet been started var err error - e.sshServer, err = e.sshServerFunc(e.config.SSHKey, - fmt.Sprintf("%s:%d", e.wgInterface.Address().IP.String(), nbssh.DefaultSSHPort)) + e.sshServer, err = e.sshServerFunc(e.config.SSHKey, listenAddr) + if err != nil { - return err + return fmt.Errorf("create ssh server: %w", err) } go func() { // blocking @@ -759,7 +764,7 @@ func (e *Engine) updateConfig(conf *mgmProto.PeerConfig) error { if conf.GetSshConfig() != nil { err := e.updateSSH(conf.GetSshConfig()) if err != nil { - log.Warnf("failed handling SSH server setup %v", err) + log.Warnf("failed handling SSH server setup: %v", err) } } diff --git a/client/internal/engine_test.go b/client/internal/engine_test.go index 1deea1cb8..ca49eca09 100644 --- a/client/internal/engine_test.go +++ b/client/internal/engine_test.go @@ -71,8 +71,7 @@ func TestMain(m *testing.M) { } func TestEngine_SSH(t *testing.T) { - // todo resolve test execution on freebsd - if runtime.GOOS == "windows" || runtime.GOOS == "freebsd" { + if runtime.GOOS == "windows" { t.Skip("skipping TestEngine_SSH") } diff --git a/client/ssh/login.go b/client/ssh/login.go index e6019578d..d1d56ceb0 100644 --- a/client/ssh/login.go +++ b/client/ssh/login.go @@ -2,14 +2,29 @@ package ssh import ( "fmt" - "github.com/netbirdio/netbird/util" "net" "net/netip" + "os" "os/exec" "runtime" + + "github.com/netbirdio/netbird/util" ) +func isRoot() bool { + return os.Geteuid() == 0 +} + func getLoginCmd(user string, remoteAddr net.Addr) (loginPath string, args []string, err error) { + if !isRoot() { + shell := getUserShell(user) + if shell == "" { + shell = "/bin/sh" + } + + return shell, []string{"-l"}, nil + } + loginPath, err = exec.LookPath("login") if err != nil { return "", nil, err @@ -20,17 +35,17 @@ func getLoginCmd(user string, remoteAddr net.Addr) (loginPath string, args []str return "", nil, err } - if runtime.GOOS == "linux" { - + switch runtime.GOOS { + case "linux": if util.FileExists("/etc/arch-release") && !util.FileExists("/etc/pam.d/remote") { - // detect if Arch Linux return loginPath, []string{"-f", user, "-p"}, nil } - return loginPath, []string{"-f", user, "-h", addrPort.Addr().String(), "-p"}, nil - } else if runtime.GOOS == "darwin" { + case "darwin": return loginPath, []string{"-fp", "-h", addrPort.Addr().String(), user}, nil + case "freebsd": + return loginPath, []string{"-f", user, "-h", addrPort.Addr().String(), "-p"}, nil + default: + return "", nil, fmt.Errorf("unsupported platform: %s", runtime.GOOS) } - - return "", nil, fmt.Errorf("unsupported platform") } diff --git a/client/ssh/lookup.go b/client/ssh/lookup.go index 7acef8f0b..9a7f6ff2e 100644 --- a/client/ssh/lookup.go +++ b/client/ssh/lookup.go @@ -6,5 +6,9 @@ package ssh import "os/user" func userNameLookup(username string) (*user.User, error) { + if username == "" || (username == "root" && !isRoot()) { + return user.Current() + } + return user.Lookup(username) } diff --git a/client/ssh/lookup_darwin.go b/client/ssh/lookup_darwin.go index e6f3c3b93..913d049dc 100644 --- a/client/ssh/lookup_darwin.go +++ b/client/ssh/lookup_darwin.go @@ -12,6 +12,10 @@ import ( ) func userNameLookup(username string) (*user.User, error) { + if username == "" || (username == "root" && !isRoot()) { + return user.Current() + } + var userObject *user.User userObject, err := user.Lookup(username) if err != nil && err.Error() == user.UnknownUserError(username).Error() { diff --git a/client/ssh/server.go b/client/ssh/server.go index a390302b7..1f2001d0f 100644 --- a/client/ssh/server.go +++ b/client/ssh/server.go @@ -168,8 +168,12 @@ func (srv *DefaultServer) sessionHandler(session ssh.Session) { cmd := exec.Command(loginCmd, loginArgs...) go func() { <-session.Context().Done() + if cmd.Process == nil { + return + } err := cmd.Process.Kill() if err != nil { + log.Debugf("failed killing SSH process %v", err) return } }() @@ -185,7 +189,7 @@ func (srv *DefaultServer) sessionHandler(session ssh.Session) { log.Debugf("Login command: %s", cmd.String()) file, err := pty.Start(cmd) if err != nil { - log.Errorf("failed starting SSH server %v", err) + log.Errorf("failed starting SSH server: %v", err) } go func() { diff --git a/go.mod b/go.mod index 0c6d6be99..1d738dbae 100644 --- a/go.mod +++ b/go.mod @@ -19,8 +19,8 @@ require ( github.com/spf13/cobra v1.7.0 github.com/spf13/pflag v1.0.5 github.com/vishvananda/netlink v1.2.1-beta.2 - golang.org/x/crypto v0.31.0 - golang.org/x/sys v0.28.0 + golang.org/x/crypto v0.32.0 + golang.org/x/sys v0.29.0 golang.zx2c4.com/wireguard v0.0.0-20230704135630-469159ecf7d1 golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 golang.zx2c4.com/wireguard/windows v0.5.3 @@ -41,7 +41,7 @@ require ( github.com/davecgh/go-spew v1.1.1 github.com/eko/gocache/v3 v3.1.1 github.com/fsnotify/fsnotify v1.7.0 - github.com/gliderlabs/ssh v0.3.4 + github.com/gliderlabs/ssh v0.3.8 github.com/godbus/dbus/v5 v5.1.0 github.com/golang/mock v1.6.0 github.com/google/go-cmp v0.6.0 @@ -94,7 +94,7 @@ require ( golang.org/x/net v0.30.0 golang.org/x/oauth2 v0.19.0 golang.org/x/sync v0.10.0 - golang.org/x/term v0.27.0 + golang.org/x/term v0.28.0 google.golang.org/api v0.177.0 gopkg.in/yaml.v3 v3.0.1 gorm.io/driver/mysql v1.5.7 diff --git a/go.sum b/go.sum index f8b6c208b..5ca6c007e 100644 --- a/go.sum +++ b/go.sum @@ -212,8 +212,8 @@ github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2H github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmCsR2Do= -github.com/gliderlabs/ssh v0.3.4 h1:+AXBtim7MTKaLVPgvE+3mhewYRawNLTd+jEEz/wExZw= -github.com/gliderlabs/ssh v0.3.4/go.mod h1:ZSS+CUoKHDrqVakTfTWUlKSr9MtMFkC4UvtQKD7O914= +github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= +github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= github.com/go-gl/gl v0.0.0-20211210172815-726fda9656d6 h1:zDw5v7qm4yH7N8C8uWd+8Ii9rROdgWxQuGoJ9WDXxfk= github.com/go-gl/gl v0.0.0-20211210172815-726fda9656d6/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= @@ -776,14 +776,13 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= -golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= -golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= +golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -982,8 +981,8 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= -golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -991,8 +990,8 @@ golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= -golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= -golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= +golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg= +golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= From 516de93627b11850ae33081a64fd3d93e21bdb97 Mon Sep 17 00:00:00 2001 From: James Hilliard Date: Wed, 15 Jan 2025 11:54:51 +0200 Subject: [PATCH 04/92] [client] Fix gvisor.dev/gvisor commit (#3179) Commit b8a429915ff1 was replaced with db3d49b921f9 in gvisor project. --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 1d738dbae..147577cc3 100644 --- a/go.mod +++ b/go.mod @@ -233,7 +233,7 @@ require ( gopkg.in/square/go-jose.v2 v2.6.0 // indirect gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637 // indirect - gvisor.dev/gvisor v0.0.0-20231020174304-b8a429915ff1 // indirect + gvisor.dev/gvisor v0.0.0-20231020174304-db3d49b921f9 // indirect k8s.io/apimachinery v0.26.2 // indirect ) diff --git a/go.sum b/go.sum index 5ca6c007e..253429798 100644 --- a/go.sum +++ b/go.sum @@ -1241,8 +1241,8 @@ gorm.io/gorm v1.25.7 h1:VsD6acwRjz2zFxGO50gPO6AkNs7KKnvfzUjHQhZDz/A= gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= gotest.tools/v3 v3.5.0 h1:Ljk6PdHdOhAb5aDMWXjDLMMhph+BpztA4v1QdqEW2eY= gotest.tools/v3 v3.5.0/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= -gvisor.dev/gvisor v0.0.0-20231020174304-b8a429915ff1 h1:qDCwdCWECGnwQSQC01Dpnp09fRHxJs9PbktotUqG+hs= -gvisor.dev/gvisor v0.0.0-20231020174304-b8a429915ff1/go.mod h1:8hmigyCdYtw5xJGfQDJzSH5Ju8XEIDBnpyi8+O6GRt8= +gvisor.dev/gvisor v0.0.0-20231020174304-db3d49b921f9 h1:sCEaoA7ZmkuFwa2IR61pl4+RYZPwCJOiaSYT0k+BRf8= +gvisor.dev/gvisor v0.0.0-20231020174304-db3d49b921f9/go.mod h1:8hmigyCdYtw5xJGfQDJzSH5Ju8XEIDBnpyi8+O6GRt8= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= From b9efda3ce8a9c4c7de8a9ffc74034a8a43c85bfe Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Wed, 15 Jan 2025 13:14:13 +0100 Subject: [PATCH 05/92] [client] Disable DNS host manager for netstack mode (#3183) --- client/internal/dns/server.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/client/internal/dns/server.go b/client/internal/dns/server.go index bb097c4cb..1fe913fd9 100644 --- a/client/internal/dns/server.go +++ b/client/internal/dns/server.go @@ -12,6 +12,7 @@ import ( "github.com/mitchellh/hashstructure/v2" log "github.com/sirupsen/logrus" + "github.com/netbirdio/netbird/client/iface/netstack" "github.com/netbirdio/netbird/client/internal/listener" "github.com/netbirdio/netbird/client/internal/peer" "github.com/netbirdio/netbird/client/internal/statemanager" @@ -239,7 +240,10 @@ func (s *DefaultServer) Initialize() (err error) { s.stateManager.RegisterState(&ShutdownState{}) - if s.disableSys { + // use noop host manager if requested or running in netstack mode. + // Netstack mode currently doesn't have a way to receive DNS requests. + // TODO: Use listener on localhost in netstack mode when running as root. + if s.disableSys || netstack.IsEnabled() { log.Info("system DNS is disabled, not setting up host manager") s.hostManager = &noopHostConfigurator{} return nil From b34887a92055f740b17633fb7624625f55f59c4c Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Wed, 15 Jan 2025 13:14:46 +0100 Subject: [PATCH 06/92] [client] Fix a panic on shutdown if dns host manager failed to initialize (#3182) --- client/internal/dns/host_unix.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/client/internal/dns/host_unix.go b/client/internal/dns/host_unix.go index 7bd4aec64..297d50822 100644 --- a/client/internal/dns/host_unix.go +++ b/client/internal/dns/host_unix.go @@ -48,11 +48,17 @@ type restoreHostManager interface { func newHostManager(wgInterface string) (hostManager, error) { osManager, err := getOSDNSManagerType() if err != nil { - return nil, err + return nil, fmt.Errorf("get os dns manager type: %w", err) } log.Infof("System DNS manager discovered: %s", osManager) - return newHostManagerFromType(wgInterface, osManager) + mgr, err := newHostManagerFromType(wgInterface, osManager) + // need to explicitly return nil mgr on error to avoid returning a non-nil interface containing a nil value + if err != nil { + return nil, fmt.Errorf("create host manager: %w", err) + } + + return mgr, nil } func newHostManagerFromType(wgInterface string, osManager osManagerType) (restoreHostManager, error) { From 6a6b527f24b5f0e3843bda0e89366dbceb242291 Mon Sep 17 00:00:00 2001 From: Zoltan Papp Date: Wed, 15 Jan 2025 16:01:08 +0100 Subject: [PATCH 07/92] [relay] Code cleaning (#3074) - Keep message byte processing in message.go file - Add new unit tests --- relay/client/client.go | 13 +++-- relay/messages/message.go | 95 +++++++++++++++++----------------- relay/messages/message_test.go | 89 +++++++++++++++++++++++++++++-- relay/server/handshake.go | 10 ++-- relay/server/peer.go | 6 +-- 5 files changed, 149 insertions(+), 64 deletions(-) diff --git a/relay/client/client.go b/relay/client/client.go index db5252f50..bccd85c93 100644 --- a/relay/client/client.go +++ b/relay/client/client.go @@ -306,7 +306,7 @@ func (c *Client) handShake() error { return fmt.Errorf("validate version: %w", err) } - msgType, err := messages.DetermineServerMessageType(buf[messages.SizeOfVersionByte:n]) + msgType, err := messages.DetermineServerMessageType(buf[:n]) if err != nil { c.log.Errorf("failed to determine message type: %s", err) return err @@ -317,7 +317,7 @@ func (c *Client) handShake() error { return fmt.Errorf("unexpected message type") } - addr, err := messages.UnmarshalAuthResponse(buf[messages.SizeOfProtoHeader:n]) + addr, err := messages.UnmarshalAuthResponse(buf[:n]) if err != nil { return err } @@ -348,24 +348,27 @@ func (c *Client) readLoop(relayConn net.Conn) { c.log.Debugf("failed to read message from relay server: %s", errExit) } c.mu.Unlock() + c.bufPool.Put(bufPtr) break } - _, err := messages.ValidateVersion(buf[:n]) + buf = buf[:n] + + _, err := messages.ValidateVersion(buf) if err != nil { c.log.Errorf("failed to validate protocol version: %s", err) c.bufPool.Put(bufPtr) continue } - msgType, err := messages.DetermineServerMessageType(buf[messages.SizeOfVersionByte:n]) + msgType, err := messages.DetermineServerMessageType(buf) if err != nil { c.log.Errorf("failed to determine message type: %s", err) c.bufPool.Put(bufPtr) continue } - if !c.handleMsg(msgType, buf[messages.SizeOfProtoHeader:n], bufPtr, hc, internallyStoppedFlag) { + if !c.handleMsg(msgType, buf, bufPtr, hc, internallyStoppedFlag) { break } } diff --git a/relay/messages/message.go b/relay/messages/message.go index 39ca0aa90..7794c57bc 100644 --- a/relay/messages/message.go +++ b/relay/messages/message.go @@ -23,20 +23,26 @@ const ( MsgTypeAuth = 6 MsgTypeAuthResponse = 7 - SizeOfVersionByte = 1 - SizeOfMsgType = 1 + // base size of the message + sizeOfVersionByte = 1 + sizeOfMsgType = 1 + sizeOfProtoHeader = sizeOfVersionByte + sizeOfMsgType - SizeOfProtoHeader = SizeOfVersionByte + SizeOfMsgType - - sizeOfMagicByte = 4 - - headerSizeTransport = IDSize + // auth message + sizeOfMagicByte = 4 + headerSizeAuth = sizeOfMagicByte + IDSize + offsetMagicByte = sizeOfProtoHeader + offsetAuthPeerID = sizeOfProtoHeader + sizeOfMagicByte + headerTotalSizeAuth = sizeOfProtoHeader + headerSizeAuth + // hello message headerSizeHello = sizeOfMagicByte + IDSize headerSizeHelloResp = 0 - headerSizeAuth = sizeOfMagicByte + IDSize - headerSizeAuthResp = 0 + // transport + headerSizeTransport = IDSize + offsetTransportID = sizeOfProtoHeader + headerTotalSizeTransport = sizeOfProtoHeader + headerSizeTransport ) var ( @@ -73,7 +79,7 @@ func (m MsgType) String() string { // ValidateVersion checks if the given version is supported by the protocol func ValidateVersion(msg []byte) (int, error) { - if len(msg) < SizeOfVersionByte { + if len(msg) < sizeOfProtoHeader { return 0, ErrInvalidMessageLength } version := int(msg[0]) @@ -85,11 +91,11 @@ func ValidateVersion(msg []byte) (int, error) { // DetermineClientMessageType determines the message type from the first the message func DetermineClientMessageType(msg []byte) (MsgType, error) { - if len(msg) < SizeOfMsgType { + if len(msg) < sizeOfProtoHeader { return 0, ErrInvalidMessageLength } - msgType := MsgType(msg[0]) + msgType := MsgType(msg[1]) switch msgType { case MsgTypeHello, @@ -105,11 +111,11 @@ func DetermineClientMessageType(msg []byte) (MsgType, error) { // DetermineServerMessageType determines the message type from the first the message func DetermineServerMessageType(msg []byte) (MsgType, error) { - if len(msg) < SizeOfMsgType { + if len(msg) < sizeOfProtoHeader { return 0, ErrInvalidMessageLength } - msgType := MsgType(msg[0]) + msgType := MsgType(msg[1]) switch msgType { case MsgTypeHelloResponse, @@ -134,12 +140,12 @@ func MarshalHelloMsg(peerID []byte, additions []byte) ([]byte, error) { return nil, fmt.Errorf("invalid peerID length: %d", len(peerID)) } - msg := make([]byte, SizeOfProtoHeader+sizeOfMagicByte, SizeOfProtoHeader+headerSizeHello+len(additions)) + msg := make([]byte, sizeOfProtoHeader+sizeOfMagicByte, sizeOfProtoHeader+headerSizeHello+len(additions)) msg[0] = byte(CurrentProtocolVersion) msg[1] = byte(MsgTypeHello) - copy(msg[SizeOfProtoHeader:SizeOfProtoHeader+sizeOfMagicByte], magicHeader) + copy(msg[sizeOfProtoHeader:sizeOfProtoHeader+sizeOfMagicByte], magicHeader) msg = append(msg, peerID...) msg = append(msg, additions...) @@ -151,14 +157,14 @@ func MarshalHelloMsg(peerID []byte, additions []byte) ([]byte, error) { // UnmarshalHelloMsg extracts peerID and the additional data from the hello message. The Additional data is used to // authenticate the client with the server. func UnmarshalHelloMsg(msg []byte) ([]byte, []byte, error) { - if len(msg) < headerSizeHello { + if len(msg) < sizeOfProtoHeader+headerSizeHello { return nil, nil, ErrInvalidMessageLength } - if !bytes.Equal(msg[:sizeOfMagicByte], magicHeader) { + if !bytes.Equal(msg[sizeOfProtoHeader:sizeOfProtoHeader+sizeOfMagicByte], magicHeader) { return nil, nil, errors.New("invalid magic header") } - return msg[sizeOfMagicByte:headerSizeHello], msg[headerSizeHello:], nil + return msg[sizeOfProtoHeader+sizeOfMagicByte : sizeOfProtoHeader+headerSizeHello], msg[headerSizeHello:], nil } // Deprecated: Use MarshalAuthResponse instead. @@ -167,7 +173,7 @@ func UnmarshalHelloMsg(msg []byte) ([]byte, []byte, error) { // instance URL. This URL will be used by choose the common Relay server in case if the peers are in different Relay // servers. func MarshalHelloResponse(additionalData []byte) ([]byte, error) { - msg := make([]byte, SizeOfProtoHeader, SizeOfProtoHeader+headerSizeHelloResp+len(additionalData)) + msg := make([]byte, sizeOfProtoHeader, sizeOfProtoHeader+headerSizeHelloResp+len(additionalData)) msg[0] = byte(CurrentProtocolVersion) msg[1] = byte(MsgTypeHelloResponse) @@ -180,7 +186,7 @@ func MarshalHelloResponse(additionalData []byte) ([]byte, error) { // Deprecated: Use UnmarshalAuthResponse instead. // UnmarshalHelloResponse extracts the additional data from the hello response message. func UnmarshalHelloResponse(msg []byte) ([]byte, error) { - if len(msg) < headerSizeHelloResp { + if len(msg) < sizeOfProtoHeader+headerSizeHelloResp { return nil, ErrInvalidMessageLength } return msg, nil @@ -196,12 +202,12 @@ func MarshalAuthMsg(peerID []byte, authPayload []byte) ([]byte, error) { return nil, fmt.Errorf("invalid peerID length: %d", len(peerID)) } - msg := make([]byte, SizeOfProtoHeader+sizeOfMagicByte, SizeOfProtoHeader+headerSizeAuth+len(authPayload)) + msg := make([]byte, sizeOfProtoHeader+sizeOfMagicByte, headerTotalSizeAuth+len(authPayload)) msg[0] = byte(CurrentProtocolVersion) msg[1] = byte(MsgTypeAuth) - copy(msg[SizeOfProtoHeader:SizeOfProtoHeader+sizeOfMagicByte], magicHeader) + copy(msg[sizeOfProtoHeader:], magicHeader) msg = append(msg, peerID...) msg = append(msg, authPayload...) @@ -211,14 +217,14 @@ func MarshalAuthMsg(peerID []byte, authPayload []byte) ([]byte, error) { // UnmarshalAuthMsg extracts peerID and the auth payload from the message func UnmarshalAuthMsg(msg []byte) ([]byte, []byte, error) { - if len(msg) < headerSizeAuth { + if len(msg) < headerTotalSizeAuth { return nil, nil, ErrInvalidMessageLength } - if !bytes.Equal(msg[:sizeOfMagicByte], magicHeader) { + if !bytes.Equal(msg[offsetMagicByte:offsetMagicByte+sizeOfMagicByte], magicHeader) { return nil, nil, errors.New("invalid magic header") } - return msg[sizeOfMagicByte:headerSizeAuth], msg[headerSizeAuth:], nil + return msg[offsetAuthPeerID:headerTotalSizeAuth], msg[headerTotalSizeAuth:], nil } // MarshalAuthResponse creates a response message to the auth. @@ -227,7 +233,7 @@ func UnmarshalAuthMsg(msg []byte) ([]byte, []byte, error) { // servers. func MarshalAuthResponse(address string) ([]byte, error) { ab := []byte(address) - msg := make([]byte, SizeOfProtoHeader, SizeOfProtoHeader+headerSizeAuthResp+len(ab)) + msg := make([]byte, sizeOfProtoHeader, sizeOfProtoHeader+len(ab)) msg[0] = byte(CurrentProtocolVersion) msg[1] = byte(MsgTypeAuthResponse) @@ -243,39 +249,34 @@ func MarshalAuthResponse(address string) ([]byte, error) { // UnmarshalAuthResponse it is a confirmation message to auth success func UnmarshalAuthResponse(msg []byte) (string, error) { - if len(msg) < headerSizeAuthResp+1 { + if len(msg) < sizeOfProtoHeader+1 { return "", ErrInvalidMessageLength } - return string(msg), nil + return string(msg[sizeOfProtoHeader:]), nil } // MarshalCloseMsg creates a close message. // The close message is used to close the connection gracefully between the client and the server. The server and the // client can send this message. After receiving this message, the server or client will close the connection. func MarshalCloseMsg() []byte { - msg := make([]byte, SizeOfProtoHeader) - - msg[0] = byte(CurrentProtocolVersion) - msg[1] = byte(MsgTypeClose) - - return msg + return []byte{ + byte(CurrentProtocolVersion), + byte(MsgTypeClose), + } } // MarshalTransportMsg creates a transport message. // The transport message is used to exchange data between peers. The message contains the data to be exchanged and the // destination peer hashed ID. -func MarshalTransportMsg(peerID []byte, payload []byte) ([]byte, error) { +func MarshalTransportMsg(peerID, payload []byte) ([]byte, error) { if len(peerID) != IDSize { return nil, fmt.Errorf("invalid peerID length: %d", len(peerID)) } - msg := make([]byte, SizeOfProtoHeader+headerSizeTransport, SizeOfProtoHeader+headerSizeTransport+len(payload)) - + msg := make([]byte, headerTotalSizeTransport, headerTotalSizeTransport+len(payload)) msg[0] = byte(CurrentProtocolVersion) msg[1] = byte(MsgTypeTransport) - - copy(msg[SizeOfProtoHeader:], peerID) - + copy(msg[sizeOfProtoHeader:], peerID) msg = append(msg, payload...) return msg, nil @@ -283,29 +284,29 @@ func MarshalTransportMsg(peerID []byte, payload []byte) ([]byte, error) { // UnmarshalTransportMsg extracts the peerID and the payload from the transport message. func UnmarshalTransportMsg(buf []byte) ([]byte, []byte, error) { - if len(buf) < headerSizeTransport { + if len(buf) < headerTotalSizeTransport { return nil, nil, ErrInvalidMessageLength } - return buf[:headerSizeTransport], buf[headerSizeTransport:], nil + return buf[offsetTransportID:headerTotalSizeTransport], buf[headerTotalSizeTransport:], nil } // UnmarshalTransportID extracts the peerID from the transport message. func UnmarshalTransportID(buf []byte) ([]byte, error) { - if len(buf) < headerSizeTransport { + if len(buf) < headerTotalSizeTransport { return nil, ErrInvalidMessageLength } - return buf[:headerSizeTransport], nil + return buf[offsetTransportID:headerTotalSizeTransport], nil } // UpdateTransportMsg updates the peerID in the transport message. // With this function the server can reuse the given byte slice to update the peerID in the transport message. So do // need to allocate a new byte slice. func UpdateTransportMsg(msg []byte, peerID []byte) error { - if len(msg) < len(peerID) { + if len(msg) < offsetTransportID+len(peerID) { return ErrInvalidMessageLength } - copy(msg, peerID) + copy(msg[offsetTransportID:], peerID) return nil } diff --git a/relay/messages/message_test.go b/relay/messages/message_test.go index 6e917da71..19bede07b 100644 --- a/relay/messages/message_test.go +++ b/relay/messages/message_test.go @@ -6,12 +6,21 @@ import ( func TestMarshalHelloMsg(t *testing.T) { peerID := []byte("abdFAaBcawquEiCMzAabYosuUaGLtSNhKxz+") - bHello, err := MarshalHelloMsg(peerID, nil) + msg, err := MarshalHelloMsg(peerID, nil) if err != nil { t.Fatalf("error: %v", err) } - receivedPeerID, _, err := UnmarshalHelloMsg(bHello[SizeOfProtoHeader:]) + msgType, err := DetermineClientMessageType(msg) + if err != nil { + t.Fatalf("error: %v", err) + } + + if msgType != MsgTypeHello { + t.Errorf("expected %d, got %d", MsgTypeHello, msgType) + } + + receivedPeerID, _, err := UnmarshalHelloMsg(msg) if err != nil { t.Fatalf("error: %v", err) } @@ -22,12 +31,21 @@ func TestMarshalHelloMsg(t *testing.T) { func TestMarshalAuthMsg(t *testing.T) { peerID := []byte("abdFAaBcawquEiCMzAabYosuUaGLtSNhKxz+") - bHello, err := MarshalAuthMsg(peerID, []byte{}) + msg, err := MarshalAuthMsg(peerID, []byte{}) if err != nil { t.Fatalf("error: %v", err) } - receivedPeerID, _, err := UnmarshalAuthMsg(bHello[SizeOfProtoHeader:]) + msgType, err := DetermineClientMessageType(msg) + if err != nil { + t.Fatalf("error: %v", err) + } + + if msgType != MsgTypeAuth { + t.Errorf("expected %d, got %d", MsgTypeAuth, msgType) + } + + receivedPeerID, _, err := UnmarshalAuthMsg(msg) if err != nil { t.Fatalf("error: %v", err) } @@ -36,6 +54,31 @@ func TestMarshalAuthMsg(t *testing.T) { } } +func TestMarshalAuthResponse(t *testing.T) { + address := "myaddress" + msg, err := MarshalAuthResponse(address) + if err != nil { + t.Fatalf("error: %v", err) + } + + msgType, err := DetermineServerMessageType(msg) + if err != nil { + t.Fatalf("error: %v", err) + } + + if msgType != MsgTypeAuthResponse { + t.Errorf("expected %d, got %d", MsgTypeAuthResponse, msgType) + } + + respAddr, err := UnmarshalAuthResponse(msg) + if err != nil { + t.Fatalf("error: %v", err) + } + if respAddr != address { + t.Errorf("expected %s, got %s", address, respAddr) + } +} + func TestMarshalTransportMsg(t *testing.T) { peerID := []byte("abdFAaBcawquEiCMzAabYosuUaGLtSNhKxz+") payload := []byte("payload") @@ -44,7 +87,25 @@ func TestMarshalTransportMsg(t *testing.T) { t.Fatalf("error: %v", err) } - id, respPayload, err := UnmarshalTransportMsg(msg[SizeOfProtoHeader:]) + msgType, err := DetermineClientMessageType(msg) + if err != nil { + t.Fatalf("error: %v", err) + } + + if msgType != MsgTypeTransport { + t.Errorf("expected %d, got %d", MsgTypeTransport, msgType) + } + + uPeerID, err := UnmarshalTransportID(msg) + if err != nil { + t.Fatalf("failed to unmarshal transport id: %v", err) + } + + if string(uPeerID) != string(peerID) { + t.Errorf("expected %s, got %s", peerID, uPeerID) + } + + id, respPayload, err := UnmarshalTransportMsg(msg) if err != nil { t.Fatalf("error: %v", err) } @@ -57,3 +118,21 @@ func TestMarshalTransportMsg(t *testing.T) { t.Errorf("expected %s, got %s", payload, respPayload) } } + +func TestMarshalHealthcheck(t *testing.T) { + msg := MarshalHealthcheck() + + _, err := ValidateVersion(msg) + if err != nil { + t.Fatalf("error: %v", err) + } + + msgType, err := DetermineServerMessageType(msg) + if err != nil { + t.Fatalf("error: %v", err) + } + + if msgType != MsgTypeHealthCheck { + t.Errorf("expected %d, got %d", MsgTypeHealthCheck, msgType) + } +} diff --git a/relay/server/handshake.go b/relay/server/handshake.go index 0257300f8..babd6f955 100644 --- a/relay/server/handshake.go +++ b/relay/server/handshake.go @@ -68,12 +68,14 @@ func (h *handshake) handshakeReceive() ([]byte, error) { return nil, fmt.Errorf("read from %s: %w", h.conn.RemoteAddr(), err) } - _, err = messages.ValidateVersion(buf[:n]) + buf = buf[:n] + + _, err = messages.ValidateVersion(buf) if err != nil { return nil, fmt.Errorf("validate version from %s: %w", h.conn.RemoteAddr(), err) } - msgType, err := messages.DetermineClientMessageType(buf[messages.SizeOfVersionByte:n]) + msgType, err := messages.DetermineClientMessageType(buf) if err != nil { return nil, fmt.Errorf("determine message type from %s: %w", h.conn.RemoteAddr(), err) } @@ -85,10 +87,10 @@ func (h *handshake) handshakeReceive() ([]byte, error) { switch msgType { //nolint:staticcheck case messages.MsgTypeHello: - bytePeerID, peerID, err = h.handleHelloMsg(buf[messages.SizeOfProtoHeader:n]) + bytePeerID, peerID, err = h.handleHelloMsg(buf) case messages.MsgTypeAuth: h.handshakeMethodAuth = true - bytePeerID, peerID, err = h.handleAuthMsg(buf[messages.SizeOfProtoHeader:n]) + bytePeerID, peerID, err = h.handleAuthMsg(buf) default: return nil, fmt.Errorf("invalid message type %d from %s", msgType, h.conn.RemoteAddr()) } diff --git a/relay/server/peer.go b/relay/server/peer.go index f65fb786a..aa9790f63 100644 --- a/relay/server/peer.go +++ b/relay/server/peer.go @@ -84,7 +84,7 @@ func (p *Peer) Work() { return } - msgType, err := messages.DetermineClientMessageType(msg[messages.SizeOfVersionByte:]) + msgType, err := messages.DetermineClientMessageType(msg) if err != nil { p.log.Errorf("failed to determine message type: %s", err) return @@ -191,7 +191,7 @@ func (p *Peer) handleHealthcheckEvents(ctx context.Context, hc *healthcheck.Send } func (p *Peer) handleTransportMsg(msg []byte) { - peerID, err := messages.UnmarshalTransportID(msg[messages.SizeOfProtoHeader:]) + peerID, err := messages.UnmarshalTransportID(msg) if err != nil { p.log.Errorf("failed to unmarshal transport message: %s", err) return @@ -204,7 +204,7 @@ func (p *Peer) handleTransportMsg(msg []byte) { return } - err = messages.UpdateTransportMsg(msg[messages.SizeOfProtoHeader:], p.idB) + err = messages.UpdateTransportMsg(msg, p.idB) if err != nil { p.log.Errorf("failed to update transport message: %s", err) return From e4a25b6a60e4359bd5e0a66bc70f133de55aedc6 Mon Sep 17 00:00:00 2001 From: Edouard Vanbelle <15628033+EdouardVanbelle@users.noreply.github.com> Date: Wed, 15 Jan 2025 16:02:16 +0100 Subject: [PATCH 08/92] [client-android] add serial, product model, product manufacturer (#2958) Signed-off-by: Edouard Vanbelle --- client/system/info_android.go | 38 ++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/client/system/info_android.go b/client/system/info_android.go index 7718da913..2d44a6f52 100644 --- a/client/system/info_android.go +++ b/client/system/info_android.go @@ -39,6 +39,9 @@ func GetInfo(ctx context.Context) *Info { WiretrusteeVersion: version.NetbirdVersion(), UIVersion: extractUIVersion(ctx), KernelVersion: kernelVersion, + SystemSerialNumber: serial(), + SystemProductName: productModel(), + SystemManufacturer: productManufacturer(), } return gio @@ -49,13 +52,42 @@ func checkFileAndProcess(paths []string) ([]File, error) { return []File{}, nil } +func serial() string { + // try to fetch serial ID using different properties + properties := []string{"ril.serialnumber", "ro.serialno", "ro.boot.serialno", "sys.serialnumber"} + var value string + + for _, property := range properties { + value = getprop(property) + if len(value) > 0 { + return value + } + } + + // unable to get serial ID, fallback to ANDROID_ID + return androidId() +} + +func androidId() string { + // this is a uniq id defined on first initialization, id will be a new one if user wipes his device + return run("/system/bin/settings", "get", "secure", "android_id") +} + +func productModel() string { + return getprop("ro.product.model") +} + +func productManufacturer() string { + return getprop("ro.product.manufacturer") +} + func uname() []string { res := run("/system/bin/uname", "-a") return strings.Split(res, " ") } func osVersion() string { - return run("/system/bin/getprop", "ro.build.version.release") + return getprop("ro.build.version.release") } func extractUIVersion(ctx context.Context) string { @@ -66,6 +98,10 @@ func extractUIVersion(ctx context.Context) string { return v } +func getprop(arg ...string) string { + return run("/system/bin/getprop", arg...) +} + func run(name string, arg ...string) string { cmd := exec.Command(name, arg...) cmd.Stdin = strings.NewReader("some") From 1ffa5193871f9e4c7b14ff0b553d8432e7e280b5 Mon Sep 17 00:00:00 2001 From: Zoltan Papp Date: Wed, 15 Jan 2025 16:28:19 +0100 Subject: [PATCH 09/92] [client,relay] Add QUIC support (#2962) --- .github/workflows/golang-test-darwin.yml | 3 +- .github/workflows/golang-test-linux.yml | 6 +- .github/workflows/golang-test-windows.yml | 2 +- go.mod | 4 + go.sum | 6 + relay/client/client.go | 15 +- relay/client/dialer/net/err.go | 7 + relay/client/dialer/quic/conn.go | 97 +++++++++ relay/client/dialer/quic/quic.go | 71 ++++++ relay/client/dialer/race_dialer.go | 96 +++++++++ relay/client/dialer/race_dialer_test.go | 252 ++++++++++++++++++++++ relay/client/dialer/ws/addr.go | 6 +- relay/client/dialer/ws/conn.go | 1 + relay/client/dialer/ws/ws.go | 15 +- relay/server/listener/quic/conn.go | 101 +++++++++ relay/server/listener/quic/listener.go | 66 ++++++ relay/server/listener/ws/listener.go | 2 + relay/server/relay.go | 4 +- relay/server/server.go | 64 ++++-- relay/tls/alpn.go | 3 + relay/tls/client_dev.go | 12 ++ relay/tls/client_prod.go | 11 + relay/tls/doc.go | 36 ++++ relay/tls/server_dev.go | 79 +++++++ relay/tls/server_prod.go | 17 ++ 25 files changed, 943 insertions(+), 33 deletions(-) create mode 100644 relay/client/dialer/net/err.go create mode 100644 relay/client/dialer/quic/conn.go create mode 100644 relay/client/dialer/quic/quic.go create mode 100644 relay/client/dialer/race_dialer.go create mode 100644 relay/client/dialer/race_dialer_test.go create mode 100644 relay/server/listener/quic/conn.go create mode 100644 relay/server/listener/quic/listener.go create mode 100644 relay/tls/alpn.go create mode 100644 relay/tls/client_dev.go create mode 100644 relay/tls/client_prod.go create mode 100644 relay/tls/doc.go create mode 100644 relay/tls/server_dev.go create mode 100644 relay/tls/server_prod.go diff --git a/.github/workflows/golang-test-darwin.yml b/.github/workflows/golang-test-darwin.yml index 2dbeb106a..664e8be18 100644 --- a/.github/workflows/golang-test-darwin.yml +++ b/.github/workflows/golang-test-darwin.yml @@ -44,4 +44,5 @@ jobs: run: git --no-pager diff --exit-code - name: Test - run: NETBIRD_STORE_ENGINE=${{ matrix.store }} CI=true go test -exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE' -timeout 5m -p 1 $(go list ./... | grep -v /management) + run: NETBIRD_STORE_ENGINE=${{ matrix.store }} CI=true go test -tags=devcert -exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE' -timeout 5m -p 1 $(go list ./... | grep -v /management) + diff --git a/.github/workflows/golang-test-linux.yml b/.github/workflows/golang-test-linux.yml index 5f7d7b4a3..ba5f66746 100644 --- a/.github/workflows/golang-test-linux.yml +++ b/.github/workflows/golang-test-linux.yml @@ -134,7 +134,7 @@ jobs: run: git --no-pager diff --exit-code - name: Test - run: CGO_ENABLED=1 GOARCH=${{ matrix.arch }} CI=true go test -exec 'sudo' -timeout 10m -p 1 $(go list ./... | grep -v /management) + run: CGO_ENABLED=1 GOARCH=${{ matrix.arch }} CI=true go test -tags devcert -exec 'sudo' -timeout 10m -p 1 $(go list ./... | grep -v /management) test_management: needs: [ build-cache ] @@ -194,7 +194,7 @@ jobs: run: docker pull mlsmaycon/warmed-mysql:8 - name: Test - run: CGO_ENABLED=1 GOARCH=${{ matrix.arch }} NETBIRD_STORE_ENGINE=${{ matrix.store }} CI=true go test -p 1 -exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE' -timeout 10m $(go list ./... | grep /management) + run: CGO_ENABLED=1 GOARCH=${{ matrix.arch }} NETBIRD_STORE_ENGINE=${{ matrix.store }} CI=true go test -tags=devcert -p 1 -exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE' -timeout 10m $(go list ./... | grep /management) benchmark: needs: [ build-cache ] @@ -254,7 +254,7 @@ jobs: run: docker pull mlsmaycon/warmed-mysql:8 - name: Test - run: CGO_ENABLED=1 GOARCH=${{ matrix.arch }} NETBIRD_STORE_ENGINE=${{ matrix.store }} CI=true go test -run=^$ -bench=. -exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE' -timeout 20m ./... + run: CGO_ENABLED=1 GOARCH=${{ matrix.arch }} NETBIRD_STORE_ENGINE=${{ matrix.store }} CI=true go test -tags devcert -run=^$ -bench=. -exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE' -timeout 20m ./... api_benchmark: needs: [ build-cache ] diff --git a/.github/workflows/golang-test-windows.yml b/.github/workflows/golang-test-windows.yml index 3a3c47052..782e4c30a 100644 --- a/.github/workflows/golang-test-windows.yml +++ b/.github/workflows/golang-test-windows.yml @@ -65,7 +65,7 @@ jobs: - run: echo "files=$(go list ./... | ForEach-Object { $_ } | Where-Object { $_ -notmatch '/management' })" >> $env:GITHUB_ENV - name: test - run: PsExec64 -s -w ${{ github.workspace }} cmd.exe /c "C:\hostedtoolcache\windows\go\${{ steps.go.outputs.go-version }}\x64\bin\go.exe test -timeout 10m -p 1 ${{ env.files }} > test-out.txt 2>&1" + run: PsExec64 -s -w ${{ github.workspace }} cmd.exe /c "C:\hostedtoolcache\windows\go\${{ steps.go.outputs.go-version }}\x64\bin\go.exe test -tags=devcert -timeout 10m -p 1 ${{ env.files }} > test-out.txt 2>&1" - name: test output if: ${{ always() }} run: Get-Content test-out.txt diff --git a/go.mod b/go.mod index 147577cc3..88bcada07 100644 --- a/go.mod +++ b/go.mod @@ -71,6 +71,7 @@ require ( github.com/pion/transport/v3 v3.0.1 github.com/pion/turn/v3 v3.0.1 github.com/prometheus/client_golang v1.19.1 + github.com/quic-go/quic-go v0.48.2 github.com/rs/xid v1.3.0 github.com/shirou/gopsutil/v3 v3.24.4 github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 @@ -155,11 +156,13 @@ require ( github.com/go-ole/go-ole v1.3.0 // indirect github.com/go-redis/redis/v8 v8.11.5 // indirect github.com/go-sql-driver/mysql v1.8.1 // indirect + github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect github.com/go-text/render v0.2.0 // indirect github.com/go-text/typesetting v0.2.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/google/btree v1.1.2 // indirect + github.com/google/pprof v0.0.0-20211214055906-6f57359322fd // indirect github.com/google/s2a-go v0.1.7 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect github.com/googleapis/gax-go/v2 v2.12.3 // indirect @@ -221,6 +224,7 @@ require ( go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0 // indirect go.opentelemetry.io/otel/sdk v1.26.0 // indirect go.opentelemetry.io/otel/trace v1.26.0 // indirect + go.uber.org/mock v0.4.0 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/image v0.18.0 // indirect golang.org/x/mod v0.17.0 // indirect diff --git a/go.sum b/go.sum index 253429798..8ba94dd6a 100644 --- a/go.sum +++ b/go.sum @@ -405,6 +405,7 @@ github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/J github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= @@ -610,6 +611,8 @@ github.com/prometheus/common v0.53.0 h1:U2pL9w9nmJwJDa4qqLQ3ZaePJ6ZTwt7cMD3AG3+a github.com/prometheus/common v0.53.0/go.mod h1:BrxBKv3FWBIGXw89Mg1AeBq7FSyRzXWI3l3e7W3RN5U= github.com/prometheus/procfs v0.15.0 h1:A82kmvXJq2jTu5YUhSGNlYoxh85zLnKgPz4bMZgI5Ek= github.com/prometheus/procfs v0.15.0/go.mod h1:Y0RJ/Y5g5wJpkTisOtqwDSo4HwhGmLB4VQSw2sQJLHk= +github.com/quic-go/quic-go v0.48.2 h1:wsKXZPeGWpMpCGSWqOcqpW2wZYic/8T3aqiOID0/KWE= +github.com/quic-go/quic-go v0.48.2/go.mod h1:yBgs3rWBOADpga7F+jJsb6Ybg1LSYiQvwWlLX+/6HMs= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= @@ -761,6 +764,8 @@ go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v8 go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= +go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= @@ -970,6 +975,7 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/relay/client/client.go b/relay/client/client.go index bccd85c93..3c23b70d2 100644 --- a/relay/client/client.go +++ b/relay/client/client.go @@ -10,6 +10,8 @@ import ( log "github.com/sirupsen/logrus" auth "github.com/netbirdio/netbird/relay/auth/hmac" + "github.com/netbirdio/netbird/relay/client/dialer" + "github.com/netbirdio/netbird/relay/client/dialer/quic" "github.com/netbirdio/netbird/relay/client/dialer/ws" "github.com/netbirdio/netbird/relay/healthcheck" "github.com/netbirdio/netbird/relay/messages" @@ -95,8 +97,6 @@ func (cc *connContainer) writeMsg(msg Msg) { msg.Free() default: msg.Free() - cc.log.Infof("message queue is full") - // todo consider to close the connection } } @@ -179,8 +179,7 @@ func (c *Client) Connect() error { return nil } - err := c.connect() - if err != nil { + if err := c.connect(); err != nil { return err } @@ -264,14 +263,14 @@ func (c *Client) Close() error { } func (c *Client) connect() error { - conn, err := ws.Dial(c.connectionURL) + rd := dialer.NewRaceDial(c.log, c.connectionURL, quic.Dialer{}, ws.Dialer{}) + conn, err := rd.Dial() if err != nil { return err } c.relayConn = conn - err = c.handShake() - if err != nil { + if err = c.handShake(); err != nil { cErr := conn.Close() if cErr != nil { c.log.Errorf("failed to close connection: %s", cErr) @@ -345,7 +344,7 @@ func (c *Client) readLoop(relayConn net.Conn) { c.log.Infof("start to Relay read loop exit") c.mu.Lock() if c.serviceIsRunning && !internallyStoppedFlag.isSet() { - c.log.Debugf("failed to read message from relay server: %s", errExit) + c.log.Errorf("failed to read message from relay server: %s", errExit) } c.mu.Unlock() c.bufPool.Put(bufPtr) diff --git a/relay/client/dialer/net/err.go b/relay/client/dialer/net/err.go new file mode 100644 index 000000000..fee844963 --- /dev/null +++ b/relay/client/dialer/net/err.go @@ -0,0 +1,7 @@ +package net + +import "errors" + +var ( + ErrClosedByServer = errors.New("closed by server") +) diff --git a/relay/client/dialer/quic/conn.go b/relay/client/dialer/quic/conn.go new file mode 100644 index 000000000..d64633c8c --- /dev/null +++ b/relay/client/dialer/quic/conn.go @@ -0,0 +1,97 @@ +package quic + +import ( + "context" + "errors" + "fmt" + "net" + "time" + + "github.com/quic-go/quic-go" + log "github.com/sirupsen/logrus" + + netErr "github.com/netbirdio/netbird/relay/client/dialer/net" +) + +const ( + Network = "quic" +) + +type Addr struct { + addr string +} + +func (a Addr) Network() string { + return Network +} + +func (a Addr) String() string { + return a.addr +} + +type Conn struct { + session quic.Connection + ctx context.Context +} + +func NewConn(session quic.Connection) net.Conn { + return &Conn{ + session: session, + ctx: context.Background(), + } +} + +func (c *Conn) Read(b []byte) (n int, err error) { + dgram, err := c.session.ReceiveDatagram(c.ctx) + if err != nil { + return 0, c.remoteCloseErrHandling(err) + } + + n = copy(b, dgram) + return n, nil +} + +func (c *Conn) Write(b []byte) (int, error) { + err := c.session.SendDatagram(b) + if err != nil { + err = c.remoteCloseErrHandling(err) + log.Errorf("failed to write to QUIC stream: %v", err) + return 0, err + } + return len(b), nil +} + +func (c *Conn) RemoteAddr() net.Addr { + return c.session.RemoteAddr() +} + +func (c *Conn) LocalAddr() net.Addr { + if c.session != nil { + return c.session.LocalAddr() + } + return Addr{addr: "unknown"} +} + +func (c *Conn) SetReadDeadline(t time.Time) error { + return fmt.Errorf("SetReadDeadline is not implemented") +} + +func (c *Conn) SetWriteDeadline(t time.Time) error { + return fmt.Errorf("SetWriteDeadline is not implemented") +} + +func (c *Conn) SetDeadline(t time.Time) error { + return nil +} + +func (c *Conn) Close() error { + return c.session.CloseWithError(0, "normal closure") +} + +func (c *Conn) remoteCloseErrHandling(err error) error { + var appErr *quic.ApplicationError + if errors.As(err, &appErr) && appErr.ErrorCode == 0x0 { + return netErr.ErrClosedByServer + } + return err +} diff --git a/relay/client/dialer/quic/quic.go b/relay/client/dialer/quic/quic.go new file mode 100644 index 000000000..593d1334b --- /dev/null +++ b/relay/client/dialer/quic/quic.go @@ -0,0 +1,71 @@ +package quic + +import ( + "context" + "errors" + "fmt" + "net" + "strings" + "time" + + "github.com/quic-go/quic-go" + log "github.com/sirupsen/logrus" + + quictls "github.com/netbirdio/netbird/relay/tls" + nbnet "github.com/netbirdio/netbird/util/net" +) + +type Dialer struct { +} + +func (d Dialer) Protocol() string { + return Network +} + +func (d Dialer) Dial(ctx context.Context, address string) (net.Conn, error) { + quicURL, err := prepareURL(address) + if err != nil { + return nil, err + } + + quicConfig := &quic.Config{ + KeepAlivePeriod: 30 * time.Second, + MaxIdleTimeout: 4 * time.Minute, + EnableDatagrams: true, + } + + udpConn, err := nbnet.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4zero, Port: 0}) + if err != nil { + log.Errorf("failed to listen on UDP: %s", err) + return nil, err + } + + udpAddr, err := net.ResolveUDPAddr("udp", quicURL) + if err != nil { + log.Errorf("failed to resolve UDP address: %s", err) + return nil, err + } + + session, err := quic.Dial(ctx, udpConn, udpAddr, quictls.ClientQUICTLSConfig(), quicConfig) + if err != nil { + if errors.Is(err, context.Canceled) { + return nil, err + } + log.Errorf("failed to dial to Relay server via QUIC '%s': %s", quicURL, err) + return nil, err + } + + conn := NewConn(session) + return conn, nil +} + +func prepareURL(address string) (string, error) { + if !strings.HasPrefix(address, "rel://") && !strings.HasPrefix(address, "rels://") { + return "", fmt.Errorf("unsupported scheme: %s", address) + } + + if strings.HasPrefix(address, "rels://") { + return address[7:], nil + } + return address[6:], nil +} diff --git a/relay/client/dialer/race_dialer.go b/relay/client/dialer/race_dialer.go new file mode 100644 index 000000000..11dba5799 --- /dev/null +++ b/relay/client/dialer/race_dialer.go @@ -0,0 +1,96 @@ +package dialer + +import ( + "context" + "errors" + "net" + "time" + + log "github.com/sirupsen/logrus" +) + +var ( + connectionTimeout = 30 * time.Second +) + +type DialeFn interface { + Dial(ctx context.Context, address string) (net.Conn, error) + Protocol() string +} + +type dialResult struct { + Conn net.Conn + Protocol string + Err error +} + +type RaceDial struct { + log *log.Entry + serverURL string + dialerFns []DialeFn +} + +func NewRaceDial(log *log.Entry, serverURL string, dialerFns ...DialeFn) *RaceDial { + return &RaceDial{ + log: log, + serverURL: serverURL, + dialerFns: dialerFns, + } +} + +func (r *RaceDial) Dial() (net.Conn, error) { + connChan := make(chan dialResult, len(r.dialerFns)) + winnerConn := make(chan net.Conn, 1) + abortCtx, abort := context.WithCancel(context.Background()) + defer abort() + + for _, dfn := range r.dialerFns { + go r.dial(dfn, abortCtx, connChan) + } + + go r.processResults(connChan, winnerConn, abort) + + conn, ok := <-winnerConn + if !ok { + return nil, errors.New("failed to dial to Relay server on any protocol") + } + return conn, nil +} + +func (r *RaceDial) dial(dfn DialeFn, abortCtx context.Context, connChan chan dialResult) { + ctx, cancel := context.WithTimeout(abortCtx, connectionTimeout) + defer cancel() + + r.log.Infof("dialing Relay server via %s", dfn.Protocol()) + conn, err := dfn.Dial(ctx, r.serverURL) + connChan <- dialResult{Conn: conn, Protocol: dfn.Protocol(), Err: err} +} + +func (r *RaceDial) processResults(connChan chan dialResult, winnerConn chan net.Conn, abort context.CancelFunc) { + var hasWinner bool + for i := 0; i < len(r.dialerFns); i++ { + dr := <-connChan + if dr.Err != nil { + if errors.Is(dr.Err, context.Canceled) { + r.log.Infof("connection attempt aborted via: %s", dr.Protocol) + } else { + r.log.Errorf("failed to dial via %s: %s", dr.Protocol, dr.Err) + } + continue + } + + if hasWinner { + if cerr := dr.Conn.Close(); cerr != nil { + r.log.Warnf("failed to close connection via %s: %s", dr.Protocol, cerr) + } + continue + } + + r.log.Infof("successfully dialed via: %s", dr.Protocol) + + abort() + hasWinner = true + winnerConn <- dr.Conn + } + close(winnerConn) +} diff --git a/relay/client/dialer/race_dialer_test.go b/relay/client/dialer/race_dialer_test.go new file mode 100644 index 000000000..989abb0a6 --- /dev/null +++ b/relay/client/dialer/race_dialer_test.go @@ -0,0 +1,252 @@ +package dialer + +import ( + "context" + "errors" + "net" + "testing" + "time" + + "github.com/sirupsen/logrus" +) + +type MockAddr struct { + network string +} + +func (m *MockAddr) Network() string { + return m.network +} + +func (m *MockAddr) String() string { + return "1.2.3.4" +} + +// MockDialer is a mock implementation of DialeFn +type MockDialer struct { + dialFunc func(ctx context.Context, address string) (net.Conn, error) + protocolStr string +} + +func (m *MockDialer) Dial(ctx context.Context, address string) (net.Conn, error) { + return m.dialFunc(ctx, address) +} + +func (m *MockDialer) Protocol() string { + return m.protocolStr +} + +// MockConn implements net.Conn for testing +type MockConn struct { + remoteAddr net.Addr +} + +func (m *MockConn) Read(b []byte) (n int, err error) { + return 0, nil +} + +func (m *MockConn) Write(b []byte) (n int, err error) { + return 0, nil +} + +func (m *MockConn) Close() error { + return nil +} + +func (m *MockConn) LocalAddr() net.Addr { + return nil +} + +func (m *MockConn) RemoteAddr() net.Addr { + return m.remoteAddr +} + +func (m *MockConn) SetDeadline(t time.Time) error { + return nil +} + +func (m *MockConn) SetReadDeadline(t time.Time) error { + return nil +} + +func (m *MockConn) SetWriteDeadline(t time.Time) error { + return nil +} + +func TestRaceDialEmptyDialers(t *testing.T) { + logger := logrus.NewEntry(logrus.New()) + serverURL := "test.server.com" + + rd := NewRaceDial(logger, serverURL) + conn, err := rd.Dial() + if err == nil { + t.Errorf("Expected an error with empty dialers, got nil") + } + if conn != nil { + t.Errorf("Expected nil connection with empty dialers, got %v", conn) + } +} + +func TestRaceDialSingleSuccessfulDialer(t *testing.T) { + logger := logrus.NewEntry(logrus.New()) + serverURL := "test.server.com" + proto := "test-protocol" + + mockConn := &MockConn{ + remoteAddr: &MockAddr{network: proto}, + } + + mockDialer := &MockDialer{ + dialFunc: func(ctx context.Context, address string) (net.Conn, error) { + return mockConn, nil + }, + protocolStr: proto, + } + + rd := NewRaceDial(logger, serverURL, mockDialer) + conn, err := rd.Dial() + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + if conn == nil { + t.Errorf("Expected non-nil connection") + } +} + +func TestRaceDialMultipleDialersWithOneSuccess(t *testing.T) { + logger := logrus.NewEntry(logrus.New()) + serverURL := "test.server.com" + proto2 := "protocol2" + + mockConn2 := &MockConn{ + remoteAddr: &MockAddr{network: proto2}, + } + + mockDialer1 := &MockDialer{ + dialFunc: func(ctx context.Context, address string) (net.Conn, error) { + return nil, errors.New("first dialer failed") + }, + protocolStr: "proto1", + } + + mockDialer2 := &MockDialer{ + dialFunc: func(ctx context.Context, address string) (net.Conn, error) { + return mockConn2, nil + }, + protocolStr: "proto2", + } + + rd := NewRaceDial(logger, serverURL, mockDialer1, mockDialer2) + conn, err := rd.Dial() + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + if conn.RemoteAddr().Network() != proto2 { + t.Errorf("Expected connection with protocol %s, got %s", proto2, conn.RemoteAddr().Network()) + } +} + +func TestRaceDialTimeout(t *testing.T) { + logger := logrus.NewEntry(logrus.New()) + serverURL := "test.server.com" + + connectionTimeout = 3 * time.Second + mockDialer := &MockDialer{ + dialFunc: func(ctx context.Context, address string) (net.Conn, error) { + <-ctx.Done() + return nil, ctx.Err() + }, + protocolStr: "proto1", + } + + rd := NewRaceDial(logger, serverURL, mockDialer) + conn, err := rd.Dial() + if err == nil { + t.Errorf("Expected an error, got nil") + } + if conn != nil { + t.Errorf("Expected nil connection, got %v", conn) + } +} + +func TestRaceDialAllDialersFail(t *testing.T) { + logger := logrus.NewEntry(logrus.New()) + serverURL := "test.server.com" + + mockDialer1 := &MockDialer{ + dialFunc: func(ctx context.Context, address string) (net.Conn, error) { + return nil, errors.New("first dialer failed") + }, + protocolStr: "protocol1", + } + + mockDialer2 := &MockDialer{ + dialFunc: func(ctx context.Context, address string) (net.Conn, error) { + return nil, errors.New("second dialer failed") + }, + protocolStr: "protocol2", + } + + rd := NewRaceDial(logger, serverURL, mockDialer1, mockDialer2) + conn, err := rd.Dial() + if err == nil { + t.Errorf("Expected an error, got nil") + } + if conn != nil { + t.Errorf("Expected nil connection, got %v", conn) + } +} + +func TestRaceDialFirstSuccessfulDialerWins(t *testing.T) { + logger := logrus.NewEntry(logrus.New()) + serverURL := "test.server.com" + proto1 := "protocol1" + proto2 := "protocol2" + + mockConn1 := &MockConn{ + remoteAddr: &MockAddr{network: proto1}, + } + + mockConn2 := &MockConn{ + remoteAddr: &MockAddr{network: proto2}, + } + + mockDialer1 := &MockDialer{ + dialFunc: func(ctx context.Context, address string) (net.Conn, error) { + time.Sleep(1 * time.Second) + return mockConn1, nil + }, + protocolStr: proto1, + } + + mock2err := make(chan error) + mockDialer2 := &MockDialer{ + dialFunc: func(ctx context.Context, address string) (net.Conn, error) { + <-ctx.Done() + mock2err <- ctx.Err() + return mockConn2, ctx.Err() + }, + protocolStr: proto2, + } + + rd := NewRaceDial(logger, serverURL, mockDialer1, mockDialer2) + conn, err := rd.Dial() + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + if conn == nil { + t.Errorf("Expected non-nil connection") + } + if conn != mockConn1 { + t.Errorf("Expected first connection, got %v", conn) + } + + select { + case <-time.After(3 * time.Second): + t.Errorf("Timed out waiting for second dialer to finish") + case err := <-mock2err: + if !errors.Is(err, context.Canceled) { + t.Errorf("Expected context.Canceled error, got %v", err) + } + } +} diff --git a/relay/client/dialer/ws/addr.go b/relay/client/dialer/ws/addr.go index 43f5dd6af..11158cfbd 100644 --- a/relay/client/dialer/ws/addr.go +++ b/relay/client/dialer/ws/addr.go @@ -1,11 +1,15 @@ package ws +const ( + Network = "ws" +) + type WebsocketAddr struct { addr string } func (a WebsocketAddr) Network() string { - return "websocket" + return Network } func (a WebsocketAddr) String() string { diff --git a/relay/client/dialer/ws/conn.go b/relay/client/dialer/ws/conn.go index e7f771b8d..74bcafd82 100644 --- a/relay/client/dialer/ws/conn.go +++ b/relay/client/dialer/ws/conn.go @@ -26,6 +26,7 @@ func NewConn(wsConn *websocket.Conn, serverAddress string) net.Conn { func (c *Conn) Read(b []byte) (n int, err error) { t, ioReader, err := c.Conn.Reader(c.ctx) if err != nil { + // todo use ErrClosedByServer return 0, err } diff --git a/relay/client/dialer/ws/ws.go b/relay/client/dialer/ws/ws.go index d9388aafd..df91a66d4 100644 --- a/relay/client/dialer/ws/ws.go +++ b/relay/client/dialer/ws/ws.go @@ -2,6 +2,7 @@ package ws import ( "context" + "errors" "fmt" "net" "net/http" @@ -15,7 +16,14 @@ import ( nbnet "github.com/netbirdio/netbird/util/net" ) -func Dial(address string) (net.Conn, error) { +type Dialer struct { +} + +func (d Dialer) Protocol() string { + return "WS" +} + +func (d Dialer) Dial(ctx context.Context, address string) (net.Conn, error) { wsURL, err := prepareURL(address) if err != nil { return nil, err @@ -31,8 +39,11 @@ func Dial(address string) (net.Conn, error) { } parsedURL.Path = ws.URLPath - wsConn, resp, err := websocket.Dial(context.Background(), parsedURL.String(), opts) + wsConn, resp, err := websocket.Dial(ctx, parsedURL.String(), opts) if err != nil { + if errors.Is(err, context.Canceled) { + return nil, err + } log.Errorf("failed to dial to Relay server '%s': %s", wsURL, err) return nil, err } diff --git a/relay/server/listener/quic/conn.go b/relay/server/listener/quic/conn.go new file mode 100644 index 000000000..909ec1cc6 --- /dev/null +++ b/relay/server/listener/quic/conn.go @@ -0,0 +1,101 @@ +package quic + +import ( + "context" + "errors" + "fmt" + "net" + "sync" + "time" + + "github.com/quic-go/quic-go" +) + +type Conn struct { + session quic.Connection + closed bool + closedMu sync.Mutex + ctx context.Context + ctxCancel context.CancelFunc +} + +func NewConn(session quic.Connection) *Conn { + ctx, cancel := context.WithCancel(context.Background()) + return &Conn{ + session: session, + ctx: ctx, + ctxCancel: cancel, + } +} + +func (c *Conn) Read(b []byte) (n int, err error) { + dgram, err := c.session.ReceiveDatagram(c.ctx) + if err != nil { + return 0, c.remoteCloseErrHandling(err) + } + // Copy data to b, ensuring we don’t exceed the size of b + n = copy(b, dgram) + return n, nil +} + +func (c *Conn) Write(b []byte) (int, error) { + if err := c.session.SendDatagram(b); err != nil { + return 0, c.remoteCloseErrHandling(err) + } + return len(b), nil +} + +func (c *Conn) LocalAddr() net.Addr { + return c.session.LocalAddr() +} + +func (c *Conn) RemoteAddr() net.Addr { + return c.session.RemoteAddr() +} + +func (c *Conn) SetReadDeadline(t time.Time) error { + return nil +} + +func (c *Conn) SetWriteDeadline(t time.Time) error { + return fmt.Errorf("SetWriteDeadline is not implemented") +} + +func (c *Conn) SetDeadline(t time.Time) error { + return fmt.Errorf("SetDeadline is not implemented") +} + +func (c *Conn) Close() error { + c.closedMu.Lock() + if c.closed { + c.closedMu.Unlock() + return nil + } + c.closed = true + c.closedMu.Unlock() + + c.ctxCancel() // Cancel the context + + sessionErr := c.session.CloseWithError(0, "normal closure") + return sessionErr +} + +func (c *Conn) isClosed() bool { + c.closedMu.Lock() + defer c.closedMu.Unlock() + return c.closed +} + +func (c *Conn) remoteCloseErrHandling(err error) error { + if c.isClosed() { + return net.ErrClosed + } + + // Check if the connection was closed remotely + var appErr *quic.ApplicationError + if errors.As(err, &appErr) && appErr.ErrorCode == 0x0 { + return net.ErrClosed + } + + return err +} diff --git a/relay/server/listener/quic/listener.go b/relay/server/listener/quic/listener.go new file mode 100644 index 000000000..b6e01994f --- /dev/null +++ b/relay/server/listener/quic/listener.go @@ -0,0 +1,66 @@ +package quic + +import ( + "context" + "crypto/tls" + "errors" + "fmt" + "net" + + "github.com/quic-go/quic-go" + log "github.com/sirupsen/logrus" +) + +type Listener struct { + // Address is the address to listen on + Address string + // TLSConfig is the TLS configuration for the server + TLSConfig *tls.Config + + listener *quic.Listener + acceptFn func(conn net.Conn) +} + +func (l *Listener) Listen(acceptFn func(conn net.Conn)) error { + l.acceptFn = acceptFn + + quicCfg := &quic.Config{ + EnableDatagrams: true, + } + listener, err := quic.ListenAddr(l.Address, l.TLSConfig, quicCfg) + if err != nil { + return fmt.Errorf("failed to create QUIC listener: %v", err) + } + + l.listener = listener + log.Infof("QUIC server listening on address: %s", l.Address) + + for { + session, err := listener.Accept(context.Background()) + if err != nil { + if errors.Is(err, quic.ErrServerClosed) { + return nil + } + + log.Errorf("Failed to accept QUIC session: %v", err) + continue + } + + log.Infof("QUIC client connected from: %s", session.RemoteAddr()) + conn := NewConn(session) + l.acceptFn(conn) + } +} + +func (l *Listener) Shutdown(ctx context.Context) error { + if l.listener == nil { + return nil + } + + log.Infof("stopping QUIC listener") + if err := l.listener.Close(); err != nil { + return fmt.Errorf("listener shutdown failed: %v", err) + } + log.Infof("QUIC listener stopped") + return nil +} diff --git a/relay/server/listener/ws/listener.go b/relay/server/listener/ws/listener.go index 5c62c0826..0eb244c77 100644 --- a/relay/server/listener/ws/listener.go +++ b/relay/server/listener/ws/listener.go @@ -88,6 +88,8 @@ func (l *Listener) onAccept(w http.ResponseWriter, r *http.Request) { return } + log.Infof("WS client connected from: %s", rAddr) + conn := NewConn(wsConn, lAddr, rAddr) l.acceptFn(conn) } diff --git a/relay/server/relay.go b/relay/server/relay.go index 6cd8506ae..a5e77bc61 100644 --- a/relay/server/relay.go +++ b/relay/server/relay.go @@ -150,6 +150,8 @@ func (r *Relay) Accept(conn net.Conn) { func (r *Relay) Shutdown(ctx context.Context) { log.Infof("close connection with all peers") r.closeMu.Lock() + defer r.closeMu.Unlock() + wg := sync.WaitGroup{} peers := r.store.Peers() for _, peer := range peers { @@ -161,7 +163,7 @@ func (r *Relay) Shutdown(ctx context.Context) { } wg.Wait() r.metricsCancel() - r.closeMu.Unlock() + r.closed = true } // InstanceURL returns the instance URL of the relay server diff --git a/relay/server/server.go b/relay/server/server.go index 0036e2390..cacc3dafb 100644 --- a/relay/server/server.go +++ b/relay/server/server.go @@ -3,13 +3,17 @@ package server import ( "context" "crypto/tls" + "sync" - log "github.com/sirupsen/logrus" + "github.com/hashicorp/go-multierror" "go.opentelemetry.io/otel/metric" + nberrors "github.com/netbirdio/netbird/client/errors" "github.com/netbirdio/netbird/relay/auth" "github.com/netbirdio/netbird/relay/server/listener" + "github.com/netbirdio/netbird/relay/server/listener/quic" "github.com/netbirdio/netbird/relay/server/listener/ws" + quictls "github.com/netbirdio/netbird/relay/tls" ) // ListenerConfig is the configuration for the listener. @@ -24,8 +28,8 @@ type ListenerConfig struct { // It is the gate between the WebSocket listener and the Relay server logic. // In a new HTTP connection, the server will accept the connection and pass it to the Relay server via the Accept method. type Server struct { - relay *Relay - wSListener listener.Listener + relay *Relay + listeners []listener.Listener } // NewServer creates a new relay server instance. @@ -39,35 +43,63 @@ func NewServer(meter metric.Meter, exposedAddress string, tlsSupport bool, authV return nil, err } return &Server{ - relay: relay, + relay: relay, + listeners: make([]listener.Listener, 0, 2), }, nil } // Listen starts the relay server. func (r *Server) Listen(cfg ListenerConfig) error { - r.wSListener = &ws.Listener{ + wSListener := &ws.Listener{ Address: cfg.Address, TLSConfig: cfg.TLSConfig, } + r.listeners = append(r.listeners, wSListener) - wslErr := r.wSListener.Listen(r.relay.Accept) - if wslErr != nil { - log.Errorf("failed to bind ws server: %s", wslErr) + tlsConfigQUIC, err := quictls.ServerQUICTLSConfig(cfg.TLSConfig) + if err != nil { + return err } - return wslErr + quicListener := &quic.Listener{ + Address: cfg.Address, + TLSConfig: tlsConfigQUIC, + } + + r.listeners = append(r.listeners, quicListener) + + errChan := make(chan error, len(r.listeners)) + wg := sync.WaitGroup{} + for _, l := range r.listeners { + wg.Add(1) + go func(listener listener.Listener) { + defer wg.Done() + errChan <- listener.Listen(r.relay.Accept) + }(l) + } + + wg.Wait() + close(errChan) + var multiErr *multierror.Error + for err := range errChan { + multiErr = multierror.Append(multiErr, err) + } + + return nberrors.FormatErrorOrNil(multiErr) } // Shutdown stops the relay server. If there are active connections, they will be closed gracefully. In case of a context, // the connections will be forcefully closed. -func (r *Server) Shutdown(ctx context.Context) (err error) { - // stop service new connections - if r.wSListener != nil { - err = r.wSListener.Shutdown(ctx) - } - +func (r *Server) Shutdown(ctx context.Context) error { r.relay.Shutdown(ctx) - return + + var multiErr *multierror.Error + for _, l := range r.listeners { + if err := l.Shutdown(ctx); err != nil { + multiErr = multierror.Append(multiErr, err) + } + } + return nberrors.FormatErrorOrNil(multiErr) } // InstanceURL returns the instance URL of the relay server. diff --git a/relay/tls/alpn.go b/relay/tls/alpn.go new file mode 100644 index 000000000..29497d401 --- /dev/null +++ b/relay/tls/alpn.go @@ -0,0 +1,3 @@ +package tls + +const nbalpn = "nb-quic" diff --git a/relay/tls/client_dev.go b/relay/tls/client_dev.go new file mode 100644 index 000000000..f6b8290a0 --- /dev/null +++ b/relay/tls/client_dev.go @@ -0,0 +1,12 @@ +//go:build devcert + +package tls + +import "crypto/tls" + +func ClientQUICTLSConfig() *tls.Config { + return &tls.Config{ + InsecureSkipVerify: true, // Debug mode allows insecure connections + NextProtos: []string{nbalpn}, // Ensure this matches the server's ALPN + } +} diff --git a/relay/tls/client_prod.go b/relay/tls/client_prod.go new file mode 100644 index 000000000..686093a37 --- /dev/null +++ b/relay/tls/client_prod.go @@ -0,0 +1,11 @@ +//go:build !devcert + +package tls + +import "crypto/tls" + +func ClientQUICTLSConfig() *tls.Config { + return &tls.Config{ + NextProtos: []string{nbalpn}, + } +} diff --git a/relay/tls/doc.go b/relay/tls/doc.go new file mode 100644 index 000000000..38b807f84 --- /dev/null +++ b/relay/tls/doc.go @@ -0,0 +1,36 @@ +// Package tls provides utilities for configuring and managing Transport Layer +// Security (TLS) in server and client environments, with a focus on QUIC +// protocol support and testing configurations. +// +// The package includes functions for cloning and customizing TLS +// configurations as well as generating self-signed certificates for +// development and testing purposes. +// +// Key Features: +// +// - `ServerQUICTLSConfig`: Creates a server-side TLS configuration tailored +// for QUIC protocol with specified or default settings. QUIC requires a +// specific TLS configuration with proper ALPN (Application-Layer Protocol +// Negotiation) support, making the TLS settings crucial for establishing +// secure connections. +// +// - `ClientQUICTLSConfig`: Provides a client-side TLS configuration suitable +// for QUIC protocol. The configuration differs between development +// (insecure testing) and production (strict verification). +// +// - `generateTestTLSConfig`: Generates a self-signed TLS configuration for +// use in local development and testing scenarios. +// +// Usage: +// +// This package provides separate implementations for development and production +// environments. The development implementation (guarded by `//go:build devcert`) +// supports testing configurations with self-signed certificates and insecure +// client connections. The production implementation (guarded by `//go:build +// !devcert`) ensures that valid and secure TLS configurations are supplied +// and used. +// +// The QUIC protocol is highly reliant on properly configured TLS settings, +// and this package ensures that configurations meet the requirements for +// secure and efficient QUIC communication. +package tls diff --git a/relay/tls/server_dev.go b/relay/tls/server_dev.go new file mode 100644 index 000000000..1a01658fc --- /dev/null +++ b/relay/tls/server_dev.go @@ -0,0 +1,79 @@ +//go:build devcert + +package tls + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "math/big" + "net" + "time" + + log "github.com/sirupsen/logrus" +) + +func ServerQUICTLSConfig(originTLSCfg *tls.Config) (*tls.Config, error) { + if originTLSCfg == nil { + log.Warnf("QUIC server will use self signed certificate for testing!") + return generateTestTLSConfig() + } + + cfg := originTLSCfg.Clone() + cfg.NextProtos = []string{nbalpn} + return cfg, nil +} + +// GenerateTestTLSConfig creates a self-signed certificate for testing +func generateTestTLSConfig() (*tls.Config, error) { + log.Infof("generating test TLS config") + privateKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return nil, err + } + + template := x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{ + Organization: []string{"Test Organization"}, + }, + NotBefore: time.Now(), + NotAfter: time.Now().Add(time.Hour * 24 * 180), // Valid for 180 days + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{ + x509.ExtKeyUsageServerAuth, + }, + BasicConstraintsValid: true, + DNSNames: []string{"localhost"}, + IPAddresses: []net.IP{net.ParseIP("127.0.0.1")}, + } + + // Create certificate + certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey) + if err != nil { + return nil, err + } + + certPEM := pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Bytes: certDER, + }) + + privateKeyPEM := pem.EncodeToMemory(&pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(privateKey), + }) + + tlsCert, err := tls.X509KeyPair(certPEM, privateKeyPEM) + if err != nil { + return nil, err + } + + return &tls.Config{ + Certificates: []tls.Certificate{tlsCert}, + NextProtos: []string{nbalpn}, + }, nil +} diff --git a/relay/tls/server_prod.go b/relay/tls/server_prod.go new file mode 100644 index 000000000..9d1c47d88 --- /dev/null +++ b/relay/tls/server_prod.go @@ -0,0 +1,17 @@ +//go:build !devcert + +package tls + +import ( + "crypto/tls" + "fmt" +) + +func ServerQUICTLSConfig(originTLSCfg *tls.Config) (*tls.Config, error) { + if originTLSCfg == nil { + return nil, fmt.Errorf("valid TLS config is required for QUIC listener") + } + cfg := originTLSCfg.Clone() + cfg.NextProtos = []string{nbalpn} + return cfg, nil +} From 5a82477d48b634010d372a9a5eef482e1b3c5d1a Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Wed, 15 Jan 2025 16:57:41 +0100 Subject: [PATCH 10/92] [client] Remove outbound chains (#3157) --- client/firewall/iptables/acl_linux.go | 70 +++------------ client/firewall/iptables/manager_linux.go | 6 +- .../firewall/iptables/manager_linux_test.go | 67 +------------- client/firewall/manager/firewall.go | 1 - client/firewall/nftables/acl_linux.go | 89 ++++--------------- client/firewall/nftables/manager_linux.go | 12 ++- .../firewall/nftables/manager_linux_test.go | 28 +----- client/firewall/uspfilter/rule.go | 3 - client/firewall/uspfilter/uspfilter.go | 49 ++++------ .../uspfilter/uspfilter_bench_test.go | 14 +-- client/firewall/uspfilter/uspfilter_test.go | 47 ++-------- client/internal/acl/manager.go | 40 +++------ client/internal/acl/manager_test.go | 8 +- client/internal/dnsfwd/manager.go | 2 +- client/internal/engine.go | 1 - 15 files changed, 92 insertions(+), 345 deletions(-) diff --git a/client/firewall/iptables/acl_linux.go b/client/firewall/iptables/acl_linux.go index d774f4538..2592ff840 100644 --- a/client/firewall/iptables/acl_linux.go +++ b/client/firewall/iptables/acl_linux.go @@ -19,8 +19,7 @@ const ( tableName = "filter" // rules chains contains the effective ACL rules - chainNameInputRules = "NETBIRD-ACL-INPUT" - chainNameOutputRules = "NETBIRD-ACL-OUTPUT" + chainNameInputRules = "NETBIRD-ACL-INPUT" ) type aclEntries map[string][][]string @@ -84,7 +83,6 @@ func (m *aclManager) AddPeerFiltering( protocol firewall.Protocol, sPort *firewall.Port, dPort *firewall.Port, - direction firewall.RuleDirection, action firewall.Action, ipsetName string, ) ([]firewall.Rule, error) { @@ -97,15 +95,10 @@ func (m *aclManager) AddPeerFiltering( sPortVal = strconv.Itoa(sPort.Values[0]) } - var chain string - if direction == firewall.RuleDirectionOUT { - chain = chainNameOutputRules - } else { - chain = chainNameInputRules - } + chain := chainNameInputRules ipsetName = transformIPsetName(ipsetName, sPortVal, dPortVal) - specs := filterRuleSpecs(ip, string(protocol), sPortVal, dPortVal, direction, action, ipsetName) + specs := filterRuleSpecs(ip, string(protocol), sPortVal, dPortVal, action, ipsetName) if ipsetName != "" { if ipList, ipsetExists := m.ipsetStore.ipset(ipsetName); ipsetExists { if err := ipset.Add(ipsetName, ip.String()); err != nil { @@ -214,28 +207,7 @@ func (m *aclManager) Reset() error { // todo write less destructive cleanup mechanism func (m *aclManager) cleanChains() error { - ok, err := m.iptablesClient.ChainExists(tableName, chainNameOutputRules) - if err != nil { - log.Debugf("failed to list chains: %s", err) - return err - } - if ok { - rules := m.entries["OUTPUT"] - for _, rule := range rules { - err := m.iptablesClient.DeleteIfExists(tableName, "OUTPUT", rule...) - if err != nil { - log.Errorf("failed to delete rule: %v, %s", rule, err) - } - } - - err = m.iptablesClient.ClearAndDeleteChain(tableName, chainNameOutputRules) - if err != nil { - log.Debugf("failed to clear and delete %s chain: %s", chainNameOutputRules, err) - return err - } - } - - ok, err = m.iptablesClient.ChainExists(tableName, chainNameInputRules) + ok, err := m.iptablesClient.ChainExists(tableName, chainNameInputRules) if err != nil { log.Debugf("failed to list chains: %s", err) return err @@ -295,12 +267,6 @@ func (m *aclManager) createDefaultChains() error { return err } - // chain netbird-acl-output-rules - if err := m.iptablesClient.NewChain(tableName, chainNameOutputRules); err != nil { - log.Debugf("failed to create '%s' chain: %s", chainNameOutputRules, err) - return err - } - for chainName, rules := range m.entries { for _, rule := range rules { if err := m.iptablesClient.InsertUnique(tableName, chainName, 1, rule...); err != nil { @@ -329,8 +295,6 @@ func (m *aclManager) createDefaultChains() error { // The existing FORWARD rules/policies decide outbound traffic towards our interface. // In case the FORWARD policy is set to "drop", we add an established/related rule to allow return traffic for the inbound rule. - -// The OUTPUT chain gets an extra rule to allow traffic to any set up routes, the return traffic is handled by the INPUT related/established rule. func (m *aclManager) seedInitialEntries() { established := getConntrackEstablished() @@ -390,30 +354,18 @@ func (m *aclManager) updateState() { } // filterRuleSpecs returns the specs of a filtering rule -func filterRuleSpecs( - ip net.IP, protocol string, sPort, dPort string, direction firewall.RuleDirection, action firewall.Action, ipsetName string, -) (specs []string) { +func filterRuleSpecs(ip net.IP, protocol, sPort, dPort string, action firewall.Action, ipsetName string) (specs []string) { matchByIP := true // don't use IP matching if IP is ip 0.0.0.0 if ip.String() == "0.0.0.0" { matchByIP = false } - switch direction { - case firewall.RuleDirectionIN: - if matchByIP { - if ipsetName != "" { - specs = append(specs, "-m", "set", "--set", ipsetName, "src") - } else { - specs = append(specs, "-s", ip.String()) - } - } - case firewall.RuleDirectionOUT: - if matchByIP { - if ipsetName != "" { - specs = append(specs, "-m", "set", "--set", ipsetName, "dst") - } else { - specs = append(specs, "-d", ip.String()) - } + + if matchByIP { + if ipsetName != "" { + specs = append(specs, "-m", "set", "--set", ipsetName, "src") + } else { + specs = append(specs, "-s", ip.String()) } } if protocol != "all" { diff --git a/client/firewall/iptables/manager_linux.go b/client/firewall/iptables/manager_linux.go index da8e2c08f..75f082fc4 100644 --- a/client/firewall/iptables/manager_linux.go +++ b/client/firewall/iptables/manager_linux.go @@ -100,15 +100,14 @@ func (m *Manager) AddPeerFiltering( protocol firewall.Protocol, sPort *firewall.Port, dPort *firewall.Port, - direction firewall.RuleDirection, action firewall.Action, ipsetName string, - comment string, + _ string, ) ([]firewall.Rule, error) { m.mutex.Lock() defer m.mutex.Unlock() - return m.aclMgr.AddPeerFiltering(ip, protocol, sPort, dPort, direction, action, ipsetName) + return m.aclMgr.AddPeerFiltering(ip, protocol, sPort, dPort, action, ipsetName) } func (m *Manager) AddRouteFiltering( @@ -201,7 +200,6 @@ func (m *Manager) AllowNetbird() error { "all", nil, nil, - firewall.RuleDirectionIN, firewall.ActionAccept, "", "", diff --git a/client/firewall/iptables/manager_linux_test.go b/client/firewall/iptables/manager_linux_test.go index ebdb83137..fe0bc86de 100644 --- a/client/firewall/iptables/manager_linux_test.go +++ b/client/firewall/iptables/manager_linux_test.go @@ -68,27 +68,13 @@ func TestIptablesManager(t *testing.T) { time.Sleep(time.Second) }() - var rule1 []fw.Rule - t.Run("add first rule", func(t *testing.T) { - ip := net.ParseIP("10.20.0.2") - port := &fw.Port{Values: []int{8080}} - rule1, err = manager.AddPeerFiltering(ip, "tcp", nil, port, fw.RuleDirectionOUT, fw.ActionAccept, "", "accept HTTP traffic") - require.NoError(t, err, "failed to add rule") - - for _, r := range rule1 { - checkRuleSpecs(t, ipv4Client, chainNameOutputRules, true, r.(*Rule).specs...) - } - - }) - var rule2 []fw.Rule t.Run("add second rule", func(t *testing.T) { ip := net.ParseIP("10.20.0.3") port := &fw.Port{ Values: []int{8043: 8046}, } - rule2, err = manager.AddPeerFiltering( - ip, "tcp", port, nil, fw.RuleDirectionIN, fw.ActionAccept, "", "accept HTTPS traffic from ports range") + rule2, err = manager.AddPeerFiltering(ip, "tcp", port, nil, fw.ActionAccept, "", "accept HTTPS traffic from ports range") require.NoError(t, err, "failed to add rule") for _, r := range rule2 { @@ -97,15 +83,6 @@ func TestIptablesManager(t *testing.T) { } }) - t.Run("delete first rule", func(t *testing.T) { - for _, r := range rule1 { - err := manager.DeletePeerRule(r) - require.NoError(t, err, "failed to delete rule") - - checkRuleSpecs(t, ipv4Client, chainNameOutputRules, false, r.(*Rule).specs...) - } - }) - t.Run("delete second rule", func(t *testing.T) { for _, r := range rule2 { err := manager.DeletePeerRule(r) @@ -119,7 +96,7 @@ func TestIptablesManager(t *testing.T) { // add second rule ip := net.ParseIP("10.20.0.3") port := &fw.Port{Values: []int{5353}} - _, err = manager.AddPeerFiltering(ip, "udp", nil, port, fw.RuleDirectionOUT, fw.ActionAccept, "", "accept Fake DNS traffic") + _, err = manager.AddPeerFiltering(ip, "udp", nil, port, fw.ActionAccept, "", "accept Fake DNS traffic") require.NoError(t, err, "failed to add rule") err = manager.Reset(nil) @@ -135,9 +112,6 @@ func TestIptablesManager(t *testing.T) { } func TestIptablesManagerIPSet(t *testing.T) { - ipv4Client, err := iptables.NewWithProtocol(iptables.ProtocolIPv4) - require.NoError(t, err) - mock := &iFaceMock{ NameFunc: func() string { return "lo" @@ -167,33 +141,13 @@ func TestIptablesManagerIPSet(t *testing.T) { time.Sleep(time.Second) }() - var rule1 []fw.Rule - t.Run("add first rule with set", func(t *testing.T) { - ip := net.ParseIP("10.20.0.2") - port := &fw.Port{Values: []int{8080}} - rule1, err = manager.AddPeerFiltering( - ip, "tcp", nil, port, fw.RuleDirectionOUT, - fw.ActionAccept, "default", "accept HTTP traffic", - ) - require.NoError(t, err, "failed to add rule") - - for _, r := range rule1 { - checkRuleSpecs(t, ipv4Client, chainNameOutputRules, true, r.(*Rule).specs...) - require.Equal(t, r.(*Rule).ipsetName, "default-dport", "ipset name must be set") - require.Equal(t, r.(*Rule).ip, "10.20.0.2", "ipset IP must be set") - } - }) - var rule2 []fw.Rule t.Run("add second rule", func(t *testing.T) { ip := net.ParseIP("10.20.0.3") port := &fw.Port{ Values: []int{443}, } - rule2, err = manager.AddPeerFiltering( - ip, "tcp", port, nil, fw.RuleDirectionIN, fw.ActionAccept, - "default", "accept HTTPS traffic from ports range", - ) + rule2, err = manager.AddPeerFiltering(ip, "tcp", port, nil, fw.ActionAccept, "default", "accept HTTPS traffic from ports range") for _, r := range rule2 { require.NoError(t, err, "failed to add rule") require.Equal(t, r.(*Rule).ipsetName, "default-sport", "ipset name must be set") @@ -201,15 +155,6 @@ func TestIptablesManagerIPSet(t *testing.T) { } }) - t.Run("delete first rule", func(t *testing.T) { - for _, r := range rule1 { - err := manager.DeletePeerRule(r) - require.NoError(t, err, "failed to delete rule") - - require.NotContains(t, manager.aclMgr.ipsetStore.ipsets, r.(*Rule).ruleID, "rule must be removed form the ruleset index") - } - }) - t.Run("delete second rule", func(t *testing.T) { for _, r := range rule2 { err := manager.DeletePeerRule(r) @@ -270,11 +215,7 @@ func TestIptablesCreatePerformance(t *testing.T) { start := time.Now() for i := 0; i < testMax; i++ { port := &fw.Port{Values: []int{1000 + i}} - if i%2 == 0 { - _, err = manager.AddPeerFiltering(ip, "tcp", nil, port, fw.RuleDirectionOUT, fw.ActionAccept, "", "accept HTTP traffic") - } else { - _, err = manager.AddPeerFiltering(ip, "tcp", nil, port, fw.RuleDirectionIN, fw.ActionAccept, "", "accept HTTP traffic") - } + _, err = manager.AddPeerFiltering(ip, "tcp", nil, port, fw.ActionAccept, "", "accept HTTP traffic") require.NoError(t, err, "failed to add rule") } diff --git a/client/firewall/manager/firewall.go b/client/firewall/manager/firewall.go index 9391b47ec..f46e5eb5d 100644 --- a/client/firewall/manager/firewall.go +++ b/client/firewall/manager/firewall.go @@ -69,7 +69,6 @@ type Manager interface { proto Protocol, sPort *Port, dPort *Port, - direction RuleDirection, action Action, ipsetName string, comment string, diff --git a/client/firewall/nftables/acl_linux.go b/client/firewall/nftables/acl_linux.go index 852cfec8d..8c1d89e68 100644 --- a/client/firewall/nftables/acl_linux.go +++ b/client/firewall/nftables/acl_linux.go @@ -22,8 +22,7 @@ import ( const ( // rules chains contains the effective ACL rules - chainNameInputRules = "netbird-acl-input-rules" - chainNameOutputRules = "netbird-acl-output-rules" + chainNameInputRules = "netbird-acl-input-rules" // filter chains contains the rules that jump to the rules chains chainNameInputFilter = "netbird-acl-input-filter" @@ -45,9 +44,8 @@ type AclManager struct { wgIface iFaceMapper routingFwChainName string - workTable *nftables.Table - chainInputRules *nftables.Chain - chainOutputRules *nftables.Chain + workTable *nftables.Table + chainInputRules *nftables.Chain ipsetStore *ipsetStore rules map[string]*Rule @@ -89,7 +87,6 @@ func (m *AclManager) AddPeerFiltering( proto firewall.Protocol, sPort *firewall.Port, dPort *firewall.Port, - direction firewall.RuleDirection, action firewall.Action, ipsetName string, comment string, @@ -104,7 +101,7 @@ func (m *AclManager) AddPeerFiltering( } newRules := make([]firewall.Rule, 0, 2) - ioRule, err := m.addIOFiltering(ip, proto, sPort, dPort, direction, action, ipset, comment) + ioRule, err := m.addIOFiltering(ip, proto, sPort, dPort, action, ipset, comment) if err != nil { return nil, err } @@ -214,38 +211,6 @@ func (m *AclManager) createDefaultAllowRules() error { Exprs: expIn, }) - expOut := []expr.Any{ - &expr.Payload{ - DestRegister: 1, - Base: expr.PayloadBaseNetworkHeader, - Offset: 16, - Len: 4, - }, - // mask - &expr.Bitwise{ - SourceRegister: 1, - DestRegister: 1, - Len: 4, - Mask: []byte{0, 0, 0, 0}, - Xor: []byte{0, 0, 0, 0}, - }, - // net address - &expr.Cmp{ - Register: 1, - Data: []byte{0, 0, 0, 0}, - }, - &expr.Verdict{ - Kind: expr.VerdictAccept, - }, - } - - _ = m.rConn.InsertRule(&nftables.Rule{ - Table: m.workTable, - Chain: m.chainOutputRules, - Position: 0, - Exprs: expOut, - }) - if err := m.rConn.Flush(); err != nil { return fmt.Errorf(flushError, err) } @@ -264,15 +229,19 @@ func (m *AclManager) Flush() error { log.Errorf("failed to refresh rule handles ipv4 input chain: %v", err) } - if err := m.refreshRuleHandles(m.chainOutputRules); err != nil { - log.Errorf("failed to refresh rule handles IPv4 output chain: %v", err) - } - return nil } -func (m *AclManager) addIOFiltering(ip net.IP, proto firewall.Protocol, sPort *firewall.Port, dPort *firewall.Port, direction firewall.RuleDirection, action firewall.Action, ipset *nftables.Set, comment string) (*Rule, error) { - ruleId := generatePeerRuleId(ip, sPort, dPort, direction, action, ipset) +func (m *AclManager) addIOFiltering( + ip net.IP, + proto firewall.Protocol, + sPort *firewall.Port, + dPort *firewall.Port, + action firewall.Action, + ipset *nftables.Set, + comment string, +) (*Rule, error) { + ruleId := generatePeerRuleId(ip, sPort, dPort, action, ipset) if r, ok := m.rules[ruleId]; ok { return &Rule{ r.nftRule, @@ -310,9 +279,6 @@ func (m *AclManager) addIOFiltering(ip net.IP, proto firewall.Protocol, sPort *f if !bytes.HasPrefix(anyIP, rawIP) { // source address position addrOffset := uint32(12) - if direction == firewall.RuleDirectionOUT { - addrOffset += 4 // is ipv4 address length - } expressions = append(expressions, &expr.Payload{ @@ -383,12 +349,7 @@ func (m *AclManager) addIOFiltering(ip net.IP, proto firewall.Protocol, sPort *f userData := []byte(strings.Join([]string{ruleId, comment}, " ")) - var chain *nftables.Chain - if direction == firewall.RuleDirectionIN { - chain = m.chainInputRules - } else { - chain = m.chainOutputRules - } + chain := m.chainInputRules nftRule := m.rConn.AddRule(&nftables.Rule{ Table: m.workTable, Chain: chain, @@ -419,15 +380,6 @@ func (m *AclManager) createDefaultChains() (err error) { } m.chainInputRules = chain - // chainNameOutputRules - chain = m.createChain(chainNameOutputRules) - err = m.rConn.Flush() - if err != nil { - log.Debugf("failed to create chain (%s): %s", chainNameOutputRules, err) - return err - } - m.chainOutputRules = chain - // netbird-acl-input-filter // type filter hook input priority filter; policy accept; chain = m.createFilterChainWithHook(chainNameInputFilter, nftables.ChainHookInput) @@ -720,15 +672,8 @@ func (m *AclManager) refreshRuleHandles(chain *nftables.Chain) error { return nil } -func generatePeerRuleId( - ip net.IP, - sPort *firewall.Port, - dPort *firewall.Port, - direction firewall.RuleDirection, - action firewall.Action, - ipset *nftables.Set, -) string { - rulesetID := ":" + strconv.Itoa(int(direction)) + ":" +func generatePeerRuleId(ip net.IP, sPort *firewall.Port, dPort *firewall.Port, action firewall.Action, ipset *nftables.Set) string { + rulesetID := ":" if sPort != nil { rulesetID += sPort.String() } diff --git a/client/firewall/nftables/manager_linux.go b/client/firewall/nftables/manager_linux.go index 8e1aa0d80..a78626dbc 100644 --- a/client/firewall/nftables/manager_linux.go +++ b/client/firewall/nftables/manager_linux.go @@ -117,7 +117,6 @@ func (m *Manager) AddPeerFiltering( proto firewall.Protocol, sPort *firewall.Port, dPort *firewall.Port, - direction firewall.RuleDirection, action firewall.Action, ipsetName string, comment string, @@ -130,10 +129,17 @@ func (m *Manager) AddPeerFiltering( return nil, fmt.Errorf("unsupported IP version: %s", ip.String()) } - return m.aclManager.AddPeerFiltering(ip, proto, sPort, dPort, direction, action, ipsetName, comment) + return m.aclManager.AddPeerFiltering(ip, proto, sPort, dPort, action, ipsetName, comment) } -func (m *Manager) AddRouteFiltering(sources []netip.Prefix, destination netip.Prefix, proto firewall.Protocol, sPort *firewall.Port, dPort *firewall.Port, action firewall.Action) (firewall.Rule, error) { +func (m *Manager) AddRouteFiltering( + sources []netip.Prefix, + destination netip.Prefix, + proto firewall.Protocol, + sPort *firewall.Port, + dPort *firewall.Port, + action firewall.Action, +) (firewall.Rule, error) { m.mutex.Lock() defer m.mutex.Unlock() diff --git a/client/firewall/nftables/manager_linux_test.go b/client/firewall/nftables/manager_linux_test.go index 33fdc4b3d..9c9637282 100644 --- a/client/firewall/nftables/manager_linux_test.go +++ b/client/firewall/nftables/manager_linux_test.go @@ -74,16 +74,7 @@ func TestNftablesManager(t *testing.T) { testClient := &nftables.Conn{} - rule, err := manager.AddPeerFiltering( - ip, - fw.ProtocolTCP, - nil, - &fw.Port{Values: []int{53}}, - fw.RuleDirectionIN, - fw.ActionDrop, - "", - "", - ) + rule, err := manager.AddPeerFiltering(ip, fw.ProtocolTCP, nil, &fw.Port{Values: []int{53}}, fw.ActionDrop, "", "") require.NoError(t, err, "failed to add rule") err = manager.Flush() @@ -210,11 +201,7 @@ func TestNFtablesCreatePerformance(t *testing.T) { start := time.Now() for i := 0; i < testMax; i++ { port := &fw.Port{Values: []int{1000 + i}} - if i%2 == 0 { - _, err = manager.AddPeerFiltering(ip, "tcp", nil, port, fw.RuleDirectionOUT, fw.ActionAccept, "", "accept HTTP traffic") - } else { - _, err = manager.AddPeerFiltering(ip, "tcp", nil, port, fw.RuleDirectionIN, fw.ActionAccept, "", "accept HTTP traffic") - } + _, err = manager.AddPeerFiltering(ip, "tcp", nil, port, fw.ActionAccept, "", "accept HTTP traffic") require.NoError(t, err, "failed to add rule") if i%100 == 0 { @@ -296,16 +283,7 @@ func TestNftablesManagerCompatibilityWithIptables(t *testing.T) { }) ip := net.ParseIP("100.96.0.1") - _, err = manager.AddPeerFiltering( - ip, - fw.ProtocolTCP, - nil, - &fw.Port{Values: []int{80}}, - fw.RuleDirectionIN, - fw.ActionAccept, - "", - "test rule", - ) + _, err = manager.AddPeerFiltering(ip, fw.ProtocolTCP, nil, &fw.Port{Values: []int{80}}, fw.ActionAccept, "", "test rule") require.NoError(t, err, "failed to add peer filtering rule") _, err = manager.AddRouteFiltering( diff --git a/client/firewall/uspfilter/rule.go b/client/firewall/uspfilter/rule.go index 5c1daccaf..1f98ef43e 100644 --- a/client/firewall/uspfilter/rule.go +++ b/client/firewall/uspfilter/rule.go @@ -4,8 +4,6 @@ import ( "net" "github.com/google/gopacket" - - firewall "github.com/netbirdio/netbird/client/firewall/manager" ) // Rule to handle management of rules @@ -15,7 +13,6 @@ type Rule struct { ipLayer gopacket.LayerType matchByIP bool protoLayer gopacket.LayerType - direction firewall.RuleDirection sPort uint16 dPort uint16 drop bool diff --git a/client/firewall/uspfilter/uspfilter.go b/client/firewall/uspfilter/uspfilter.go index ebe04caee..f35d971b8 100644 --- a/client/firewall/uspfilter/uspfilter.go +++ b/client/firewall/uspfilter/uspfilter.go @@ -39,7 +39,9 @@ type RuleSet map[string]Rule // Manager userspace firewall manager type Manager struct { - outgoingRules map[string]RuleSet + // outgoingRules is used for hooks only + outgoingRules map[string]RuleSet + // incomingRules is used for filtering and hooks incomingRules map[string]RuleSet wgNetwork *net.IPNet decoders sync.Pool @@ -156,9 +158,8 @@ func (m *Manager) AddPeerFiltering( proto firewall.Protocol, sPort *firewall.Port, dPort *firewall.Port, - direction firewall.RuleDirection, action firewall.Action, - ipsetName string, + _ string, comment string, ) ([]firewall.Rule, error) { r := Rule{ @@ -166,7 +167,6 @@ func (m *Manager) AddPeerFiltering( ip: ip, ipLayer: layers.LayerTypeIPv6, matchByIP: true, - direction: direction, drop: action == firewall.ActionDrop, comment: comment, } @@ -202,17 +202,10 @@ func (m *Manager) AddPeerFiltering( } m.mutex.Lock() - if direction == firewall.RuleDirectionIN { - if _, ok := m.incomingRules[r.ip.String()]; !ok { - m.incomingRules[r.ip.String()] = make(RuleSet) - } - m.incomingRules[r.ip.String()][r.id] = r - } else { - if _, ok := m.outgoingRules[r.ip.String()]; !ok { - m.outgoingRules[r.ip.String()] = make(RuleSet) - } - m.outgoingRules[r.ip.String()][r.id] = r + if _, ok := m.incomingRules[r.ip.String()]; !ok { + m.incomingRules[r.ip.String()] = make(RuleSet) } + m.incomingRules[r.ip.String()][r.id] = r m.mutex.Unlock() return []firewall.Rule{&r}, nil } @@ -241,19 +234,10 @@ func (m *Manager) DeletePeerRule(rule firewall.Rule) error { return fmt.Errorf("delete rule: invalid rule type: %T", rule) } - if r.direction == firewall.RuleDirectionIN { - _, ok := m.incomingRules[r.ip.String()][r.id] - if !ok { - return fmt.Errorf("delete rule: no rule with such id: %v", r.id) - } - delete(m.incomingRules[r.ip.String()], r.id) - } else { - _, ok := m.outgoingRules[r.ip.String()][r.id] - if !ok { - return fmt.Errorf("delete rule: no rule with such id: %v", r.id) - } - delete(m.outgoingRules[r.ip.String()], r.id) + if _, ok := m.incomingRules[r.ip.String()][r.id]; !ok { + return fmt.Errorf("delete rule: no rule with such id: %v", r.id) } + delete(m.incomingRules[r.ip.String()], r.id) return nil } @@ -566,7 +550,6 @@ func (m *Manager) AddUDPPacketHook( protoLayer: layers.LayerTypeUDP, dPort: dPort, ipLayer: layers.LayerTypeIPv6, - direction: firewall.RuleDirectionOUT, comment: fmt.Sprintf("UDP Hook direction: %v, ip:%v, dport:%d", in, ip, dPort), udpHook: hook, } @@ -577,7 +560,6 @@ func (m *Manager) AddUDPPacketHook( m.mutex.Lock() if in { - r.direction = firewall.RuleDirectionIN if _, ok := m.incomingRules[r.ip.String()]; !ok { m.incomingRules[r.ip.String()] = make(map[string]Rule) } @@ -596,19 +578,22 @@ func (m *Manager) AddUDPPacketHook( // RemovePacketHook removes packet hook by given ID func (m *Manager) RemovePacketHook(hookID string) error { + m.mutex.Lock() + defer m.mutex.Unlock() + for _, arr := range m.incomingRules { for _, r := range arr { if r.id == hookID { - rule := r - return m.DeletePeerRule(&rule) + delete(arr, r.id) + return nil } } } for _, arr := range m.outgoingRules { for _, r := range arr { if r.id == hookID { - rule := r - return m.DeletePeerRule(&rule) + delete(arr, r.id) + return nil } } } diff --git a/client/firewall/uspfilter/uspfilter_bench_test.go b/client/firewall/uspfilter/uspfilter_bench_test.go index 3c661e71c..4a210bf47 100644 --- a/client/firewall/uspfilter/uspfilter_bench_test.go +++ b/client/firewall/uspfilter/uspfilter_bench_test.go @@ -91,7 +91,7 @@ func BenchmarkCoreFiltering(b *testing.B) { setupFunc: func(m *Manager) { // Single rule allowing all traffic _, err := m.AddPeerFiltering(net.ParseIP("0.0.0.0"), fw.ProtocolALL, nil, nil, - fw.RuleDirectionIN, fw.ActionAccept, "", "allow all") + fw.ActionAccept, "", "allow all") require.NoError(b, err) }, desc: "Baseline: Single 'allow all' rule without connection tracking", @@ -114,7 +114,7 @@ func BenchmarkCoreFiltering(b *testing.B) { _, err := m.AddPeerFiltering(ip, fw.ProtocolTCP, &fw.Port{Values: []int{1024 + i}}, &fw.Port{Values: []int{80}}, - fw.RuleDirectionIN, fw.ActionAccept, "", "explicit return") + fw.ActionAccept, "", "explicit return") require.NoError(b, err) } }, @@ -126,7 +126,7 @@ func BenchmarkCoreFiltering(b *testing.B) { setupFunc: func(m *Manager) { // Add some basic rules but rely on state for established connections _, err := m.AddPeerFiltering(net.ParseIP("0.0.0.0"), fw.ProtocolTCP, nil, nil, - fw.RuleDirectionIN, fw.ActionDrop, "", "default drop") + fw.ActionDrop, "", "default drop") require.NoError(b, err) }, desc: "Connection tracking with established connections", @@ -590,7 +590,7 @@ func BenchmarkLongLivedConnections(b *testing.B) { _, err := manager.AddPeerFiltering(net.ParseIP("0.0.0.0"), fw.ProtocolTCP, &fw.Port{Values: []int{80}}, nil, - fw.RuleDirectionIN, fw.ActionAccept, "", "return traffic") + fw.ActionAccept, "", "return traffic") require.NoError(b, err) } @@ -681,7 +681,7 @@ func BenchmarkShortLivedConnections(b *testing.B) { _, err := manager.AddPeerFiltering(net.ParseIP("0.0.0.0"), fw.ProtocolTCP, &fw.Port{Values: []int{80}}, nil, - fw.RuleDirectionIN, fw.ActionAccept, "", "return traffic") + fw.ActionAccept, "", "return traffic") require.NoError(b, err) } @@ -799,7 +799,7 @@ func BenchmarkParallelLongLivedConnections(b *testing.B) { _, err := manager.AddPeerFiltering(net.ParseIP("0.0.0.0"), fw.ProtocolTCP, &fw.Port{Values: []int{80}}, nil, - fw.RuleDirectionIN, fw.ActionAccept, "", "return traffic") + fw.ActionAccept, "", "return traffic") require.NoError(b, err) } @@ -886,7 +886,7 @@ func BenchmarkParallelShortLivedConnections(b *testing.B) { _, err := manager.AddPeerFiltering(net.ParseIP("0.0.0.0"), fw.ProtocolTCP, &fw.Port{Values: []int{80}}, nil, - fw.RuleDirectionIN, fw.ActionAccept, "", "return traffic") + fw.ActionAccept, "", "return traffic") require.NoError(b, err) } diff --git a/client/firewall/uspfilter/uspfilter_test.go b/client/firewall/uspfilter/uspfilter_test.go index d3563e6f2..7e87443aa 100644 --- a/client/firewall/uspfilter/uspfilter_test.go +++ b/client/firewall/uspfilter/uspfilter_test.go @@ -70,11 +70,10 @@ func TestManagerAddPeerFiltering(t *testing.T) { ip := net.ParseIP("192.168.1.1") proto := fw.ProtocolTCP port := &fw.Port{Values: []int{80}} - direction := fw.RuleDirectionOUT action := fw.ActionDrop comment := "Test rule" - rule, err := m.AddPeerFiltering(ip, proto, nil, port, direction, action, "", comment) + rule, err := m.AddPeerFiltering(ip, proto, nil, port, action, "", comment) if err != nil { t.Errorf("failed to add filtering: %v", err) return @@ -105,37 +104,15 @@ func TestManagerDeleteRule(t *testing.T) { ip := net.ParseIP("192.168.1.1") proto := fw.ProtocolTCP port := &fw.Port{Values: []int{80}} - direction := fw.RuleDirectionOUT action := fw.ActionDrop - comment := "Test rule" + comment := "Test rule 2" - rule, err := m.AddPeerFiltering(ip, proto, nil, port, direction, action, "", comment) + rule2, err := m.AddPeerFiltering(ip, proto, nil, port, action, "", comment) if err != nil { t.Errorf("failed to add filtering: %v", err) return } - ip = net.ParseIP("192.168.1.1") - proto = fw.ProtocolTCP - port = &fw.Port{Values: []int{80}} - direction = fw.RuleDirectionIN - action = fw.ActionDrop - comment = "Test rule 2" - - rule2, err := m.AddPeerFiltering(ip, proto, nil, port, direction, action, "", comment) - if err != nil { - t.Errorf("failed to add filtering: %v", err) - return - } - - for _, r := range rule { - err = m.DeletePeerRule(r) - if err != nil { - t.Errorf("failed to delete rule: %v", err) - return - } - } - for _, r := range rule2 { if _, ok := m.incomingRules[ip.String()][r.GetRuleID()]; !ok { t.Errorf("rule2 is not in the incomingRules") @@ -225,10 +202,6 @@ func TestAddUDPPacketHook(t *testing.T) { t.Errorf("expected protoLayer %s, got %s", layers.LayerTypeUDP, addedRule.protoLayer) return } - if tt.expDir != addedRule.direction { - t.Errorf("expected direction %d, got %d", tt.expDir, addedRule.direction) - return - } if addedRule.udpHook == nil { t.Errorf("expected udpHook to be set") return @@ -251,11 +224,10 @@ func TestManagerReset(t *testing.T) { ip := net.ParseIP("192.168.1.1") proto := fw.ProtocolTCP port := &fw.Port{Values: []int{80}} - direction := fw.RuleDirectionOUT action := fw.ActionDrop comment := "Test rule" - _, err = m.AddPeerFiltering(ip, proto, nil, port, direction, action, "", comment) + _, err = m.AddPeerFiltering(ip, proto, nil, port, action, "", comment) if err != nil { t.Errorf("failed to add filtering: %v", err) return @@ -289,11 +261,10 @@ func TestNotMatchByIP(t *testing.T) { ip := net.ParseIP("0.0.0.0") proto := fw.ProtocolUDP - direction := fw.RuleDirectionOUT action := fw.ActionAccept comment := "Test rule" - _, err = m.AddPeerFiltering(ip, proto, nil, nil, direction, action, "", comment) + _, err = m.AddPeerFiltering(ip, proto, nil, nil, action, "", comment) if err != nil { t.Errorf("failed to add filtering: %v", err) return @@ -327,7 +298,7 @@ func TestNotMatchByIP(t *testing.T) { return } - if m.dropFilter(buf.Bytes(), m.outgoingRules) { + if m.dropFilter(buf.Bytes(), m.incomingRules) { t.Errorf("expected packet to be accepted") return } @@ -493,11 +464,7 @@ func TestUSPFilterCreatePerformance(t *testing.T) { start := time.Now() for i := 0; i < testMax; i++ { port := &fw.Port{Values: []int{1000 + i}} - if i%2 == 0 { - _, err = manager.AddPeerFiltering(ip, "tcp", nil, port, fw.RuleDirectionOUT, fw.ActionAccept, "", "accept HTTP traffic") - } else { - _, err = manager.AddPeerFiltering(ip, "tcp", nil, port, fw.RuleDirectionIN, fw.ActionAccept, "", "accept HTTP traffic") - } + _, err = manager.AddPeerFiltering(ip, "tcp", nil, port, fw.ActionAccept, "", "accept HTTP traffic") require.NoError(t, err, "failed to add rule") } diff --git a/client/internal/acl/manager.go b/client/internal/acl/manager.go index 5bb0905d2..0ade5d7ce 100644 --- a/client/internal/acl/manager.go +++ b/client/internal/acl/manager.go @@ -151,7 +151,7 @@ func (d *DefaultManager) applyPeerACLs(networkMap *mgmProto.NetworkMap) { d.rollBack(newRulePairs) break } - if len(rules) > 0 { + if len(rulePair) > 0 { d.peerRulesPairs[pairID] = rulePair newRulePairs[pairID] = rulePair } @@ -288,6 +288,8 @@ func (d *DefaultManager) protoRuleToFirewallRule( case mgmProto.RuleDirection_IN: rules, err = d.addInRules(ip, protocol, port, action, ipsetName, "") case mgmProto.RuleDirection_OUT: + // TODO: Remove this soon. Outbound rules are obsolete. + // We only maintain this for return traffic (inbound dir) which is now handled by the stateful firewall already rules, err = d.addOutRules(ip, protocol, port, action, ipsetName, "") default: return "", nil, fmt.Errorf("invalid direction, skipping firewall rule") @@ -308,25 +310,12 @@ func (d *DefaultManager) addInRules( ipsetName string, comment string, ) ([]firewall.Rule, error) { - var rules []firewall.Rule - rule, err := d.firewall.AddPeerFiltering( - ip, protocol, nil, port, firewall.RuleDirectionIN, action, ipsetName, comment) + rule, err := d.firewall.AddPeerFiltering(ip, protocol, nil, port, action, ipsetName, comment) if err != nil { - return nil, fmt.Errorf("failed to add firewall rule: %v", err) - } - rules = append(rules, rule...) - - if shouldSkipInvertedRule(protocol, port) { - return rules, nil + return nil, fmt.Errorf("add firewall rule: %w", err) } - rule, err = d.firewall.AddPeerFiltering( - ip, protocol, port, nil, firewall.RuleDirectionOUT, action, ipsetName, comment) - if err != nil { - return nil, fmt.Errorf("failed to add firewall rule: %v", err) - } - - return append(rules, rule...), nil + return rule, nil } func (d *DefaultManager) addOutRules( @@ -337,25 +326,16 @@ func (d *DefaultManager) addOutRules( ipsetName string, comment string, ) ([]firewall.Rule, error) { - var rules []firewall.Rule - rule, err := d.firewall.AddPeerFiltering( - ip, protocol, nil, port, firewall.RuleDirectionOUT, action, ipsetName, comment) - if err != nil { - return nil, fmt.Errorf("failed to add firewall rule: %v", err) - } - rules = append(rules, rule...) - if shouldSkipInvertedRule(protocol, port) { - return rules, nil + return nil, nil } - rule, err = d.firewall.AddPeerFiltering( - ip, protocol, port, nil, firewall.RuleDirectionIN, action, ipsetName, comment) + rule, err := d.firewall.AddPeerFiltering(ip, protocol, port, nil, action, ipsetName, comment) if err != nil { - return nil, fmt.Errorf("failed to add firewall rule: %v", err) + return nil, fmt.Errorf("add firewall rule: %w", err) } - return append(rules, rule...), nil + return rule, nil } // getPeerRuleID() returns unique ID for the rule based on its parameters. diff --git a/client/internal/acl/manager_test.go b/client/internal/acl/manager_test.go index 9a766021a..6049b4f48 100644 --- a/client/internal/acl/manager_test.go +++ b/client/internal/acl/manager_test.go @@ -119,8 +119,8 @@ func TestDefaultManager(t *testing.T) { networkMap.FirewallRulesIsEmpty = false acl.ApplyFiltering(networkMap) - if len(acl.peerRulesPairs) != 2 { - t.Errorf("rules should contain 2 rules if FirewallRulesIsEmpty is not set, got: %v", len(acl.peerRulesPairs)) + if len(acl.peerRulesPairs) != 1 { + t.Errorf("rules should contain 1 rules if FirewallRulesIsEmpty is not set, got: %v", len(acl.peerRulesPairs)) return } }) @@ -356,8 +356,8 @@ func TestDefaultManagerEnableSSHRules(t *testing.T) { acl.ApplyFiltering(networkMap) - if len(acl.peerRulesPairs) != 4 { - t.Errorf("expect 4 rules (last must be SSH), got: %d", len(acl.peerRulesPairs)) + if len(acl.peerRulesPairs) != 3 { + t.Errorf("expect 3 rules (last must be SSH), got: %d", len(acl.peerRulesPairs)) return } } diff --git a/client/internal/dnsfwd/manager.go b/client/internal/dnsfwd/manager.go index e6dfd278e..968f2d398 100644 --- a/client/internal/dnsfwd/manager.go +++ b/client/internal/dnsfwd/manager.go @@ -88,7 +88,7 @@ func (h *Manager) allowDNSFirewall() error { return nil } - dnsRules, err := h.firewall.AddPeerFiltering(net.IP{0, 0, 0, 0}, firewall.ProtocolUDP, nil, dport, firewall.RuleDirectionIN, firewall.ActionAccept, "", "") + dnsRules, err := h.firewall.AddPeerFiltering(net.IP{0, 0, 0, 0}, firewall.ProtocolUDP, nil, dport, firewall.ActionAccept, "", "") if err != nil { log.Errorf("failed to add allow DNS router rules, err: %v", err) return err diff --git a/client/internal/engine.go b/client/internal/engine.go index a5247bc27..1042f003d 100644 --- a/client/internal/engine.go +++ b/client/internal/engine.go @@ -495,7 +495,6 @@ func (e *Engine) initFirewall() error { manager.ProtocolUDP, nil, &port, - manager.RuleDirectionIN, manager.ActionAccept, "", "", From 78795a4a734cf9883eae766f34e78b07227af91f Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Wed, 15 Jan 2025 17:39:47 +0100 Subject: [PATCH 11/92] [client] Add block lan access flag for routers (#3171) --- client/cmd/root.go | 2 + client/cmd/up.go | 9 + client/internal/config.go | 14 + client/internal/connect.go | 2 + client/internal/engine.go | 79 +++++ client/proto/daemon.pb.go | 698 +++++++++++++++++++------------------ client/proto/daemon.proto | 2 + client/server/debug.go | 7 + client/server/server.go | 5 + 9 files changed, 475 insertions(+), 343 deletions(-) diff --git a/client/cmd/root.go b/client/cmd/root.go index 0305bacc8..b25c2750c 100644 --- a/client/cmd/root.go +++ b/client/cmd/root.go @@ -38,6 +38,7 @@ const ( extraIFaceBlackListFlag = "extra-iface-blacklist" dnsRouteIntervalFlag = "dns-router-interval" systemInfoFlag = "system-info" + blockLANAccessFlag = "block-lan-access" ) var ( @@ -73,6 +74,7 @@ var ( anonymizeFlag bool debugSystemInfoFlag bool dnsRouteInterval time.Duration + blockLANAccess bool rootCmd = &cobra.Command{ Use: "netbird", diff --git a/client/cmd/up.go b/client/cmd/up.go index cd5521371..9f8f738bc 100644 --- a/client/cmd/up.go +++ b/client/cmd/up.go @@ -48,6 +48,7 @@ func init() { ) upCmd.PersistentFlags().StringSliceVar(&extraIFaceBlackList, extraIFaceBlackListFlag, nil, "Extra list of default interfaces to ignore for listening") upCmd.PersistentFlags().DurationVar(&dnsRouteInterval, dnsRouteIntervalFlag, time.Minute, "DNS route update interval") + upCmd.PersistentFlags().BoolVar(&blockLANAccess, blockLANAccessFlag, false, "Block access to local networks (LAN) when using this peer as a router or exit node") } func upFunc(cmd *cobra.Command, args []string) error { @@ -160,6 +161,10 @@ func runInForegroundMode(ctx context.Context, cmd *cobra.Command) error { ic.DisableFirewall = &disableFirewall } + if cmd.Flag(blockLANAccessFlag).Changed { + ic.BlockLANAccess = &blockLANAccess + } + providedSetupKey, err := getSetupKey() if err != nil { return err @@ -290,6 +295,10 @@ func runInDaemonMode(ctx context.Context, cmd *cobra.Command) error { loginRequest.DisableFirewall = &disableFirewall } + if cmd.Flag(blockLANAccessFlag).Changed { + loginRequest.BlockLanAccess = &blockLANAccess + } + var loginErr error var loginResp *proto.LoginResponse diff --git a/client/internal/config.go b/client/internal/config.go index 594bdc570..3196c4e04 100644 --- a/client/internal/config.go +++ b/client/internal/config.go @@ -66,6 +66,8 @@ type ConfigInput struct { DisableServerRoutes *bool DisableDNS *bool DisableFirewall *bool + + BlockLANAccess *bool } // Config Configuration type @@ -89,6 +91,8 @@ type Config struct { DisableDNS bool DisableFirewall bool + BlockLANAccess bool + // SSHKey is a private SSH key in a PEM format SSHKey string @@ -455,6 +459,16 @@ func (config *Config) apply(input ConfigInput) (updated bool, err error) { updated = true } + if input.BlockLANAccess != nil && *input.BlockLANAccess != config.BlockLANAccess { + if *input.BlockLANAccess { + log.Infof("blocking LAN access") + } else { + log.Infof("allowing LAN access") + } + config.BlockLANAccess = *input.BlockLANAccess + updated = true + } + if input.ClientCertKeyPath != "" { config.ClientCertKeyPath = input.ClientCertKeyPath updated = true diff --git a/client/internal/connect.go b/client/internal/connect.go index afd1f4454..4dde7fa41 100644 --- a/client/internal/connect.go +++ b/client/internal/connect.go @@ -420,6 +420,8 @@ func createEngineConfig(key wgtypes.Key, config *Config, peerConfig *mgmProto.Pe DisableServerRoutes: config.DisableServerRoutes, DisableDNS: config.DisableDNS, DisableFirewall: config.DisableFirewall, + + BlockLANAccess: config.BlockLANAccess, } if config.PreSharedKey != "" { diff --git a/client/internal/engine.go b/client/internal/engine.go index 1042f003d..12cc191b9 100644 --- a/client/internal/engine.go +++ b/client/internal/engine.go @@ -16,12 +16,14 @@ import ( "sync/atomic" "time" + "github.com/hashicorp/go-multierror" "github.com/pion/ice/v3" "github.com/pion/stun/v2" log "github.com/sirupsen/logrus" "golang.zx2c4.com/wireguard/wgctrl/wgtypes" "google.golang.org/protobuf/proto" + nberrors "github.com/netbirdio/netbird/client/errors" "github.com/netbirdio/netbird/client/firewall" "github.com/netbirdio/netbird/client/firewall/manager" "github.com/netbirdio/netbird/client/iface" @@ -114,6 +116,8 @@ type EngineConfig struct { DisableServerRoutes bool DisableDNS bool DisableFirewall bool + + BlockLANAccess bool } // Engine is a mechanism responsible for reacting on Signal and Management stream events and managing connections to the remote peers. @@ -482,6 +486,10 @@ func (e *Engine) initFirewall() error { } } + if e.config.BlockLANAccess { + e.blockLanAccess() + } + if e.rpManager == nil || !e.config.RosenpassEnabled { return nil } @@ -508,6 +516,35 @@ func (e *Engine) initFirewall() error { return nil } +func (e *Engine) blockLanAccess() { + var merr *multierror.Error + + // TODO: keep this updated + toBlock, err := getInterfacePrefixes() + if err != nil { + merr = multierror.Append(merr, fmt.Errorf("get local addresses: %w", err)) + } + + log.Infof("blocking route LAN access for networks: %v", toBlock) + v4 := netip.PrefixFrom(netip.IPv4Unspecified(), 0) + for _, network := range toBlock { + if _, err := e.firewall.AddRouteFiltering( + []netip.Prefix{v4}, + network, + manager.ProtocolALL, + nil, + nil, + manager.ActionDrop, + ); err != nil { + merr = multierror.Append(merr, fmt.Errorf("add fw rule for network %s: %w", network, err)) + } + } + + if merr != nil { + log.Warnf("encountered errors blocking IPs to block LAN access: %v", nberrors.FormatErrorOrNil(merr)) + } +} + // modifyPeers updates peers that have been modified (e.g. IP address has been changed). // It closes the existing connection, removes it from the peerConns map, and creates a new one. func (e *Engine) modifyPeers(peersUpdate []*mgmProto.RemotePeerConfig) error { @@ -1689,3 +1726,45 @@ func isChecksEqual(checks []*mgmProto.Checks, oChecks []*mgmProto.Checks) bool { return slices.Equal(checks.Files, oChecks.Files) }) } + +func getInterfacePrefixes() ([]netip.Prefix, error) { + ifaces, err := net.Interfaces() + if err != nil { + return nil, fmt.Errorf("get interfaces: %w", err) + } + + var prefixes []netip.Prefix + var merr *multierror.Error + + for _, iface := range ifaces { + addrs, err := iface.Addrs() + if err != nil { + merr = multierror.Append(merr, fmt.Errorf("get addresses for interface %s: %w", iface.Name, err)) + continue + } + for _, addr := range addrs { + ipNet, ok := addr.(*net.IPNet) + if !ok { + merr = multierror.Append(merr, fmt.Errorf("cast address to IPNet: %v", addr)) + continue + } + addr, ok := netip.AddrFromSlice(ipNet.IP) + if !ok { + merr = multierror.Append(merr, fmt.Errorf("cast IPNet to netip.Addr: %v", ipNet.IP)) + continue + } + ones, _ := ipNet.Mask.Size() + prefix := netip.PrefixFrom(addr.Unmap(), ones).Masked() + ip := prefix.Addr() + + // TODO: add IPv6 + if !ip.Is4() || ip.IsLoopback() || ip.IsMulticast() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() { + continue + } + + prefixes = append(prefixes, prefix) + } + } + + return prefixes, nberrors.FormatErrorOrNil(merr) +} diff --git a/client/proto/daemon.pb.go b/client/proto/daemon.pb.go index 659277570..413f94a54 100644 --- a/client/proto/daemon.pb.go +++ b/client/proto/daemon.pb.go @@ -126,6 +126,7 @@ type LoginRequest struct { DisableServerRoutes *bool `protobuf:"varint,21,opt,name=disable_server_routes,json=disableServerRoutes,proto3,oneof" json:"disable_server_routes,omitempty"` DisableDns *bool `protobuf:"varint,22,opt,name=disable_dns,json=disableDns,proto3,oneof" json:"disable_dns,omitempty"` DisableFirewall *bool `protobuf:"varint,23,opt,name=disable_firewall,json=disableFirewall,proto3,oneof" json:"disable_firewall,omitempty"` + BlockLanAccess *bool `protobuf:"varint,24,opt,name=block_lan_access,json=blockLanAccess,proto3,oneof" json:"block_lan_access,omitempty"` } func (x *LoginRequest) Reset() { @@ -322,6 +323,13 @@ func (x *LoginRequest) GetDisableFirewall() bool { return false } +func (x *LoginRequest) GetBlockLanAccess() bool { + if x != nil && x.BlockLanAccess != nil { + return *x.BlockLanAccess + } + return false +} + type LoginResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -2573,7 +2581,7 @@ var file_daemon_proto_rawDesc = []byte{ 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x64, 0x75, 0x72, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xd1, 0x0a, 0x0a, 0x0c, 0x4c, 0x6f, + 0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x95, 0x0b, 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, 0x26, 0x0a, 0x0c, 0x70, 0x72, 0x65, 0x53, 0x68, 0x61, @@ -2641,354 +2649,358 @@ var file_daemon_proto_rawDesc = []byte{ 0x44, 0x6e, 0x73, 0x88, 0x01, 0x01, 0x12, 0x2e, 0x0a, 0x10, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x66, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x18, 0x17, 0x20, 0x01, 0x28, 0x08, 0x48, 0x0c, 0x52, 0x0f, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x46, 0x69, 0x72, 0x65, 0x77, - 0x61, 0x6c, 0x6c, 0x88, 0x01, 0x01, 0x42, 0x13, 0x0a, 0x11, 0x5f, 0x72, 0x6f, 0x73, 0x65, 0x6e, - 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x42, 0x10, 0x0a, 0x0e, 0x5f, - 0x69, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x42, 0x10, 0x0a, - 0x0e, 0x5f, 0x77, 0x69, 0x72, 0x65, 0x67, 0x75, 0x61, 0x72, 0x64, 0x50, 0x6f, 0x72, 0x74, 0x42, - 0x17, 0x0a, 0x15, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x50, 0x72, 0x65, 0x53, - 0x68, 0x61, 0x72, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x42, 0x15, 0x0a, 0x13, 0x5f, 0x64, 0x69, 0x73, - 0x61, 0x62, 0x6c, 0x65, 0x41, 0x75, 0x74, 0x6f, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x42, - 0x13, 0x0a, 0x11, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x53, 0x53, 0x48, 0x41, 0x6c, 0x6c, - 0x6f, 0x77, 0x65, 0x64, 0x42, 0x16, 0x0a, 0x14, 0x5f, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, - 0x73, 0x73, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x76, 0x65, 0x42, 0x11, 0x0a, 0x0f, - 0x5f, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x42, - 0x13, 0x0a, 0x11, 0x5f, 0x64, 0x6e, 0x73, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x49, 0x6e, 0x74, 0x65, - 0x72, 0x76, 0x61, 0x6c, 0x42, 0x18, 0x0a, 0x16, 0x5f, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, - 0x5f, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x42, 0x18, - 0x0a, 0x16, 0x5f, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, - 0x72, 0x5f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x42, 0x0e, 0x0a, 0x0c, 0x5f, 0x64, 0x69, 0x73, - 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x64, 0x6e, 0x73, 0x42, 0x13, 0x0a, 0x11, 0x5f, 0x64, 0x69, 0x73, - 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x66, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x22, 0xb5, 0x01, - 0x0a, 0x0d, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x24, 0x0a, 0x0d, 0x6e, 0x65, 0x65, 0x64, 0x73, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x6e, 0x65, 0x65, 0x64, 0x73, 0x53, 0x53, 0x4f, - 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x43, 0x6f, 0x64, - 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x43, 0x6f, 0x64, - 0x65, 0x12, 0x28, 0x0a, 0x0f, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x55, 0x52, 0x49, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x76, 0x65, 0x72, 0x69, - 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x55, 0x52, 0x49, 0x12, 0x38, 0x0a, 0x17, 0x76, - 0x65, 0x72, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x55, 0x52, 0x49, 0x43, 0x6f, - 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x17, 0x76, 0x65, - 0x72, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x55, 0x52, 0x49, 0x43, 0x6f, 0x6d, - 0x70, 0x6c, 0x65, 0x74, 0x65, 0x22, 0x4d, 0x0a, 0x13, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, - 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, - 0x75, 0x73, 0x65, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, - 0x75, 0x73, 0x65, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x68, 0x6f, 0x73, 0x74, - 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x68, 0x6f, 0x73, 0x74, - 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x16, 0x0a, 0x14, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 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, 0x3d, 0x0a, 0x0d, 0x53, 0x74, 0x61, 0x74, 0x75, - 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2c, 0x0a, 0x11, 0x67, 0x65, 0x74, 0x46, - 0x75, 0x6c, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x08, 0x52, 0x11, 0x67, 0x65, 0x74, 0x46, 0x75, 0x6c, 0x6c, 0x50, 0x65, 0x65, 0x72, - 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x82, 0x01, 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, 0x12, 0x32, 0x0a, 0x0a, 0x66, 0x75, 0x6c, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x46, - 0x75, 0x6c, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x0a, 0x66, 0x75, 0x6c, 0x6c, 0x53, - 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x24, 0x0a, 0x0d, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x56, - 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x64, 0x61, - 0x65, 0x6d, 0x6f, 0x6e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 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, 0xb9, - 0x03, 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, 0x12, 0x24, 0x0a, 0x0d, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, - 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x69, 0x6e, 0x74, - 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x24, 0x0a, 0x0d, 0x77, 0x69, - 0x72, 0x65, 0x67, 0x75, 0x61, 0x72, 0x64, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, - 0x03, 0x52, 0x0d, 0x77, 0x69, 0x72, 0x65, 0x67, 0x75, 0x61, 0x72, 0x64, 0x50, 0x6f, 0x72, 0x74, - 0x12, 0x2e, 0x0a, 0x12, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x41, 0x75, 0x74, 0x6f, 0x43, - 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x12, 0x64, 0x69, - 0x73, 0x61, 0x62, 0x6c, 0x65, 0x41, 0x75, 0x74, 0x6f, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, - 0x12, 0x2a, 0x0a, 0x10, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x53, 0x53, 0x48, 0x41, 0x6c, 0x6c, - 0x6f, 0x77, 0x65, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x73, 0x65, 0x72, 0x76, - 0x65, 0x72, 0x53, 0x53, 0x48, 0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x12, 0x2a, 0x0a, 0x10, - 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, - 0x18, 0x0b, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, - 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x30, 0x0a, 0x13, 0x72, 0x6f, 0x73, 0x65, - 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x76, 0x65, 0x18, - 0x0c, 0x20, 0x01, 0x28, 0x08, 0x52, 0x13, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, - 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x76, 0x65, 0x22, 0xde, 0x05, 0x0a, 0x09, 0x50, - 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x50, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x50, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x75, 0x62, 0x4b, - 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, - 0x12, 0x1e, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, - 0x12, 0x46, 0x0a, 0x10, 0x63, 0x6f, 0x6e, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x55, 0x70, - 0x64, 0x61, 0x74, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, - 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, - 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x10, 0x63, 0x6f, 0x6e, 0x6e, 0x53, 0x74, 0x61, 0x74, - 0x75, 0x73, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x72, 0x65, 0x6c, 0x61, - 0x79, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x72, 0x65, 0x6c, 0x61, 0x79, - 0x65, 0x64, 0x12, 0x34, 0x0a, 0x15, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x49, 0x63, 0x65, 0x43, 0x61, - 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x54, 0x79, 0x70, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x15, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, - 0x64, 0x61, 0x74, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x36, 0x0a, 0x16, 0x72, 0x65, 0x6d, 0x6f, - 0x74, 0x65, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x54, 0x79, - 0x70, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x16, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, - 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x54, 0x79, 0x70, 0x65, - 0x12, 0x12, 0x0a, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, - 0x66, 0x71, 0x64, 0x6e, 0x12, 0x3c, 0x0a, 0x19, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x49, 0x63, 0x65, - 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, - 0x74, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x19, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x49, 0x63, - 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, - 0x6e, 0x74, 0x12, 0x3e, 0x0a, 0x1a, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x49, 0x63, 0x65, 0x43, - 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, - 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x1a, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x49, 0x63, - 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, - 0x6e, 0x74, 0x12, 0x52, 0x0a, 0x16, 0x6c, 0x61, 0x73, 0x74, 0x57, 0x69, 0x72, 0x65, 0x67, 0x75, - 0x61, 0x72, 0x64, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x18, 0x0c, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x16, - 0x6c, 0x61, 0x73, 0x74, 0x57, 0x69, 0x72, 0x65, 0x67, 0x75, 0x61, 0x72, 0x64, 0x48, 0x61, 0x6e, - 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x62, 0x79, 0x74, 0x65, 0x73, 0x52, - 0x78, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x62, 0x79, 0x74, 0x65, 0x73, 0x52, 0x78, - 0x12, 0x18, 0x0a, 0x07, 0x62, 0x79, 0x74, 0x65, 0x73, 0x54, 0x78, 0x18, 0x0e, 0x20, 0x01, 0x28, - 0x03, 0x52, 0x07, 0x62, 0x79, 0x74, 0x65, 0x73, 0x54, 0x78, 0x12, 0x2a, 0x0a, 0x10, 0x72, 0x6f, - 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x0f, - 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, - 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, - 0x6b, 0x73, 0x18, 0x10, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, - 0x6b, 0x73, 0x12, 0x33, 0x0a, 0x07, 0x6c, 0x61, 0x74, 0x65, 0x6e, 0x63, 0x79, 0x18, 0x11, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x07, - 0x6c, 0x61, 0x74, 0x65, 0x6e, 0x63, 0x79, 0x12, 0x22, 0x0a, 0x0c, 0x72, 0x65, 0x6c, 0x61, 0x79, - 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x12, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x72, - 0x65, 0x6c, 0x61, 0x79, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x22, 0xf0, 0x01, 0x0a, 0x0e, - 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x0e, - 0x0a, 0x02, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x50, 0x12, 0x16, - 0x0a, 0x06, 0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, - 0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x12, 0x28, 0x0a, 0x0f, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, - 0x49, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, - 0x0f, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, - 0x12, 0x12, 0x0a, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, - 0x66, 0x71, 0x64, 0x6e, 0x12, 0x2a, 0x0a, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, - 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, - 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, - 0x12, 0x30, 0x0a, 0x13, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, 0x65, 0x72, - 0x6d, 0x69, 0x73, 0x73, 0x69, 0x76, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x13, 0x72, - 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, - 0x76, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x18, 0x07, - 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x22, 0x53, - 0x0a, 0x0b, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x10, 0x0a, - 0x03, 0x55, 0x52, 0x4c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x55, 0x52, 0x4c, 0x12, - 0x1c, 0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x08, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65, 0x64, 0x12, 0x14, 0x0a, - 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, - 0x72, 0x6f, 0x72, 0x22, 0x57, 0x0a, 0x0f, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, - 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x55, 0x52, 0x4c, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x03, 0x55, 0x52, 0x4c, 0x12, 0x1c, 0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x6e, - 0x65, 0x63, 0x74, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x63, 0x6f, 0x6e, - 0x6e, 0x65, 0x63, 0x74, 0x65, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, - 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, 0x52, 0x0a, 0x0a, - 0x52, 0x65, 0x6c, 0x61, 0x79, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x55, 0x52, - 0x49, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x55, 0x52, 0x49, 0x12, 0x1c, 0x0a, 0x09, - 0x61, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, - 0x09, 0x61, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, + 0x61, 0x6c, 0x6c, 0x88, 0x01, 0x01, 0x12, 0x2d, 0x0a, 0x10, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x5f, + 0x6c, 0x61, 0x6e, 0x5f, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x18, 0x18, 0x20, 0x01, 0x28, 0x08, + 0x48, 0x0d, 0x52, 0x0e, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x4c, 0x61, 0x6e, 0x41, 0x63, 0x63, 0x65, + 0x73, 0x73, 0x88, 0x01, 0x01, 0x42, 0x13, 0x0a, 0x11, 0x5f, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, + 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x42, 0x10, 0x0a, 0x0e, 0x5f, 0x69, + 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x42, 0x10, 0x0a, 0x0e, + 0x5f, 0x77, 0x69, 0x72, 0x65, 0x67, 0x75, 0x61, 0x72, 0x64, 0x50, 0x6f, 0x72, 0x74, 0x42, 0x17, + 0x0a, 0x15, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x50, 0x72, 0x65, 0x53, 0x68, + 0x61, 0x72, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x42, 0x15, 0x0a, 0x13, 0x5f, 0x64, 0x69, 0x73, 0x61, + 0x62, 0x6c, 0x65, 0x41, 0x75, 0x74, 0x6f, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x42, 0x13, + 0x0a, 0x11, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x53, 0x53, 0x48, 0x41, 0x6c, 0x6c, 0x6f, + 0x77, 0x65, 0x64, 0x42, 0x16, 0x0a, 0x14, 0x5f, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, + 0x73, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x76, 0x65, 0x42, 0x11, 0x0a, 0x0f, 0x5f, + 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x42, 0x13, + 0x0a, 0x11, 0x5f, 0x64, 0x6e, 0x73, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x49, 0x6e, 0x74, 0x65, 0x72, + 0x76, 0x61, 0x6c, 0x42, 0x18, 0x0a, 0x16, 0x5f, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, + 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x42, 0x18, 0x0a, + 0x16, 0x5f, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, + 0x5f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x42, 0x0e, 0x0a, 0x0c, 0x5f, 0x64, 0x69, 0x73, 0x61, + 0x62, 0x6c, 0x65, 0x5f, 0x64, 0x6e, 0x73, 0x42, 0x13, 0x0a, 0x11, 0x5f, 0x64, 0x69, 0x73, 0x61, + 0x62, 0x6c, 0x65, 0x5f, 0x66, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x42, 0x13, 0x0a, 0x11, + 0x5f, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x5f, 0x6c, 0x61, 0x6e, 0x5f, 0x61, 0x63, 0x63, 0x65, 0x73, + 0x73, 0x22, 0xb5, 0x01, 0x0a, 0x0d, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x24, 0x0a, 0x0d, 0x6e, 0x65, 0x65, 0x64, 0x73, 0x53, 0x53, 0x4f, 0x4c, + 0x6f, 0x67, 0x69, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x6e, 0x65, 0x65, 0x64, + 0x73, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, + 0x72, 0x43, 0x6f, 0x64, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, + 0x72, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x28, 0x0a, 0x0f, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x63, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x55, 0x52, 0x49, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, + 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x55, 0x52, 0x49, 0x12, + 0x38, 0x0a, 0x17, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x55, + 0x52, 0x49, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x17, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x55, 0x52, + 0x49, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x22, 0x4d, 0x0a, 0x13, 0x57, 0x61, 0x69, + 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x1a, 0x0a, 0x08, + 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, + 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x16, 0x0a, 0x14, 0x57, 0x61, 0x69, 0x74, + 0x53, 0x53, 0x4f, 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, 0x3d, 0x0a, 0x0d, 0x53, + 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2c, 0x0a, 0x11, + 0x67, 0x65, 0x74, 0x46, 0x75, 0x6c, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x75, + 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x11, 0x67, 0x65, 0x74, 0x46, 0x75, 0x6c, 0x6c, + 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x82, 0x01, 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, 0x12, 0x32, 0x0a, 0x0a, 0x66, 0x75, 0x6c, 0x6c, 0x53, 0x74, 0x61, + 0x74, 0x75, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x64, 0x61, 0x65, 0x6d, + 0x6f, 0x6e, 0x2e, 0x46, 0x75, 0x6c, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x0a, 0x66, + 0x75, 0x6c, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x24, 0x0a, 0x0d, 0x64, 0x61, 0x65, + 0x6d, 0x6f, 0x6e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0d, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 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, 0xb9, 0x03, 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, 0x12, 0x24, 0x0a, 0x0d, 0x69, 0x6e, 0x74, 0x65, + 0x72, 0x66, 0x61, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0d, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x24, + 0x0a, 0x0d, 0x77, 0x69, 0x72, 0x65, 0x67, 0x75, 0x61, 0x72, 0x64, 0x50, 0x6f, 0x72, 0x74, 0x18, + 0x07, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, 0x77, 0x69, 0x72, 0x65, 0x67, 0x75, 0x61, 0x72, 0x64, + 0x50, 0x6f, 0x72, 0x74, 0x12, 0x2e, 0x0a, 0x12, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x41, + 0x75, 0x74, 0x6f, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, + 0x52, 0x12, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x41, 0x75, 0x74, 0x6f, 0x43, 0x6f, 0x6e, + 0x6e, 0x65, 0x63, 0x74, 0x12, 0x2a, 0x0a, 0x10, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x53, 0x53, + 0x48, 0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, + 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x53, 0x53, 0x48, 0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, + 0x12, 0x2a, 0x0a, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, + 0x62, 0x6c, 0x65, 0x64, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x72, 0x6f, 0x73, 0x65, + 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x30, 0x0a, 0x13, + 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, + 0x69, 0x76, 0x65, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x08, 0x52, 0x13, 0x72, 0x6f, 0x73, 0x65, 0x6e, + 0x70, 0x61, 0x73, 0x73, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x76, 0x65, 0x22, 0xde, + 0x05, 0x0a, 0x09, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x0e, 0x0a, 0x02, + 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x50, 0x12, 0x16, 0x0a, 0x06, + 0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x75, + 0x62, 0x4b, 0x65, 0x79, 0x12, 0x1e, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x6e, 0x53, 0x74, 0x61, 0x74, + 0x75, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x6e, 0x53, 0x74, + 0x61, 0x74, 0x75, 0x73, 0x12, 0x46, 0x0a, 0x10, 0x63, 0x6f, 0x6e, 0x6e, 0x53, 0x74, 0x61, 0x74, + 0x75, 0x73, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, + 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, + 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x10, 0x63, 0x6f, 0x6e, 0x6e, + 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x18, 0x0a, 0x07, + 0x72, 0x65, 0x6c, 0x61, 0x79, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x72, + 0x65, 0x6c, 0x61, 0x79, 0x65, 0x64, 0x12, 0x34, 0x0a, 0x15, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x49, + 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x54, 0x79, 0x70, 0x65, 0x18, + 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x15, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x49, 0x63, 0x65, 0x43, + 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x36, 0x0a, 0x16, + 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, + 0x74, 0x65, 0x54, 0x79, 0x70, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x16, 0x72, 0x65, + 0x6d, 0x6f, 0x74, 0x65, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, + 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x18, 0x09, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x12, 0x3c, 0x0a, 0x19, 0x6c, 0x6f, 0x63, 0x61, + 0x6c, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x64, + 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x19, 0x6c, 0x6f, 0x63, + 0x61, 0x6c, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, + 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x3e, 0x0a, 0x1a, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, + 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x64, 0x70, + 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x1a, 0x72, 0x65, 0x6d, 0x6f, + 0x74, 0x65, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, + 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x52, 0x0a, 0x16, 0x6c, 0x61, 0x73, 0x74, 0x57, 0x69, + 0x72, 0x65, 0x67, 0x75, 0x61, 0x72, 0x64, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, + 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, + 0x6d, 0x70, 0x52, 0x16, 0x6c, 0x61, 0x73, 0x74, 0x57, 0x69, 0x72, 0x65, 0x67, 0x75, 0x61, 0x72, + 0x64, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x62, 0x79, + 0x74, 0x65, 0x73, 0x52, 0x78, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x62, 0x79, 0x74, + 0x65, 0x73, 0x52, 0x78, 0x12, 0x18, 0x0a, 0x07, 0x62, 0x79, 0x74, 0x65, 0x73, 0x54, 0x78, 0x18, + 0x0e, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x62, 0x79, 0x74, 0x65, 0x73, 0x54, 0x78, 0x12, 0x2a, + 0x0a, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, + 0x65, 0x64, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, + 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x6e, 0x65, + 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x18, 0x10, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x6e, 0x65, + 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x12, 0x33, 0x0a, 0x07, 0x6c, 0x61, 0x74, 0x65, 0x6e, 0x63, + 0x79, 0x18, 0x11, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x52, 0x07, 0x6c, 0x61, 0x74, 0x65, 0x6e, 0x63, 0x79, 0x12, 0x22, 0x0a, 0x0c, 0x72, + 0x65, 0x6c, 0x61, 0x79, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x12, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0c, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x22, + 0xf0, 0x01, 0x0a, 0x0e, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, + 0x74, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, + 0x49, 0x50, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x06, 0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x12, 0x28, 0x0a, 0x0f, 0x6b, 0x65, + 0x72, 0x6e, 0x65, 0x6c, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x08, 0x52, 0x0f, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x49, 0x6e, 0x74, 0x65, 0x72, + 0x66, 0x61, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x18, 0x04, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x12, 0x2a, 0x0a, 0x10, 0x72, 0x6f, 0x73, 0x65, + 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, + 0x28, 0x08, 0x52, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, + 0x62, 0x6c, 0x65, 0x64, 0x12, 0x30, 0x0a, 0x13, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, + 0x73, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x76, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, + 0x08, 0x52, 0x13, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, 0x65, 0x72, 0x6d, + 0x69, 0x73, 0x73, 0x69, 0x76, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, + 0x6b, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, + 0x6b, 0x73, 0x22, 0x53, 0x0a, 0x0b, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, + 0x65, 0x12, 0x10, 0x0a, 0x03, 0x55, 0x52, 0x4c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, + 0x55, 0x52, 0x4c, 0x12, 0x1c, 0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65, 0x64, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65, + 0x64, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, 0x57, 0x0a, 0x0f, 0x4d, 0x61, 0x6e, 0x61, 0x67, + 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x55, 0x52, + 0x4c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x55, 0x52, 0x4c, 0x12, 0x1c, 0x0a, 0x09, + 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, + 0x09, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, - 0x22, 0x72, 0x0a, 0x0c, 0x4e, 0x53, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x53, 0x74, 0x61, 0x74, 0x65, - 0x12, 0x18, 0x0a, 0x07, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, - 0x09, 0x52, 0x07, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x64, 0x6f, - 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x64, 0x6f, 0x6d, - 0x61, 0x69, 0x6e, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, - 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x14, - 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, - 0x72, 0x72, 0x6f, 0x72, 0x22, 0xd2, 0x02, 0x0a, 0x0a, 0x46, 0x75, 0x6c, 0x6c, 0x53, 0x74, 0x61, - 0x74, 0x75, 0x73, 0x12, 0x41, 0x0a, 0x0f, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, - 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x64, - 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, - 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x0f, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, - 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x35, 0x0a, 0x0b, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, - 0x53, 0x74, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x64, 0x61, - 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x65, - 0x52, 0x0b, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x3e, 0x0a, - 0x0e, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x18, - 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, - 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x0e, 0x6c, - 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x27, 0x0a, - 0x05, 0x70, 0x65, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x64, - 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, - 0x05, 0x70, 0x65, 0x65, 0x72, 0x73, 0x12, 0x2a, 0x0a, 0x06, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x73, - 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, - 0x52, 0x65, 0x6c, 0x61, 0x79, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x06, 0x72, 0x65, 0x6c, 0x61, - 0x79, 0x73, 0x12, 0x35, 0x0a, 0x0b, 0x64, 0x6e, 0x73, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, - 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, - 0x2e, 0x4e, 0x53, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x0a, 0x64, - 0x6e, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x22, 0x15, 0x0a, 0x13, 0x4c, 0x69, 0x73, - 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x22, 0x3f, 0x0a, 0x14, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x27, 0x0a, 0x06, 0x72, 0x6f, 0x75, 0x74, - 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, - 0x6e, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x52, 0x06, 0x72, 0x6f, 0x75, 0x74, 0x65, - 0x73, 0x22, 0x61, 0x0a, 0x15, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, - 0x72, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1e, 0x0a, 0x0a, 0x6e, 0x65, - 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x49, 0x44, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, - 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x49, 0x44, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x61, 0x70, - 0x70, 0x65, 0x6e, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x61, 0x70, 0x70, 0x65, - 0x6e, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x61, 0x6c, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, - 0x03, 0x61, 0x6c, 0x6c, 0x22, 0x18, 0x0a, 0x16, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, - 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1a, - 0x0a, 0x06, 0x49, 0x50, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x69, 0x70, 0x73, 0x18, - 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x03, 0x69, 0x70, 0x73, 0x22, 0xf9, 0x01, 0x0a, 0x07, 0x4e, - 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x02, 0x49, 0x44, 0x12, 0x14, 0x0a, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x1a, 0x0a, 0x08, - 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, - 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x65, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x64, 0x6f, 0x6d, 0x61, - 0x69, 0x6e, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, - 0x6e, 0x73, 0x12, 0x42, 0x0a, 0x0b, 0x72, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x64, 0x49, 0x50, - 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, - 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, - 0x64, 0x49, 0x50, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0b, 0x72, 0x65, 0x73, 0x6f, 0x6c, - 0x76, 0x65, 0x64, 0x49, 0x50, 0x73, 0x1a, 0x4e, 0x0a, 0x10, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, - 0x65, 0x64, 0x49, 0x50, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, - 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x24, 0x0a, 0x05, - 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x64, 0x61, - 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x49, 0x50, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x05, 0x76, 0x61, 0x6c, - 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x6a, 0x0a, 0x12, 0x44, 0x65, 0x62, 0x75, 0x67, 0x42, - 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, - 0x61, 0x6e, 0x6f, 0x6e, 0x79, 0x6d, 0x69, 0x7a, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, - 0x09, 0x61, 0x6e, 0x6f, 0x6e, 0x79, 0x6d, 0x69, 0x7a, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, - 0x61, 0x74, 0x75, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, - 0x75, 0x73, 0x12, 0x1e, 0x0a, 0x0a, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x49, 0x6e, 0x66, 0x6f, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x49, 0x6e, - 0x66, 0x6f, 0x22, 0x29, 0x0a, 0x13, 0x44, 0x65, 0x62, 0x75, 0x67, 0x42, 0x75, 0x6e, 0x64, 0x6c, - 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, - 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x22, 0x14, 0x0a, - 0x12, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x22, 0x3d, 0x0a, 0x13, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, - 0x65, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x26, 0x0a, 0x05, 0x6c, 0x65, - 0x76, 0x65, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x10, 0x2e, 0x64, 0x61, 0x65, 0x6d, - 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x05, 0x6c, 0x65, 0x76, - 0x65, 0x6c, 0x22, 0x3c, 0x0a, 0x12, 0x53, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, - 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x05, 0x6c, 0x65, 0x76, 0x65, - 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x10, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, - 0x2e, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, - 0x22, 0x15, 0x0a, 0x13, 0x53, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1b, 0x0a, 0x05, 0x53, 0x74, 0x61, 0x74, 0x65, - 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, - 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x13, 0x0a, 0x11, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, - 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x3b, 0x0a, 0x12, 0x4c, 0x69, 0x73, - 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x25, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, - 0x0d, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x06, - 0x73, 0x74, 0x61, 0x74, 0x65, 0x73, 0x22, 0x44, 0x0a, 0x11, 0x43, 0x6c, 0x65, 0x61, 0x6e, 0x53, - 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x73, - 0x74, 0x61, 0x74, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x09, 0x73, 0x74, 0x61, 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x61, 0x6c, - 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x61, 0x6c, 0x6c, 0x22, 0x3b, 0x0a, 0x12, - 0x43, 0x6c, 0x65, 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x63, 0x6c, 0x65, 0x61, 0x6e, 0x65, 0x64, 0x5f, 0x73, 0x74, - 0x61, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0d, 0x63, 0x6c, 0x65, 0x61, - 0x6e, 0x65, 0x64, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x22, 0x45, 0x0a, 0x12, 0x44, 0x65, 0x6c, - 0x65, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, + 0x22, 0x52, 0x0a, 0x0a, 0x52, 0x65, 0x6c, 0x61, 0x79, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x10, + 0x0a, 0x03, 0x55, 0x52, 0x49, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x55, 0x52, 0x49, + 0x12, 0x1c, 0x0a, 0x09, 0x61, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x08, 0x52, 0x09, 0x61, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x14, + 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, + 0x72, 0x72, 0x6f, 0x72, 0x22, 0x72, 0x0a, 0x0c, 0x4e, 0x53, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x53, + 0x74, 0x61, 0x74, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x18, + 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x12, 0x18, + 0x0a, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, + 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, + 0x6c, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, + 0x65, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, 0xd2, 0x02, 0x0a, 0x0a, 0x46, 0x75, 0x6c, + 0x6c, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x41, 0x0a, 0x0f, 0x6d, 0x61, 0x6e, 0x61, 0x67, + 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x17, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, + 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x0f, 0x6d, 0x61, 0x6e, 0x61, 0x67, + 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x35, 0x0a, 0x0b, 0x73, 0x69, + 0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x13, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53, + 0x74, 0x61, 0x74, 0x65, 0x52, 0x0b, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, + 0x65, 0x12, 0x3e, 0x0a, 0x0e, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, + 0x61, 0x74, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x64, 0x61, 0x65, 0x6d, + 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, + 0x65, 0x52, 0x0e, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, + 0x65, 0x12, 0x27, 0x0a, 0x05, 0x70, 0x65, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x11, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, + 0x61, 0x74, 0x65, 0x52, 0x05, 0x70, 0x65, 0x65, 0x72, 0x73, 0x12, 0x2a, 0x0a, 0x06, 0x72, 0x65, + 0x6c, 0x61, 0x79, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x64, 0x61, 0x65, + 0x6d, 0x6f, 0x6e, 0x2e, 0x52, 0x65, 0x6c, 0x61, 0x79, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x06, + 0x72, 0x65, 0x6c, 0x61, 0x79, 0x73, 0x12, 0x35, 0x0a, 0x0b, 0x64, 0x6e, 0x73, 0x5f, 0x73, 0x65, + 0x72, 0x76, 0x65, 0x72, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x64, 0x61, + 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4e, 0x53, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x53, 0x74, 0x61, 0x74, + 0x65, 0x52, 0x0a, 0x64, 0x6e, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x22, 0x15, 0x0a, + 0x13, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x22, 0x3f, 0x0a, 0x14, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x65, 0x74, 0x77, + 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x27, 0x0a, 0x06, + 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x64, + 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x52, 0x06, 0x72, + 0x6f, 0x75, 0x74, 0x65, 0x73, 0x22, 0x61, 0x0a, 0x15, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, + 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1e, + 0x0a, 0x0a, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x49, 0x44, 0x73, 0x18, 0x01, 0x20, 0x03, + 0x28, 0x09, 0x52, 0x0a, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x49, 0x44, 0x73, 0x12, 0x16, + 0x0a, 0x06, 0x61, 0x70, 0x70, 0x65, 0x6e, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, + 0x61, 0x70, 0x70, 0x65, 0x6e, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x61, 0x6c, 0x6c, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x08, 0x52, 0x03, 0x61, 0x6c, 0x6c, 0x22, 0x18, 0x0a, 0x16, 0x53, 0x65, 0x6c, 0x65, + 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x22, 0x1a, 0x0a, 0x06, 0x49, 0x50, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, + 0x69, 0x70, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x03, 0x69, 0x70, 0x73, 0x22, 0xf9, + 0x01, 0x0a, 0x07, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x44, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x44, 0x12, 0x14, 0x0a, 0x05, 0x72, 0x61, + 0x6e, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, + 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x08, 0x52, 0x08, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x65, 0x64, 0x12, 0x18, 0x0a, 0x07, + 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x64, + 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x42, 0x0a, 0x0b, 0x72, 0x65, 0x73, 0x6f, 0x6c, 0x76, + 0x65, 0x64, 0x49, 0x50, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x64, 0x61, + 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x2e, 0x52, 0x65, 0x73, + 0x6f, 0x6c, 0x76, 0x65, 0x64, 0x49, 0x50, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0b, 0x72, + 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x64, 0x49, 0x50, 0x73, 0x1a, 0x4e, 0x0a, 0x10, 0x52, 0x65, + 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x64, 0x49, 0x50, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, + 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, + 0x12, 0x24, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x0e, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x49, 0x50, 0x4c, 0x69, 0x73, 0x74, 0x52, + 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x6a, 0x0a, 0x12, 0x44, 0x65, + 0x62, 0x75, 0x67, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x12, 0x1c, 0x0a, 0x09, 0x61, 0x6e, 0x6f, 0x6e, 0x79, 0x6d, 0x69, 0x7a, 0x65, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x08, 0x52, 0x09, 0x61, 0x6e, 0x6f, 0x6e, 0x79, 0x6d, 0x69, 0x7a, 0x65, 0x12, 0x16, + 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, + 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x1e, 0x0a, 0x0a, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, + 0x49, 0x6e, 0x66, 0x6f, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x73, 0x79, 0x73, 0x74, + 0x65, 0x6d, 0x49, 0x6e, 0x66, 0x6f, 0x22, 0x29, 0x0a, 0x13, 0x44, 0x65, 0x62, 0x75, 0x67, 0x42, + 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x12, 0x0a, + 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, + 0x68, 0x22, 0x14, 0x0a, 0x12, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x3d, 0x0a, 0x13, 0x47, 0x65, 0x74, 0x4c, 0x6f, + 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x26, + 0x0a, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x10, 0x2e, + 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, + 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x22, 0x3c, 0x0a, 0x12, 0x53, 0x65, 0x74, 0x4c, 0x6f, 0x67, + 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x05, + 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x10, 0x2e, 0x64, 0x61, + 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x05, 0x6c, + 0x65, 0x76, 0x65, 0x6c, 0x22, 0x15, 0x0a, 0x13, 0x53, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, + 0x76, 0x65, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1b, 0x0a, 0x05, 0x53, + 0x74, 0x61, 0x74, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x13, 0x0a, 0x11, 0x4c, 0x69, 0x73, 0x74, + 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x3b, 0x0a, + 0x12, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x25, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x74, 0x61, + 0x74, 0x65, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x65, 0x73, 0x22, 0x44, 0x0a, 0x11, 0x43, 0x6c, + 0x65, 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x74, 0x61, 0x74, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x74, 0x61, 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x61, 0x6c, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x61, 0x6c, 0x6c, - 0x22, 0x3c, 0x0a, 0x13, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x64, 0x65, 0x6c, 0x65, 0x74, - 0x65, 0x64, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, - 0x0d, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x22, 0x3b, - 0x0a, 0x1f, 0x53, 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x50, - 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x22, 0x22, 0x0a, 0x20, 0x53, - 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x50, 0x65, 0x72, 0x73, - 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2a, - 0x62, 0x0a, 0x08, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x0b, 0x0a, 0x07, 0x55, - 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x50, 0x41, 0x4e, 0x49, - 0x43, 0x10, 0x01, 0x12, 0x09, 0x0a, 0x05, 0x46, 0x41, 0x54, 0x41, 0x4c, 0x10, 0x02, 0x12, 0x09, - 0x0a, 0x05, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x03, 0x12, 0x08, 0x0a, 0x04, 0x57, 0x41, 0x52, - 0x4e, 0x10, 0x04, 0x12, 0x08, 0x0a, 0x04, 0x49, 0x4e, 0x46, 0x4f, 0x10, 0x05, 0x12, 0x09, 0x0a, - 0x05, 0x44, 0x45, 0x42, 0x55, 0x47, 0x10, 0x06, 0x12, 0x09, 0x0a, 0x05, 0x54, 0x52, 0x41, 0x43, - 0x45, 0x10, 0x07, 0x32, 0x93, 0x09, 0x0a, 0x0d, 0x44, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x53, 0x65, - 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x36, 0x0a, 0x05, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x14, - 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x15, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, - 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x4b, 0x0a, - 0x0c, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x1b, 0x2e, - 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, - 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x64, 0x61, 0x65, - 0x6d, 0x6f, 0x6e, 0x2e, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x2d, 0x0a, 0x02, 0x55, 0x70, - 0x12, 0x11, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x55, 0x70, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x55, 0x70, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x39, 0x0a, 0x06, 0x53, 0x74, 0x61, - 0x74, 0x75, 0x73, 0x12, 0x15, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x74, 0x61, - 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x64, 0x61, 0x65, - 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x22, 0x00, 0x12, 0x33, 0x0a, 0x04, 0x44, 0x6f, 0x77, 0x6e, 0x12, 0x13, 0x2e, 0x64, - 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x14, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x6f, 0x77, 0x6e, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x42, 0x0a, 0x09, 0x47, 0x65, 0x74, - 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x18, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, - 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x19, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x4b, 0x0a, - 0x0c, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x12, 0x1b, 0x2e, - 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, - 0x72, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x64, 0x61, 0x65, - 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x51, 0x0a, 0x0e, 0x53, 0x65, - 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x12, 0x1d, 0x2e, 0x64, - 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, - 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x64, 0x61, - 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, - 0x72, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x53, 0x0a, - 0x10, 0x44, 0x65, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, - 0x73, 0x12, 0x1d, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x6c, 0x65, 0x63, - 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x1e, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, - 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x22, 0x00, 0x12, 0x48, 0x0a, 0x0b, 0x44, 0x65, 0x62, 0x75, 0x67, 0x42, 0x75, 0x6e, 0x64, 0x6c, - 0x65, 0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x62, 0x75, 0x67, - 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, - 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x62, 0x75, 0x67, 0x42, 0x75, 0x6e, 0x64, - 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x0b, - 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x1a, 0x2e, 0x64, 0x61, + 0x22, 0x3b, 0x0a, 0x12, 0x43, 0x6c, 0x65, 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x63, 0x6c, 0x65, 0x61, 0x6e, 0x65, + 0x64, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0d, + 0x63, 0x6c, 0x65, 0x61, 0x6e, 0x65, 0x64, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x22, 0x45, 0x0a, + 0x12, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x74, 0x61, 0x74, 0x65, 0x5f, 0x6e, 0x61, 0x6d, + 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x74, 0x61, 0x74, 0x65, 0x4e, 0x61, + 0x6d, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x61, 0x6c, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, + 0x03, 0x61, 0x6c, 0x6c, 0x22, 0x3c, 0x0a, 0x13, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x74, + 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x64, + 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x05, 0x52, 0x0d, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x53, 0x74, 0x61, 0x74, + 0x65, 0x73, 0x22, 0x3b, 0x0a, 0x1f, 0x53, 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, + 0x4d, 0x61, 0x70, 0x50, 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x22, + 0x22, 0x0a, 0x20, 0x53, 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, + 0x50, 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x2a, 0x62, 0x0a, 0x08, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, + 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, + 0x50, 0x41, 0x4e, 0x49, 0x43, 0x10, 0x01, 0x12, 0x09, 0x0a, 0x05, 0x46, 0x41, 0x54, 0x41, 0x4c, + 0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x03, 0x12, 0x08, 0x0a, + 0x04, 0x57, 0x41, 0x52, 0x4e, 0x10, 0x04, 0x12, 0x08, 0x0a, 0x04, 0x49, 0x4e, 0x46, 0x4f, 0x10, + 0x05, 0x12, 0x09, 0x0a, 0x05, 0x44, 0x45, 0x42, 0x55, 0x47, 0x10, 0x06, 0x12, 0x09, 0x0a, 0x05, + 0x54, 0x52, 0x41, 0x43, 0x45, 0x10, 0x07, 0x32, 0x93, 0x09, 0x0a, 0x0d, 0x44, 0x61, 0x65, 0x6d, + 0x6f, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x36, 0x0a, 0x05, 0x4c, 0x6f, 0x67, + 0x69, 0x6e, 0x12, 0x14, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x67, 0x69, + 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x15, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, + 0x6e, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, + 0x00, 0x12, 0x4b, 0x0a, 0x0c, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, + 0x6e, 0x12, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x57, 0x61, 0x69, 0x74, 0x53, + 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, + 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, + 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x2d, + 0x0a, 0x02, 0x55, 0x70, 0x12, 0x11, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x55, 0x70, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, + 0x2e, 0x55, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x39, 0x0a, + 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x15, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, + 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, + 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x33, 0x0a, 0x04, 0x44, 0x6f, 0x77, 0x6e, + 0x12, 0x13, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x6f, 0x77, 0x6e, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, + 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x42, 0x0a, + 0x09, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x18, 0x2e, 0x64, 0x61, 0x65, + 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, + 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, + 0x00, 0x12, 0x4b, 0x0a, 0x0c, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, + 0x73, 0x12, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4e, + 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, + 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x65, 0x74, 0x77, + 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x51, + 0x0a, 0x0e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, + 0x12, 0x1d, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, + 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x1e, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, + 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, + 0x00, 0x12, 0x53, 0x0a, 0x10, 0x44, 0x65, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, + 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x12, 0x1d, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, + 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, + 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x0b, 0x44, 0x65, 0x62, 0x75, 0x67, 0x42, + 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, + 0x65, 0x62, 0x75, 0x67, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x62, 0x75, 0x67, + 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, + 0x12, 0x48, 0x0a, 0x0b, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, + 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, + 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, - 0x2e, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x0b, 0x53, 0x65, 0x74, 0x4c, 0x6f, 0x67, - 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, - 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x74, 0x4c, 0x6f, - 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, - 0x12, 0x45, 0x0a, 0x0a, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x12, 0x19, - 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, - 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, - 0x6f, 0x6e, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x45, 0x0a, 0x0a, 0x43, 0x6c, 0x65, 0x61, 0x6e, - 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x19, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x43, - 0x6c, 0x65, 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x43, 0x6c, 0x65, 0x61, 0x6e, 0x53, - 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x48, - 0x0a, 0x0b, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x1a, 0x2e, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x0b, 0x53, 0x65, + 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, + 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, + 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x22, 0x00, 0x12, 0x45, 0x0a, 0x0a, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, + 0x65, 0x73, 0x12, 0x19, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x69, 0x73, 0x74, + 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, + 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, + 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x45, 0x0a, 0x0a, 0x43, + 0x6c, 0x65, 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x19, 0x2e, 0x64, 0x61, 0x65, 0x6d, + 0x6f, 0x6e, 0x2e, 0x43, 0x6c, 0x65, 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x43, 0x6c, + 0x65, 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x22, 0x00, 0x12, 0x48, 0x0a, 0x0b, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, + 0x65, 0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, + 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x74, 0x61, - 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, - 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x6f, 0x0a, 0x18, 0x53, 0x65, 0x74, 0x4e, - 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x50, 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, - 0x65, 0x6e, 0x63, 0x65, 0x12, 0x27, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, - 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x50, 0x65, 0x72, 0x73, 0x69, - 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x28, 0x2e, - 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, - 0x6b, 0x4d, 0x61, 0x70, 0x50, 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 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, + 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x6f, 0x0a, 0x18, + 0x53, 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x50, 0x65, 0x72, + 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x27, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, + 0x6e, 0x2e, 0x53, 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x50, + 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x28, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x74, 0x4e, 0x65, + 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x50, 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, + 0x6e, 0x63, 0x65, 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 ( diff --git a/client/proto/daemon.proto b/client/proto/daemon.proto index ad3a4bc1a..b626276de 100644 --- a/client/proto/daemon.proto +++ b/client/proto/daemon.proto @@ -112,6 +112,8 @@ message LoginRequest { optional bool disable_server_routes = 21; optional bool disable_dns = 22; optional bool disable_firewall = 23; + + optional bool block_lan_access = 24; } message LoginResponse { diff --git a/client/server/debug.go b/client/server/debug.go index 3c4967b4e..de63697bf 100644 --- a/client/server/debug.go +++ b/client/server/debug.go @@ -293,6 +293,13 @@ func (s *Server) addCommonConfigFields(configContent *strings.Builder) { } configContent.WriteString(fmt.Sprintf("DisableAutoConnect: %v\n", s.config.DisableAutoConnect)) configContent.WriteString(fmt.Sprintf("DNSRouteInterval: %s\n", s.config.DNSRouteInterval)) + + configContent.WriteString(fmt.Sprintf("DisableClientRoutes: %v\n", s.config.DisableClientRoutes)) + configContent.WriteString(fmt.Sprintf("DisableServerRoutes: %v\n", s.config.DisableServerRoutes)) + configContent.WriteString(fmt.Sprintf("DisableDNS: %v\n", s.config.DisableDNS)) + configContent.WriteString(fmt.Sprintf("DisableFirewall: %v\n", s.config.DisableFirewall)) + + configContent.WriteString(fmt.Sprintf("BlockLANAccess: %v\n", s.config.BlockLANAccess)) } func (s *Server) addRoutes(req *proto.DebugBundleRequest, anonymizer *anonymize.Anonymizer, archive *zip.Writer) error { diff --git a/client/server/server.go b/client/server/server.go index 70d19bfab..638ede386 100644 --- a/client/server/server.go +++ b/client/server/server.go @@ -416,6 +416,11 @@ func (s *Server) Login(callerCtx context.Context, msg *proto.LoginRequest) (*pro s.latestConfigInput.DisableFirewall = msg.DisableFirewall } + if msg.BlockLanAccess != nil { + inputConfig.BlockLANAccess = msg.BlockLanAccess + s.latestConfigInput.BlockLANAccess = msg.BlockLanAccess + } + s.mutex.Unlock() if msg.OptionalPreSharedKey != nil { From 992a6c79b4b4f4a7d7cc091bfac4110b295a713d Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Wed, 15 Jan 2025 23:26:31 +0100 Subject: [PATCH 12/92] [client] Flush macOS DNS cache after changes (#3185) --- client/internal/dns/host_darwin.go | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/client/internal/dns/host_darwin.go b/client/internal/dns/host_darwin.go index b8ba33e34..2f92dd367 100644 --- a/client/internal/dns/host_darwin.go +++ b/client/internal/dns/host_darwin.go @@ -28,6 +28,7 @@ const ( arraySymbol = "* " digitSymbol = "# " scutilPath = "/usr/sbin/scutil" + dscacheutilPath = "/usr/bin/dscacheutil" searchSuffix = "Search" matchSuffix = "Match" localSuffix = "Local" @@ -106,6 +107,10 @@ func (s *systemConfigurator) applyDNSConfig(config HostDNSConfig, stateManager * return fmt.Errorf("add search domains: %w", err) } + if err := s.flushDNSCache(); err != nil { + log.Errorf("failed to flush DNS cache: %v", err) + } + return nil } @@ -123,6 +128,10 @@ func (s *systemConfigurator) restoreHostDNS() error { } } + if err := s.flushDNSCache(); err != nil { + log.Errorf("failed to flush DNS cache: %v", err) + } + return nil } @@ -316,6 +325,21 @@ func (s *systemConfigurator) getPrimaryService() (string, string, error) { return primaryService, router, nil } +func (s *systemConfigurator) flushDNSCache() error { + cmd := exec.Command(dscacheutilPath, "-flushcache") + if out, err := cmd.CombinedOutput(); err != nil { + return fmt.Errorf("flush DNS cache: %w, output: %s", err, out) + } + + cmd = exec.Command("killall", "-HUP", "mDNSResponder") + if out, err := cmd.CombinedOutput(); err != nil { + return fmt.Errorf("restart mDNSResponder: %w, output: %s", err, out) + } + + log.Info("flushed DNS cache") + return nil +} + func (s *systemConfigurator) restoreUncleanShutdownDNS() error { if err := s.restoreHostDNS(); err != nil { return fmt.Errorf("restoring dns via scutil: %w", err) From c6f7a299a9c011d8e2449e7dd7534f18b3f47bdf Mon Sep 17 00:00:00 2001 From: Pascal Fischer <32096965+pascal-fischer@users.noreply.github.com> Date: Thu, 16 Jan 2025 13:39:15 +0100 Subject: [PATCH 13/92] [management] fix groups delete and resource create and update error response (#3189) --- management/server/group.go | 6 +++--- management/server/http/handlers/groups/groups_handler.go | 5 +++-- .../server/http/handlers/groups/groups_handler_test.go | 5 ++++- management/server/networks/resources/manager.go | 4 ++-- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/management/server/group.go b/management/server/group.go index f1057dda6..8f8196e3b 100644 --- a/management/server/group.go +++ b/management/server/group.go @@ -463,7 +463,7 @@ func validateDeleteGroup(ctx context.Context, transaction store.Store, group *ty if group.Issued == types.GroupIssuedIntegration { executingUser, err := transaction.GetUserByUserID(ctx, store.LockingStrengthShare, userID) if err != nil { - return err + return status.Errorf(status.Internal, "failed to get user") } if executingUser.Role != types.UserRoleAdmin || !executingUser.IsServiceUser { return status.Errorf(status.PermissionDenied, "only service users with admin power can delete integration group") @@ -505,7 +505,7 @@ func validateDeleteGroup(ctx context.Context, transaction store.Store, group *ty func checkGroupLinkedToSettings(ctx context.Context, transaction store.Store, group *types.Group) error { dnsSettings, err := transaction.GetAccountDNSSettings(ctx, store.LockingStrengthShare, group.AccountID) if err != nil { - return err + return status.Errorf(status.Internal, "failed to get DNS settings") } if slices.Contains(dnsSettings.DisabledManagementGroups, group.ID) { @@ -514,7 +514,7 @@ func checkGroupLinkedToSettings(ctx context.Context, transaction store.Store, gr settings, err := transaction.GetAccountSettings(ctx, store.LockingStrengthShare, group.AccountID) if err != nil { - return err + return status.Errorf(status.Internal, "failed to get account settings") } if settings.Extra != nil && slices.Contains(settings.Extra.IntegratedValidatorGroups, group.ID) { diff --git a/management/server/http/handlers/groups/groups_handler.go b/management/server/http/handlers/groups/groups_handler.go index 0ecea7ec2..b7121c234 100644 --- a/management/server/http/handlers/groups/groups_handler.go +++ b/management/server/http/handlers/groups/groups_handler.go @@ -239,8 +239,9 @@ func (h *handler) deleteGroup(w http.ResponseWriter, r *http.Request) { err = h.accountManager.DeleteGroup(r.Context(), accountID, userID, groupID) if err != nil { - _, ok := err.(*server.GroupLinkError) - if ok { + wrappedErr, ok := err.(interface{ Unwrap() []error }) + if ok && len(wrappedErr.Unwrap()) > 0 { + err = wrappedErr.Unwrap()[0] util.WriteErrorResponse(err.Error(), http.StatusBadRequest, w) return } diff --git a/management/server/http/handlers/groups/groups_handler_test.go b/management/server/http/handlers/groups/groups_handler_test.go index 49805ca9b..96e381da1 100644 --- a/management/server/http/handlers/groups/groups_handler_test.go +++ b/management/server/http/handlers/groups/groups_handler_test.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "encoding/json" + "errors" "fmt" "io" "net" @@ -73,10 +74,12 @@ func initGroupTestData(initGroups ...*types.Group) *handler { }, DeleteGroupFunc: func(_ context.Context, accountID, userId, groupID string) error { if groupID == "linked-grp" { - return &server.GroupLinkError{ + err := &server.GroupLinkError{ Resource: "something", Name: "linked-grp", } + var allErrors error + return errors.Join(allErrors, err) } if groupID == "invalid-grp" { return fmt.Errorf("internal error") diff --git a/management/server/networks/resources/manager.go b/management/server/networks/resources/manager.go index 725d15496..5b542d886 100644 --- a/management/server/networks/resources/manager.go +++ b/management/server/networks/resources/manager.go @@ -113,7 +113,7 @@ func (m *managerImpl) CreateResource(ctx context.Context, userID string, resourc err = m.store.ExecuteInTransaction(ctx, func(transaction store.Store) error { _, err = transaction.GetNetworkResourceByName(ctx, store.LockingStrengthShare, resource.AccountID, resource.Name) if err == nil { - return errors.New("resource already exists") + return status.Errorf(status.InvalidArgument, "resource with name %s already exists", resource.Name) } network, err := transaction.GetNetworkByID(ctx, store.LockingStrengthUpdate, resource.AccountID, resource.NetworkID) @@ -223,7 +223,7 @@ func (m *managerImpl) UpdateResource(ctx context.Context, userID string, resourc oldResource, err := transaction.GetNetworkResourceByName(ctx, store.LockingStrengthShare, resource.AccountID, resource.Name) if err == nil && oldResource.ID != resource.ID { - return errors.New("new resource name already exists") + return status.Errorf(status.InvalidArgument, "new resource name already exists") } oldResource, err = transaction.GetNetworkResourceByID(ctx, store.LockingStrengthShare, resource.AccountID, resource.ID) From bc7b2c6ba367d54a612365a79c00e7cc1f247005 Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Thu, 16 Jan 2025 13:58:00 +0100 Subject: [PATCH 14/92] [client] Report client system flags to management server on login (#3187) --- client/android/login.go | 2 +- client/internal/connect.go | 13 +- client/internal/engine.go | 28 + client/internal/login.go | 35 +- client/ios/NetBirdSDK/client.go | 2 +- client/ios/NetBirdSDK/login.go | 2 +- client/system/info.go | 25 + management/client/grpc.go | 10 + management/proto/management.pb.go | 1269 ++++++++++++++++------------- management/proto/management.proto | 11 + 10 files changed, 819 insertions(+), 578 deletions(-) diff --git a/client/android/login.go b/client/android/login.go index bb61edfa8..3d674c5be 100644 --- a/client/android/login.go +++ b/client/android/login.go @@ -162,7 +162,7 @@ func (a *Auth) login(urlOpener URLOpener) error { // check if we need to generate JWT token err := a.withBackOff(a.ctx, func() (err error) { - needsLogin, err = internal.IsLoginRequired(a.ctx, a.config.PrivateKey, a.config.ManagementURL, a.config.SSHKey) + needsLogin, err = internal.IsLoginRequired(a.ctx, a.config) return }) if err != nil { diff --git a/client/internal/connect.go b/client/internal/connect.go index 4dde7fa41..a1e8f0f8c 100644 --- a/client/internal/connect.go +++ b/client/internal/connect.go @@ -183,7 +183,7 @@ func (c *ConnectClient) run(mobileDependency MobileDependency, probes *ProbeHold }() // connect (just a connection, no stream yet) and login to Management Service to get an initial global Wiretrustee config - loginResp, err := loginToManagement(engineCtx, mgmClient, publicSSHKey) + loginResp, err := loginToManagement(engineCtx, mgmClient, publicSSHKey, c.config) if err != nil { log.Debug(err) if s, ok := gstatus.FromError(err); ok && (s.Code() == codes.PermissionDenied) { @@ -463,7 +463,7 @@ func connectToSignal(ctx context.Context, wtConfig *mgmProto.WiretrusteeConfig, } // loginToManagement creates Management Services client, establishes a connection, logs-in and gets a global Wiretrustee config (signal, turn, stun hosts, etc) -func loginToManagement(ctx context.Context, client mgm.Client, pubSSHKey []byte) (*mgmProto.LoginResponse, error) { +func loginToManagement(ctx context.Context, client mgm.Client, pubSSHKey []byte, config *Config) (*mgmProto.LoginResponse, error) { serverPublicKey, err := client.GetServerPublicKey() if err != nil { @@ -471,6 +471,15 @@ func loginToManagement(ctx context.Context, client mgm.Client, pubSSHKey []byte) } sysInfo := system.GetInfo(ctx) + sysInfo.SetFlags( + config.RosenpassEnabled, + config.RosenpassPermissive, + config.ServerSSHAllowed, + config.DisableClientRoutes, + config.DisableServerRoutes, + config.DisableDNS, + config.DisableFirewall, + ) loginResp, err := client.Login(*serverPublicKey, sysInfo, pubSSHKey) if err != nil { return nil, err diff --git a/client/internal/engine.go b/client/internal/engine.go index 12cc191b9..b3689c911 100644 --- a/client/internal/engine.go +++ b/client/internal/engine.go @@ -716,6 +716,15 @@ func (e *Engine) updateChecksIfNew(checks []*mgmProto.Checks) error { log.Warnf("failed to get system info with checks: %v", err) info = system.GetInfo(e.ctx) } + info.SetFlags( + e.config.RosenpassEnabled, + e.config.RosenpassPermissive, + &e.config.ServerSSHAllowed, + e.config.DisableClientRoutes, + e.config.DisableServerRoutes, + e.config.DisableDNS, + e.config.DisableFirewall, + ) if err := e.mgmClient.SyncMeta(info); err != nil { log.Errorf("could not sync meta: error %s", err) @@ -824,6 +833,15 @@ func (e *Engine) receiveManagementEvents() { log.Warnf("failed to get system info with checks: %v", err) info = system.GetInfo(e.ctx) } + info.SetFlags( + e.config.RosenpassEnabled, + e.config.RosenpassPermissive, + &e.config.ServerSSHAllowed, + e.config.DisableClientRoutes, + e.config.DisableServerRoutes, + e.config.DisableDNS, + e.config.DisableFirewall, + ) // err = e.mgmClient.Sync(info, e.handleSync) err = e.mgmClient.Sync(e.ctx, info, e.handleSync) @@ -1354,6 +1372,16 @@ func (e *Engine) close() { func (e *Engine) readInitialSettings() ([]*route.Route, *nbdns.Config, error) { info := system.GetInfo(e.ctx) + info.SetFlags( + e.config.RosenpassEnabled, + e.config.RosenpassPermissive, + &e.config.ServerSSHAllowed, + e.config.DisableClientRoutes, + e.config.DisableServerRoutes, + e.config.DisableDNS, + e.config.DisableFirewall, + ) + netMap, err := e.mgmClient.GetNetworkMap(info) if err != nil { return nil, nil, err diff --git a/client/internal/login.go b/client/internal/login.go index 9b6cf7848..b4ab1e363 100644 --- a/client/internal/login.go +++ b/client/internal/login.go @@ -17,8 +17,9 @@ import ( ) // IsLoginRequired check that the server is support SSO or not -func IsLoginRequired(ctx context.Context, privateKey string, mgmURL *url.URL, sshKey string) (bool, error) { - mgmClient, err := getMgmClient(ctx, privateKey, mgmURL) +func IsLoginRequired(ctx context.Context, config *Config) (bool, error) { + mgmURL := config.ManagementURL + mgmClient, err := getMgmClient(ctx, config.PrivateKey, mgmURL) if err != nil { return false, err } @@ -33,12 +34,12 @@ func IsLoginRequired(ctx context.Context, privateKey string, mgmURL *url.URL, ss }() log.Debugf("connected to the Management service %s", mgmURL.String()) - pubSSHKey, err := ssh.GeneratePublicKey([]byte(sshKey)) + pubSSHKey, err := ssh.GeneratePublicKey([]byte(config.SSHKey)) if err != nil { return false, err } - _, err = doMgmLogin(ctx, mgmClient, pubSSHKey) + _, err = doMgmLogin(ctx, mgmClient, pubSSHKey, config) if isLoginNeeded(err) { return true, nil } @@ -67,10 +68,10 @@ func Login(ctx context.Context, config *Config, setupKey string, jwtToken string return err } - serverKey, err := doMgmLogin(ctx, mgmClient, pubSSHKey) + serverKey, err := doMgmLogin(ctx, mgmClient, pubSSHKey, config) if serverKey != nil && isRegistrationNeeded(err) { log.Debugf("peer registration required") - _, err = registerPeer(ctx, *serverKey, mgmClient, setupKey, jwtToken, pubSSHKey) + _, err = registerPeer(ctx, *serverKey, mgmClient, setupKey, jwtToken, pubSSHKey, config) return err } @@ -99,7 +100,7 @@ func getMgmClient(ctx context.Context, privateKey string, mgmURL *url.URL) (*mgm return mgmClient, err } -func doMgmLogin(ctx context.Context, mgmClient *mgm.GrpcClient, pubSSHKey []byte) (*wgtypes.Key, error) { +func doMgmLogin(ctx context.Context, mgmClient *mgm.GrpcClient, pubSSHKey []byte, config *Config) (*wgtypes.Key, error) { serverKey, err := mgmClient.GetServerPublicKey() if err != nil { log.Errorf("failed while getting Management Service public key: %v", err) @@ -107,13 +108,22 @@ func doMgmLogin(ctx context.Context, mgmClient *mgm.GrpcClient, pubSSHKey []byte } sysInfo := system.GetInfo(ctx) + sysInfo.SetFlags( + config.RosenpassEnabled, + config.RosenpassPermissive, + config.ServerSSHAllowed, + config.DisableClientRoutes, + config.DisableServerRoutes, + config.DisableDNS, + config.DisableFirewall, + ) _, err = mgmClient.Login(*serverKey, sysInfo, pubSSHKey) return serverKey, err } // registerPeer checks whether setupKey was provided via cmd line and if not then it prompts user to enter a key. // Otherwise tries to register with the provided setupKey via command line. -func registerPeer(ctx context.Context, serverPublicKey wgtypes.Key, client *mgm.GrpcClient, setupKey string, jwtToken string, pubSSHKey []byte) (*mgmProto.LoginResponse, error) { +func registerPeer(ctx context.Context, serverPublicKey wgtypes.Key, client *mgm.GrpcClient, setupKey string, jwtToken string, pubSSHKey []byte, config *Config) (*mgmProto.LoginResponse, error) { validSetupKey, err := uuid.Parse(setupKey) if err != nil && jwtToken == "" { return nil, status.Errorf(codes.InvalidArgument, "invalid setup-key or no sso information provided, err: %v", err) @@ -121,6 +131,15 @@ func registerPeer(ctx context.Context, serverPublicKey wgtypes.Key, client *mgm. log.Debugf("sending peer registration request to Management Service") info := system.GetInfo(ctx) + info.SetFlags( + config.RosenpassEnabled, + config.RosenpassPermissive, + config.ServerSSHAllowed, + config.DisableClientRoutes, + config.DisableServerRoutes, + config.DisableDNS, + config.DisableFirewall, + ) loginResp, err := client.Register(serverPublicKey, validSetupKey.String(), jwtToken, info, pubSSHKey) if err != nil { log.Errorf("failed registering peer %v,%s", err, validSetupKey.String()) diff --git a/client/ios/NetBirdSDK/client.go b/client/ios/NetBirdSDK/client.go index befce56a2..622f8e840 100644 --- a/client/ios/NetBirdSDK/client.go +++ b/client/ios/NetBirdSDK/client.go @@ -207,7 +207,7 @@ func (c *Client) IsLoginRequired() bool { ConfigPath: c.cfgFile, }) - needsLogin, _ := internal.IsLoginRequired(ctx, cfg.PrivateKey, cfg.ManagementURL, cfg.SSHKey) + needsLogin, _ := internal.IsLoginRequired(ctx, cfg) return needsLogin } diff --git a/client/ios/NetBirdSDK/login.go b/client/ios/NetBirdSDK/login.go index ff637edd4..986874758 100644 --- a/client/ios/NetBirdSDK/login.go +++ b/client/ios/NetBirdSDK/login.go @@ -123,7 +123,7 @@ func (a *Auth) Login() error { // check if we need to generate JWT token err := a.withBackOff(a.ctx, func() (err error) { - needsLogin, err = internal.IsLoginRequired(a.ctx, a.config.PrivateKey, a.config.ManagementURL, a.config.SSHKey) + needsLogin, err = internal.IsLoginRequired(a.ctx, a.config) return }) if err != nil { diff --git a/client/system/info.go b/client/system/info.go index 200d835df..4ab4292ae 100644 --- a/client/system/info.go +++ b/client/system/info.go @@ -59,6 +59,31 @@ type Info struct { SystemManufacturer string Environment Environment Files []File // for posture checks + + RosenpassEnabled bool + RosenpassPermissive bool + ServerSSHAllowed bool + DisableClientRoutes bool + DisableServerRoutes bool + DisableDNS bool + DisableFirewall bool +} + +func (i *Info) SetFlags( + rosenpassEnabled, rosenpassPermissive bool, + serverSSHAllowed *bool, + disableClientRoutes, disableServerRoutes, + disableDNS, disableFirewall bool, +) { + i.RosenpassEnabled = rosenpassEnabled + i.RosenpassPermissive = rosenpassPermissive + if serverSSHAllowed != nil { + i.ServerSSHAllowed = *serverSSHAllowed + } + i.DisableClientRoutes = disableClientRoutes + i.DisableServerRoutes = disableServerRoutes + i.DisableDNS = disableDNS + i.DisableFirewall = disableFirewall } // StaticInfo is an object that contains machine information that does not change diff --git a/management/client/grpc.go b/management/client/grpc.go index 74e808c32..9a9c603df 100644 --- a/management/client/grpc.go +++ b/management/client/grpc.go @@ -540,5 +540,15 @@ func infoToMetaData(info *system.Info) *proto.PeerSystemMeta { Platform: info.Environment.Platform, }, Files: files, + + Flags: &proto.Flags{ + RosenpassEnabled: info.RosenpassEnabled, + RosenpassPermissive: info.RosenpassPermissive, + ServerSSHAllowed: info.ServerSSHAllowed, + DisableClientRoutes: info.DisableClientRoutes, + DisableServerRoutes: info.DisableServerRoutes, + DisableDNS: info.DisableDNS, + DisableFirewall: info.DisableFirewall, + }, } } diff --git a/management/proto/management.pb.go b/management/proto/management.pb.go index b4ff16e6d..7846c286d 100644 --- a/management/proto/management.pb.go +++ b/management/proto/management.pb.go @@ -223,7 +223,7 @@ func (x HostConfig_Protocol) Number() protoreflect.EnumNumber { // Deprecated: Use HostConfig_Protocol.Descriptor instead. func (HostConfig_Protocol) EnumDescriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{13, 0} + return file_management_proto_rawDescGZIP(), []int{14, 0} } type DeviceAuthorizationFlowProvider int32 @@ -266,7 +266,7 @@ func (x DeviceAuthorizationFlowProvider) Number() protoreflect.EnumNumber { // Deprecated: Use DeviceAuthorizationFlowProvider.Descriptor instead. func (DeviceAuthorizationFlowProvider) EnumDescriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{21, 0} + return file_management_proto_rawDescGZIP(), []int{22, 0} } type EncryptedMessage struct { @@ -784,6 +784,101 @@ func (x *File) GetProcessIsRunning() bool { return false } +type Flags struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + RosenpassEnabled bool `protobuf:"varint,1,opt,name=rosenpassEnabled,proto3" json:"rosenpassEnabled,omitempty"` + RosenpassPermissive bool `protobuf:"varint,2,opt,name=rosenpassPermissive,proto3" json:"rosenpassPermissive,omitempty"` + ServerSSHAllowed bool `protobuf:"varint,3,opt,name=serverSSHAllowed,proto3" json:"serverSSHAllowed,omitempty"` + DisableClientRoutes bool `protobuf:"varint,4,opt,name=disableClientRoutes,proto3" json:"disableClientRoutes,omitempty"` + DisableServerRoutes bool `protobuf:"varint,5,opt,name=disableServerRoutes,proto3" json:"disableServerRoutes,omitempty"` + DisableDNS bool `protobuf:"varint,6,opt,name=disableDNS,proto3" json:"disableDNS,omitempty"` + DisableFirewall bool `protobuf:"varint,7,opt,name=disableFirewall,proto3" json:"disableFirewall,omitempty"` +} + +func (x *Flags) Reset() { + *x = Flags{} + if protoimpl.UnsafeEnabled { + mi := &file_management_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Flags) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Flags) ProtoMessage() {} + +func (x *Flags) ProtoReflect() protoreflect.Message { + mi := &file_management_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 Flags.ProtoReflect.Descriptor instead. +func (*Flags) Descriptor() ([]byte, []int) { + return file_management_proto_rawDescGZIP(), []int{8} +} + +func (x *Flags) GetRosenpassEnabled() bool { + if x != nil { + return x.RosenpassEnabled + } + return false +} + +func (x *Flags) GetRosenpassPermissive() bool { + if x != nil { + return x.RosenpassPermissive + } + return false +} + +func (x *Flags) GetServerSSHAllowed() bool { + if x != nil { + return x.ServerSSHAllowed + } + return false +} + +func (x *Flags) GetDisableClientRoutes() bool { + if x != nil { + return x.DisableClientRoutes + } + return false +} + +func (x *Flags) GetDisableServerRoutes() bool { + if x != nil { + return x.DisableServerRoutes + } + return false +} + +func (x *Flags) GetDisableDNS() bool { + if x != nil { + return x.DisableDNS + } + return false +} + +func (x *Flags) GetDisableFirewall() bool { + if x != nil { + return x.DisableFirewall + } + return false +} + // PeerSystemMeta is machine meta data like OS and version. type PeerSystemMeta struct { state protoimpl.MessageState @@ -806,12 +901,13 @@ type PeerSystemMeta struct { SysManufacturer string `protobuf:"bytes,14,opt,name=sysManufacturer,proto3" json:"sysManufacturer,omitempty"` Environment *Environment `protobuf:"bytes,15,opt,name=environment,proto3" json:"environment,omitempty"` Files []*File `protobuf:"bytes,16,rep,name=files,proto3" json:"files,omitempty"` + Flags *Flags `protobuf:"bytes,17,opt,name=flags,proto3" json:"flags,omitempty"` } func (x *PeerSystemMeta) Reset() { *x = PeerSystemMeta{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[8] + mi := &file_management_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -824,7 +920,7 @@ func (x *PeerSystemMeta) String() string { func (*PeerSystemMeta) ProtoMessage() {} func (x *PeerSystemMeta) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[8] + mi := &file_management_proto_msgTypes[9] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -837,7 +933,7 @@ func (x *PeerSystemMeta) ProtoReflect() protoreflect.Message { // Deprecated: Use PeerSystemMeta.ProtoReflect.Descriptor instead. func (*PeerSystemMeta) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{8} + return file_management_proto_rawDescGZIP(), []int{9} } func (x *PeerSystemMeta) GetHostname() string { @@ -952,6 +1048,13 @@ func (x *PeerSystemMeta) GetFiles() []*File { return nil } +func (x *PeerSystemMeta) GetFlags() *Flags { + if x != nil { + return x.Flags + } + return nil +} + type LoginResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -968,7 +1071,7 @@ type LoginResponse struct { func (x *LoginResponse) Reset() { *x = LoginResponse{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[9] + mi := &file_management_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -981,7 +1084,7 @@ func (x *LoginResponse) String() string { func (*LoginResponse) ProtoMessage() {} func (x *LoginResponse) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[9] + mi := &file_management_proto_msgTypes[10] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -994,7 +1097,7 @@ func (x *LoginResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use LoginResponse.ProtoReflect.Descriptor instead. func (*LoginResponse) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{9} + return file_management_proto_rawDescGZIP(), []int{10} } func (x *LoginResponse) GetWiretrusteeConfig() *WiretrusteeConfig { @@ -1034,7 +1137,7 @@ type ServerKeyResponse struct { func (x *ServerKeyResponse) Reset() { *x = ServerKeyResponse{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[10] + mi := &file_management_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1047,7 +1150,7 @@ func (x *ServerKeyResponse) String() string { func (*ServerKeyResponse) ProtoMessage() {} func (x *ServerKeyResponse) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[10] + mi := &file_management_proto_msgTypes[11] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1060,7 +1163,7 @@ func (x *ServerKeyResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ServerKeyResponse.ProtoReflect.Descriptor instead. func (*ServerKeyResponse) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{10} + return file_management_proto_rawDescGZIP(), []int{11} } func (x *ServerKeyResponse) GetKey() string { @@ -1093,7 +1196,7 @@ type Empty struct { func (x *Empty) Reset() { *x = Empty{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[11] + mi := &file_management_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1106,7 +1209,7 @@ func (x *Empty) String() string { func (*Empty) ProtoMessage() {} func (x *Empty) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[11] + mi := &file_management_proto_msgTypes[12] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1119,7 +1222,7 @@ func (x *Empty) ProtoReflect() protoreflect.Message { // Deprecated: Use Empty.ProtoReflect.Descriptor instead. func (*Empty) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{11} + return file_management_proto_rawDescGZIP(), []int{12} } // WiretrusteeConfig is a common configuration of any Wiretrustee peer. It contains STUN, TURN, Signal and Management servers configurations @@ -1140,7 +1243,7 @@ type WiretrusteeConfig struct { func (x *WiretrusteeConfig) Reset() { *x = WiretrusteeConfig{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[12] + mi := &file_management_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1153,7 +1256,7 @@ func (x *WiretrusteeConfig) String() string { func (*WiretrusteeConfig) ProtoMessage() {} func (x *WiretrusteeConfig) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[12] + mi := &file_management_proto_msgTypes[13] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1166,7 +1269,7 @@ func (x *WiretrusteeConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use WiretrusteeConfig.ProtoReflect.Descriptor instead. func (*WiretrusteeConfig) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{12} + return file_management_proto_rawDescGZIP(), []int{13} } func (x *WiretrusteeConfig) GetStuns() []*HostConfig { @@ -1211,7 +1314,7 @@ type HostConfig struct { func (x *HostConfig) Reset() { *x = HostConfig{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[13] + mi := &file_management_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1224,7 +1327,7 @@ func (x *HostConfig) String() string { func (*HostConfig) ProtoMessage() {} func (x *HostConfig) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[13] + mi := &file_management_proto_msgTypes[14] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1237,7 +1340,7 @@ func (x *HostConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use HostConfig.ProtoReflect.Descriptor instead. func (*HostConfig) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{13} + return file_management_proto_rawDescGZIP(), []int{14} } func (x *HostConfig) GetUri() string { @@ -1267,7 +1370,7 @@ type RelayConfig struct { func (x *RelayConfig) Reset() { *x = RelayConfig{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[14] + mi := &file_management_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1280,7 +1383,7 @@ func (x *RelayConfig) String() string { func (*RelayConfig) ProtoMessage() {} func (x *RelayConfig) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[14] + mi := &file_management_proto_msgTypes[15] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1293,7 +1396,7 @@ func (x *RelayConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use RelayConfig.ProtoReflect.Descriptor instead. func (*RelayConfig) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{14} + return file_management_proto_rawDescGZIP(), []int{15} } func (x *RelayConfig) GetUrls() []string { @@ -1332,7 +1435,7 @@ type ProtectedHostConfig struct { func (x *ProtectedHostConfig) Reset() { *x = ProtectedHostConfig{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[15] + mi := &file_management_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1345,7 +1448,7 @@ func (x *ProtectedHostConfig) String() string { func (*ProtectedHostConfig) ProtoMessage() {} func (x *ProtectedHostConfig) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[15] + mi := &file_management_proto_msgTypes[16] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1358,7 +1461,7 @@ func (x *ProtectedHostConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use ProtectedHostConfig.ProtoReflect.Descriptor instead. func (*ProtectedHostConfig) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{15} + return file_management_proto_rawDescGZIP(), []int{16} } func (x *ProtectedHostConfig) GetHostConfig() *HostConfig { @@ -1403,7 +1506,7 @@ type PeerConfig struct { func (x *PeerConfig) Reset() { *x = PeerConfig{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[16] + mi := &file_management_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1416,7 +1519,7 @@ func (x *PeerConfig) String() string { func (*PeerConfig) ProtoMessage() {} func (x *PeerConfig) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[16] + mi := &file_management_proto_msgTypes[17] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1429,7 +1532,7 @@ func (x *PeerConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use PeerConfig.ProtoReflect.Descriptor instead. func (*PeerConfig) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{16} + return file_management_proto_rawDescGZIP(), []int{17} } func (x *PeerConfig) GetAddress() string { @@ -1502,7 +1605,7 @@ type NetworkMap struct { func (x *NetworkMap) Reset() { *x = NetworkMap{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[17] + mi := &file_management_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1515,7 +1618,7 @@ func (x *NetworkMap) String() string { func (*NetworkMap) ProtoMessage() {} func (x *NetworkMap) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[17] + mi := &file_management_proto_msgTypes[18] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1528,7 +1631,7 @@ func (x *NetworkMap) ProtoReflect() protoreflect.Message { // Deprecated: Use NetworkMap.ProtoReflect.Descriptor instead. func (*NetworkMap) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{17} + return file_management_proto_rawDescGZIP(), []int{18} } func (x *NetworkMap) GetSerial() uint64 { @@ -1628,7 +1731,7 @@ type RemotePeerConfig struct { func (x *RemotePeerConfig) Reset() { *x = RemotePeerConfig{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[18] + mi := &file_management_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1641,7 +1744,7 @@ func (x *RemotePeerConfig) String() string { func (*RemotePeerConfig) ProtoMessage() {} func (x *RemotePeerConfig) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[18] + mi := &file_management_proto_msgTypes[19] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1654,7 +1757,7 @@ func (x *RemotePeerConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use RemotePeerConfig.ProtoReflect.Descriptor instead. func (*RemotePeerConfig) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{18} + return file_management_proto_rawDescGZIP(), []int{19} } func (x *RemotePeerConfig) GetWgPubKey() string { @@ -1701,7 +1804,7 @@ type SSHConfig struct { func (x *SSHConfig) Reset() { *x = SSHConfig{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[19] + mi := &file_management_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1714,7 +1817,7 @@ func (x *SSHConfig) String() string { func (*SSHConfig) ProtoMessage() {} func (x *SSHConfig) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[19] + mi := &file_management_proto_msgTypes[20] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1727,7 +1830,7 @@ func (x *SSHConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use SSHConfig.ProtoReflect.Descriptor instead. func (*SSHConfig) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{19} + return file_management_proto_rawDescGZIP(), []int{20} } func (x *SSHConfig) GetSshEnabled() bool { @@ -1754,7 +1857,7 @@ type DeviceAuthorizationFlowRequest struct { func (x *DeviceAuthorizationFlowRequest) Reset() { *x = DeviceAuthorizationFlowRequest{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[20] + mi := &file_management_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1767,7 +1870,7 @@ func (x *DeviceAuthorizationFlowRequest) String() string { func (*DeviceAuthorizationFlowRequest) ProtoMessage() {} func (x *DeviceAuthorizationFlowRequest) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[20] + mi := &file_management_proto_msgTypes[21] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1780,7 +1883,7 @@ func (x *DeviceAuthorizationFlowRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DeviceAuthorizationFlowRequest.ProtoReflect.Descriptor instead. func (*DeviceAuthorizationFlowRequest) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{20} + return file_management_proto_rawDescGZIP(), []int{21} } // DeviceAuthorizationFlow represents Device Authorization Flow information @@ -1799,7 +1902,7 @@ type DeviceAuthorizationFlow struct { func (x *DeviceAuthorizationFlow) Reset() { *x = DeviceAuthorizationFlow{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[21] + mi := &file_management_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1812,7 +1915,7 @@ func (x *DeviceAuthorizationFlow) String() string { func (*DeviceAuthorizationFlow) ProtoMessage() {} func (x *DeviceAuthorizationFlow) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[21] + mi := &file_management_proto_msgTypes[22] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1825,7 +1928,7 @@ func (x *DeviceAuthorizationFlow) ProtoReflect() protoreflect.Message { // Deprecated: Use DeviceAuthorizationFlow.ProtoReflect.Descriptor instead. func (*DeviceAuthorizationFlow) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{21} + return file_management_proto_rawDescGZIP(), []int{22} } func (x *DeviceAuthorizationFlow) GetProvider() DeviceAuthorizationFlowProvider { @@ -1852,7 +1955,7 @@ type PKCEAuthorizationFlowRequest struct { func (x *PKCEAuthorizationFlowRequest) Reset() { *x = PKCEAuthorizationFlowRequest{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[22] + mi := &file_management_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1865,7 +1968,7 @@ func (x *PKCEAuthorizationFlowRequest) String() string { func (*PKCEAuthorizationFlowRequest) ProtoMessage() {} func (x *PKCEAuthorizationFlowRequest) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[22] + mi := &file_management_proto_msgTypes[23] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1878,7 +1981,7 @@ func (x *PKCEAuthorizationFlowRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use PKCEAuthorizationFlowRequest.ProtoReflect.Descriptor instead. func (*PKCEAuthorizationFlowRequest) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{22} + return file_management_proto_rawDescGZIP(), []int{23} } // PKCEAuthorizationFlow represents Authorization Code Flow information @@ -1895,7 +1998,7 @@ type PKCEAuthorizationFlow struct { func (x *PKCEAuthorizationFlow) Reset() { *x = PKCEAuthorizationFlow{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[23] + mi := &file_management_proto_msgTypes[24] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1908,7 +2011,7 @@ func (x *PKCEAuthorizationFlow) String() string { func (*PKCEAuthorizationFlow) ProtoMessage() {} func (x *PKCEAuthorizationFlow) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[23] + mi := &file_management_proto_msgTypes[24] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1921,7 +2024,7 @@ func (x *PKCEAuthorizationFlow) ProtoReflect() protoreflect.Message { // Deprecated: Use PKCEAuthorizationFlow.ProtoReflect.Descriptor instead. func (*PKCEAuthorizationFlow) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{23} + return file_management_proto_rawDescGZIP(), []int{24} } func (x *PKCEAuthorizationFlow) GetProviderConfig() *ProviderConfig { @@ -1963,7 +2066,7 @@ type ProviderConfig struct { func (x *ProviderConfig) Reset() { *x = ProviderConfig{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[24] + mi := &file_management_proto_msgTypes[25] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1976,7 +2079,7 @@ func (x *ProviderConfig) String() string { func (*ProviderConfig) ProtoMessage() {} func (x *ProviderConfig) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[24] + mi := &file_management_proto_msgTypes[25] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1989,7 +2092,7 @@ func (x *ProviderConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use ProviderConfig.ProtoReflect.Descriptor instead. func (*ProviderConfig) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{24} + return file_management_proto_rawDescGZIP(), []int{25} } func (x *ProviderConfig) GetClientID() string { @@ -2082,7 +2185,7 @@ type Route struct { func (x *Route) Reset() { *x = Route{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[25] + mi := &file_management_proto_msgTypes[26] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2095,7 +2198,7 @@ func (x *Route) String() string { func (*Route) ProtoMessage() {} func (x *Route) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[25] + mi := &file_management_proto_msgTypes[26] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2108,7 +2211,7 @@ func (x *Route) ProtoReflect() protoreflect.Message { // Deprecated: Use Route.ProtoReflect.Descriptor instead. func (*Route) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{25} + return file_management_proto_rawDescGZIP(), []int{26} } func (x *Route) GetID() string { @@ -2188,7 +2291,7 @@ type DNSConfig struct { func (x *DNSConfig) Reset() { *x = DNSConfig{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[26] + mi := &file_management_proto_msgTypes[27] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2201,7 +2304,7 @@ func (x *DNSConfig) String() string { func (*DNSConfig) ProtoMessage() {} func (x *DNSConfig) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[26] + mi := &file_management_proto_msgTypes[27] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2214,7 +2317,7 @@ func (x *DNSConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use DNSConfig.ProtoReflect.Descriptor instead. func (*DNSConfig) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{26} + return file_management_proto_rawDescGZIP(), []int{27} } func (x *DNSConfig) GetServiceEnable() bool { @@ -2251,7 +2354,7 @@ type CustomZone struct { func (x *CustomZone) Reset() { *x = CustomZone{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[27] + mi := &file_management_proto_msgTypes[28] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2264,7 +2367,7 @@ func (x *CustomZone) String() string { func (*CustomZone) ProtoMessage() {} func (x *CustomZone) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[27] + mi := &file_management_proto_msgTypes[28] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2277,7 +2380,7 @@ func (x *CustomZone) ProtoReflect() protoreflect.Message { // Deprecated: Use CustomZone.ProtoReflect.Descriptor instead. func (*CustomZone) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{27} + return file_management_proto_rawDescGZIP(), []int{28} } func (x *CustomZone) GetDomain() string { @@ -2310,7 +2413,7 @@ type SimpleRecord struct { func (x *SimpleRecord) Reset() { *x = SimpleRecord{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[28] + mi := &file_management_proto_msgTypes[29] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2323,7 +2426,7 @@ func (x *SimpleRecord) String() string { func (*SimpleRecord) ProtoMessage() {} func (x *SimpleRecord) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[28] + mi := &file_management_proto_msgTypes[29] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2336,7 +2439,7 @@ func (x *SimpleRecord) ProtoReflect() protoreflect.Message { // Deprecated: Use SimpleRecord.ProtoReflect.Descriptor instead. func (*SimpleRecord) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{28} + return file_management_proto_rawDescGZIP(), []int{29} } func (x *SimpleRecord) GetName() string { @@ -2389,7 +2492,7 @@ type NameServerGroup struct { func (x *NameServerGroup) Reset() { *x = NameServerGroup{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[29] + mi := &file_management_proto_msgTypes[30] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2402,7 +2505,7 @@ func (x *NameServerGroup) String() string { func (*NameServerGroup) ProtoMessage() {} func (x *NameServerGroup) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[29] + mi := &file_management_proto_msgTypes[30] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2415,7 +2518,7 @@ func (x *NameServerGroup) ProtoReflect() protoreflect.Message { // Deprecated: Use NameServerGroup.ProtoReflect.Descriptor instead. func (*NameServerGroup) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{29} + return file_management_proto_rawDescGZIP(), []int{30} } func (x *NameServerGroup) GetNameServers() []*NameServer { @@ -2460,7 +2563,7 @@ type NameServer struct { func (x *NameServer) Reset() { *x = NameServer{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[30] + mi := &file_management_proto_msgTypes[31] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2473,7 +2576,7 @@ func (x *NameServer) String() string { func (*NameServer) ProtoMessage() {} func (x *NameServer) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[30] + mi := &file_management_proto_msgTypes[31] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2486,7 +2589,7 @@ func (x *NameServer) ProtoReflect() protoreflect.Message { // Deprecated: Use NameServer.ProtoReflect.Descriptor instead. func (*NameServer) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{30} + return file_management_proto_rawDescGZIP(), []int{31} } func (x *NameServer) GetIP() string { @@ -2526,7 +2629,7 @@ type FirewallRule struct { func (x *FirewallRule) Reset() { *x = FirewallRule{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[31] + mi := &file_management_proto_msgTypes[32] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2539,7 +2642,7 @@ func (x *FirewallRule) String() string { func (*FirewallRule) ProtoMessage() {} func (x *FirewallRule) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[31] + mi := &file_management_proto_msgTypes[32] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2552,7 +2655,7 @@ func (x *FirewallRule) ProtoReflect() protoreflect.Message { // Deprecated: Use FirewallRule.ProtoReflect.Descriptor instead. func (*FirewallRule) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{31} + return file_management_proto_rawDescGZIP(), []int{32} } func (x *FirewallRule) GetPeerIP() string { @@ -2602,7 +2705,7 @@ type NetworkAddress struct { func (x *NetworkAddress) Reset() { *x = NetworkAddress{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[32] + mi := &file_management_proto_msgTypes[33] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2615,7 +2718,7 @@ func (x *NetworkAddress) String() string { func (*NetworkAddress) ProtoMessage() {} func (x *NetworkAddress) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[32] + mi := &file_management_proto_msgTypes[33] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2628,7 +2731,7 @@ func (x *NetworkAddress) ProtoReflect() protoreflect.Message { // Deprecated: Use NetworkAddress.ProtoReflect.Descriptor instead. func (*NetworkAddress) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{32} + return file_management_proto_rawDescGZIP(), []int{33} } func (x *NetworkAddress) GetNetIP() string { @@ -2656,7 +2759,7 @@ type Checks struct { func (x *Checks) Reset() { *x = Checks{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[33] + mi := &file_management_proto_msgTypes[34] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2669,7 +2772,7 @@ func (x *Checks) String() string { func (*Checks) ProtoMessage() {} func (x *Checks) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[33] + mi := &file_management_proto_msgTypes[34] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2682,7 +2785,7 @@ func (x *Checks) ProtoReflect() protoreflect.Message { // Deprecated: Use Checks.ProtoReflect.Descriptor instead. func (*Checks) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{33} + return file_management_proto_rawDescGZIP(), []int{34} } func (x *Checks) GetFiles() []string { @@ -2707,7 +2810,7 @@ type PortInfo struct { func (x *PortInfo) Reset() { *x = PortInfo{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[34] + mi := &file_management_proto_msgTypes[35] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2720,7 +2823,7 @@ func (x *PortInfo) String() string { func (*PortInfo) ProtoMessage() {} func (x *PortInfo) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[34] + mi := &file_management_proto_msgTypes[35] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2733,7 +2836,7 @@ func (x *PortInfo) ProtoReflect() protoreflect.Message { // Deprecated: Use PortInfo.ProtoReflect.Descriptor instead. func (*PortInfo) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{34} + return file_management_proto_rawDescGZIP(), []int{35} } func (m *PortInfo) GetPortSelection() isPortInfo_PortSelection { @@ -2800,7 +2903,7 @@ type RouteFirewallRule struct { func (x *RouteFirewallRule) Reset() { *x = RouteFirewallRule{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[35] + mi := &file_management_proto_msgTypes[36] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2813,7 +2916,7 @@ func (x *RouteFirewallRule) String() string { func (*RouteFirewallRule) ProtoMessage() {} func (x *RouteFirewallRule) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[35] + mi := &file_management_proto_msgTypes[36] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2826,7 +2929,7 @@ func (x *RouteFirewallRule) ProtoReflect() protoreflect.Message { // Deprecated: Use RouteFirewallRule.ProtoReflect.Descriptor instead. func (*RouteFirewallRule) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{35} + return file_management_proto_rawDescGZIP(), []int{36} } func (x *RouteFirewallRule) GetSourceRanges() []string { @@ -2897,7 +3000,7 @@ type PortInfo_Range struct { func (x *PortInfo_Range) Reset() { *x = PortInfo_Range{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[36] + mi := &file_management_proto_msgTypes[37] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2910,7 +3013,7 @@ func (x *PortInfo_Range) String() string { func (*PortInfo_Range) ProtoMessage() {} func (x *PortInfo_Range) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[36] + mi := &file_management_proto_msgTypes[37] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2923,7 +3026,7 @@ func (x *PortInfo_Range) ProtoReflect() protoreflect.Message { // Deprecated: Use PortInfo_Range.ProtoReflect.Descriptor instead. func (*PortInfo_Range) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{34, 0} + return file_management_proto_rawDescGZIP(), []int{35, 0} } func (x *PortInfo_Range) GetStart() uint32 { @@ -3008,364 +3111,386 @@ var file_management_proto_rawDesc = []byte{ 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x05, 0x65, 0x78, 0x69, 0x73, 0x74, 0x12, 0x2a, 0x0a, 0x10, 0x70, 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x49, 0x73, 0x52, 0x75, 0x6e, 0x6e, 0x69, 0x6e, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x70, 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, - 0x49, 0x73, 0x52, 0x75, 0x6e, 0x6e, 0x69, 0x6e, 0x67, 0x22, 0xd1, 0x04, 0x0a, 0x0e, 0x50, 0x65, - 0x65, 0x72, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x4d, 0x65, 0x74, 0x61, 0x12, 0x1a, 0x0a, 0x08, - 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, - 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x67, 0x6f, 0x4f, 0x53, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x67, 0x6f, 0x4f, 0x53, 0x12, 0x16, 0x0a, 0x06, - 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6b, 0x65, - 0x72, 0x6e, 0x65, 0x6c, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, 0x72, 0x65, 0x18, 0x04, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x04, 0x63, 0x6f, 0x72, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x6c, 0x61, 0x74, - 0x66, 0x6f, 0x72, 0x6d, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x6c, 0x61, 0x74, - 0x66, 0x6f, 0x72, 0x6d, 0x12, 0x0e, 0x0a, 0x02, 0x4f, 0x53, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x02, 0x4f, 0x53, 0x12, 0x2e, 0x0a, 0x12, 0x77, 0x69, 0x72, 0x65, 0x74, 0x72, 0x75, 0x73, - 0x74, 0x65, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x12, 0x77, 0x69, 0x72, 0x65, 0x74, 0x72, 0x75, 0x73, 0x74, 0x65, 0x65, 0x56, 0x65, 0x72, - 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1c, 0x0a, 0x09, 0x75, 0x69, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, - 0x6e, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x75, 0x69, 0x56, 0x65, 0x72, 0x73, 0x69, - 0x6f, 0x6e, 0x12, 0x24, 0x0a, 0x0d, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x56, 0x65, 0x72, 0x73, - 0x69, 0x6f, 0x6e, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6b, 0x65, 0x72, 0x6e, 0x65, - 0x6c, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1c, 0x0a, 0x09, 0x4f, 0x53, 0x56, 0x65, - 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x4f, 0x53, 0x56, - 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x46, 0x0a, 0x10, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, - 0x6b, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x18, 0x0b, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x4e, 0x65, - 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x52, 0x10, 0x6e, 0x65, - 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x12, 0x28, - 0x0a, 0x0f, 0x73, 0x79, 0x73, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x4e, 0x75, 0x6d, 0x62, 0x65, - 0x72, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x73, 0x79, 0x73, 0x53, 0x65, 0x72, 0x69, - 0x61, 0x6c, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x12, 0x26, 0x0a, 0x0e, 0x73, 0x79, 0x73, 0x50, - 0x72, 0x6f, 0x64, 0x75, 0x63, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x0e, 0x73, 0x79, 0x73, 0x50, 0x72, 0x6f, 0x64, 0x75, 0x63, 0x74, 0x4e, 0x61, 0x6d, 0x65, - 0x12, 0x28, 0x0a, 0x0f, 0x73, 0x79, 0x73, 0x4d, 0x61, 0x6e, 0x75, 0x66, 0x61, 0x63, 0x74, 0x75, - 0x72, 0x65, 0x72, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x73, 0x79, 0x73, 0x4d, 0x61, - 0x6e, 0x75, 0x66, 0x61, 0x63, 0x74, 0x75, 0x72, 0x65, 0x72, 0x12, 0x39, 0x0a, 0x0b, 0x65, 0x6e, - 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x17, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x76, - 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x0b, 0x65, 0x6e, 0x76, 0x69, 0x72, 0x6f, - 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x26, 0x0a, 0x05, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x18, 0x10, - 0x20, 0x03, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, - 0x74, 0x2e, 0x46, 0x69, 0x6c, 0x65, 0x52, 0x05, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x22, 0xc0, 0x01, - 0x0a, 0x0d, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x4b, 0x0a, 0x11, 0x77, 0x69, 0x72, 0x65, 0x74, 0x72, 0x75, 0x73, 0x74, 0x65, 0x65, 0x43, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x6d, 0x61, 0x6e, - 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x57, 0x69, 0x72, 0x65, 0x74, 0x72, 0x75, 0x73, - 0x74, 0x65, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x11, 0x77, 0x69, 0x72, 0x65, 0x74, - 0x72, 0x75, 0x73, 0x74, 0x65, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x36, 0x0a, 0x0a, - 0x70, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x65, - 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0a, 0x70, 0x65, 0x65, 0x72, 0x43, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x12, 0x2a, 0x0a, 0x06, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x18, 0x03, - 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, - 0x74, 0x2e, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x52, 0x06, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, - 0x22, 0x79, 0x0a, 0x11, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x38, 0x0a, 0x09, 0x65, 0x78, 0x70, 0x69, 0x72, - 0x65, 0x73, 0x41, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, - 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, - 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x41, - 0x74, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x05, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x07, 0x0a, 0x05, 0x45, - 0x6d, 0x70, 0x74, 0x79, 0x22, 0xd7, 0x01, 0x0a, 0x11, 0x57, 0x69, 0x72, 0x65, 0x74, 0x72, 0x75, - 0x73, 0x74, 0x65, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x2c, 0x0a, 0x05, 0x73, 0x74, - 0x75, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, - 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, - 0x67, 0x52, 0x05, 0x73, 0x74, 0x75, 0x6e, 0x73, 0x12, 0x35, 0x0a, 0x05, 0x74, 0x75, 0x72, 0x6e, - 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, - 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x72, 0x6f, 0x74, 0x65, 0x63, 0x74, 0x65, 0x64, 0x48, 0x6f, - 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x05, 0x74, 0x75, 0x72, 0x6e, 0x73, 0x12, - 0x2e, 0x0a, 0x06, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x48, 0x6f, 0x73, - 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x06, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x12, - 0x2d, 0x0a, 0x05, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, - 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x65, 0x6c, 0x61, - 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x05, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x22, 0x98, - 0x01, 0x0a, 0x0a, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x10, 0x0a, - 0x03, 0x75, 0x72, 0x69, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x69, 0x12, - 0x3b, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x0e, 0x32, 0x1f, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x48, - 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, - 0x6f, 0x6c, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x22, 0x3b, 0x0a, 0x08, - 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x07, 0x0a, 0x03, 0x55, 0x44, 0x50, 0x10, - 0x00, 0x12, 0x07, 0x0a, 0x03, 0x54, 0x43, 0x50, 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x48, 0x54, - 0x54, 0x50, 0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, 0x48, 0x54, 0x54, 0x50, 0x53, 0x10, 0x03, 0x12, - 0x08, 0x0a, 0x04, 0x44, 0x54, 0x4c, 0x53, 0x10, 0x04, 0x22, 0x6d, 0x0a, 0x0b, 0x52, 0x65, 0x6c, - 0x61, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x72, 0x6c, 0x73, - 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x04, 0x75, 0x72, 0x6c, 0x73, 0x12, 0x22, 0x0a, 0x0c, - 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x0c, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, - 0x12, 0x26, 0x0a, 0x0e, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, - 0x72, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x53, - 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x22, 0x7d, 0x0a, 0x13, 0x50, 0x72, 0x6f, 0x74, - 0x65, 0x63, 0x74, 0x65, 0x64, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, - 0x36, 0x0a, 0x0a, 0x68, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, - 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0a, 0x68, 0x6f, 0x73, - 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x12, 0x1a, 0x0a, 0x08, 0x70, - 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, - 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, 0xcb, 0x01, 0x0a, 0x0a, 0x50, 0x65, 0x65, 0x72, - 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, - 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, - 0x12, 0x10, 0x0a, 0x03, 0x64, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x64, - 0x6e, 0x73, 0x12, 0x33, 0x0a, 0x09, 0x73, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, - 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, - 0x6e, 0x74, 0x2e, 0x53, 0x53, 0x48, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x09, 0x73, 0x73, - 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x18, - 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x12, 0x48, 0x0a, 0x1f, 0x52, - 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, 0x50, 0x65, 0x65, 0x72, 0x44, 0x6e, 0x73, 0x52, 0x65, 0x73, - 0x6f, 0x6c, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x05, - 0x20, 0x01, 0x28, 0x08, 0x52, 0x1f, 0x52, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, 0x50, 0x65, 0x65, - 0x72, 0x44, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, - 0x61, 0x62, 0x6c, 0x65, 0x64, 0x22, 0xf3, 0x04, 0x0a, 0x0a, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, - 0x6b, 0x4d, 0x61, 0x70, 0x12, 0x16, 0x0a, 0x06, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x12, 0x36, 0x0a, 0x0a, - 0x70, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x65, - 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0a, 0x70, 0x65, 0x65, 0x72, 0x43, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x12, 0x3e, 0x0a, 0x0b, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, - 0x65, 0x72, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, - 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, - 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0b, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, - 0x65, 0x65, 0x72, 0x73, 0x12, 0x2e, 0x0a, 0x12, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, - 0x65, 0x72, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, - 0x52, 0x12, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x49, 0x73, 0x45, - 0x6d, 0x70, 0x74, 0x79, 0x12, 0x29, 0x0a, 0x06, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x18, 0x05, - 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, - 0x74, 0x2e, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x52, 0x06, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x12, - 0x33, 0x0a, 0x09, 0x44, 0x4e, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x06, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, - 0x44, 0x4e, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x09, 0x44, 0x4e, 0x53, 0x43, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x12, 0x40, 0x0a, 0x0c, 0x6f, 0x66, 0x66, 0x6c, 0x69, 0x6e, 0x65, 0x50, - 0x65, 0x65, 0x72, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, - 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, - 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0c, 0x6f, 0x66, 0x66, 0x6c, 0x69, 0x6e, - 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x12, 0x3e, 0x0a, 0x0d, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, - 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, - 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x46, 0x69, 0x72, 0x65, 0x77, - 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x52, 0x0d, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, - 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x12, 0x32, 0x0a, 0x14, 0x66, 0x69, 0x72, 0x65, 0x77, 0x61, - 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x18, 0x09, - 0x20, 0x01, 0x28, 0x08, 0x52, 0x14, 0x66, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, - 0x6c, 0x65, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x4f, 0x0a, 0x13, 0x72, 0x6f, - 0x75, 0x74, 0x65, 0x73, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, - 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, - 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, - 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x52, 0x13, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x46, 0x69, - 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x12, 0x3e, 0x0a, 0x1a, 0x72, - 0x6f, 0x75, 0x74, 0x65, 0x73, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, - 0x65, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x08, 0x52, - 0x1a, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, - 0x75, 0x6c, 0x65, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x97, 0x01, 0x0a, 0x10, - 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, - 0x12, 0x1a, 0x0a, 0x08, 0x77, 0x67, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x08, 0x77, 0x67, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x12, 0x1e, 0x0a, 0x0a, - 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x49, 0x70, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, - 0x52, 0x0a, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x49, 0x70, 0x73, 0x12, 0x33, 0x0a, 0x09, - 0x73, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x15, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x53, 0x48, - 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x09, 0x73, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, - 0x67, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x04, 0x66, 0x71, 0x64, 0x6e, 0x22, 0x49, 0x0a, 0x09, 0x53, 0x53, 0x48, 0x43, 0x6f, 0x6e, 0x66, - 0x69, 0x67, 0x12, 0x1e, 0x0a, 0x0a, 0x73, 0x73, 0x68, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x73, 0x73, 0x68, 0x45, 0x6e, 0x61, 0x62, 0x6c, - 0x65, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x73, 0x68, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x73, 0x73, 0x68, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, - 0x22, 0x20, 0x0a, 0x1e, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, - 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x22, 0xbf, 0x01, 0x0a, 0x17, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, - 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x12, 0x48, - 0x0a, 0x08, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, - 0x32, 0x2c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x44, 0x65, - 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x52, 0x08, - 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x42, 0x0a, 0x0e, 0x50, 0x72, 0x6f, 0x76, - 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, + 0x49, 0x73, 0x52, 0x75, 0x6e, 0x6e, 0x69, 0x6e, 0x67, 0x22, 0xbf, 0x02, 0x0a, 0x05, 0x46, 0x6c, + 0x61, 0x67, 0x73, 0x12, 0x2a, 0x0a, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, + 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x72, + 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, + 0x30, 0x0a, 0x13, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, 0x65, 0x72, 0x6d, + 0x69, 0x73, 0x73, 0x69, 0x76, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x13, 0x72, 0x6f, + 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x76, + 0x65, 0x12, 0x2a, 0x0a, 0x10, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x53, 0x53, 0x48, 0x41, 0x6c, + 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x73, 0x65, 0x72, + 0x76, 0x65, 0x72, 0x53, 0x53, 0x48, 0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x12, 0x30, 0x0a, + 0x13, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, 0x6f, + 0x75, 0x74, 0x65, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x13, 0x64, 0x69, 0x73, 0x61, + 0x62, 0x6c, 0x65, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x12, + 0x30, 0x0a, 0x13, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, + 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x13, 0x64, 0x69, + 0x73, 0x61, 0x62, 0x6c, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x6f, 0x75, 0x74, 0x65, + 0x73, 0x12, 0x1e, 0x0a, 0x0a, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x44, 0x4e, 0x53, 0x18, + 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x44, 0x4e, + 0x53, 0x12, 0x28, 0x0a, 0x0f, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x46, 0x69, 0x72, 0x65, + 0x77, 0x61, 0x6c, 0x6c, 0x18, 0x07, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, 0x64, 0x69, 0x73, 0x61, + 0x62, 0x6c, 0x65, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x22, 0xfa, 0x04, 0x0a, 0x0e, + 0x50, 0x65, 0x65, 0x72, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x4d, 0x65, 0x74, 0x61, 0x12, 0x1a, + 0x0a, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x67, 0x6f, + 0x4f, 0x53, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x67, 0x6f, 0x4f, 0x53, 0x12, 0x16, + 0x0a, 0x06, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, + 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, 0x72, 0x65, 0x18, 0x04, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x63, 0x6f, 0x72, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x6c, + 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x6c, + 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x12, 0x0e, 0x0a, 0x02, 0x4f, 0x53, 0x18, 0x06, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x02, 0x4f, 0x53, 0x12, 0x2e, 0x0a, 0x12, 0x77, 0x69, 0x72, 0x65, 0x74, 0x72, + 0x75, 0x73, 0x74, 0x65, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x07, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x12, 0x77, 0x69, 0x72, 0x65, 0x74, 0x72, 0x75, 0x73, 0x74, 0x65, 0x65, 0x56, + 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1c, 0x0a, 0x09, 0x75, 0x69, 0x56, 0x65, 0x72, 0x73, + 0x69, 0x6f, 0x6e, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x75, 0x69, 0x56, 0x65, 0x72, + 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x24, 0x0a, 0x0d, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x56, 0x65, + 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6b, 0x65, 0x72, + 0x6e, 0x65, 0x6c, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1c, 0x0a, 0x09, 0x4f, 0x53, + 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x4f, + 0x53, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x46, 0x0a, 0x10, 0x6e, 0x65, 0x74, 0x77, + 0x6f, 0x72, 0x6b, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x18, 0x0b, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, + 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x52, 0x10, + 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, + 0x12, 0x28, 0x0a, 0x0f, 0x73, 0x79, 0x73, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x4e, 0x75, 0x6d, + 0x62, 0x65, 0x72, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x73, 0x79, 0x73, 0x53, 0x65, + 0x72, 0x69, 0x61, 0x6c, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x12, 0x26, 0x0a, 0x0e, 0x73, 0x79, + 0x73, 0x50, 0x72, 0x6f, 0x64, 0x75, 0x63, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x0d, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0e, 0x73, 0x79, 0x73, 0x50, 0x72, 0x6f, 0x64, 0x75, 0x63, 0x74, 0x4e, 0x61, + 0x6d, 0x65, 0x12, 0x28, 0x0a, 0x0f, 0x73, 0x79, 0x73, 0x4d, 0x61, 0x6e, 0x75, 0x66, 0x61, 0x63, + 0x74, 0x75, 0x72, 0x65, 0x72, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x73, 0x79, 0x73, + 0x4d, 0x61, 0x6e, 0x75, 0x66, 0x61, 0x63, 0x74, 0x75, 0x72, 0x65, 0x72, 0x12, 0x39, 0x0a, 0x0b, + 0x65, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x18, 0x0f, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x17, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, + 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x0b, 0x65, 0x6e, 0x76, 0x69, + 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x26, 0x0a, 0x05, 0x66, 0x69, 0x6c, 0x65, 0x73, + 0x18, 0x10, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, + 0x65, 0x6e, 0x74, 0x2e, 0x46, 0x69, 0x6c, 0x65, 0x52, 0x05, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x12, + 0x27, 0x0a, 0x05, 0x66, 0x6c, 0x61, 0x67, 0x73, 0x18, 0x11, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, + 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x46, 0x6c, 0x61, 0x67, + 0x73, 0x52, 0x05, 0x66, 0x6c, 0x61, 0x67, 0x73, 0x22, 0xc0, 0x01, 0x0a, 0x0d, 0x4c, 0x6f, 0x67, + 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4b, 0x0a, 0x11, 0x77, 0x69, + 0x72, 0x65, 0x74, 0x72, 0x75, 0x73, 0x74, 0x65, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, + 0x6e, 0x74, 0x2e, 0x57, 0x69, 0x72, 0x65, 0x74, 0x72, 0x75, 0x73, 0x74, 0x65, 0x65, 0x43, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x52, 0x11, 0x77, 0x69, 0x72, 0x65, 0x74, 0x72, 0x75, 0x73, 0x74, 0x65, + 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x36, 0x0a, 0x0a, 0x70, 0x65, 0x65, 0x72, 0x43, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, + 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x52, 0x0a, 0x70, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, + 0x2a, 0x0a, 0x06, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x12, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x68, 0x65, + 0x63, 0x6b, 0x73, 0x52, 0x06, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x22, 0x79, 0x0a, 0x11, 0x53, + 0x65, 0x72, 0x76, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, + 0x65, 0x79, 0x12, 0x38, 0x0a, 0x09, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x41, 0x74, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, + 0x70, 0x52, 0x09, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x41, 0x74, 0x12, 0x18, 0x0a, 0x07, + 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x07, 0x76, + 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x07, 0x0a, 0x05, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, + 0xd7, 0x01, 0x0a, 0x11, 0x57, 0x69, 0x72, 0x65, 0x74, 0x72, 0x75, 0x73, 0x74, 0x65, 0x65, 0x43, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x2c, 0x0a, 0x05, 0x73, 0x74, 0x75, 0x6e, 0x73, 0x18, 0x01, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, + 0x74, 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x05, 0x73, 0x74, + 0x75, 0x6e, 0x73, 0x12, 0x35, 0x0a, 0x05, 0x74, 0x75, 0x72, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, + 0x50, 0x72, 0x6f, 0x74, 0x65, 0x63, 0x74, 0x65, 0x64, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x52, 0x05, 0x74, 0x75, 0x72, 0x6e, 0x73, 0x12, 0x2e, 0x0a, 0x06, 0x73, 0x69, + 0x67, 0x6e, 0x61, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, + 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x52, 0x06, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x12, 0x2d, 0x0a, 0x05, 0x72, 0x65, + 0x6c, 0x61, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x6d, 0x61, 0x6e, 0x61, + 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x65, 0x6c, 0x61, 0x79, 0x43, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x52, 0x05, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x22, 0x98, 0x01, 0x0a, 0x0a, 0x48, 0x6f, + 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x69, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x69, 0x12, 0x3b, 0x0a, 0x08, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1f, 0x2e, 0x6d, + 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x52, 0x08, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x22, 0x3b, 0x0a, 0x08, 0x50, 0x72, 0x6f, 0x74, 0x6f, + 0x63, 0x6f, 0x6c, 0x12, 0x07, 0x0a, 0x03, 0x55, 0x44, 0x50, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, + 0x54, 0x43, 0x50, 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x48, 0x54, 0x54, 0x50, 0x10, 0x02, 0x12, + 0x09, 0x0a, 0x05, 0x48, 0x54, 0x54, 0x50, 0x53, 0x10, 0x03, 0x12, 0x08, 0x0a, 0x04, 0x44, 0x54, + 0x4c, 0x53, 0x10, 0x04, 0x22, 0x6d, 0x0a, 0x0b, 0x52, 0x65, 0x6c, 0x61, 0x79, 0x43, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x72, 0x6c, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, + 0x09, 0x52, 0x04, 0x75, 0x72, 0x6c, 0x73, 0x12, 0x22, 0x0a, 0x0c, 0x74, 0x6f, 0x6b, 0x65, 0x6e, + 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x74, + 0x6f, 0x6b, 0x65, 0x6e, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x12, 0x26, 0x0a, 0x0e, 0x74, + 0x6f, 0x6b, 0x65, 0x6e, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0e, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, + 0x75, 0x72, 0x65, 0x22, 0x7d, 0x0a, 0x13, 0x50, 0x72, 0x6f, 0x74, 0x65, 0x63, 0x74, 0x65, 0x64, + 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x36, 0x0a, 0x0a, 0x68, 0x6f, + 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, + 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x48, 0x6f, 0x73, 0x74, + 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0a, 0x68, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, + 0x72, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, + 0x72, 0x64, 0x22, 0xcb, 0x01, 0x0a, 0x0a, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x64, + 0x6e, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x64, 0x6e, 0x73, 0x12, 0x33, 0x0a, + 0x09, 0x73, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x15, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x53, + 0x48, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x09, 0x73, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x12, 0x48, 0x0a, 0x1f, 0x52, 0x6f, 0x75, 0x74, 0x69, 0x6e, + 0x67, 0x50, 0x65, 0x65, 0x72, 0x44, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x75, 0x74, 0x69, + 0x6f, 0x6e, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, + 0x1f, 0x52, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, 0x50, 0x65, 0x65, 0x72, 0x44, 0x6e, 0x73, 0x52, + 0x65, 0x73, 0x6f, 0x6c, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, + 0x22, 0xf3, 0x04, 0x0a, 0x0a, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x12, + 0x16, 0x0a, 0x06, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, + 0x06, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x12, 0x36, 0x0a, 0x0a, 0x70, 0x65, 0x65, 0x72, 0x43, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, + 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x52, 0x0a, 0x70, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, + 0x3e, 0x0a, 0x0b, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x18, 0x03, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, + 0x74, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x52, 0x0b, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x12, + 0x2e, 0x0a, 0x12, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x49, 0x73, + 0x45, 0x6d, 0x70, 0x74, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x12, 0x72, 0x65, 0x6d, + 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, + 0x29, 0x0a, 0x06, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x6f, 0x75, + 0x74, 0x65, 0x52, 0x06, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x12, 0x33, 0x0a, 0x09, 0x44, 0x4e, + 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, + 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x44, 0x4e, 0x53, 0x43, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x52, 0x09, 0x44, 0x4e, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, + 0x40, 0x0a, 0x0c, 0x6f, 0x66, 0x66, 0x6c, 0x69, 0x6e, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x18, + 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, + 0x6e, 0x74, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x52, 0x0c, 0x6f, 0x66, 0x66, 0x6c, 0x69, 0x6e, 0x65, 0x50, 0x65, 0x65, 0x72, + 0x73, 0x12, 0x3e, 0x0a, 0x0d, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, + 0x65, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, + 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, + 0x6c, 0x65, 0x52, 0x0d, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, + 0x73, 0x12, 0x32, 0x0a, 0x14, 0x66, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, + 0x65, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, + 0x14, 0x66, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x49, 0x73, + 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x4f, 0x0a, 0x13, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x46, + 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x18, 0x0a, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, + 0x52, 0x6f, 0x75, 0x74, 0x65, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, + 0x65, 0x52, 0x13, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, + 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x12, 0x3e, 0x0a, 0x1a, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, + 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x49, 0x73, 0x45, + 0x6d, 0x70, 0x74, 0x79, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x08, 0x52, 0x1a, 0x72, 0x6f, 0x75, 0x74, + 0x65, 0x73, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x49, + 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x97, 0x01, 0x0a, 0x10, 0x52, 0x65, 0x6d, 0x6f, 0x74, + 0x65, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1a, 0x0a, 0x08, 0x77, + 0x67, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x77, + 0x67, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x12, 0x1e, 0x0a, 0x0a, 0x61, 0x6c, 0x6c, 0x6f, 0x77, + 0x65, 0x64, 0x49, 0x70, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, 0x61, 0x6c, 0x6c, + 0x6f, 0x77, 0x65, 0x64, 0x49, 0x70, 0x73, 0x12, 0x33, 0x0a, 0x09, 0x73, 0x73, 0x68, 0x43, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6d, 0x61, 0x6e, + 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x53, 0x48, 0x43, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x52, 0x09, 0x73, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, 0x0a, 0x04, + 0x66, 0x71, 0x64, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x71, 0x64, 0x6e, + 0x22, 0x49, 0x0a, 0x09, 0x53, 0x53, 0x48, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1e, 0x0a, + 0x0a, 0x73, 0x73, 0x68, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x08, 0x52, 0x0a, 0x73, 0x73, 0x68, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x1c, 0x0a, + 0x09, 0x73, 0x73, 0x68, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, + 0x52, 0x09, 0x73, 0x73, 0x68, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x22, 0x20, 0x0a, 0x1e, 0x44, + 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xbf, 0x01, + 0x0a, 0x17, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x12, 0x48, 0x0a, 0x08, 0x50, 0x72, 0x6f, + 0x76, 0x69, 0x64, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x2c, 0x2e, 0x6d, 0x61, + 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, + 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, + 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x52, 0x08, 0x50, 0x72, 0x6f, 0x76, 0x69, + 0x64, 0x65, 0x72, 0x12, 0x42, 0x0a, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61, + 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, + 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, + 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0x16, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x76, 0x69, + 0x64, 0x65, 0x72, 0x12, 0x0a, 0x0a, 0x06, 0x48, 0x4f, 0x53, 0x54, 0x45, 0x44, 0x10, 0x00, 0x22, + 0x1e, 0x0a, 0x1c, 0x50, 0x4b, 0x43, 0x45, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, + 0x5b, 0x0a, 0x15, 0x50, 0x4b, 0x43, 0x45, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x12, 0x42, 0x0a, 0x0e, 0x50, 0x72, 0x6f, 0x76, + 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0e, 0x50, 0x72, - 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0x16, 0x0a, 0x08, - 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x0a, 0x0a, 0x06, 0x48, 0x4f, 0x53, 0x54, - 0x45, 0x44, 0x10, 0x00, 0x22, 0x1e, 0x0a, 0x1c, 0x50, 0x4b, 0x43, 0x45, 0x41, 0x75, 0x74, 0x68, - 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x22, 0x5b, 0x0a, 0x15, 0x50, 0x4b, 0x43, 0x45, 0x41, 0x75, 0x74, 0x68, - 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x12, 0x42, 0x0a, - 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, - 0x6e, 0x74, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, - 0x67, 0x52, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, - 0x67, 0x22, 0xea, 0x02, 0x0a, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1a, 0x0a, 0x08, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x44, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x44, - 0x12, 0x22, 0x0a, 0x0c, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x53, 0x65, - 0x63, 0x72, 0x65, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x1a, 0x0a, 0x08, - 0x41, 0x75, 0x64, 0x69, 0x65, 0x6e, 0x63, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, - 0x41, 0x75, 0x64, 0x69, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x2e, 0x0a, 0x12, 0x44, 0x65, 0x76, 0x69, - 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x05, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, - 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x24, 0x0a, 0x0d, 0x54, 0x6f, 0x6b, 0x65, - 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x0d, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x14, - 0x0a, 0x05, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x53, - 0x63, 0x6f, 0x70, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x55, 0x73, 0x65, 0x49, 0x44, 0x54, 0x6f, 0x6b, - 0x65, 0x6e, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x55, 0x73, 0x65, 0x49, 0x44, 0x54, - 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x34, 0x0a, 0x15, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x09, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x15, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x22, 0x0a, 0x0c, 0x52, 0x65, - 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x55, 0x52, 0x4c, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x09, - 0x52, 0x0c, 0x52, 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x55, 0x52, 0x4c, 0x73, 0x22, 0xed, - 0x01, 0x0a, 0x05, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x44, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x44, 0x12, 0x18, 0x0a, 0x07, 0x4e, 0x65, 0x74, 0x77, - 0x6f, 0x72, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x4e, 0x65, 0x74, 0x77, 0x6f, - 0x72, 0x6b, 0x12, 0x20, 0x0a, 0x0b, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x54, 0x79, 0x70, - 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, - 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x65, 0x65, 0x72, 0x18, 0x04, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x04, 0x50, 0x65, 0x65, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x4d, 0x65, 0x74, 0x72, - 0x69, 0x63, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, - 0x12, 0x1e, 0x0a, 0x0a, 0x4d, 0x61, 0x73, 0x71, 0x75, 0x65, 0x72, 0x61, 0x64, 0x65, 0x18, 0x06, - 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x4d, 0x61, 0x73, 0x71, 0x75, 0x65, 0x72, 0x61, 0x64, 0x65, - 0x12, 0x14, 0x0a, 0x05, 0x4e, 0x65, 0x74, 0x49, 0x44, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x05, 0x4e, 0x65, 0x74, 0x49, 0x44, 0x12, 0x18, 0x0a, 0x07, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, - 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, - 0x12, 0x1c, 0x0a, 0x09, 0x6b, 0x65, 0x65, 0x70, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x18, 0x09, 0x20, - 0x01, 0x28, 0x08, 0x52, 0x09, 0x6b, 0x65, 0x65, 0x70, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x22, 0xb4, - 0x01, 0x0a, 0x09, 0x44, 0x4e, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x24, 0x0a, 0x0d, - 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x08, 0x52, 0x0d, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x45, 0x6e, 0x61, 0x62, - 0x6c, 0x65, 0x12, 0x47, 0x0a, 0x10, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, - 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x6d, - 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, - 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x10, 0x4e, 0x61, 0x6d, 0x65, 0x53, - 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x12, 0x38, 0x0a, 0x0b, 0x43, - 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x75, - 0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, 0x52, 0x0b, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, - 0x5a, 0x6f, 0x6e, 0x65, 0x73, 0x22, 0x58, 0x0a, 0x0a, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a, - 0x6f, 0x6e, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x32, 0x0a, 0x07, 0x52, - 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x6d, - 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x69, 0x6d, 0x70, 0x6c, 0x65, - 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x52, 0x07, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x22, - 0x74, 0x0a, 0x0c, 0x53, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x12, - 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, - 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x03, 0x52, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x43, 0x6c, 0x61, 0x73, 0x73, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x12, 0x10, 0x0a, - 0x03, 0x54, 0x54, 0x4c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x03, 0x54, 0x54, 0x4c, 0x12, - 0x14, 0x0a, 0x05, 0x52, 0x44, 0x61, 0x74, 0x61, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, - 0x52, 0x44, 0x61, 0x74, 0x61, 0x22, 0xb3, 0x01, 0x0a, 0x0f, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, - 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x38, 0x0a, 0x0b, 0x4e, 0x61, 0x6d, - 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, - 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x4e, 0x61, 0x6d, 0x65, - 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x0b, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, - 0x65, 0x72, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x50, 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x50, 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, 0x12, 0x18, 0x0a, - 0x07, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, - 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x32, 0x0a, 0x14, 0x53, 0x65, 0x61, 0x72, 0x63, - 0x68, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, - 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x14, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x44, 0x6f, 0x6d, - 0x61, 0x69, 0x6e, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x22, 0x48, 0x0a, 0x0a, 0x4e, - 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x50, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x50, 0x12, 0x16, 0x0a, 0x06, 0x4e, 0x53, 0x54, - 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x4e, 0x53, 0x54, 0x79, 0x70, - 0x65, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, - 0x04, 0x50, 0x6f, 0x72, 0x74, 0x22, 0xd9, 0x01, 0x0a, 0x0c, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, - 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x50, 0x65, 0x65, 0x72, 0x49, 0x50, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x50, 0x65, 0x65, 0x72, 0x49, 0x50, 0x12, 0x37, - 0x0a, 0x09, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x0e, 0x32, 0x19, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, - 0x75, 0x6c, 0x65, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x09, 0x44, 0x69, - 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x2e, 0x0a, 0x06, 0x41, 0x63, 0x74, 0x69, 0x6f, - 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, - 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, - 0x06, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x34, 0x0a, 0x08, 0x50, 0x72, 0x6f, 0x74, 0x6f, - 0x63, 0x6f, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x18, 0x2e, 0x6d, 0x61, 0x6e, 0x61, - 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, - 0x63, 0x6f, 0x6c, 0x52, 0x08, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x12, 0x0a, - 0x04, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x50, 0x6f, 0x72, - 0x74, 0x22, 0x38, 0x0a, 0x0e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x41, 0x64, 0x64, 0x72, - 0x65, 0x73, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x6e, 0x65, 0x74, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x05, 0x6e, 0x65, 0x74, 0x49, 0x50, 0x12, 0x10, 0x0a, 0x03, 0x6d, 0x61, 0x63, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6d, 0x61, 0x63, 0x22, 0x1e, 0x0a, 0x06, 0x43, - 0x68, 0x65, 0x63, 0x6b, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x18, 0x01, - 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x22, 0x96, 0x01, 0x0a, 0x08, - 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x14, 0x0a, 0x04, 0x70, 0x6f, 0x72, 0x74, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x48, 0x00, 0x52, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x12, 0x32, - 0x0a, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, - 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x49, - 0x6e, 0x66, 0x6f, 0x2e, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x48, 0x00, 0x52, 0x05, 0x72, 0x61, 0x6e, - 0x67, 0x65, 0x1a, 0x2f, 0x0a, 0x05, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x73, - 0x74, 0x61, 0x72, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05, 0x73, 0x74, 0x61, 0x72, - 0x74, 0x12, 0x10, 0x0a, 0x03, 0x65, 0x6e, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x03, - 0x65, 0x6e, 0x64, 0x42, 0x0f, 0x0a, 0x0d, 0x70, 0x6f, 0x72, 0x74, 0x53, 0x65, 0x6c, 0x65, 0x63, - 0x74, 0x69, 0x6f, 0x6e, 0x22, 0xd1, 0x02, 0x0a, 0x11, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x46, 0x69, - 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x22, 0x0a, 0x0c, 0x73, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, - 0x52, 0x0c, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x12, 0x2e, - 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x16, - 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, - 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x20, - 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x12, 0x34, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x04, 0x20, 0x01, - 0x28, 0x0e, 0x32, 0x18, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, - 0x52, 0x75, 0x6c, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x52, 0x08, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x30, 0x0a, 0x08, 0x70, 0x6f, 0x72, 0x74, 0x49, 0x6e, - 0x66, 0x6f, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, - 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x08, - 0x70, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x1c, 0x0a, 0x09, 0x69, 0x73, 0x44, 0x79, - 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x69, 0x73, 0x44, - 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x12, 0x18, 0x0a, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, - 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, - 0x12, 0x26, 0x0a, 0x0e, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, - 0x6f, 0x6c, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0e, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, - 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x2a, 0x4c, 0x0a, 0x0c, 0x52, 0x75, 0x6c, 0x65, - 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, - 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x41, 0x4c, 0x4c, 0x10, 0x01, 0x12, 0x07, - 0x0a, 0x03, 0x54, 0x43, 0x50, 0x10, 0x02, 0x12, 0x07, 0x0a, 0x03, 0x55, 0x44, 0x50, 0x10, 0x03, - 0x12, 0x08, 0x0a, 0x04, 0x49, 0x43, 0x4d, 0x50, 0x10, 0x04, 0x12, 0x0a, 0x0a, 0x06, 0x43, 0x55, - 0x53, 0x54, 0x4f, 0x4d, 0x10, 0x05, 0x2a, 0x20, 0x0a, 0x0d, 0x52, 0x75, 0x6c, 0x65, 0x44, 0x69, - 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x06, 0x0a, 0x02, 0x49, 0x4e, 0x10, 0x00, 0x12, - 0x07, 0x0a, 0x03, 0x4f, 0x55, 0x54, 0x10, 0x01, 0x2a, 0x22, 0x0a, 0x0a, 0x52, 0x75, 0x6c, 0x65, - 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0a, 0x0a, 0x06, 0x41, 0x43, 0x43, 0x45, 0x50, 0x54, - 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x44, 0x52, 0x4f, 0x50, 0x10, 0x01, 0x32, 0x90, 0x04, 0x0a, - 0x11, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, - 0x63, 0x65, 0x12, 0x45, 0x0a, 0x05, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x1c, 0x2e, 0x6d, 0x61, - 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, - 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, - 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, - 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12, 0x46, 0x0a, 0x04, 0x53, 0x79, 0x6e, - 0x63, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, - 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, - 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, - 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x30, - 0x01, 0x12, 0x42, 0x0a, 0x0c, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4b, 0x65, - 0x79, 0x12, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, - 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1d, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, - 0x74, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x33, 0x0a, 0x09, 0x69, 0x73, 0x48, 0x65, 0x61, 0x6c, 0x74, - 0x68, 0x79, 0x12, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, - 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, - 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x5a, 0x0a, 0x1a, 0x47, 0x65, - 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, - 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, - 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, + 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0xea, 0x02, 0x0a, + 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, + 0x1a, 0x0a, 0x08, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x08, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x44, 0x12, 0x22, 0x0a, 0x0c, 0x43, + 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0c, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x12, + 0x16, 0x0a, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x41, 0x75, 0x64, 0x69, 0x65, + 0x6e, 0x63, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x41, 0x75, 0x64, 0x69, 0x65, + 0x6e, 0x63, 0x65, 0x12, 0x2e, 0x0a, 0x12, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, + 0x68, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x12, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x45, 0x6e, 0x64, 0x70, 0x6f, + 0x69, 0x6e, 0x74, 0x12, 0x24, 0x0a, 0x0d, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x45, 0x6e, 0x64, 0x70, + 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x54, 0x6f, 0x6b, 0x65, + 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x53, 0x63, 0x6f, + 0x70, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x12, + 0x1e, 0x0a, 0x0a, 0x55, 0x73, 0x65, 0x49, 0x44, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x08, 0x20, + 0x01, 0x28, 0x08, 0x52, 0x0a, 0x55, 0x73, 0x65, 0x49, 0x44, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, + 0x34, 0x0a, 0x15, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x15, + 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x64, + 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x22, 0x0a, 0x0c, 0x52, 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, + 0x74, 0x55, 0x52, 0x4c, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0c, 0x52, 0x65, 0x64, + 0x69, 0x72, 0x65, 0x63, 0x74, 0x55, 0x52, 0x4c, 0x73, 0x22, 0xed, 0x01, 0x0a, 0x05, 0x52, 0x6f, + 0x75, 0x74, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x02, 0x49, 0x44, 0x12, 0x18, 0x0a, 0x07, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x12, 0x20, 0x0a, + 0x0b, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x54, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x0b, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x54, 0x79, 0x70, 0x65, 0x12, + 0x12, 0x0a, 0x04, 0x50, 0x65, 0x65, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x50, + 0x65, 0x65, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x18, 0x05, 0x20, + 0x01, 0x28, 0x03, 0x52, 0x06, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x12, 0x1e, 0x0a, 0x0a, 0x4d, + 0x61, 0x73, 0x71, 0x75, 0x65, 0x72, 0x61, 0x64, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, + 0x0a, 0x4d, 0x61, 0x73, 0x71, 0x75, 0x65, 0x72, 0x61, 0x64, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x4e, + 0x65, 0x74, 0x49, 0x44, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x4e, 0x65, 0x74, 0x49, + 0x44, 0x12, 0x18, 0x0a, 0x07, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x08, 0x20, 0x03, + 0x28, 0x09, 0x52, 0x07, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x6b, + 0x65, 0x65, 0x70, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, + 0x6b, 0x65, 0x65, 0x70, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x22, 0xb4, 0x01, 0x0a, 0x09, 0x44, 0x4e, + 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x24, 0x0a, 0x0d, 0x53, 0x65, 0x72, 0x76, 0x69, + 0x63, 0x65, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, + 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x47, 0x0a, + 0x10, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, + 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, + 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, + 0x72, 0x6f, 0x75, 0x70, 0x52, 0x10, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, + 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x12, 0x38, 0x0a, 0x0b, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, + 0x5a, 0x6f, 0x6e, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, + 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a, + 0x6f, 0x6e, 0x65, 0x52, 0x0b, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, 0x73, + 0x22, 0x58, 0x0a, 0x0a, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, 0x12, 0x16, + 0x0a, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, + 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x32, 0x0a, 0x07, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, + 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, + 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x52, 0x65, 0x63, 0x6f, 0x72, + 0x64, 0x52, 0x07, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x22, 0x74, 0x0a, 0x0c, 0x53, 0x69, + 0x6d, 0x70, 0x6c, 0x65, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, + 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x12, + 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x54, 0x79, + 0x70, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x05, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x54, 0x54, 0x4c, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x03, 0x54, 0x54, 0x4c, 0x12, 0x14, 0x0a, 0x05, 0x52, 0x44, + 0x61, 0x74, 0x61, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x52, 0x44, 0x61, 0x74, 0x61, + 0x22, 0xb3, 0x01, 0x0a, 0x0f, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, + 0x72, 0x6f, 0x75, 0x70, 0x12, 0x38, 0x0a, 0x0b, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, + 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, + 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, + 0x72, 0x52, 0x0b, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x12, 0x18, + 0x0a, 0x07, 0x50, 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, + 0x07, 0x50, 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x44, 0x6f, 0x6d, 0x61, + 0x69, 0x6e, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x44, 0x6f, 0x6d, 0x61, 0x69, + 0x6e, 0x73, 0x12, 0x32, 0x0a, 0x14, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x44, 0x6f, 0x6d, 0x61, + 0x69, 0x6e, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, + 0x52, 0x14, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x45, + 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x22, 0x48, 0x0a, 0x0a, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, + 0x72, 0x76, 0x65, 0x72, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x02, 0x49, 0x50, 0x12, 0x16, 0x0a, 0x06, 0x4e, 0x53, 0x54, 0x79, 0x70, 0x65, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x4e, 0x53, 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, + 0x50, 0x6f, 0x72, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x50, 0x6f, 0x72, 0x74, + 0x22, 0xd9, 0x01, 0x0a, 0x0c, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, + 0x65, 0x12, 0x16, 0x0a, 0x06, 0x50, 0x65, 0x65, 0x72, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x06, 0x50, 0x65, 0x65, 0x72, 0x49, 0x50, 0x12, 0x37, 0x0a, 0x09, 0x44, 0x69, 0x72, + 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x19, 0x2e, 0x6d, + 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x44, 0x69, + 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x09, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, + 0x6f, 0x6e, 0x12, 0x2e, 0x0a, 0x06, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x0e, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, + 0x52, 0x75, 0x6c, 0x65, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x41, 0x63, 0x74, 0x69, + 0x6f, 0x6e, 0x12, 0x34, 0x0a, 0x08, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x04, + 0x20, 0x01, 0x28, 0x0e, 0x32, 0x18, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, + 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x52, 0x08, + 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x6f, 0x72, 0x74, + 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x22, 0x38, 0x0a, 0x0e, + 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x14, + 0x0a, 0x05, 0x6e, 0x65, 0x74, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6e, + 0x65, 0x74, 0x49, 0x50, 0x12, 0x10, 0x0a, 0x03, 0x6d, 0x61, 0x63, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x03, 0x6d, 0x61, 0x63, 0x22, 0x1e, 0x0a, 0x06, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, + 0x12, 0x14, 0x0a, 0x05, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, + 0x05, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x22, 0x96, 0x01, 0x0a, 0x08, 0x50, 0x6f, 0x72, 0x74, 0x49, + 0x6e, 0x66, 0x6f, 0x12, 0x14, 0x0a, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0d, 0x48, 0x00, 0x52, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x12, 0x32, 0x0a, 0x05, 0x72, 0x61, 0x6e, + 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, + 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x2e, 0x52, + 0x61, 0x6e, 0x67, 0x65, 0x48, 0x00, 0x52, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x1a, 0x2f, 0x0a, + 0x05, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x12, 0x10, 0x0a, 0x03, + 0x65, 0x6e, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x03, 0x65, 0x6e, 0x64, 0x42, 0x0f, + 0x0a, 0x0d, 0x70, 0x6f, 0x72, 0x74, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, + 0xd1, 0x02, 0x0a, 0x11, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, + 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x22, 0x0a, 0x0c, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, + 0x61, 0x6e, 0x67, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0c, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x12, 0x2e, 0x0a, 0x06, 0x61, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, + 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x41, 0x63, 0x74, 0x69, 0x6f, + 0x6e, 0x52, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, + 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, + 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x34, 0x0a, 0x08, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x18, 0x2e, + 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x50, + 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, + 0x6c, 0x12, 0x30, 0x0a, 0x08, 0x70, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x18, 0x05, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, + 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x08, 0x70, 0x6f, 0x72, 0x74, 0x49, + 0x6e, 0x66, 0x6f, 0x12, 0x1c, 0x0a, 0x09, 0x69, 0x73, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, + 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x69, 0x73, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, + 0x63, 0x12, 0x18, 0x0a, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x07, 0x20, 0x03, + 0x28, 0x09, 0x52, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x26, 0x0a, 0x0e, 0x63, + 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x08, 0x20, + 0x01, 0x28, 0x0d, 0x52, 0x0e, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x50, 0x72, 0x6f, 0x74, 0x6f, + 0x63, 0x6f, 0x6c, 0x2a, 0x4c, 0x0a, 0x0c, 0x52, 0x75, 0x6c, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, + 0x63, 0x6f, 0x6c, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, + 0x12, 0x07, 0x0a, 0x03, 0x41, 0x4c, 0x4c, 0x10, 0x01, 0x12, 0x07, 0x0a, 0x03, 0x54, 0x43, 0x50, + 0x10, 0x02, 0x12, 0x07, 0x0a, 0x03, 0x55, 0x44, 0x50, 0x10, 0x03, 0x12, 0x08, 0x0a, 0x04, 0x49, + 0x43, 0x4d, 0x50, 0x10, 0x04, 0x12, 0x0a, 0x0a, 0x06, 0x43, 0x55, 0x53, 0x54, 0x4f, 0x4d, 0x10, + 0x05, 0x2a, 0x20, 0x0a, 0x0d, 0x52, 0x75, 0x6c, 0x65, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, + 0x6f, 0x6e, 0x12, 0x06, 0x0a, 0x02, 0x49, 0x4e, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x4f, 0x55, + 0x54, 0x10, 0x01, 0x2a, 0x22, 0x0a, 0x0a, 0x52, 0x75, 0x6c, 0x65, 0x41, 0x63, 0x74, 0x69, 0x6f, + 0x6e, 0x12, 0x0a, 0x0a, 0x06, 0x41, 0x43, 0x43, 0x45, 0x50, 0x54, 0x10, 0x00, 0x12, 0x08, 0x0a, + 0x04, 0x44, 0x52, 0x4f, 0x50, 0x10, 0x01, 0x32, 0x90, 0x04, 0x0a, 0x11, 0x4d, 0x61, 0x6e, 0x61, + 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x45, 0x0a, + 0x05, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, - 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12, 0x58, 0x0a, 0x18, 0x47, 0x65, 0x74, 0x50, 0x4b, 0x43, - 0x45, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, - 0x6f, 0x77, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, - 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, - 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, - 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, - 0x12, 0x3d, 0x0a, 0x08, 0x53, 0x79, 0x6e, 0x63, 0x4d, 0x65, 0x74, 0x61, 0x12, 0x1c, 0x2e, 0x6d, + 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, + 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, + 0x67, 0x65, 0x22, 0x00, 0x12, 0x46, 0x0a, 0x04, 0x53, 0x79, 0x6e, 0x63, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, - 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x11, 0x2e, 0x6d, 0x61, 0x6e, - 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x42, - 0x08, 0x5a, 0x06, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x33, + 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, + 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, + 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x30, 0x01, 0x12, 0x42, 0x0a, 0x0c, + 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x12, 0x11, 0x2e, 0x6d, + 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, + 0x1d, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x65, 0x72, + 0x76, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, + 0x12, 0x33, 0x0a, 0x09, 0x69, 0x73, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x79, 0x12, 0x11, 0x2e, + 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, + 0x1a, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, + 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x5a, 0x0a, 0x1a, 0x47, 0x65, 0x74, 0x44, 0x65, 0x76, 0x69, + 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, + 0x6c, 0x6f, 0x77, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, + 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, + 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, + 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, + 0x00, 0x12, 0x58, 0x0a, 0x18, 0x47, 0x65, 0x74, 0x50, 0x4b, 0x43, 0x45, 0x41, 0x75, 0x74, 0x68, + 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x12, 0x1c, 0x2e, + 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, + 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, + 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, + 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12, 0x3d, 0x0a, 0x08, 0x53, + 0x79, 0x6e, 0x63, 0x4d, 0x65, 0x74, 0x61, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, + 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, + 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, + 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x42, 0x08, 0x5a, 0x06, 0x2f, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -3381,7 +3506,7 @@ func file_management_proto_rawDescGZIP() []byte { } var file_management_proto_enumTypes = make([]protoimpl.EnumInfo, 5) -var file_management_proto_msgTypes = make([]protoimpl.MessageInfo, 37) +var file_management_proto_msgTypes = make([]protoimpl.MessageInfo, 38) var file_management_proto_goTypes = []interface{}{ (RuleProtocol)(0), // 0: management.RuleProtocol (RuleDirection)(0), // 1: management.RuleDirection @@ -3396,102 +3521,104 @@ var file_management_proto_goTypes = []interface{}{ (*PeerKeys)(nil), // 10: management.PeerKeys (*Environment)(nil), // 11: management.Environment (*File)(nil), // 12: management.File - (*PeerSystemMeta)(nil), // 13: management.PeerSystemMeta - (*LoginResponse)(nil), // 14: management.LoginResponse - (*ServerKeyResponse)(nil), // 15: management.ServerKeyResponse - (*Empty)(nil), // 16: management.Empty - (*WiretrusteeConfig)(nil), // 17: management.WiretrusteeConfig - (*HostConfig)(nil), // 18: management.HostConfig - (*RelayConfig)(nil), // 19: management.RelayConfig - (*ProtectedHostConfig)(nil), // 20: management.ProtectedHostConfig - (*PeerConfig)(nil), // 21: management.PeerConfig - (*NetworkMap)(nil), // 22: management.NetworkMap - (*RemotePeerConfig)(nil), // 23: management.RemotePeerConfig - (*SSHConfig)(nil), // 24: management.SSHConfig - (*DeviceAuthorizationFlowRequest)(nil), // 25: management.DeviceAuthorizationFlowRequest - (*DeviceAuthorizationFlow)(nil), // 26: management.DeviceAuthorizationFlow - (*PKCEAuthorizationFlowRequest)(nil), // 27: management.PKCEAuthorizationFlowRequest - (*PKCEAuthorizationFlow)(nil), // 28: management.PKCEAuthorizationFlow - (*ProviderConfig)(nil), // 29: management.ProviderConfig - (*Route)(nil), // 30: management.Route - (*DNSConfig)(nil), // 31: management.DNSConfig - (*CustomZone)(nil), // 32: management.CustomZone - (*SimpleRecord)(nil), // 33: management.SimpleRecord - (*NameServerGroup)(nil), // 34: management.NameServerGroup - (*NameServer)(nil), // 35: management.NameServer - (*FirewallRule)(nil), // 36: management.FirewallRule - (*NetworkAddress)(nil), // 37: management.NetworkAddress - (*Checks)(nil), // 38: management.Checks - (*PortInfo)(nil), // 39: management.PortInfo - (*RouteFirewallRule)(nil), // 40: management.RouteFirewallRule - (*PortInfo_Range)(nil), // 41: management.PortInfo.Range - (*timestamppb.Timestamp)(nil), // 42: google.protobuf.Timestamp + (*Flags)(nil), // 13: management.Flags + (*PeerSystemMeta)(nil), // 14: management.PeerSystemMeta + (*LoginResponse)(nil), // 15: management.LoginResponse + (*ServerKeyResponse)(nil), // 16: management.ServerKeyResponse + (*Empty)(nil), // 17: management.Empty + (*WiretrusteeConfig)(nil), // 18: management.WiretrusteeConfig + (*HostConfig)(nil), // 19: management.HostConfig + (*RelayConfig)(nil), // 20: management.RelayConfig + (*ProtectedHostConfig)(nil), // 21: management.ProtectedHostConfig + (*PeerConfig)(nil), // 22: management.PeerConfig + (*NetworkMap)(nil), // 23: management.NetworkMap + (*RemotePeerConfig)(nil), // 24: management.RemotePeerConfig + (*SSHConfig)(nil), // 25: management.SSHConfig + (*DeviceAuthorizationFlowRequest)(nil), // 26: management.DeviceAuthorizationFlowRequest + (*DeviceAuthorizationFlow)(nil), // 27: management.DeviceAuthorizationFlow + (*PKCEAuthorizationFlowRequest)(nil), // 28: management.PKCEAuthorizationFlowRequest + (*PKCEAuthorizationFlow)(nil), // 29: management.PKCEAuthorizationFlow + (*ProviderConfig)(nil), // 30: management.ProviderConfig + (*Route)(nil), // 31: management.Route + (*DNSConfig)(nil), // 32: management.DNSConfig + (*CustomZone)(nil), // 33: management.CustomZone + (*SimpleRecord)(nil), // 34: management.SimpleRecord + (*NameServerGroup)(nil), // 35: management.NameServerGroup + (*NameServer)(nil), // 36: management.NameServer + (*FirewallRule)(nil), // 37: management.FirewallRule + (*NetworkAddress)(nil), // 38: management.NetworkAddress + (*Checks)(nil), // 39: management.Checks + (*PortInfo)(nil), // 40: management.PortInfo + (*RouteFirewallRule)(nil), // 41: management.RouteFirewallRule + (*PortInfo_Range)(nil), // 42: management.PortInfo.Range + (*timestamppb.Timestamp)(nil), // 43: google.protobuf.Timestamp } var file_management_proto_depIdxs = []int32{ - 13, // 0: management.SyncRequest.meta:type_name -> management.PeerSystemMeta - 17, // 1: management.SyncResponse.wiretrusteeConfig:type_name -> management.WiretrusteeConfig - 21, // 2: management.SyncResponse.peerConfig:type_name -> management.PeerConfig - 23, // 3: management.SyncResponse.remotePeers:type_name -> management.RemotePeerConfig - 22, // 4: management.SyncResponse.NetworkMap:type_name -> management.NetworkMap - 38, // 5: management.SyncResponse.Checks:type_name -> management.Checks - 13, // 6: management.SyncMetaRequest.meta:type_name -> management.PeerSystemMeta - 13, // 7: management.LoginRequest.meta:type_name -> management.PeerSystemMeta + 14, // 0: management.SyncRequest.meta:type_name -> management.PeerSystemMeta + 18, // 1: management.SyncResponse.wiretrusteeConfig:type_name -> management.WiretrusteeConfig + 22, // 2: management.SyncResponse.peerConfig:type_name -> management.PeerConfig + 24, // 3: management.SyncResponse.remotePeers:type_name -> management.RemotePeerConfig + 23, // 4: management.SyncResponse.NetworkMap:type_name -> management.NetworkMap + 39, // 5: management.SyncResponse.Checks:type_name -> management.Checks + 14, // 6: management.SyncMetaRequest.meta:type_name -> management.PeerSystemMeta + 14, // 7: management.LoginRequest.meta:type_name -> management.PeerSystemMeta 10, // 8: management.LoginRequest.peerKeys:type_name -> management.PeerKeys - 37, // 9: management.PeerSystemMeta.networkAddresses:type_name -> management.NetworkAddress + 38, // 9: management.PeerSystemMeta.networkAddresses:type_name -> management.NetworkAddress 11, // 10: management.PeerSystemMeta.environment:type_name -> management.Environment 12, // 11: management.PeerSystemMeta.files:type_name -> management.File - 17, // 12: management.LoginResponse.wiretrusteeConfig:type_name -> management.WiretrusteeConfig - 21, // 13: management.LoginResponse.peerConfig:type_name -> management.PeerConfig - 38, // 14: management.LoginResponse.Checks:type_name -> management.Checks - 42, // 15: management.ServerKeyResponse.expiresAt:type_name -> google.protobuf.Timestamp - 18, // 16: management.WiretrusteeConfig.stuns:type_name -> management.HostConfig - 20, // 17: management.WiretrusteeConfig.turns:type_name -> management.ProtectedHostConfig - 18, // 18: management.WiretrusteeConfig.signal:type_name -> management.HostConfig - 19, // 19: management.WiretrusteeConfig.relay:type_name -> management.RelayConfig - 3, // 20: management.HostConfig.protocol:type_name -> management.HostConfig.Protocol - 18, // 21: management.ProtectedHostConfig.hostConfig:type_name -> management.HostConfig - 24, // 22: management.PeerConfig.sshConfig:type_name -> management.SSHConfig - 21, // 23: management.NetworkMap.peerConfig:type_name -> management.PeerConfig - 23, // 24: management.NetworkMap.remotePeers:type_name -> management.RemotePeerConfig - 30, // 25: management.NetworkMap.Routes:type_name -> management.Route - 31, // 26: management.NetworkMap.DNSConfig:type_name -> management.DNSConfig - 23, // 27: management.NetworkMap.offlinePeers:type_name -> management.RemotePeerConfig - 36, // 28: management.NetworkMap.FirewallRules:type_name -> management.FirewallRule - 40, // 29: management.NetworkMap.routesFirewallRules:type_name -> management.RouteFirewallRule - 24, // 30: management.RemotePeerConfig.sshConfig:type_name -> management.SSHConfig - 4, // 31: management.DeviceAuthorizationFlow.Provider:type_name -> management.DeviceAuthorizationFlow.provider - 29, // 32: management.DeviceAuthorizationFlow.ProviderConfig:type_name -> management.ProviderConfig - 29, // 33: management.PKCEAuthorizationFlow.ProviderConfig:type_name -> management.ProviderConfig - 34, // 34: management.DNSConfig.NameServerGroups:type_name -> management.NameServerGroup - 32, // 35: management.DNSConfig.CustomZones:type_name -> management.CustomZone - 33, // 36: management.CustomZone.Records:type_name -> management.SimpleRecord - 35, // 37: management.NameServerGroup.NameServers:type_name -> management.NameServer - 1, // 38: management.FirewallRule.Direction:type_name -> management.RuleDirection - 2, // 39: management.FirewallRule.Action:type_name -> management.RuleAction - 0, // 40: management.FirewallRule.Protocol:type_name -> management.RuleProtocol - 41, // 41: management.PortInfo.range:type_name -> management.PortInfo.Range - 2, // 42: management.RouteFirewallRule.action:type_name -> management.RuleAction - 0, // 43: management.RouteFirewallRule.protocol:type_name -> management.RuleProtocol - 39, // 44: management.RouteFirewallRule.portInfo:type_name -> management.PortInfo - 5, // 45: management.ManagementService.Login:input_type -> management.EncryptedMessage - 5, // 46: management.ManagementService.Sync:input_type -> management.EncryptedMessage - 16, // 47: management.ManagementService.GetServerKey:input_type -> management.Empty - 16, // 48: management.ManagementService.isHealthy:input_type -> management.Empty - 5, // 49: management.ManagementService.GetDeviceAuthorizationFlow:input_type -> management.EncryptedMessage - 5, // 50: management.ManagementService.GetPKCEAuthorizationFlow:input_type -> management.EncryptedMessage - 5, // 51: management.ManagementService.SyncMeta:input_type -> management.EncryptedMessage - 5, // 52: management.ManagementService.Login:output_type -> management.EncryptedMessage - 5, // 53: management.ManagementService.Sync:output_type -> management.EncryptedMessage - 15, // 54: management.ManagementService.GetServerKey:output_type -> management.ServerKeyResponse - 16, // 55: management.ManagementService.isHealthy:output_type -> management.Empty - 5, // 56: management.ManagementService.GetDeviceAuthorizationFlow:output_type -> management.EncryptedMessage - 5, // 57: management.ManagementService.GetPKCEAuthorizationFlow:output_type -> management.EncryptedMessage - 16, // 58: management.ManagementService.SyncMeta:output_type -> management.Empty - 52, // [52:59] is the sub-list for method output_type - 45, // [45:52] is the sub-list for method input_type - 45, // [45:45] is the sub-list for extension type_name - 45, // [45:45] is the sub-list for extension extendee - 0, // [0:45] is the sub-list for field type_name + 13, // 12: management.PeerSystemMeta.flags:type_name -> management.Flags + 18, // 13: management.LoginResponse.wiretrusteeConfig:type_name -> management.WiretrusteeConfig + 22, // 14: management.LoginResponse.peerConfig:type_name -> management.PeerConfig + 39, // 15: management.LoginResponse.Checks:type_name -> management.Checks + 43, // 16: management.ServerKeyResponse.expiresAt:type_name -> google.protobuf.Timestamp + 19, // 17: management.WiretrusteeConfig.stuns:type_name -> management.HostConfig + 21, // 18: management.WiretrusteeConfig.turns:type_name -> management.ProtectedHostConfig + 19, // 19: management.WiretrusteeConfig.signal:type_name -> management.HostConfig + 20, // 20: management.WiretrusteeConfig.relay:type_name -> management.RelayConfig + 3, // 21: management.HostConfig.protocol:type_name -> management.HostConfig.Protocol + 19, // 22: management.ProtectedHostConfig.hostConfig:type_name -> management.HostConfig + 25, // 23: management.PeerConfig.sshConfig:type_name -> management.SSHConfig + 22, // 24: management.NetworkMap.peerConfig:type_name -> management.PeerConfig + 24, // 25: management.NetworkMap.remotePeers:type_name -> management.RemotePeerConfig + 31, // 26: management.NetworkMap.Routes:type_name -> management.Route + 32, // 27: management.NetworkMap.DNSConfig:type_name -> management.DNSConfig + 24, // 28: management.NetworkMap.offlinePeers:type_name -> management.RemotePeerConfig + 37, // 29: management.NetworkMap.FirewallRules:type_name -> management.FirewallRule + 41, // 30: management.NetworkMap.routesFirewallRules:type_name -> management.RouteFirewallRule + 25, // 31: management.RemotePeerConfig.sshConfig:type_name -> management.SSHConfig + 4, // 32: management.DeviceAuthorizationFlow.Provider:type_name -> management.DeviceAuthorizationFlow.provider + 30, // 33: management.DeviceAuthorizationFlow.ProviderConfig:type_name -> management.ProviderConfig + 30, // 34: management.PKCEAuthorizationFlow.ProviderConfig:type_name -> management.ProviderConfig + 35, // 35: management.DNSConfig.NameServerGroups:type_name -> management.NameServerGroup + 33, // 36: management.DNSConfig.CustomZones:type_name -> management.CustomZone + 34, // 37: management.CustomZone.Records:type_name -> management.SimpleRecord + 36, // 38: management.NameServerGroup.NameServers:type_name -> management.NameServer + 1, // 39: management.FirewallRule.Direction:type_name -> management.RuleDirection + 2, // 40: management.FirewallRule.Action:type_name -> management.RuleAction + 0, // 41: management.FirewallRule.Protocol:type_name -> management.RuleProtocol + 42, // 42: management.PortInfo.range:type_name -> management.PortInfo.Range + 2, // 43: management.RouteFirewallRule.action:type_name -> management.RuleAction + 0, // 44: management.RouteFirewallRule.protocol:type_name -> management.RuleProtocol + 40, // 45: management.RouteFirewallRule.portInfo:type_name -> management.PortInfo + 5, // 46: management.ManagementService.Login:input_type -> management.EncryptedMessage + 5, // 47: management.ManagementService.Sync:input_type -> management.EncryptedMessage + 17, // 48: management.ManagementService.GetServerKey:input_type -> management.Empty + 17, // 49: management.ManagementService.isHealthy:input_type -> management.Empty + 5, // 50: management.ManagementService.GetDeviceAuthorizationFlow:input_type -> management.EncryptedMessage + 5, // 51: management.ManagementService.GetPKCEAuthorizationFlow:input_type -> management.EncryptedMessage + 5, // 52: management.ManagementService.SyncMeta:input_type -> management.EncryptedMessage + 5, // 53: management.ManagementService.Login:output_type -> management.EncryptedMessage + 5, // 54: management.ManagementService.Sync:output_type -> management.EncryptedMessage + 16, // 55: management.ManagementService.GetServerKey:output_type -> management.ServerKeyResponse + 17, // 56: management.ManagementService.isHealthy:output_type -> management.Empty + 5, // 57: management.ManagementService.GetDeviceAuthorizationFlow:output_type -> management.EncryptedMessage + 5, // 58: management.ManagementService.GetPKCEAuthorizationFlow:output_type -> management.EncryptedMessage + 17, // 59: management.ManagementService.SyncMeta:output_type -> management.Empty + 53, // [53:60] is the sub-list for method output_type + 46, // [46:53] is the sub-list for method input_type + 46, // [46:46] is the sub-list for extension type_name + 46, // [46:46] is the sub-list for extension extendee + 0, // [0:46] is the sub-list for field type_name } func init() { file_management_proto_init() } @@ -3597,7 +3724,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*PeerSystemMeta); i { + switch v := v.(*Flags); i { case 0: return &v.state case 1: @@ -3609,7 +3736,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*LoginResponse); i { + switch v := v.(*PeerSystemMeta); i { case 0: return &v.state case 1: @@ -3621,7 +3748,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ServerKeyResponse); i { + switch v := v.(*LoginResponse); i { case 0: return &v.state case 1: @@ -3633,7 +3760,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[11].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Empty); i { + switch v := v.(*ServerKeyResponse); i { case 0: return &v.state case 1: @@ -3645,7 +3772,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[12].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*WiretrusteeConfig); i { + switch v := v.(*Empty); i { case 0: return &v.state case 1: @@ -3657,7 +3784,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[13].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*HostConfig); i { + switch v := v.(*WiretrusteeConfig); i { case 0: return &v.state case 1: @@ -3669,7 +3796,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[14].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*RelayConfig); i { + switch v := v.(*HostConfig); i { case 0: return &v.state case 1: @@ -3681,7 +3808,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[15].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ProtectedHostConfig); i { + switch v := v.(*RelayConfig); i { case 0: return &v.state case 1: @@ -3693,7 +3820,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[16].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*PeerConfig); i { + switch v := v.(*ProtectedHostConfig); i { case 0: return &v.state case 1: @@ -3705,7 +3832,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[17].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*NetworkMap); i { + switch v := v.(*PeerConfig); i { case 0: return &v.state case 1: @@ -3717,7 +3844,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[18].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*RemotePeerConfig); i { + switch v := v.(*NetworkMap); i { case 0: return &v.state case 1: @@ -3729,7 +3856,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[19].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*SSHConfig); i { + switch v := v.(*RemotePeerConfig); i { case 0: return &v.state case 1: @@ -3741,7 +3868,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[20].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DeviceAuthorizationFlowRequest); i { + switch v := v.(*SSHConfig); i { case 0: return &v.state case 1: @@ -3753,7 +3880,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[21].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DeviceAuthorizationFlow); i { + switch v := v.(*DeviceAuthorizationFlowRequest); i { case 0: return &v.state case 1: @@ -3765,7 +3892,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[22].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*PKCEAuthorizationFlowRequest); i { + switch v := v.(*DeviceAuthorizationFlow); i { case 0: return &v.state case 1: @@ -3777,7 +3904,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[23].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*PKCEAuthorizationFlow); i { + switch v := v.(*PKCEAuthorizationFlowRequest); i { case 0: return &v.state case 1: @@ -3789,7 +3916,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[24].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ProviderConfig); i { + switch v := v.(*PKCEAuthorizationFlow); i { case 0: return &v.state case 1: @@ -3801,7 +3928,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[25].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Route); i { + switch v := v.(*ProviderConfig); i { case 0: return &v.state case 1: @@ -3813,7 +3940,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[26].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DNSConfig); i { + switch v := v.(*Route); i { case 0: return &v.state case 1: @@ -3825,7 +3952,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[27].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*CustomZone); i { + switch v := v.(*DNSConfig); i { case 0: return &v.state case 1: @@ -3837,7 +3964,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[28].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*SimpleRecord); i { + switch v := v.(*CustomZone); i { case 0: return &v.state case 1: @@ -3849,7 +3976,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[29].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*NameServerGroup); i { + switch v := v.(*SimpleRecord); i { case 0: return &v.state case 1: @@ -3861,7 +3988,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[30].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*NameServer); i { + switch v := v.(*NameServerGroup); i { case 0: return &v.state case 1: @@ -3873,7 +4000,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[31].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*FirewallRule); i { + switch v := v.(*NameServer); i { case 0: return &v.state case 1: @@ -3885,7 +4012,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[32].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*NetworkAddress); i { + switch v := v.(*FirewallRule); i { case 0: return &v.state case 1: @@ -3897,7 +4024,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[33].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Checks); i { + switch v := v.(*NetworkAddress); i { case 0: return &v.state case 1: @@ -3909,7 +4036,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[34].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*PortInfo); i { + switch v := v.(*Checks); i { case 0: return &v.state case 1: @@ -3921,7 +4048,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[35].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*RouteFirewallRule); i { + switch v := v.(*PortInfo); i { case 0: return &v.state case 1: @@ -3933,6 +4060,18 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[36].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RouteFirewallRule); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_management_proto_msgTypes[37].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*PortInfo_Range); i { case 0: return &v.state @@ -3945,7 +4084,7 @@ func file_management_proto_init() { } } } - file_management_proto_msgTypes[34].OneofWrappers = []interface{}{ + file_management_proto_msgTypes[35].OneofWrappers = []interface{}{ (*PortInfo_Port)(nil), (*PortInfo_Range_)(nil), } @@ -3955,7 +4094,7 @@ func file_management_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_management_proto_rawDesc, NumEnums: 5, - NumMessages: 37, + NumMessages: 38, NumExtensions: 0, NumServices: 1, }, diff --git a/management/proto/management.proto b/management/proto/management.proto index 5f4e0df46..2318fc675 100644 --- a/management/proto/management.proto +++ b/management/proto/management.proto @@ -128,6 +128,16 @@ message File { bool processIsRunning = 3; } +message Flags { + bool rosenpassEnabled = 1; + bool rosenpassPermissive = 2; + bool serverSSHAllowed = 3; + bool disableClientRoutes = 4; + bool disableServerRoutes = 5; + bool disableDNS = 6; + bool disableFirewall = 7; +} + // PeerSystemMeta is machine meta data like OS and version. message PeerSystemMeta { string hostname = 1; @@ -146,6 +156,7 @@ message PeerSystemMeta { string sysManufacturer = 14; Environment environment = 15; repeated File files = 16; + Flags flags = 17; } message LoginResponse { From 481bbe8513ee0e67d62570dbb4e1ab5abc389ea3 Mon Sep 17 00:00:00 2001 From: Zoltan Papp Date: Thu, 16 Jan 2025 16:19:07 +0100 Subject: [PATCH 15/92] [relay] Set InitialPacketSize to the maximum allowable value (#3188) Fixes an issue on macOS where the server throws errors with default settings: failed to write transport message to: DATAGRAM frame too large. Further investigation is required to optimize MTU-related values. --- relay/client/dialer/quic/quic.go | 7 ++++--- relay/server/listener/quic/listener.go | 3 ++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/relay/client/dialer/quic/quic.go b/relay/client/dialer/quic/quic.go index 593d1334b..7fd486f87 100644 --- a/relay/client/dialer/quic/quic.go +++ b/relay/client/dialer/quic/quic.go @@ -29,9 +29,10 @@ func (d Dialer) Dial(ctx context.Context, address string) (net.Conn, error) { } quicConfig := &quic.Config{ - KeepAlivePeriod: 30 * time.Second, - MaxIdleTimeout: 4 * time.Minute, - EnableDatagrams: true, + KeepAlivePeriod: 30 * time.Second, + MaxIdleTimeout: 4 * time.Minute, + EnableDatagrams: true, + InitialPacketSize: 1452, } udpConn, err := nbnet.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4zero, Port: 0}) diff --git a/relay/server/listener/quic/listener.go b/relay/server/listener/quic/listener.go index b6e01994f..17a5e8ab6 100644 --- a/relay/server/listener/quic/listener.go +++ b/relay/server/listener/quic/listener.go @@ -25,7 +25,8 @@ func (l *Listener) Listen(acceptFn func(conn net.Conn)) error { l.acceptFn = acceptFn quicCfg := &quic.Config{ - EnableDatagrams: true, + EnableDatagrams: true, + InitialPacketSize: 1452, } listener, err := quic.ListenAddr(l.Address, l.TLSConfig, quicCfg) if err != nil { From 3e9f0d57ac4ef943763f950505ce3bb68bcf61ae Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Thu, 16 Jan 2025 22:19:32 +0100 Subject: [PATCH 16/92] [client] Fix windows info out of bounds panic (#3196) --- client/system/info_windows.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/system/info_windows.go b/client/system/info_windows.go index 28bd3d300..f3f387f28 100644 --- a/client/system/info_windows.go +++ b/client/system/info_windows.go @@ -105,7 +105,7 @@ func getOSNameAndVersion() (string, string) { split := strings.Split(dst[0].Caption, " ") - if len(split) < 3 { + if len(split) <= 3 { return "Windows", getBuildVersion() } From 1b2517ea2004ce84974aa33d62cc9e39b10a6079 Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Fri, 17 Jan 2025 11:39:08 +0100 Subject: [PATCH 17/92] [relay] Don't start relay quic listener on invalid TLS config (#3202) --- relay/server/server.go | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/relay/server/server.go b/relay/server/server.go index cacc3dafb..10aabcace 100644 --- a/relay/server/server.go +++ b/relay/server/server.go @@ -6,6 +6,7 @@ import ( "sync" "github.com/hashicorp/go-multierror" + log "github.com/sirupsen/logrus" "go.opentelemetry.io/otel/metric" nberrors "github.com/netbirdio/netbird/client/errors" @@ -58,16 +59,16 @@ func (r *Server) Listen(cfg ListenerConfig) error { tlsConfigQUIC, err := quictls.ServerQUICTLSConfig(cfg.TLSConfig) if err != nil { - return err - } + log.Warnf("Not starting QUIC listener: %v", err) + } else { + quicListener := &quic.Listener{ + Address: cfg.Address, + TLSConfig: tlsConfigQUIC, + } - quicListener := &quic.Listener{ - Address: cfg.Address, - TLSConfig: tlsConfigQUIC, + r.listeners = append(r.listeners, quicListener) } - r.listeners = append(r.listeners, quicListener) - errChan := make(chan error, len(r.listeners)) wg := sync.WaitGroup{} for _, l := range r.listeners { From c01874e9ce8d2f0d1c5c1bd4d8443014d7c331ea Mon Sep 17 00:00:00 2001 From: Bethuel Mmbaga Date: Fri, 17 Jan 2025 14:00:46 +0300 Subject: [PATCH 18/92] [management] Fix network migration issue in postgres (#3198) Signed-off-by: bcmmbaga --- management/server/migration/migration.go | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/management/server/migration/migration.go b/management/server/migration/migration.go index 8986d77b5..d7abbad47 100644 --- a/management/server/migration/migration.go +++ b/management/server/migration/migration.go @@ -330,10 +330,7 @@ func MigrateNewField[T any](ctx context.Context, db *gorm.DB, columnName string, } var rows []map[string]any - if err := tx.Table(tableName). - Select("id", columnName). - Where(columnName + " IS NULL OR " + columnName + " = ''"). - Find(&rows).Error; err != nil { + if err := tx.Table(tableName).Select("id", columnName).Where(columnName + " IS NULL").Find(&rows).Error; err != nil { return fmt.Errorf("failed to find rows with empty %s: %w", columnName, err) } From 3e836db1d1bc29d13ff8e9f5df1e7f04a0bffb17 Mon Sep 17 00:00:00 2001 From: Pascal Fischer <32096965+pascal-fischer@users.noreply.github.com> Date: Fri, 17 Jan 2025 12:26:44 +0100 Subject: [PATCH 19/92] [management] add duration logs to Sync (#3203) --- management/server/account.go | 5 +++++ management/server/grpcserver.go | 2 ++ management/server/peer.go | 13 ++++++++++++- 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/management/server/account.go b/management/server/account.go index 41da7f079..eeb8b2fb8 100644 --- a/management/server/account.go +++ b/management/server/account.go @@ -1549,6 +1549,11 @@ func domainIsUpToDate(domain string, domainCategory string, claims jwtclaims.Aut } func (am *DefaultAccountManager) SyncAndMarkPeer(ctx context.Context, accountID string, peerPubKey string, meta nbpeer.PeerSystemMeta, realIP net.IP) (*nbpeer.Peer, *types.NetworkMap, []*posture.Checks, error) { + start := time.Now() + defer func() { + log.WithContext(ctx).Debugf("SyncAndMarkPeer: took %v", time.Since(start)) + }() + accountUnlock := am.Store.AcquireReadLockByUID(ctx, accountID) defer accountUnlock() peerUnlock := am.Store.AcquireWriteLockByUID(ctx, peerPubKey) diff --git a/management/server/grpcserver.go b/management/server/grpcserver.go index daa23d2ab..a21dcd5b8 100644 --- a/management/server/grpcserver.go +++ b/management/server/grpcserver.go @@ -208,6 +208,8 @@ func (s *GRPCServer) Sync(req *proto.EncryptedMessage, srv proto.ManagementServi unlock() unlock = nil + log.WithContext(ctx).Debugf("Sync: took %v", time.Since(reqStart)) + return s.handleUpdates(ctx, accountID, peerKey, peer, updates, srv) } diff --git a/management/server/peer.go b/management/server/peer.go index bfa20bae2..57b38ce81 100644 --- a/management/server/peer.go +++ b/management/server/peer.go @@ -11,10 +11,11 @@ import ( "sync" "time" - "github.com/netbirdio/netbird/management/server/util" "github.com/rs/xid" log "github.com/sirupsen/logrus" + "github.com/netbirdio/netbird/management/server/util" + "github.com/netbirdio/netbird/management/server/idp" "github.com/netbirdio/netbird/management/server/posture" "github.com/netbirdio/netbird/management/server/store" @@ -111,6 +112,11 @@ func (am *DefaultAccountManager) GetPeers(ctx context.Context, accountID, userID // MarkPeerConnected marks peer as connected (true) or disconnected (false) func (am *DefaultAccountManager) MarkPeerConnected(ctx context.Context, peerPubKey string, connected bool, realIP net.IP, account *types.Account) error { + start := time.Now() + defer func() { + log.WithContext(ctx).Debugf("MarkPeerConnected: took %v", time.Since(start)) + }() + peer, err := account.FindPeerByPubKey(peerPubKey) if err != nil { return fmt.Errorf("failed to find peer by pub key: %w", err) @@ -654,6 +660,11 @@ func (am *DefaultAccountManager) getFreeIP(ctx context.Context, s store.Store, a // SyncPeer checks whether peer is eligible for receiving NetworkMap (authenticated) and returns its NetworkMap if eligible func (am *DefaultAccountManager) SyncPeer(ctx context.Context, sync PeerSync, account *types.Account) (*nbpeer.Peer, *types.NetworkMap, []*posture.Checks, error) { + start := time.Now() + defer func() { + log.WithContext(ctx).Debugf("SyncPeer: took %v", time.Since(start)) + }() + peer, err := account.FindPeerByPubKey(sync.WireGuardPubKey) if err != nil { return nil, nil, nil, status.NewPeerNotRegisteredError() From 9f4db0a953bd956d2042804a62e09e8e1dd4a190 Mon Sep 17 00:00:00 2001 From: Maycon Santos Date: Sat, 18 Jan 2025 00:18:59 +0100 Subject: [PATCH 20/92] [client] Close ice agent only if not nil (#3210) --- client/internal/peer/worker_ice.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/client/internal/peer/worker_ice.go b/client/internal/peer/worker_ice.go index 4cdd18ff1..008318492 100644 --- a/client/internal/peer/worker_ice.go +++ b/client/internal/peer/worker_ice.go @@ -255,6 +255,10 @@ func (w *WorkerICE) closeAgent(cancel context.CancelFunc) { defer w.muxAgent.Unlock() cancel() + if w.agent == nil { + return + } + if err := w.agent.Close(); err != nil { w.log.Warnf("failed to close ICE agent: %s", err) } From c619bf5b0cec2269e08ce1a9c7daf2554d343252 Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Mon, 20 Jan 2025 11:02:09 +0100 Subject: [PATCH 21/92] [client] Allow freebsd to build netbird-ui (#3212) --- .github/workflows/golang-test-freebsd.yml | 2 +- client/ui/client_ui.go | 4 ++-- client/ui/font_bsd.go | 24 +++++++++++++---------- client/ui/font_darwin.go | 18 +++++++++++++++++ client/ui/network.go | 2 +- 5 files changed, 36 insertions(+), 14 deletions(-) create mode 100644 client/ui/font_darwin.go diff --git a/.github/workflows/golang-test-freebsd.yml b/.github/workflows/golang-test-freebsd.yml index a2d743715..7a2d3cf3c 100644 --- a/.github/workflows/golang-test-freebsd.yml +++ b/.github/workflows/golang-test-freebsd.yml @@ -24,7 +24,7 @@ jobs: copyback: false release: "14.1" prepare: | - pkg install -y go + pkg install -y go pkgconf xorg # -x - to print all executed commands # -e - to faile on first error diff --git a/client/ui/client_ui.go b/client/ui/client_ui.go index 49b0f53cf..f22ee377b 100644 --- a/client/ui/client_ui.go +++ b/client/ui/client_ui.go @@ -1,4 +1,4 @@ -//go:build !(linux && 386) && !freebsd +//go:build !(linux && 386) package main @@ -876,7 +876,7 @@ func openURL(url string) error { err = exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start() case "darwin": err = exec.Command("open", url).Start() - case "linux": + case "linux", "freebsd": err = exec.Command("xdg-open", url).Start() default: err = fmt.Errorf("unsupported platform") diff --git a/client/ui/font_bsd.go b/client/ui/font_bsd.go index 84cb5993d..139f38f40 100644 --- a/client/ui/font_bsd.go +++ b/client/ui/font_bsd.go @@ -1,4 +1,4 @@ -//go:build darwin +//go:build freebsd || openbsd || netbsd || dragonfly package main @@ -9,18 +9,22 @@ import ( log "github.com/sirupsen/logrus" ) -const defaultFontPath = "/Library/Fonts/Arial Unicode.ttf" - func (s *serviceClient) setDefaultFonts() { - // TODO: add other bsd paths - if runtime.GOOS != "darwin" { - return + paths := []string{ + "/usr/local/share/fonts/TTF/DejaVuSans.ttf", + "/usr/local/share/fonts/dejavu/DejaVuSans.ttf", + "/usr/local/share/noto/NotoSans-Regular.ttf", + "/usr/local/share/fonts/noto/NotoSans-Regular.ttf", + "/usr/local/share/fonts/liberation-fonts-ttf/LiberationSans-Regular.ttf", } - if _, err := os.Stat(defaultFontPath); err != nil { - log.Errorf("Failed to find default font file: %v", err) - return + for _, fontPath := range paths { + if _, err := os.Stat(fontPath); err == nil { + os.Setenv("FYNE_FONT", fontPath) + log.Debugf("Using font: %s", fontPath) + return + } } - os.Setenv("FYNE_FONT", defaultFontPath) + log.Errorf("Failed to find any suitable font files for %s", runtime.GOOS) } diff --git a/client/ui/font_darwin.go b/client/ui/font_darwin.go new file mode 100644 index 000000000..cafb72f59 --- /dev/null +++ b/client/ui/font_darwin.go @@ -0,0 +1,18 @@ +package main + +import ( + "os" + + log "github.com/sirupsen/logrus" +) + +const defaultFontPath = "/Library/Fonts/Arial Unicode.ttf" + +func (s *serviceClient) setDefaultFonts() { + if _, err := os.Stat(defaultFontPath); err != nil { + log.Errorf("Failed to find default font file: %v", err) + return + } + + os.Setenv("FYNE_FONT", defaultFontPath) +} diff --git a/client/ui/network.go b/client/ui/network.go index e6f027f0e..852c4765b 100644 --- a/client/ui/network.go +++ b/client/ui/network.go @@ -1,4 +1,4 @@ -//go:build !(linux && 386) && !freebsd +//go:build !(linux && 386) package main From 1ad2cb55827afdb324c34ce78370f5c6a1046dfa Mon Sep 17 00:00:00 2001 From: Bethuel Mmbaga Date: Mon, 20 Jan 2025 20:41:46 +0300 Subject: [PATCH 22/92] [management] Refactor peers to use store methods (#2893) --- .github/workflows/golang-test-linux.yml | 6 +- go.mod | 2 +- go.sum | 4 +- management/server/account.go | 108 +- management/server/account_test.go | 43 +- management/server/ephemeral.go | 49 +- management/server/ephemeral_test.go | 18 +- management/server/groups/manager.go | 59 +- .../server/http/handlers/networks/handler.go | 4 +- .../handlers/networks/resources_handler.go | 25 +- .../http/handlers/peers/peers_handler.go | 72 +- .../http/handlers/peers/peers_handler_test.go | 141 ++- .../peers_handler_benchmark_test.go | 62 +- .../setupkeys_handler_benchmark_test.go | 68 +- .../users_handler_benchmark_test.go | 28 +- management/server/integrated_validator.go | 30 +- .../server/integrated_validator/interface.go | 2 +- management/server/management_proto_test.go | 2 +- management/server/mock_server/account_mock.go | 24 +- management/server/peer.go | 1014 +++++++++++------ management/server/peer/peer.go | 2 +- management/server/peer_test.go | 2 +- management/server/status/error.go | 5 + management/server/store/sql_store.go | 176 ++- management/server/store/sql_store_test.go | 444 ++++++-- management/server/store/store.go | 21 +- .../server/testdata/store_policy_migrate.sql | 1 + .../testdata/store_with_expired_peers.sql | 9 +- management/server/testdata/storev1.sql | 10 +- management/server/user.go | 40 +- 30 files changed, 1614 insertions(+), 857 deletions(-) diff --git a/.github/workflows/golang-test-linux.yml b/.github/workflows/golang-test-linux.yml index ba5f66746..a4a3da66c 100644 --- a/.github/workflows/golang-test-linux.yml +++ b/.github/workflows/golang-test-linux.yml @@ -262,7 +262,7 @@ jobs: fail-fast: false matrix: arch: [ '386','amd64' ] - store: [ 'sqlite', 'postgres', 'mysql' ] + store: [ 'sqlite', 'postgres' ] runs-on: ubuntu-22.04 steps: - name: Install Go @@ -314,7 +314,7 @@ jobs: run: docker pull mlsmaycon/warmed-mysql:8 - name: Test - run: CGO_ENABLED=1 GOARCH=${{ matrix.arch }} NETBIRD_STORE_ENGINE=${{ matrix.store }} CI=true go test -tags=benchmark -run=^$ -bench=. -exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE' -timeout 10m $(go list ./... | grep /management) + run: CGO_ENABLED=1 GOARCH=${{ matrix.arch }} NETBIRD_STORE_ENGINE=${{ matrix.store }} CI=true go test -run=^$ -tags=benchmark -bench=. -exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE' -timeout 30m $(go list -tags=benchmark ./... | grep /management) api_integration_test: needs: [ build-cache ] @@ -363,7 +363,7 @@ jobs: run: git --no-pager diff --exit-code - name: Test - run: CGO_ENABLED=1 GOARCH=${{ matrix.arch }} NETBIRD_STORE_ENGINE=${{ matrix.store }} CI=true go test -p 1 -exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE' -timeout 10m -tags=integration $(go list ./... | grep /management) + run: CGO_ENABLED=1 GOARCH=${{ matrix.arch }} NETBIRD_STORE_ENGINE=${{ matrix.store }} CI=true go test -tags=integration -p 1 -exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE' -timeout 30m $(go list -tags=integration ./... | grep /management) test_client_on_docker: needs: [ build-cache ] diff --git a/go.mod b/go.mod index 88bcada07..fa573bb9c 100644 --- a/go.mod +++ b/go.mod @@ -60,7 +60,7 @@ require ( github.com/miekg/dns v1.1.59 github.com/mitchellh/hashstructure/v2 v2.0.2 github.com/nadoo/ipset v0.5.0 - github.com/netbirdio/management-integrations/integrations v0.0.0-20241211172827-ba0a446be480 + github.com/netbirdio/management-integrations/integrations v0.0.0-20250115083837-a09722b8d2a6 github.com/netbirdio/signal-dispatcher/dispatcher v0.0.0-20241010133937-e0df50df217d github.com/okta/okta-sdk-golang/v2 v2.18.0 github.com/oschwald/maxminddb-golang v1.12.0 diff --git a/go.sum b/go.sum index 8ba94dd6a..a099498fb 100644 --- a/go.sum +++ b/go.sum @@ -527,8 +527,8 @@ github.com/netbirdio/go-netroute v0.0.0-20240611143515-f59b0e1d3944 h1:TDtJKmM6S github.com/netbirdio/go-netroute v0.0.0-20240611143515-f59b0e1d3944/go.mod h1:sHA6TRxjQ6RLbnI+3R4DZo2Eseg/iKiPRfNmcuNySVQ= github.com/netbirdio/ice/v3 v3.0.0-20240315174635-e72a50fcb64e h1:PURA50S8u4mF6RrkYYCAvvPCixhqqEiEy3Ej6avh04c= github.com/netbirdio/ice/v3 v3.0.0-20240315174635-e72a50fcb64e/go.mod h1:YMLU7qbKfVjmEv7EoZPIVEI+kNYxWCdPK3VS0BU+U4Q= -github.com/netbirdio/management-integrations/integrations v0.0.0-20241211172827-ba0a446be480 h1:M+UPn/o+plVE7ZehgL6/1dftptsO1tyTPssgImgi+28= -github.com/netbirdio/management-integrations/integrations v0.0.0-20241211172827-ba0a446be480/go.mod h1:RC0PnyATSBPrRWKQgb+7KcC1tMta9eYyzuA414RG9wQ= +github.com/netbirdio/management-integrations/integrations v0.0.0-20250115083837-a09722b8d2a6 h1:I/ODkZ8rSDOzlJbhEjD2luSI71zl+s5JgNvFHY0+mBU= +github.com/netbirdio/management-integrations/integrations v0.0.0-20250115083837-a09722b8d2a6/go.mod h1:izUUs1NT7ja+PwSX3kJ7ox8Kkn478tboBJSjL4kU6J0= github.com/netbirdio/service v0.0.0-20240911161631-f62744f42502 h1:3tHlFmhTdX9axERMVN63dqyFqnvuD+EMJHzM7mNGON8= github.com/netbirdio/service v0.0.0-20240911161631-f62744f42502/go.mod h1:CIMRFEJVL+0DS1a3Nx06NaMn4Dz63Ng6O7dl0qH0zVM= github.com/netbirdio/signal-dispatcher/dispatcher v0.0.0-20241010133937-e0df50df217d h1:bRq5TKgC7Iq20pDiuC54yXaWnAVeS5PdGpSokFTlR28= diff --git a/management/server/account.go b/management/server/account.go index eeb8b2fb8..2c62a2453 100644 --- a/management/server/account.go +++ b/management/server/account.go @@ -45,6 +45,7 @@ import ( const ( CacheExpirationMax = 7 * 24 * 3600 * time.Second // 7 days CacheExpirationMin = 3 * 24 * 3600 * time.Second // 3 days + peerSchedulerRetryInterval = 3 * time.Second emptyUserID = "empty user ID in claims" errorGettingDomainAccIDFmt = "error getting account ID by private domain: %v" ) @@ -85,7 +86,7 @@ type AccountManager interface { GetUser(ctx context.Context, claims jwtclaims.AuthorizationClaims) (*types.User, error) ListUsers(ctx context.Context, accountID string) ([]*types.User, error) GetPeers(ctx context.Context, accountID, userID string) ([]*nbpeer.Peer, error) - MarkPeerConnected(ctx context.Context, peerKey string, connected bool, realIP net.IP, account *types.Account) error + MarkPeerConnected(ctx context.Context, peerKey string, connected bool, realIP net.IP, accountID string) error DeletePeer(ctx context.Context, accountID, peerID, userID string) error UpdatePeer(ctx context.Context, accountID, userID string, peer *nbpeer.Peer) (*nbpeer.Peer, error) GetNetworkMap(ctx context.Context, peerID string) (*types.NetworkMap, error) @@ -105,6 +106,7 @@ type AccountManager interface { DeleteGroups(ctx context.Context, accountId, userId string, groupIDs []string) error GroupAddPeer(ctx context.Context, accountId, groupID, peerID string) error GroupDeletePeer(ctx context.Context, accountId, groupID, peerID string) error + GetPeerGroups(ctx context.Context, accountID, peerID string) ([]*types.Group, error) GetPolicy(ctx context.Context, accountID, policyID, userID string) (*types.Policy, error) SavePolicy(ctx context.Context, accountID, userID string, policy *types.Policy) (*types.Policy, error) DeletePolicy(ctx context.Context, accountID, policyID, userID string) error @@ -126,8 +128,8 @@ type AccountManager interface { SaveDNSSettings(ctx context.Context, accountID string, userID string, dnsSettingsToSave *types.DNSSettings) error GetPeer(ctx context.Context, accountID, peerID, userID string) (*nbpeer.Peer, error) UpdateAccountSettings(ctx context.Context, accountID, userID string, newSettings *types.Settings) (*types.Account, error) - LoginPeer(ctx context.Context, login PeerLogin) (*nbpeer.Peer, *types.NetworkMap, []*posture.Checks, error) // used by peer gRPC API - SyncPeer(ctx context.Context, sync PeerSync, account *types.Account) (*nbpeer.Peer, *types.NetworkMap, []*posture.Checks, error) // used by peer gRPC API + LoginPeer(ctx context.Context, login PeerLogin) (*nbpeer.Peer, *types.NetworkMap, []*posture.Checks, error) // used by peer gRPC API + SyncPeer(ctx context.Context, sync PeerSync, accountID string) (*nbpeer.Peer, *types.NetworkMap, []*posture.Checks, error) // used by peer gRPC API GetAllConnectedPeers() (map[string]struct{}, error) HasConnectedChannel(peerID string) bool GetExternalCacheManager() ExternalCacheManager @@ -138,7 +140,7 @@ type AccountManager interface { GetIdpManager() idp.Manager UpdateIntegratedValidatorGroups(ctx context.Context, accountID string, userID string, groups []string) error GroupValidation(ctx context.Context, accountId string, groups []string) (bool, error) - GetValidatedPeers(account *types.Account) (map[string]struct{}, error) + GetValidatedPeers(ctx context.Context, accountID string) (map[string]struct{}, error) SyncAndMarkPeer(ctx context.Context, accountID string, peerPubKey string, meta nbpeer.PeerSystemMeta, realIP net.IP) (*nbpeer.Peer, *types.NetworkMap, []*posture.Checks, error) OnPeerDisconnected(ctx context.Context, accountID string, peerPubKey string) error SyncPeerMeta(ctx context.Context, peerPubKey string, meta nbpeer.PeerSystemMeta) error @@ -379,14 +381,14 @@ func (am *DefaultAccountManager) UpdateAccountSettings(ctx context.Context, acco event = activity.AccountPeerLoginExpirationDisabled am.peerLoginExpiry.Cancel(ctx, []string{accountID}) } else { - am.checkAndSchedulePeerLoginExpiration(ctx, account) + am.checkAndSchedulePeerLoginExpiration(ctx, accountID) } am.StoreEvent(ctx, userID, accountID, accountID, event, nil) } if oldSettings.PeerLoginExpiration != newSettings.PeerLoginExpiration { am.StoreEvent(ctx, userID, accountID, accountID, activity.AccountPeerLoginExpirationDurationUpdated, nil) - am.checkAndSchedulePeerLoginExpiration(ctx, account) + am.checkAndSchedulePeerLoginExpiration(ctx, accountID) } updateAccountPeers := false @@ -400,7 +402,7 @@ func (am *DefaultAccountManager) UpdateAccountSettings(ctx context.Context, acco account.Network.Serial++ } - err = am.handleInactivityExpirationSettings(ctx, account, oldSettings, newSettings, userID, accountID) + err = am.handleInactivityExpirationSettings(ctx, oldSettings, newSettings, userID, accountID) if err != nil { return nil, err } @@ -437,13 +439,13 @@ func (am *DefaultAccountManager) handleGroupsPropagationSettings(ctx context.Con return nil } -func (am *DefaultAccountManager) handleInactivityExpirationSettings(ctx context.Context, account *types.Account, oldSettings, newSettings *types.Settings, userID, accountID string) error { +func (am *DefaultAccountManager) handleInactivityExpirationSettings(ctx context.Context, oldSettings, newSettings *types.Settings, userID, accountID string) error { if newSettings.PeerInactivityExpirationEnabled { if oldSettings.PeerInactivityExpiration != newSettings.PeerInactivityExpiration { oldSettings.PeerInactivityExpiration = newSettings.PeerInactivityExpiration am.StoreEvent(ctx, userID, accountID, accountID, activity.AccountPeerInactivityExpirationDurationUpdated, nil) - am.checkAndSchedulePeerInactivityExpiration(ctx, account) + am.checkAndSchedulePeerInactivityExpiration(ctx, accountID) } } else { if oldSettings.PeerInactivityExpirationEnabled != newSettings.PeerInactivityExpirationEnabled { @@ -452,7 +454,7 @@ func (am *DefaultAccountManager) handleInactivityExpirationSettings(ctx context. event = activity.AccountPeerInactivityExpirationDisabled am.peerInactivityExpiry.Cancel(ctx, []string{accountID}) } else { - am.checkAndSchedulePeerInactivityExpiration(ctx, account) + am.checkAndSchedulePeerInactivityExpiration(ctx, accountID) } am.StoreEvent(ctx, userID, accountID, accountID, event, nil) } @@ -466,33 +468,31 @@ func (am *DefaultAccountManager) peerLoginExpirationJob(ctx context.Context, acc unlock := am.Store.AcquireWriteLockByUID(ctx, accountID) defer unlock() - account, err := am.Store.GetAccount(ctx, accountID) + expiredPeers, err := am.getExpiredPeers(ctx, accountID) if err != nil { - log.WithContext(ctx).Errorf("failed getting account %s expiring peers", accountID) - return account.GetNextPeerExpiration() + return peerSchedulerRetryInterval, true } - expiredPeers := account.GetExpiredPeers() var peerIDs []string for _, peer := range expiredPeers { peerIDs = append(peerIDs, peer.ID) } - log.WithContext(ctx).Debugf("discovered %d peers to expire for account %s", len(peerIDs), account.Id) + log.WithContext(ctx).Debugf("discovered %d peers to expire for account %s", len(peerIDs), accountID) - if err := am.expireAndUpdatePeers(ctx, account, expiredPeers); err != nil { - log.WithContext(ctx).Errorf("failed updating account peers while expiring peers for account %s", account.Id) - return account.GetNextPeerExpiration() + if err := am.expireAndUpdatePeers(ctx, accountID, expiredPeers); err != nil { + log.WithContext(ctx).Errorf("failed updating account peers while expiring peers for account %s", accountID) + return peerSchedulerRetryInterval, true } - return account.GetNextPeerExpiration() + return am.getNextPeerExpiration(ctx, accountID) } } -func (am *DefaultAccountManager) checkAndSchedulePeerLoginExpiration(ctx context.Context, account *types.Account) { - am.peerLoginExpiry.Cancel(ctx, []string{account.Id}) - if nextRun, ok := account.GetNextPeerExpiration(); ok { - go am.peerLoginExpiry.Schedule(ctx, nextRun, account.Id, am.peerLoginExpirationJob(ctx, account.Id)) +func (am *DefaultAccountManager) checkAndSchedulePeerLoginExpiration(ctx context.Context, accountID string) { + am.peerLoginExpiry.Cancel(ctx, []string{accountID}) + if nextRun, ok := am.getNextPeerExpiration(ctx, accountID); ok { + go am.peerLoginExpiry.Schedule(ctx, nextRun, accountID, am.peerLoginExpirationJob(ctx, accountID)) } } @@ -502,34 +502,33 @@ func (am *DefaultAccountManager) peerInactivityExpirationJob(ctx context.Context unlock := am.Store.AcquireWriteLockByUID(ctx, accountID) defer unlock() - account, err := am.Store.GetAccount(ctx, accountID) + inactivePeers, err := am.getInactivePeers(ctx, accountID) if err != nil { - log.Errorf("failed getting account %s expiring peers", accountID) - return account.GetNextInactivePeerExpiration() + log.WithContext(ctx).Errorf("failed getting inactive peers for account %s", accountID) + return peerSchedulerRetryInterval, true } - expiredPeers := account.GetInactivePeers() var peerIDs []string - for _, peer := range expiredPeers { + for _, peer := range inactivePeers { peerIDs = append(peerIDs, peer.ID) } - log.Debugf("discovered %d peers to expire for account %s", len(peerIDs), account.Id) + log.Debugf("discovered %d peers to expire for account %s", len(peerIDs), accountID) - if err := am.expireAndUpdatePeers(ctx, account, expiredPeers); err != nil { - log.Errorf("failed updating account peers while expiring peers for account %s", account.Id) - return account.GetNextInactivePeerExpiration() + if err := am.expireAndUpdatePeers(ctx, accountID, inactivePeers); err != nil { + log.Errorf("failed updating account peers while expiring peers for account %s", accountID) + return peerSchedulerRetryInterval, true } - return account.GetNextInactivePeerExpiration() + return am.getNextInactivePeerExpiration(ctx, accountID) } } // checkAndSchedulePeerInactivityExpiration periodically checks for inactive peers to end their sessions -func (am *DefaultAccountManager) checkAndSchedulePeerInactivityExpiration(ctx context.Context, account *types.Account) { - am.peerInactivityExpiry.Cancel(ctx, []string{account.Id}) - if nextRun, ok := account.GetNextInactivePeerExpiration(); ok { - go am.peerInactivityExpiry.Schedule(ctx, nextRun, account.Id, am.peerInactivityExpirationJob(ctx, account.Id)) +func (am *DefaultAccountManager) checkAndSchedulePeerInactivityExpiration(ctx context.Context, accountID string) { + am.peerInactivityExpiry.Cancel(ctx, []string{accountID}) + if nextRun, ok := am.getNextInactivePeerExpiration(ctx, accountID); ok { + go am.peerInactivityExpiry.Schedule(ctx, nextRun, accountID, am.peerInactivityExpirationJob(ctx, accountID)) } } @@ -665,7 +664,7 @@ func (am *DefaultAccountManager) GetAccountIDByUserID(ctx context.Context, userI return "", status.Errorf(status.NotFound, "no valid userID provided") } - accountID, err := am.Store.GetAccountIDByUserID(userID) + accountID, err := am.Store.GetAccountIDByUserID(ctx, store.LockingStrengthShare, userID) if err != nil { if s, ok := status.FromError(err); ok && s.Type() == status.NotFound { account, err := am.GetOrCreateAccountByUser(ctx, userID, domain) @@ -1450,7 +1449,7 @@ func (am *DefaultAccountManager) getAccountIDWithAuthorizationClaims(ctx context return "", err } - userAccountID, err := am.Store.GetAccountIDByUserID(claims.UserId) + userAccountID, err := am.Store.GetAccountIDByUserID(ctx, store.LockingStrengthShare, claims.UserId) if handleNotFound(err) != nil { log.WithContext(ctx).Errorf("error getting account ID by user ID: %v", err) return "", err @@ -1497,7 +1496,7 @@ func (am *DefaultAccountManager) getPrivateDomainWithGlobalLock(ctx context.Cont } func (am *DefaultAccountManager) handlePrivateAccountWithIDFromClaim(ctx context.Context, claims jwtclaims.AuthorizationClaims) (string, error) { - userAccountID, err := am.Store.GetAccountIDByUserID(claims.UserId) + userAccountID, err := am.Store.GetAccountIDByUserID(ctx, store.LockingStrengthShare, claims.UserId) if err != nil { log.WithContext(ctx).Errorf("error getting account ID by user ID: %v", err) return "", err @@ -1559,17 +1558,12 @@ func (am *DefaultAccountManager) SyncAndMarkPeer(ctx context.Context, accountID peerUnlock := am.Store.AcquireWriteLockByUID(ctx, peerPubKey) defer peerUnlock() - account, err := am.Store.GetAccount(ctx, accountID) - if err != nil { - return nil, nil, nil, status.NewGetAccountError(err) - } - - peer, netMap, postureChecks, err := am.SyncPeer(ctx, PeerSync{WireGuardPubKey: peerPubKey, Meta: meta}, account) + peer, netMap, postureChecks, err := am.SyncPeer(ctx, PeerSync{WireGuardPubKey: peerPubKey, Meta: meta}, accountID) if err != nil { return nil, nil, nil, fmt.Errorf("error syncing peer: %w", err) } - err = am.MarkPeerConnected(ctx, peerPubKey, true, realIP, account) + err = am.MarkPeerConnected(ctx, peerPubKey, true, realIP, accountID) if err != nil { log.WithContext(ctx).Warnf("failed marking peer as connected %s %v", peerPubKey, err) } @@ -1583,12 +1577,7 @@ func (am *DefaultAccountManager) OnPeerDisconnected(ctx context.Context, account peerUnlock := am.Store.AcquireWriteLockByUID(ctx, peerPubKey) defer peerUnlock() - account, err := am.Store.GetAccount(ctx, accountID) - if err != nil { - return status.NewGetAccountError(err) - } - - err = am.MarkPeerConnected(ctx, peerPubKey, false, nil, account) + err := am.MarkPeerConnected(ctx, peerPubKey, false, nil, accountID) if err != nil { log.WithContext(ctx).Warnf("failed marking peer as disconnected %s %v", peerPubKey, err) } @@ -1609,12 +1598,7 @@ func (am *DefaultAccountManager) SyncPeerMeta(ctx context.Context, peerPubKey st unlockPeer := am.Store.AcquireWriteLockByUID(ctx, peerPubKey) defer unlockPeer() - account, err := am.Store.GetAccount(ctx, accountID) - if err != nil { - return err - } - - _, _, _, err = am.SyncPeer(ctx, PeerSync{WireGuardPubKey: peerPubKey, Meta: meta, UpdateAccountPeers: true}, account) + _, _, _, err = am.SyncPeer(ctx, PeerSync{WireGuardPubKey: peerPubKey, Meta: meta, UpdateAccountPeers: true}, accountID) if err != nil { return mapError(ctx, err) } @@ -1683,8 +1667,8 @@ func (am *DefaultAccountManager) GetAccountIDForPeerKey(ctx context.Context, pee return am.Store.GetAccountIDByPeerPubKey(ctx, peerKey) } -func (am *DefaultAccountManager) handleUserPeer(ctx context.Context, peer *nbpeer.Peer, settings *types.Settings) (bool, error) { - user, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, peer.UserID) +func (am *DefaultAccountManager) handleUserPeer(ctx context.Context, transaction store.Store, peer *nbpeer.Peer, settings *types.Settings) (bool, error) { + user, err := transaction.GetUserByUserID(ctx, store.LockingStrengthShare, peer.UserID) if err != nil { return false, err } @@ -1695,7 +1679,7 @@ func (am *DefaultAccountManager) handleUserPeer(ctx context.Context, peer *nbpee } if peerLoginExpired(ctx, peer, settings) { - err = am.handleExpiredPeer(ctx, user, peer) + err = am.handleExpiredPeer(ctx, transaction, user, peer) if err != nil { return false, err } diff --git a/management/server/account_test.go b/management/server/account_test.go index e4f079507..57bc0c757 100644 --- a/management/server/account_test.go +++ b/management/server/account_test.go @@ -1450,7 +1450,6 @@ func TestAccountManager_DeletePeer(t *testing.T) { return } - userID := "account_creator" account, err := createAccount(manager, "test_account", userID, "netbird.cloud") if err != nil { t.Fatal(err) @@ -1479,7 +1478,7 @@ func TestAccountManager_DeletePeer(t *testing.T) { return } - err = manager.DeletePeer(context.Background(), account.Id, peerKey, userID) + err = manager.DeletePeer(context.Background(), account.Id, peer.ID, userID) if err != nil { return } @@ -1501,7 +1500,7 @@ func TestAccountManager_DeletePeer(t *testing.T) { assert.Equal(t, peer.Name, ev.Meta["name"]) assert.Equal(t, peer.FQDN(account.Domain), ev.Meta["fqdn"]) assert.Equal(t, userID, ev.InitiatorID) - assert.Equal(t, peer.IP.String(), ev.TargetID) + assert.Equal(t, peer.ID, ev.TargetID) assert.Equal(t, peer.IP.String(), fmt.Sprint(ev.Meta["ip"])) } @@ -1855,13 +1854,10 @@ func TestDefaultAccountManager_UpdatePeer_PeerLoginExpiration(t *testing.T) { accountID, err := manager.GetAccountIDByUserID(context.Background(), userID, "") require.NoError(t, err, "unable to get the account") - account, err := manager.Store.GetAccount(context.Background(), accountID) - require.NoError(t, err, "unable to get the account") - - err = manager.MarkPeerConnected(context.Background(), key.PublicKey().String(), true, nil, account) + err = manager.MarkPeerConnected(context.Background(), key.PublicKey().String(), true, nil, accountID) require.NoError(t, err, "unable to mark peer connected") - account, err = manager.UpdateAccountSettings(context.Background(), accountID, userID, &types.Settings{ + account, err := manager.UpdateAccountSettings(context.Background(), accountID, userID, &types.Settings{ PeerLoginExpiration: time.Hour, PeerLoginExpirationEnabled: true, }) @@ -1929,11 +1925,8 @@ func TestDefaultAccountManager_MarkPeerConnected_PeerLoginExpiration(t *testing. accountID, err = manager.GetAccountIDByUserID(context.Background(), userID, "") require.NoError(t, err, "unable to get the account") - account, err := manager.Store.GetAccount(context.Background(), accountID) - require.NoError(t, err, "unable to get the account") - // when we mark peer as connected, the peer login expiration routine should trigger - err = manager.MarkPeerConnected(context.Background(), key.PublicKey().String(), true, nil, account) + err = manager.MarkPeerConnected(context.Background(), key.PublicKey().String(), true, nil, accountID) require.NoError(t, err, "unable to mark peer connected") failed := waitTimeout(wg, time.Second) @@ -1964,7 +1957,7 @@ func TestDefaultAccountManager_UpdateAccountSettings_PeerLoginExpiration(t *test account, err := manager.Store.GetAccount(context.Background(), accountID) require.NoError(t, err, "unable to get the account") - err = manager.MarkPeerConnected(context.Background(), key.PublicKey().String(), true, nil, account) + err = manager.MarkPeerConnected(context.Background(), key.PublicKey().String(), true, nil, accountID) require.NoError(t, err, "unable to mark peer connected") wg := &sync.WaitGroup{} @@ -3089,12 +3082,12 @@ func BenchmarkLoginPeer_ExistingPeer(b *testing.B) { minMsPerOpCICD float64 maxMsPerOpCICD float64 }{ - {"Small", 50, 5, 102, 110, 102, 130}, - {"Medium", 500, 100, 105, 140, 105, 190}, - {"Large", 5000, 200, 160, 200, 160, 320}, - {"Small single", 50, 10, 102, 110, 102, 130}, - {"Medium single", 500, 10, 105, 140, 105, 190}, - {"Large 5", 5000, 15, 160, 200, 160, 290}, + {"Small", 50, 5, 102, 110, 3, 20}, + {"Medium", 500, 100, 105, 140, 20, 110}, + {"Large", 5000, 200, 160, 200, 120, 260}, + {"Small single", 50, 10, 102, 110, 5, 40}, + {"Medium single", 500, 10, 105, 140, 10, 60}, + {"Large 5", 5000, 15, 160, 200, 60, 180}, } log.SetOutput(io.Discard) @@ -3163,12 +3156,12 @@ func BenchmarkLoginPeer_NewPeer(b *testing.B) { minMsPerOpCICD float64 maxMsPerOpCICD float64 }{ - {"Small", 50, 5, 107, 120, 107, 160}, - {"Medium", 500, 100, 105, 140, 105, 220}, - {"Large", 5000, 200, 180, 220, 180, 395}, - {"Small single", 50, 10, 107, 120, 105, 160}, - {"Medium single", 500, 10, 105, 140, 105, 170}, - {"Large 5", 5000, 15, 180, 220, 180, 340}, + {"Small", 50, 5, 107, 120, 10, 80}, + {"Medium", 500, 100, 105, 140, 30, 140}, + {"Large", 5000, 200, 180, 220, 140, 300}, + {"Small single", 50, 10, 107, 120, 10, 80}, + {"Medium single", 500, 10, 105, 140, 20, 60}, + {"Large 5", 5000, 15, 180, 220, 80, 200}, } log.SetOutput(io.Discard) diff --git a/management/server/ephemeral.go b/management/server/ephemeral.go index 3c629a0db..3d6d01434 100644 --- a/management/server/ephemeral.go +++ b/management/server/ephemeral.go @@ -10,7 +10,6 @@ import ( "github.com/netbirdio/netbird/management/server/activity" nbpeer "github.com/netbirdio/netbird/management/server/peer" "github.com/netbirdio/netbird/management/server/store" - "github.com/netbirdio/netbird/management/server/types" ) const ( @@ -22,10 +21,10 @@ var ( ) type ephemeralPeer struct { - id string - account *types.Account - deadline time.Time - next *ephemeralPeer + id string + accountID string + deadline time.Time + next *ephemeralPeer } // todo: consider to remove peer from ephemeral list when the peer has been deleted via API. If we do not do it @@ -106,12 +105,6 @@ func (e *EphemeralManager) OnPeerDisconnected(ctx context.Context, peer *nbpeer. log.WithContext(ctx).Tracef("add peer to ephemeral list: %s", peer.ID) - a, err := e.store.GetAccountByPeerID(context.Background(), peer.ID) - if err != nil { - log.WithContext(ctx).Errorf("failed to add peer to ephemeral list: %s", err) - return - } - e.peersLock.Lock() defer e.peersLock.Unlock() @@ -119,7 +112,7 @@ func (e *EphemeralManager) OnPeerDisconnected(ctx context.Context, peer *nbpeer. return } - e.addPeer(peer.ID, a, newDeadLine()) + e.addPeer(peer.AccountID, peer.ID, newDeadLine()) if e.timer == nil { e.timer = time.AfterFunc(e.headPeer.deadline.Sub(timeNow()), func() { e.cleanup(ctx) @@ -128,18 +121,18 @@ func (e *EphemeralManager) OnPeerDisconnected(ctx context.Context, peer *nbpeer. } func (e *EphemeralManager) loadEphemeralPeers(ctx context.Context) { - accounts := e.store.GetAllAccounts(context.Background()) - t := newDeadLine() - count := 0 - for _, a := range accounts { - for id, p := range a.Peers { - if p.Ephemeral { - count++ - e.addPeer(id, a, t) - } - } + peers, err := e.store.GetAllEphemeralPeers(ctx, store.LockingStrengthShare) + if err != nil { + log.WithContext(ctx).Debugf("failed to load ephemeral peers: %s", err) + return } - log.WithContext(ctx).Debugf("loaded ephemeral peer(s): %d", count) + + t := newDeadLine() + for _, p := range peers { + e.addPeer(p.AccountID, p.ID, t) + } + + log.WithContext(ctx).Debugf("loaded ephemeral peer(s): %d", len(peers)) } func (e *EphemeralManager) cleanup(ctx context.Context) { @@ -172,18 +165,18 @@ func (e *EphemeralManager) cleanup(ctx context.Context) { for id, p := range deletePeers { log.WithContext(ctx).Debugf("delete ephemeral peer: %s", id) - err := e.accountManager.DeletePeer(ctx, p.account.Id, id, activity.SystemInitiator) + err := e.accountManager.DeletePeer(ctx, p.accountID, id, activity.SystemInitiator) if err != nil { log.WithContext(ctx).Errorf("failed to delete ephemeral peer: %s", err) } } } -func (e *EphemeralManager) addPeer(id string, account *types.Account, deadline time.Time) { +func (e *EphemeralManager) addPeer(accountID string, peerID string, deadline time.Time) { ep := &ephemeralPeer{ - id: id, - account: account, - deadline: deadline, + id: peerID, + accountID: accountID, + deadline: deadline, } if e.headPeer == nil { diff --git a/management/server/ephemeral_test.go b/management/server/ephemeral_test.go index ac8372440..df8fe98c3 100644 --- a/management/server/ephemeral_test.go +++ b/management/server/ephemeral_test.go @@ -7,7 +7,6 @@ import ( "time" nbpeer "github.com/netbirdio/netbird/management/server/peer" - "github.com/netbirdio/netbird/management/server/status" "github.com/netbirdio/netbird/management/server/store" "github.com/netbirdio/netbird/management/server/types" ) @@ -17,17 +16,14 @@ type MockStore struct { account *types.Account } -func (s *MockStore) GetAllAccounts(_ context.Context) []*types.Account { - return []*types.Account{s.account} -} - -func (s *MockStore) GetAccountByPeerID(_ context.Context, peerId string) (*types.Account, error) { - _, ok := s.account.Peers[peerId] - if ok { - return s.account, nil +func (s *MockStore) GetAllEphemeralPeers(_ context.Context, _ store.LockingStrength) ([]*nbpeer.Peer, error) { + var peers []*nbpeer.Peer + for _, v := range s.account.Peers { + if v.Ephemeral { + peers = append(peers, v) + } } - - return nil, status.NewPeerNotFoundError(peerId) + return peers, nil } type MocAccountManager struct { diff --git a/management/server/groups/manager.go b/management/server/groups/manager.go index f5abb212e..cfc7ee57b 100644 --- a/management/server/groups/manager.go +++ b/management/server/groups/manager.go @@ -13,7 +13,8 @@ import ( ) type Manager interface { - GetAllGroups(ctx context.Context, accountID, userID string) (map[string]*types.Group, error) + GetAllGroups(ctx context.Context, accountID, userID string) ([]*types.Group, error) + GetAllGroupsMap(ctx context.Context, accountID, userID string) (map[string]*types.Group, error) GetResourceGroupsInTransaction(ctx context.Context, transaction store.Store, lockingStrength store.LockingStrength, accountID, resourceID string) ([]*types.Group, error) AddResourceToGroup(ctx context.Context, accountID, userID, groupID string, resourceID *types.Resource) error AddResourceToGroupInTransaction(ctx context.Context, transaction store.Store, accountID, userID, groupID string, resourceID *types.Resource) (func(), error) @@ -37,7 +38,7 @@ func NewManager(store store.Store, permissionsManager permissions.Manager, accou } } -func (m *managerImpl) GetAllGroups(ctx context.Context, accountID, userID string) (map[string]*types.Group, error) { +func (m *managerImpl) GetAllGroups(ctx context.Context, accountID, userID string) ([]*types.Group, error) { ok, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, permissions.Groups, permissions.Read) if err != nil { return nil, err @@ -51,6 +52,15 @@ func (m *managerImpl) GetAllGroups(ctx context.Context, accountID, userID string return nil, fmt.Errorf("error getting account groups: %w", err) } + return groups, nil +} + +func (m *managerImpl) GetAllGroupsMap(ctx context.Context, accountID, userID string) (map[string]*types.Group, error) { + groups, err := m.GetAllGroups(ctx, accountID, userID) + if err != nil { + return nil, err + } + groupsMap := make(map[string]*types.Group) for _, group := range groups { groupsMap[group.ID] = group @@ -130,44 +140,43 @@ func (m *managerImpl) GetResourceGroupsInTransaction(ctx context.Context, transa return transaction.GetResourceGroups(ctx, lockingStrength, accountID, resourceID) } -func ToGroupsInfo(groups map[string]*types.Group, id string) []api.GroupMinimum { - groupsInfo := []api.GroupMinimum{} - groupsChecked := make(map[string]struct{}) +func ToGroupsInfoMap(groups []*types.Group, idCount int) map[string][]api.GroupMinimum { + groupsInfoMap := make(map[string][]api.GroupMinimum, idCount) + groupsChecked := make(map[string]struct{}, len(groups)) // not sure why this is needed (left over from old implementation) for _, group := range groups { _, ok := groupsChecked[group.ID] if ok { continue } + groupsChecked[group.ID] = struct{}{} for _, pk := range group.Peers { - if pk == id { - info := api.GroupMinimum{ - Id: group.ID, - Name: group.Name, - PeersCount: len(group.Peers), - ResourcesCount: len(group.Resources), - } - groupsInfo = append(groupsInfo, info) - break + info := api.GroupMinimum{ + Id: group.ID, + Name: group.Name, + PeersCount: len(group.Peers), + ResourcesCount: len(group.Resources), } + groupsInfoMap[pk] = append(groupsInfoMap[pk], info) } for _, rk := range group.Resources { - if rk.ID == id { - info := api.GroupMinimum{ - Id: group.ID, - Name: group.Name, - PeersCount: len(group.Peers), - ResourcesCount: len(group.Resources), - } - groupsInfo = append(groupsInfo, info) - break + info := api.GroupMinimum{ + Id: group.ID, + Name: group.Name, + PeersCount: len(group.Peers), + ResourcesCount: len(group.Resources), } + groupsInfoMap[rk.ID] = append(groupsInfoMap[rk.ID], info) } } - return groupsInfo + return groupsInfoMap } -func (m *mockManager) GetAllGroups(ctx context.Context, accountID, userID string) (map[string]*types.Group, error) { +func (m *mockManager) GetAllGroups(ctx context.Context, accountID, userID string) ([]*types.Group, error) { + return []*types.Group{}, nil +} + +func (m *mockManager) GetAllGroupsMap(ctx context.Context, accountID, userID string) (map[string]*types.Group, error) { return map[string]*types.Group{}, nil } diff --git a/management/server/http/handlers/networks/handler.go b/management/server/http/handlers/networks/handler.go index 6b36a8fce..316b93611 100644 --- a/management/server/http/handlers/networks/handler.go +++ b/management/server/http/handlers/networks/handler.go @@ -82,7 +82,7 @@ func (h *handler) getAllNetworks(w http.ResponseWriter, r *http.Request) { return } - groups, err := h.groupsManager.GetAllGroups(r.Context(), accountID, userID) + groups, err := h.groupsManager.GetAllGroupsMap(r.Context(), accountID, userID) if err != nil { util.WriteError(r.Context(), err, w) return @@ -267,7 +267,7 @@ func (h *handler) collectIDsInNetwork(ctx context.Context, accountID, userID, ne return nil, nil, 0, fmt.Errorf("failed to get routers in network: %w", err) } - groups, err := h.groupsManager.GetAllGroups(ctx, accountID, userID) + groups, err := h.groupsManager.GetAllGroupsMap(ctx, accountID, userID) if err != nil { return nil, nil, 0, fmt.Errorf("failed to get groups: %w", err) } diff --git a/management/server/http/handlers/networks/resources_handler.go b/management/server/http/handlers/networks/resources_handler.go index 6499bd652..f2dc8e3b8 100644 --- a/management/server/http/handlers/networks/resources_handler.go +++ b/management/server/http/handlers/networks/resources_handler.go @@ -66,10 +66,11 @@ func (h *resourceHandler) getAllResourcesInNetwork(w http.ResponseWriter, r *htt return } + grpsInfoMap := groups.ToGroupsInfoMap(grps, len(resources)) + var resourcesResponse []*api.NetworkResource for _, resource := range resources { - groupMinimumInfo := groups.ToGroupsInfo(grps, resource.ID) - resourcesResponse = append(resourcesResponse, resource.ToAPIResponse(groupMinimumInfo)) + resourcesResponse = append(resourcesResponse, resource.ToAPIResponse(grpsInfoMap[resource.ID])) } util.WriteJSONObject(r.Context(), w, resourcesResponse) @@ -94,10 +95,11 @@ func (h *resourceHandler) getAllResourcesInAccount(w http.ResponseWriter, r *htt return } + grpsInfoMap := groups.ToGroupsInfoMap(grps, 0) + var resourcesResponse []*api.NetworkResource for _, resource := range resources { - groupMinimumInfo := groups.ToGroupsInfo(grps, resource.ID) - resourcesResponse = append(resourcesResponse, resource.ToAPIResponse(groupMinimumInfo)) + resourcesResponse = append(resourcesResponse, resource.ToAPIResponse(grpsInfoMap[resource.ID])) } util.WriteJSONObject(r.Context(), w, resourcesResponse) @@ -136,8 +138,9 @@ func (h *resourceHandler) createResource(w http.ResponseWriter, r *http.Request) return } - groupMinimumInfo := groups.ToGroupsInfo(grps, resource.ID) - util.WriteJSONObject(r.Context(), w, resource.ToAPIResponse(groupMinimumInfo)) + grpsInfoMap := groups.ToGroupsInfoMap(grps, 0) + + util.WriteJSONObject(r.Context(), w, resource.ToAPIResponse(grpsInfoMap[resource.ID])) } func (h *resourceHandler) getResource(w http.ResponseWriter, r *http.Request) { @@ -162,8 +165,9 @@ func (h *resourceHandler) getResource(w http.ResponseWriter, r *http.Request) { return } - groupMinimumInfo := groups.ToGroupsInfo(grps, resource.ID) - util.WriteJSONObject(r.Context(), w, resource.ToAPIResponse(groupMinimumInfo)) + grpsInfoMap := groups.ToGroupsInfoMap(grps, 0) + + util.WriteJSONObject(r.Context(), w, resource.ToAPIResponse(grpsInfoMap[resource.ID])) } func (h *resourceHandler) updateResource(w http.ResponseWriter, r *http.Request) { @@ -199,8 +203,9 @@ func (h *resourceHandler) updateResource(w http.ResponseWriter, r *http.Request) return } - groupMinimumInfo := groups.ToGroupsInfo(grps, resource.ID) - util.WriteJSONObject(r.Context(), w, resource.ToAPIResponse(groupMinimumInfo)) + grpsInfoMap := groups.ToGroupsInfoMap(grps, 0) + + util.WriteJSONObject(r.Context(), w, resource.ToAPIResponse(grpsInfoMap[resource.ID])) } func (h *resourceHandler) deleteResource(w http.ResponseWriter, r *http.Request) { diff --git a/management/server/http/handlers/peers/peers_handler.go b/management/server/http/handlers/peers/peers_handler.go index 7eb8e2153..cdd8026f2 100644 --- a/management/server/http/handlers/peers/peers_handler.go +++ b/management/server/http/handlers/peers/peers_handler.go @@ -58,8 +58,8 @@ func (h *Handler) checkPeerStatus(peer *nbpeer.Peer) (*nbpeer.Peer, error) { return peerToReturn, nil } -func (h *Handler) getPeer(ctx context.Context, account *types.Account, peerID, userID string, w http.ResponseWriter) { - peer, err := h.accountManager.GetPeer(ctx, account.Id, peerID, userID) +func (h *Handler) getPeer(ctx context.Context, accountID, peerID, userID string, w http.ResponseWriter) { + peer, err := h.accountManager.GetPeer(ctx, accountID, peerID, userID) if err != nil { util.WriteError(ctx, err, w) return @@ -72,20 +72,21 @@ func (h *Handler) getPeer(ctx context.Context, account *types.Account, peerID, u } dnsDomain := h.accountManager.GetDNSDomain() - groupsInfo := groups.ToGroupsInfo(account.Groups, peer.ID) + grps, _ := h.accountManager.GetPeerGroups(ctx, accountID, peerID) + grpsInfoMap := groups.ToGroupsInfoMap(grps, 0) - validPeers, err := h.accountManager.GetValidatedPeers(account) + validPeers, err := h.accountManager.GetValidatedPeers(ctx, accountID) if err != nil { - log.WithContext(ctx).Errorf("failed to list appreoved peers: %v", err) + log.WithContext(ctx).Errorf("failed to list approved peers: %v", err) util.WriteError(ctx, fmt.Errorf("internal error"), w) return } _, valid := validPeers[peer.ID] - util.WriteJSONObject(ctx, w, toSinglePeerResponse(peerToReturn, groupsInfo, dnsDomain, valid)) + util.WriteJSONObject(ctx, w, toSinglePeerResponse(peerToReturn, grpsInfoMap[peerID], dnsDomain, valid)) } -func (h *Handler) updatePeer(ctx context.Context, account *types.Account, userID, peerID string, w http.ResponseWriter, r *http.Request) { +func (h *Handler) updatePeer(ctx context.Context, accountID, userID, peerID string, w http.ResponseWriter, r *http.Request) { req := &api.PeerRequest{} err := json.NewDecoder(r.Body).Decode(&req) if err != nil { @@ -109,16 +110,22 @@ func (h *Handler) updatePeer(ctx context.Context, account *types.Account, userID } } - peer, err := h.accountManager.UpdatePeer(ctx, account.Id, userID, update) + peer, err := h.accountManager.UpdatePeer(ctx, accountID, userID, update) if err != nil { util.WriteError(ctx, err, w) return } dnsDomain := h.accountManager.GetDNSDomain() - groupMinimumInfo := groups.ToGroupsInfo(account.Groups, peer.ID) + peerGroups, err := h.accountManager.GetPeerGroups(ctx, accountID, peer.ID) + if err != nil { + util.WriteError(ctx, err, w) + return + } - validPeers, err := h.accountManager.GetValidatedPeers(account) + grpsInfoMap := groups.ToGroupsInfoMap(peerGroups, 0) + + validPeers, err := h.accountManager.GetValidatedPeers(ctx, accountID) if err != nil { log.WithContext(ctx).Errorf("failed to list appreoved peers: %v", err) util.WriteError(ctx, fmt.Errorf("internal error"), w) @@ -127,7 +134,7 @@ func (h *Handler) updatePeer(ctx context.Context, account *types.Account, userID _, valid := validPeers[peer.ID] - util.WriteJSONObject(r.Context(), w, toSinglePeerResponse(peer, groupMinimumInfo, dnsDomain, valid)) + util.WriteJSONObject(r.Context(), w, toSinglePeerResponse(peer, grpsInfoMap[peerID], dnsDomain, valid)) } func (h *Handler) deletePeer(ctx context.Context, accountID, userID string, peerID string, w http.ResponseWriter) { @@ -159,18 +166,11 @@ func (h *Handler) HandlePeer(w http.ResponseWriter, r *http.Request) { case http.MethodDelete: h.deletePeer(r.Context(), accountID, userID, peerID, w) return - case http.MethodGet, http.MethodPut: - account, err := h.accountManager.GetAccountByID(r.Context(), accountID, userID) - if err != nil { - util.WriteError(r.Context(), err, w) - return - } - - if r.Method == http.MethodGet { - h.getPeer(r.Context(), account, peerID, userID, w) - } else { - h.updatePeer(r.Context(), account, userID, peerID, w, r) - } + case http.MethodGet: + h.getPeer(r.Context(), accountID, peerID, userID, w) + return + case http.MethodPut: + h.updatePeer(r.Context(), accountID, userID, peerID, w, r) return default: util.WriteError(r.Context(), status.Errorf(status.NotFound, "unknown METHOD"), w) @@ -186,7 +186,7 @@ func (h *Handler) GetAllPeers(w http.ResponseWriter, r *http.Request) { return } - account, err := h.accountManager.GetAccountByID(r.Context(), accountID, userID) + peers, err := h.accountManager.GetPeers(r.Context(), accountID, userID) if err != nil { util.WriteError(r.Context(), err, w) return @@ -194,18 +194,9 @@ func (h *Handler) GetAllPeers(w http.ResponseWriter, r *http.Request) { dnsDomain := h.accountManager.GetDNSDomain() - peers, err := h.accountManager.GetPeers(r.Context(), accountID, userID) - if err != nil { - util.WriteError(r.Context(), err, w) - return - } - - groupsMap := map[string]*types.Group{} grps, _ := h.accountManager.GetAllGroups(r.Context(), accountID, userID) - for _, group := range grps { - groupsMap[group.ID] = group - } + grpsInfoMap := groups.ToGroupsInfoMap(grps, len(peers)) respBody := make([]*api.PeerBatch, 0, len(peers)) for _, peer := range peers { peerToReturn, err := h.checkPeerStatus(peer) @@ -213,12 +204,11 @@ func (h *Handler) GetAllPeers(w http.ResponseWriter, r *http.Request) { util.WriteError(r.Context(), err, w) return } - groupMinimumInfo := groups.ToGroupsInfo(groupsMap, peer.ID) - respBody = append(respBody, toPeerListItemResponse(peerToReturn, groupMinimumInfo, dnsDomain, 0)) + respBody = append(respBody, toPeerListItemResponse(peerToReturn, grpsInfoMap[peer.ID], dnsDomain, 0)) } - validPeersMap, err := h.accountManager.GetValidatedPeers(account) + validPeersMap, err := h.accountManager.GetValidatedPeers(r.Context(), accountID) if err != nil { log.WithContext(r.Context()).Errorf("failed to list appreoved peers: %v", err) util.WriteError(r.Context(), fmt.Errorf("internal error"), w) @@ -281,16 +271,16 @@ func (h *Handler) GetAccessiblePeers(w http.ResponseWriter, r *http.Request) { } } - dnsDomain := h.accountManager.GetDNSDomain() - - validPeers, err := h.accountManager.GetValidatedPeers(account) + validPeers, err := h.accountManager.GetValidatedPeers(r.Context(), accountID) if err != nil { log.WithContext(r.Context()).Errorf("failed to list approved peers: %v", err) util.WriteError(r.Context(), fmt.Errorf("internal error"), w) return } - customZone := account.GetPeersCustomZone(r.Context(), h.accountManager.GetDNSDomain()) + dnsDomain := h.accountManager.GetDNSDomain() + + customZone := account.GetPeersCustomZone(r.Context(), dnsDomain) netMap := account.GetPeerNetworkMap(r.Context(), peerID, customZone, validPeers, account.GetResourcePoliciesMap(), account.GetResourceRoutersMap(), nil) util.WriteJSONObject(r.Context(), w, toAccessiblePeers(netMap, dnsDomain)) diff --git a/management/server/http/handlers/peers/peers_handler_test.go b/management/server/http/handlers/peers/peers_handler_test.go index 83abc1c40..16065a677 100644 --- a/management/server/http/handlers/peers/peers_handler_test.go +++ b/management/server/http/handlers/peers/peers_handler_test.go @@ -38,6 +38,68 @@ const ( ) func initTestMetaData(peers ...*nbpeer.Peer) *Handler { + + peersMap := make(map[string]*nbpeer.Peer) + for _, peer := range peers { + peersMap[peer.ID] = peer.Copy() + } + + policy := &types.Policy{ + ID: "policy", + AccountID: "test_id", + Name: "policy", + Enabled: true, + Rules: []*types.PolicyRule{ + { + ID: "rule", + Name: "rule", + Enabled: true, + Action: "accept", + Destinations: []string{"group1"}, + Sources: []string{"group1"}, + Bidirectional: true, + Protocol: "all", + Ports: []string{"80"}, + }, + }, + } + + srvUser := types.NewRegularUser(serviceUser) + srvUser.IsServiceUser = true + + account := &types.Account{ + Id: "test_id", + Domain: "hotmail.com", + Peers: peersMap, + Users: map[string]*types.User{ + adminUser: types.NewAdminUser(adminUser), + regularUser: types.NewRegularUser(regularUser), + serviceUser: srvUser, + }, + Groups: map[string]*types.Group{ + "group1": { + ID: "group1", + AccountID: "test_id", + Name: "group1", + Issued: "api", + Peers: maps.Keys(peersMap), + }, + }, + Settings: &types.Settings{ + PeerLoginExpirationEnabled: true, + PeerLoginExpiration: time.Hour, + }, + Policies: []*types.Policy{policy}, + Network: &types.Network{ + Identifier: "ciclqisab2ss43jdn8q0", + Net: net.IPNet{ + IP: net.ParseIP("100.67.0.0"), + Mask: net.IPv4Mask(255, 255, 0, 0), + }, + Serial: 51, + }, + } + return &Handler{ accountManager: &mock_server.MockAccountManager{ UpdatePeerFunc: func(_ context.Context, accountID, userID string, update *nbpeer.Peer) (*nbpeer.Peer, error) { @@ -66,74 +128,31 @@ func initTestMetaData(peers ...*nbpeer.Peer) *Handler { GetPeersFunc: func(_ context.Context, accountID, userID string) ([]*nbpeer.Peer, error) { return peers, nil }, + GetPeerGroupsFunc: func(ctx context.Context, accountID, peerID string) ([]*types.Group, error) { + peersID := make([]string, len(peers)) + for _, peer := range peers { + peersID = append(peersID, peer.ID) + } + return []*types.Group{ + { + ID: "group1", + AccountID: accountID, + Name: "group1", + Issued: "api", + Peers: peersID, + }, + }, nil + }, GetDNSDomainFunc: func() string { return "netbird.selfhosted" }, GetAccountIDFromTokenFunc: func(_ context.Context, claims jwtclaims.AuthorizationClaims) (string, string, error) { return claims.AccountId, claims.UserId, nil }, + GetAccountFunc: func(ctx context.Context, accountID string) (*types.Account, error) { + return account, nil + }, GetAccountByIDFunc: func(ctx context.Context, accountID string, userID string) (*types.Account, error) { - peersMap := make(map[string]*nbpeer.Peer) - for _, peer := range peers { - peersMap[peer.ID] = peer.Copy() - } - - policy := &types.Policy{ - ID: "policy", - AccountID: accountID, - Name: "policy", - Enabled: true, - Rules: []*types.PolicyRule{ - { - ID: "rule", - Name: "rule", - Enabled: true, - Action: "accept", - Destinations: []string{"group1"}, - Sources: []string{"group1"}, - Bidirectional: true, - Protocol: "all", - Ports: []string{"80"}, - }, - }, - } - - srvUser := types.NewRegularUser(serviceUser) - srvUser.IsServiceUser = true - - account := &types.Account{ - Id: accountID, - Domain: "hotmail.com", - Peers: peersMap, - Users: map[string]*types.User{ - adminUser: types.NewAdminUser(adminUser), - regularUser: types.NewRegularUser(regularUser), - serviceUser: srvUser, - }, - Groups: map[string]*types.Group{ - "group1": { - ID: "group1", - AccountID: accountID, - Name: "group1", - Issued: "api", - Peers: maps.Keys(peersMap), - }, - }, - Settings: &types.Settings{ - PeerLoginExpirationEnabled: true, - PeerLoginExpiration: time.Hour, - }, - Policies: []*types.Policy{policy}, - Network: &types.Network{ - Identifier: "ciclqisab2ss43jdn8q0", - Net: net.IPNet{ - IP: net.ParseIP("100.67.0.0"), - Mask: net.IPv4Mask(255, 255, 0, 0), - }, - Serial: 51, - }, - } - return account, nil }, HasConnectedChannelFunc: func(peerID string) bool { diff --git a/management/server/http/testing/benchmarks/peers_handler_benchmark_test.go b/management/server/http/testing/benchmarks/peers_handler_benchmark_test.go index e76370426..2eb50e4b4 100644 --- a/management/server/http/testing/benchmarks/peers_handler_benchmark_test.go +++ b/management/server/http/testing/benchmarks/peers_handler_benchmark_test.go @@ -35,14 +35,14 @@ var benchCasesPeers = map[string]testing_tools.BenchmarkCase{ func BenchmarkUpdatePeer(b *testing.B) { var expectedMetrics = map[string]testing_tools.PerformanceMetrics{ - "Peers - XS": {MinMsPerOpLocal: 1300, MaxMsPerOpLocal: 1700, MinMsPerOpCICD: 2200, MaxMsPerOpCICD: 13000}, + "Peers - XS": {MinMsPerOpLocal: 400, MaxMsPerOpLocal: 600, MinMsPerOpCICD: 600, MaxMsPerOpCICD: 3500}, "Peers - S": {MinMsPerOpLocal: 100, MaxMsPerOpLocal: 130, MinMsPerOpCICD: 80, MaxMsPerOpCICD: 200}, - "Peers - M": {MinMsPerOpLocal: 160, MaxMsPerOpLocal: 190, MinMsPerOpCICD: 100, MaxMsPerOpCICD: 500}, - "Peers - L": {MinMsPerOpLocal: 400, MaxMsPerOpLocal: 430, MinMsPerOpCICD: 450, MaxMsPerOpCICD: 1400}, - "Groups - L": {MinMsPerOpLocal: 1200, MaxMsPerOpLocal: 1500, MinMsPerOpCICD: 1900, MaxMsPerOpCICD: 13000}, - "Users - L": {MinMsPerOpLocal: 600, MaxMsPerOpLocal: 800, MinMsPerOpCICD: 800, MaxMsPerOpCICD: 2800}, - "Setup Keys - L": {MinMsPerOpLocal: 400, MaxMsPerOpLocal: 700, MinMsPerOpCICD: 600, MaxMsPerOpCICD: 1300}, - "Peers - XL": {MinMsPerOpLocal: 1400, MaxMsPerOpLocal: 1900, MinMsPerOpCICD: 2200, MaxMsPerOpCICD: 5000}, + "Peers - M": {MinMsPerOpLocal: 130, MaxMsPerOpLocal: 150, MinMsPerOpCICD: 100, MaxMsPerOpCICD: 300}, + "Peers - L": {MinMsPerOpLocal: 230, MaxMsPerOpLocal: 270, MinMsPerOpCICD: 200, MaxMsPerOpCICD: 500}, + "Groups - L": {MinMsPerOpLocal: 400, MaxMsPerOpLocal: 600, MinMsPerOpCICD: 650, MaxMsPerOpCICD: 3500}, + "Users - L": {MinMsPerOpLocal: 200, MaxMsPerOpLocal: 400, MinMsPerOpCICD: 250, MaxMsPerOpCICD: 600}, + "Setup Keys - L": {MinMsPerOpLocal: 200, MaxMsPerOpLocal: 400, MinMsPerOpCICD: 250, MaxMsPerOpCICD: 600}, + "Peers - XL": {MinMsPerOpLocal: 600, MaxMsPerOpLocal: 1000, MinMsPerOpCICD: 600, MaxMsPerOpCICD: 2000}, } log.SetOutput(io.Discard) @@ -77,14 +77,14 @@ func BenchmarkUpdatePeer(b *testing.B) { func BenchmarkGetOnePeer(b *testing.B) { var expectedMetrics = map[string]testing_tools.PerformanceMetrics{ - "Peers - XS": {MinMsPerOpLocal: 600, MaxMsPerOpLocal: 900, MinMsPerOpCICD: 1100, MaxMsPerOpCICD: 7000}, - "Peers - S": {MinMsPerOpLocal: 3, MaxMsPerOpLocal: 7, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 30}, - "Peers - M": {MinMsPerOpLocal: 20, MaxMsPerOpLocal: 40, MinMsPerOpCICD: 35, MaxMsPerOpCICD: 80}, - "Peers - L": {MinMsPerOpLocal: 120, MaxMsPerOpLocal: 160, MinMsPerOpCICD: 100, MaxMsPerOpCICD: 300}, - "Groups - L": {MinMsPerOpLocal: 500, MaxMsPerOpLocal: 750, MinMsPerOpCICD: 900, MaxMsPerOpCICD: 6500}, - "Users - L": {MinMsPerOpLocal: 200, MaxMsPerOpLocal: 300, MinMsPerOpCICD: 200, MaxMsPerOpCICD: 600}, - "Setup Keys - L": {MinMsPerOpLocal: 200, MaxMsPerOpLocal: 300, MinMsPerOpCICD: 200, MaxMsPerOpCICD: 600}, - "Peers - XL": {MinMsPerOpLocal: 600, MaxMsPerOpLocal: 800, MinMsPerOpCICD: 600, MaxMsPerOpCICD: 1500}, + "Peers - XS": {MinMsPerOpLocal: 15, MaxMsPerOpLocal: 40, MinMsPerOpCICD: 30, MaxMsPerOpCICD: 70}, + "Peers - S": {MinMsPerOpLocal: 1, MaxMsPerOpLocal: 5, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 30}, + "Peers - M": {MinMsPerOpLocal: 9, MaxMsPerOpLocal: 18, MinMsPerOpCICD: 15, MaxMsPerOpCICD: 50}, + "Peers - L": {MinMsPerOpLocal: 40, MaxMsPerOpLocal: 90, MinMsPerOpCICD: 50, MaxMsPerOpCICD: 130}, + "Groups - L": {MinMsPerOpLocal: 80, MaxMsPerOpLocal: 130, MinMsPerOpCICD: 30, MaxMsPerOpCICD: 200}, + "Users - L": {MinMsPerOpLocal: 40, MaxMsPerOpLocal: 90, MinMsPerOpCICD: 50, MaxMsPerOpCICD: 130}, + "Setup Keys - L": {MinMsPerOpLocal: 40, MaxMsPerOpLocal: 90, MinMsPerOpCICD: 50, MaxMsPerOpCICD: 130}, + "Peers - XL": {MinMsPerOpLocal: 200, MaxMsPerOpLocal: 400, MinMsPerOpCICD: 200, MaxMsPerOpCICD: 750}, } log.SetOutput(io.Discard) @@ -111,14 +111,14 @@ func BenchmarkGetOnePeer(b *testing.B) { func BenchmarkGetAllPeers(b *testing.B) { var expectedMetrics = map[string]testing_tools.PerformanceMetrics{ - "Peers - XS": {MinMsPerOpLocal: 600, MaxMsPerOpLocal: 900, MinMsPerOpCICD: 1100, MaxMsPerOpCICD: 6000}, - "Peers - S": {MinMsPerOpLocal: 4, MaxMsPerOpLocal: 10, MinMsPerOpCICD: 7, MaxMsPerOpCICD: 30}, - "Peers - M": {MinMsPerOpLocal: 20, MaxMsPerOpLocal: 50, MinMsPerOpCICD: 40, MaxMsPerOpCICD: 90}, - "Peers - L": {MinMsPerOpLocal: 130, MaxMsPerOpLocal: 170, MinMsPerOpCICD: 150, MaxMsPerOpCICD: 350}, - "Groups - L": {MinMsPerOpLocal: 5000, MaxMsPerOpLocal: 5500, MinMsPerOpCICD: 7000, MaxMsPerOpCICD: 15000}, - "Users - L": {MinMsPerOpLocal: 250, MaxMsPerOpLocal: 300, MinMsPerOpCICD: 250, MaxMsPerOpCICD: 700}, - "Setup Keys - L": {MinMsPerOpLocal: 250, MaxMsPerOpLocal: 350, MinMsPerOpCICD: 250, MaxMsPerOpCICD: 700}, - "Peers - XL": {MinMsPerOpLocal: 900, MaxMsPerOpLocal: 1300, MinMsPerOpCICD: 1100, MaxMsPerOpCICD: 2200}, + "Peers - XS": {MinMsPerOpLocal: 40, MaxMsPerOpLocal: 70, MinMsPerOpCICD: 50, MaxMsPerOpCICD: 150}, + "Peers - S": {MinMsPerOpLocal: 2, MaxMsPerOpLocal: 10, MinMsPerOpCICD: 5, MaxMsPerOpCICD: 30}, + "Peers - M": {MinMsPerOpLocal: 20, MaxMsPerOpLocal: 50, MinMsPerOpCICD: 20, MaxMsPerOpCICD: 70}, + "Peers - L": {MinMsPerOpLocal: 110, MaxMsPerOpLocal: 150, MinMsPerOpCICD: 100, MaxMsPerOpCICD: 300}, + "Groups - L": {MinMsPerOpLocal: 150, MaxMsPerOpLocal: 200, MinMsPerOpCICD: 130, MaxMsPerOpCICD: 500}, + "Users - L": {MinMsPerOpLocal: 100, MaxMsPerOpLocal: 170, MinMsPerOpCICD: 100, MaxMsPerOpCICD: 400}, + "Setup Keys - L": {MinMsPerOpLocal: 100, MaxMsPerOpLocal: 170, MinMsPerOpCICD: 100, MaxMsPerOpCICD: 400}, + "Peers - XL": {MinMsPerOpLocal: 450, MaxMsPerOpLocal: 800, MinMsPerOpCICD: 500, MaxMsPerOpCICD: 1500}, } log.SetOutput(io.Discard) @@ -145,14 +145,14 @@ func BenchmarkGetAllPeers(b *testing.B) { func BenchmarkDeletePeer(b *testing.B) { var expectedMetrics = map[string]testing_tools.PerformanceMetrics{ - "Peers - XS": {MinMsPerOpLocal: 600, MaxMsPerOpLocal: 800, MinMsPerOpCICD: 1100, MaxMsPerOpCICD: 7000}, - "Peers - S": {MinMsPerOpLocal: 20, MaxMsPerOpLocal: 50, MinMsPerOpCICD: 50, MaxMsPerOpCICD: 210}, - "Peers - M": {MinMsPerOpLocal: 20, MaxMsPerOpLocal: 50, MinMsPerOpCICD: 50, MaxMsPerOpCICD: 230}, - "Peers - L": {MinMsPerOpLocal: 20, MaxMsPerOpLocal: 50, MinMsPerOpCICD: 50, MaxMsPerOpCICD: 210}, - "Groups - L": {MinMsPerOpLocal: 400, MaxMsPerOpLocal: 550, MinMsPerOpCICD: 700, MaxMsPerOpCICD: 5500}, - "Users - L": {MinMsPerOpLocal: 170, MaxMsPerOpLocal: 210, MinMsPerOpCICD: 290, MaxMsPerOpCICD: 1700}, - "Setup Keys - L": {MinMsPerOpLocal: 30, MaxMsPerOpLocal: 125, MinMsPerOpCICD: 55, MaxMsPerOpCICD: 280}, - "Peers - XL": {MinMsPerOpLocal: 30, MaxMsPerOpLocal: 50, MinMsPerOpCICD: 60, MaxMsPerOpCICD: 250}, + "Peers - XS": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 4, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 15}, + "Peers - S": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 4, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 15}, + "Peers - M": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 4, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 15}, + "Peers - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 4, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 15}, + "Groups - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 4, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 15}, + "Users - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 4, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 15}, + "Setup Keys - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 4, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 15}, + "Peers - XL": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 4, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 15}, } log.SetOutput(io.Discard) diff --git a/management/server/http/testing/benchmarks/setupkeys_handler_benchmark_test.go b/management/server/http/testing/benchmarks/setupkeys_handler_benchmark_test.go index bbdb4250b..ed643f75e 100644 --- a/management/server/http/testing/benchmarks/setupkeys_handler_benchmark_test.go +++ b/management/server/http/testing/benchmarks/setupkeys_handler_benchmark_test.go @@ -35,14 +35,14 @@ var benchCasesSetupKeys = map[string]testing_tools.BenchmarkCase{ func BenchmarkCreateSetupKey(b *testing.B) { var expectedMetrics = map[string]testing_tools.PerformanceMetrics{ - "Setup Keys - XS": {MinMsPerOpLocal: 0.5, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 1, MaxMsPerOpCICD: 17}, - "Setup Keys - S": {MinMsPerOpLocal: 0.5, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 1, MaxMsPerOpCICD: 17}, - "Setup Keys - M": {MinMsPerOpLocal: 0.5, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 1, MaxMsPerOpCICD: 17}, - "Setup Keys - L": {MinMsPerOpLocal: 0.5, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 1, MaxMsPerOpCICD: 17}, - "Peers - L": {MinMsPerOpLocal: 0.5, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 1, MaxMsPerOpCICD: 17}, - "Groups - L": {MinMsPerOpLocal: 0.5, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 1, MaxMsPerOpCICD: 17}, - "Users - L": {MinMsPerOpLocal: 0.5, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 1, MaxMsPerOpCICD: 17}, - "Setup Keys - XL": {MinMsPerOpLocal: 0.5, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 1, MaxMsPerOpCICD: 17}, + "Setup Keys - XS": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 0, MaxMsPerOpCICD: 17}, + "Setup Keys - S": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 0, MaxMsPerOpCICD: 17}, + "Setup Keys - M": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 0, MaxMsPerOpCICD: 17}, + "Setup Keys - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 0, MaxMsPerOpCICD: 17}, + "Peers - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 0, MaxMsPerOpCICD: 17}, + "Groups - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 0, MaxMsPerOpCICD: 17}, + "Users - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 0, MaxMsPerOpCICD: 17}, + "Setup Keys - XL": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 0, MaxMsPerOpCICD: 17}, } log.SetOutput(io.Discard) @@ -81,14 +81,14 @@ func BenchmarkCreateSetupKey(b *testing.B) { func BenchmarkUpdateSetupKey(b *testing.B) { var expectedMetrics = map[string]testing_tools.PerformanceMetrics{ - "Setup Keys - XS": {MinMsPerOpLocal: 0.5, MaxMsPerOpLocal: 3, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 19}, - "Setup Keys - S": {MinMsPerOpLocal: 0.5, MaxMsPerOpLocal: 3, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 19}, - "Setup Keys - M": {MinMsPerOpLocal: 0.5, MaxMsPerOpLocal: 3, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 19}, - "Setup Keys - L": {MinMsPerOpLocal: 0.5, MaxMsPerOpLocal: 3, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 19}, - "Peers - L": {MinMsPerOpLocal: 0.5, MaxMsPerOpLocal: 3, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 19}, - "Groups - L": {MinMsPerOpLocal: 0.5, MaxMsPerOpLocal: 3, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 19}, - "Users - L": {MinMsPerOpLocal: 0.5, MaxMsPerOpLocal: 3, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 19}, - "Setup Keys - XL": {MinMsPerOpLocal: 0.5, MaxMsPerOpLocal: 3, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 19}, + "Setup Keys - XS": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 3, MinMsPerOpCICD: 0, MaxMsPerOpCICD: 19}, + "Setup Keys - S": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 3, MinMsPerOpCICD: 0, MaxMsPerOpCICD: 19}, + "Setup Keys - M": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 3, MinMsPerOpCICD: 0, MaxMsPerOpCICD: 19}, + "Setup Keys - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 3, MinMsPerOpCICD: 0, MaxMsPerOpCICD: 19}, + "Peers - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 3, MinMsPerOpCICD: 0, MaxMsPerOpCICD: 19}, + "Groups - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 3, MinMsPerOpCICD: 0, MaxMsPerOpCICD: 19}, + "Users - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 3, MinMsPerOpCICD: 0, MaxMsPerOpCICD: 19}, + "Setup Keys - XL": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 3, MinMsPerOpCICD: 0, MaxMsPerOpCICD: 19}, } log.SetOutput(io.Discard) @@ -128,14 +128,14 @@ func BenchmarkUpdateSetupKey(b *testing.B) { func BenchmarkGetOneSetupKey(b *testing.B) { var expectedMetrics = map[string]testing_tools.PerformanceMetrics{ - "Setup Keys - XS": {MinMsPerOpLocal: 0.5, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 1, MaxMsPerOpCICD: 16}, - "Setup Keys - S": {MinMsPerOpLocal: 0.5, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 1, MaxMsPerOpCICD: 16}, - "Setup Keys - M": {MinMsPerOpLocal: 0.5, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 1, MaxMsPerOpCICD: 16}, - "Setup Keys - L": {MinMsPerOpLocal: 0.5, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 1, MaxMsPerOpCICD: 16}, - "Peers - L": {MinMsPerOpLocal: 0.5, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 1, MaxMsPerOpCICD: 16}, - "Groups - L": {MinMsPerOpLocal: 0.5, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 1, MaxMsPerOpCICD: 16}, - "Users - L": {MinMsPerOpLocal: 0.5, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 1, MaxMsPerOpCICD: 16}, - "Setup Keys - XL": {MinMsPerOpLocal: 0.5, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 1, MaxMsPerOpCICD: 16}, + "Setup Keys - XS": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 0, MaxMsPerOpCICD: 16}, + "Setup Keys - S": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 0, MaxMsPerOpCICD: 16}, + "Setup Keys - M": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 0, MaxMsPerOpCICD: 16}, + "Setup Keys - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 0, MaxMsPerOpCICD: 16}, + "Peers - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 0, MaxMsPerOpCICD: 16}, + "Groups - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 0, MaxMsPerOpCICD: 16}, + "Users - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 0, MaxMsPerOpCICD: 16}, + "Setup Keys - XL": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 0, MaxMsPerOpCICD: 16}, } log.SetOutput(io.Discard) @@ -162,8 +162,8 @@ func BenchmarkGetOneSetupKey(b *testing.B) { func BenchmarkGetAllSetupKeys(b *testing.B) { var expectedMetrics = map[string]testing_tools.PerformanceMetrics{ - "Setup Keys - XS": {MinMsPerOpLocal: 0.5, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 1, MaxMsPerOpCICD: 12}, - "Setup Keys - S": {MinMsPerOpLocal: 0.5, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 1, MaxMsPerOpCICD: 15}, + "Setup Keys - XS": {MinMsPerOpLocal: 0.5, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 0, MaxMsPerOpCICD: 12}, + "Setup Keys - S": {MinMsPerOpLocal: 0.5, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 0, MaxMsPerOpCICD: 15}, "Setup Keys - M": {MinMsPerOpLocal: 5, MaxMsPerOpLocal: 10, MinMsPerOpCICD: 5, MaxMsPerOpCICD: 40}, "Setup Keys - L": {MinMsPerOpLocal: 30, MaxMsPerOpLocal: 50, MinMsPerOpCICD: 30, MaxMsPerOpCICD: 150}, "Peers - L": {MinMsPerOpLocal: 30, MaxMsPerOpLocal: 50, MinMsPerOpCICD: 30, MaxMsPerOpCICD: 150}, @@ -196,14 +196,14 @@ func BenchmarkGetAllSetupKeys(b *testing.B) { func BenchmarkDeleteSetupKey(b *testing.B) { var expectedMetrics = map[string]testing_tools.PerformanceMetrics{ - "Setup Keys - XS": {MinMsPerOpLocal: 0.5, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 1, MaxMsPerOpCICD: 16}, - "Setup Keys - S": {MinMsPerOpLocal: 0.5, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 1, MaxMsPerOpCICD: 16}, - "Setup Keys - M": {MinMsPerOpLocal: 0.5, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 1, MaxMsPerOpCICD: 16}, - "Setup Keys - L": {MinMsPerOpLocal: 0.5, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 1, MaxMsPerOpCICD: 16}, - "Peers - L": {MinMsPerOpLocal: 0.5, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 1, MaxMsPerOpCICD: 16}, - "Groups - L": {MinMsPerOpLocal: 0.5, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 1, MaxMsPerOpCICD: 16}, - "Users - L": {MinMsPerOpLocal: 0.5, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 1, MaxMsPerOpCICD: 16}, - "Setup Keys - XL": {MinMsPerOpLocal: 0.5, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 1, MaxMsPerOpCICD: 16}, + "Setup Keys - XS": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 1, MaxMsPerOpCICD: 16}, + "Setup Keys - S": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 1, MaxMsPerOpCICD: 16}, + "Setup Keys - M": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 1, MaxMsPerOpCICD: 16}, + "Setup Keys - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 1, MaxMsPerOpCICD: 16}, + "Peers - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 1, MaxMsPerOpCICD: 16}, + "Groups - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 1, MaxMsPerOpCICD: 16}, + "Users - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 1, MaxMsPerOpCICD: 16}, + "Setup Keys - XL": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 1, MaxMsPerOpCICD: 16}, } log.SetOutput(io.Discard) diff --git a/management/server/http/testing/benchmarks/users_handler_benchmark_test.go b/management/server/http/testing/benchmarks/users_handler_benchmark_test.go index b62341995..549a51c0e 100644 --- a/management/server/http/testing/benchmarks/users_handler_benchmark_test.go +++ b/management/server/http/testing/benchmarks/users_handler_benchmark_test.go @@ -35,14 +35,14 @@ var benchCasesUsers = map[string]testing_tools.BenchmarkCase{ func BenchmarkUpdateUser(b *testing.B) { var expectedMetrics = map[string]testing_tools.PerformanceMetrics{ - "Users - XS": {MinMsPerOpLocal: 700, MaxMsPerOpLocal: 1000, MinMsPerOpCICD: 1300, MaxMsPerOpCICD: 7000}, - "Users - S": {MinMsPerOpLocal: 1, MaxMsPerOpLocal: 5, MinMsPerOpCICD: 6, MaxMsPerOpCICD: 40}, - "Users - M": {MinMsPerOpLocal: 20, MaxMsPerOpLocal: 40, MinMsPerOpCICD: 30, MaxMsPerOpCICD: 200}, - "Users - L": {MinMsPerOpLocal: 60, MaxMsPerOpLocal: 100, MinMsPerOpCICD: 130, MaxMsPerOpCICD: 700}, - "Peers - L": {MinMsPerOpLocal: 300, MaxMsPerOpLocal: 500, MinMsPerOpCICD: 550, MaxMsPerOpCICD: 2000}, + "Users - XS": {MinMsPerOpLocal: 700, MaxMsPerOpLocal: 1000, MinMsPerOpCICD: 1300, MaxMsPerOpCICD: 8000}, + "Users - S": {MinMsPerOpLocal: 1, MaxMsPerOpLocal: 5, MinMsPerOpCICD: 4, MaxMsPerOpCICD: 50}, + "Users - M": {MinMsPerOpLocal: 20, MaxMsPerOpLocal: 40, MinMsPerOpCICD: 30, MaxMsPerOpCICD: 250}, + "Users - L": {MinMsPerOpLocal: 60, MaxMsPerOpLocal: 100, MinMsPerOpCICD: 90, MaxMsPerOpCICD: 700}, + "Peers - L": {MinMsPerOpLocal: 300, MaxMsPerOpLocal: 500, MinMsPerOpCICD: 550, MaxMsPerOpCICD: 2400}, "Groups - L": {MinMsPerOpLocal: 400, MaxMsPerOpLocal: 600, MinMsPerOpCICD: 750, MaxMsPerOpCICD: 5000}, - "Setup Keys - L": {MinMsPerOpLocal: 50, MaxMsPerOpLocal: 200, MinMsPerOpCICD: 150, MaxMsPerOpCICD: 1000}, - "Users - XL": {MinMsPerOpLocal: 350, MaxMsPerOpLocal: 550, MinMsPerOpCICD: 700, MaxMsPerOpCICD: 3500}, + "Setup Keys - L": {MinMsPerOpLocal: 50, MaxMsPerOpLocal: 200, MinMsPerOpCICD: 130, MaxMsPerOpCICD: 1000}, + "Users - XL": {MinMsPerOpLocal: 350, MaxMsPerOpLocal: 550, MinMsPerOpCICD: 650, MaxMsPerOpCICD: 3500}, } log.SetOutput(io.Discard) @@ -119,11 +119,11 @@ func BenchmarkGetOneUser(b *testing.B) { func BenchmarkGetAllUsers(b *testing.B) { var expectedMetrics = map[string]testing_tools.PerformanceMetrics{ "Users - XS": {MinMsPerOpLocal: 50, MaxMsPerOpLocal: 90, MinMsPerOpCICD: 60, MaxMsPerOpCICD: 180}, - "Users - S": {MinMsPerOpLocal: 0.5, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 1, MaxMsPerOpCICD: 30}, - "Users - M": {MinMsPerOpLocal: 5, MaxMsPerOpLocal: 12, MinMsPerOpCICD: 1, MaxMsPerOpCICD: 30}, - "Users - L": {MinMsPerOpLocal: 0.5, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 1, MaxMsPerOpCICD: 30}, - "Peers - L": {MinMsPerOpLocal: 0.5, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 1, MaxMsPerOpCICD: 30}, - "Groups - L": {MinMsPerOpLocal: 0.5, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 1, MaxMsPerOpCICD: 30}, + "Users - S": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 0, MaxMsPerOpCICD: 30}, + "Users - M": {MinMsPerOpLocal: 5, MaxMsPerOpLocal: 12, MinMsPerOpCICD: 0, MaxMsPerOpCICD: 30}, + "Users - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 0, MaxMsPerOpCICD: 30}, + "Peers - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 0, MaxMsPerOpCICD: 30}, + "Groups - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 0, MaxMsPerOpCICD: 30}, "Setup Keys - L": {MinMsPerOpLocal: 40, MaxMsPerOpLocal: 140, MinMsPerOpCICD: 60, MaxMsPerOpCICD: 200}, "Users - XL": {MinMsPerOpLocal: 15, MaxMsPerOpLocal: 40, MinMsPerOpCICD: 20, MaxMsPerOpCICD: 90}, } @@ -152,13 +152,13 @@ func BenchmarkGetAllUsers(b *testing.B) { func BenchmarkDeleteUsers(b *testing.B) { var expectedMetrics = map[string]testing_tools.PerformanceMetrics{ - "Users - XS": {MinMsPerOpLocal: 1000, MaxMsPerOpLocal: 1600, MinMsPerOpCICD: 1900, MaxMsPerOpCICD: 10000}, + "Users - XS": {MinMsPerOpLocal: 1000, MaxMsPerOpLocal: 1600, MinMsPerOpCICD: 1900, MaxMsPerOpCICD: 11000}, "Users - S": {MinMsPerOpLocal: 15, MaxMsPerOpLocal: 40, MinMsPerOpCICD: 30, MaxMsPerOpCICD: 200}, "Users - M": {MinMsPerOpLocal: 15, MaxMsPerOpLocal: 70, MinMsPerOpCICD: 15, MaxMsPerOpCICD: 230}, "Users - L": {MinMsPerOpLocal: 15, MaxMsPerOpLocal: 45, MinMsPerOpCICD: 30, MaxMsPerOpCICD: 190}, "Peers - L": {MinMsPerOpLocal: 400, MaxMsPerOpLocal: 600, MinMsPerOpCICD: 650, MaxMsPerOpCICD: 1800}, "Groups - L": {MinMsPerOpLocal: 600, MaxMsPerOpLocal: 800, MinMsPerOpCICD: 1200, MaxMsPerOpCICD: 7500}, - "Setup Keys - L": {MinMsPerOpLocal: 20, MaxMsPerOpLocal: 200, MinMsPerOpCICD: 55, MaxMsPerOpCICD: 600}, + "Setup Keys - L": {MinMsPerOpLocal: 20, MaxMsPerOpLocal: 200, MinMsPerOpCICD: 40, MaxMsPerOpCICD: 600}, "Users - XL": {MinMsPerOpLocal: 50, MaxMsPerOpLocal: 150, MinMsPerOpCICD: 80, MaxMsPerOpCICD: 400}, } diff --git a/management/server/integrated_validator.go b/management/server/integrated_validator.go index 62e9213f7..b9827f457 100644 --- a/management/server/integrated_validator.go +++ b/management/server/integrated_validator.go @@ -76,8 +76,31 @@ func (am *DefaultAccountManager) GroupValidation(ctx context.Context, accountID return true, nil } -func (am *DefaultAccountManager) GetValidatedPeers(account *types.Account) (map[string]struct{}, error) { - return am.integratedPeerValidator.GetValidatedPeers(account.Id, account.Groups, account.Peers, account.Settings.Extra) +func (am *DefaultAccountManager) GetValidatedPeers(ctx context.Context, accountID string) (map[string]struct{}, error) { + var err error + var groups []*types.Group + var peers []*nbpeer.Peer + var settings *types.Settings + + err = am.Store.ExecuteInTransaction(ctx, func(transaction store.Store) error { + groups, err = transaction.GetAccountGroups(ctx, store.LockingStrengthShare, accountID) + if err != nil { + return err + } + + peers, err = transaction.GetAccountPeers(ctx, store.LockingStrengthShare, accountID) + return err + }) + if err != nil { + return nil, err + } + + settings, err = am.Store.GetAccountSettings(ctx, store.LockingStrengthShare, accountID) + if err != nil { + return nil, err + } + + return am.integratedPeerValidator.GetValidatedPeers(accountID, groups, peers, settings.Extra) } type MocIntegratedValidator struct { @@ -94,7 +117,8 @@ func (a MocIntegratedValidator) ValidatePeer(_ context.Context, update *nbpeer.P } return update, false, nil } -func (a MocIntegratedValidator) GetValidatedPeers(accountID string, groups map[string]*types.Group, peers map[string]*nbpeer.Peer, extraSettings *account.ExtraSettings) (map[string]struct{}, error) { + +func (a MocIntegratedValidator) GetValidatedPeers(accountID string, groups []*types.Group, peers []*nbpeer.Peer, extraSettings *account.ExtraSettings) (map[string]struct{}, error) { validatedPeers := make(map[string]struct{}) for _, peer := range peers { validatedPeers[peer.ID] = struct{}{} diff --git a/management/server/integrated_validator/interface.go b/management/server/integrated_validator/interface.go index 22b8026aa..ff179e3c0 100644 --- a/management/server/integrated_validator/interface.go +++ b/management/server/integrated_validator/interface.go @@ -14,7 +14,7 @@ type IntegratedValidator interface { ValidatePeer(ctx context.Context, update *nbpeer.Peer, peer *nbpeer.Peer, userID string, accountID string, dnsDomain string, peersGroup []string, extraSettings *account.ExtraSettings) (*nbpeer.Peer, bool, error) PreparePeer(ctx context.Context, accountID string, peer *nbpeer.Peer, peersGroup []string, extraSettings *account.ExtraSettings) *nbpeer.Peer IsNotValidPeer(ctx context.Context, accountID string, peer *nbpeer.Peer, peersGroup []string, extraSettings *account.ExtraSettings) (bool, bool, error) - GetValidatedPeers(accountID string, groups map[string]*types.Group, peers map[string]*nbpeer.Peer, extraSettings *account.ExtraSettings) (map[string]struct{}, error) + GetValidatedPeers(accountID string, groups []*types.Group, peers []*nbpeer.Peer, extraSettings *account.ExtraSettings) (map[string]struct{}, error) PeerDeleted(ctx context.Context, accountID, peerID string) error SetPeerInvalidationListener(fn func(accountID string)) Stop(ctx context.Context) diff --git a/management/server/management_proto_test.go b/management/server/management_proto_test.go index 8147afa44..0df2462f4 100644 --- a/management/server/management_proto_test.go +++ b/management/server/management_proto_test.go @@ -249,7 +249,7 @@ func Test_SyncProtocol(t *testing.T) { t.Fatal("expecting SyncResponse to have non-nil NetworkMap") } - if len(networkMap.GetRemotePeers()) != 3 { + if len(networkMap.GetRemotePeers()) != 4 { t.Fatalf("expecting SyncResponse to have NetworkMap with 3 remote peers, got %d", len(networkMap.GetRemotePeers())) } diff --git a/management/server/mock_server/account_mock.go b/management/server/mock_server/account_mock.go index 042137b1b..c8e42d20a 100644 --- a/management/server/mock_server/account_mock.go +++ b/management/server/mock_server/account_mock.go @@ -47,6 +47,7 @@ type MockAccountManager struct { DeleteGroupsFunc func(ctx context.Context, accountId, userId string, groupIDs []string) error GroupAddPeerFunc func(ctx context.Context, accountID, groupID, peerID string) error GroupDeletePeerFunc func(ctx context.Context, accountID, groupID, peerID string) error + GetPeerGroupsFunc func(ctx context.Context, accountID, peerID string) ([]*types.Group, error) DeleteRuleFunc func(ctx context.Context, accountID, ruleID, userID string) error GetPolicyFunc func(ctx context.Context, accountID, policyID, userID string) (*types.Policy, error) SavePolicyFunc func(ctx context.Context, accountID, userID string, policy *types.Policy) (*types.Policy, error) @@ -90,7 +91,7 @@ type MockAccountManager struct { GetPeerFunc func(ctx context.Context, accountID, peerID, userID string) (*nbpeer.Peer, error) UpdateAccountSettingsFunc func(ctx context.Context, accountID, userID string, newSettings *types.Settings) (*types.Account, error) LoginPeerFunc func(ctx context.Context, login server.PeerLogin) (*nbpeer.Peer, *types.NetworkMap, []*posture.Checks, error) - SyncPeerFunc func(ctx context.Context, sync server.PeerSync, account *types.Account) (*nbpeer.Peer, *types.NetworkMap, []*posture.Checks, error) + SyncPeerFunc func(ctx context.Context, sync server.PeerSync, accountID string) (*nbpeer.Peer, *types.NetworkMap, []*posture.Checks, error) InviteUserFunc func(ctx context.Context, accountID string, initiatorUserID string, targetUserEmail string) error GetAllConnectedPeersFunc func() (map[string]struct{}, error) HasConnectedChannelFunc func(peerID string) bool @@ -134,7 +135,12 @@ func (am *MockAccountManager) OnPeerDisconnected(_ context.Context, accountID st panic("implement me") } -func (am *MockAccountManager) GetValidatedPeers(account *types.Account) (map[string]struct{}, error) { +func (am *MockAccountManager) GetValidatedPeers(ctx context.Context, accountID string) (map[string]struct{}, error) { + account, err := am.GetAccountFunc(ctx, accountID) + if err != nil { + return nil, err + } + approvedPeers := make(map[string]struct{}) for id := range account.Peers { approvedPeers[id] = struct{}{} @@ -225,7 +231,7 @@ func (am *MockAccountManager) GetAccountIDByUserID(ctx context.Context, userId, } // MarkPeerConnected mock implementation of MarkPeerConnected from server.AccountManager interface -func (am *MockAccountManager) MarkPeerConnected(ctx context.Context, peerKey string, connected bool, realIP net.IP, account *types.Account) error { +func (am *MockAccountManager) MarkPeerConnected(ctx context.Context, peerKey string, connected bool, realIP net.IP, accountID string) error { if am.MarkPeerConnectedFunc != nil { return am.MarkPeerConnectedFunc(ctx, peerKey, connected, realIP) } @@ -686,9 +692,9 @@ func (am *MockAccountManager) LoginPeer(ctx context.Context, login server.PeerLo } // SyncPeer mocks SyncPeer of the AccountManager interface -func (am *MockAccountManager) SyncPeer(ctx context.Context, sync server.PeerSync, account *types.Account) (*nbpeer.Peer, *types.NetworkMap, []*posture.Checks, error) { +func (am *MockAccountManager) SyncPeer(ctx context.Context, sync server.PeerSync, accountID string) (*nbpeer.Peer, *types.NetworkMap, []*posture.Checks, error) { if am.SyncPeerFunc != nil { - return am.SyncPeerFunc(ctx, sync, account) + return am.SyncPeerFunc(ctx, sync, accountID) } return nil, nil, nil, status.Errorf(codes.Unimplemented, "method SyncPeer is not implemented") } @@ -835,3 +841,11 @@ func (am *MockAccountManager) GetAccount(ctx context.Context, accountID string) } return nil, status.Errorf(codes.Unimplemented, "method GetAccount is not implemented") } + +// GetPeerGroups mocks GetPeerGroups of the AccountManager interface +func (am *MockAccountManager) GetPeerGroups(ctx context.Context, accountID, peerID string) ([]*types.Group, error) { + if am.GetPeerGroupsFunc != nil { + return am.GetPeerGroupsFunc(ctx, accountID, peerID) + } + return nil, status.Errorf(codes.Unimplemented, "method GetPeerGroups is not implemented") +} diff --git a/management/server/peer.go b/management/server/peer.go index 57b38ce81..5b0f12899 100644 --- a/management/server/peer.go +++ b/management/server/peer.go @@ -13,8 +13,9 @@ import ( "github.com/rs/xid" log "github.com/sirupsen/logrus" + "golang.org/x/exp/maps" - "github.com/netbirdio/netbird/management/server/util" + "github.com/netbirdio/netbird/management/server/geolocation" "github.com/netbirdio/netbird/management/server/idp" "github.com/netbirdio/netbird/management/server/posture" @@ -57,43 +58,55 @@ type PeerLogin struct { // GetPeers returns a list of peers under the given account filtering out peers that do not belong to a user if // the current user is not an admin. func (am *DefaultAccountManager) GetPeers(ctx context.Context, accountID, userID string) ([]*nbpeer.Peer, error) { - account, err := am.Store.GetAccount(ctx, accountID) + user, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, userID) if err != nil { return nil, err } - user, err := account.FindUser(userID) + if user.AccountID != accountID { + return nil, status.NewUserNotPartOfAccountError() + } + + settings, err := am.Store.GetAccountSettings(ctx, store.LockingStrengthShare, accountID) if err != nil { return nil, err } - approvedPeersMap, err := am.GetValidatedPeers(account) + if user.IsRegularUser() && settings.RegularUsersViewBlocked { + return []*nbpeer.Peer{}, nil + } + + accountPeers, err := am.Store.GetAccountPeers(ctx, store.LockingStrengthShare, accountID) if err != nil { return nil, err } + peers := make([]*nbpeer.Peer, 0) peersMap := make(map[string]*nbpeer.Peer) - regularUser := !user.HasAdminPower() && !user.IsServiceUser - - if regularUser && account.Settings.RegularUsersViewBlocked { - return peers, nil - } - - for _, peer := range account.Peers { - if regularUser && user.Id != peer.UserID { + for _, peer := range accountPeers { + if user.IsRegularUser() && user.Id != peer.UserID { // only display peers that belong to the current user if the current user is not an admin continue } - p := peer.Copy() - peers = append(peers, p) - peersMap[peer.ID] = p + peers = append(peers, peer) + peersMap[peer.ID] = peer } - if !regularUser { + if user.IsAdminOrServiceUser() { return peers, nil } + account, err := am.requestBuffer.GetAccountWithBackpressure(ctx, accountID) + if err != nil { + return nil, err + } + + approvedPeersMap, err := am.integratedPeerValidator.GetValidatedPeers(accountID, maps.Values(account.Groups), maps.Values(account.Peers), account.Settings.Extra) + if err != nil { + return nil, err + } + // fetch all the peers that have access to the user's peers for _, peer := range peers { aclPeers, _ := account.GetPeerConnectionResources(ctx, peer.ID, approvedPeersMap) @@ -102,53 +115,59 @@ func (am *DefaultAccountManager) GetPeers(ctx context.Context, accountID, userID } } - peers = make([]*nbpeer.Peer, 0, len(peersMap)) - for _, peer := range peersMap { - peers = append(peers, peer) - } - - return peers, nil + return maps.Values(peersMap), nil } // MarkPeerConnected marks peer as connected (true) or disconnected (false) -func (am *DefaultAccountManager) MarkPeerConnected(ctx context.Context, peerPubKey string, connected bool, realIP net.IP, account *types.Account) error { +func (am *DefaultAccountManager) MarkPeerConnected(ctx context.Context, peerPubKey string, connected bool, realIP net.IP, accountID string) error { start := time.Now() defer func() { log.WithContext(ctx).Debugf("MarkPeerConnected: took %v", time.Since(start)) }() - peer, err := account.FindPeerByPubKey(peerPubKey) - if err != nil { - return fmt.Errorf("failed to find peer by pub key: %w", err) - } + var peer *nbpeer.Peer + var settings *types.Settings + var expired bool + var err error - expired, err := am.updatePeerStatusAndLocation(ctx, peer, connected, realIP, account) - if err != nil { - return fmt.Errorf("failed to update peer status and location: %w", err) - } - - log.WithContext(ctx).Debugf("mark peer %s connected: %t", peer.ID, connected) - - if peer.AddedWithSSOLogin() { - if peer.LoginExpirationEnabled && account.Settings.PeerLoginExpirationEnabled { - am.checkAndSchedulePeerLoginExpiration(ctx, account) + err = am.Store.ExecuteInTransaction(ctx, func(transaction store.Store) error { + peer, err = transaction.GetPeerByPeerPubKey(ctx, store.LockingStrengthUpdate, peerPubKey) + if err != nil { + return err } - if peer.InactivityExpirationEnabled && account.Settings.PeerInactivityExpirationEnabled { - am.checkAndSchedulePeerInactivityExpiration(ctx, account) + expired, err = updatePeerStatusAndLocation(ctx, am.geo, transaction, peer, connected, realIP, accountID) + return err + }) + if err != nil { + return err + } + + if peer.AddedWithSSOLogin() { + settings, err = am.Store.GetAccountSettings(ctx, store.LockingStrengthShare, accountID) + if err != nil { + return err + } + + if peer.LoginExpirationEnabled && settings.PeerLoginExpirationEnabled { + am.checkAndSchedulePeerLoginExpiration(ctx, accountID) + } + + if peer.InactivityExpirationEnabled && settings.PeerInactivityExpirationEnabled { + am.checkAndSchedulePeerInactivityExpiration(ctx, accountID) } } if expired { // we need to update other peers because when peer login expires all other peers are notified to disconnect from // the expired one. Here we notify them that connection is now allowed again. - am.UpdateAccountPeers(ctx, account.Id) + am.UpdateAccountPeers(ctx, accountID) } return nil } -func (am *DefaultAccountManager) updatePeerStatusAndLocation(ctx context.Context, peer *nbpeer.Peer, connected bool, realIP net.IP, account *types.Account) (bool, error) { +func updatePeerStatusAndLocation(ctx context.Context, geo geolocation.Geolocation, transaction store.Store, peer *nbpeer.Peer, connected bool, realIP net.IP, accountID string) (bool, error) { oldStatus := peer.Status.Copy() newStatus := oldStatus newStatus.LastSeen = time.Now().UTC() @@ -159,8 +178,8 @@ func (am *DefaultAccountManager) updatePeerStatusAndLocation(ctx context.Context } peer.Status = newStatus - if am.geo != nil && realIP != nil { - location, err := am.geo.Lookup(realIP) + if geo != nil && realIP != nil { + location, err := geo.Lookup(realIP) if err != nil { log.WithContext(ctx).Warnf("failed to get location for peer %s realip: [%s]: %v", peer.ID, realIP.String(), err) } else { @@ -168,20 +187,18 @@ func (am *DefaultAccountManager) updatePeerStatusAndLocation(ctx context.Context peer.Location.CountryCode = location.Country.ISOCode peer.Location.CityName = location.City.Names.En peer.Location.GeoNameID = location.City.GeonameID - err = am.Store.SavePeerLocation(account.Id, peer) + err = transaction.SavePeerLocation(ctx, store.LockingStrengthUpdate, accountID, peer) if err != nil { log.WithContext(ctx).Warnf("could not store location for peer %s: %s", peer.ID, err) } } } - account.UpdatePeer(peer) - log.WithContext(ctx).Tracef("saving peer status for peer %s is connected: %t", peer.ID, connected) - err := am.Store.SavePeerStatus(account.Id, peer.ID, *newStatus) + err := transaction.SavePeerStatus(ctx, store.LockingStrengthUpdate, accountID, peer.ID, *newStatus) if err != nil { - return false, fmt.Errorf("failed to save peer status: %w", err) + return false, err } return oldStatus.LoginExpired, nil @@ -192,174 +209,183 @@ func (am *DefaultAccountManager) UpdatePeer(ctx context.Context, accountID, user unlock := am.Store.AcquireWriteLockByUID(ctx, accountID) defer unlock() - account, err := am.Store.GetAccount(ctx, accountID) + user, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, userID) if err != nil { return nil, err } - peer := account.GetPeer(update.ID) - if peer == nil { - return nil, status.Errorf(status.NotFound, "peer %s not found", update.ID) + if user.AccountID != accountID { + return nil, status.NewUserNotPartOfAccountError() } + var peer *nbpeer.Peer + var settings *types.Settings + var peerGroupList []string var requiresPeerUpdates bool - update, requiresPeerUpdates, err = am.integratedPeerValidator.ValidatePeer(ctx, update, peer, userID, accountID, am.GetDNSDomain(), account.GetPeerGroupsList(peer.ID), account.Settings.Extra) + var peerLabelChanged bool + var sshChanged bool + var loginExpirationChanged bool + var inactivityExpirationChanged bool + + err = am.Store.ExecuteInTransaction(ctx, func(transaction store.Store) error { + peer, err = transaction.GetPeerByID(ctx, store.LockingStrengthUpdate, accountID, update.ID) + if err != nil { + return err + } + + settings, err = transaction.GetAccountSettings(ctx, store.LockingStrengthShare, accountID) + if err != nil { + return err + } + + peerGroupList, err = getPeerGroupIDs(ctx, transaction, accountID, update.ID) + if err != nil { + return err + } + + update, requiresPeerUpdates, err = am.integratedPeerValidator.ValidatePeer(ctx, update, peer, userID, accountID, am.GetDNSDomain(), peerGroupList, settings.Extra) + if err != nil { + return err + } + + if peer.Name != update.Name { + existingLabels, err := getPeerDNSLabels(ctx, transaction, accountID) + if err != nil { + return err + } + + newLabel, err := types.GetPeerHostLabel(update.Name, existingLabels) + if err != nil { + return err + } + + peer.Name = update.Name + peer.DNSLabel = newLabel + peerLabelChanged = true + } + + if peer.SSHEnabled != update.SSHEnabled { + peer.SSHEnabled = update.SSHEnabled + sshChanged = true + } + + if peer.LoginExpirationEnabled != update.LoginExpirationEnabled { + if !peer.AddedWithSSOLogin() { + return status.Errorf(status.PreconditionFailed, "this peer hasn't been added with the SSO login, therefore the login expiration can't be updated") + } + peer.LoginExpirationEnabled = update.LoginExpirationEnabled + loginExpirationChanged = true + } + + if peer.InactivityExpirationEnabled != update.InactivityExpirationEnabled { + if !peer.AddedWithSSOLogin() { + return status.Errorf(status.PreconditionFailed, "this peer hasn't been added with the SSO login, therefore the inactivity expiration can't be updated") + } + peer.InactivityExpirationEnabled = update.InactivityExpirationEnabled + inactivityExpirationChanged = true + } + + return transaction.SavePeer(ctx, store.LockingStrengthUpdate, accountID, peer) + }) if err != nil { return nil, err } - sshEnabledUpdated := peer.SSHEnabled != update.SSHEnabled - if sshEnabledUpdated { - peer.SSHEnabled = update.SSHEnabled + if sshChanged { event := activity.PeerSSHEnabled - if !update.SSHEnabled { + if !peer.SSHEnabled { event = activity.PeerSSHDisabled } am.StoreEvent(ctx, userID, peer.IP.String(), accountID, event, peer.EventMeta(am.GetDNSDomain())) } - peerLabelUpdated := peer.Name != update.Name - - if peerLabelUpdated { - peer.Name = update.Name - - existingLabels := account.GetPeerDNSLabels() - - newLabel, err := types.GetPeerHostLabel(peer.Name, existingLabels) - if err != nil { - return nil, err - } - - peer.DNSLabel = newLabel - + if peerLabelChanged { am.StoreEvent(ctx, userID, peer.ID, accountID, activity.PeerRenamed, peer.EventMeta(am.GetDNSDomain())) } - if peer.LoginExpirationEnabled != update.LoginExpirationEnabled { - - if !peer.AddedWithSSOLogin() { - return nil, status.Errorf(status.PreconditionFailed, "this peer hasn't been added with the SSO login, therefore the login expiration can't be updated") - } - - peer.LoginExpirationEnabled = update.LoginExpirationEnabled - + if loginExpirationChanged { event := activity.PeerLoginExpirationEnabled - if !update.LoginExpirationEnabled { + if !peer.LoginExpirationEnabled { event = activity.PeerLoginExpirationDisabled } am.StoreEvent(ctx, userID, peer.IP.String(), accountID, event, peer.EventMeta(am.GetDNSDomain())) - if peer.AddedWithSSOLogin() && peer.LoginExpirationEnabled && account.Settings.PeerLoginExpirationEnabled { - am.checkAndSchedulePeerLoginExpiration(ctx, account) + if peer.AddedWithSSOLogin() && peer.LoginExpirationEnabled && settings.PeerLoginExpirationEnabled { + am.checkAndSchedulePeerLoginExpiration(ctx, accountID) } } - if peer.InactivityExpirationEnabled != update.InactivityExpirationEnabled { - - if !peer.AddedWithSSOLogin() { - return nil, status.Errorf(status.PreconditionFailed, "this peer hasn't been added with the SSO login, therefore the login expiration can't be updated") - } - - peer.InactivityExpirationEnabled = update.InactivityExpirationEnabled - + if inactivityExpirationChanged { event := activity.PeerInactivityExpirationEnabled - if !update.InactivityExpirationEnabled { + if !peer.InactivityExpirationEnabled { event = activity.PeerInactivityExpirationDisabled } am.StoreEvent(ctx, userID, peer.IP.String(), accountID, event, peer.EventMeta(am.GetDNSDomain())) - if peer.AddedWithSSOLogin() && peer.InactivityExpirationEnabled && account.Settings.PeerInactivityExpirationEnabled { - am.checkAndSchedulePeerInactivityExpiration(ctx, account) + if peer.AddedWithSSOLogin() && peer.InactivityExpirationEnabled && settings.PeerInactivityExpirationEnabled { + am.checkAndSchedulePeerInactivityExpiration(ctx, accountID) } } - account.UpdatePeer(peer) - - err = am.Store.SaveAccount(ctx, account) - if err != nil { - return nil, err - } - - if peerLabelUpdated || requiresPeerUpdates { + if peerLabelChanged || requiresPeerUpdates { am.UpdateAccountPeers(ctx, accountID) - } else if sshEnabledUpdated { - am.UpdateAccountPeer(ctx, account, peer) + } else if sshChanged { + am.UpdateAccountPeer(ctx, accountID, peer.ID) } return peer, nil } -// deletePeers will delete all specified peers and send updates to the remote peers. Don't call without acquiring account lock -func (am *DefaultAccountManager) deletePeers(ctx context.Context, account *types.Account, peerIDs []string, userID string) error { - - // the first loop is needed to ensure all peers present under the account before modifying, otherwise - // we might have some inconsistencies - peers := make([]*nbpeer.Peer, 0, len(peerIDs)) - for _, peerID := range peerIDs { - - peer := account.GetPeer(peerID) - if peer == nil { - return status.Errorf(status.NotFound, "peer %s not found", peerID) - } - peers = append(peers, peer) - } - - // the 2nd loop performs the actual modification - for _, peer := range peers { - - err := am.integratedPeerValidator.PeerDeleted(ctx, account.Id, peer.ID) - if err != nil { - return err - } - - account.DeletePeer(peer.ID) - am.peersUpdateManager.SendUpdate(ctx, peer.ID, - &UpdateMessage{ - Update: &proto.SyncResponse{ - // fill those field for backward compatibility - RemotePeers: []*proto.RemotePeerConfig{}, - RemotePeersIsEmpty: true, - // new field - NetworkMap: &proto.NetworkMap{ - Serial: account.Network.CurrentSerial(), - RemotePeers: []*proto.RemotePeerConfig{}, - RemotePeersIsEmpty: true, - FirewallRules: []*proto.FirewallRule{}, - FirewallRulesIsEmpty: true, - }, - }, - NetworkMap: &types.NetworkMap{}, - }) - am.peersUpdateManager.CloseChannel(ctx, peer.ID) - am.StoreEvent(ctx, userID, peer.ID, account.Id, activity.PeerRemovedByUser, peer.EventMeta(am.GetDNSDomain())) - } - - return nil -} - // DeletePeer removes peer from the account by its IP func (am *DefaultAccountManager) DeletePeer(ctx context.Context, accountID, peerID, userID string) error { unlock := am.Store.AcquireWriteLockByUID(ctx, accountID) defer unlock() - account, err := am.Store.GetAccount(ctx, accountID) + if userID != activity.SystemInitiator { + user, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, userID) + if err != nil { + return err + } + + if user.AccountID != accountID { + return status.NewUserNotPartOfAccountError() + } + } + + peerAccountID, err := am.Store.GetAccountIDByPeerID(ctx, store.LockingStrengthShare, peerID) if err != nil { return err } - updateAccountPeers, err := am.isPeerInActiveGroup(ctx, account, peerID) - if err != nil { - return err + if peerAccountID != accountID { + return status.NewPeerNotPartOfAccountError() } - err = am.deletePeers(ctx, account, []string{peerID}, userID) - if err != nil { - return err - } + var peer *nbpeer.Peer + var updateAccountPeers bool + var eventsToStore []func() - err = am.Store.SaveAccount(ctx, account) - if err != nil { + err = am.Store.ExecuteInTransaction(ctx, func(transaction store.Store) error { + peer, err = transaction.GetPeerByID(ctx, store.LockingStrengthUpdate, accountID, peerID) + if err != nil { + return err + } + + updateAccountPeers, err = isPeerInActiveGroup(ctx, transaction, accountID, peerID) + if err != nil { + return err + } + + if err = transaction.IncrementNetworkSerial(ctx, store.LockingStrengthUpdate, accountID); err != nil { + return err + } + + eventsToStore, err = deletePeers(ctx, am, transaction, accountID, userID, []*nbpeer.Peer{peer}) return err + }) + + for _, storeEvent := range eventsToStore { + storeEvent() } if updateAccountPeers { @@ -386,7 +412,7 @@ func (am *DefaultAccountManager) GetNetworkMap(ctx context.Context, peerID strin groups[groupID] = group.Peers } - validatedPeers, err := am.integratedPeerValidator.GetValidatedPeers(account.Id, account.Groups, account.Peers, account.Settings.Extra) + validatedPeers, err := am.integratedPeerValidator.GetValidatedPeers(account.Id, maps.Values(account.Groups), maps.Values(account.Peers), account.Settings.Extra) if err != nil { return nil, err } @@ -425,7 +451,7 @@ func (am *DefaultAccountManager) AddPeer(ctx context.Context, setupKey, userID s addedByUser := false if len(userID) > 0 { addedByUser = true - accountID, err = am.Store.GetAccountIDByUserID(userID) + accountID, err = am.Store.GetAccountIDByUserID(ctx, store.LockingStrengthShare, userID) } else { accountID, err = am.Store.GetAccountIDBySetupKey(ctx, encodedHashedKey) } @@ -456,12 +482,13 @@ func (am *DefaultAccountManager) AddPeer(ctx context.Context, setupKey, userID s } var newPeer *nbpeer.Peer - var groupsToAdd []string + var updateAccountPeers bool err = am.Store.ExecuteInTransaction(ctx, func(transaction store.Store) error { var setupKeyID string var setupKeyName string var ephemeral bool + var groupsToAdd []string if addedByUser { user, err := transaction.GetUserByUserID(ctx, store.LockingStrengthUpdate, userID) if err != nil { @@ -503,7 +530,7 @@ func (am *DefaultAccountManager) AddPeer(ctx context.Context, setupKey, userID s return fmt.Errorf("failed to get free DNS label: %w", err) } - freeIP, err := am.getFreeIP(ctx, transaction, accountID) + freeIP, err := getFreeIP(ctx, transaction, accountID) if err != nil { return fmt.Errorf("failed to get free IP: %w", err) } @@ -521,7 +548,7 @@ func (am *DefaultAccountManager) AddPeer(ctx context.Context, setupKey, userID s Status: &nbpeer.PeerStatus{Connected: false, LastSeen: registrationTime}, SSHEnabled: false, SSHKey: peer.SSHKey, - LastLogin: util.ToPtr(registrationTime), + LastLogin: ®istrationTime, CreatedAt: registrationTime, LoginExpirationEnabled: addedByUser, Ephemeral: ephemeral, @@ -551,21 +578,21 @@ func (am *DefaultAccountManager) AddPeer(ctx context.Context, setupKey, userID s } newPeer = am.integratedPeerValidator.PreparePeer(ctx, accountID, newPeer, groupsToAdd, settings.Extra) - err = transaction.AddPeerToAllGroup(ctx, accountID, newPeer.ID) + err = transaction.AddPeerToAllGroup(ctx, store.LockingStrengthUpdate, accountID, newPeer.ID) if err != nil { return fmt.Errorf("failed adding peer to All group: %w", err) } if len(groupsToAdd) > 0 { for _, g := range groupsToAdd { - err = transaction.AddPeerToGroup(ctx, accountID, newPeer.ID, g) + err = transaction.AddPeerToGroup(ctx, store.LockingStrengthUpdate, accountID, newPeer.ID, g) if err != nil { return err } } } - err = transaction.AddPeerToAccount(ctx, newPeer) + err = transaction.AddPeerToAccount(ctx, store.LockingStrengthUpdate, newPeer) if err != nil { return fmt.Errorf("failed to add peer to account: %w", err) } @@ -587,6 +614,11 @@ func (am *DefaultAccountManager) AddPeer(ctx context.Context, setupKey, userID s } } + updateAccountPeers, err = isPeerInActiveGroup(ctx, transaction, accountID, newPeer.ID) + if err != nil { + return err + } + log.WithContext(ctx).Debugf("Peer %s added to account %s", newPeer.ID, accountID) return nil }) @@ -604,48 +636,20 @@ func (am *DefaultAccountManager) AddPeer(ctx context.Context, setupKey, userID s unlock() unlock = nil - account, err := am.requestBuffer.GetAccountWithBackpressure(ctx, accountID) - if err != nil { - return nil, nil, nil, status.NewGetAccountError(err) - } - - allGroup, err := account.GetGroupAll() - if err != nil { - return nil, nil, nil, fmt.Errorf("error getting all group ID: %w", err) - } - groupsToAdd = append(groupsToAdd, allGroup.ID) - - newGroupsAffectsPeers, err := areGroupChangesAffectPeers(ctx, am.Store, accountID, groupsToAdd) - if err != nil { - return nil, nil, nil, err - } - - if newGroupsAffectsPeers { + if updateAccountPeers { am.UpdateAccountPeers(ctx, accountID) } - approvedPeersMap, err := am.GetValidatedPeers(account) - if err != nil { - return nil, nil, nil, err - } - - postureChecks, err := am.getPeerPostureChecks(account, newPeer.ID) - if err != nil { - return nil, nil, nil, err - } - - customZone := account.GetPeersCustomZone(ctx, am.dnsDomain) - networkMap := account.GetPeerNetworkMap(ctx, newPeer.ID, customZone, approvedPeersMap, account.GetResourcePoliciesMap(), account.GetResourceRoutersMap(), am.metrics.AccountManagerMetrics()) - return newPeer, networkMap, postureChecks, nil + return am.getValidatedPeerWithMap(ctx, false, accountID, newPeer) } -func (am *DefaultAccountManager) getFreeIP(ctx context.Context, s store.Store, accountID string) (net.IP, error) { - takenIps, err := s.GetTakenIPs(ctx, store.LockingStrengthUpdate, accountID) +func getFreeIP(ctx context.Context, transaction store.Store, accountID string) (net.IP, error) { + takenIps, err := transaction.GetTakenIPs(ctx, store.LockingStrengthShare, accountID) if err != nil { return nil, fmt.Errorf("failed to get taken IPs: %w", err) } - network, err := s.GetAccountNetwork(ctx, store.LockingStrengthUpdate, accountID) + network, err := transaction.GetAccountNetwork(ctx, store.LockingStrengthUpdate, accountID) if err != nil { return nil, fmt.Errorf("failed getting network: %w", err) } @@ -659,72 +663,79 @@ func (am *DefaultAccountManager) getFreeIP(ctx context.Context, s store.Store, a } // SyncPeer checks whether peer is eligible for receiving NetworkMap (authenticated) and returns its NetworkMap if eligible -func (am *DefaultAccountManager) SyncPeer(ctx context.Context, sync PeerSync, account *types.Account) (*nbpeer.Peer, *types.NetworkMap, []*posture.Checks, error) { +func (am *DefaultAccountManager) SyncPeer(ctx context.Context, sync PeerSync, accountID string) (*nbpeer.Peer, *types.NetworkMap, []*posture.Checks, error) { start := time.Now() defer func() { log.WithContext(ctx).Debugf("SyncPeer: took %v", time.Since(start)) }() - peer, err := account.FindPeerByPubKey(sync.WireGuardPubKey) + var peer *nbpeer.Peer + var peerNotValid bool + var isStatusChanged bool + var updated bool + var err error + var postureChecks []*posture.Checks + + settings, err := am.Store.GetAccountSettings(ctx, store.LockingStrengthShare, accountID) if err != nil { - return nil, nil, nil, status.NewPeerNotRegisteredError() + return nil, nil, nil, err } - if peer.UserID != "" { - user, err := account.FindUser(peer.UserID) + err = am.Store.ExecuteInTransaction(ctx, func(transaction store.Store) error { + peer, err = transaction.GetPeerByPeerPubKey(ctx, store.LockingStrengthUpdate, sync.WireGuardPubKey) if err != nil { - return nil, nil, nil, fmt.Errorf("failed to get user: %w", err) + return status.NewPeerNotRegisteredError() } - err = checkIfPeerOwnerIsBlocked(peer, user) - if err != nil { - return nil, nil, nil, err + if peer.UserID != "" { + user, err := transaction.GetUserByUserID(ctx, store.LockingStrengthShare, peer.UserID) + if err != nil { + return err + } + + if err = checkIfPeerOwnerIsBlocked(peer, user); err != nil { + return err + } } - } - if peerLoginExpired(ctx, peer, account.Settings) { - return nil, nil, nil, status.NewPeerLoginExpiredError() - } - - updated := peer.UpdateMetaIfNew(sync.Meta) - if updated { - am.metrics.AccountManagerMetrics().CountPeerMetUpdate() - account.Peers[peer.ID] = peer - log.WithContext(ctx).Tracef("peer %s metadata updated", peer.ID) - err = am.Store.SavePeer(ctx, account.Id, peer) - if err != nil { - return nil, nil, nil, fmt.Errorf("failed to save peer: %w", err) + if peerLoginExpired(ctx, peer, settings) { + return status.NewPeerLoginExpiredError() } - } - peerNotValid, isStatusChanged, err := am.integratedPeerValidator.IsNotValidPeer(ctx, account.Id, peer, account.GetPeerGroupsList(peer.ID), account.Settings.Extra) - if err != nil { - return nil, nil, nil, fmt.Errorf("failed to validate peer: %w", err) - } + peerGroupIDs, err := getPeerGroupIDs(ctx, transaction, accountID, peer.ID) + if err != nil { + return err + } - postureChecks, err := am.getPeerPostureChecks(account, peer.ID) + peerNotValid, isStatusChanged, err = am.integratedPeerValidator.IsNotValidPeer(ctx, accountID, peer, peerGroupIDs, settings.Extra) + if err != nil { + return err + } + + updated = peer.UpdateMetaIfNew(sync.Meta) + if updated { + am.metrics.AccountManagerMetrics().CountPeerMetUpdate() + log.WithContext(ctx).Tracef("peer %s metadata updated", peer.ID) + if err = transaction.SavePeer(ctx, store.LockingStrengthUpdate, accountID, peer); err != nil { + return err + } + + postureChecks, err = getPeerPostureChecks(ctx, transaction, accountID, peer.ID) + if err != nil { + return err + } + } + return nil + }) if err != nil { return nil, nil, nil, err } if isStatusChanged || sync.UpdateAccountPeers || (updated && len(postureChecks) > 0) { - am.UpdateAccountPeers(ctx, account.Id) + am.UpdateAccountPeers(ctx, accountID) } - if peerNotValid { - emptyMap := &types.NetworkMap{ - Network: account.Network.Copy(), - } - return peer, emptyMap, []*posture.Checks{}, nil - } - - validPeersMap, err := am.GetValidatedPeers(account) - if err != nil { - return nil, nil, nil, fmt.Errorf("failed to get validated peers: %w", err) - } - - customZone := account.GetPeersCustomZone(ctx, am.dnsDomain) - return peer, account.GetPeerNetworkMap(ctx, peer.ID, customZone, validPeersMap, account.GetResourcePoliciesMap(), account.GetResourceRoutersMap(), am.metrics.AccountManagerMetrics()), postureChecks, nil + return am.getValidatedPeerWithMap(ctx, peerNotValid, accountID, peer) } func (am *DefaultAccountManager) handlePeerLoginNotFound(ctx context.Context, login PeerLogin, err error) (*nbpeer.Peer, *types.NetworkMap, []*posture.Checks, error) { @@ -772,92 +783,150 @@ func (am *DefaultAccountManager) LoginPeer(ctx context.Context, login PeerLogin) } }() - peer, err := am.Store.GetPeerByPeerPubKey(ctx, store.LockingStrengthUpdate, login.WireGuardPubKey) - if err != nil { - return nil, nil, nil, err - } + var peer *nbpeer.Peer + var updateRemotePeers bool + var isRequiresApproval bool + var isStatusChanged bool + var isPeerUpdated bool + var postureChecks []*posture.Checks settings, err := am.Store.GetAccountSettings(ctx, store.LockingStrengthShare, accountID) if err != nil { return nil, nil, nil, err } - // this flag prevents unnecessary calls to the persistent store. - shouldStorePeer := false - updateRemotePeers := false - - if login.UserID != "" { - if peer.UserID != login.UserID { - log.Warnf("user mismatch when logging in peer %s: peer user %s, login user %s ", peer.ID, peer.UserID, login.UserID) - return nil, nil, nil, status.Errorf(status.Unauthenticated, "invalid user") - } - - changed, err := am.handleUserPeer(ctx, peer, settings) + err = am.Store.ExecuteInTransaction(ctx, func(transaction store.Store) error { + peer, err = transaction.GetPeerByPeerPubKey(ctx, store.LockingStrengthUpdate, login.WireGuardPubKey) if err != nil { - return nil, nil, nil, err + return err } - if changed { - shouldStorePeer = true - updateRemotePeers = true - } - } - groups, err := am.Store.GetAccountGroups(ctx, store.LockingStrengthShare, accountID) - if err != nil { - return nil, nil, nil, err - } + // this flag prevents unnecessary calls to the persistent store. + shouldStorePeer := false - var grps []string - for _, group := range groups { - for _, id := range group.Peers { - if id == peer.ID { - grps = append(grps, group.ID) - break + if login.UserID != "" { + if peer.UserID != login.UserID { + log.Warnf("user mismatch when logging in peer %s: peer user %s, login user %s ", peer.ID, peer.UserID, login.UserID) + return status.Errorf(status.Unauthenticated, "invalid user") + } + + changed, err := am.handleUserPeer(ctx, transaction, peer, settings) + if err != nil { + return err + } + + if changed { + shouldStorePeer = true + updateRemotePeers = true } } - } - isRequiresApproval, isStatusChanged, err := am.integratedPeerValidator.IsNotValidPeer(ctx, accountID, peer, grps, settings.Extra) + peerGroupIDs, err := getPeerGroupIDs(ctx, transaction, accountID, peer.ID) + if err != nil { + return err + } + + isRequiresApproval, isStatusChanged, err = am.integratedPeerValidator.IsNotValidPeer(ctx, accountID, peer, peerGroupIDs, settings.Extra) + if err != nil { + return err + } + + isPeerUpdated = peer.UpdateMetaIfNew(login.Meta) + if isPeerUpdated { + am.metrics.AccountManagerMetrics().CountPeerMetUpdate() + shouldStorePeer = true + + postureChecks, err = getPeerPostureChecks(ctx, transaction, accountID, peer.ID) + if err != nil { + return err + } + } + + if peer.SSHKey != login.SSHKey { + peer.SSHKey = login.SSHKey + shouldStorePeer = true + } + + if shouldStorePeer { + if err = transaction.SavePeer(ctx, store.LockingStrengthUpdate, accountID, peer); err != nil { + return err + } + } + + return nil + }) if err != nil { return nil, nil, nil, err } - updated := peer.UpdateMetaIfNew(login.Meta) - if updated { - am.metrics.AccountManagerMetrics().CountPeerMetUpdate() - shouldStorePeer = true - } - - if peer.SSHKey != login.SSHKey { - peer.SSHKey = login.SSHKey - shouldStorePeer = true - } - - if shouldStorePeer { - err = am.Store.SavePeer(ctx, accountID, peer) - if err != nil { - return nil, nil, nil, err - } - } - unlockPeer() unlockPeer = nil - account, err := am.requestBuffer.GetAccountWithBackpressure(ctx, accountID) - if err != nil { - return nil, nil, nil, err - } - - postureChecks, err := am.getPeerPostureChecks(account, peer.ID) - if err != nil { - return nil, nil, nil, err - } - - if updateRemotePeers || isStatusChanged || (updated && len(postureChecks) > 0) { + if updateRemotePeers || isStatusChanged || (isPeerUpdated && len(postureChecks) > 0) { am.UpdateAccountPeers(ctx, accountID) } - return am.getValidatedPeerWithMap(ctx, isRequiresApproval, account, peer) + return am.getValidatedPeerWithMap(ctx, isRequiresApproval, accountID, peer) +} + +// getPeerPostureChecks returns the posture checks for the peer. +func getPeerPostureChecks(ctx context.Context, transaction store.Store, accountID, peerID string) ([]*posture.Checks, error) { + policies, err := transaction.GetAccountPolicies(ctx, store.LockingStrengthShare, accountID) + if err != nil { + return nil, err + } + + if len(policies) == 0 { + return nil, nil + } + + var peerPostureChecksIDs []string + + for _, policy := range policies { + if !policy.Enabled || len(policy.SourcePostureChecks) == 0 { + continue + } + + postureChecksIDs, err := processPeerPostureChecks(ctx, transaction, policy, accountID, peerID) + if err != nil { + return nil, err + } + + peerPostureChecksIDs = append(peerPostureChecksIDs, postureChecksIDs...) + } + + peerPostureChecks, err := transaction.GetPostureChecksByIDs(ctx, store.LockingStrengthShare, accountID, peerPostureChecksIDs) + if err != nil { + return nil, err + } + + return maps.Values(peerPostureChecks), nil +} + +// processPeerPostureChecks checks if the peer is in the source group of the policy and returns the posture checks. +func processPeerPostureChecks(ctx context.Context, transaction store.Store, policy *types.Policy, accountID, peerID string) ([]string, error) { + for _, rule := range policy.Rules { + if !rule.Enabled { + continue + } + + sourceGroups, err := transaction.GetGroupsByIDs(ctx, store.LockingStrengthShare, accountID, rule.Sources) + if err != nil { + return nil, err + } + + for _, sourceGroup := range rule.Sources { + group, ok := sourceGroups[sourceGroup] + if !ok { + return nil, fmt.Errorf("failed to check peer in policy source group") + } + + if slices.Contains(group.Peers, peerID) { + return policy.SourcePostureChecks, nil + } + } + } + return nil, nil } // checkIFPeerNeedsLoginWithoutLock checks if the peer needs login without acquiring the account lock. The check validate if the peer was not added via SSO @@ -889,22 +958,35 @@ func (am *DefaultAccountManager) checkIFPeerNeedsLoginWithoutLock(ctx context.Co return nil } -func (am *DefaultAccountManager) getValidatedPeerWithMap(ctx context.Context, isRequiresApproval bool, account *types.Account, peer *nbpeer.Peer) (*nbpeer.Peer, *types.NetworkMap, []*posture.Checks, error) { - var postureChecks []*posture.Checks +func (am *DefaultAccountManager) getValidatedPeerWithMap(ctx context.Context, isRequiresApproval bool, accountID string, peer *nbpeer.Peer) (*nbpeer.Peer, *types.NetworkMap, []*posture.Checks, error) { + start := time.Now() + defer func() { + log.WithContext(ctx).Debugf("getValidatedPeerWithMap: took %s", time.Since(start)) + }() if isRequiresApproval { + network, err := am.Store.GetAccountNetwork(ctx, store.LockingStrengthShare, accountID) + if err != nil { + return nil, nil, nil, err + } + emptyMap := &types.NetworkMap{ - Network: account.Network.Copy(), + Network: network.Copy(), } return peer, emptyMap, nil, nil } - approvedPeersMap, err := am.GetValidatedPeers(account) + account, err := am.Store.GetAccount(ctx, accountID) if err != nil { return nil, nil, nil, err } - postureChecks, err = am.getPeerPostureChecks(account, peer.ID) + approvedPeersMap, err := am.integratedPeerValidator.GetValidatedPeers(account.Id, maps.Values(account.Groups), maps.Values(account.Peers), account.Settings.Extra) + if err != nil { + return nil, nil, nil, err + } + + postureChecks, err := am.getPeerPostureChecks(account, peer.ID) if err != nil { return nil, nil, nil, err } @@ -913,7 +995,7 @@ func (am *DefaultAccountManager) getValidatedPeerWithMap(ctx context.Context, is return peer, account.GetPeerNetworkMap(ctx, peer.ID, customZone, approvedPeersMap, account.GetResourcePoliciesMap(), account.GetResourceRoutersMap(), am.metrics.AccountManagerMetrics()), postureChecks, nil } -func (am *DefaultAccountManager) handleExpiredPeer(ctx context.Context, user *types.User, peer *nbpeer.Peer) error { +func (am *DefaultAccountManager) handleExpiredPeer(ctx context.Context, transaction store.Store, user *types.User, peer *nbpeer.Peer) error { err := checkAuth(ctx, user.Id, peer) if err != nil { return err @@ -921,12 +1003,12 @@ func (am *DefaultAccountManager) handleExpiredPeer(ctx context.Context, user *ty // If peer was expired before and if it reached this point, it is re-authenticated. // UserID is present, meaning that JWT validation passed successfully in the API layer. peer = peer.UpdateLastLogin() - err = am.Store.SavePeer(ctx, peer.AccountID, peer) + err = transaction.SavePeer(ctx, store.LockingStrengthUpdate, peer.AccountID, peer) if err != nil { return err } - err = am.Store.SaveUserLastLogin(ctx, user.AccountID, user.Id, peer.GetLastLogin()) + err = transaction.SaveUserLastLogin(ctx, user.AccountID, user.Id, peer.GetLastLogin()) if err != nil { return err } @@ -968,41 +1050,47 @@ func peerLoginExpired(ctx context.Context, peer *nbpeer.Peer, settings *types.Se // GetPeer for a given accountID, peerID and userID error if not found. func (am *DefaultAccountManager) GetPeer(ctx context.Context, accountID, peerID, userID string) (*nbpeer.Peer, error) { - unlock := am.Store.AcquireWriteLockByUID(ctx, accountID) - defer unlock() - - account, err := am.Store.GetAccount(ctx, accountID) + user, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, userID) if err != nil { return nil, err } - user, err := account.FindUser(userID) + if user.AccountID != accountID { + return nil, status.NewUserNotPartOfAccountError() + } + + settings, err := am.Store.GetAccountSettings(ctx, store.LockingStrengthShare, accountID) if err != nil { return nil, err } - if !user.HasAdminPower() && !user.IsServiceUser && account.Settings.RegularUsersViewBlocked { + if user.IsRegularUser() && settings.RegularUsersViewBlocked { return nil, status.Errorf(status.Internal, "user %s has no access to his own peer %s under account %s", userID, peerID, accountID) } - peer := account.GetPeer(peerID) - if peer == nil { - return nil, status.Errorf(status.NotFound, "peer with %s not found under account %s", peerID, accountID) + peer, err := am.Store.GetPeerByID(ctx, store.LockingStrengthShare, accountID, peerID) + if err != nil { + return nil, err } // if admin or user owns this peer, return peer - if user.HasAdminPower() || user.IsServiceUser || peer.UserID == userID { + if user.IsAdminOrServiceUser() || peer.UserID == userID { return peer, nil } // it is also possible that user doesn't own the peer but some of his peers have access to it, // this is a valid case, show the peer as well. - userPeers, err := account.FindUserPeers(userID) + userPeers, err := am.Store.GetUserPeers(ctx, store.LockingStrengthShare, accountID, userID) if err != nil { return nil, err } - approvedPeersMap, err := am.GetValidatedPeers(account) + account, err := am.requestBuffer.GetAccountWithBackpressure(ctx, accountID) + if err != nil { + return nil, err + } + + approvedPeersMap, err := am.integratedPeerValidator.GetValidatedPeers(accountID, maps.Values(account.Groups), maps.Values(account.Peers), account.Settings.Extra) if err != nil { return nil, err } @@ -1024,7 +1112,7 @@ func (am *DefaultAccountManager) GetPeer(ctx context.Context, accountID, peerID, func (am *DefaultAccountManager) UpdateAccountPeers(ctx context.Context, accountID string) { account, err := am.requestBuffer.GetAccountWithBackpressure(ctx, accountID) if err != nil { - log.WithContext(ctx).Errorf("failed to send out updates to peers: %v", err) + log.WithContext(ctx).Errorf("failed to send out updates to peers. failed to get account: %v", err) return } @@ -1035,11 +1123,9 @@ func (am *DefaultAccountManager) UpdateAccountPeers(ctx context.Context, account } }() - peers := account.GetPeers() - - approvedPeersMap, err := am.GetValidatedPeers(account) + approvedPeersMap, err := am.integratedPeerValidator.GetValidatedPeers(account.Id, maps.Values(account.Groups), maps.Values(account.Peers), account.Settings.Extra) if err != nil { - log.WithContext(ctx).Errorf("failed to send out updates to peers, failed to validate peer: %v", err) + log.WithContext(ctx).Errorf("failed to send out updates to peers, failed to get validate peers: %v", err) return } @@ -1051,7 +1137,7 @@ func (am *DefaultAccountManager) UpdateAccountPeers(ctx context.Context, account resourcePolicies := account.GetResourcePoliciesMap() routers := account.GetResourceRoutersMap() - for _, peer := range peers { + for _, peer := range account.Peers { if !am.peersUpdateManager.HasChannel(peer.ID) { log.WithContext(ctx).Tracef("peer %s doesn't have a channel, skipping network map update", peer.ID) continue @@ -1065,7 +1151,7 @@ func (am *DefaultAccountManager) UpdateAccountPeers(ctx context.Context, account postureChecks, err := am.getPeerPostureChecks(account, p.ID) if err != nil { - log.WithContext(ctx).Errorf("failed to send out updates to peers, failed to get peer: %s posture checks: %v", p.ID, err) + log.WithContext(ctx).Debugf("failed to get posture checks for peer %s: %v", peer.ID, err) return } @@ -1080,15 +1166,27 @@ func (am *DefaultAccountManager) UpdateAccountPeers(ctx context.Context, account // UpdateAccountPeer updates a single peer that belongs to an account. // Should be called when changes need to be synced to a specific peer only. -func (am *DefaultAccountManager) UpdateAccountPeer(ctx context.Context, account *types.Account, peer *nbpeer.Peer) { - if !am.peersUpdateManager.HasChannel(peer.ID) { - log.WithContext(ctx).Tracef("peer %s doesn't have a channel, skipping network map update", peer.ID) +func (am *DefaultAccountManager) UpdateAccountPeer(ctx context.Context, accountId string, peerId string) { + if !am.peersUpdateManager.HasChannel(peerId) { + log.WithContext(ctx).Tracef("peer %s doesn't have a channel, skipping network map update", peerId) return } - approvedPeersMap, err := am.GetValidatedPeers(account) + account, err := am.requestBuffer.GetAccountWithBackpressure(ctx, accountId) if err != nil { - log.WithContext(ctx).Errorf("failed to send update to peer %s, failed to validate peers: %v", peer.ID, err) + log.WithContext(ctx).Errorf("failed to send out updates to peer %s. failed to get account: %v", peerId, err) + return + } + + peer := account.GetPeer(peerId) + if peer == nil { + log.WithContext(ctx).Tracef("peer %s doesn't exists in account %s", peerId, accountId) + return + } + + approvedPeersMap, err := am.integratedPeerValidator.GetValidatedPeers(account.Id, maps.Values(account.Groups), maps.Values(account.Peers), account.Settings.Extra) + if err != nil { + log.WithContext(ctx).Errorf("failed to send update to peer %s, failed to validate peers: %v", peerId, err) return } @@ -1097,17 +1195,235 @@ func (am *DefaultAccountManager) UpdateAccountPeer(ctx context.Context, account resourcePolicies := account.GetResourcePoliciesMap() routers := account.GetResourceRoutersMap() - postureChecks, err := am.getPeerPostureChecks(account, peer.ID) + postureChecks, err := am.getPeerPostureChecks(account, peerId) if err != nil { - log.WithContext(ctx).Errorf("failed to send update to peer %s, failed to get posture checks: %v", peer.ID, err) + log.WithContext(ctx).Errorf("failed to send update to peer %s, failed to get posture checks: %v", peerId, err) return } - remotePeerNetworkMap := account.GetPeerNetworkMap(ctx, peer.ID, customZone, approvedPeersMap, resourcePolicies, routers, am.metrics.AccountManagerMetrics()) + remotePeerNetworkMap := account.GetPeerNetworkMap(ctx, peerId, customZone, approvedPeersMap, resourcePolicies, routers, am.metrics.AccountManagerMetrics()) update := toSyncResponse(ctx, nil, peer, nil, nil, remotePeerNetworkMap, am.GetDNSDomain(), postureChecks, dnsCache, account.Settings.RoutingPeerDNSResolutionEnabled) am.peersUpdateManager.SendUpdate(ctx, peer.ID, &UpdateMessage{Update: update, NetworkMap: remotePeerNetworkMap}) } +// getNextPeerExpiration returns the minimum duration in which the next peer of the account will expire if it was found. +// If there is no peer that expires this function returns false and a duration of 0. +// This function only considers peers that haven't been expired yet and that are connected. +func (am *DefaultAccountManager) getNextPeerExpiration(ctx context.Context, accountID string) (time.Duration, bool) { + peersWithExpiry, err := am.Store.GetAccountPeersWithExpiration(ctx, store.LockingStrengthShare, accountID) + if err != nil { + log.WithContext(ctx).Errorf("failed to get peers with expiration: %v", err) + return peerSchedulerRetryInterval, true + } + + if len(peersWithExpiry) == 0 { + return 0, false + } + + settings, err := am.Store.GetAccountSettings(ctx, store.LockingStrengthShare, accountID) + if err != nil { + log.WithContext(ctx).Errorf("failed to get account settings: %v", err) + return peerSchedulerRetryInterval, true + } + + var nextExpiry *time.Duration + for _, peer := range peersWithExpiry { + // consider only connected peers because others will require login on connecting to the management server + if peer.Status.LoginExpired || !peer.Status.Connected { + continue + } + _, duration := peer.LoginExpired(settings.PeerLoginExpiration) + if nextExpiry == nil || duration < *nextExpiry { + // if expiration is below 1s return 1s duration + // this avoids issues with ticker that can't be set to < 0 + if duration < time.Second { + return time.Second, true + } + nextExpiry = &duration + } + } + + if nextExpiry == nil { + return 0, false + } + + return *nextExpiry, true +} + +// GetNextInactivePeerExpiration returns the minimum duration in which the next peer of the account will expire if it was found. +// If there is no peer that expires this function returns false and a duration of 0. +// This function only considers peers that haven't been expired yet and that are not connected. +func (am *DefaultAccountManager) getNextInactivePeerExpiration(ctx context.Context, accountID string) (time.Duration, bool) { + peersWithInactivity, err := am.Store.GetAccountPeersWithInactivity(ctx, store.LockingStrengthShare, accountID) + if err != nil { + log.WithContext(ctx).Errorf("failed to get peers with inactivity: %v", err) + return peerSchedulerRetryInterval, true + } + + if len(peersWithInactivity) == 0 { + return 0, false + } + + settings, err := am.Store.GetAccountSettings(ctx, store.LockingStrengthShare, accountID) + if err != nil { + log.WithContext(ctx).Errorf("failed to get account settings: %v", err) + return peerSchedulerRetryInterval, true + } + + var nextExpiry *time.Duration + for _, peer := range peersWithInactivity { + if peer.Status.LoginExpired || peer.Status.Connected { + continue + } + _, duration := peer.SessionExpired(settings.PeerInactivityExpiration) + if nextExpiry == nil || duration < *nextExpiry { + // if expiration is below 1s return 1s duration + // this avoids issues with ticker that can't be set to < 0 + if duration < time.Second { + return time.Second, true + } + nextExpiry = &duration + } + } + + if nextExpiry == nil { + return 0, false + } + + return *nextExpiry, true +} + +// getExpiredPeers returns peers that have been expired. +func (am *DefaultAccountManager) getExpiredPeers(ctx context.Context, accountID string) ([]*nbpeer.Peer, error) { + peersWithExpiry, err := am.Store.GetAccountPeersWithExpiration(ctx, store.LockingStrengthShare, accountID) + if err != nil { + return nil, err + } + + settings, err := am.Store.GetAccountSettings(ctx, store.LockingStrengthShare, accountID) + if err != nil { + return nil, err + } + + var peers []*nbpeer.Peer + for _, peer := range peersWithExpiry { + expired, _ := peer.LoginExpired(settings.PeerLoginExpiration) + if expired { + peers = append(peers, peer) + } + } + + return peers, nil +} + +// getInactivePeers returns peers that have been expired by inactivity +func (am *DefaultAccountManager) getInactivePeers(ctx context.Context, accountID string) ([]*nbpeer.Peer, error) { + peersWithInactivity, err := am.Store.GetAccountPeersWithInactivity(ctx, store.LockingStrengthShare, accountID) + if err != nil { + return nil, err + } + + settings, err := am.Store.GetAccountSettings(ctx, store.LockingStrengthShare, accountID) + if err != nil { + return nil, err + } + + var peers []*nbpeer.Peer + for _, inactivePeer := range peersWithInactivity { + inactive, _ := inactivePeer.SessionExpired(settings.PeerInactivityExpiration) + if inactive { + peers = append(peers, inactivePeer) + } + } + + return peers, nil +} + +// GetPeerGroups returns groups that the peer is part of. +func (am *DefaultAccountManager) GetPeerGroups(ctx context.Context, accountID, peerID string) ([]*types.Group, error) { + return am.Store.GetPeerGroups(ctx, store.LockingStrengthShare, accountID, peerID) +} + +// getPeerGroupIDs returns the IDs of the groups that the peer is part of. +func getPeerGroupIDs(ctx context.Context, transaction store.Store, accountID string, peerID string) ([]string, error) { + groups, err := transaction.GetPeerGroups(ctx, store.LockingStrengthShare, accountID, peerID) + if err != nil { + return nil, err + } + + groupIDs := make([]string, 0, len(groups)) + for _, group := range groups { + groupIDs = append(groupIDs, group.ID) + } + + return groupIDs, err +} + +func getPeerDNSLabels(ctx context.Context, transaction store.Store, accountID string) (types.LookupMap, error) { + dnsLabels, err := transaction.GetPeerLabelsInAccount(ctx, store.LockingStrengthShare, accountID) + if err != nil { + return nil, err + } + + existingLabels := make(types.LookupMap) + for _, label := range dnsLabels { + existingLabels[label] = struct{}{} + } + return existingLabels, nil +} + +// IsPeerInActiveGroup checks if the given peer is part of a group that is used +// in an active DNS, route, or ACL configuration. +func isPeerInActiveGroup(ctx context.Context, transaction store.Store, accountID, peerID string) (bool, error) { + peerGroupIDs, err := getPeerGroupIDs(ctx, transaction, accountID, peerID) + if err != nil { + return false, err + } + return areGroupChangesAffectPeers(ctx, transaction, accountID, peerGroupIDs) // TODO: use transaction +} + +// deletePeers deletes all specified peers and sends updates to the remote peers. +// Returns a slice of functions to save events after successful peer deletion. +func deletePeers(ctx context.Context, am *DefaultAccountManager, transaction store.Store, accountID, userID string, peers []*nbpeer.Peer) ([]func(), error) { + var peerDeletedEvents []func() + + for _, peer := range peers { + if err := am.integratedPeerValidator.PeerDeleted(ctx, accountID, peer.ID); err != nil { + return nil, err + } + + network, err := transaction.GetAccountNetwork(ctx, store.LockingStrengthShare, accountID) + if err != nil { + return nil, err + } + + if err = transaction.DeletePeer(ctx, store.LockingStrengthUpdate, accountID, peer.ID); err != nil { + return nil, err + } + + am.peersUpdateManager.SendUpdate(ctx, peer.ID, &UpdateMessage{ + Update: &proto.SyncResponse{ + RemotePeers: []*proto.RemotePeerConfig{}, + RemotePeersIsEmpty: true, + NetworkMap: &proto.NetworkMap{ + Serial: network.CurrentSerial(), + RemotePeers: []*proto.RemotePeerConfig{}, + RemotePeersIsEmpty: true, + FirewallRules: []*proto.FirewallRule{}, + FirewallRulesIsEmpty: true, + }, + }, + NetworkMap: &types.NetworkMap{}, + }) + am.peersUpdateManager.CloseChannel(ctx, peer.ID) + peerDeletedEvents = append(peerDeletedEvents, func() { + am.StoreEvent(ctx, userID, peer.ID, accountID, activity.PeerRemovedByUser, peer.EventMeta(am.GetDNSDomain())) + }) + } + + return peerDeletedEvents, nil +} + func ConvertSliceToMap(existingLabels []string) map[string]struct{} { labelMap := make(map[string]struct{}, len(existingLabels)) for _, label := range existingLabels { @@ -1115,15 +1431,3 @@ func ConvertSliceToMap(existingLabels []string) map[string]struct{} { } return labelMap } - -// IsPeerInActiveGroup checks if the given peer is part of a group that is used -// in an active DNS, route, or ACL configuration. -func (am *DefaultAccountManager) isPeerInActiveGroup(ctx context.Context, account *types.Account, peerID string) (bool, error) { - peerGroupIDs := make([]string, 0) - for _, group := range account.Groups { - if slices.Contains(group.Peers, peerID) { - peerGroupIDs = append(peerGroupIDs, group.ID) - } - } - return areGroupChangesAffectPeers(ctx, am.Store, account.Id, peerGroupIDs) -} diff --git a/management/server/peer/peer.go b/management/server/peer/peer.go index 355d78ce0..199c7c89d 100644 --- a/management/server/peer/peer.go +++ b/management/server/peer/peer.go @@ -46,7 +46,7 @@ type Peer struct { // CreatedAt records the time the peer was created CreatedAt time.Time // Indicate ephemeral peer attribute - Ephemeral bool + Ephemeral bool `gorm:"index"` // Geo location based on connection IP Location Location `gorm:"embedded;embeddedPrefix:location_"` } diff --git a/management/server/peer_test.go b/management/server/peer_test.go index 2f5d0e047..bf712f38a 100644 --- a/management/server/peer_test.go +++ b/management/server/peer_test.go @@ -938,7 +938,7 @@ func BenchmarkUpdateAccountPeers(b *testing.B) { {"Small single", 50, 10, 90, 120, 90, 120}, {"Medium single", 500, 10, 110, 170, 120, 200}, {"Large 5", 5000, 15, 1300, 2100, 4900, 7000}, - {"Extra Large", 2000, 2000, 1300, 2400, 3800, 6400}, + {"Extra Large", 2000, 2000, 1300, 2400, 3000, 6400}, } log.SetOutput(io.Discard) diff --git a/management/server/status/error.go b/management/server/status/error.go index d9cab0231..7e384922d 100644 --- a/management/server/status/error.go +++ b/management/server/status/error.go @@ -86,6 +86,11 @@ func NewAccountNotFoundError(accountKey string) error { return Errorf(NotFound, "account not found: %s", accountKey) } +// NewPeerNotPartOfAccountError creates a new Error with PermissionDenied type for a peer not being part of an account +func NewPeerNotPartOfAccountError() error { + return Errorf(PermissionDenied, "peer is not part of this account") +} + // NewUserNotFoundError creates a new Error with NotFound type for a missing user func NewUserNotFoundError(userKey string) error { return Errorf(NotFound, "user not found: %s", userKey) diff --git a/management/server/store/sql_store.go b/management/server/store/sql_store.go index 7b1a63411..900d81322 100644 --- a/management/server/store/sql_store.go +++ b/management/server/store/sql_store.go @@ -313,12 +313,12 @@ func (s *SqlStore) GetInstallationID() string { return installation.InstallationIDValue } -func (s *SqlStore) SavePeer(ctx context.Context, accountID string, peer *nbpeer.Peer) error { +func (s *SqlStore) SavePeer(ctx context.Context, lockStrength LockingStrength, accountID string, peer *nbpeer.Peer) error { // To maintain data integrity, we create a copy of the peer's to prevent unintended updates to other fields. peerCopy := peer.Copy() peerCopy.AccountID = accountID - err := s.db.Transaction(func(tx *gorm.DB) error { + err := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Transaction(func(tx *gorm.DB) error { // check if peer exists before saving var peerID string result := tx.Model(&nbpeer.Peer{}).Select("id").Find(&peerID, accountAndIDQueryCondition, accountID, peer.ID) @@ -332,7 +332,7 @@ func (s *SqlStore) SavePeer(ctx context.Context, accountID string, peer *nbpeer. result = tx.Model(&nbpeer.Peer{}).Where(accountAndIDQueryCondition, accountID, peer.ID).Save(peerCopy) if result.Error != nil { - return result.Error + return status.Errorf(status.Internal, "failed to save peer to store: %v", result.Error) } return nil @@ -358,7 +358,7 @@ func (s *SqlStore) UpdateAccountDomainAttributes(ctx context.Context, accountID Where(idQueryCondition, accountID). Updates(&accountCopy) if result.Error != nil { - return result.Error + return status.Errorf(status.Internal, "failed to update account domain attributes to store: %v", result.Error) } if result.RowsAffected == 0 { @@ -368,7 +368,7 @@ func (s *SqlStore) UpdateAccountDomainAttributes(ctx context.Context, accountID return nil } -func (s *SqlStore) SavePeerStatus(accountID, peerID string, peerStatus nbpeer.PeerStatus) error { +func (s *SqlStore) SavePeerStatus(ctx context.Context, lockStrength LockingStrength, accountID, peerID string, peerStatus nbpeer.PeerStatus) error { var peerCopy nbpeer.Peer peerCopy.Status = &peerStatus @@ -376,12 +376,12 @@ func (s *SqlStore) SavePeerStatus(accountID, peerID string, peerStatus nbpeer.Pe "peer_status_last_seen", "peer_status_connected", "peer_status_login_expired", "peer_status_required_approval", } - result := s.db.Model(&nbpeer.Peer{}). + result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Model(&nbpeer.Peer{}). Select(fieldsToUpdate). Where(accountAndIDQueryCondition, accountID, peerID). Updates(&peerCopy) if result.Error != nil { - return result.Error + return status.Errorf(status.Internal, "failed to save peer status to store: %v", result.Error) } if result.RowsAffected == 0 { @@ -391,22 +391,22 @@ func (s *SqlStore) SavePeerStatus(accountID, peerID string, peerStatus nbpeer.Pe return nil } -func (s *SqlStore) SavePeerLocation(accountID string, peerWithLocation *nbpeer.Peer) error { +func (s *SqlStore) SavePeerLocation(ctx context.Context, lockStrength LockingStrength, accountID string, peerWithLocation *nbpeer.Peer) error { // To maintain data integrity, we create a copy of the peer's location to prevent unintended updates to other fields. var peerCopy nbpeer.Peer // Since the location field has been migrated to JSON serialization, // updating the struct ensures the correct data format is inserted into the database. peerCopy.Location = peerWithLocation.Location - result := s.db.Model(&nbpeer.Peer{}). + result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Model(&nbpeer.Peer{}). Where(accountAndIDQueryCondition, accountID, peerWithLocation.ID). Updates(peerCopy) if result.Error != nil { - return result.Error + return status.Errorf(status.Internal, "failed to save peer locations to store: %v", result.Error) } - if result.RowsAffected == 0 && s.storeEngine != MysqlStoreEngine { + if result.RowsAffected == 0 { return status.Errorf(status.NotFound, peerNotFoundFMT, peerWithLocation.ID) } @@ -773,9 +773,10 @@ func (s *SqlStore) GetAccountIDByPeerPubKey(ctx context.Context, peerKey string) return accountID, nil } -func (s *SqlStore) GetAccountIDByUserID(userID string) (string, error) { +func (s *SqlStore) GetAccountIDByUserID(ctx context.Context, lockStrength LockingStrength, userID string) (string, error) { var accountID string - result := s.db.Model(&types.User{}).Select("account_id").Where(idQueryCondition, userID).First(&accountID) + result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Model(&types.User{}). + Select("account_id").Where(idQueryCondition, userID).First(&accountID) if result.Error != nil { if errors.Is(result.Error, gorm.ErrRecordNotFound) { return "", status.Errorf(status.NotFound, "account not found: index lookup failed") @@ -786,6 +787,20 @@ func (s *SqlStore) GetAccountIDByUserID(userID string) (string, error) { return accountID, nil } +func (s *SqlStore) GetAccountIDByPeerID(ctx context.Context, lockStrength LockingStrength, peerID string) (string, error) { + var accountID string + result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Model(&nbpeer.Peer{}). + Select("account_id").Where(idQueryCondition, peerID).First(&accountID) + if result.Error != nil { + if errors.Is(result.Error, gorm.ErrRecordNotFound) { + return "", status.Errorf(status.NotFound, "peer %s account not found", peerID) + } + return "", status.NewGetAccountFromStoreError(result.Error) + } + + return accountID, nil +} + func (s *SqlStore) GetAccountIDBySetupKey(ctx context.Context, setupKey string) (string, error) { var accountID string result := s.db.Model(&types.SetupKey{}).Select("account_id").Where(GetKeyQueryCondition(s), setupKey).First(&accountID) @@ -865,7 +880,7 @@ func (s *SqlStore) GetPeerByPeerPubKey(ctx context.Context, lockStrength Locking if result.Error != nil { if errors.Is(result.Error, gorm.ErrRecordNotFound) { - return nil, status.Errorf(status.NotFound, "peer not found") + return nil, status.NewPeerNotFoundError(peerKey) } return nil, status.Errorf(status.Internal, "issue getting peer from store: %s", result.Error) } @@ -1096,9 +1111,10 @@ func (s *SqlStore) IncrementSetupKeyUsage(ctx context.Context, setupKeyID string } // AddPeerToAllGroup adds a peer to the 'All' group. Method always needs to run in a transaction -func (s *SqlStore) AddPeerToAllGroup(ctx context.Context, accountID string, peerID string) error { +func (s *SqlStore) AddPeerToAllGroup(ctx context.Context, lockStrength LockingStrength, accountID string, peerID string) error { var group types.Group - result := s.db.Where("account_id = ? AND name = ?", accountID, "All").First(&group) + result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}). + First(&group, "account_id = ? AND name = ?", accountID, "All") if result.Error != nil { if errors.Is(result.Error, gorm.ErrRecordNotFound) { return status.Errorf(status.NotFound, "group 'All' not found for account") @@ -1114,7 +1130,7 @@ func (s *SqlStore) AddPeerToAllGroup(ctx context.Context, accountID string, peer group.Peers = append(group.Peers, peerID) - if err := s.db.Save(&group).Error; err != nil { + if err := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Save(&group).Error; err != nil { return status.Errorf(status.Internal, "issue updating group 'All': %s", err) } @@ -1122,9 +1138,10 @@ func (s *SqlStore) AddPeerToAllGroup(ctx context.Context, accountID string, peer } // AddPeerToGroup adds a peer to a group. Method always needs to run in a transaction -func (s *SqlStore) AddPeerToGroup(ctx context.Context, accountId string, peerId string, groupID string) error { +func (s *SqlStore) AddPeerToGroup(ctx context.Context, lockStrength LockingStrength, accountId string, peerId string, groupID string) error { var group types.Group - result := s.db.Where(accountAndIDQueryCondition, accountId, groupID).First(&group) + result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Where(accountAndIDQueryCondition, accountId, groupID). + First(&group) if result.Error != nil { if errors.Is(result.Error, gorm.ErrRecordNotFound) { return status.NewGroupNotFoundError(groupID) @@ -1141,7 +1158,7 @@ func (s *SqlStore) AddPeerToGroup(ctx context.Context, accountId string, peerId group.Peers = append(group.Peers, peerId) - if err := s.db.Save(&group).Error; err != nil { + if err := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Save(&group).Error; err != nil { return status.Errorf(status.Internal, "issue updating group: %s", err) } @@ -1201,13 +1218,52 @@ func (s *SqlStore) RemoveResourceFromGroup(ctx context.Context, accountId string return nil } -// GetUserPeers retrieves peers for a user. -func (s *SqlStore) GetUserPeers(ctx context.Context, lockStrength LockingStrength, accountID, userID string) ([]*nbpeer.Peer, error) { - return getRecords[*nbpeer.Peer](s.db.Where("user_id = ?", userID), lockStrength, accountID) +// GetPeerGroups retrieves all groups assigned to a specific peer in a given account. +func (s *SqlStore) GetPeerGroups(ctx context.Context, lockStrength LockingStrength, accountId string, peerId string) ([]*types.Group, error) { + var groups []*types.Group + query := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}). + Find(&groups, "account_id = ? AND peers LIKE ?", accountId, fmt.Sprintf(`%%"%s"%%`, peerId)) + + if query.Error != nil { + return nil, query.Error + } + + return groups, nil } -func (s *SqlStore) AddPeerToAccount(ctx context.Context, peer *nbpeer.Peer) error { - if err := s.db.Create(peer).Error; err != nil { +// GetAccountPeers retrieves peers for an account. +func (s *SqlStore) GetAccountPeers(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*nbpeer.Peer, error) { + var peers []*nbpeer.Peer + result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Find(&peers, accountIDCondition, accountID) + if err := result.Error; err != nil { + log.WithContext(ctx).Errorf("failed to get peers from the store: %s", err) + return nil, status.Errorf(status.Internal, "failed to get peers from store") + } + + return peers, nil +} + +// GetUserPeers retrieves peers for a user. +func (s *SqlStore) GetUserPeers(ctx context.Context, lockStrength LockingStrength, accountID, userID string) ([]*nbpeer.Peer, error) { + var peers []*nbpeer.Peer + + // Exclude peers added via setup keys, as they are not user-specific and have an empty user_id. + if userID == "" { + return peers, nil + } + + result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}). + Find(&peers, "account_id = ? AND user_id = ?", accountID, userID) + if err := result.Error; err != nil { + log.WithContext(ctx).Errorf("failed to get peers from the store: %s", err) + return nil, status.Errorf(status.Internal, "failed to get peers from store") + } + + return peers, nil +} + +func (s *SqlStore) AddPeerToAccount(ctx context.Context, lockStrength LockingStrength, peer *nbpeer.Peer) error { + if err := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Create(peer).Error; err != nil { return status.Errorf(status.Internal, "issue adding peer to account: %s", err) } @@ -1221,7 +1277,7 @@ func (s *SqlStore) GetPeerByID(ctx context.Context, lockStrength LockingStrength First(&peer, accountAndIDQueryCondition, accountID, peerID) if result.Error != nil { if errors.Is(result.Error, gorm.ErrRecordNotFound) { - return nil, status.Errorf(status.NotFound, "peer not found") + return nil, status.NewPeerNotFoundError(peerID) } log.WithContext(ctx).Errorf("failed to get peer from store: %s", result.Error) return nil, status.Errorf(status.Internal, "failed to get peer from store") @@ -1247,6 +1303,68 @@ func (s *SqlStore) GetPeersByIDs(ctx context.Context, lockStrength LockingStreng return peersMap, nil } +// GetAccountPeersWithExpiration retrieves a list of peers that have login expiration enabled and added by a user. +func (s *SqlStore) GetAccountPeersWithExpiration(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*nbpeer.Peer, error) { + var peers []*nbpeer.Peer + result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}). + Where("login_expiration_enabled = ? AND user_id IS NOT NULL AND user_id != ''", true). + Find(&peers, accountIDCondition, accountID) + if err := result.Error; err != nil { + log.WithContext(ctx).Errorf("failed to get peers with expiration from the store: %s", result.Error) + return nil, status.Errorf(status.Internal, "failed to get peers with expiration from store") + } + + return peers, nil +} + +// GetAccountPeersWithInactivity retrieves a list of peers that have login expiration enabled and added by a user. +func (s *SqlStore) GetAccountPeersWithInactivity(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*nbpeer.Peer, error) { + var peers []*nbpeer.Peer + result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}). + Where("inactivity_expiration_enabled = ? AND user_id IS NOT NULL AND user_id != ''", true). + Find(&peers, accountIDCondition, accountID) + if err := result.Error; err != nil { + log.WithContext(ctx).Errorf("failed to get peers with inactivity from the store: %s", result.Error) + return nil, status.Errorf(status.Internal, "failed to get peers with inactivity from store") + } + + return peers, nil +} + +// GetAllEphemeralPeers retrieves all peers with Ephemeral set to true across all accounts, optimized for batch processing. +func (s *SqlStore) GetAllEphemeralPeers(ctx context.Context, lockStrength LockingStrength) ([]*nbpeer.Peer, error) { + var allEphemeralPeers, batchPeers []*nbpeer.Peer + result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}). + Where("ephemeral = ?", true). + FindInBatches(&batchPeers, 1000, func(tx *gorm.DB, batch int) error { + allEphemeralPeers = append(allEphemeralPeers, batchPeers...) + return nil + }) + + if result.Error != nil { + log.WithContext(ctx).Errorf("failed to retrieve ephemeral peers: %s", result.Error) + return nil, fmt.Errorf("failed to retrieve ephemeral peers") + } + + return allEphemeralPeers, nil +} + +// DeletePeer removes a peer from the store. +func (s *SqlStore) DeletePeer(ctx context.Context, lockStrength LockingStrength, accountID string, peerID string) error { + result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}). + Delete(&nbpeer.Peer{}, accountAndIDQueryCondition, accountID, peerID) + if err := result.Error; err != nil { + log.WithContext(ctx).Errorf("failed to delete peer from the store: %s", err) + return status.Errorf(status.Internal, "failed to delete peer from store") + } + + if result.RowsAffected == 0 { + return status.NewPeerNotFoundError(peerID) + } + + return nil +} + func (s *SqlStore) IncrementNetworkSerial(ctx context.Context, lockStrength LockingStrength, accountId string) error { result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}). Model(&types.Account{}).Where(idQueryCondition, accountId).Update("network_serial", gorm.Expr("network_serial + 1")) @@ -1638,7 +1756,7 @@ func (s *SqlStore) DeleteSetupKey(ctx context.Context, lockStrength LockingStren // GetAccountNameServerGroups retrieves name server groups for an account. func (s *SqlStore) GetAccountNameServerGroups(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*nbdns.NameServerGroup, error) { var nsGroups []*nbdns.NameServerGroup - result := s.db.WithContext(ctx).Clauses(clause.Locking{Strength: string(lockStrength)}).Find(&nsGroups, accountIDCondition, accountID) + result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Find(&nsGroups, accountIDCondition, accountID) if err := result.Error; err != nil { log.WithContext(ctx).Errorf("failed to get name server groups from the store: %s", err) return nil, status.Errorf(status.Internal, "failed to get name server groups from store") @@ -1650,7 +1768,7 @@ func (s *SqlStore) GetAccountNameServerGroups(ctx context.Context, lockStrength // GetNameServerGroupByID retrieves a name server group by its ID and account ID. func (s *SqlStore) GetNameServerGroupByID(ctx context.Context, lockStrength LockingStrength, accountID, nsGroupID string) (*nbdns.NameServerGroup, error) { var nsGroup *nbdns.NameServerGroup - result := s.db.WithContext(ctx).Clauses(clause.Locking{Strength: string(lockStrength)}). + result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}). First(&nsGroup, accountAndIDQueryCondition, accountID, nsGroupID) if err := result.Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { @@ -1665,7 +1783,7 @@ func (s *SqlStore) GetNameServerGroupByID(ctx context.Context, lockStrength Lock // SaveNameServerGroup saves a name server group to the database. func (s *SqlStore) SaveNameServerGroup(ctx context.Context, lockStrength LockingStrength, nameServerGroup *nbdns.NameServerGroup) error { - result := s.db.WithContext(ctx).Clauses(clause.Locking{Strength: string(lockStrength)}).Save(nameServerGroup) + result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Save(nameServerGroup) if err := result.Error; err != nil { log.WithContext(ctx).Errorf("failed to save name server group to the store: %s", err) return status.Errorf(status.Internal, "failed to save name server group to store") diff --git a/management/server/store/sql_store_test.go b/management/server/store/sql_store_test.go index 5928b45ba..cb51dab51 100644 --- a/management/server/store/sql_store_test.go +++ b/management/server/store/sql_store_test.go @@ -14,6 +14,7 @@ import ( "time" "github.com/google/uuid" + "github.com/netbirdio/netbird/management/server/util" "github.com/rs/xid" log "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" @@ -422,12 +423,7 @@ func TestSqlite_GetAccount(t *testing.T) { require.Equal(t, status.NotFound, parsedErr.Type(), "should return not found error") } -func TestSqlite_SavePeer(t *testing.T) { - if runtime.GOOS == "windows" { - t.Skip("The SQLite store is not properly supported by Windows yet") - } - - t.Setenv("NETBIRD_STORE_ENGINE", string(SqliteStoreEngine)) +func TestSqlStore_SavePeer(t *testing.T) { store, cleanUp, err := NewTestStoreFromSQL(context.Background(), "../testdata/store.sql", t.TempDir()) t.Cleanup(cleanUp) assert.NoError(t, err) @@ -437,15 +433,16 @@ func TestSqlite_SavePeer(t *testing.T) { // save status of non-existing peer peer := &nbpeer.Peer{ - Key: "peerkey", - ID: "testpeer", - IP: net.IP{127, 0, 0, 1}, - Meta: nbpeer.PeerSystemMeta{Hostname: "testingpeer"}, - Name: "peer name", - Status: &nbpeer.PeerStatus{Connected: true, LastSeen: time.Now().UTC()}, + Key: "peerkey", + ID: "testpeer", + IP: net.IP{127, 0, 0, 1}, + Meta: nbpeer.PeerSystemMeta{Hostname: "testingpeer"}, + Name: "peer name", + Status: &nbpeer.PeerStatus{Connected: true, LastSeen: time.Now().UTC()}, + CreatedAt: time.Now().UTC(), } ctx := context.Background() - err = store.SavePeer(ctx, account.Id, peer) + err = store.SavePeer(ctx, LockingStrengthUpdate, account.Id, peer) assert.Error(t, err) parsedErr, ok := status.FromError(err) require.True(t, ok) @@ -461,23 +458,21 @@ func TestSqlite_SavePeer(t *testing.T) { updatedPeer.Status.Connected = false updatedPeer.Meta.Hostname = "updatedpeer" - err = store.SavePeer(ctx, account.Id, updatedPeer) + err = store.SavePeer(ctx, LockingStrengthUpdate, account.Id, updatedPeer) require.NoError(t, err) account, err = store.GetAccount(context.Background(), account.Id) require.NoError(t, err) actual := account.Peers[peer.ID] - assert.Equal(t, updatedPeer.Status, actual.Status) assert.Equal(t, updatedPeer.Meta, actual.Meta) + assert.Equal(t, updatedPeer.Status.Connected, actual.Status.Connected) + assert.Equal(t, updatedPeer.Status.LoginExpired, actual.Status.LoginExpired) + assert.Equal(t, updatedPeer.Status.RequiresApproval, actual.Status.RequiresApproval) + assert.WithinDurationf(t, updatedPeer.Status.LastSeen, actual.Status.LastSeen.UTC(), time.Millisecond, "LastSeen should be equal") } -func TestSqlite_SavePeerStatus(t *testing.T) { - if runtime.GOOS == "windows" { - t.Skip("The SQLite store is not properly supported by Windows yet") - } - - t.Setenv("NETBIRD_STORE_ENGINE", string(SqliteStoreEngine)) +func TestSqlStore_SavePeerStatus(t *testing.T) { store, cleanUp, err := NewTestStoreFromSQL(context.Background(), "../testdata/store.sql", t.TempDir()) t.Cleanup(cleanUp) assert.NoError(t, err) @@ -487,7 +482,7 @@ func TestSqlite_SavePeerStatus(t *testing.T) { // save status of non-existing peer newStatus := nbpeer.PeerStatus{Connected: false, LastSeen: time.Now().UTC()} - err = store.SavePeerStatus(account.Id, "non-existing-peer", newStatus) + err = store.SavePeerStatus(context.Background(), LockingStrengthUpdate, account.Id, "non-existing-peer", newStatus) assert.Error(t, err) parsedErr, ok := status.FromError(err) require.True(t, ok) @@ -506,33 +501,34 @@ func TestSqlite_SavePeerStatus(t *testing.T) { err = store.SaveAccount(context.Background(), account) require.NoError(t, err) - err = store.SavePeerStatus(account.Id, "testpeer", newStatus) + err = store.SavePeerStatus(context.Background(), LockingStrengthUpdate, account.Id, "testpeer", newStatus) require.NoError(t, err) account, err = store.GetAccount(context.Background(), account.Id) require.NoError(t, err) actual := account.Peers["testpeer"].Status - assert.Equal(t, newStatus, *actual) + assert.Equal(t, newStatus.Connected, actual.Connected) + assert.Equal(t, newStatus.LoginExpired, actual.LoginExpired) + assert.Equal(t, newStatus.RequiresApproval, actual.RequiresApproval) + assert.WithinDurationf(t, newStatus.LastSeen, actual.LastSeen.UTC(), time.Millisecond, "LastSeen should be equal") newStatus.Connected = true - err = store.SavePeerStatus(account.Id, "testpeer", newStatus) + err = store.SavePeerStatus(context.Background(), LockingStrengthUpdate, account.Id, "testpeer", newStatus) require.NoError(t, err) account, err = store.GetAccount(context.Background(), account.Id) require.NoError(t, err) actual = account.Peers["testpeer"].Status - assert.Equal(t, newStatus, *actual) + assert.Equal(t, newStatus.Connected, actual.Connected) + assert.Equal(t, newStatus.LoginExpired, actual.LoginExpired) + assert.Equal(t, newStatus.RequiresApproval, actual.RequiresApproval) + assert.WithinDurationf(t, newStatus.LastSeen, actual.LastSeen.UTC(), time.Millisecond, "LastSeen should be equal") } -func TestSqlite_SavePeerLocation(t *testing.T) { - if runtime.GOOS == "windows" { - t.Skip("The SQLite store is not properly supported by Windows yet") - } - - t.Setenv("NETBIRD_STORE_ENGINE", string(SqliteStoreEngine)) +func TestSqlStore_SavePeerLocation(t *testing.T) { store, cleanUp, err := NewTestStoreFromSQL(context.Background(), "../testdata/store.sql", t.TempDir()) t.Cleanup(cleanUp) assert.NoError(t, err) @@ -549,10 +545,11 @@ func TestSqlite_SavePeerLocation(t *testing.T) { CityName: "City", GeoNameID: 1, }, - Meta: nbpeer.PeerSystemMeta{}, + CreatedAt: time.Now().UTC(), + Meta: nbpeer.PeerSystemMeta{}, } // error is expected as peer is not in store yet - err = store.SavePeerLocation(account.Id, peer) + err = store.SavePeerLocation(context.Background(), LockingStrengthUpdate, account.Id, peer) assert.Error(t, err) account.Peers[peer.ID] = peer @@ -564,7 +561,7 @@ func TestSqlite_SavePeerLocation(t *testing.T) { peer.Location.CityName = "Berlin" peer.Location.GeoNameID = 2950159 - err = store.SavePeerLocation(account.Id, account.Peers[peer.ID]) + err = store.SavePeerLocation(context.Background(), LockingStrengthUpdate, account.Id, account.Peers[peer.ID]) assert.NoError(t, err) account, err = store.GetAccount(context.Background(), account.Id) @@ -574,7 +571,7 @@ func TestSqlite_SavePeerLocation(t *testing.T) { assert.Equal(t, peer.Location, actual) peer.ID = "non-existing-peer" - err = store.SavePeerLocation(account.Id, peer) + err = store.SavePeerLocation(context.Background(), LockingStrengthUpdate, account.Id, peer) assert.Error(t, err) parsedErr, ok := status.FromError(err) require.True(t, ok) @@ -925,47 +922,6 @@ func TestPostgresql_DeleteAccount(t *testing.T) { } -func TestPostgresql_SavePeerStatus(t *testing.T) { - if (os.Getenv("CI") == "true" && runtime.GOOS == "darwin") || runtime.GOOS == "windows" { - t.Skip("skip CI tests on darwin and windows") - } - - t.Setenv("NETBIRD_STORE_ENGINE", string(PostgresStoreEngine)) - store, cleanUp, err := NewTestStoreFromSQL(context.Background(), "../testdata/store.sql", t.TempDir()) - t.Cleanup(cleanUp) - assert.NoError(t, err) - - account, err := store.GetAccount(context.Background(), "bf1c8084-ba50-4ce7-9439-34653001fc3b") - require.NoError(t, err) - - // save status of non-existing peer - newStatus := nbpeer.PeerStatus{Connected: true, LastSeen: time.Now().UTC()} - err = store.SavePeerStatus(account.Id, "non-existing-peer", newStatus) - assert.Error(t, err) - - // save new status of existing peer - account.Peers["testpeer"] = &nbpeer.Peer{ - Key: "peerkey", - ID: "testpeer", - IP: net.IP{127, 0, 0, 1}, - Meta: nbpeer.PeerSystemMeta{}, - Name: "peer name", - Status: &nbpeer.PeerStatus{Connected: false, LastSeen: time.Now().UTC()}, - } - - err = store.SaveAccount(context.Background(), account) - require.NoError(t, err) - - err = store.SavePeerStatus(account.Id, "testpeer", newStatus) - require.NoError(t, err) - - account, err = store.GetAccount(context.Background(), account.Id) - require.NoError(t, err) - - actual := account.Peers["testpeer"].Status - assert.Equal(t, newStatus.Connected, actual.Connected) -} - func TestPostgresql_TestGetAccountByPrivateDomain(t *testing.T) { if (os.Getenv("CI") == "true" && runtime.GOOS == "darwin") || runtime.GOOS == "windows" { t.Skip("skip CI tests on darwin and windows") @@ -1043,7 +999,7 @@ func TestSqlite_GetTakenIPs(t *testing.T) { AccountID: existingAccountID, IP: net.IP{1, 1, 1, 1}, } - err = store.AddPeerToAccount(context.Background(), peer1) + err = store.AddPeerToAccount(context.Background(), LockingStrengthUpdate, peer1) require.NoError(t, err) takenIPs, err = store.GetTakenIPs(context.Background(), LockingStrengthShare, existingAccountID) @@ -1056,7 +1012,7 @@ func TestSqlite_GetTakenIPs(t *testing.T) { AccountID: existingAccountID, IP: net.IP{2, 2, 2, 2}, } - err = store.AddPeerToAccount(context.Background(), peer2) + err = store.AddPeerToAccount(context.Background(), LockingStrengthUpdate, peer2) require.NoError(t, err) takenIPs, err = store.GetTakenIPs(context.Background(), LockingStrengthShare, existingAccountID) @@ -1088,7 +1044,7 @@ func TestSqlite_GetPeerLabelsInAccount(t *testing.T) { AccountID: existingAccountID, DNSLabel: "peer1.domain.test", } - err = store.AddPeerToAccount(context.Background(), peer1) + err = store.AddPeerToAccount(context.Background(), LockingStrengthUpdate, peer1) require.NoError(t, err) labels, err = store.GetPeerLabelsInAccount(context.Background(), LockingStrengthShare, existingAccountID) @@ -1100,7 +1056,7 @@ func TestSqlite_GetPeerLabelsInAccount(t *testing.T) { AccountID: existingAccountID, DNSLabel: "peer2.domain.test", } - err = store.AddPeerToAccount(context.Background(), peer2) + err = store.AddPeerToAccount(context.Background(), LockingStrengthUpdate, peer2) require.NoError(t, err) labels, err = store.GetPeerLabelsInAccount(context.Background(), LockingStrengthShare, existingAccountID) @@ -2561,3 +2517,329 @@ func TestSqlStore_AddAndRemoveResourceFromGroup(t *testing.T) { require.NoError(t, err) require.NotContains(t, group.Resources, *res) } + +func TestSqlStore_AddPeerToGroup(t *testing.T) { + store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/store_policy_migrate.sql", t.TempDir()) + t.Cleanup(cleanup) + require.NoError(t, err) + + accountID := "bf1c8084-ba50-4ce7-9439-34653001fc3b" + peerID := "cfefqs706sqkneg59g4g" + groupID := "cfefqs706sqkneg59g4h" + + group, err := store.GetGroupByID(context.Background(), LockingStrengthShare, accountID, groupID) + require.NoError(t, err, "failed to get group") + require.Len(t, group.Peers, 0, "group should have 0 peers") + + err = store.AddPeerToGroup(context.Background(), LockingStrengthUpdate, accountID, peerID, groupID) + require.NoError(t, err, "failed to add peer to group") + + group, err = store.GetGroupByID(context.Background(), LockingStrengthShare, accountID, groupID) + require.NoError(t, err, "failed to get group") + require.Len(t, group.Peers, 1, "group should have 1 peers") + require.Contains(t, group.Peers, peerID) +} + +func TestSqlStore_AddPeerToAllGroup(t *testing.T) { + store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/store_policy_migrate.sql", t.TempDir()) + t.Cleanup(cleanup) + require.NoError(t, err) + + accountID := "bf1c8084-ba50-4ce7-9439-34653001fc3b" + groupID := "cfefqs706sqkneg59g3g" + + peer := &nbpeer.Peer{ + ID: "peer1", + AccountID: accountID, + DNSLabel: "peer1.domain.test", + } + + group, err := store.GetGroupByID(context.Background(), LockingStrengthShare, accountID, groupID) + require.NoError(t, err, "failed to get group") + require.Len(t, group.Peers, 2, "group should have 2 peers") + require.NotContains(t, group.Peers, peer.ID) + + err = store.AddPeerToAccount(context.Background(), LockingStrengthUpdate, peer) + require.NoError(t, err, "failed to add peer to account") + + err = store.AddPeerToAllGroup(context.Background(), LockingStrengthUpdate, accountID, peer.ID) + require.NoError(t, err, "failed to add peer to all group") + + group, err = store.GetGroupByID(context.Background(), LockingStrengthShare, accountID, groupID) + require.NoError(t, err, "failed to get group") + require.Len(t, group.Peers, 3, "group should have peers") + require.Contains(t, group.Peers, peer.ID) +} + +func TestSqlStore_AddPeerToAccount(t *testing.T) { + store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/store_policy_migrate.sql", t.TempDir()) + t.Cleanup(cleanup) + require.NoError(t, err) + + accountID := "bf1c8084-ba50-4ce7-9439-34653001fc3b" + + peer := &nbpeer.Peer{ + ID: "peer1", + AccountID: accountID, + Key: "key", + IP: net.IP{1, 1, 1, 1}, + Meta: nbpeer.PeerSystemMeta{ + Hostname: "hostname", + GoOS: "linux", + Kernel: "Linux", + Core: "21.04", + Platform: "x86_64", + OS: "Ubuntu", + WtVersion: "development", + UIVersion: "development", + }, + Name: "peer.test", + DNSLabel: "peer", + Status: &nbpeer.PeerStatus{ + LastSeen: time.Now().UTC(), + Connected: true, + LoginExpired: false, + RequiresApproval: false, + }, + SSHKey: "ssh-key", + SSHEnabled: false, + LoginExpirationEnabled: true, + InactivityExpirationEnabled: false, + LastLogin: util.ToPtr(time.Now().UTC()), + CreatedAt: time.Now().UTC(), + Ephemeral: true, + } + err = store.AddPeerToAccount(context.Background(), LockingStrengthUpdate, peer) + require.NoError(t, err, "failed to add peer to account") + + storedPeer, err := store.GetPeerByID(context.Background(), LockingStrengthShare, accountID, peer.ID) + require.NoError(t, err, "failed to get peer") + + assert.Equal(t, peer.ID, storedPeer.ID) + assert.Equal(t, peer.AccountID, storedPeer.AccountID) + assert.Equal(t, peer.Key, storedPeer.Key) + assert.Equal(t, peer.IP.String(), storedPeer.IP.String()) + assert.Equal(t, peer.Meta, storedPeer.Meta) + assert.Equal(t, peer.Name, storedPeer.Name) + assert.Equal(t, peer.DNSLabel, storedPeer.DNSLabel) + assert.Equal(t, peer.SSHKey, storedPeer.SSHKey) + assert.Equal(t, peer.SSHEnabled, storedPeer.SSHEnabled) + assert.Equal(t, peer.LoginExpirationEnabled, storedPeer.LoginExpirationEnabled) + assert.Equal(t, peer.InactivityExpirationEnabled, storedPeer.InactivityExpirationEnabled) + assert.WithinDurationf(t, peer.GetLastLogin(), storedPeer.GetLastLogin().UTC(), time.Millisecond, "LastLogin should be equal") + assert.WithinDurationf(t, peer.CreatedAt, storedPeer.CreatedAt.UTC(), time.Millisecond, "CreatedAt should be equal") + assert.Equal(t, peer.Ephemeral, storedPeer.Ephemeral) + assert.Equal(t, peer.Status.Connected, storedPeer.Status.Connected) + assert.Equal(t, peer.Status.LoginExpired, storedPeer.Status.LoginExpired) + assert.Equal(t, peer.Status.RequiresApproval, storedPeer.Status.RequiresApproval) + assert.WithinDurationf(t, peer.Status.LastSeen, storedPeer.Status.LastSeen.UTC(), time.Millisecond, "LastSeen should be equal") +} + +func TestSqlStore_GetPeerGroups(t *testing.T) { + store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/store_policy_migrate.sql", t.TempDir()) + t.Cleanup(cleanup) + require.NoError(t, err) + + accountID := "bf1c8084-ba50-4ce7-9439-34653001fc3b" + peerID := "cfefqs706sqkneg59g4g" + + groups, err := store.GetPeerGroups(context.Background(), LockingStrengthShare, accountID, peerID) + require.NoError(t, err) + assert.Len(t, groups, 1) + assert.Equal(t, groups[0].Name, "All") + + err = store.AddPeerToGroup(context.Background(), LockingStrengthUpdate, accountID, peerID, "cfefqs706sqkneg59g4h") + require.NoError(t, err) + + groups, err = store.GetPeerGroups(context.Background(), LockingStrengthShare, accountID, peerID) + require.NoError(t, err) + assert.Len(t, groups, 2) +} + +func TestSqlStore_GetAccountPeers(t *testing.T) { + store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/store_with_expired_peers.sql", t.TempDir()) + t.Cleanup(cleanup) + require.NoError(t, err) + + tests := []struct { + name string + accountID string + expectedCount int + }{ + { + name: "should retrieve peers for an existing account ID", + accountID: "bf1c8084-ba50-4ce7-9439-34653001fc3b", + expectedCount: 4, + }, + { + name: "should return no peers for a non-existing account ID", + accountID: "nonexistent", + expectedCount: 0, + }, + { + name: "should return no peers for an empty account ID", + accountID: "", + expectedCount: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + peers, err := store.GetAccountPeers(context.Background(), LockingStrengthShare, tt.accountID) + require.NoError(t, err) + require.Len(t, peers, tt.expectedCount) + }) + } + +} + +func TestSqlStore_GetAccountPeersWithExpiration(t *testing.T) { + store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/store_with_expired_peers.sql", t.TempDir()) + t.Cleanup(cleanup) + require.NoError(t, err) + + tests := []struct { + name string + accountID string + expectedCount int + }{ + { + name: "should retrieve peers with expiration for an existing account ID", + accountID: "bf1c8084-ba50-4ce7-9439-34653001fc3b", + expectedCount: 1, + }, + { + name: "should return no peers with expiration for a non-existing account ID", + accountID: "nonexistent", + expectedCount: 0, + }, + { + name: "should return no peers with expiration for a empty account ID", + accountID: "", + expectedCount: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + peers, err := store.GetAccountPeersWithExpiration(context.Background(), LockingStrengthShare, tt.accountID) + require.NoError(t, err) + require.Len(t, peers, tt.expectedCount) + }) + } +} + +func TestSqlStore_GetAccountPeersWithInactivity(t *testing.T) { + store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/store_with_expired_peers.sql", t.TempDir()) + t.Cleanup(cleanup) + require.NoError(t, err) + + tests := []struct { + name string + accountID string + expectedCount int + }{ + { + name: "should retrieve peers with inactivity for an existing account ID", + accountID: "bf1c8084-ba50-4ce7-9439-34653001fc3b", + expectedCount: 1, + }, + { + name: "should return no peers with inactivity for a non-existing account ID", + accountID: "nonexistent", + expectedCount: 0, + }, + { + name: "should return no peers with inactivity for an empty account ID", + accountID: "", + expectedCount: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + peers, err := store.GetAccountPeersWithInactivity(context.Background(), LockingStrengthShare, tt.accountID) + require.NoError(t, err) + require.Len(t, peers, tt.expectedCount) + }) + } +} + +func TestSqlStore_GetAllEphemeralPeers(t *testing.T) { + store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/storev1.sql", t.TempDir()) + t.Cleanup(cleanup) + require.NoError(t, err) + + peers, err := store.GetAllEphemeralPeers(context.Background(), LockingStrengthShare) + require.NoError(t, err) + require.Len(t, peers, 1) + require.True(t, peers[0].Ephemeral) +} + +func TestSqlStore_GetUserPeers(t *testing.T) { + store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/store_with_expired_peers.sql", t.TempDir()) + t.Cleanup(cleanup) + require.NoError(t, err) + + tests := []struct { + name string + accountID string + userID string + expectedCount int + }{ + { + name: "should retrieve peers for existing account ID and user ID", + accountID: "bf1c8084-ba50-4ce7-9439-34653001fc3b", + userID: "f4f6d672-63fb-11ec-90d6-0242ac120003", + expectedCount: 1, + }, + { + name: "should return no peers for non-existing account ID with existing user ID", + accountID: "nonexistent", + userID: "f4f6d672-63fb-11ec-90d6-0242ac120003", + expectedCount: 0, + }, + { + name: "should return no peers for non-existing user ID with existing account ID", + accountID: "bf1c8084-ba50-4ce7-9439-34653001fc3b", + userID: "nonexistent_user", + expectedCount: 0, + }, + { + name: "should retrieve peers for another valid account ID and user ID", + accountID: "bf1c8084-ba50-4ce7-9439-34653001fc3b", + userID: "edafee4e-63fb-11ec-90d6-0242ac120003", + expectedCount: 2, + }, + { + name: "should return no peers for existing account ID with empty user ID", + accountID: "bf1c8084-ba50-4ce7-9439-34653001fc3b", + userID: "", + expectedCount: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + peers, err := store.GetUserPeers(context.Background(), LockingStrengthShare, tt.accountID, tt.userID) + require.NoError(t, err) + require.Len(t, peers, tt.expectedCount) + }) + } +} + +func TestSqlStore_DeletePeer(t *testing.T) { + store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/store_with_expired_peers.sql", t.TempDir()) + t.Cleanup(cleanup) + require.NoError(t, err) + + accountID := "bf1c8084-ba50-4ce7-9439-34653001fc3b" + peerID := "csrnkiq7qv9d8aitqd50" + + err = store.DeletePeer(context.Background(), LockingStrengthUpdate, accountID, peerID) + require.NoError(t, err) + + peer, err := store.GetPeerByID(context.Background(), LockingStrengthShare, accountID, peerID) + require.Error(t, err) + require.Nil(t, peer) +} diff --git a/management/server/store/store.go b/management/server/store/store.go index 91ae93c7c..245df1c3e 100644 --- a/management/server/store/store.go +++ b/management/server/store/store.go @@ -50,8 +50,9 @@ type Store interface { GetAccountByUser(ctx context.Context, userID string) (*types.Account, error) GetAccountByPeerPubKey(ctx context.Context, peerKey string) (*types.Account, error) GetAccountIDByPeerPubKey(ctx context.Context, peerKey string) (string, error) - GetAccountIDByUserID(userID string) (string, error) + GetAccountIDByUserID(ctx context.Context, lockStrength LockingStrength, userID string) (string, error) GetAccountIDBySetupKey(ctx context.Context, peerKey string) (string, error) + GetAccountIDByPeerID(ctx context.Context, lockStrength LockingStrength, peerID string) (string, error) GetAccountByPeerID(ctx context.Context, peerID string) (*types.Account, error) GetAccountBySetupKey(ctx context.Context, setupKey string) (*types.Account, error) // todo use key hash later GetAccountByPrivateDomain(ctx context.Context, domain string) (*types.Account, error) @@ -97,18 +98,24 @@ type Store interface { DeletePostureChecks(ctx context.Context, lockStrength LockingStrength, accountID, postureChecksID string) error GetPeerLabelsInAccount(ctx context.Context, lockStrength LockingStrength, accountId string) ([]string, error) - AddPeerToAllGroup(ctx context.Context, accountID string, peerID string) error - AddPeerToGroup(ctx context.Context, accountId string, peerId string, groupID string) error + AddPeerToAllGroup(ctx context.Context, lockStrength LockingStrength, accountID string, peerID string) error + AddPeerToGroup(ctx context.Context, lockStrength LockingStrength, accountId string, peerId string, groupID string) error + GetPeerGroups(ctx context.Context, lockStrength LockingStrength, accountId string, peerId string) ([]*types.Group, error) AddResourceToGroup(ctx context.Context, accountId string, groupID string, resource *types.Resource) error RemoveResourceFromGroup(ctx context.Context, accountId string, groupID string, resourceID string) error - AddPeerToAccount(ctx context.Context, peer *nbpeer.Peer) error + AddPeerToAccount(ctx context.Context, lockStrength LockingStrength, peer *nbpeer.Peer) error GetPeerByPeerPubKey(ctx context.Context, lockStrength LockingStrength, peerKey string) (*nbpeer.Peer, error) + GetAccountPeers(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*nbpeer.Peer, error) GetUserPeers(ctx context.Context, lockStrength LockingStrength, accountID, userID string) ([]*nbpeer.Peer, error) GetPeerByID(ctx context.Context, lockStrength LockingStrength, accountID string, peerID string) (*nbpeer.Peer, error) GetPeersByIDs(ctx context.Context, lockStrength LockingStrength, accountID string, peerIDs []string) (map[string]*nbpeer.Peer, error) - SavePeer(ctx context.Context, accountID string, peer *nbpeer.Peer) error - SavePeerStatus(accountID, peerID string, status nbpeer.PeerStatus) error - SavePeerLocation(accountID string, peer *nbpeer.Peer) error + GetAccountPeersWithExpiration(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*nbpeer.Peer, error) + GetAccountPeersWithInactivity(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*nbpeer.Peer, error) + GetAllEphemeralPeers(ctx context.Context, lockStrength LockingStrength) ([]*nbpeer.Peer, error) + SavePeer(ctx context.Context, lockStrength LockingStrength, accountID string, peer *nbpeer.Peer) error + SavePeerStatus(ctx context.Context, lockStrength LockingStrength, accountID, peerID string, status nbpeer.PeerStatus) error + SavePeerLocation(ctx context.Context, lockStrength LockingStrength, accountID string, peer *nbpeer.Peer) error + DeletePeer(ctx context.Context, lockStrength LockingStrength, accountID string, peerID string) error GetSetupKeyBySecret(ctx context.Context, lockStrength LockingStrength, key string) (*types.SetupKey, error) IncrementSetupKeyUsage(ctx context.Context, setupKeyID string) error diff --git a/management/server/testdata/store_policy_migrate.sql b/management/server/testdata/store_policy_migrate.sql index 9c961e389..a88411795 100644 --- a/management/server/testdata/store_policy_migrate.sql +++ b/management/server/testdata/store_policy_migrate.sql @@ -32,4 +32,5 @@ INSERT INTO peers VALUES('cfeg6sf06sqkneg59g50','bf1c8084-ba50-4ce7-9439-3465300 INSERT INTO users VALUES('edafee4e-63fb-11ec-90d6-0242ac120003','bf1c8084-ba50-4ce7-9439-34653001fc3b','admin',0,0,'','[]',0,NULL,'2024-10-02 16:04:23.539152+02:00','api',0,''); INSERT INTO users VALUES('f4f6d672-63fb-11ec-90d6-0242ac120003','bf1c8084-ba50-4ce7-9439-34653001fc3b','user',0,0,'','[]',0,NULL,'2024-10-02 16:04:23.539152+02:00','api',0,''); INSERT INTO "groups" VALUES('cfefqs706sqkneg59g3g','bf1c8084-ba50-4ce7-9439-34653001fc3b','All','api','["cfefqs706sqkneg59g4g","cfeg6sf06sqkneg59g50"]',0,''); +INSERT INTO "groups" VALUES('cfefqs706sqkneg59g4h','bf1c8084-ba50-4ce7-9439-34653001fc3b','groupA','api','',0,''); INSERT INTO installations VALUES(1,''); diff --git a/management/server/testdata/store_with_expired_peers.sql b/management/server/testdata/store_with_expired_peers.sql index 518c484d7..5990a0625 100644 --- a/management/server/testdata/store_with_expired_peers.sql +++ b/management/server/testdata/store_with_expired_peers.sql @@ -1,6 +1,6 @@ CREATE TABLE `accounts` (`id` text,`created_by` text,`created_at` datetime,`domain` text,`domain_category` text,`is_domain_primary_account` numeric,`network_identifier` text,`network_net` text,`network_dns` text,`network_serial` integer,`dns_settings_disabled_management_groups` text,`settings_peer_login_expiration_enabled` numeric,`settings_peer_login_expiration` integer,`settings_regular_users_view_blocked` numeric,`settings_groups_propagation_enabled` numeric,`settings_jwt_groups_enabled` numeric,`settings_jwt_groups_claim_name` text,`settings_jwt_allow_groups` text,`settings_extra_peer_approval_enabled` numeric,`settings_extra_integrated_validator_groups` text,PRIMARY KEY (`id`)); CREATE TABLE `setup_keys` (`id` text,`account_id` text,`key` text,`name` text,`type` text,`created_at` datetime,`expires_at` datetime,`updated_at` datetime,`revoked` numeric,`used_times` integer,`last_used` datetime DEFAULT NULL,`auto_groups` text,`usage_limit` integer,`ephemeral` numeric,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_setup_keys_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`)); -CREATE TABLE `peers` (`id` text,`account_id` text,`key` text,`setup_key` text,`ip` text,`meta_hostname` text,`meta_go_os` text,`meta_kernel` text,`meta_core` text,`meta_platform` text,`meta_os` text,`meta_os_version` text,`meta_wt_version` text,`meta_ui_version` text,`meta_kernel_version` text,`meta_network_addresses` text,`meta_system_serial_number` text,`meta_system_product_name` text,`meta_system_manufacturer` text,`meta_environment` text,`meta_files` text,`name` text,`dns_label` text,`peer_status_last_seen` datetime,`peer_status_connected` numeric,`peer_status_login_expired` numeric,`peer_status_requires_approval` numeric,`user_id` text,`ssh_key` text,`ssh_enabled` numeric,`login_expiration_enabled` numeric,`last_login` datetime,`created_at` datetime,`ephemeral` numeric,`location_connection_ip` text,`location_country_code` text,`location_city_name` text,`location_geo_name_id` integer,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_peers_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`)); +CREATE TABLE `peers` (`id` text,`account_id` text,`key` text,`setup_key` text,`ip` text,`meta_hostname` text,`meta_go_os` text,`meta_kernel` text,`meta_core` text,`meta_platform` text,`meta_os` text,`meta_os_version` text,`meta_wt_version` text,`meta_ui_version` text,`meta_kernel_version` text,`meta_network_addresses` text,`meta_system_serial_number` text,`meta_system_product_name` text,`meta_system_manufacturer` text,`meta_environment` text,`meta_files` text,`name` text,`dns_label` text,`peer_status_last_seen` datetime,`peer_status_connected` numeric,`peer_status_login_expired` numeric,`peer_status_requires_approval` numeric,`user_id` text,`ssh_key` text,`ssh_enabled` numeric,`login_expiration_enabled` numeric,`inactivity_expiration_enabled` numeric,`last_login` datetime,`created_at` datetime,`ephemeral` numeric,`location_connection_ip` text,`location_country_code` text,`location_city_name` text,`location_geo_name_id` integer,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_peers_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`)); CREATE TABLE `users` (`id` text,`account_id` text,`role` text,`is_service_user` numeric,`non_deletable` numeric,`service_user_name` text,`auto_groups` text,`blocked` numeric,`last_login` datetime DEFAULT NULL,`created_at` datetime,`issued` text DEFAULT "api",`integration_ref_id` integer,`integration_ref_integration_type` text,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_users_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`)); CREATE TABLE `personal_access_tokens` (`id` text,`user_id` text,`name` text,`hashed_token` text,`expiration_date` datetime,`created_by` text,`created_at` datetime,`last_used` datetime,PRIMARY KEY (`id`),CONSTRAINT `fk_users_pa_ts_g` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`)); CREATE TABLE `groups` (`id` text,`account_id` text,`name` text,`issued` text,`peers` text,`integration_ref_id` integer,`integration_ref_integration_type` text,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_groups_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`)); @@ -27,9 +27,10 @@ CREATE INDEX `idx_posture_checks_account_id` ON `posture_checks`(`account_id`); INSERT INTO accounts VALUES('bf1c8084-ba50-4ce7-9439-34653001fc3b','','2024-10-02 17:00:32.527528+02:00','test.com','private',1,'af1c8024-ha40-4ce2-9418-34653101fc3c','{"IP":"100.64.0.0","Mask":"//8AAA=="}','',0,'[]',1,3600000000000,0,0,0,'',NULL,NULL,NULL); INSERT INTO setup_keys VALUES('','bf1c8084-ba50-4ce7-9439-34653001fc3b','A2C8E62B-38F5-4553-B31E-DD66C696CEBB','Default key','reusable','2021-08-19 20:46:20.005936822+02:00','2321-09-18 20:46:20.005936822+02:00','2021-08-19 20:46:20.005936822+02:00',0,0,NULL,'[]',0,0); -INSERT INTO peers VALUES('cfvprsrlo1hqoo49ohog','bf1c8084-ba50-4ce7-9439-34653001fc3b','5rvhvriKJZ3S9oxYToVj5TzDM9u9y8cxg7htIMWlYAg=','72546A29-6BC8-4311-BCFC-9CDBF33F1A48','"100.64.114.31"','f2a34f6a4731','linux','Linux','11','unknown','Debian GNU/Linux','','0.12.0','','',NULL,'','','','{"Cloud":"","Platform":""}',NULL,'f2a34f6a4731','f2a34f6a4731','2023-03-02 09:21:02.189035775+01:00',0,0,0,'','ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILzUUSYG/LGnV8zarb2SGN+tib/PZ+M7cL4WtTzUrTpk',0,1,'2023-03-01 19:48:19.817799698+01:00','2024-10-02 17:00:32.527947+02:00',0,'""','','',0); -INSERT INTO peers VALUES('cg05lnblo1hkg2j514p0','bf1c8084-ba50-4ce7-9439-34653001fc3b','RlSy2vzoG2HyMBTUImXOiVhCBiiBa5qD5xzMxkiFDW4=','','"100.64.39.54"','expiredhost','linux','Linux','22.04','x86_64','Ubuntu','','development','','',NULL,'','','','{"Cloud":"","Platform":""}',NULL,'expiredhost','expiredhost','2023-03-02 09:19:57.276717255+01:00',0,1,0,'edafee4e-63fb-11ec-90d6-0242ac120003','ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMbK5ZXJsGOOWoBT4OmkPtgdPZe2Q7bDuS/zjn2CZxhK',0,1,'2023-03-02 09:14:21.791679181+01:00','2024-10-02 17:00:32.527947+02:00',0,'""','','',0); -INSERT INTO peers VALUES('cg3161rlo1hs9cq94gdg','bf1c8084-ba50-4ce7-9439-34653001fc3b','mVABSKj28gv+JRsf7e0NEGKgSOGTfU/nPB2cpuG56HU=','','"100.64.117.96"','testhost','linux','Linux','22.04','x86_64','Ubuntu','','development','','',NULL,'','','','{"Cloud":"","Platform":""}',NULL,'testhost','testhost','2023-03-06 18:21:27.252010027+01:00',0,0,0,'edafee4e-63fb-11ec-90d6-0242ac120003','ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINWvvUkFFcrj48CWTkNUb/do/n52i1L5dH4DhGu+4ZuM',0,0,'2023-03-07 09:02:47.442857106+01:00','2024-10-02 17:00:32.527947+02:00',0,'""','','',0); +INSERT INTO peers VALUES('cfvprsrlo1hqoo49ohog','bf1c8084-ba50-4ce7-9439-34653001fc3b','5rvhvriKJZ3S9oxYToVj5TzDM9u9y8cxg7htIMWlYAg=','72546A29-6BC8-4311-BCFC-9CDBF33F1A48','"100.64.114.31"','f2a34f6a4731','linux','Linux','11','unknown','Debian GNU/Linux','','0.12.0','','',NULL,'','','','{"Cloud":"","Platform":""}',NULL,'f2a34f6a4731','f2a34f6a4731','2023-03-02 09:21:02.189035775+01:00',0,0,0,'','ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILzUUSYG/LGnV8zarb2SGN+tib/PZ+M7cL4WtTzUrTpk',0,1,0,'2023-03-01 19:48:19.817799698+01:00','2024-10-02 17:00:32.527947+02:00',0,'""','','',0); +INSERT INTO peers VALUES('cg05lnblo1hkg2j514p0','bf1c8084-ba50-4ce7-9439-34653001fc3b','RlSy2vzoG2HyMBTUImXOiVhCBiiBa5qD5xzMxkiFDW4=','','"100.64.39.54"','expiredhost','linux','Linux','22.04','x86_64','Ubuntu','','development','','',NULL,'','','','{"Cloud":"","Platform":""}',NULL,'expiredhost','expiredhost','2023-03-02 09:19:57.276717255+01:00',0,1,0,'edafee4e-63fb-11ec-90d6-0242ac120003','ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMbK5ZXJsGOOWoBT4OmkPtgdPZe2Q7bDuS/zjn2CZxhK',0,1,0,'2023-03-02 09:14:21.791679181+01:00','2024-10-02 17:00:32.527947+02:00',0,'""','','',0); +INSERT INTO peers VALUES('cg3161rlo1hs9cq94gdg','bf1c8084-ba50-4ce7-9439-34653001fc3b','mVABSKj28gv+JRsf7e0NEGKgSOGTfU/nPB2cpuG56HU=','','"100.64.117.96"','testhost','linux','Linux','22.04','x86_64','Ubuntu','','development','','',NULL,'','','','{"Cloud":"","Platform":""}',NULL,'testhost','testhost','2023-03-06 18:21:27.252010027+01:00',0,0,0,'edafee4e-63fb-11ec-90d6-0242ac120003','ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINWvvUkFFcrj48CWTkNUb/do/n52i1L5dH4DhGu+4ZuM',0,0,0,'2023-03-07 09:02:47.442857106+01:00','2024-10-02 17:00:32.527947+02:00',0,'""','','',0); +INSERT INTO peers VALUES('csrnkiq7qv9d8aitqd50','bf1c8084-ba50-4ce7-9439-34653001fc3b','mVABSKj28gv+JRsf7e0NEGKgSOGTfU/nPB2cpuG56HU=','','"100.64.117.96"','testhost','linux','Linux','22.04','x86_64','Ubuntu','','development','','',NULL,'','','','{"Cloud":"","Platform":""}',NULL,'testhost','testhost','2023-03-06 18:21:27.252010027+01:00',0,0,0,'f4f6d672-63fb-11ec-90d6-0242ac120003','ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINWvvUkFFcrj48CWTkNUb/do/n52i1L5dH4DhGu+4ZuM',0,0,1,'2023-03-07 09:02:47.442857106+01:00','2024-10-02 17:00:32.527947+02:00',0,'""','','',0); INSERT INTO users VALUES('f4f6d672-63fb-11ec-90d6-0242ac120003','bf1c8084-ba50-4ce7-9439-34653001fc3b','user',0,0,'','[]',0,NULL,'2024-10-02 17:00:32.528196+02:00','api',0,''); INSERT INTO users VALUES('edafee4e-63fb-11ec-90d6-0242ac120003','bf1c8084-ba50-4ce7-9439-34653001fc3b','admin',0,0,'','[]',0,NULL,'2024-10-02 17:00:32.528196+02:00','api',0,''); INSERT INTO installations VALUES(1,''); diff --git a/management/server/testdata/storev1.sql b/management/server/testdata/storev1.sql index 69194d623..cda333d4f 100644 --- a/management/server/testdata/storev1.sql +++ b/management/server/testdata/storev1.sql @@ -1,6 +1,6 @@ CREATE TABLE `accounts` (`id` text,`created_by` text,`created_at` datetime,`domain` text,`domain_category` text,`is_domain_primary_account` numeric,`network_identifier` text,`network_net` text,`network_dns` text,`network_serial` integer,`dns_settings_disabled_management_groups` text,`settings_peer_login_expiration_enabled` numeric,`settings_peer_login_expiration` integer,`settings_regular_users_view_blocked` numeric,`settings_groups_propagation_enabled` numeric,`settings_jwt_groups_enabled` numeric,`settings_jwt_groups_claim_name` text,`settings_jwt_allow_groups` text,`settings_extra_peer_approval_enabled` numeric,`settings_extra_integrated_validator_groups` text,PRIMARY KEY (`id`)); CREATE TABLE `setup_keys` (`id` text,`account_id` text,`key` text,`name` text,`type` text,`created_at` datetime,`expires_at` datetime,`updated_at` datetime,`revoked` numeric,`used_times` integer,`last_used` datetime,`auto_groups` text,`usage_limit` integer,`ephemeral` numeric,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_setup_keys_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`)); -CREATE TABLE `peers` (`id` text,`account_id` text,`key` text,`setup_key` text,`ip` text,`meta_hostname` text,`meta_go_os` text,`meta_kernel` text,`meta_core` text,`meta_platform` text,`meta_os` text,`meta_os_version` text,`meta_wt_version` text,`meta_ui_version` text,`meta_kernel_version` text,`meta_network_addresses` text,`meta_system_serial_number` text,`meta_system_product_name` text,`meta_system_manufacturer` text,`meta_environment` text,`meta_files` text,`name` text,`dns_label` text,`peer_status_last_seen` datetime,`peer_status_connected` numeric,`peer_status_login_expired` numeric,`peer_status_requires_approval` numeric,`user_id` text,`ssh_key` text,`ssh_enabled` numeric,`login_expiration_enabled` numeric,`last_login` datetime,`created_at` datetime,`ephemeral` numeric,`location_connection_ip` text,`location_country_code` text,`location_city_name` text,`location_geo_name_id` integer,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_peers_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`)); +CREATE TABLE `peers` (`id` text,`account_id` text,`key` text,`setup_key` text,`ip` text,`meta_hostname` text,`meta_go_os` text,`meta_kernel` text,`meta_core` text,`meta_platform` text,`meta_os` text,`meta_os_version` text,`meta_wt_version` text,`meta_ui_version` text,`meta_kernel_version` text,`meta_network_addresses` text,`meta_system_serial_number` text,`meta_system_product_name` text,`meta_system_manufacturer` text,`meta_environment` text,`meta_files` text,`name` text,`dns_label` text,`peer_status_last_seen` datetime,`peer_status_connected` numeric,`peer_status_login_expired` numeric,`peer_status_requires_approval` numeric,`user_id` text,`ssh_key` text,`ssh_enabled` numeric,`login_expiration_enabled` numeric,`last_login` datetime DEFAULT NULL,`created_at` datetime,`ephemeral` numeric,`location_connection_ip` text,`location_country_code` text,`location_city_name` text,`location_geo_name_id` integer,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_peers_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`)); CREATE TABLE `users` (`id` text,`account_id` text,`role` text,`is_service_user` numeric,`non_deletable` numeric,`service_user_name` text,`auto_groups` text,`blocked` numeric,`last_login` datetime,`created_at` datetime,`issued` text DEFAULT "api",`integration_ref_id` integer,`integration_ref_integration_type` text,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_users_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`)); CREATE TABLE `personal_access_tokens` (`id` text,`user_id` text,`name` text,`hashed_token` text,`expiration_date` datetime,`created_by` text,`created_at` datetime,`last_used` datetime,PRIMARY KEY (`id`),CONSTRAINT `fk_users_pa_ts_g` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`)); CREATE TABLE `groups` (`id` text,`account_id` text,`name` text,`issued` text,`peers` text,`integration_ref_id` integer,`integration_ref_integration_type` text,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_groups_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`)); @@ -31,9 +31,9 @@ INSERT INTO setup_keys VALUES('831727121','auth0|61bf82ddeab084006aa1bccd','1B2B INSERT INTO setup_keys VALUES('1769568301','auth0|61bf82ddeab084006aa1bccd','EB51E9EB-A11F-4F6E-8E49-C982891B405A','Default key','reusable','2021-12-24 16:09:45.926073628+01:00','2022-01-23 16:09:45.926073628+01:00','2021-12-24 16:09:45.926073628+01:00',0,1,'2021-12-24 16:13:06.236748538+01:00','[]',0,0); INSERT INTO setup_keys VALUES('2485964613','google-oauth2|103201118415301331038','5AFB60DB-61F2-4251-8E11-494847EE88E9','Default key','reusable','2021-12-24 16:10:02.238476+01:00','2022-01-23 16:10:02.238476+01:00','2021-12-24 16:10:02.238476+01:00',0,1,'2021-12-24 16:12:05.994307717+01:00','[]',0,0); INSERT INTO setup_keys VALUES('3504804807','google-oauth2|103201118415301331038','A72E4DC2-00DE-4542-8A24-62945438104E','One-off key','one-off','2021-12-24 16:10:02.238478209+01:00','2022-01-23 16:10:02.238478209+01:00','2021-12-24 16:10:02.238478209+01:00',0,1,'2021-12-24 16:11:27.015741738+01:00','[]',0,0); -INSERT INTO peers VALUES('oMNaI8qWi0CyclSuwGR++SurxJyM3pQEiPEHwX8IREo=','auth0|61bf82ddeab084006aa1bccd','oMNaI8qWi0CyclSuwGR++SurxJyM3pQEiPEHwX8IREo=','EB51E9EB-A11F-4F6E-8E49-C982891B405A','"100.64.0.2"','braginini','linux','Linux','21.04','x86_64','Ubuntu','','','','',NULL,'','','','{"Cloud":"","Platform":""}',NULL,'braginini','braginini','2021-12-24 16:13:11.244342541+01:00',0,0,0,'','',0,0,'0001-01-01 00:00:00+00:00','2024-10-02 17:00:54.182618+02:00',0,'""','','',0); -INSERT INTO peers VALUES('xlx9/9D8+ibnRiIIB8nHGMxGOzxV17r8ShPHgi4aYSM=','auth0|61bf82ddeab084006aa1bccd','xlx9/9D8+ibnRiIIB8nHGMxGOzxV17r8ShPHgi4aYSM=','1B2B50B0-B3E8-4B0C-A426-525EDB8481BD','"100.64.0.1"','braginini','linux','Linux','21.04','x86_64','Ubuntu','','','','',NULL,'','','','{"Cloud":"","Platform":""}',NULL,'braginini','braginini-1','2021-12-24 16:12:49.089339333+01:00',0,0,0,'','',0,0,'0001-01-01 00:00:00+00:00','2024-10-02 17:00:54.182618+02:00',0,'""','','',0); -INSERT INTO peers VALUES('6kjbmVq1hmucVzvBXo5OucY5OYv+jSsB1jUTLq291Dw=','google-oauth2|103201118415301331038','6kjbmVq1hmucVzvBXo5OucY5OYv+jSsB1jUTLq291Dw=','5AFB60DB-61F2-4251-8E11-494847EE88E9','"100.64.0.2"','braginini','linux','Linux','21.04','x86_64','Ubuntu','','','','',NULL,'','','','{"Cloud":"","Platform":""}',NULL,'braginini','braginini','2021-12-24 16:12:05.994305438+01:00',0,0,0,'','',0,0,'0001-01-01 00:00:00+00:00','2024-10-02 17:00:54.228182+02:00',0,'""','','',0); -INSERT INTO peers VALUES('Ok+5QMdt/UjoktNOvicGYj+IX2g98p+0N2PJ3vJ45RI=','google-oauth2|103201118415301331038','Ok+5QMdt/UjoktNOvicGYj+IX2g98p+0N2PJ3vJ45RI=','A72E4DC2-00DE-4542-8A24-62945438104E','"100.64.0.1"','braginini','linux','Linux','21.04','x86_64','Ubuntu','','','','',NULL,'','','','{"Cloud":"","Platform":""}',NULL,'braginini','braginini-1','2021-12-24 16:11:27.015739803+01:00',0,0,0,'','',0,0,'0001-01-01 00:00:00+00:00','2024-10-02 17:00:54.228182+02:00',0,'""','','',0); +INSERT INTO peers VALUES('oMNaI8qWi0CyclSuwGR++SurxJyM3pQEiPEHwX8IREo=','auth0|61bf82ddeab084006aa1bccd','oMNaI8qWi0CyclSuwGR++SurxJyM3pQEiPEHwX8IREo=','EB51E9EB-A11F-4F6E-8E49-C982891B405A','"100.64.0.2"','braginini','linux','Linux','21.04','x86_64','Ubuntu','','','','',NULL,'','','','{"Cloud":"","Platform":""}',NULL,'braginini','braginini','2021-12-24 16:13:11.244342541+01:00',0,0,0,'','',0,0,NULL,'2024-10-02 17:00:54.182618+02:00',0,'""','','',0); +INSERT INTO peers VALUES('xlx9/9D8+ibnRiIIB8nHGMxGOzxV17r8ShPHgi4aYSM=','auth0|61bf82ddeab084006aa1bccd','xlx9/9D8+ibnRiIIB8nHGMxGOzxV17r8ShPHgi4aYSM=','1B2B50B0-B3E8-4B0C-A426-525EDB8481BD','"100.64.0.1"','braginini','linux','Linux','21.04','x86_64','Ubuntu','','','','',NULL,'','','','{"Cloud":"","Platform":""}',NULL,'braginini','braginini-1','2021-12-24 16:12:49.089339333+01:00',0,0,0,'','',0,0,NULL,'2024-10-02 17:00:54.182618+02:00',0,'""','','',0); +INSERT INTO peers VALUES('6kjbmVq1hmucVzvBXo5OucY5OYv+jSsB1jUTLq291Dw=','google-oauth2|103201118415301331038','6kjbmVq1hmucVzvBXo5OucY5OYv+jSsB1jUTLq291Dw=','5AFB60DB-61F2-4251-8E11-494847EE88E9','"100.64.0.2"','braginini','linux','Linux','21.04','x86_64','Ubuntu','','','','',NULL,'','','','{"Cloud":"","Platform":""}',NULL,'braginini','braginini','2021-12-24 16:12:05.994305438+01:00',0,0,0,'','',0,0,NULL,'2024-10-02 17:00:54.228182+02:00',0,'""','','',0); +INSERT INTO peers VALUES('Ok+5QMdt/UjoktNOvicGYj+IX2g98p+0N2PJ3vJ45RI=','google-oauth2|103201118415301331038','Ok+5QMdt/UjoktNOvicGYj+IX2g98p+0N2PJ3vJ45RI=','A72E4DC2-00DE-4542-8A24-62945438104E','"100.64.0.1"','braginini','linux','Linux','21.04','x86_64','Ubuntu','','','','',NULL,'','','','{"Cloud":"","Platform":""}',NULL,'braginini','braginini-1','2021-12-24 16:11:27.015739803+01:00',0,0,0,'','',0,0,NULL,'2024-10-02 17:00:54.228182+02:00',1,'""','','',0); INSERT INTO installations VALUES(1,''); diff --git a/management/server/user.go b/management/server/user.go index fcf3d34ff..17770a423 100644 --- a/management/server/user.go +++ b/management/server/user.go @@ -287,6 +287,10 @@ func (am *DefaultAccountManager) deleteRegularUser(ctx context.Context, account } delete(account.Users, targetUserID) + if updateAccountPeers { + account.Network.IncSerial() + } + err = am.Store.SaveAccount(ctx, account) if err != nil { return err @@ -311,12 +315,20 @@ func (am *DefaultAccountManager) deleteUserPeers(ctx context.Context, initiatorU return false, nil } - peerIDs := make([]string, 0, len(peers)) - for _, peer := range peers { - peerIDs = append(peerIDs, peer.ID) + eventsToStore, err := deletePeers(ctx, am, am.Store, account.Id, initiatorUserID, peers) + if err != nil { + return false, err } - return hadPeers, am.deletePeers(ctx, account, peerIDs, initiatorUserID) + for _, storeEvent := range eventsToStore { + storeEvent() + } + + for _, peer := range peers { + account.DeletePeer(peer.ID) + } + + return hadPeers, nil } // InviteUser resend invitations to users who haven't activated their accounts prior to the expiration period. @@ -628,7 +640,7 @@ func (am *DefaultAccountManager) SaveOrAddUsers(ctx context.Context, accountID, } if len(expiredPeers) > 0 { - if err := am.expireAndUpdatePeers(ctx, account, expiredPeers); err != nil { + if err := am.expireAndUpdatePeers(ctx, account.Id, expiredPeers); err != nil { log.WithContext(ctx).Errorf("failed update expired peers: %s", err) return nil, err } @@ -955,7 +967,7 @@ func (am *DefaultAccountManager) GetUsersFromAccount(ctx context.Context, accoun } // expireAndUpdatePeers expires all peers of the given user and updates them in the account -func (am *DefaultAccountManager) expireAndUpdatePeers(ctx context.Context, account *types.Account, peers []*nbpeer.Peer) error { +func (am *DefaultAccountManager) expireAndUpdatePeers(ctx context.Context, accountID string, peers []*nbpeer.Peer) error { var peerIDs []string for _, peer := range peers { // nolint:staticcheck @@ -966,16 +978,13 @@ func (am *DefaultAccountManager) expireAndUpdatePeers(ctx context.Context, accou } peerIDs = append(peerIDs, peer.ID) peer.MarkLoginExpired(true) - account.UpdatePeer(peer) - if err := am.Store.SavePeerStatus(account.Id, peer.ID, *peer.Status); err != nil { - return fmt.Errorf("failed saving peer status for peer %s: %s", peer.ID, err) + + if err := am.Store.SavePeerStatus(ctx, store.LockingStrengthUpdate, accountID, peer.ID, *peer.Status); err != nil { + return err } - - log.WithContext(ctx).Tracef("mark peer %s login expired", peer.ID) - am.StoreEvent( ctx, - peer.UserID, peer.ID, account.Id, + peer.UserID, peer.ID, accountID, activity.PeerLoginExpired, peer.EventMeta(am.GetDNSDomain()), ) } @@ -983,7 +992,7 @@ func (am *DefaultAccountManager) expireAndUpdatePeers(ctx context.Context, accou if len(peerIDs) != 0 { // this will trigger peer disconnect from the management service am.peersUpdateManager.CloseChannels(ctx, peerIDs) - am.UpdateAccountPeers(ctx, account.Id) + am.UpdateAccountPeers(ctx, accountID) } return nil } @@ -1085,6 +1094,9 @@ func (am *DefaultAccountManager) DeleteRegularUsers(ctx context.Context, account deletedUsersMeta[targetUserID] = meta } + if updateAccountPeers { + account.Network.IncSerial() + } err = am.Store.SaveAccount(ctx, account) if err != nil { return fmt.Errorf("failed to delete users: %w", err) From 78da6b42ad41bedfa51d9a49f5dcc26f15f353a5 Mon Sep 17 00:00:00 2001 From: Eddie Garcia Date: Wed, 22 Jan 2025 12:57:54 -0500 Subject: [PATCH 23/92] [misc] Fix typo in test output (#3216) Fix a typo in test output --- .github/workflows/golang-test-freebsd.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/golang-test-freebsd.yml b/.github/workflows/golang-test-freebsd.yml index 7a2d3cf3c..0f510cb3a 100644 --- a/.github/workflows/golang-test-freebsd.yml +++ b/.github/workflows/golang-test-freebsd.yml @@ -33,7 +33,7 @@ jobs: time go build -o netbird client/main.go # check all component except management, since we do not support management server on freebsd time go test -timeout 1m -failfast ./base62/... - # NOTE: without -p1 `client/internal/dns` will fail becasue of `listen udp4 :33100: bind: address already in use` + # NOTE: without -p1 `client/internal/dns` will fail because of `listen udp4 :33100: bind: address already in use` time go test -timeout 8m -failfast -p 1 ./client/... time go test -timeout 1m -failfast ./dns/... time go test -timeout 1m -failfast ./encryption/... From 8c965434ae67329885cc5bfc66ad580e42fbb984 Mon Sep 17 00:00:00 2001 From: Pascal Fischer <32096965+pascal-fischer@users.noreply.github.com> Date: Wed, 22 Jan 2025 19:33:20 +0100 Subject: [PATCH 24/92] [management] remove peer from group on delete (#3223) --- .../peers_handler_benchmark_test.go | 16 +++--- management/server/peer.go | 13 +++++ management/server/peer_test.go | 49 +++++++++++++++++++ 3 files changed, 70 insertions(+), 8 deletions(-) diff --git a/management/server/http/testing/benchmarks/peers_handler_benchmark_test.go b/management/server/http/testing/benchmarks/peers_handler_benchmark_test.go index 2eb50e4b4..23b4edefb 100644 --- a/management/server/http/testing/benchmarks/peers_handler_benchmark_test.go +++ b/management/server/http/testing/benchmarks/peers_handler_benchmark_test.go @@ -145,14 +145,14 @@ func BenchmarkGetAllPeers(b *testing.B) { func BenchmarkDeletePeer(b *testing.B) { var expectedMetrics = map[string]testing_tools.PerformanceMetrics{ - "Peers - XS": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 4, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 15}, - "Peers - S": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 4, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 15}, - "Peers - M": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 4, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 15}, - "Peers - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 4, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 15}, - "Groups - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 4, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 15}, - "Users - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 4, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 15}, - "Setup Keys - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 4, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 15}, - "Peers - XL": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 4, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 15}, + "Peers - XS": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 4, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 16}, + "Peers - S": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 4, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 16}, + "Peers - M": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 4, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 16}, + "Peers - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 4, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 16}, + "Groups - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 4, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 16}, + "Users - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 4, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 16}, + "Setup Keys - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 4, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 16}, + "Peers - XL": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 4, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 16}, } log.SetOutput(io.Discard) diff --git a/management/server/peer.go b/management/server/peer.go index 5b0f12899..e5442acea 100644 --- a/management/server/peer.go +++ b/management/server/peer.go @@ -380,6 +380,19 @@ func (am *DefaultAccountManager) DeletePeer(ctx context.Context, accountID, peer return err } + groups, err := transaction.GetPeerGroups(ctx, store.LockingStrengthUpdate, accountID, peerID) + if err != nil { + return fmt.Errorf("failed to get peer groups: %w", err) + } + + for _, group := range groups { + group.RemovePeer(peerID) + err = transaction.SaveGroup(ctx, store.LockingStrengthUpdate, group) + if err != nil { + return fmt.Errorf("failed to save group: %w", err) + } + } + eventsToStore, err = deletePeers(ctx, am, transaction, accountID, userID, []*nbpeer.Peer{peer}) return err }) diff --git a/management/server/peer_test.go b/management/server/peer_test.go index bf712f38a..40f8d15d5 100644 --- a/management/server/peer_test.go +++ b/management/server/peer_test.go @@ -1728,3 +1728,52 @@ func TestPeerAccountPeersUpdate(t *testing.T) { } }) } + +func Test_DeletePeer(t *testing.T) { + manager, err := createManager(t) + if err != nil { + t.Fatal(err) + return + } + + // account with an admin and a regular user + accountID := "test_account" + adminUser := "account_creator" + account := newAccountWithId(context.Background(), accountID, adminUser, "") + account.Peers = map[string]*nbpeer.Peer{ + "peer1": { + ID: "peer1", + AccountID: accountID, + }, + "peer2": { + ID: "peer2", + AccountID: accountID, + }, + } + account.Groups = map[string]*types.Group{ + "group1": { + ID: "group1", + Name: "Group1", + Peers: []string{"peer1", "peer2"}, + }, + } + + err = manager.Store.SaveAccount(context.Background(), account) + if err != nil { + t.Fatal(err) + return + } + + err = manager.DeletePeer(context.Background(), accountID, "peer1", adminUser) + if err != nil { + t.Fatalf("DeletePeer failed: %v", err) + } + + _, err = manager.GetPeer(context.Background(), accountID, "peer1", adminUser) + assert.Error(t, err) + + group, err := manager.GetGroup(context.Background(), accountID, "group1", adminUser) + assert.NoError(t, err) + assert.NotContains(t, group.Peers, "peer1") + +} From 69f48db0a35f99028170c1e94fdb5b322993c2b0 Mon Sep 17 00:00:00 2001 From: Pascal Fischer <32096965+pascal-fischer@users.noreply.github.com> Date: Wed, 22 Jan 2025 19:53:20 +0100 Subject: [PATCH 25/92] [management] disable prepareStmt for sqlite (#3228) --- go.mod | 6 +- go.sum | 11 ++-- management/server/store/sql_store.go | 14 +++-- management/server/store/sql_store_test.go | 74 ++++++++++++++++++++++- management/server/store/store.go | 2 +- 5 files changed, 92 insertions(+), 15 deletions(-) diff --git a/go.mod b/go.mod index fa573bb9c..895ad5618 100644 --- a/go.mod +++ b/go.mod @@ -55,7 +55,7 @@ require ( github.com/libdns/route53 v1.5.0 github.com/libp2p/go-netroute v0.2.1 github.com/magiconair/properties v1.8.7 - github.com/mattn/go-sqlite3 v1.14.19 + github.com/mattn/go-sqlite3 v1.14.22 github.com/mdlayher/socket v0.5.1 github.com/miekg/dns v1.1.59 github.com/mitchellh/hashstructure/v2 v2.0.2 @@ -100,8 +100,8 @@ require ( gopkg.in/yaml.v3 v3.0.1 gorm.io/driver/mysql v1.5.7 gorm.io/driver/postgres v1.5.7 - gorm.io/driver/sqlite v1.5.3 - gorm.io/gorm v1.25.7 + gorm.io/driver/sqlite v1.5.7 + gorm.io/gorm v1.25.12 nhooyr.io/websocket v1.8.11 ) diff --git a/go.sum b/go.sum index a099498fb..2aae595f0 100644 --- a/go.sum +++ b/go.sum @@ -475,8 +475,8 @@ github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= -github.com/mattn/go-sqlite3 v1.14.19 h1:fhGleo2h1p8tVChob4I9HpmVFIAkKGpiukdrgQbWfGI= -github.com/mattn/go-sqlite3 v1.14.19/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw= github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o= github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g= @@ -1241,10 +1241,11 @@ gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo= gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM= gorm.io/driver/postgres v1.5.7 h1:8ptbNJTDbEmhdr62uReG5BGkdQyeasu/FZHxI0IMGnM= gorm.io/driver/postgres v1.5.7/go.mod h1:3e019WlBaYI5o5LIdNV+LyxCMNtLOQETBXL2h4chKpA= -gorm.io/driver/sqlite v1.5.3 h1:7/0dUgX28KAcopdfbRWWl68Rflh6osa4rDh+m51KL2g= -gorm.io/driver/sqlite v1.5.3/go.mod h1:qxAuCol+2r6PannQDpOP1FP6ag3mKi4esLnB/jHed+4= -gorm.io/gorm v1.25.7 h1:VsD6acwRjz2zFxGO50gPO6AkNs7KKnvfzUjHQhZDz/A= +gorm.io/driver/sqlite v1.5.7 h1:8NvsrhP0ifM7LX9G4zPB97NwovUakUxc+2V2uuf3Z1I= +gorm.io/driver/sqlite v1.5.7/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4= gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= +gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= +gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= gotest.tools/v3 v3.5.0 h1:Ljk6PdHdOhAb5aDMWXjDLMMhph+BpztA4v1QdqEW2eY= gotest.tools/v3 v3.5.0/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= gvisor.dev/gvisor v0.0.0-20231020174304-db3d49b921f9 h1:sCEaoA7ZmkuFwa2IR61pl4+RYZPwCJOiaSYT0k+BRf8= diff --git a/management/server/store/sql_store.go b/management/server/store/sql_store.go index 900d81322..2179f0754 100644 --- a/management/server/store/sql_store.go +++ b/management/server/store/sql_store.go @@ -956,7 +956,7 @@ func NewSqliteStore(ctx context.Context, dataDir string, metrics telemetry.AppMe } file := filepath.Join(dataDir, storeStr) - db, err := gorm.Open(sqlite.Open(file), getGormConfig()) + db, err := gorm.Open(sqlite.Open(file), getGormConfig(SqliteStoreEngine)) if err != nil { return nil, err } @@ -966,7 +966,7 @@ func NewSqliteStore(ctx context.Context, dataDir string, metrics telemetry.AppMe // NewPostgresqlStore creates a new Postgres store. func NewPostgresqlStore(ctx context.Context, dsn string, metrics telemetry.AppMetrics) (*SqlStore, error) { - db, err := gorm.Open(postgres.Open(dsn), getGormConfig()) + db, err := gorm.Open(postgres.Open(dsn), getGormConfig(PostgresStoreEngine)) if err != nil { return nil, err } @@ -976,7 +976,7 @@ func NewPostgresqlStore(ctx context.Context, dsn string, metrics telemetry.AppMe // NewMysqlStore creates a new MySQL store. func NewMysqlStore(ctx context.Context, dsn string, metrics telemetry.AppMetrics) (*SqlStore, error) { - db, err := gorm.Open(mysql.Open(dsn+"?charset=utf8&parseTime=True&loc=Local"), getGormConfig()) + db, err := gorm.Open(mysql.Open(dsn+"?charset=utf8&parseTime=True&loc=Local"), getGormConfig(MysqlStoreEngine)) if err != nil { return nil, err } @@ -984,11 +984,15 @@ func NewMysqlStore(ctx context.Context, dsn string, metrics telemetry.AppMetrics return NewSqlStore(ctx, db, MysqlStoreEngine, metrics) } -func getGormConfig() *gorm.Config { +func getGormConfig(engine Engine) *gorm.Config { + prepStmt := true + if engine == SqliteStoreEngine { + prepStmt = false + } return &gorm.Config{ Logger: logger.Default.LogMode(logger.Silent), CreateBatchSize: 400, - PrepareStmt: true, + PrepareStmt: prepStmt, } } diff --git a/management/server/store/sql_store_test.go b/management/server/store/sql_store_test.go index cb51dab51..9350da1c8 100644 --- a/management/server/store/sql_store_test.go +++ b/management/server/store/sql_store_test.go @@ -10,16 +10,18 @@ import ( "net/netip" "os" "runtime" + "sync" "testing" "time" "github.com/google/uuid" - "github.com/netbirdio/netbird/management/server/util" "github.com/rs/xid" log "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/netbirdio/netbird/management/server/util" + nbdns "github.com/netbirdio/netbird/dns" resourceTypes "github.com/netbirdio/netbird/management/server/networks/resources/types" routerTypes "github.com/netbirdio/netbird/management/server/networks/routers/types" @@ -2843,3 +2845,73 @@ func TestSqlStore_DeletePeer(t *testing.T) { require.Error(t, err) require.Nil(t, peer) } + +func TestSqlStore_DatabaseBlocking(t *testing.T) { + store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/store_with_expired_peers.sql", t.TempDir()) + t.Cleanup(cleanup) + if err != nil { + t.Fatal(err) + } + + concurrentReads := 40 + + testRunSuccessful := false + wgSuccess := sync.WaitGroup{} + wgSuccess.Add(concurrentReads) + + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + + start := make(chan struct{}) + + for i := 0; i < concurrentReads/2; i++ { + go func() { + t.Logf("Entered routine 1-%d", i) + + <-start + err := store.ExecuteInTransaction(context.Background(), func(tx Store) error { + _, err := tx.GetAccountIDByPeerID(context.Background(), LockingStrengthShare, "cfvprsrlo1hqoo49ohog") + return err + }) + if err != nil { + t.Errorf("Failed, got error: %v", err) + return + } + + t.Log("Got User from routine 1") + wgSuccess.Done() + }() + } + + for i := 0; i < concurrentReads/2; i++ { + go func() { + t.Logf("Entered routine 2-%d", i) + + <-start + _, err := store.GetAccountIDByPeerID(context.Background(), LockingStrengthShare, "cfvprsrlo1hqoo49ohog") + if err != nil { + t.Errorf("Failed, got error: %v", err) + return + } + + t.Log("Got User from routine 2") + wgSuccess.Done() + }() + } + + time.Sleep(200 * time.Millisecond) + close(start) + t.Log("Started routines") + + go func() { + wgSuccess.Wait() + testRunSuccessful = true + }() + + <-ctx.Done() + if !testRunSuccessful { + t.Fatalf("Test failed") + } + + t.Logf("Test completed") +} diff --git a/management/server/store/store.go b/management/server/store/store.go index 245df1c3e..4b4dcfb4f 100644 --- a/management/server/store/store.go +++ b/management/server/store/store.go @@ -319,7 +319,7 @@ func NewTestStoreFromSQL(ctx context.Context, filename string, dataDir string) ( } file := filepath.Join(dataDir, storeStr) - db, err := gorm.Open(sqlite.Open(file), getGormConfig()) + db, err := gorm.Open(sqlite.Open(file), getGormConfig(kind)) if err != nil { return nil, nil, err } From aafa9c67fc33b92eea831cc13e309a22bd9d23e4 Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Thu, 23 Jan 2025 16:57:11 +0100 Subject: [PATCH 26/92] [client] Fix freebsd default routes (#3230) --- client/iface/device/device_kernel_unix.go | 2 -- client/iface/device/device_usp_unix.go | 13 ------------- client/iface/freebsd/link.go | 5 +++++ 3 files changed, 5 insertions(+), 15 deletions(-) diff --git a/client/iface/device/device_kernel_unix.go b/client/iface/device/device_kernel_unix.go index f355d2cf7..0dfed4d90 100644 --- a/client/iface/device/device_kernel_unix.go +++ b/client/iface/device/device_kernel_unix.go @@ -33,8 +33,6 @@ type TunKernelDevice struct { } func NewKernelDevice(name string, address WGAddress, wgPort int, key string, mtu int, transportNet transport.Net) *TunKernelDevice { - checkUser() - ctx, cancel := context.WithCancel(context.Background()) return &TunKernelDevice{ ctx: ctx, diff --git a/client/iface/device/device_usp_unix.go b/client/iface/device/device_usp_unix.go index 643d77565..3562f312d 100644 --- a/client/iface/device/device_usp_unix.go +++ b/client/iface/device/device_usp_unix.go @@ -4,8 +4,6 @@ package device import ( "fmt" - "os" - "runtime" log "github.com/sirupsen/logrus" "golang.zx2c4.com/wireguard/device" @@ -32,8 +30,6 @@ type USPDevice struct { func NewUSPDevice(name string, address WGAddress, port int, key string, mtu int, iceBind *bind.ICEBind) *USPDevice { log.Infof("using userspace bind mode") - checkUser() - return &USPDevice{ name: name, address: address, @@ -134,12 +130,3 @@ func (t *USPDevice) assignAddr() error { return link.assignAddr(t.address) } - -func checkUser() { - if runtime.GOOS == "freebsd" { - euid := os.Geteuid() - if euid != 0 { - log.Warn("newTunUSPDevice: on netbird must run as root to be able to assign address to the tun interface with ifconfig") - } - } -} diff --git a/client/iface/freebsd/link.go b/client/iface/freebsd/link.go index b7924f04b..e28970fa1 100644 --- a/client/iface/freebsd/link.go +++ b/client/iface/freebsd/link.go @@ -203,6 +203,11 @@ func (l *Link) setAddr(ip, netmask string) error { return fmt.Errorf("set interface addr: %w", err) } + cmd = exec.Command("ifconfig", l.name, "inet6", "fe80::/64") + if out, err := cmd.CombinedOutput(); err != nil { + log.Debugf("adding address command '%v' failed with output: %s", cmd.String(), out) + } + return nil } From 3cc485759ea15208e188b6bb27bd989a9b1eb45e Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Thu, 23 Jan 2025 17:59:22 +0100 Subject: [PATCH 27/92] [client] Use correct stdout/stderr log paths for debug bundle on macOS (#3231) --- client/server/debug.go | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/client/server/debug.go b/client/server/debug.go index de63697bf..a35a77a00 100644 --- a/client/server/debug.go +++ b/client/server/debug.go @@ -16,6 +16,7 @@ import ( "net/netip" "os" "path/filepath" + "runtime" "sort" "strings" "time" @@ -132,6 +133,9 @@ const ( clientLogFile = "client.log" errorLogFile = "netbird.err" stdoutLogFile = "netbird.out" + + darwinErrorLogPath = "/var/log/netbird.out.log" + darwinStdoutLogPath = "/var/log/netbird.err.log" ) // DebugBundle creates a debug bundle and returns the location. @@ -410,12 +414,17 @@ func (s *Server) addLogfile(req *proto.DebugBundleRequest, anonymizer *anonymize return fmt.Errorf("add client log file to zip: %w", err) } - errLogPath := filepath.Join(logDir, errorLogFile) - if err := s.addSingleLogfile(errLogPath, errorLogFile, req, anonymizer, archive); err != nil { + stdErrLogPath := filepath.Join(logDir, errorLogFile) + stdoutLogPath := filepath.Join(logDir, stdoutLogFile) + if runtime.GOOS == "darwin" { + stdErrLogPath = darwinErrorLogPath + stdoutLogPath = darwinStdoutLogPath + } + + if err := s.addSingleLogfile(stdErrLogPath, errorLogFile, req, anonymizer, archive); err != nil { log.Warnf("Failed to add %s to zip: %v", errorLogFile, err) } - stdoutLogPath := filepath.Join(logDir, stdoutLogFile) if err := s.addSingleLogfile(stdoutLogPath, stdoutLogFile, req, anonymizer, archive); err != nil { log.Warnf("Failed to add %s to zip: %v", stdoutLogFile, err) } From 2e61ce006d71f27d147b1d490a794b1861d8c362 Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Thu, 23 Jan 2025 17:59:44 +0100 Subject: [PATCH 28/92] [client] Back up corrupted state files and present them in the debug bundle (#3227) --- client/internal/statemanager/manager.go | 25 ++++++++++++------ client/server/debug.go | 34 +++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 8 deletions(-) diff --git a/client/internal/statemanager/manager.go b/client/internal/statemanager/manager.go index 9a99c76f1..29f962ad2 100644 --- a/client/internal/statemanager/manager.go +++ b/client/internal/statemanager/manager.go @@ -303,20 +303,29 @@ func (m *Manager) loadStateFile(deleteCorrupt bool) (map[string]json.RawMessage, var rawStates map[string]json.RawMessage if err := json.Unmarshal(data, &rawStates); err != nil { - if deleteCorrupt { - log.Warn("State file appears to be corrupted, attempting to delete it", err) - if err := os.Remove(m.filePath); err != nil { - log.Errorf("Failed to delete corrupted state file: %v", err) - } else { - log.Info("State file deleted") - } - } + m.handleCorruptedState(deleteCorrupt) return nil, fmt.Errorf("unmarshal states: %w", err) } return rawStates, nil } +// handleCorruptedState creates a backup of a corrupted state file by moving it +func (m *Manager) handleCorruptedState(deleteCorrupt bool) { + if !deleteCorrupt { + return + } + log.Warn("State file appears to be corrupted, attempting to back it up") + + backupPath := fmt.Sprintf("%s.corrupted.%d", m.filePath, time.Now().UnixNano()) + if err := os.Rename(m.filePath, backupPath); err != nil { + log.Errorf("Failed to backup corrupted state file: %v", err) + return + } + + log.Infof("Created backup of corrupted state file at: %s", backupPath) +} + // loadSingleRawState unmarshals a raw state into a concrete state object func (m *Manager) loadSingleRawState(name string, rawState json.RawMessage) (State, error) { stateType, ok := m.stateTypes[name] diff --git a/client/server/debug.go b/client/server/debug.go index a35a77a00..a37195b29 100644 --- a/client/server/debug.go +++ b/client/server/debug.go @@ -196,6 +196,10 @@ func (s *Server) createArchive(bundlePath *os.File, req *proto.DebugBundleReques log.Errorf("Failed to add state file to debug bundle: %v", err) } + if err := s.addCorruptedStateFiles(archive); err != nil { + log.Errorf("Failed to add corrupted state files to debug bundle: %v", err) + } + if s.logFile != "console" { if err := s.addLogfile(req, anonymizer, archive); err != nil { return fmt.Errorf("add log file: %w", err) @@ -407,6 +411,36 @@ func (s *Server) addStateFile(req *proto.DebugBundleRequest, anonymizer *anonymi return nil } +func (s *Server) addCorruptedStateFiles(archive *zip.Writer) error { + pattern := statemanager.GetDefaultStatePath() + if pattern == "" { + return nil + } + pattern += "*.corrupted.*" + matches, err := filepath.Glob(pattern) + if err != nil { + return fmt.Errorf("find corrupted state files: %w", err) + } + + for _, match := range matches { + data, err := os.ReadFile(match) + if err != nil { + log.Warnf("Failed to read corrupted state file %s: %v", match, err) + continue + } + + fileName := filepath.Base(match) + if err := addFileToZip(archive, bytes.NewReader(data), "corrupted_states/"+fileName); err != nil { + log.Warnf("Failed to add corrupted state file %s to zip: %v", fileName, err) + continue + } + + log.Debugf("Added corrupted state file to debug bundle: %s", fileName) + } + + return nil +} + func (s *Server) addLogfile(req *proto.DebugBundleRequest, anonymizer *anonymize.Anonymizer, archive *zip.Writer) error { logDir := filepath.Dir(s.logFile) From 790a9ed7df80fb8d207f8be5d0429d26442eb567 Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Thu, 23 Jan 2025 18:00:05 +0100 Subject: [PATCH 29/92] [client] Match more specific dns handler first (#3226) --- client/internal/dns/handler_chain.go | 27 +++- client/internal/dns/handler_chain_test.go | 153 ++++++++++++++++++++++ 2 files changed, 173 insertions(+), 7 deletions(-) diff --git a/client/internal/dns/handler_chain.go b/client/internal/dns/handler_chain.go index 5f63d1ab3..673f410e2 100644 --- a/client/internal/dns/handler_chain.go +++ b/client/internal/dns/handler_chain.go @@ -105,17 +105,30 @@ func (c *HandlerChain) AddHandler(pattern string, handler dns.Handler, priority MatchSubdomains: matchSubdomains, } - // Insert handler in priority order - pos := 0 + pos := c.findHandlerPosition(entry) + c.handlers = append(c.handlers[:pos], append([]HandlerEntry{entry}, c.handlers[pos:]...)...) +} + +// findHandlerPosition determines where to insert a new handler based on priority and specificity +func (c *HandlerChain) findHandlerPosition(newEntry HandlerEntry) int { for i, h := range c.handlers { - if h.Priority < priority { - pos = i - break + // prio first + if h.Priority < newEntry.Priority { + return i + } + + // domain specificity next + if h.Priority == newEntry.Priority { + newDots := strings.Count(newEntry.Pattern, ".") + existingDots := strings.Count(h.Pattern, ".") + if newDots > existingDots { + return i + } } - pos = i + 1 } - c.handlers = append(c.handlers[:pos], append([]HandlerEntry{entry}, c.handlers[pos:]...)...) + // add at end + return len(c.handlers) } // RemoveHandler removes a handler for the given pattern and priority diff --git a/client/internal/dns/handler_chain_test.go b/client/internal/dns/handler_chain_test.go index eb40c907f..d04bfbbb3 100644 --- a/client/internal/dns/handler_chain_test.go +++ b/client/internal/dns/handler_chain_test.go @@ -677,3 +677,156 @@ func TestHandlerChain_CaseSensitivity(t *testing.T) { }) } } + +func TestHandlerChain_DomainSpecificityOrdering(t *testing.T) { + tests := []struct { + name string + scenario string + ops []struct { + action string + pattern string + priority int + subdomain bool + } + query string + expectedMatch string + }{ + { + name: "more specific domain matches first", + scenario: "sub.example.com should match before example.com", + ops: []struct { + action string + pattern string + priority int + subdomain bool + }{ + {"add", "example.com.", nbdns.PriorityMatchDomain, true}, + {"add", "sub.example.com.", nbdns.PriorityMatchDomain, false}, + }, + query: "sub.example.com.", + expectedMatch: "sub.example.com.", + }, + { + name: "more specific domain matches first, both match subdomains", + scenario: "sub.example.com should match before example.com", + ops: []struct { + action string + pattern string + priority int + subdomain bool + }{ + {"add", "example.com.", nbdns.PriorityMatchDomain, true}, + {"add", "sub.example.com.", nbdns.PriorityMatchDomain, true}, + }, + query: "sub.example.com.", + expectedMatch: "sub.example.com.", + }, + { + name: "maintain specificity order after removal", + scenario: "after removing most specific, should fall back to less specific", + ops: []struct { + action string + pattern string + priority int + subdomain bool + }{ + {"add", "example.com.", nbdns.PriorityMatchDomain, true}, + {"add", "sub.example.com.", nbdns.PriorityMatchDomain, true}, + {"add", "test.sub.example.com.", nbdns.PriorityMatchDomain, false}, + {"remove", "test.sub.example.com.", nbdns.PriorityMatchDomain, false}, + }, + query: "test.sub.example.com.", + expectedMatch: "sub.example.com.", + }, + { + name: "priority overrides specificity", + scenario: "less specific domain with higher priority should match first", + ops: []struct { + action string + pattern string + priority int + subdomain bool + }{ + {"add", "sub.example.com.", nbdns.PriorityMatchDomain, false}, + {"add", "example.com.", nbdns.PriorityDNSRoute, true}, + }, + query: "sub.example.com.", + expectedMatch: "example.com.", + }, + { + name: "equal priority respects specificity", + scenario: "with equal priority, more specific domain should match", + ops: []struct { + action string + pattern string + priority int + subdomain bool + }{ + {"add", "example.com.", nbdns.PriorityMatchDomain, true}, + {"add", "other.example.com.", nbdns.PriorityMatchDomain, true}, + {"add", "sub.example.com.", nbdns.PriorityMatchDomain, false}, + }, + query: "sub.example.com.", + expectedMatch: "sub.example.com.", + }, + { + name: "specific matches before wildcard", + scenario: "specific domain should match before wildcard at same priority", + ops: []struct { + action string + pattern string + priority int + subdomain bool + }{ + {"add", "*.example.com.", nbdns.PriorityDNSRoute, false}, + {"add", "sub.example.com.", nbdns.PriorityDNSRoute, false}, + }, + query: "sub.example.com.", + expectedMatch: "sub.example.com.", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + chain := nbdns.NewHandlerChain() + handlers := make(map[string]*nbdns.MockSubdomainHandler) + + for _, op := range tt.ops { + if op.action == "add" { + handler := &nbdns.MockSubdomainHandler{Subdomains: op.subdomain} + handlers[op.pattern] = handler + chain.AddHandler(op.pattern, handler, op.priority, nil) + } else { + chain.RemoveHandler(op.pattern, op.priority) + } + } + + r := new(dns.Msg) + r.SetQuestion(tt.query, dns.TypeA) + w := &nbdns.ResponseWriterChain{ResponseWriter: &mockResponseWriter{}} + + // Setup handler expectations + for pattern, handler := range handlers { + if pattern == tt.expectedMatch { + handler.On("ServeDNS", mock.Anything, r).Run(func(args mock.Arguments) { + w := args.Get(0).(dns.ResponseWriter) + r := args.Get(1).(*dns.Msg) + resp := new(dns.Msg) + resp.SetReply(r) + assert.NoError(t, w.WriteMsg(resp)) + }).Once() + } + } + + chain.ServeDNS(w, r) + + for pattern, handler := range handlers { + if pattern == tt.expectedMatch { + handler.AssertNumberOfCalls(t, "ServeDNS", 1) + } else { + handler.AssertNumberOfCalls(t, "ServeDNS", 0) + } + } + }) + } +} From eb2ac039c771e223178713a7391453b7f63ebab5 Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Thu, 23 Jan 2025 18:00:51 +0100 Subject: [PATCH 30/92] [client] Mark redirected traffic early to match input filters on pre-DNAT ports (#3205) --- client/firewall/iptables/acl_linux.go | 48 ++++--- client/firewall/iptables/rule.go | 7 +- client/firewall/nftables/acl_linux.go | 168 +++++++++++++++---------- client/firewall/nftables/rule_linux.go | 9 +- 4 files changed, 145 insertions(+), 87 deletions(-) diff --git a/client/firewall/iptables/acl_linux.go b/client/firewall/iptables/acl_linux.go index 2592ff840..2e745a31e 100644 --- a/client/firewall/iptables/acl_linux.go +++ b/client/firewall/iptables/acl_linux.go @@ -3,6 +3,7 @@ package iptables import ( "fmt" "net" + "slices" "strconv" "github.com/coreos/go-iptables/iptables" @@ -99,6 +100,16 @@ func (m *aclManager) AddPeerFiltering( ipsetName = transformIPsetName(ipsetName, sPortVal, dPortVal) specs := filterRuleSpecs(ip, string(protocol), sPortVal, dPortVal, action, ipsetName) + + mangleSpecs := slices.Clone(specs) + mangleSpecs = append(mangleSpecs, + "-i", m.wgIface.Name(), + "-m", "addrtype", "--dst-type", "LOCAL", + "-j", "MARK", "--set-xmark", fmt.Sprintf("%#x", nbnet.PreroutingFwmarkRedirected), + ) + + specs = append(specs, "-j", actionToStr(action)) + if ipsetName != "" { if ipList, ipsetExists := m.ipsetStore.ipset(ipsetName); ipsetExists { if err := ipset.Add(ipsetName, ip.String()); err != nil { @@ -130,7 +141,7 @@ func (m *aclManager) AddPeerFiltering( m.ipsetStore.addIpList(ipsetName, ipList) } - ok, err := m.iptablesClient.Exists("filter", chain, specs...) + ok, err := m.iptablesClient.Exists(tableFilter, chain, specs...) if err != nil { return nil, fmt.Errorf("failed to check rule: %w", err) } @@ -138,16 +149,22 @@ func (m *aclManager) AddPeerFiltering( return nil, fmt.Errorf("rule already exists") } - if err := m.iptablesClient.Append("filter", chain, specs...); err != nil { + if err := m.iptablesClient.Append(tableFilter, chain, specs...); err != nil { return nil, err } + if err := m.iptablesClient.Append(tableMangle, chainRTPRE, mangleSpecs...); err != nil { + log.Errorf("failed to add mangle rule: %v", err) + mangleSpecs = nil + } + rule := &Rule{ - ruleID: uuid.New().String(), - specs: specs, - ipsetName: ipsetName, - ip: ip.String(), - chain: chain, + ruleID: uuid.New().String(), + specs: specs, + mangleSpecs: mangleSpecs, + ipsetName: ipsetName, + ip: ip.String(), + chain: chain, } m.updateState() @@ -190,6 +207,12 @@ func (m *aclManager) DeletePeerRule(rule firewall.Rule) error { return fmt.Errorf("failed to delete rule: %s, %v: %w", r.chain, r.specs, err) } + if r.mangleSpecs != nil { + if err := m.iptablesClient.Delete(tableMangle, chainRTPRE, r.mangleSpecs...); err != nil { + log.Errorf("failed to delete mangle rule: %v", err) + } + } + m.updateState() return nil @@ -310,17 +333,10 @@ func (m *aclManager) seedInitialEntries() { func (m *aclManager) seedInitialOptionalEntries() { m.optionalEntries["FORWARD"] = []entry{ { - spec: []string{"-m", "mark", "--mark", fmt.Sprintf("%#x", nbnet.PreroutingFwmarkRedirected), "-j", chainNameInputRules}, + spec: []string{"-m", "mark", "--mark", fmt.Sprintf("%#x", nbnet.PreroutingFwmarkRedirected), "-j", "ACCEPT"}, position: 2, }, } - - m.optionalEntries["PREROUTING"] = []entry{ - { - spec: []string{"-t", "mangle", "-i", m.wgIface.Name(), "-m", "addrtype", "--dst-type", "LOCAL", "-j", "MARK", "--set-mark", fmt.Sprintf("%#x", nbnet.PreroutingFwmarkRedirected)}, - position: 1, - }, - } } func (m *aclManager) appendToEntries(chainName string, spec []string) { @@ -377,7 +393,7 @@ func filterRuleSpecs(ip net.IP, protocol, sPort, dPort string, action firewall.A if dPort != "" { specs = append(specs, "--dport", dPort) } - return append(specs, "-j", actionToStr(action)) + return specs } func actionToStr(action firewall.Action) string { diff --git a/client/firewall/iptables/rule.go b/client/firewall/iptables/rule.go index 1047c5cf8..e90e32f8b 100644 --- a/client/firewall/iptables/rule.go +++ b/client/firewall/iptables/rule.go @@ -5,9 +5,10 @@ type Rule struct { ruleID string ipsetName string - specs []string - ip string - chain string + specs []string + mangleSpecs []string + ip string + chain string } // GetRuleID returns the rule id diff --git a/client/firewall/nftables/acl_linux.go b/client/firewall/nftables/acl_linux.go index 8c1d89e68..0d1d659af 100644 --- a/client/firewall/nftables/acl_linux.go +++ b/client/firewall/nftables/acl_linux.go @@ -5,6 +5,7 @@ import ( "encoding/binary" "fmt" "net" + "slices" "strconv" "strings" "time" @@ -46,6 +47,7 @@ type AclManager struct { workTable *nftables.Table chainInputRules *nftables.Chain + chainPrerouting *nftables.Chain ipsetStore *ipsetStore rules map[string]*Rule @@ -118,23 +120,32 @@ func (m *AclManager) DeletePeerRule(rule firewall.Rule) error { } if r.nftSet == nil { - err := m.rConn.DelRule(r.nftRule) - if err != nil { + if err := m.rConn.DelRule(r.nftRule); err != nil { log.Errorf("failed to delete rule: %v", err) } + if r.mangleRule != nil { + if err := m.rConn.DelRule(r.mangleRule); err != nil { + log.Errorf("failed to delete mangle rule: %v", err) + } + } delete(m.rules, r.GetRuleID()) return m.rConn.Flush() } ips, ok := m.ipsetStore.ips(r.nftSet.Name) if !ok { - err := m.rConn.DelRule(r.nftRule) - if err != nil { + if err := m.rConn.DelRule(r.nftRule); err != nil { log.Errorf("failed to delete rule: %v", err) } + if r.mangleRule != nil { + if err := m.rConn.DelRule(r.mangleRule); err != nil { + log.Errorf("failed to delete mangle rule: %v", err) + } + } delete(m.rules, r.GetRuleID()) return m.rConn.Flush() } + if _, ok := ips[r.ip.String()]; ok { err := m.sConn.SetDeleteElements(r.nftSet, []nftables.SetElement{{Key: r.ip.To4()}}) if err != nil { @@ -153,12 +164,16 @@ func (m *AclManager) DeletePeerRule(rule firewall.Rule) error { return nil } - err := m.rConn.DelRule(r.nftRule) - if err != nil { + if err := m.rConn.DelRule(r.nftRule); err != nil { log.Errorf("failed to delete rule: %v", err) } - err = m.rConn.Flush() - if err != nil { + if r.mangleRule != nil { + if err := m.rConn.DelRule(r.mangleRule); err != nil { + log.Errorf("failed to delete mangle rule: %v", err) + } + } + + if err := m.rConn.Flush(); err != nil { return err } @@ -225,9 +240,12 @@ func (m *AclManager) Flush() error { return err } - if err := m.refreshRuleHandles(m.chainInputRules); err != nil { + if err := m.refreshRuleHandles(m.chainInputRules, false); err != nil { log.Errorf("failed to refresh rule handles ipv4 input chain: %v", err) } + if err := m.refreshRuleHandles(m.chainPrerouting, true); err != nil { + log.Errorf("failed to refresh rule handles prerouting chain: %v", err) + } return nil } @@ -244,10 +262,11 @@ func (m *AclManager) addIOFiltering( ruleId := generatePeerRuleId(ip, sPort, dPort, action, ipset) if r, ok := m.rules[ruleId]; ok { return &Rule{ - r.nftRule, - r.nftSet, - r.ruleID, - ip, + nftRule: r.nftRule, + mangleRule: r.mangleRule, + nftSet: r.nftSet, + ruleID: r.ruleID, + ip: ip, }, nil } @@ -340,11 +359,13 @@ func (m *AclManager) addIOFiltering( ) } + mainExpressions := slices.Clone(expressions) + switch action { case firewall.ActionAccept: - expressions = append(expressions, &expr.Verdict{Kind: expr.VerdictAccept}) + mainExpressions = append(mainExpressions, &expr.Verdict{Kind: expr.VerdictAccept}) case firewall.ActionDrop: - expressions = append(expressions, &expr.Verdict{Kind: expr.VerdictDrop}) + mainExpressions = append(mainExpressions, &expr.Verdict{Kind: expr.VerdictDrop}) } userData := []byte(strings.Join([]string{ruleId, comment}, " ")) @@ -353,15 +374,16 @@ func (m *AclManager) addIOFiltering( nftRule := m.rConn.AddRule(&nftables.Rule{ Table: m.workTable, Chain: chain, - Exprs: expressions, + Exprs: mainExpressions, UserData: userData, }) rule := &Rule{ - nftRule: nftRule, - nftSet: ipset, - ruleID: ruleId, - ip: ip, + nftRule: nftRule, + mangleRule: m.createPreroutingRule(expressions, userData), + nftSet: ipset, + ruleID: ruleId, + ip: ip, } m.rules[ruleId] = rule if ipset != nil { @@ -370,6 +392,59 @@ func (m *AclManager) addIOFiltering( return rule, nil } +func (m *AclManager) createPreroutingRule(expressions []expr.Any, userData []byte) *nftables.Rule { + if m.chainPrerouting == nil { + log.Warn("prerouting chain is not created") + return nil + } + + preroutingExprs := slices.Clone(expressions) + + // interface + preroutingExprs = append([]expr.Any{ + &expr.Meta{ + Key: expr.MetaKeyIIFNAME, + Register: 1, + }, + &expr.Cmp{ + Op: expr.CmpOpEq, + Register: 1, + Data: ifname(m.wgIface.Name()), + }, + }, preroutingExprs...) + + // local destination and mark + preroutingExprs = append(preroutingExprs, + &expr.Fib{ + Register: 1, + ResultADDRTYPE: true, + FlagDADDR: true, + }, + &expr.Cmp{ + Op: expr.CmpOpEq, + Register: 1, + Data: binaryutil.NativeEndian.PutUint32(unix.RTN_LOCAL), + }, + + &expr.Immediate{ + Register: 1, + Data: binaryutil.NativeEndian.PutUint32(nbnet.PreroutingFwmarkRedirected), + }, + &expr.Meta{ + Key: expr.MetaKeyMARK, + Register: 1, + SourceRegister: true, + }, + ) + + return m.rConn.AddRule(&nftables.Rule{ + Table: m.workTable, + Chain: m.chainPrerouting, + Exprs: preroutingExprs, + UserData: userData, + }) +} + func (m *AclManager) createDefaultChains() (err error) { // chainNameInputRules chain := m.createChain(chainNameInputRules) @@ -413,7 +488,7 @@ func (m *AclManager) createDefaultChains() (err error) { // go through the input filter as well. This will enable e.g. Docker services to keep working by accessing the // netbird peer IP. func (m *AclManager) allowRedirectedTraffic(chainFwFilter *nftables.Chain) error { - preroutingChain := m.rConn.AddChain(&nftables.Chain{ + m.chainPrerouting = m.rConn.AddChain(&nftables.Chain{ Name: chainNamePrerouting, Table: m.workTable, Type: nftables.ChainTypeFilter, @@ -421,8 +496,6 @@ func (m *AclManager) allowRedirectedTraffic(chainFwFilter *nftables.Chain) error Priority: nftables.ChainPriorityMangle, }) - m.addPreroutingRule(preroutingChain) - m.addFwmarkToForward(chainFwFilter) if err := m.rConn.Flush(); err != nil { @@ -432,43 +505,6 @@ func (m *AclManager) allowRedirectedTraffic(chainFwFilter *nftables.Chain) error return nil } -func (m *AclManager) addPreroutingRule(preroutingChain *nftables.Chain) { - m.rConn.AddRule(&nftables.Rule{ - Table: m.workTable, - Chain: preroutingChain, - Exprs: []expr.Any{ - &expr.Meta{ - Key: expr.MetaKeyIIFNAME, - Register: 1, - }, - &expr.Cmp{ - Op: expr.CmpOpEq, - Register: 1, - Data: ifname(m.wgIface.Name()), - }, - &expr.Fib{ - Register: 1, - ResultADDRTYPE: true, - FlagDADDR: true, - }, - &expr.Cmp{ - Op: expr.CmpOpEq, - Register: 1, - Data: binaryutil.NativeEndian.PutUint32(unix.RTN_LOCAL), - }, - &expr.Immediate{ - Register: 1, - Data: binaryutil.NativeEndian.PutUint32(nbnet.PreroutingFwmarkRedirected), - }, - &expr.Meta{ - Key: expr.MetaKeyMARK, - Register: 1, - SourceRegister: true, - }, - }, - }) -} - func (m *AclManager) addFwmarkToForward(chainFwFilter *nftables.Chain) { m.rConn.InsertRule(&nftables.Rule{ Table: m.workTable, @@ -484,8 +520,7 @@ func (m *AclManager) addFwmarkToForward(chainFwFilter *nftables.Chain) { Data: binaryutil.NativeEndian.PutUint32(nbnet.PreroutingFwmarkRedirected), }, &expr.Verdict{ - Kind: expr.VerdictJump, - Chain: m.chainInputRules.Name, + Kind: expr.VerdictAccept, }, }, }) @@ -632,6 +667,7 @@ func (m *AclManager) flushWithBackoff() (err error) { for i := 0; ; i++ { err = m.rConn.Flush() if err != nil { + log.Debugf("failed to flush nftables: %v", err) if !strings.Contains(err.Error(), "busy") { return } @@ -648,7 +684,7 @@ func (m *AclManager) flushWithBackoff() (err error) { return } -func (m *AclManager) refreshRuleHandles(chain *nftables.Chain) error { +func (m *AclManager) refreshRuleHandles(chain *nftables.Chain, mangle bool) error { if m.workTable == nil || chain == nil { return nil } @@ -665,7 +701,11 @@ func (m *AclManager) refreshRuleHandles(chain *nftables.Chain) error { split := bytes.Split(rule.UserData, []byte(" ")) r, ok := m.rules[string(split[0])] if ok { - *r.nftRule = *rule + if mangle { + *r.mangleRule = *rule + } else { + *r.nftRule = *rule + } } } diff --git a/client/firewall/nftables/rule_linux.go b/client/firewall/nftables/rule_linux.go index 678c10b44..4d652346b 100644 --- a/client/firewall/nftables/rule_linux.go +++ b/client/firewall/nftables/rule_linux.go @@ -8,10 +8,11 @@ import ( // Rule to handle management of rules type Rule struct { - nftRule *nftables.Rule - nftSet *nftables.Set - ruleID string - ip net.IP + nftRule *nftables.Rule + mangleRule *nftables.Rule + nftSet *nftables.Set + ruleID string + ip net.IP } // GetRuleID returns the rule id From 2605948e015fe215640c2707e3a8a056a032ac1d Mon Sep 17 00:00:00 2001 From: Pascal Fischer <32096965+pascal-fischer@users.noreply.github.com> Date: Fri, 24 Jan 2025 12:04:50 +0100 Subject: [PATCH 31/92] [management] use account request buffer on sync (#3229) --- management/server/account_test.go | 36 ++++++++++--------- .../peers_handler_benchmark_test.go | 16 ++++----- management/server/peer.go | 2 +- 3 files changed, 29 insertions(+), 25 deletions(-) diff --git a/management/server/account_test.go b/management/server/account_test.go index 57bc0c757..1fc1ceb92 100644 --- a/management/server/account_test.go +++ b/management/server/account_test.go @@ -3005,6 +3005,8 @@ func peerShouldReceiveUpdate(t *testing.T, updateMessage <-chan *UpdateMessage) } func BenchmarkSyncAndMarkPeer(b *testing.B) { + b.Setenv("NB_GET_ACCOUNT_BUFFER_INTERVAL", "0") + benchCases := []struct { name string peers int @@ -3015,10 +3017,10 @@ func BenchmarkSyncAndMarkPeer(b *testing.B) { minMsPerOpCICD float64 maxMsPerOpCICD float64 }{ - {"Small", 50, 5, 1, 3, 3, 19}, - {"Medium", 500, 100, 7, 13, 10, 90}, - {"Large", 5000, 200, 65, 80, 60, 240}, - {"Small single", 50, 10, 1, 3, 3, 80}, + {"Small", 50, 5, 1, 5, 3, 19}, + {"Medium", 500, 100, 7, 22, 10, 90}, + {"Large", 5000, 200, 65, 110, 60, 240}, + {"Small single", 50, 10, 1, 4, 3, 80}, {"Medium single", 500, 10, 7, 13, 10, 37}, {"Large 5", 5000, 15, 65, 80, 60, 220}, } @@ -3072,6 +3074,7 @@ func BenchmarkSyncAndMarkPeer(b *testing.B) { } func BenchmarkLoginPeer_ExistingPeer(b *testing.B) { + b.Setenv("NB_GET_ACCOUNT_BUFFER_INTERVAL", "0") benchCases := []struct { name string peers int @@ -3082,12 +3085,12 @@ func BenchmarkLoginPeer_ExistingPeer(b *testing.B) { minMsPerOpCICD float64 maxMsPerOpCICD float64 }{ - {"Small", 50, 5, 102, 110, 3, 20}, - {"Medium", 500, 100, 105, 140, 20, 110}, - {"Large", 5000, 200, 160, 200, 120, 260}, - {"Small single", 50, 10, 102, 110, 5, 40}, - {"Medium single", 500, 10, 105, 140, 10, 60}, - {"Large 5", 5000, 15, 160, 200, 60, 180}, + {"Small", 50, 5, 2, 10, 3, 35}, + {"Medium", 500, 100, 5, 40, 20, 110}, + {"Large", 5000, 200, 60, 100, 120, 260}, + {"Small single", 50, 10, 2, 10, 5, 40}, + {"Medium single", 500, 10, 5, 40, 10, 60}, + {"Large 5", 5000, 15, 60, 100, 60, 180}, } log.SetOutput(io.Discard) @@ -3146,6 +3149,7 @@ func BenchmarkLoginPeer_ExistingPeer(b *testing.B) { } func BenchmarkLoginPeer_NewPeer(b *testing.B) { + b.Setenv("NB_GET_ACCOUNT_BUFFER_INTERVAL", "0") benchCases := []struct { name string peers int @@ -3156,12 +3160,12 @@ func BenchmarkLoginPeer_NewPeer(b *testing.B) { minMsPerOpCICD float64 maxMsPerOpCICD float64 }{ - {"Small", 50, 5, 107, 120, 10, 80}, - {"Medium", 500, 100, 105, 140, 30, 140}, - {"Large", 5000, 200, 180, 220, 140, 300}, - {"Small single", 50, 10, 107, 120, 10, 80}, - {"Medium single", 500, 10, 105, 140, 20, 60}, - {"Large 5", 5000, 15, 180, 220, 80, 200}, + {"Small", 50, 5, 7, 20, 10, 80}, + {"Medium", 500, 100, 5, 40, 30, 140}, + {"Large", 5000, 200, 80, 120, 140, 300}, + {"Small single", 50, 10, 7, 20, 10, 80}, + {"Medium single", 500, 10, 5, 40, 20, 60}, + {"Large 5", 5000, 15, 80, 120, 80, 200}, } log.SetOutput(io.Discard) diff --git a/management/server/http/testing/benchmarks/peers_handler_benchmark_test.go b/management/server/http/testing/benchmarks/peers_handler_benchmark_test.go index 23b4edefb..7f8eee6e7 100644 --- a/management/server/http/testing/benchmarks/peers_handler_benchmark_test.go +++ b/management/server/http/testing/benchmarks/peers_handler_benchmark_test.go @@ -145,14 +145,14 @@ func BenchmarkGetAllPeers(b *testing.B) { func BenchmarkDeletePeer(b *testing.B) { var expectedMetrics = map[string]testing_tools.PerformanceMetrics{ - "Peers - XS": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 4, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 16}, - "Peers - S": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 4, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 16}, - "Peers - M": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 4, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 16}, - "Peers - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 4, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 16}, - "Groups - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 4, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 16}, - "Users - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 4, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 16}, - "Setup Keys - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 4, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 16}, - "Peers - XL": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 4, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 16}, + "Peers - XS": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 4, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 18}, + "Peers - S": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 4, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 18}, + "Peers - M": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 4, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 18}, + "Peers - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 4, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 18}, + "Groups - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 4, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 18}, + "Users - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 4, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 18}, + "Setup Keys - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 4, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 18}, + "Peers - XL": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 4, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 18}, } log.SetOutput(io.Discard) diff --git a/management/server/peer.go b/management/server/peer.go index e5442acea..0eafb35e0 100644 --- a/management/server/peer.go +++ b/management/server/peer.go @@ -989,7 +989,7 @@ func (am *DefaultAccountManager) getValidatedPeerWithMap(ctx context.Context, is return peer, emptyMap, nil, nil } - account, err := am.Store.GetAccount(ctx, accountID) + account, err := am.requestBuffer.GetAccountWithBackpressure(ctx, accountID) if err != nil { return nil, nil, nil, err } From b6abd4b4da2af7d32c5b76c0211836477107383d Mon Sep 17 00:00:00 2001 From: Pascal Fischer <32096965+pascal-fischer@users.noreply.github.com> Date: Fri, 24 Jan 2025 14:17:30 +0100 Subject: [PATCH 32/92] [management/signal/relay] add metrics descriptions (#3233) --- management/server/peer.go | 8 ++- .../telemetry/accountmanager_metrics.go | 13 +++-- management/server/telemetry/grpc_metrics.go | 32 +++++++++--- .../server/telemetry/http_api_metrics.go | 35 ++++++++++--- management/server/telemetry/idp_metrics.go | 50 +++++++++++++++---- management/server/telemetry/store_metrics.go | 23 +++++++-- .../server/telemetry/updatechannel_metrics.go | 42 +++++++++++++--- management/server/types/account.go | 6 +-- relay/metrics/realy.go | 30 ++++++++--- signal/metrics/app.go | 40 +++++++++++---- 10 files changed, 213 insertions(+), 66 deletions(-) diff --git a/management/server/peer.go b/management/server/peer.go index 0eafb35e0..efd9c64e3 100644 --- a/management/server/peer.go +++ b/management/server/peer.go @@ -1130,11 +1130,6 @@ func (am *DefaultAccountManager) UpdateAccountPeers(ctx context.Context, account } start := time.Now() - defer func() { - if am.metrics != nil { - am.metrics.AccountManagerMetrics().CountUpdateAccountPeersDuration(time.Since(start)) - } - }() approvedPeersMap, err := am.integratedPeerValidator.GetValidatedPeers(account.Id, maps.Values(account.Groups), maps.Values(account.Peers), account.Settings.Extra) if err != nil { @@ -1175,6 +1170,9 @@ func (am *DefaultAccountManager) UpdateAccountPeers(ctx context.Context, account } wg.Wait() + if am.metrics != nil { + am.metrics.AccountManagerMetrics().CountUpdateAccountPeersDuration(time.Since(start)) + } } // UpdateAccountPeer updates a single peer that belongs to an account. diff --git a/management/server/telemetry/accountmanager_metrics.go b/management/server/telemetry/accountmanager_metrics.go index 4a5a31e2d..3b1e078eb 100644 --- a/management/server/telemetry/accountmanager_metrics.go +++ b/management/server/telemetry/accountmanager_metrics.go @@ -22,7 +22,8 @@ func NewAccountManagerMetrics(ctx context.Context, meter metric.Meter) (*Account metric.WithUnit("milliseconds"), metric.WithExplicitBucketBoundaries( 0.5, 1, 2.5, 5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000, 30000, - )) + ), + metric.WithDescription("Duration of triggering the account peers update and preparing the required data for the network map being sent to the clients")) if err != nil { return nil, err } @@ -31,7 +32,8 @@ func NewAccountManagerMetrics(ctx context.Context, meter metric.Meter) (*Account metric.WithUnit("milliseconds"), metric.WithExplicitBucketBoundaries( 0.1, 0.5, 1, 2.5, 5, 10, 25, 50, 100, 250, 500, 1000, - )) + ), + metric.WithDescription("Duration of calculating the peer network map that is sent to the clients")) if err != nil { return nil, err } @@ -40,12 +42,15 @@ func NewAccountManagerMetrics(ctx context.Context, meter metric.Meter) (*Account metric.WithUnit("objects"), metric.WithExplicitBucketBoundaries( 50, 100, 200, 500, 1000, 2500, 5000, 10000, - )) + ), + metric.WithDescription("Number of objects in the network map like peers, routes, firewall rules, etc. that are sent to the clients")) if err != nil { return nil, err } - peerMetaUpdateCount, err := meter.Int64Counter("management.account.peer.meta.update.counter", metric.WithUnit("1")) + peerMetaUpdateCount, err := meter.Int64Counter("management.account.peer.meta.update.counter", + metric.WithUnit("1"), + metric.WithDescription("Number of updates with new meta data from the peers")) if err != nil { return nil, err } diff --git a/management/server/telemetry/grpc_metrics.go b/management/server/telemetry/grpc_metrics.go index acbe1281c..ac6ff2ea8 100644 --- a/management/server/telemetry/grpc_metrics.go +++ b/management/server/telemetry/grpc_metrics.go @@ -22,32 +22,50 @@ type GRPCMetrics struct { // NewGRPCMetrics creates new GRPCMetrics struct and registers common metrics of the gRPC server func NewGRPCMetrics(ctx context.Context, meter metric.Meter) (*GRPCMetrics, error) { - syncRequestsCounter, err := meter.Int64Counter("management.grpc.sync.request.counter", metric.WithUnit("1")) + syncRequestsCounter, err := meter.Int64Counter("management.grpc.sync.request.counter", + metric.WithUnit("1"), + metric.WithDescription("Number of sync gRPC requests from the peers to establish a connection and receive network map updates (update channel)"), + ) if err != nil { return nil, err } - loginRequestsCounter, err := meter.Int64Counter("management.grpc.login.request.counter", metric.WithUnit("1")) + loginRequestsCounter, err := meter.Int64Counter("management.grpc.login.request.counter", + metric.WithUnit("1"), + metric.WithDescription("Number of login gRPC requests from the peers to authenticate and receive initial configuration and relay credentials"), + ) if err != nil { return nil, err } - getKeyRequestsCounter, err := meter.Int64Counter("management.grpc.key.request.counter", metric.WithUnit("1")) + getKeyRequestsCounter, err := meter.Int64Counter("management.grpc.key.request.counter", + metric.WithUnit("1"), + metric.WithDescription("Number of key gRPC requests from the peers to get the server's public WireGuard key"), + ) if err != nil { return nil, err } - activeStreamsGauge, err := meter.Int64ObservableGauge("management.grpc.connected.streams", metric.WithUnit("1")) + activeStreamsGauge, err := meter.Int64ObservableGauge("management.grpc.connected.streams", + metric.WithUnit("1"), + metric.WithDescription("Number of active peer streams connected to the gRPC server"), + ) if err != nil { return nil, err } - syncRequestDuration, err := meter.Int64Histogram("management.grpc.sync.request.duration.ms", metric.WithUnit("milliseconds")) + syncRequestDuration, err := meter.Int64Histogram("management.grpc.sync.request.duration.ms", + metric.WithUnit("milliseconds"), + metric.WithDescription("Duration of the sync gRPC requests from the peers to establish a connection and receive network map updates (update channel)"), + ) if err != nil { return nil, err } - loginRequestDuration, err := meter.Int64Histogram("management.grpc.login.request.duration.ms", metric.WithUnit("milliseconds")) + loginRequestDuration, err := meter.Int64Histogram("management.grpc.login.request.duration.ms", + metric.WithUnit("milliseconds"), + metric.WithDescription("Duration of the login gRPC requests from the peers to authenticate and receive initial configuration and relay credentials"), + ) if err != nil { return nil, err } @@ -57,7 +75,7 @@ func NewGRPCMetrics(ctx context.Context, meter metric.Meter) (*GRPCMetrics, erro // TODO(yury): This needs custom bucketing as we are interested in the values from 0 to server.channelBufferSize (100) channelQueue, err := meter.Int64Histogram( "management.grpc.updatechannel.queue", - metric.WithDescription("Number of update messages in the channel queue"), + metric.WithDescription("Number of update messages piling up in the update channel queue"), metric.WithUnit("length"), ) if err != nil { diff --git a/management/server/telemetry/http_api_metrics.go b/management/server/telemetry/http_api_metrics.go index 357f019c7..5ef9e6d02 100644 --- a/management/server/telemetry/http_api_metrics.go +++ b/management/server/telemetry/http_api_metrics.go @@ -74,37 +74,58 @@ type HTTPMiddleware struct { // NewMetricsMiddleware creates a new HTTPMiddleware func NewMetricsMiddleware(ctx context.Context, meter metric.Meter) (*HTTPMiddleware, error) { - httpRequestCounter, err := meter.Int64Counter(httpRequestCounterPrefix, metric.WithUnit("1")) + httpRequestCounter, err := meter.Int64Counter(httpRequestCounterPrefix, + metric.WithUnit("1"), + metric.WithDescription("Number of incoming HTTP requests by endpoint and method"), + ) if err != nil { return nil, err } - httpResponseCounter, err := meter.Int64Counter(httpResponseCounterPrefix, metric.WithUnit("1")) + httpResponseCounter, err := meter.Int64Counter(httpResponseCounterPrefix, + metric.WithUnit("1"), + metric.WithDescription("Number of outgoing HTTP responses by endpoint, method and returned status code"), + ) if err != nil { return nil, err } - totalHTTPRequestsCounter, err := meter.Int64Counter(fmt.Sprintf("%s.total", httpRequestCounterPrefix), metric.WithUnit("1")) + totalHTTPRequestsCounter, err := meter.Int64Counter(fmt.Sprintf("%s.total", httpRequestCounterPrefix), + metric.WithUnit("1"), + metric.WithDescription("Number of incoming HTTP requests"), + ) if err != nil { return nil, err } - totalHTTPResponseCounter, err := meter.Int64Counter(fmt.Sprintf("%s.total", httpResponseCounterPrefix), metric.WithUnit("1")) + totalHTTPResponseCounter, err := meter.Int64Counter(fmt.Sprintf("%s.total", httpResponseCounterPrefix), + metric.WithUnit("1"), + metric.WithDescription("Number of outgoing HTTP responses"), + ) if err != nil { return nil, err } - totalHTTPResponseCodeCounter, err := meter.Int64Counter(fmt.Sprintf("%s.code.total", httpResponseCounterPrefix), metric.WithUnit("1")) + totalHTTPResponseCodeCounter, err := meter.Int64Counter(fmt.Sprintf("%s.code.total", httpResponseCounterPrefix), + metric.WithUnit("1"), + metric.WithDescription("Number of outgoing HTTP responses by status code"), + ) if err != nil { return nil, err } - httpRequestDuration, err := meter.Int64Histogram(httpRequestDurationPrefix, metric.WithUnit("milliseconds")) + httpRequestDuration, err := meter.Int64Histogram(httpRequestDurationPrefix, + metric.WithUnit("milliseconds"), + metric.WithDescription("Duration of incoming HTTP requests by endpoint and method"), + ) if err != nil { return nil, err } - totalHTTPRequestDuration, err := meter.Int64Histogram(fmt.Sprintf("%s.total", httpRequestDurationPrefix), metric.WithUnit("milliseconds")) + totalHTTPRequestDuration, err := meter.Int64Histogram(fmt.Sprintf("%s.total", httpRequestDurationPrefix), + metric.WithUnit("milliseconds"), + metric.WithDescription("Duration of incoming HTTP requests"), + ) if err != nil { return nil, err } diff --git a/management/server/telemetry/idp_metrics.go b/management/server/telemetry/idp_metrics.go index 0bcd5d432..5337c91c2 100644 --- a/management/server/telemetry/idp_metrics.go +++ b/management/server/telemetry/idp_metrics.go @@ -23,43 +23,73 @@ type IDPMetrics struct { // NewIDPMetrics creates new IDPMetrics struct and registers common func NewIDPMetrics(ctx context.Context, meter metric.Meter) (*IDPMetrics, error) { - metaUpdateCounter, err := meter.Int64Counter("management.idp.update.user.meta.counter", metric.WithUnit("1")) + metaUpdateCounter, err := meter.Int64Counter("management.idp.update.user.meta.counter", + metric.WithUnit("1"), + metric.WithDescription("Number of updates of user metadata sent to the configured identity provider"), + ) if err != nil { return nil, err } - getUserByEmailCounter, err := meter.Int64Counter("management.idp.get.user.by.email.counter", metric.WithUnit("1")) + getUserByEmailCounter, err := meter.Int64Counter("management.idp.get.user.by.email.counter", + metric.WithUnit("1"), + metric.WithDescription("Number of requests to get a user by email from the configured identity provider"), + ) if err != nil { return nil, err } - getAllAccountsCounter, err := meter.Int64Counter("management.idp.get.accounts.counter", metric.WithUnit("1")) + getAllAccountsCounter, err := meter.Int64Counter("management.idp.get.accounts.counter", + metric.WithUnit("1"), + metric.WithDescription("Number of requests to get all accounts from the configured identity provider"), + ) if err != nil { return nil, err } - createUserCounter, err := meter.Int64Counter("management.idp.create.user.counter", metric.WithUnit("1")) + createUserCounter, err := meter.Int64Counter("management.idp.create.user.counter", + metric.WithUnit("1"), + metric.WithDescription("Number of requests to create a new user in the configured identity provider"), + ) if err != nil { return nil, err } - deleteUserCounter, err := meter.Int64Counter("management.idp.delete.user.counter", metric.WithUnit("1")) + deleteUserCounter, err := meter.Int64Counter("management.idp.delete.user.counter", + metric.WithUnit("1"), + metric.WithDescription("Number of requests to delete a user from the configured identity provider"), + ) if err != nil { return nil, err } - getAccountCounter, err := meter.Int64Counter("management.idp.get.account.counter", metric.WithUnit("1")) + getAccountCounter, err := meter.Int64Counter("management.idp.get.account.counter", + metric.WithUnit("1"), + metric.WithDescription("Number of requests to get all users in an account from the configured identity provider"), + ) if err != nil { return nil, err } - getUserByIDCounter, err := meter.Int64Counter("management.idp.get.user.by.id.counter", metric.WithUnit("1")) + getUserByIDCounter, err := meter.Int64Counter("management.idp.get.user.by.id.counter", + metric.WithUnit("1"), + metric.WithDescription("Number of requests to get a user by ID from the configured identity provider"), + ) if err != nil { return nil, err } - authenticateRequestCounter, err := meter.Int64Counter("management.idp.authenticate.request.counter", metric.WithUnit("1")) + authenticateRequestCounter, err := meter.Int64Counter("management.idp.authenticate.request.counter", + metric.WithUnit("1"), + metric.WithDescription("Number of requests to authenticate the server with the configured identity provider"), + ) if err != nil { return nil, err } - requestErrorCounter, err := meter.Int64Counter("management.idp.request.error.counter", metric.WithUnit("1")) + requestErrorCounter, err := meter.Int64Counter("management.idp.request.error.counter", + metric.WithUnit("1"), + metric.WithDescription("Number of errors that happened when doing http request to the configured identity provider"), + ) if err != nil { return nil, err } - requestStatusErrorCounter, err := meter.Int64Counter("management.idp.request.status.error.counter", metric.WithUnit("1")) + requestStatusErrorCounter, err := meter.Int64Counter("management.idp.request.status.error.counter", + metric.WithUnit("1"), + metric.WithDescription("Number of responses that came from the configured identity provider with non success status code"), + ) if err != nil { return nil, err } diff --git a/management/server/telemetry/store_metrics.go b/management/server/telemetry/store_metrics.go index bb3745b5a..f035ce847 100644 --- a/management/server/telemetry/store_metrics.go +++ b/management/server/telemetry/store_metrics.go @@ -20,28 +20,41 @@ type StoreMetrics struct { // NewStoreMetrics creates an instance of StoreMetrics func NewStoreMetrics(ctx context.Context, meter metric.Meter) (*StoreMetrics, error) { globalLockAcquisitionDurationMicro, err := meter.Int64Histogram("management.store.global.lock.acquisition.duration.micro", - metric.WithUnit("microseconds")) + metric.WithUnit("microseconds"), + metric.WithDescription("Duration of how long it takes to acquire the global lock in the store to block all other requests to the store"), + ) if err != nil { return nil, err } - globalLockAcquisitionDurationMs, err := meter.Int64Histogram("management.store.global.lock.acquisition.duration.ms") + globalLockAcquisitionDurationMs, err := meter.Int64Histogram("management.store.global.lock.acquisition.duration.ms", + metric.WithUnit("milliseconds"), + metric.WithDescription("Duration of how long a process holds the acquired global lock in the store"), + ) if err != nil { return nil, err } persistenceDurationMicro, err := meter.Int64Histogram("management.store.persistence.duration.micro", - metric.WithUnit("microseconds")) + metric.WithUnit("microseconds"), + metric.WithDescription("Duration of how long it takes to save or delete an account in the store"), + ) if err != nil { return nil, err } - persistenceDurationMs, err := meter.Int64Histogram("management.store.persistence.duration.ms") + persistenceDurationMs, err := meter.Int64Histogram("management.store.persistence.duration.ms", + metric.WithUnit("milliseconds"), + metric.WithDescription("Duration of how long it takes to save or delete an account in the store"), + ) if err != nil { return nil, err } - transactionDurationMs, err := meter.Int64Histogram("management.store.transaction.duration.ms") + transactionDurationMs, err := meter.Int64Histogram("management.store.transaction.duration.ms", + metric.WithUnit("milliseconds"), + metric.WithDescription("Duration of how long it takes to execute a transaction in the store"), + ) if err != nil { return nil, err } diff --git a/management/server/telemetry/updatechannel_metrics.go b/management/server/telemetry/updatechannel_metrics.go index 2582006e5..584b9ec20 100644 --- a/management/server/telemetry/updatechannel_metrics.go +++ b/management/server/telemetry/updatechannel_metrics.go @@ -23,42 +23,68 @@ type UpdateChannelMetrics struct { // NewUpdateChannelMetrics creates an instance of UpdateChannel func NewUpdateChannelMetrics(ctx context.Context, meter metric.Meter) (*UpdateChannelMetrics, error) { - createChannelDurationMicro, err := meter.Int64Histogram("management.updatechannel.create.duration.micro") + createChannelDurationMicro, err := meter.Int64Histogram("management.updatechannel.create.duration.micro", + metric.WithUnit("microseconds"), + metric.WithDescription("Duration of how long it takes to create a new peer update channel"), + ) if err != nil { return nil, err } - closeChannelDurationMicro, err := meter.Int64Histogram("management.updatechannel.close.one.duration.micro") + closeChannelDurationMicro, err := meter.Int64Histogram("management.updatechannel.close.one.duration.micro", + metric.WithUnit("microseconds"), + metric.WithDescription("Duration of how long it takes to close a peer update channel"), + ) if err != nil { return nil, err } - closeChannelsDurationMicro, err := meter.Int64Histogram("management.updatechannel.close.multiple.duration.micro") + closeChannelsDurationMicro, err := meter.Int64Histogram("management.updatechannel.close.multiple.duration.micro", + metric.WithUnit("microseconds"), + metric.WithDescription("Duration of how long it takes to close a set of peer update channels"), + ) + if err != nil { return nil, err } - closeChannels, err := meter.Int64Histogram("management.updatechannel.close.multiple.channels") + closeChannels, err := meter.Int64Histogram("management.updatechannel.close.multiple.channels", + metric.WithUnit("1"), + metric.WithDescription("Number of peer update channels that have been closed"), + ) + if err != nil { return nil, err } - sendUpdateDurationMicro, err := meter.Int64Histogram("management.updatechannel.send.duration.micro") + sendUpdateDurationMicro, err := meter.Int64Histogram("management.updatechannel.send.duration.micro", + metric.WithUnit("microseconds"), + metric.WithDescription("Duration of how long it takes to send an network map update to a peer"), + ) if err != nil { return nil, err } - getAllConnectedPeersDurationMicro, err := meter.Int64Histogram("management.updatechannel.get.all.duration.micro") + getAllConnectedPeersDurationMicro, err := meter.Int64Histogram("management.updatechannel.get.all.duration.micro", + metric.WithUnit("microseconds"), + metric.WithDescription("Duration of how long it takes to get all connected peers"), + ) if err != nil { return nil, err } - getAllConnectedPeers, err := meter.Int64Histogram("management.updatechannel.get.all.peers") + getAllConnectedPeers, err := meter.Int64Histogram("management.updatechannel.get.all.peers", + metric.WithUnit("1"), + metric.WithDescription("Number of connected peers"), + ) if err != nil { return nil, err } - hasChannelDurationMicro, err := meter.Int64Histogram("management.updatechannel.haschannel.duration.micro") + hasChannelDurationMicro, err := meter.Int64Histogram("management.updatechannel.haschannel.duration.micro", + metric.WithUnit("microseconds"), + metric.WithDescription("Duration of how long it takes to check if a peer has a channel"), + ) if err != nil { return nil, err } diff --git a/management/server/types/account.go b/management/server/types/account.go index f74d38cb6..0df15816f 100644 --- a/management/server/types/account.go +++ b/management/server/types/account.go @@ -289,14 +289,14 @@ func (a *Account) GetPeerNetworkMap( } if metrics != nil { - objectCount := int64(len(peersToConnect) + len(expiredPeers) + len(routesUpdate) + len(firewallRules)) + objectCount := int64(len(peersToConnectIncludingRouters) + len(expiredPeers) + len(routesUpdate) + len(networkResourcesRoutes) + len(firewallRules) + +len(networkResourcesFirewallRules) + len(routesFirewallRules)) metrics.CountNetworkMapObjects(objectCount) metrics.CountGetPeerNetworkMapDuration(time.Since(start)) if objectCount > 5000 { log.WithContext(ctx).Tracef("account: %s has a total resource count of %d objects, "+ - "peers to connect: %d, expired peers: %d, routes: %d, firewall rules: %d", - a.Id, objectCount, len(peersToConnect), len(expiredPeers), len(routesUpdate), len(firewallRules)) + "peers to connect: %d, expired peers: %d, routes: %d, firewall rules: %d, network resources routes: %d, network resources firewall rules: %d, routes firewall rules: %d", + a.Id, objectCount, len(peersToConnectIncludingRouters), len(expiredPeers), len(routesUpdate), len(firewallRules), len(networkResourcesRoutes), len(networkResourcesFirewallRules), len(routesFirewallRules)) } } diff --git a/relay/metrics/realy.go b/relay/metrics/realy.go index 4dc98a0e0..2e90940e6 100644 --- a/relay/metrics/realy.go +++ b/relay/metrics/realy.go @@ -29,37 +29,53 @@ type Metrics struct { } func NewMetrics(ctx context.Context, meter metric.Meter) (*Metrics, error) { - bytesSent, err := meter.Int64Counter("relay_transfer_sent_bytes_total") + bytesSent, err := meter.Int64Counter("relay_transfer_sent_bytes_total", + metric.WithDescription("Total number of bytes sent to peers"), + ) if err != nil { return nil, err } - bytesRecv, err := meter.Int64Counter("relay_transfer_received_bytes_total") + bytesRecv, err := meter.Int64Counter("relay_transfer_received_bytes_total", + metric.WithDescription("Total number of bytes received from peers"), + ) if err != nil { return nil, err } - peers, err := meter.Int64UpDownCounter("relay_peers") + peers, err := meter.Int64UpDownCounter("relay_peers", + metric.WithDescription("Number of connected peers"), + ) if err != nil { return nil, err } - peersActive, err := meter.Int64ObservableGauge("relay_peers_active") + peersActive, err := meter.Int64ObservableGauge("relay_peers_active", + metric.WithDescription("Number of active connected peers"), + ) if err != nil { return nil, err } - peersIdle, err := meter.Int64ObservableGauge("relay_peers_idle") + peersIdle, err := meter.Int64ObservableGauge("relay_peers_idle", + metric.WithDescription("Number of idle connected peers"), + ) if err != nil { return nil, err } - authTime, err := meter.Float64Histogram("relay_peer_authentication_time_milliseconds", metric.WithExplicitBucketBoundaries(getStandardBucketBoundaries()...)) + authTime, err := meter.Float64Histogram("relay_peer_authentication_time_milliseconds", + metric.WithExplicitBucketBoundaries(getStandardBucketBoundaries()...), + metric.WithDescription("Time taken to authenticate a peer"), + ) if err != nil { return nil, err } - peerStoreTime, err := meter.Float64Histogram("relay_peer_store_time_milliseconds", metric.WithExplicitBucketBoundaries(getStandardBucketBoundaries()...)) + peerStoreTime, err := meter.Float64Histogram("relay_peer_store_time_milliseconds", + metric.WithExplicitBucketBoundaries(getStandardBucketBoundaries()...), + metric.WithDescription("Time taken to store a new peer connection"), + ) if err != nil { return nil, err } diff --git a/signal/metrics/app.go b/signal/metrics/app.go index f8be88be7..b3457cf96 100644 --- a/signal/metrics/app.go +++ b/signal/metrics/app.go @@ -23,56 +23,76 @@ type AppMetrics struct { } func NewAppMetrics(meter metric.Meter) (*AppMetrics, error) { - activePeers, err := meter.Int64UpDownCounter("active_peers") + activePeers, err := meter.Int64UpDownCounter("active_peers", + metric.WithDescription("Number of active connected peers"), + ) if err != nil { return nil, err } peerConnectionDuration, err := meter.Int64Histogram("peer_connection_duration_seconds", - metric.WithExplicitBucketBoundaries(getPeerConnectionDurationBucketBoundaries()...)) + metric.WithExplicitBucketBoundaries(getPeerConnectionDurationBucketBoundaries()...), + metric.WithDescription("Duration of how long a peer was connected"), + ) if err != nil { return nil, err } - registrations, err := meter.Int64Counter("registrations_total") + registrations, err := meter.Int64Counter("registrations_total", + metric.WithDescription("Total number of peer registrations"), + ) if err != nil { return nil, err } - deregistrations, err := meter.Int64Counter("deregistrations_total") + deregistrations, err := meter.Int64Counter("deregistrations_total", + metric.WithDescription("Total number of peer deregistrations"), + ) if err != nil { return nil, err } - registrationFailures, err := meter.Int64Counter("registration_failures_total") + registrationFailures, err := meter.Int64Counter("registration_failures_total", + metric.WithDescription("Total number of peer registration failures"), + ) if err != nil { return nil, err } registrationDelay, err := meter.Float64Histogram("registration_delay_milliseconds", - metric.WithExplicitBucketBoundaries(getStandardBucketBoundaries()...)) + metric.WithExplicitBucketBoundaries(getStandardBucketBoundaries()...), + metric.WithDescription("Duration of how long it takes to register a peer"), + ) if err != nil { return nil, err } getRegistrationDelay, err := meter.Float64Histogram("get_registration_delay_milliseconds", - metric.WithExplicitBucketBoundaries(getStandardBucketBoundaries()...)) + metric.WithExplicitBucketBoundaries(getStandardBucketBoundaries()...), + metric.WithDescription("Duration of how long it takes to load a connection from the registry"), + ) if err != nil { return nil, err } - messagesForwarded, err := meter.Int64Counter("messages_forwarded_total") + messagesForwarded, err := meter.Int64Counter("messages_forwarded_total", + metric.WithDescription("Total number of messages forwarded to peers"), + ) if err != nil { return nil, err } - messageForwardFailures, err := meter.Int64Counter("message_forward_failures_total") + messageForwardFailures, err := meter.Int64Counter("message_forward_failures_total", + metric.WithDescription("Total number of message forwarding failures"), + ) if err != nil { return nil, err } messageForwardLatency, err := meter.Float64Histogram("message_forward_latency_milliseconds", - metric.WithExplicitBucketBoundaries(getStandardBucketBoundaries()...)) + metric.WithExplicitBucketBoundaries(getStandardBucketBoundaries()...), + metric.WithDescription("Duration of how long it takes to forward a message to a peer"), + ) if err != nil { return nil, err } From 5c05131a948acb885af45d00123423f2934313f6 Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Mon, 27 Jan 2025 13:51:57 +0100 Subject: [PATCH 33/92] [client] Support port ranges in peer ACLs (#3232) --- client/firewall/iptables/acl_linux.go | 33 +-- .../firewall/iptables/manager_linux_test.go | 9 +- client/firewall/iptables/router_linux.go | 4 +- client/firewall/iptables/router_linux_test.go | 12 +- client/firewall/manager/port.go | 8 +- client/firewall/nftables/acl_linux.go | 40 +--- .../firewall/nftables/manager_linux_test.go | 8 +- client/firewall/nftables/router_linux.go | 6 +- client/firewall/nftables/router_linux_test.go | 12 +- client/firewall/uspfilter/rule.go | 6 +- client/firewall/uspfilter/uspfilter.go | 46 ++-- .../uspfilter/uspfilter_bench_test.go | 12 +- client/firewall/uspfilter/uspfilter_test.go | 12 +- client/internal/acl/manager.go | 13 +- client/internal/dnsfwd/manager.go | 2 +- client/internal/engine.go | 2 +- management/proto/management.pb.go | 214 +++++++++--------- management/proto/management.proto | 1 + 18 files changed, 206 insertions(+), 234 deletions(-) diff --git a/client/firewall/iptables/acl_linux.go b/client/firewall/iptables/acl_linux.go index 2e745a31e..6c4895e05 100644 --- a/client/firewall/iptables/acl_linux.go +++ b/client/firewall/iptables/acl_linux.go @@ -4,7 +4,6 @@ import ( "fmt" "net" "slices" - "strconv" "github.com/coreos/go-iptables/iptables" "github.com/google/uuid" @@ -87,19 +86,10 @@ func (m *aclManager) AddPeerFiltering( action firewall.Action, ipsetName string, ) ([]firewall.Rule, error) { - var dPortVal, sPortVal string - if dPort != nil && dPort.Values != nil { - // TODO: we support only one port per rule in current implementation of ACLs - dPortVal = strconv.Itoa(dPort.Values[0]) - } - if sPort != nil && sPort.Values != nil { - sPortVal = strconv.Itoa(sPort.Values[0]) - } - chain := chainNameInputRules - ipsetName = transformIPsetName(ipsetName, sPortVal, dPortVal) - specs := filterRuleSpecs(ip, string(protocol), sPortVal, dPortVal, action, ipsetName) + ipsetName = transformIPsetName(ipsetName, sPort, dPort) + specs := filterRuleSpecs(ip, string(protocol), sPort, dPort, action, ipsetName) mangleSpecs := slices.Clone(specs) mangleSpecs = append(mangleSpecs, @@ -109,7 +99,6 @@ func (m *aclManager) AddPeerFiltering( ) specs = append(specs, "-j", actionToStr(action)) - if ipsetName != "" { if ipList, ipsetExists := m.ipsetStore.ipset(ipsetName); ipsetExists { if err := ipset.Add(ipsetName, ip.String()); err != nil { @@ -370,7 +359,7 @@ func (m *aclManager) updateState() { } // filterRuleSpecs returns the specs of a filtering rule -func filterRuleSpecs(ip net.IP, protocol, sPort, dPort string, action firewall.Action, ipsetName string) (specs []string) { +func filterRuleSpecs(ip net.IP, protocol string, sPort, dPort *firewall.Port, action firewall.Action, ipsetName string) (specs []string) { matchByIP := true // don't use IP matching if IP is ip 0.0.0.0 if ip.String() == "0.0.0.0" { @@ -387,12 +376,8 @@ func filterRuleSpecs(ip net.IP, protocol, sPort, dPort string, action firewall.A if protocol != "all" { specs = append(specs, "-p", protocol) } - if sPort != "" { - specs = append(specs, "--sport", sPort) - } - if dPort != "" { - specs = append(specs, "--dport", dPort) - } + specs = append(specs, applyPort("--sport", sPort)...) + specs = append(specs, applyPort("--dport", dPort)...) return specs } @@ -403,15 +388,15 @@ func actionToStr(action firewall.Action) string { return "DROP" } -func transformIPsetName(ipsetName string, sPort, dPort string) string { +func transformIPsetName(ipsetName string, sPort, dPort *firewall.Port) string { switch { case ipsetName == "": return "" - case sPort != "" && dPort != "": + case sPort != nil && dPort != nil: return ipsetName + "-sport-dport" - case sPort != "": + case sPort != nil: return ipsetName + "-sport" - case dPort != "": + case dPort != nil: return ipsetName + "-dport" default: return ipsetName diff --git a/client/firewall/iptables/manager_linux_test.go b/client/firewall/iptables/manager_linux_test.go index fe0bc86de..ba578c033 100644 --- a/client/firewall/iptables/manager_linux_test.go +++ b/client/firewall/iptables/manager_linux_test.go @@ -72,7 +72,8 @@ func TestIptablesManager(t *testing.T) { t.Run("add second rule", func(t *testing.T) { ip := net.ParseIP("10.20.0.3") port := &fw.Port{ - Values: []int{8043: 8046}, + IsRange: true, + Values: []uint16{8043, 8046}, } rule2, err = manager.AddPeerFiltering(ip, "tcp", port, nil, fw.ActionAccept, "", "accept HTTPS traffic from ports range") require.NoError(t, err, "failed to add rule") @@ -95,7 +96,7 @@ func TestIptablesManager(t *testing.T) { t.Run("reset check", func(t *testing.T) { // add second rule ip := net.ParseIP("10.20.0.3") - port := &fw.Port{Values: []int{5353}} + port := &fw.Port{Values: []uint16{5353}} _, err = manager.AddPeerFiltering(ip, "udp", nil, port, fw.ActionAccept, "", "accept Fake DNS traffic") require.NoError(t, err, "failed to add rule") @@ -145,7 +146,7 @@ func TestIptablesManagerIPSet(t *testing.T) { t.Run("add second rule", func(t *testing.T) { ip := net.ParseIP("10.20.0.3") port := &fw.Port{ - Values: []int{443}, + Values: []uint16{443}, } rule2, err = manager.AddPeerFiltering(ip, "tcp", port, nil, fw.ActionAccept, "default", "accept HTTPS traffic from ports range") for _, r := range rule2 { @@ -214,7 +215,7 @@ func TestIptablesCreatePerformance(t *testing.T) { ip := net.ParseIP("10.20.0.100") start := time.Now() for i := 0; i < testMax; i++ { - port := &fw.Port{Values: []int{1000 + i}} + port := &fw.Port{Values: []uint16{uint16(1000 + i)}} _, err = manager.AddPeerFiltering(ip, "tcp", nil, port, fw.ActionAccept, "", "accept HTTP traffic") require.NoError(t, err, "failed to add rule") diff --git a/client/firewall/iptables/router_linux.go b/client/firewall/iptables/router_linux.go index d067a3e7b..a47d3ffe6 100644 --- a/client/firewall/iptables/router_linux.go +++ b/client/firewall/iptables/router_linux.go @@ -590,10 +590,10 @@ func applyPort(flag string, port *firewall.Port) []string { if len(port.Values) > 1 { portList := make([]string, len(port.Values)) for i, p := range port.Values { - portList[i] = strconv.Itoa(p) + portList[i] = strconv.Itoa(int(p)) } return []string{"-m", "multiport", flag, strings.Join(portList, ",")} } - return []string{flag, strconv.Itoa(port.Values[0])} + return []string{flag, strconv.Itoa(int(port.Values[0]))} } diff --git a/client/firewall/iptables/router_linux_test.go b/client/firewall/iptables/router_linux_test.go index 861bf8601..0eb207567 100644 --- a/client/firewall/iptables/router_linux_test.go +++ b/client/firewall/iptables/router_linux_test.go @@ -239,7 +239,7 @@ func TestRouter_AddRouteFiltering(t *testing.T) { destination: netip.MustParsePrefix("10.0.0.0/24"), proto: firewall.ProtocolTCP, sPort: nil, - dPort: &firewall.Port{Values: []int{80}}, + dPort: &firewall.Port{Values: []uint16{80}}, direction: firewall.RuleDirectionIN, action: firewall.ActionAccept, expectSet: false, @@ -252,7 +252,7 @@ func TestRouter_AddRouteFiltering(t *testing.T) { }, destination: netip.MustParsePrefix("10.0.0.0/8"), proto: firewall.ProtocolUDP, - sPort: &firewall.Port{Values: []int{1024, 2048}, IsRange: true}, + sPort: &firewall.Port{Values: []uint16{1024, 2048}, IsRange: true}, dPort: nil, direction: firewall.RuleDirectionOUT, action: firewall.ActionDrop, @@ -285,7 +285,7 @@ func TestRouter_AddRouteFiltering(t *testing.T) { sources: []netip.Prefix{netip.MustParsePrefix("172.16.0.0/12")}, destination: netip.MustParsePrefix("192.168.0.0/16"), proto: firewall.ProtocolTCP, - sPort: &firewall.Port{Values: []int{80, 443, 8080}}, + sPort: &firewall.Port{Values: []uint16{80, 443, 8080}}, dPort: nil, direction: firewall.RuleDirectionOUT, action: firewall.ActionAccept, @@ -297,7 +297,7 @@ func TestRouter_AddRouteFiltering(t *testing.T) { destination: netip.MustParsePrefix("10.0.0.0/24"), proto: firewall.ProtocolUDP, sPort: nil, - dPort: &firewall.Port{Values: []int{5000, 5100}, IsRange: true}, + dPort: &firewall.Port{Values: []uint16{5000, 5100}, IsRange: true}, direction: firewall.RuleDirectionIN, action: firewall.ActionDrop, expectSet: false, @@ -307,8 +307,8 @@ func TestRouter_AddRouteFiltering(t *testing.T) { sources: []netip.Prefix{netip.MustParsePrefix("10.0.0.0/24")}, destination: netip.MustParsePrefix("172.16.0.0/16"), proto: firewall.ProtocolTCP, - sPort: &firewall.Port{Values: []int{1024, 65535}, IsRange: true}, - dPort: &firewall.Port{Values: []int{22}}, + sPort: &firewall.Port{Values: []uint16{1024, 65535}, IsRange: true}, + dPort: &firewall.Port{Values: []uint16{22}}, direction: firewall.RuleDirectionOUT, action: firewall.ActionAccept, expectSet: false, diff --git a/client/firewall/manager/port.go b/client/firewall/manager/port.go index 9061c1e63..df02e3117 100644 --- a/client/firewall/manager/port.go +++ b/client/firewall/manager/port.go @@ -30,7 +30,7 @@ type Port struct { IsRange bool // Values contains one value for single port, multiple values for the list of ports, or two values for the range of ports - Values []int + Values []uint16 } // String interface implementation @@ -40,7 +40,11 @@ func (p *Port) String() string { if ports != "" { ports += "," } - ports += strconv.Itoa(port) + ports += strconv.Itoa(int(port)) } + if p.IsRange { + ports = "range:" + ports + } + return ports } diff --git a/client/firewall/nftables/acl_linux.go b/client/firewall/nftables/acl_linux.go index 0d1d659af..fc5cc6873 100644 --- a/client/firewall/nftables/acl_linux.go +++ b/client/firewall/nftables/acl_linux.go @@ -2,7 +2,6 @@ package nftables import ( "bytes" - "encoding/binary" "fmt" "net" "slices" @@ -327,37 +326,8 @@ func (m *AclManager) addIOFiltering( } } - if sPort != nil && len(sPort.Values) != 0 { - expressions = append(expressions, - &expr.Payload{ - DestRegister: 1, - Base: expr.PayloadBaseTransportHeader, - Offset: 0, - Len: 2, - }, - &expr.Cmp{ - Op: expr.CmpOpEq, - Register: 1, - Data: encodePort(*sPort), - }, - ) - } - - if dPort != nil && len(dPort.Values) != 0 { - expressions = append(expressions, - &expr.Payload{ - DestRegister: 1, - Base: expr.PayloadBaseTransportHeader, - Offset: 2, - Len: 2, - }, - &expr.Cmp{ - Op: expr.CmpOpEq, - Register: 1, - Data: encodePort(*dPort), - }, - ) - } + expressions = append(expressions, applyPort(sPort, true)...) + expressions = append(expressions, applyPort(dPort, false)...) mainExpressions := slices.Clone(expressions) @@ -729,12 +699,6 @@ func generatePeerRuleId(ip net.IP, sPort *firewall.Port, dPort *firewall.Port, a return "set:" + ipset.Name + rulesetID } -func encodePort(port firewall.Port) []byte { - bs := make([]byte, 2) - binary.BigEndian.PutUint16(bs, uint16(port.Values[0])) - return bs -} - func ifname(n string) []byte { b := make([]byte, 16) copy(b, n+"\x00") diff --git a/client/firewall/nftables/manager_linux_test.go b/client/firewall/nftables/manager_linux_test.go index 9c9637282..8d693725a 100644 --- a/client/firewall/nftables/manager_linux_test.go +++ b/client/firewall/nftables/manager_linux_test.go @@ -74,7 +74,7 @@ func TestNftablesManager(t *testing.T) { testClient := &nftables.Conn{} - rule, err := manager.AddPeerFiltering(ip, fw.ProtocolTCP, nil, &fw.Port{Values: []int{53}}, fw.ActionDrop, "", "") + rule, err := manager.AddPeerFiltering(ip, fw.ProtocolTCP, nil, &fw.Port{Values: []uint16{53}}, fw.ActionDrop, "", "") require.NoError(t, err, "failed to add rule") err = manager.Flush() @@ -200,7 +200,7 @@ func TestNFtablesCreatePerformance(t *testing.T) { ip := net.ParseIP("10.20.0.100") start := time.Now() for i := 0; i < testMax; i++ { - port := &fw.Port{Values: []int{1000 + i}} + port := &fw.Port{Values: []uint16{uint16(1000 + i)}} _, err = manager.AddPeerFiltering(ip, "tcp", nil, port, fw.ActionAccept, "", "accept HTTP traffic") require.NoError(t, err, "failed to add rule") @@ -283,7 +283,7 @@ func TestNftablesManagerCompatibilityWithIptables(t *testing.T) { }) ip := net.ParseIP("100.96.0.1") - _, err = manager.AddPeerFiltering(ip, fw.ProtocolTCP, nil, &fw.Port{Values: []int{80}}, fw.ActionAccept, "", "test rule") + _, err = manager.AddPeerFiltering(ip, fw.ProtocolTCP, nil, &fw.Port{Values: []uint16{80}}, fw.ActionAccept, "", "test rule") require.NoError(t, err, "failed to add peer filtering rule") _, err = manager.AddRouteFiltering( @@ -291,7 +291,7 @@ func TestNftablesManagerCompatibilityWithIptables(t *testing.T) { netip.MustParsePrefix("10.1.0.0/24"), fw.ProtocolTCP, nil, - &fw.Port{Values: []int{443}}, + &fw.Port{Values: []uint16{443}}, fw.ActionAccept, ) require.NoError(t, err, "failed to add route filtering rule") diff --git a/client/firewall/nftables/router_linux.go b/client/firewall/nftables/router_linux.go index 34bc9a9bc..19734673b 100644 --- a/client/firewall/nftables/router_linux.go +++ b/client/firewall/nftables/router_linux.go @@ -956,12 +956,12 @@ func applyPort(port *firewall.Port, isSource bool) []expr.Any { &expr.Cmp{ Op: expr.CmpOpGte, Register: 1, - Data: binaryutil.BigEndian.PutUint16(uint16(port.Values[0])), + Data: binaryutil.BigEndian.PutUint16(port.Values[0]), }, &expr.Cmp{ Op: expr.CmpOpLte, Register: 1, - Data: binaryutil.BigEndian.PutUint16(uint16(port.Values[1])), + Data: binaryutil.BigEndian.PutUint16(port.Values[1]), }, ) } else { @@ -980,7 +980,7 @@ func applyPort(port *firewall.Port, isSource bool) []expr.Any { exprs = append(exprs, &expr.Cmp{ Op: expr.CmpOpEq, Register: 1, - Data: binaryutil.BigEndian.PutUint16(uint16(p)), + Data: binaryutil.BigEndian.PutUint16(p), }) } } diff --git a/client/firewall/nftables/router_linux_test.go b/client/firewall/nftables/router_linux_test.go index afc4d5c39..2a5d7168d 100644 --- a/client/firewall/nftables/router_linux_test.go +++ b/client/firewall/nftables/router_linux_test.go @@ -222,7 +222,7 @@ func TestRouter_AddRouteFiltering(t *testing.T) { destination: netip.MustParsePrefix("10.0.0.0/24"), proto: firewall.ProtocolTCP, sPort: nil, - dPort: &firewall.Port{Values: []int{80}}, + dPort: &firewall.Port{Values: []uint16{80}}, direction: firewall.RuleDirectionIN, action: firewall.ActionAccept, expectSet: false, @@ -235,7 +235,7 @@ func TestRouter_AddRouteFiltering(t *testing.T) { }, destination: netip.MustParsePrefix("10.0.0.0/8"), proto: firewall.ProtocolUDP, - sPort: &firewall.Port{Values: []int{1024, 2048}, IsRange: true}, + sPort: &firewall.Port{Values: []uint16{1024, 2048}, IsRange: true}, dPort: nil, direction: firewall.RuleDirectionOUT, action: firewall.ActionDrop, @@ -268,7 +268,7 @@ func TestRouter_AddRouteFiltering(t *testing.T) { sources: []netip.Prefix{netip.MustParsePrefix("172.16.0.0/12")}, destination: netip.MustParsePrefix("192.168.0.0/16"), proto: firewall.ProtocolTCP, - sPort: &firewall.Port{Values: []int{80, 443, 8080}}, + sPort: &firewall.Port{Values: []uint16{80, 443, 8080}}, dPort: nil, direction: firewall.RuleDirectionOUT, action: firewall.ActionAccept, @@ -280,7 +280,7 @@ func TestRouter_AddRouteFiltering(t *testing.T) { destination: netip.MustParsePrefix("10.0.0.0/24"), proto: firewall.ProtocolUDP, sPort: nil, - dPort: &firewall.Port{Values: []int{5000, 5100}, IsRange: true}, + dPort: &firewall.Port{Values: []uint16{5000, 5100}, IsRange: true}, direction: firewall.RuleDirectionIN, action: firewall.ActionDrop, expectSet: false, @@ -290,8 +290,8 @@ func TestRouter_AddRouteFiltering(t *testing.T) { sources: []netip.Prefix{netip.MustParsePrefix("10.0.0.0/24")}, destination: netip.MustParsePrefix("172.16.0.0/16"), proto: firewall.ProtocolTCP, - sPort: &firewall.Port{Values: []int{1024, 65535}, IsRange: true}, - dPort: &firewall.Port{Values: []int{22}}, + sPort: &firewall.Port{Values: []uint16{1024, 65535}, IsRange: true}, + dPort: &firewall.Port{Values: []uint16{22}}, direction: firewall.RuleDirectionOUT, action: firewall.ActionAccept, expectSet: false, diff --git a/client/firewall/uspfilter/rule.go b/client/firewall/uspfilter/rule.go index 1f98ef43e..c59d4b264 100644 --- a/client/firewall/uspfilter/rule.go +++ b/client/firewall/uspfilter/rule.go @@ -4,6 +4,8 @@ import ( "net" "github.com/google/gopacket" + + firewall "github.com/netbirdio/netbird/client/firewall/manager" ) // Rule to handle management of rules @@ -13,8 +15,8 @@ type Rule struct { ipLayer gopacket.LayerType matchByIP bool protoLayer gopacket.LayerType - sPort uint16 - dPort uint16 + sPort *firewall.Port + dPort *firewall.Port drop bool comment string diff --git a/client/firewall/uspfilter/uspfilter.go b/client/firewall/uspfilter/uspfilter.go index f35d971b8..757249b2d 100644 --- a/client/firewall/uspfilter/uspfilter.go +++ b/client/firewall/uspfilter/uspfilter.go @@ -179,13 +179,8 @@ func (m *Manager) AddPeerFiltering( r.matchByIP = false } - if sPort != nil && len(sPort.Values) == 1 { - r.sPort = uint16(sPort.Values[0]) - } - - if dPort != nil && len(dPort.Values) == 1 { - r.dPort = uint16(dPort.Values[0]) - } + r.sPort = sPort + r.dPort = dPort switch proto { case firewall.ProtocolTCP: @@ -364,7 +359,7 @@ func (m *Manager) checkUDPHooks(d *decoder, dstIP net.IP, packetData []byte) boo for _, ipKey := range []string{dstIP.String(), "0.0.0.0", "::"} { if rules, exists := m.outgoingRules[ipKey]; exists { for _, rule := range rules { - if rule.udpHook != nil && (rule.dPort == 0 || rule.dPort == uint16(d.udp.DstPort)) { + if rule.udpHook != nil && portsMatch(rule.dPort, uint16(d.udp.DstPort)) { return rule.udpHook(packetData) } } @@ -484,6 +479,23 @@ func (m *Manager) applyRules(srcIP net.IP, packetData []byte, rules map[string]R return true } +func portsMatch(rulePort *firewall.Port, packetPort uint16) bool { + if rulePort == nil { + return true + } + + if rulePort.IsRange { + return packetPort >= rulePort.Values[0] && packetPort <= rulePort.Values[1] + } + + for _, p := range rulePort.Values { + if p == packetPort { + return true + } + } + return false +} + func validateRule(ip net.IP, packetData []byte, rules map[string]Rule, d *decoder) (bool, bool) { payloadLayer := d.decoded[1] for _, rule := range rules { @@ -501,13 +513,7 @@ func validateRule(ip net.IP, packetData []byte, rules map[string]Rule, d *decode switch payloadLayer { case layers.LayerTypeTCP: - if rule.sPort == 0 && rule.dPort == 0 { - return rule.drop, true - } - if rule.sPort != 0 && rule.sPort == uint16(d.tcp.SrcPort) { - return rule.drop, true - } - if rule.dPort != 0 && rule.dPort == uint16(d.tcp.DstPort) { + if portsMatch(rule.sPort, uint16(d.tcp.SrcPort)) && portsMatch(rule.dPort, uint16(d.tcp.DstPort)) { return rule.drop, true } case layers.LayerTypeUDP: @@ -517,13 +523,7 @@ func validateRule(ip net.IP, packetData []byte, rules map[string]Rule, d *decode return rule.udpHook(packetData), true } - if rule.sPort == 0 && rule.dPort == 0 { - return rule.drop, true - } - if rule.sPort != 0 && rule.sPort == uint16(d.udp.SrcPort) { - return rule.drop, true - } - if rule.dPort != 0 && rule.dPort == uint16(d.udp.DstPort) { + if portsMatch(rule.sPort, uint16(d.udp.SrcPort)) && portsMatch(rule.dPort, uint16(d.udp.DstPort)) { return rule.drop, true } case layers.LayerTypeICMPv4, layers.LayerTypeICMPv6: @@ -548,7 +548,7 @@ func (m *Manager) AddUDPPacketHook( id: uuid.New().String(), ip: ip, protoLayer: layers.LayerTypeUDP, - dPort: dPort, + dPort: &firewall.Port{Values: []uint16{dPort}}, ipLayer: layers.LayerTypeIPv6, comment: fmt.Sprintf("UDP Hook direction: %v, ip:%v, dport:%d", in, ip, dPort), udpHook: hook, diff --git a/client/firewall/uspfilter/uspfilter_bench_test.go b/client/firewall/uspfilter/uspfilter_bench_test.go index 4a210bf47..46bc4439d 100644 --- a/client/firewall/uspfilter/uspfilter_bench_test.go +++ b/client/firewall/uspfilter/uspfilter_bench_test.go @@ -112,8 +112,8 @@ func BenchmarkCoreFiltering(b *testing.B) { for i := 0; i < 1000; i++ { // Simulate realistic ruleset size ip := generateRandomIPs(1)[0] _, err := m.AddPeerFiltering(ip, fw.ProtocolTCP, - &fw.Port{Values: []int{1024 + i}}, - &fw.Port{Values: []int{80}}, + &fw.Port{Values: []uint16{uint16(1024 + i)}}, + &fw.Port{Values: []uint16{80}}, fw.ActionAccept, "", "explicit return") require.NoError(b, err) } @@ -588,7 +588,7 @@ func BenchmarkLongLivedConnections(b *testing.B) { if sc.rules { // Single rule to allow all return traffic from port 80 _, err := manager.AddPeerFiltering(net.ParseIP("0.0.0.0"), fw.ProtocolTCP, - &fw.Port{Values: []int{80}}, + &fw.Port{Values: []uint16{80}}, nil, fw.ActionAccept, "", "return traffic") require.NoError(b, err) @@ -679,7 +679,7 @@ func BenchmarkShortLivedConnections(b *testing.B) { if sc.rules { // Single rule to allow all return traffic from port 80 _, err := manager.AddPeerFiltering(net.ParseIP("0.0.0.0"), fw.ProtocolTCP, - &fw.Port{Values: []int{80}}, + &fw.Port{Values: []uint16{80}}, nil, fw.ActionAccept, "", "return traffic") require.NoError(b, err) @@ -797,7 +797,7 @@ func BenchmarkParallelLongLivedConnections(b *testing.B) { // Setup initial state based on scenario if sc.rules { _, err := manager.AddPeerFiltering(net.ParseIP("0.0.0.0"), fw.ProtocolTCP, - &fw.Port{Values: []int{80}}, + &fw.Port{Values: []uint16{80}}, nil, fw.ActionAccept, "", "return traffic") require.NoError(b, err) @@ -884,7 +884,7 @@ func BenchmarkParallelShortLivedConnections(b *testing.B) { if sc.rules { _, err := manager.AddPeerFiltering(net.ParseIP("0.0.0.0"), fw.ProtocolTCP, - &fw.Port{Values: []int{80}}, + &fw.Port{Values: []uint16{80}}, nil, fw.ActionAccept, "", "return traffic") require.NoError(b, err) diff --git a/client/firewall/uspfilter/uspfilter_test.go b/client/firewall/uspfilter/uspfilter_test.go index 7e87443aa..9d795de69 100644 --- a/client/firewall/uspfilter/uspfilter_test.go +++ b/client/firewall/uspfilter/uspfilter_test.go @@ -69,7 +69,7 @@ func TestManagerAddPeerFiltering(t *testing.T) { ip := net.ParseIP("192.168.1.1") proto := fw.ProtocolTCP - port := &fw.Port{Values: []int{80}} + port := &fw.Port{Values: []uint16{80}} action := fw.ActionDrop comment := "Test rule" @@ -103,7 +103,7 @@ func TestManagerDeleteRule(t *testing.T) { ip := net.ParseIP("192.168.1.1") proto := fw.ProtocolTCP - port := &fw.Port{Values: []int{80}} + port := &fw.Port{Values: []uint16{80}} action := fw.ActionDrop comment := "Test rule 2" @@ -194,8 +194,8 @@ func TestAddUDPPacketHook(t *testing.T) { t.Errorf("expected ip %s, got %s", tt.ip, addedRule.ip) return } - if tt.dPort != addedRule.dPort { - t.Errorf("expected dPort %d, got %d", tt.dPort, addedRule.dPort) + if tt.dPort != addedRule.dPort.Values[0] { + t.Errorf("expected dPort %d, got %d", tt.dPort, addedRule.dPort.Values[0]) return } if layers.LayerTypeUDP != addedRule.protoLayer { @@ -223,7 +223,7 @@ func TestManagerReset(t *testing.T) { ip := net.ParseIP("192.168.1.1") proto := fw.ProtocolTCP - port := &fw.Port{Values: []int{80}} + port := &fw.Port{Values: []uint16{80}} action := fw.ActionDrop comment := "Test rule" @@ -463,7 +463,7 @@ func TestUSPFilterCreatePerformance(t *testing.T) { ip := net.ParseIP("10.20.0.100") start := time.Now() for i := 0; i < testMax; i++ { - port := &fw.Port{Values: []int{1000 + i}} + port := &fw.Port{Values: []uint16{uint16(1000 + i)}} _, err = manager.AddPeerFiltering(ip, "tcp", nil, port, fw.ActionAccept, "", "accept HTTP traffic") require.NoError(t, err, "failed to add rule") diff --git a/client/internal/acl/manager.go b/client/internal/acl/manager.go index 0ade5d7ce..9ec0bb031 100644 --- a/client/internal/acl/manager.go +++ b/client/internal/acl/manager.go @@ -268,13 +268,16 @@ func (d *DefaultManager) protoRuleToFirewallRule( } var port *firewall.Port - if r.Port != "" { + if r.PortInfo != nil { + port = convertPortInfo(r.PortInfo) + } else if r.Port != "" { + // old version of management, single port value, err := strconv.Atoi(r.Port) if err != nil { - return "", nil, fmt.Errorf("invalid port, skipping firewall rule") + return "", nil, fmt.Errorf("invalid port: %w", err) } port = &firewall.Port{ - Values: []int{value}, + Values: []uint16{uint16(value)}, } } @@ -539,14 +542,14 @@ func convertPortInfo(portInfo *mgmProto.PortInfo) *firewall.Port { if portInfo.GetPort() != 0 { return &firewall.Port{ - Values: []int{int(portInfo.GetPort())}, + Values: []uint16{uint16(int(portInfo.GetPort()))}, } } if portInfo.GetRange() != nil { return &firewall.Port{ IsRange: true, - Values: []int{int(portInfo.GetRange().Start), int(portInfo.GetRange().End)}, + Values: []uint16{uint16(portInfo.GetRange().Start), uint16(portInfo.GetRange().End)}, } } diff --git a/client/internal/dnsfwd/manager.go b/client/internal/dnsfwd/manager.go index 968f2d398..5d3036dde 100644 --- a/client/internal/dnsfwd/manager.go +++ b/client/internal/dnsfwd/manager.go @@ -81,7 +81,7 @@ func (m *Manager) Stop(ctx context.Context) error { func (h *Manager) allowDNSFirewall() error { dport := &firewall.Port{ IsRange: false, - Values: []int{ListenPort}, + Values: []uint16{ListenPort}, } if h.firewall == nil { diff --git a/client/internal/engine.go b/client/internal/engine.go index b3689c911..43749fbe5 100644 --- a/client/internal/engine.go +++ b/client/internal/engine.go @@ -495,7 +495,7 @@ func (e *Engine) initFirewall() error { } rosenpassPort := e.rpManager.GetAddress().Port - port := manager.Port{Values: []int{rosenpassPort}} + port := manager.Port{Values: []uint16{uint16(rosenpassPort)}} // this rule is static and will be torn down on engine down by the firewall manager if _, err := e.firewall.AddPeerFiltering( diff --git a/management/proto/management.pb.go b/management/proto/management.pb.go index 7846c286d..ae6559675 100644 --- a/management/proto/management.pb.go +++ b/management/proto/management.pb.go @@ -2624,6 +2624,7 @@ type FirewallRule struct { Action RuleAction `protobuf:"varint,3,opt,name=Action,proto3,enum=management.RuleAction" json:"Action,omitempty"` Protocol RuleProtocol `protobuf:"varint,4,opt,name=Protocol,proto3,enum=management.RuleProtocol" json:"Protocol,omitempty"` Port string `protobuf:"bytes,5,opt,name=Port,proto3" json:"Port,omitempty"` + PortInfo *PortInfo `protobuf:"bytes,6,opt,name=PortInfo,proto3" json:"PortInfo,omitempty"` } func (x *FirewallRule) Reset() { @@ -2693,6 +2694,13 @@ func (x *FirewallRule) GetPort() string { return "" } +func (x *FirewallRule) GetPortInfo() *PortInfo { + if x != nil { + return x.PortInfo + } + return nil +} + type NetworkAddress struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -3397,7 +3405,7 @@ var file_management_proto_rawDesc = []byte{ 0x52, 0x02, 0x49, 0x50, 0x12, 0x16, 0x0a, 0x06, 0x4e, 0x53, 0x54, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x4e, 0x53, 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x50, 0x6f, 0x72, 0x74, - 0x22, 0xd9, 0x01, 0x0a, 0x0c, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, + 0x22, 0x8b, 0x02, 0x0a, 0x0c, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x50, 0x65, 0x65, 0x72, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x50, 0x65, 0x65, 0x72, 0x49, 0x50, 0x12, 0x37, 0x0a, 0x09, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x19, 0x2e, 0x6d, @@ -3410,87 +3418,90 @@ var file_management_proto_rawDesc = []byte{ 0x20, 0x01, 0x28, 0x0e, 0x32, 0x18, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x52, 0x08, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x6f, 0x72, 0x74, - 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x22, 0x38, 0x0a, 0x0e, - 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x14, - 0x0a, 0x05, 0x6e, 0x65, 0x74, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6e, - 0x65, 0x74, 0x49, 0x50, 0x12, 0x10, 0x0a, 0x03, 0x6d, 0x61, 0x63, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x03, 0x6d, 0x61, 0x63, 0x22, 0x1e, 0x0a, 0x06, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, - 0x12, 0x14, 0x0a, 0x05, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, - 0x05, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x22, 0x96, 0x01, 0x0a, 0x08, 0x50, 0x6f, 0x72, 0x74, 0x49, - 0x6e, 0x66, 0x6f, 0x12, 0x14, 0x0a, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x0d, 0x48, 0x00, 0x52, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x12, 0x32, 0x0a, 0x05, 0x72, 0x61, 0x6e, - 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, - 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x2e, 0x52, - 0x61, 0x6e, 0x67, 0x65, 0x48, 0x00, 0x52, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x1a, 0x2f, 0x0a, - 0x05, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x12, 0x10, 0x0a, 0x03, - 0x65, 0x6e, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x03, 0x65, 0x6e, 0x64, 0x42, 0x0f, - 0x0a, 0x0d, 0x70, 0x6f, 0x72, 0x74, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, - 0xd1, 0x02, 0x0a, 0x11, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, - 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x22, 0x0a, 0x0c, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, - 0x61, 0x6e, 0x67, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0c, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x12, 0x2e, 0x0a, 0x06, 0x61, 0x63, 0x74, - 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, - 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x41, 0x63, 0x74, 0x69, 0x6f, - 0x6e, 0x52, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, - 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, - 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x34, 0x0a, 0x08, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x18, 0x2e, - 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x50, - 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, - 0x6c, 0x12, 0x30, 0x0a, 0x08, 0x70, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x18, 0x05, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, - 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x08, 0x70, 0x6f, 0x72, 0x74, 0x49, - 0x6e, 0x66, 0x6f, 0x12, 0x1c, 0x0a, 0x09, 0x69, 0x73, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, - 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x69, 0x73, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, - 0x63, 0x12, 0x18, 0x0a, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x07, 0x20, 0x03, - 0x28, 0x09, 0x52, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x26, 0x0a, 0x0e, 0x63, - 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x08, 0x20, - 0x01, 0x28, 0x0d, 0x52, 0x0e, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x50, 0x72, 0x6f, 0x74, 0x6f, - 0x63, 0x6f, 0x6c, 0x2a, 0x4c, 0x0a, 0x0c, 0x52, 0x75, 0x6c, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, - 0x63, 0x6f, 0x6c, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, - 0x12, 0x07, 0x0a, 0x03, 0x41, 0x4c, 0x4c, 0x10, 0x01, 0x12, 0x07, 0x0a, 0x03, 0x54, 0x43, 0x50, - 0x10, 0x02, 0x12, 0x07, 0x0a, 0x03, 0x55, 0x44, 0x50, 0x10, 0x03, 0x12, 0x08, 0x0a, 0x04, 0x49, - 0x43, 0x4d, 0x50, 0x10, 0x04, 0x12, 0x0a, 0x0a, 0x06, 0x43, 0x55, 0x53, 0x54, 0x4f, 0x4d, 0x10, - 0x05, 0x2a, 0x20, 0x0a, 0x0d, 0x52, 0x75, 0x6c, 0x65, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, - 0x6f, 0x6e, 0x12, 0x06, 0x0a, 0x02, 0x49, 0x4e, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x4f, 0x55, - 0x54, 0x10, 0x01, 0x2a, 0x22, 0x0a, 0x0a, 0x52, 0x75, 0x6c, 0x65, 0x41, 0x63, 0x74, 0x69, 0x6f, - 0x6e, 0x12, 0x0a, 0x0a, 0x06, 0x41, 0x43, 0x43, 0x45, 0x50, 0x54, 0x10, 0x00, 0x12, 0x08, 0x0a, - 0x04, 0x44, 0x52, 0x4f, 0x50, 0x10, 0x01, 0x32, 0x90, 0x04, 0x0a, 0x11, 0x4d, 0x61, 0x6e, 0x61, - 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x45, 0x0a, - 0x05, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, + 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x30, 0x0a, 0x08, + 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, + 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x6f, 0x72, 0x74, + 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x08, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x22, 0x38, + 0x0a, 0x0e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, + 0x12, 0x14, 0x0a, 0x05, 0x6e, 0x65, 0x74, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x05, 0x6e, 0x65, 0x74, 0x49, 0x50, 0x12, 0x10, 0x0a, 0x03, 0x6d, 0x61, 0x63, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x03, 0x6d, 0x61, 0x63, 0x22, 0x1e, 0x0a, 0x06, 0x43, 0x68, 0x65, 0x63, + 0x6b, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, + 0x09, 0x52, 0x05, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x22, 0x96, 0x01, 0x0a, 0x08, 0x50, 0x6f, 0x72, + 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x14, 0x0a, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x0d, 0x48, 0x00, 0x52, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x12, 0x32, 0x0a, 0x05, 0x72, + 0x61, 0x6e, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e, + 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, + 0x2e, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x48, 0x00, 0x52, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x1a, + 0x2f, 0x0a, 0x05, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x72, + 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x12, 0x10, + 0x0a, 0x03, 0x65, 0x6e, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x03, 0x65, 0x6e, 0x64, + 0x42, 0x0f, 0x0a, 0x0d, 0x70, 0x6f, 0x72, 0x74, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, + 0x6e, 0x22, 0xd1, 0x02, 0x0a, 0x11, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x46, 0x69, 0x72, 0x65, 0x77, + 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x22, 0x0a, 0x0c, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0c, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x12, 0x2e, 0x0a, 0x06, 0x61, + 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x16, 0x2e, 0x6d, 0x61, + 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x41, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x20, 0x0a, 0x0b, 0x64, + 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0b, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x34, 0x0a, + 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, + 0x18, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, + 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x63, 0x6f, 0x6c, 0x12, 0x30, 0x0a, 0x08, 0x70, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x18, + 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, + 0x6e, 0x74, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x08, 0x70, 0x6f, 0x72, + 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x1c, 0x0a, 0x09, 0x69, 0x73, 0x44, 0x79, 0x6e, 0x61, 0x6d, + 0x69, 0x63, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x69, 0x73, 0x44, 0x79, 0x6e, 0x61, + 0x6d, 0x69, 0x63, 0x12, 0x18, 0x0a, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x07, + 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x26, 0x0a, + 0x0e, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, + 0x08, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0e, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x50, 0x72, 0x6f, + 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x2a, 0x4c, 0x0a, 0x0c, 0x52, 0x75, 0x6c, 0x65, 0x50, 0x72, 0x6f, + 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, + 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x41, 0x4c, 0x4c, 0x10, 0x01, 0x12, 0x07, 0x0a, 0x03, 0x54, + 0x43, 0x50, 0x10, 0x02, 0x12, 0x07, 0x0a, 0x03, 0x55, 0x44, 0x50, 0x10, 0x03, 0x12, 0x08, 0x0a, + 0x04, 0x49, 0x43, 0x4d, 0x50, 0x10, 0x04, 0x12, 0x0a, 0x0a, 0x06, 0x43, 0x55, 0x53, 0x54, 0x4f, + 0x4d, 0x10, 0x05, 0x2a, 0x20, 0x0a, 0x0d, 0x52, 0x75, 0x6c, 0x65, 0x44, 0x69, 0x72, 0x65, 0x63, + 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x06, 0x0a, 0x02, 0x49, 0x4e, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, + 0x4f, 0x55, 0x54, 0x10, 0x01, 0x2a, 0x22, 0x0a, 0x0a, 0x52, 0x75, 0x6c, 0x65, 0x41, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x12, 0x0a, 0x0a, 0x06, 0x41, 0x43, 0x43, 0x45, 0x50, 0x54, 0x10, 0x00, 0x12, + 0x08, 0x0a, 0x04, 0x44, 0x52, 0x4f, 0x50, 0x10, 0x01, 0x32, 0x90, 0x04, 0x0a, 0x11, 0x4d, 0x61, + 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, + 0x45, 0x0a, 0x05, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, + 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, + 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, - 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, - 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, - 0x67, 0x65, 0x22, 0x00, 0x12, 0x46, 0x0a, 0x04, 0x53, 0x79, 0x6e, 0x63, 0x12, 0x1c, 0x2e, 0x6d, + 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12, 0x46, 0x0a, 0x04, 0x53, 0x79, 0x6e, 0x63, 0x12, 0x1c, + 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, + 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, - 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, - 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, - 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x30, 0x01, 0x12, 0x42, 0x0a, 0x0c, - 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x12, 0x11, 0x2e, 0x6d, - 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, - 0x1d, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x65, 0x72, - 0x76, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, - 0x12, 0x33, 0x0a, 0x09, 0x69, 0x73, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x79, 0x12, 0x11, 0x2e, - 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, - 0x1a, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, - 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x5a, 0x0a, 0x1a, 0x47, 0x65, 0x74, 0x44, 0x65, 0x76, 0x69, - 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, - 0x6c, 0x6f, 0x77, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, + 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x30, 0x01, 0x12, 0x42, + 0x0a, 0x0c, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x12, 0x11, + 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, + 0x79, 0x1a, 0x1d, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, + 0x65, 0x72, 0x76, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x22, 0x00, 0x12, 0x33, 0x0a, 0x09, 0x69, 0x73, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x79, 0x12, + 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, + 0x74, 0x79, 0x1a, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, + 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x5a, 0x0a, 0x1a, 0x47, 0x65, 0x74, 0x44, 0x65, + 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, + 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, + 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, - 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, - 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, - 0x00, 0x12, 0x58, 0x0a, 0x18, 0x47, 0x65, 0x74, 0x50, 0x4b, 0x43, 0x45, 0x41, 0x75, 0x74, 0x68, - 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x12, 0x1c, 0x2e, + 0x65, 0x22, 0x00, 0x12, 0x58, 0x0a, 0x18, 0x47, 0x65, 0x74, 0x50, 0x4b, 0x43, 0x45, 0x41, 0x75, + 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x12, + 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, + 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, - 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, - 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, - 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12, 0x3d, 0x0a, 0x08, 0x53, - 0x79, 0x6e, 0x63, 0x4d, 0x65, 0x74, 0x61, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, - 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, - 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, - 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x42, 0x08, 0x5a, 0x06, 0x2f, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12, 0x3d, 0x0a, + 0x08, 0x53, 0x79, 0x6e, 0x63, 0x4d, 0x65, 0x74, 0x61, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, + 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, + 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, + 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x42, 0x08, 0x5a, 0x06, + 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -3596,29 +3607,30 @@ var file_management_proto_depIdxs = []int32{ 1, // 39: management.FirewallRule.Direction:type_name -> management.RuleDirection 2, // 40: management.FirewallRule.Action:type_name -> management.RuleAction 0, // 41: management.FirewallRule.Protocol:type_name -> management.RuleProtocol - 42, // 42: management.PortInfo.range:type_name -> management.PortInfo.Range - 2, // 43: management.RouteFirewallRule.action:type_name -> management.RuleAction - 0, // 44: management.RouteFirewallRule.protocol:type_name -> management.RuleProtocol - 40, // 45: management.RouteFirewallRule.portInfo:type_name -> management.PortInfo - 5, // 46: management.ManagementService.Login:input_type -> management.EncryptedMessage - 5, // 47: management.ManagementService.Sync:input_type -> management.EncryptedMessage - 17, // 48: management.ManagementService.GetServerKey:input_type -> management.Empty - 17, // 49: management.ManagementService.isHealthy:input_type -> management.Empty - 5, // 50: management.ManagementService.GetDeviceAuthorizationFlow:input_type -> management.EncryptedMessage - 5, // 51: management.ManagementService.GetPKCEAuthorizationFlow:input_type -> management.EncryptedMessage - 5, // 52: management.ManagementService.SyncMeta:input_type -> management.EncryptedMessage - 5, // 53: management.ManagementService.Login:output_type -> management.EncryptedMessage - 5, // 54: management.ManagementService.Sync:output_type -> management.EncryptedMessage - 16, // 55: management.ManagementService.GetServerKey:output_type -> management.ServerKeyResponse - 17, // 56: management.ManagementService.isHealthy:output_type -> management.Empty - 5, // 57: management.ManagementService.GetDeviceAuthorizationFlow:output_type -> management.EncryptedMessage - 5, // 58: management.ManagementService.GetPKCEAuthorizationFlow:output_type -> management.EncryptedMessage - 17, // 59: management.ManagementService.SyncMeta:output_type -> management.Empty - 53, // [53:60] is the sub-list for method output_type - 46, // [46:53] is the sub-list for method input_type - 46, // [46:46] is the sub-list for extension type_name - 46, // [46:46] is the sub-list for extension extendee - 0, // [0:46] is the sub-list for field type_name + 40, // 42: management.FirewallRule.PortInfo:type_name -> management.PortInfo + 42, // 43: management.PortInfo.range:type_name -> management.PortInfo.Range + 2, // 44: management.RouteFirewallRule.action:type_name -> management.RuleAction + 0, // 45: management.RouteFirewallRule.protocol:type_name -> management.RuleProtocol + 40, // 46: management.RouteFirewallRule.portInfo:type_name -> management.PortInfo + 5, // 47: management.ManagementService.Login:input_type -> management.EncryptedMessage + 5, // 48: management.ManagementService.Sync:input_type -> management.EncryptedMessage + 17, // 49: management.ManagementService.GetServerKey:input_type -> management.Empty + 17, // 50: management.ManagementService.isHealthy:input_type -> management.Empty + 5, // 51: management.ManagementService.GetDeviceAuthorizationFlow:input_type -> management.EncryptedMessage + 5, // 52: management.ManagementService.GetPKCEAuthorizationFlow:input_type -> management.EncryptedMessage + 5, // 53: management.ManagementService.SyncMeta:input_type -> management.EncryptedMessage + 5, // 54: management.ManagementService.Login:output_type -> management.EncryptedMessage + 5, // 55: management.ManagementService.Sync:output_type -> management.EncryptedMessage + 16, // 56: management.ManagementService.GetServerKey:output_type -> management.ServerKeyResponse + 17, // 57: management.ManagementService.isHealthy:output_type -> management.Empty + 5, // 58: management.ManagementService.GetDeviceAuthorizationFlow:output_type -> management.EncryptedMessage + 5, // 59: management.ManagementService.GetPKCEAuthorizationFlow:output_type -> management.EncryptedMessage + 17, // 60: management.ManagementService.SyncMeta:output_type -> management.Empty + 54, // [54:61] is the sub-list for method output_type + 47, // [47:54] is the sub-list for method input_type + 47, // [47:47] is the sub-list for extension type_name + 47, // [47:47] is the sub-list for extension extendee + 0, // [0:47] is the sub-list for field type_name } func init() { file_management_proto_init() } diff --git a/management/proto/management.proto b/management/proto/management.proto index 2318fc675..9db66ec4d 100644 --- a/management/proto/management.proto +++ b/management/proto/management.proto @@ -430,6 +430,7 @@ message FirewallRule { RuleAction Action = 3; RuleProtocol Protocol = 4; string Port = 5; + PortInfo PortInfo = 6; } message NetworkAddress { From a32ec97911962c14d386724463b183ef1e487a4d Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Mon, 27 Jan 2025 18:13:10 +0100 Subject: [PATCH 34/92] [client] Use dynamic dns route resolution on iOS (#3243) --- client/internal/routemanager/client.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/internal/routemanager/client.go b/client/internal/routemanager/client.go index 73f552aab..faf0fadaa 100644 --- a/client/internal/routemanager/client.go +++ b/client/internal/routemanager/client.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "reflect" + runtime "runtime" "time" "github.com/hashicorp/go-multierror" @@ -439,7 +440,7 @@ func handlerType(rt *route.Route, useNewDNSRoute bool) int { return handlerTypeStatic } - if useNewDNSRoute { + if useNewDNSRoute && runtime.GOOS != "ios" { return handlerTypeDomain } return handlerTypeDynamic From 7335c825531abf14f5851437d03aef26e88ff53f Mon Sep 17 00:00:00 2001 From: Pascal Fischer <32096965+pascal-fischer@users.noreply.github.com> Date: Tue, 28 Jan 2025 07:05:21 +0100 Subject: [PATCH 35/92] [management] copy destination and source resource on policyRUle copy (#3235) --- management/server/types/policyrule.go | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/management/server/types/policyrule.go b/management/server/types/policyrule.go index bd9a99292..721621a4b 100644 --- a/management/server/types/policyrule.go +++ b/management/server/types/policyrule.go @@ -66,18 +66,20 @@ type PolicyRule struct { // Copy returns a copy of a policy rule func (pm *PolicyRule) Copy() *PolicyRule { rule := &PolicyRule{ - ID: pm.ID, - PolicyID: pm.PolicyID, - Name: pm.Name, - Description: pm.Description, - Enabled: pm.Enabled, - Action: pm.Action, - Destinations: make([]string, len(pm.Destinations)), - Sources: make([]string, len(pm.Sources)), - Bidirectional: pm.Bidirectional, - Protocol: pm.Protocol, - Ports: make([]string, len(pm.Ports)), - PortRanges: make([]RulePortRange, len(pm.PortRanges)), + ID: pm.ID, + PolicyID: pm.PolicyID, + Name: pm.Name, + Description: pm.Description, + Enabled: pm.Enabled, + Action: pm.Action, + Destinations: make([]string, len(pm.Destinations)), + DestinationResource: pm.DestinationResource, + Sources: make([]string, len(pm.Sources)), + SourceResource: pm.SourceResource, + Bidirectional: pm.Bidirectional, + Protocol: pm.Protocol, + Ports: make([]string, len(pm.Ports)), + PortRanges: make([]RulePortRange, len(pm.PortRanges)), } copy(rule.Destinations, pm.Destinations) copy(rule.Sources, pm.Sources) From a7ddb8f1f8fe2893d90132ee7de038d0adc93b46 Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Tue, 28 Jan 2025 12:25:45 +0100 Subject: [PATCH 36/92] [client] Replace engine probes with direct calls (#3195) --- client/cmd/up.go | 2 +- client/internal/connect.go | 17 ++--- client/internal/engine.go | 133 ++++++++++++------------------------- client/internal/probe.go | 58 ---------------- client/server/server.go | 46 +++++-------- 5 files changed, 69 insertions(+), 187 deletions(-) delete mode 100644 client/internal/probe.go diff --git a/client/cmd/up.go b/client/cmd/up.go index 9f8f738bc..f7c2bbfe4 100644 --- a/client/cmd/up.go +++ b/client/cmd/up.go @@ -190,7 +190,7 @@ func runInForegroundMode(ctx context.Context, cmd *cobra.Command) error { r.GetFullStatus() connectClient := internal.NewConnectClient(ctx, config, r) - return connectClient.Run() + return connectClient.Run(nil) } func runInDaemonMode(ctx context.Context, cmd *cobra.Command) error { diff --git a/client/internal/connect.go b/client/internal/connect.go index a1e8f0f8c..3e3f04f17 100644 --- a/client/internal/connect.go +++ b/client/internal/connect.go @@ -59,13 +59,8 @@ func NewConnectClient( } // Run with main logic. -func (c *ConnectClient) Run() error { - return c.run(MobileDependency{}, nil, nil) -} - -// RunWithProbes runs the client's main logic with probes attached -func (c *ConnectClient) RunWithProbes(probes *ProbeHolder, runningChan chan error) error { - return c.run(MobileDependency{}, probes, runningChan) +func (c *ConnectClient) Run(runningChan chan error) error { + return c.run(MobileDependency{}, runningChan) } // RunOnAndroid with main logic on mobile system @@ -84,7 +79,7 @@ func (c *ConnectClient) RunOnAndroid( HostDNSAddresses: dnsAddresses, DnsReadyListener: dnsReadyListener, } - return c.run(mobileDependency, nil, nil) + return c.run(mobileDependency, nil) } func (c *ConnectClient) RunOniOS( @@ -102,10 +97,10 @@ func (c *ConnectClient) RunOniOS( DnsManager: dnsManager, StateFilePath: stateFilePath, } - return c.run(mobileDependency, nil, nil) + return c.run(mobileDependency, nil) } -func (c *ConnectClient) run(mobileDependency MobileDependency, probes *ProbeHolder, runningChan chan error) error { +func (c *ConnectClient) run(mobileDependency MobileDependency, runningChan chan error) error { defer func() { if r := recover(); r != nil { log.Panicf("Panic occurred: %v, stack trace: %s", r, string(debug.Stack())) @@ -261,7 +256,7 @@ func (c *ConnectClient) run(mobileDependency MobileDependency, probes *ProbeHold checks := loginResp.GetChecks() c.engineMutex.Lock() - c.engine = NewEngineWithProbes(engineCtx, cancel, signalClient, mgmClient, relayManager, engineConfig, mobileDependency, c.statusRecorder, probes, checks) + c.engine = NewEngine(engineCtx, cancel, signalClient, mgmClient, relayManager, engineConfig, mobileDependency, c.statusRecorder, checks) c.engine.SetNetworkMapPersistence(c.persistNetworkMap) c.engineMutex.Unlock() diff --git a/client/internal/engine.go b/client/internal/engine.go index 43749fbe5..4f69adfa6 100644 --- a/client/internal/engine.go +++ b/client/internal/engine.go @@ -175,8 +175,6 @@ type Engine struct { dnsServer dns.Server - probes *ProbeHolder - // checks are the client-applied posture checks that need to be evaluated on the client checks []*mgmProto.Checks @@ -196,7 +194,7 @@ type Peer struct { WgAllowedIps string } -// NewEngine creates a new Connection Engine +// NewEngine creates a new Connection Engine with probes attached func NewEngine( clientCtx context.Context, clientCancel context.CancelFunc, @@ -207,33 +205,6 @@ func NewEngine( mobileDep MobileDependency, statusRecorder *peer.Status, checks []*mgmProto.Checks, -) *Engine { - return NewEngineWithProbes( - clientCtx, - clientCancel, - signalClient, - mgmClient, - relayManager, - config, - mobileDep, - statusRecorder, - nil, - checks, - ) -} - -// NewEngineWithProbes creates a new Connection Engine with probes attached -func NewEngineWithProbes( - clientCtx context.Context, - clientCancel context.CancelFunc, - signalClient signal.Client, - mgmClient mgm.Client, - relayManager *relayClient.Manager, - config *EngineConfig, - mobileDep MobileDependency, - statusRecorder *peer.Status, - probes *ProbeHolder, - checks []*mgmProto.Checks, ) *Engine { engine := &Engine{ clientCtx: clientCtx, @@ -251,7 +222,6 @@ func NewEngineWithProbes( networkSerial: 0, sshServerFunc: nbssh.DefaultSSHServer, statusRecorder: statusRecorder, - probes: probes, checks: checks, connSemaphore: semaphoregroup.NewSemaphoreGroup(connInitLimit), } @@ -450,7 +420,6 @@ func (e *Engine) Start() error { e.receiveSignalEvents() e.receiveManagementEvents() - e.receiveProbeEvents() // starting network monitor at the very last to avoid disruptions e.startNetworkMonitor() @@ -1513,72 +1482,58 @@ func (e *Engine) getRosenpassAddr() string { return "" } -func (e *Engine) receiveProbeEvents() { - if e.probes == nil { - return +// RunHealthProbes executes health checks for Signal, Management, Relay and WireGuard services +// and updates the status recorder with the latest states. +func (e *Engine) RunHealthProbes() bool { + signalHealthy := e.signal.IsHealthy() + log.Debugf("signal health check: healthy=%t", signalHealthy) + + managementHealthy := e.mgmClient.IsHealthy() + log.Debugf("management health check: healthy=%t", managementHealthy) + + results := append(e.probeSTUNs(), e.probeTURNs()...) + e.statusRecorder.UpdateRelayStates(results) + + relayHealthy := true + for _, res := range results { + if res.Err != nil { + relayHealthy = false + break + } } - if e.probes.SignalProbe != nil { - go e.probes.SignalProbe.Receive(e.ctx, func() bool { - healthy := e.signal.IsHealthy() - log.Debugf("received signal probe request, healthy: %t", healthy) - return healthy - }) + log.Debugf("relay health check: healthy=%t", relayHealthy) + + for _, key := range e.peerStore.PeersPubKey() { + wgStats, err := e.wgInterface.GetStats(key) + if err != nil { + log.Debugf("failed to get wg stats for peer %s: %s", key, err) + continue + } + // wgStats could be zero value, in which case we just reset the stats + if err := e.statusRecorder.UpdateWireGuardPeerState(key, wgStats); err != nil { + log.Debugf("failed to update wg stats for peer %s: %s", key, err) + } } - if e.probes.MgmProbe != nil { - go e.probes.MgmProbe.Receive(e.ctx, func() bool { - healthy := e.mgmClient.IsHealthy() - log.Debugf("received management probe request, healthy: %t", healthy) - return healthy - }) - } - - if e.probes.RelayProbe != nil { - go e.probes.RelayProbe.Receive(e.ctx, func() bool { - healthy := true - - results := append(e.probeSTUNs(), e.probeTURNs()...) - e.statusRecorder.UpdateRelayStates(results) - - // A single failed server will result in a "failed" probe - for _, res := range results { - if res.Err != nil { - healthy = false - break - } - } - - log.Debugf("received relay probe request, healthy: %t", healthy) - return healthy - }) - } - - if e.probes.WgProbe != nil { - go e.probes.WgProbe.Receive(e.ctx, func() bool { - log.Debug("received wg probe request") - - for _, key := range e.peerStore.PeersPubKey() { - wgStats, err := e.wgInterface.GetStats(key) - if err != nil { - log.Debugf("failed to get wg stats for peer %s: %s", key, err) - } - // wgStats could be zero value, in which case we just reset the stats - if err := e.statusRecorder.UpdateWireGuardPeerState(key, wgStats); err != nil { - log.Debugf("failed to update wg stats for peer %s: %s", key, err) - } - } - - return true - }) - } + allHealthy := signalHealthy && managementHealthy && relayHealthy + log.Debugf("all health checks completed: healthy=%t", allHealthy) + return allHealthy } func (e *Engine) probeSTUNs() []relay.ProbeResult { - return relay.ProbeAll(e.ctx, relay.ProbeSTUN, e.STUNs) + e.syncMsgMux.Lock() + stuns := slices.Clone(e.STUNs) + e.syncMsgMux.Unlock() + + return relay.ProbeAll(e.ctx, relay.ProbeSTUN, stuns) } func (e *Engine) probeTURNs() []relay.ProbeResult { - return relay.ProbeAll(e.ctx, relay.ProbeTURN, e.TURNs) + e.syncMsgMux.Lock() + turns := slices.Clone(e.TURNs) + e.syncMsgMux.Unlock() + + return relay.ProbeAll(e.ctx, relay.ProbeTURN, turns) } func (e *Engine) restartEngine() { diff --git a/client/internal/probe.go b/client/internal/probe.go deleted file mode 100644 index 23290cf74..000000000 --- a/client/internal/probe.go +++ /dev/null @@ -1,58 +0,0 @@ -package internal - -import "context" - -type ProbeHolder struct { - MgmProbe *Probe - SignalProbe *Probe - RelayProbe *Probe - WgProbe *Probe -} - -// Probe allows to run on-demand callbacks from different code locations. -// Pass the probe to a receiving and a sending end. The receiving end starts listening -// to requests with Receive and executes a callback when the sending end requests it -// by calling Probe. -type Probe struct { - request chan struct{} - result chan bool - ready bool -} - -// NewProbe returns a new initialized probe. -func NewProbe() *Probe { - return &Probe{ - request: make(chan struct{}), - result: make(chan bool), - } -} - -// Probe requests the callback to be run and returns a bool indicating success. -// It always returns true as long as the receiver is not ready. -func (p *Probe) Probe() bool { - if !p.ready { - return true - } - - p.request <- struct{}{} - return <-p.result -} - -// Receive starts listening for probe requests. On such a request it runs the supplied -// callback func which must return a bool indicating success. -// Blocks until the passed context is cancelled. -func (p *Probe) Receive(ctx context.Context, callback func() bool) { - p.ready = true - defer func() { - p.ready = false - }() - - for { - select { - case <-ctx.Done(): - return - case <-p.request: - p.result <- callback() - } - } -} diff --git a/client/server/server.go b/client/server/server.go index 638ede386..42420d1c1 100644 --- a/client/server/server.go +++ b/client/server/server.go @@ -63,12 +63,7 @@ type Server struct { statusRecorder *peer.Status sessionWatcher *internal.SessionWatcher - mgmProbe *internal.Probe - signalProbe *internal.Probe - relayProbe *internal.Probe - wgProbe *internal.Probe - lastProbe time.Time - + lastProbe time.Time persistNetworkMap bool } @@ -86,12 +81,7 @@ func New(ctx context.Context, configPath, logFile string) *Server { latestConfigInput: internal.ConfigInput{ ConfigPath: configPath, }, - logFile: logFile, - mgmProbe: internal.NewProbe(), - signalProbe: internal.NewProbe(), - relayProbe: internal.NewProbe(), - wgProbe: internal.NewProbe(), - + logFile: logFile, persistNetworkMap: true, } } @@ -202,14 +192,7 @@ func (s *Server) connectWithRetryRuns(ctx context.Context, config *internal.Conf s.connectClient = internal.NewConnectClient(ctx, config, statusRecorder) s.connectClient.SetNetworkMapPersistence(s.persistNetworkMap) - probes := internal.ProbeHolder{ - MgmProbe: s.mgmProbe, - SignalProbe: s.signalProbe, - RelayProbe: s.relayProbe, - WgProbe: s.wgProbe, - } - - err := s.connectClient.RunWithProbes(&probes, runningChan) + err := s.connectClient.Run(runningChan) if err != nil { log.Debugf("run client connection exited with error: %v. Will retry in the background", err) } @@ -676,9 +659,13 @@ func (s *Server) Down(ctx context.Context, _ *proto.DownRequest) (*proto.DownRes // Status returns the daemon status func (s *Server) Status( - _ context.Context, + ctx context.Context, msg *proto.StatusRequest, ) (*proto.StatusResponse, error) { + if ctx.Err() != nil { + return nil, ctx.Err() + } + s.mutex.Lock() defer s.mutex.Unlock() @@ -707,14 +694,17 @@ func (s *Server) Status( } func (s *Server) runProbes() { - if time.Since(s.lastProbe) > probeThreshold { - managementHealthy := s.mgmProbe.Probe() - signalHealthy := s.signalProbe.Probe() - relayHealthy := s.relayProbe.Probe() - wgProbe := s.wgProbe.Probe() + if s.connectClient == nil { + return + } - // Update last time only if all probes were successful - if managementHealthy && signalHealthy && relayHealthy && wgProbe { + engine := s.connectClient.Engine() + if engine == nil { + return + } + + if time.Since(s.lastProbe) > probeThreshold { + if engine.RunHealthProbes() { s.lastProbe = time.Now() } } From 46766e7e24e6ab1e111aad9b11c5969a8e5e170a Mon Sep 17 00:00:00 2001 From: Maycon Santos Date: Tue, 28 Jan 2025 22:48:19 +0100 Subject: [PATCH 37/92] [misc] Update sign pipeline version (#3246) --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 183cdb02c..8f267ebdd 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,7 +9,7 @@ on: pull_request: env: - SIGN_PIPE_VER: "v0.0.17" + SIGN_PIPE_VER: "v0.0.18" GORELEASER_VER: "v2.3.2" PRODUCT_NAME: "NetBird" COPYRIGHT: "Wiretrustee UG (haftungsbeschreankt)" From e20be2397c0c7ee83b36c09c04d5258b3a10e642 Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Tue, 28 Jan 2025 23:25:22 +0100 Subject: [PATCH 38/92] [client] Add missing peer ACL flush (#3247) --- client/firewall/nftables/acl_linux.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/client/firewall/nftables/acl_linux.go b/client/firewall/nftables/acl_linux.go index fc5cc6873..aff9e9188 100644 --- a/client/firewall/nftables/acl_linux.go +++ b/client/firewall/nftables/acl_linux.go @@ -348,6 +348,10 @@ func (m *AclManager) addIOFiltering( UserData: userData, }) + if err := m.rConn.Flush(); err != nil { + return nil, fmt.Errorf(flushError, err) + } + rule := &Rule{ nftRule: nftRule, mangleRule: m.createPreroutingRule(expressions, userData), @@ -359,6 +363,7 @@ func (m *AclManager) addIOFiltering( if ipset != nil { m.ipsetStore.AddReferenceToIpset(ipset.Name) } + return rule, nil } From 771c99a523569576d4377f0eb8819fa85dd5ace2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 29 Jan 2025 18:25:06 +0100 Subject: [PATCH 39/92] [clien]t Bump golang.org/x/net from 0.30.0 to 0.33.0 (#3218) Bumps [golang.org/x/net](https://github.com/golang/net) from 0.30.0 to 0.33.0. - [Commits](https://github.com/golang/net/compare/v0.30.0...v0.33.0) --- updated-dependencies: - dependency-name: golang.org/x/net dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 895ad5618..e65296a53 100644 --- a/go.mod +++ b/go.mod @@ -92,7 +92,7 @@ require ( goauthentik.io/api/v3 v3.2023051.3 golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 golang.org/x/mobile v0.0.0-20231127183840-76ac6878050a - golang.org/x/net v0.30.0 + golang.org/x/net v0.33.0 golang.org/x/oauth2 v0.19.0 golang.org/x/sync v0.10.0 golang.org/x/term v0.28.0 diff --git a/go.sum b/go.sum index 2aae595f0..e3670b99e 100644 --- a/go.sum +++ b/go.sum @@ -883,8 +883,8 @@ golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= -golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= -golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= +golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= From f930ef2ee608b26d6e924c26e5cfe3085404b739 Mon Sep 17 00:00:00 2001 From: Zoltan Papp Date: Mon, 3 Feb 2025 17:54:35 +0100 Subject: [PATCH 40/92] Cleanup magiconair usage from repo (#3276) --- client/internal/peer/conn_status_test.go | 3 ++- client/internal/peer/conn_test.go | 2 +- go.mod | 2 +- .../server/http/handlers/groups/groups_handler.go | 7 +++---- .../http/handlers/groups/groups_handler_test.go | 2 +- .../handlers/policies/policies_handler_test.go | 13 +++++-------- .../http/handlers/routes/routes_handler_test.go | 14 ++++++-------- 7 files changed, 19 insertions(+), 24 deletions(-) diff --git a/client/internal/peer/conn_status_test.go b/client/internal/peer/conn_status_test.go index 564fd41c1..6088df55d 100644 --- a/client/internal/peer/conn_status_test.go +++ b/client/internal/peer/conn_status_test.go @@ -1,8 +1,9 @@ package peer import ( - "github.com/magiconair/properties/assert" "testing" + + "github.com/stretchr/testify/assert" ) func TestConnStatus_String(t *testing.T) { diff --git a/client/internal/peer/conn_test.go b/client/internal/peer/conn_test.go index b3e9d5b60..505bedb7f 100644 --- a/client/internal/peer/conn_test.go +++ b/client/internal/peer/conn_test.go @@ -7,7 +7,7 @@ import ( "testing" "time" - "github.com/magiconair/properties/assert" + "github.com/stretchr/testify/assert" "github.com/netbirdio/netbird/client/iface" "github.com/netbirdio/netbird/client/internal/peer/guard" diff --git a/go.mod b/go.mod index e65296a53..13adeff09 100644 --- a/go.mod +++ b/go.mod @@ -54,7 +54,6 @@ require ( github.com/hashicorp/go-version v1.6.0 github.com/libdns/route53 v1.5.0 github.com/libp2p/go-netroute v0.2.1 - github.com/magiconair/properties v1.8.7 github.com/mattn/go-sqlite3 v1.14.22 github.com/mdlayher/socket v0.5.1 github.com/miekg/dns v1.1.59 @@ -185,6 +184,7 @@ require ( github.com/klauspost/cpuid/v2 v2.2.7 // indirect github.com/libdns/libdns v0.2.2 // indirect github.com/lufia/plan9stats v0.0.0-20240513124658-fba389f38bae // indirect + github.com/magiconair/properties v1.8.7 // indirect github.com/mdlayher/genetlink v1.3.2 // indirect github.com/mdlayher/netlink v1.7.2 // indirect github.com/mholt/acmez/v2 v2.0.1 // indirect diff --git a/management/server/http/handlers/groups/groups_handler.go b/management/server/http/handlers/groups/groups_handler.go index b7121c234..ec635a358 100644 --- a/management/server/http/handlers/groups/groups_handler.go +++ b/management/server/http/handlers/groups/groups_handler.go @@ -7,15 +7,14 @@ import ( "github.com/gorilla/mux" log "github.com/sirupsen/logrus" - "github.com/netbirdio/netbird/management/server/http/configs" - nbpeer "github.com/netbirdio/netbird/management/server/peer" - "github.com/netbirdio/netbird/management/server/types" - "github.com/netbirdio/netbird/management/server" "github.com/netbirdio/netbird/management/server/http/api" + "github.com/netbirdio/netbird/management/server/http/configs" "github.com/netbirdio/netbird/management/server/http/util" "github.com/netbirdio/netbird/management/server/jwtclaims" + nbpeer "github.com/netbirdio/netbird/management/server/peer" "github.com/netbirdio/netbird/management/server/status" + "github.com/netbirdio/netbird/management/server/types" ) // handler is a handler that returns groups of the account diff --git a/management/server/http/handlers/groups/groups_handler_test.go b/management/server/http/handlers/groups/groups_handler_test.go index 96e381da1..0668982f3 100644 --- a/management/server/http/handlers/groups/groups_handler_test.go +++ b/management/server/http/handlers/groups/groups_handler_test.go @@ -14,7 +14,7 @@ import ( "testing" "github.com/gorilla/mux" - "github.com/magiconair/properties/assert" + "github.com/stretchr/testify/assert" "golang.org/x/exp/maps" "github.com/netbirdio/netbird/management/server" diff --git a/management/server/http/handlers/policies/policies_handler_test.go b/management/server/http/handlers/policies/policies_handler_test.go index 3e1be187c..8fbf84d4b 100644 --- a/management/server/http/handlers/policies/policies_handler_test.go +++ b/management/server/http/handlers/policies/policies_handler_test.go @@ -10,17 +10,14 @@ import ( "strings" "testing" + "github.com/gorilla/mux" + "github.com/stretchr/testify/assert" + "github.com/netbirdio/netbird/management/server/http/api" + "github.com/netbirdio/netbird/management/server/jwtclaims" + "github.com/netbirdio/netbird/management/server/mock_server" "github.com/netbirdio/netbird/management/server/status" "github.com/netbirdio/netbird/management/server/types" - - "github.com/gorilla/mux" - - "github.com/netbirdio/netbird/management/server/jwtclaims" - - "github.com/magiconair/properties/assert" - - "github.com/netbirdio/netbird/management/server/mock_server" ) func initPoliciesTestData(policies ...*types.Policy) *handler { diff --git a/management/server/http/handlers/routes/routes_handler_test.go b/management/server/http/handlers/routes/routes_handler_test.go index 45c465587..4064ec361 100644 --- a/management/server/http/handlers/routes/routes_handler_test.go +++ b/management/server/http/handlers/routes/routes_handler_test.go @@ -11,21 +11,19 @@ import ( "net/netip" "testing" - "github.com/netbirdio/netbird/management/server/util" + "github.com/gorilla/mux" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/netbirdio/netbird/management/domain" "github.com/netbirdio/netbird/management/server/http/api" + "github.com/netbirdio/netbird/management/server/jwtclaims" + "github.com/netbirdio/netbird/management/server/mock_server" nbpeer "github.com/netbirdio/netbird/management/server/peer" "github.com/netbirdio/netbird/management/server/status" "github.com/netbirdio/netbird/management/server/types" + "github.com/netbirdio/netbird/management/server/util" "github.com/netbirdio/netbird/route" - - "github.com/gorilla/mux" - "github.com/magiconair/properties/assert" - - "github.com/netbirdio/netbird/management/domain" - "github.com/netbirdio/netbird/management/server/jwtclaims" - "github.com/netbirdio/netbird/management/server/mock_server" ) const ( From 7d385b8dc327bcd3ba67b300ad64e937dd49a251 Mon Sep 17 00:00:00 2001 From: "M. Essam" Date: Tue, 4 Feb 2025 12:10:10 +0200 Subject: [PATCH 41/92] [management] REST client package (#3278) --- management/client/rest/accounts.go | 54 ++ management/client/rest/accounts_test.go | 169 ++++++ management/client/rest/client.go | 133 +++++ management/client/rest/client_test.go | 30 + management/client/rest/dns.go | 110 ++++ management/client/rest/dns_test.go | 295 ++++++++++ management/client/rest/events.go | 24 + management/client/rest/events_test.go | 65 ++ management/client/rest/geo.go | 36 ++ management/client/rest/geo_test.go | 96 +++ management/client/rest/groups.go | 82 +++ management/client/rest/groups_test.go | 210 +++++++ management/client/rest/networks.go | 246 ++++++++ management/client/rest/networks_test.go | 589 +++++++++++++++++++ management/client/rest/peers.go | 78 +++ management/client/rest/peers_test.go | 203 +++++++ management/client/rest/policies.go | 82 +++ management/client/rest/policies_test.go | 236 ++++++++ management/client/rest/posturechecks.go | 82 +++ management/client/rest/posturechecks_test.go | 228 +++++++ management/client/rest/routes.go | 82 +++ management/client/rest/routes_test.go | 226 +++++++ management/client/rest/setupkeys.go | 82 +++ management/client/rest/setupkeys_test.go | 227 +++++++ management/client/rest/tokens.go | 66 +++ management/client/rest/tokens_test.go | 175 ++++++ management/client/rest/users.go | 82 +++ management/client/rest/users_test.go | 222 +++++++ management/server/testdata/store.sql | 5 + 29 files changed, 4215 insertions(+) create mode 100644 management/client/rest/accounts.go create mode 100644 management/client/rest/accounts_test.go create mode 100644 management/client/rest/client.go create mode 100644 management/client/rest/client_test.go create mode 100644 management/client/rest/dns.go create mode 100644 management/client/rest/dns_test.go create mode 100644 management/client/rest/events.go create mode 100644 management/client/rest/events_test.go create mode 100644 management/client/rest/geo.go create mode 100644 management/client/rest/geo_test.go create mode 100644 management/client/rest/groups.go create mode 100644 management/client/rest/groups_test.go create mode 100644 management/client/rest/networks.go create mode 100644 management/client/rest/networks_test.go create mode 100644 management/client/rest/peers.go create mode 100644 management/client/rest/peers_test.go create mode 100644 management/client/rest/policies.go create mode 100644 management/client/rest/policies_test.go create mode 100644 management/client/rest/posturechecks.go create mode 100644 management/client/rest/posturechecks_test.go create mode 100644 management/client/rest/routes.go create mode 100644 management/client/rest/routes_test.go create mode 100644 management/client/rest/setupkeys.go create mode 100644 management/client/rest/setupkeys_test.go create mode 100644 management/client/rest/tokens.go create mode 100644 management/client/rest/tokens_test.go create mode 100644 management/client/rest/users.go create mode 100644 management/client/rest/users_test.go diff --git a/management/client/rest/accounts.go b/management/client/rest/accounts.go new file mode 100644 index 000000000..f38b19f70 --- /dev/null +++ b/management/client/rest/accounts.go @@ -0,0 +1,54 @@ +package rest + +import ( + "bytes" + "context" + "encoding/json" + + "github.com/netbirdio/netbird/management/server/http/api" +) + +// AccountsAPI APIs for accounts, do not use directly +type AccountsAPI struct { + c *Client +} + +// List list all accounts, only returns one account always +// See more: https://docs.netbird.io/api/resources/accounts#list-all-accounts +func (a *AccountsAPI) List(ctx context.Context) ([]api.Account, error) { + resp, err := a.c.newRequest(ctx, "GET", "/api/accounts", nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[[]api.Account](resp) + return ret, err +} + +// Update update account settings +// See more: https://docs.netbird.io/api/resources/accounts#update-an-account +func (a *AccountsAPI) Update(ctx context.Context, accountID string, request api.PutApiAccountsAccountIdJSONRequestBody) (*api.Account, error) { + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + resp, err := a.c.newRequest(ctx, "PUT", "/api/accounts/"+accountID, bytes.NewReader(requestBytes)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.Account](resp) + return &ret, err +} + +// Delete delete account +// See more: https://docs.netbird.io/api/resources/accounts#delete-an-account +func (a *AccountsAPI) Delete(ctx context.Context, accountID string) error { + resp, err := a.c.newRequest(ctx, "DELETE", "/api/accounts/"+accountID, nil) + if err != nil { + return err + } + defer resp.Body.Close() + + return nil +} diff --git a/management/client/rest/accounts_test.go b/management/client/rest/accounts_test.go new file mode 100644 index 000000000..3c1925fbc --- /dev/null +++ b/management/client/rest/accounts_test.go @@ -0,0 +1,169 @@ +package rest + +import ( + "context" + "encoding/json" + "io" + "net/http" + "testing" + + "github.com/netbirdio/netbird/management/server/http/api" + "github.com/netbirdio/netbird/management/server/http/util" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var ( + testAccount = api.Account{ + Id: "Test", + Settings: api.AccountSettings{ + Extra: &api.AccountExtraSettings{ + PeerApprovalEnabled: ptr(false), + }, + GroupsPropagationEnabled: ptr(true), + JwtGroupsEnabled: ptr(false), + PeerInactivityExpiration: 7, + PeerInactivityExpirationEnabled: true, + PeerLoginExpiration: 24, + PeerLoginExpirationEnabled: true, + RegularUsersViewBlocked: false, + RoutingPeerDnsResolutionEnabled: ptr(false), + }, + } +) + +func TestAccounts_List_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/accounts", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal([]api.Account{testAccount}) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Accounts.List(context.Background()) + require.NoError(t, err) + assert.Len(t, ret, 1) + assert.Equal(t, testAccount, ret[0]) + }) +} + +func TestAccounts_List_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/accounts", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Accounts.List(context.Background()) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} + +func TestAccounts_Update_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/accounts/Test", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "PUT", r.Method) + reqBytes, err := io.ReadAll(r.Body) + require.NoError(t, err) + var req api.PutApiAccountsAccountIdJSONRequestBody + err = json.Unmarshal(reqBytes, &req) + require.NoError(t, err) + assert.Equal(t, true, *req.Settings.RoutingPeerDnsResolutionEnabled) + retBytes, _ := json.Marshal(testAccount) + _, err = w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Accounts.Update(context.Background(), "Test", api.PutApiAccountsAccountIdJSONRequestBody{ + Settings: api.AccountSettings{ + RoutingPeerDnsResolutionEnabled: ptr(true), + }, + }) + require.NoError(t, err) + assert.Equal(t, testAccount, *ret) + }) + +} + +func TestAccounts_Update_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/accounts/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Accounts.Update(context.Background(), "Test", api.PutApiAccountsAccountIdJSONRequestBody{ + Settings: api.AccountSettings{ + RoutingPeerDnsResolutionEnabled: ptr(true), + }, + }) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Nil(t, ret) + }) +} + +func TestAccounts_Delete_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/accounts/Test", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "DELETE", r.Method) + w.WriteHeader(200) + }) + err := c.Accounts.Delete(context.Background(), "Test") + require.NoError(t, err) + }) +} + +func TestAccounts_Delete_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/accounts/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404}) + w.WriteHeader(404) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + err := c.Accounts.Delete(context.Background(), "Test") + assert.Error(t, err) + assert.Equal(t, "Not found", err.Error()) + }) +} + +func TestAccounts_Integration_List(t *testing.T) { + withBlackBoxServer(t, func(c *Client) { + accounts, err := c.Accounts.List(context.Background()) + require.NoError(t, err) + assert.Len(t, accounts, 1) + assert.Equal(t, "bf1c8084-ba50-4ce7-9439-34653001fc3b", accounts[0].Id) + assert.Equal(t, false, *accounts[0].Settings.Extra.PeerApprovalEnabled) + }) +} + +func TestAccounts_Integration_Update(t *testing.T) { + withBlackBoxServer(t, func(c *Client) { + accounts, err := c.Accounts.List(context.Background()) + require.NoError(t, err) + assert.Len(t, accounts, 1) + accounts[0].Settings.JwtAllowGroups = ptr([]string{"test"}) + account, err := c.Accounts.Update(context.Background(), accounts[0].Id, api.AccountRequest{ + Settings: accounts[0].Settings, + }) + require.NoError(t, err) + assert.Equal(t, accounts[0].Id, account.Id) + assert.Equal(t, []string{"test"}, *account.Settings.JwtAllowGroups) + }) +} + +// Account deletion on MySQL and PostgreSQL databases causes unknown errors +// func TestAccounts_Integration_Delete(t *testing.T) { +// withBlackBoxServer(t, func(c *Client) { +// accounts, err := c.Accounts.List(context.Background()) +// require.NoError(t, err) +// assert.Len(t, accounts, 1) +// err = c.Accounts.Delete(context.Background(), accounts[0].Id) +// require.NoError(t, err) +// _, err = c.Accounts.List(context.Background()) +// assert.Error(t, err) +// }) +// } diff --git a/management/client/rest/client.go b/management/client/rest/client.go new file mode 100644 index 000000000..f55e2d11e --- /dev/null +++ b/management/client/rest/client.go @@ -0,0 +1,133 @@ +package rest + +import ( + "context" + "encoding/json" + "errors" + "io" + "net/http" + + "github.com/netbirdio/netbird/management/server/http/util" +) + +// Client Management service HTTP REST API Client +type Client struct { + managementURL string + authHeader string + + // Accounts NetBird account APIs + // see more: https://docs.netbird.io/api/resources/accounts + Accounts *AccountsAPI + + // Users NetBird users APIs + // see more: https://docs.netbird.io/api/resources/users + Users *UsersAPI + + // Tokens NetBird tokens APIs + // see more: https://docs.netbird.io/api/resources/tokens + Tokens *TokensAPI + + // Peers NetBird peers APIs + // see more: https://docs.netbird.io/api/resources/peers + Peers *PeersAPI + + // SetupKeys NetBird setup keys APIs + // see more: https://docs.netbird.io/api/resources/setup-keys + SetupKeys *SetupKeysAPI + + // Groups NetBird groups APIs + // see more: https://docs.netbird.io/api/resources/groups + Groups *GroupsAPI + + // Policies NetBird policies APIs + // see more: https://docs.netbird.io/api/resources/policies + Policies *PoliciesAPI + + // PostureChecks NetBird posture checks APIs + // see more: https://docs.netbird.io/api/resources/posture-checks + PostureChecks *PostureChecksAPI + + // Networks NetBird networks APIs + // see more: https://docs.netbird.io/api/resources/networks + Networks *NetworksAPI + + // Routes NetBird routes APIs + // see more: https://docs.netbird.io/api/resources/routes + Routes *RoutesAPI + + // DNS NetBird DNS APIs + // see more: https://docs.netbird.io/api/resources/routes + DNS *DNSAPI + + // GeoLocation NetBird Geo Location APIs + // see more: https://docs.netbird.io/api/resources/geo-locations + GeoLocation *GeoLocationAPI + + // Events NetBird Events APIs + // see more: https://docs.netbird.io/api/resources/events + Events *EventsAPI +} + +// New initialize new Client instance +func New(managementURL, token string) *Client { + client := &Client{ + managementURL: managementURL, + authHeader: "Token " + token, + } + client.Accounts = &AccountsAPI{client} + client.Users = &UsersAPI{client} + client.Tokens = &TokensAPI{client} + client.Peers = &PeersAPI{client} + client.SetupKeys = &SetupKeysAPI{client} + client.Groups = &GroupsAPI{client} + client.Policies = &PoliciesAPI{client} + client.PostureChecks = &PostureChecksAPI{client} + client.Networks = &NetworksAPI{client} + client.Routes = &RoutesAPI{client} + client.DNS = &DNSAPI{client} + client.GeoLocation = &GeoLocationAPI{client} + client.Events = &EventsAPI{client} + return client +} + +func (c *Client) newRequest(ctx context.Context, method, path string, body io.Reader) (*http.Response, error) { + req, err := http.NewRequestWithContext(ctx, method, c.managementURL+path, body) + if err != nil { + return nil, err + } + + req.Header.Add("Authorization", c.authHeader) + req.Header.Add("Accept", "application/json") + if body != nil { + req.Header.Add("Content-Type", "application/json") + } + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, err + } + + if resp.StatusCode > 299 { + parsedErr, pErr := parseResponse[util.ErrorResponse](resp) + if pErr != nil { + return nil, err + } + return nil, errors.New(parsedErr.Message) + } + + return resp, nil +} + +func parseResponse[T any](resp *http.Response) (T, error) { + var ret T + if resp.Body == nil { + return ret, errors.New("No body") + } + bs, err := io.ReadAll(resp.Body) + if err != nil { + return ret, err + } + err = json.Unmarshal(bs, &ret) + + return ret, err +} diff --git a/management/client/rest/client_test.go b/management/client/rest/client_test.go new file mode 100644 index 000000000..a42b12fa3 --- /dev/null +++ b/management/client/rest/client_test.go @@ -0,0 +1,30 @@ +package rest + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/netbirdio/netbird/management/server/http/testing/testing_tools" +) + +func withMockClient(callback func(*Client, *http.ServeMux)) { + mux := &http.ServeMux{} + server := httptest.NewServer(mux) + defer server.Close() + c := New(server.URL, "ABC") + callback(c, mux) +} + +func ptr[T any, PT *T](x T) PT { + return &x +} + +func withBlackBoxServer(t *testing.T, callback func(*Client)) { + t.Helper() + handler, _, _ := testing_tools.BuildApiBlackBoxWithDBState(t, "../../server/testdata/store.sql", nil, false) + server := httptest.NewServer(handler) + defer server.Close() + c := New(server.URL, "nbp_apTmlmUXHSC4PKmHwtIZNaGr8eqcVI2gMURp") + callback(c) +} diff --git a/management/client/rest/dns.go b/management/client/rest/dns.go new file mode 100644 index 000000000..ef9923b1f --- /dev/null +++ b/management/client/rest/dns.go @@ -0,0 +1,110 @@ +package rest + +import ( + "bytes" + "context" + "encoding/json" + + "github.com/netbirdio/netbird/management/server/http/api" +) + +// DNSAPI APIs for DNS Management, do not use directly +type DNSAPI struct { + c *Client +} + +// ListNameserverGroups list all nameserver groups +// See more: https://docs.netbird.io/api/resources/dns#list-all-nameserver-groups +func (a *DNSAPI) ListNameserverGroups(ctx context.Context) ([]api.NameserverGroup, error) { + resp, err := a.c.newRequest(ctx, "GET", "/api/dns/nameservers", nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[[]api.NameserverGroup](resp) + return ret, err +} + +// GetNameserverGroup get nameserver group info +// See more: https://docs.netbird.io/api/resources/dns#retrieve-a-nameserver-group +func (a *DNSAPI) GetNameserverGroup(ctx context.Context, nameserverGroupID string) (*api.NameserverGroup, error) { + resp, err := a.c.newRequest(ctx, "GET", "/api/dns/nameservers/"+nameserverGroupID, nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.NameserverGroup](resp) + return &ret, err +} + +// CreateNameserverGroup create new nameserver group +// See more: https://docs.netbird.io/api/resources/dns#create-a-nameserver-group +func (a *DNSAPI) CreateNameserverGroup(ctx context.Context, request api.PostApiDnsNameserversJSONRequestBody) (*api.NameserverGroup, error) { + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + resp, err := a.c.newRequest(ctx, "POST", "/api/dns/nameservers", bytes.NewReader(requestBytes)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.NameserverGroup](resp) + return &ret, err +} + +// UpdateNameserverGroup update nameserver group info +// See more: https://docs.netbird.io/api/resources/dns#update-a-nameserver-group +func (a *DNSAPI) UpdateNameserverGroup(ctx context.Context, nameserverGroupID string, request api.PutApiDnsNameserversNsgroupIdJSONRequestBody) (*api.NameserverGroup, error) { + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + resp, err := a.c.newRequest(ctx, "PUT", "/api/dns/nameservers/"+nameserverGroupID, bytes.NewReader(requestBytes)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.NameserverGroup](resp) + return &ret, err +} + +// DeleteNameserverGroup delete nameserver group +// See more: https://docs.netbird.io/api/resources/dns#delete-a-nameserver-group +func (a *DNSAPI) DeleteNameserverGroup(ctx context.Context, nameserverGroupID string) error { + resp, err := a.c.newRequest(ctx, "DELETE", "/api/dns/nameservers/"+nameserverGroupID, nil) + if err != nil { + return err + } + defer resp.Body.Close() + + return nil +} + +// GetSettings get DNS settings +// See more: https://docs.netbird.io/api/resources/dns#retrieve-dns-settings +func (a *DNSAPI) GetSettings(ctx context.Context) (*api.DNSSettings, error) { + resp, err := a.c.newRequest(ctx, "GET", "/api/dns/settings", nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.DNSSettings](resp) + return &ret, err +} + +// UpdateSettings update DNS settings +// See more: https://docs.netbird.io/api/resources/dns#update-dns-settings +func (a *DNSAPI) UpdateSettings(ctx context.Context, request api.PutApiDnsSettingsJSONRequestBody) (*api.DNSSettings, error) { + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + resp, err := a.c.newRequest(ctx, "PUT", "/api/dns/settings", bytes.NewReader(requestBytes)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.DNSSettings](resp) + return &ret, err +} diff --git a/management/client/rest/dns_test.go b/management/client/rest/dns_test.go new file mode 100644 index 000000000..d2c00549c --- /dev/null +++ b/management/client/rest/dns_test.go @@ -0,0 +1,295 @@ +package rest + +import ( + "context" + "encoding/json" + "io" + "net/http" + "testing" + + "github.com/netbirdio/netbird/management/server/http/api" + "github.com/netbirdio/netbird/management/server/http/util" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var ( + testNameserverGroup = api.NameserverGroup{ + Id: "Test", + Name: "wow", + } + + testSettings = api.DNSSettings{ + DisabledManagementGroups: []string{"gone"}, + } +) + +func TestDNSNameserverGroup_List_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/dns/nameservers", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal([]api.NameserverGroup{testNameserverGroup}) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.DNS.ListNameserverGroups(context.Background()) + require.NoError(t, err) + assert.Len(t, ret, 1) + assert.Equal(t, testNameserverGroup, ret[0]) + }) +} + +func TestDNSNameserverGroup_List_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/dns/nameservers", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.DNS.ListNameserverGroups(context.Background()) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} + +func TestDNSNameserverGroup_Get_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/dns/nameservers/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(testNameserverGroup) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.DNS.GetNameserverGroup(context.Background(), "Test") + require.NoError(t, err) + assert.Equal(t, testNameserverGroup, *ret) + }) +} + +func TestDNSNameserverGroup_Get_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/dns/nameservers/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.DNS.GetNameserverGroup(context.Background(), "Test") + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} + +func TestDNSNameserverGroup_Create_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/dns/nameservers", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "POST", r.Method) + reqBytes, err := io.ReadAll(r.Body) + require.NoError(t, err) + var req api.PostApiDnsNameserversJSONRequestBody + err = json.Unmarshal(reqBytes, &req) + require.NoError(t, err) + assert.Equal(t, "weaw", req.Name) + retBytes, _ := json.Marshal(testNameserverGroup) + _, err = w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.DNS.CreateNameserverGroup(context.Background(), api.PostApiDnsNameserversJSONRequestBody{ + Name: "weaw", + }) + require.NoError(t, err) + assert.Equal(t, testNameserverGroup, *ret) + }) +} + +func TestDNSNameserverGroup_Create_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/dns/nameservers", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.DNS.CreateNameserverGroup(context.Background(), api.PostApiDnsNameserversJSONRequestBody{ + Name: "weaw", + }) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Nil(t, ret) + }) +} + +func TestDNSNameserverGroup_Update_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/dns/nameservers/Test", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "PUT", r.Method) + reqBytes, err := io.ReadAll(r.Body) + require.NoError(t, err) + var req api.PutApiDnsNameserversNsgroupIdJSONRequestBody + err = json.Unmarshal(reqBytes, &req) + require.NoError(t, err) + assert.Equal(t, "weaw", req.Name) + retBytes, _ := json.Marshal(testNameserverGroup) + _, err = w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.DNS.UpdateNameserverGroup(context.Background(), "Test", api.PutApiDnsNameserversNsgroupIdJSONRequestBody{ + Name: "weaw", + }) + require.NoError(t, err) + assert.Equal(t, testNameserverGroup, *ret) + }) +} + +func TestDNSNameserverGroup_Update_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/dns/nameservers/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.DNS.UpdateNameserverGroup(context.Background(), "Test", api.PutApiDnsNameserversNsgroupIdJSONRequestBody{ + Name: "weaw", + }) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Nil(t, ret) + }) +} + +func TestDNSNameserverGroup_Delete_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/dns/nameservers/Test", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "DELETE", r.Method) + w.WriteHeader(200) + }) + err := c.DNS.DeleteNameserverGroup(context.Background(), "Test") + require.NoError(t, err) + }) +} + +func TestDNSNameserverGroup_Delete_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/dns/nameservers/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404}) + w.WriteHeader(404) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + err := c.DNS.DeleteNameserverGroup(context.Background(), "Test") + assert.Error(t, err) + assert.Equal(t, "Not found", err.Error()) + }) +} + +func TestDNSSettings_Get_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/dns/settings", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(testSettings) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.DNS.GetSettings(context.Background()) + require.NoError(t, err) + assert.Equal(t, testSettings, *ret) + }) +} + +func TestDNSSettings_Get_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/dns/settings", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.DNS.GetSettings(context.Background()) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} + +func TestDNSSettings_Update_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/dns/settings", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "PUT", r.Method) + reqBytes, err := io.ReadAll(r.Body) + require.NoError(t, err) + var req api.PutApiDnsSettingsJSONRequestBody + err = json.Unmarshal(reqBytes, &req) + require.NoError(t, err) + assert.Equal(t, []string{"test"}, req.DisabledManagementGroups) + retBytes, _ := json.Marshal(testSettings) + _, err = w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.DNS.UpdateSettings(context.Background(), api.PutApiDnsSettingsJSONRequestBody{ + DisabledManagementGroups: []string{"test"}, + }) + require.NoError(t, err) + assert.Equal(t, testSettings, *ret) + }) +} + +func TestDNSSettings_Update_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/dns/settings", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.DNS.UpdateSettings(context.Background(), api.PutApiDnsSettingsJSONRequestBody{ + DisabledManagementGroups: []string{"test"}, + }) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Nil(t, ret) + }) +} + +func TestDNS_Integration(t *testing.T) { + nsGroupReq := api.NameserverGroupRequest{ + Description: "Test", + Enabled: true, + Groups: []string{"cs1tnh0hhcjnqoiuebeg"}, + Name: "test", + Nameservers: []api.Nameserver{ + { + Ip: "8.8.8.8", + NsType: api.NameserverNsTypeUdp, + Port: 53, + }, + }, + Primary: true, + SearchDomainsEnabled: false, + } + withBlackBoxServer(t, func(c *Client) { + // Create + nsGroup, err := c.DNS.CreateNameserverGroup(context.Background(), nsGroupReq) + require.NoError(t, err) + + // List + nsGroups, err := c.DNS.ListNameserverGroups(context.Background()) + require.NoError(t, err) + assert.Equal(t, *nsGroup, nsGroups[0]) + + // Update + nsGroupReq.Description = "TestUpdate" + nsGroup, err = c.DNS.UpdateNameserverGroup(context.Background(), nsGroup.Id, nsGroupReq) + require.NoError(t, err) + assert.Equal(t, "TestUpdate", nsGroup.Description) + + // Delete + err = c.DNS.DeleteNameserverGroup(context.Background(), nsGroup.Id) + require.NoError(t, err) + + // List again to ensure deletion + nsGroups, err = c.DNS.ListNameserverGroups(context.Background()) + require.NoError(t, err) + assert.Len(t, nsGroups, 0) + }) +} diff --git a/management/client/rest/events.go b/management/client/rest/events.go new file mode 100644 index 000000000..1157700ff --- /dev/null +++ b/management/client/rest/events.go @@ -0,0 +1,24 @@ +package rest + +import ( + "context" + + "github.com/netbirdio/netbird/management/server/http/api" +) + +// EventsAPI APIs for Events, do not use directly +type EventsAPI struct { + c *Client +} + +// List list all events +// See more: https://docs.netbird.io/api/resources/events#list-all-events +func (a *EventsAPI) List(ctx context.Context) ([]api.Event, error) { + resp, err := a.c.newRequest(ctx, "GET", "/api/events", nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[[]api.Event](resp) + return ret, err +} diff --git a/management/client/rest/events_test.go b/management/client/rest/events_test.go new file mode 100644 index 000000000..515c227e6 --- /dev/null +++ b/management/client/rest/events_test.go @@ -0,0 +1,65 @@ +package rest + +import ( + "context" + "encoding/json" + "net/http" + "testing" + + "github.com/netbirdio/netbird/management/server/http/api" + "github.com/netbirdio/netbird/management/server/http/util" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var ( + testEvent = api.Event{ + Activity: "AccountCreate", + ActivityCode: api.EventActivityCodeAccountCreate, + } +) + +func TestEvents_List_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/events", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal([]api.Event{testEvent}) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Events.List(context.Background()) + require.NoError(t, err) + assert.Len(t, ret, 1) + assert.Equal(t, testEvent, ret[0]) + }) +} + +func TestEvents_List_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/events", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Events.List(context.Background()) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} + +func TestEvents_Integration(t *testing.T) { + withBlackBoxServer(t, func(c *Client) { + // Do something that would trigger any event + _, err := c.SetupKeys.Create(context.Background(), api.CreateSetupKeyRequest{ + Ephemeral: ptr(true), + Name: "TestSetupKey", + Type: "reusable", + }) + require.NoError(t, err) + + events, err := c.Events.List(context.Background()) + require.NoError(t, err) + assert.NotEmpty(t, events) + }) +} diff --git a/management/client/rest/geo.go b/management/client/rest/geo.go new file mode 100644 index 000000000..ed9090fe2 --- /dev/null +++ b/management/client/rest/geo.go @@ -0,0 +1,36 @@ +package rest + +import ( + "context" + + "github.com/netbirdio/netbird/management/server/http/api" +) + +// GeoLocationAPI APIs for Geo-Location, do not use directly +type GeoLocationAPI struct { + c *Client +} + +// ListCountries list all country codes +// See more: https://docs.netbird.io/api/resources/geo-locations#list-all-country-codes +func (a *GeoLocationAPI) ListCountries(ctx context.Context) ([]api.Country, error) { + resp, err := a.c.newRequest(ctx, "GET", "/api/locations/countries", nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[[]api.Country](resp) + return ret, err +} + +// ListCountryCities Get a list of all English city names for a given country code +// See more: https://docs.netbird.io/api/resources/geo-locations#list-all-city-names-by-country +func (a *GeoLocationAPI) ListCountryCities(ctx context.Context, countryCode string) ([]api.City, error) { + resp, err := a.c.newRequest(ctx, "GET", "/api/locations/countries/"+countryCode+"/cities", nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[[]api.City](resp) + return ret, err +} diff --git a/management/client/rest/geo_test.go b/management/client/rest/geo_test.go new file mode 100644 index 000000000..dd42ecba8 --- /dev/null +++ b/management/client/rest/geo_test.go @@ -0,0 +1,96 @@ +package rest + +import ( + "context" + "encoding/json" + "net/http" + "testing" + + "github.com/netbirdio/netbird/management/server/http/api" + "github.com/netbirdio/netbird/management/server/http/util" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var ( + testCountry = api.Country{ + CountryCode: "DE", + CountryName: "Germany", + } + + testCity = api.City{ + CityName: "Berlin", + GeonameId: 2950158, + } +) + +func TestGeo_ListCountries_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/locations/countries", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal([]api.Country{testCountry}) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.GeoLocation.ListCountries(context.Background()) + require.NoError(t, err) + assert.Len(t, ret, 1) + assert.Equal(t, testCountry, ret[0]) + }) +} + +func TestGeo_ListCountries_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/locations/countries", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.GeoLocation.ListCountries(context.Background()) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} + +func TestGeo_ListCountryCities_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/locations/countries/Test/cities", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal([]api.City{testCity}) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.GeoLocation.ListCountryCities(context.Background(), "Test") + require.NoError(t, err) + assert.Len(t, ret, 1) + assert.Equal(t, testCity, ret[0]) + }) +} + +func TestGeo_ListCountryCities_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/locations/countries/Test/cities", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.GeoLocation.ListCountryCities(context.Background(), "Test") + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} + +func TestGeo_Integration(t *testing.T) { + // Blackbox is initialized with empty GeoLocations + withBlackBoxServer(t, func(c *Client) { + countries, err := c.GeoLocation.ListCountries(context.Background()) + require.NoError(t, err) + assert.Empty(t, countries) + + cities, err := c.GeoLocation.ListCountryCities(context.Background(), "DE") + require.NoError(t, err) + assert.Empty(t, cities) + }) +} diff --git a/management/client/rest/groups.go b/management/client/rest/groups.go new file mode 100644 index 000000000..feb664273 --- /dev/null +++ b/management/client/rest/groups.go @@ -0,0 +1,82 @@ +package rest + +import ( + "bytes" + "context" + "encoding/json" + + "github.com/netbirdio/netbird/management/server/http/api" +) + +// GroupsAPI APIs for Groups, do not use directly +type GroupsAPI struct { + c *Client +} + +// List list all groups +// See more: https://docs.netbird.io/api/resources/groups#list-all-groups +func (a *GroupsAPI) List(ctx context.Context) ([]api.Group, error) { + resp, err := a.c.newRequest(ctx, "GET", "/api/groups", nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[[]api.Group](resp) + return ret, err +} + +// Get get group info +// See more: https://docs.netbird.io/api/resources/groups#retrieve-a-group +func (a *GroupsAPI) Get(ctx context.Context, groupID string) (*api.Group, error) { + resp, err := a.c.newRequest(ctx, "GET", "/api/groups/"+groupID, nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.Group](resp) + return &ret, err +} + +// Create create new group +// See more: https://docs.netbird.io/api/resources/groups#create-a-group +func (a *GroupsAPI) Create(ctx context.Context, request api.PostApiGroupsJSONRequestBody) (*api.Group, error) { + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + resp, err := a.c.newRequest(ctx, "POST", "/api/groups", bytes.NewReader(requestBytes)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.Group](resp) + return &ret, err +} + +// Update update group info +// See more: https://docs.netbird.io/api/resources/groups#update-a-group +func (a *GroupsAPI) Update(ctx context.Context, groupID string, request api.PutApiGroupsGroupIdJSONRequestBody) (*api.Group, error) { + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + resp, err := a.c.newRequest(ctx, "PUT", "/api/groups/"+groupID, bytes.NewReader(requestBytes)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.Group](resp) + return &ret, err +} + +// Delete delete group +// See more: https://docs.netbird.io/api/resources/groups#delete-a-group +func (a *GroupsAPI) Delete(ctx context.Context, groupID string) error { + resp, err := a.c.newRequest(ctx, "DELETE", "/api/groups/"+groupID, nil) + if err != nil { + return err + } + defer resp.Body.Close() + + return nil +} diff --git a/management/client/rest/groups_test.go b/management/client/rest/groups_test.go new file mode 100644 index 000000000..ac534437d --- /dev/null +++ b/management/client/rest/groups_test.go @@ -0,0 +1,210 @@ +package rest + +import ( + "context" + "encoding/json" + "io" + "net/http" + "testing" + + "github.com/netbirdio/netbird/management/server/http/api" + "github.com/netbirdio/netbird/management/server/http/util" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var ( + testGroup = api.Group{ + Id: "Test", + Name: "wow", + PeersCount: 0, + } +) + +func TestGroups_List_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/groups", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal([]api.Group{testGroup}) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Groups.List(context.Background()) + require.NoError(t, err) + assert.Len(t, ret, 1) + assert.Equal(t, testGroup, ret[0]) + }) +} + +func TestGroups_List_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/groups", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Groups.List(context.Background()) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} + +func TestGroups_Get_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/groups/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(testGroup) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Groups.Get(context.Background(), "Test") + require.NoError(t, err) + assert.Equal(t, testGroup, *ret) + }) +} + +func TestGroups_Get_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/groups/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Groups.Get(context.Background(), "Test") + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} + +func TestGroups_Create_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/groups", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "POST", r.Method) + reqBytes, err := io.ReadAll(r.Body) + require.NoError(t, err) + var req api.PostApiGroupsJSONRequestBody + err = json.Unmarshal(reqBytes, &req) + require.NoError(t, err) + assert.Equal(t, "weaw", req.Name) + retBytes, _ := json.Marshal(testGroup) + _, err = w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Groups.Create(context.Background(), api.PostApiGroupsJSONRequestBody{ + Name: "weaw", + }) + require.NoError(t, err) + assert.Equal(t, testGroup, *ret) + }) +} + +func TestGroups_Create_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/groups", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Groups.Create(context.Background(), api.PostApiGroupsJSONRequestBody{ + Name: "weaw", + }) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Nil(t, ret) + }) +} + +func TestGroups_Update_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/groups/Test", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "PUT", r.Method) + reqBytes, err := io.ReadAll(r.Body) + require.NoError(t, err) + var req api.PutApiGroupsGroupIdJSONRequestBody + err = json.Unmarshal(reqBytes, &req) + require.NoError(t, err) + assert.Equal(t, "weaw", req.Name) + retBytes, _ := json.Marshal(testGroup) + _, err = w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Groups.Update(context.Background(), "Test", api.PutApiGroupsGroupIdJSONRequestBody{ + Name: "weaw", + }) + require.NoError(t, err) + assert.Equal(t, testGroup, *ret) + }) +} + +func TestGroups_Update_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/groups/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Groups.Update(context.Background(), "Test", api.PutApiGroupsGroupIdJSONRequestBody{ + Name: "weaw", + }) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Nil(t, ret) + }) +} + +func TestGroups_Delete_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/groups/Test", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "DELETE", r.Method) + w.WriteHeader(200) + }) + err := c.Groups.Delete(context.Background(), "Test") + require.NoError(t, err) + }) +} + +func TestGroups_Delete_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/groups/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404}) + w.WriteHeader(404) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + err := c.Groups.Delete(context.Background(), "Test") + assert.Error(t, err) + assert.Equal(t, "Not found", err.Error()) + }) +} + +func TestGroups_Integration(t *testing.T) { + withBlackBoxServer(t, func(c *Client) { + groups, err := c.Groups.List(context.Background()) + require.NoError(t, err) + assert.Len(t, groups, 1) + + group, err := c.Groups.Create(context.Background(), api.GroupRequest{ + Name: "Test", + }) + require.NoError(t, err) + assert.Equal(t, "Test", group.Name) + assert.NotEmpty(t, group.Id) + + group, err = c.Groups.Update(context.Background(), group.Id, api.GroupRequest{ + Name: "Testnt", + }) + require.NoError(t, err) + assert.Equal(t, "Testnt", group.Name) + + err = c.Groups.Delete(context.Background(), group.Id) + require.NoError(t, err) + + groups, err = c.Groups.List(context.Background()) + require.NoError(t, err) + assert.Len(t, groups, 1) + }) +} diff --git a/management/client/rest/networks.go b/management/client/rest/networks.go new file mode 100644 index 000000000..2cdd6d73d --- /dev/null +++ b/management/client/rest/networks.go @@ -0,0 +1,246 @@ +package rest + +import ( + "bytes" + "context" + "encoding/json" + + "github.com/netbirdio/netbird/management/server/http/api" +) + +// NetworksAPI APIs for Networks, do not use directly +type NetworksAPI struct { + c *Client +} + +// List list all networks +// See more: https://docs.netbird.io/api/resources/networks#list-all-networks +func (a *NetworksAPI) List(ctx context.Context) ([]api.Network, error) { + resp, err := a.c.newRequest(ctx, "GET", "/api/networks", nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[[]api.Network](resp) + return ret, err +} + +// Get get network info +// See more: https://docs.netbird.io/api/resources/networks#retrieve-a-network +func (a *NetworksAPI) Get(ctx context.Context, networkID string) (*api.Network, error) { + resp, err := a.c.newRequest(ctx, "GET", "/api/networks/"+networkID, nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.Network](resp) + return &ret, err +} + +// Create create new network +// See more: https://docs.netbird.io/api/resources/networks#create-a-network +func (a *NetworksAPI) Create(ctx context.Context, request api.PostApiNetworksJSONRequestBody) (*api.Network, error) { + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + resp, err := a.c.newRequest(ctx, "POST", "/api/networks", bytes.NewReader(requestBytes)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.Network](resp) + return &ret, err +} + +// Update update network +// See more: https://docs.netbird.io/api/resources/networks#update-a-network +func (a *NetworksAPI) Update(ctx context.Context, networkID string, request api.PutApiNetworksNetworkIdJSONRequestBody) (*api.Network, error) { + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + resp, err := a.c.newRequest(ctx, "PUT", "/api/networks/"+networkID, bytes.NewReader(requestBytes)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.Network](resp) + return &ret, err +} + +// Delete delete network +// See more: https://docs.netbird.io/api/resources/networks#delete-a-network +func (a *NetworksAPI) Delete(ctx context.Context, networkID string) error { + resp, err := a.c.newRequest(ctx, "DELETE", "/api/networks/"+networkID, nil) + if err != nil { + return err + } + defer resp.Body.Close() + + return nil +} + +// NetworkResourcesAPI APIs for Network Resources, do not use directly +type NetworkResourcesAPI struct { + c *Client + networkID string +} + +// Resources APIs for network resources +func (a *NetworksAPI) Resources(networkID string) *NetworkResourcesAPI { + return &NetworkResourcesAPI{ + c: a.c, + networkID: networkID, + } +} + +// List list all resources in networks +// See more: https://docs.netbird.io/api/resources/networks#list-all-network-resources +func (a *NetworkResourcesAPI) List(ctx context.Context) ([]api.NetworkResource, error) { + resp, err := a.c.newRequest(ctx, "GET", "/api/networks/"+a.networkID+"/resources", nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[[]api.NetworkResource](resp) + return ret, err +} + +// Get get network resource info +// See more: https://docs.netbird.io/api/resources/networks#retrieve-a-network-resource +func (a *NetworkResourcesAPI) Get(ctx context.Context, networkResourceID string) (*api.NetworkResource, error) { + resp, err := a.c.newRequest(ctx, "GET", "/api/networks/"+a.networkID+"/resources/"+networkResourceID, nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.NetworkResource](resp) + return &ret, err +} + +// Create create new network resource +// See more: https://docs.netbird.io/api/resources/networks#create-a-network-resource +func (a *NetworkResourcesAPI) Create(ctx context.Context, request api.PostApiNetworksNetworkIdResourcesJSONRequestBody) (*api.NetworkResource, error) { + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + resp, err := a.c.newRequest(ctx, "POST", "/api/networks/"+a.networkID+"/resources", bytes.NewReader(requestBytes)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.NetworkResource](resp) + return &ret, err +} + +// Update update network resource +// See more: https://docs.netbird.io/api/resources/networks#update-a-network-resource +func (a *NetworkResourcesAPI) Update(ctx context.Context, networkResourceID string, request api.PutApiNetworksNetworkIdResourcesResourceIdJSONRequestBody) (*api.NetworkResource, error) { + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + resp, err := a.c.newRequest(ctx, "PUT", "/api/networks/"+a.networkID+"/resources/"+networkResourceID, bytes.NewReader(requestBytes)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.NetworkResource](resp) + return &ret, err +} + +// Delete delete network resource +// See more: https://docs.netbird.io/api/resources/networks#delete-a-network-resource +func (a *NetworkResourcesAPI) Delete(ctx context.Context, networkResourceID string) error { + resp, err := a.c.newRequest(ctx, "DELETE", "/api/networks/"+a.networkID+"/resources/"+networkResourceID, nil) + if err != nil { + return err + } + defer resp.Body.Close() + + return nil +} + +// NetworkRoutersAPI APIs for Network Routers, do not use directly +type NetworkRoutersAPI struct { + c *Client + networkID string +} + +// Routers APIs for network routers +func (a *NetworksAPI) Routers(networkID string) *NetworkRoutersAPI { + return &NetworkRoutersAPI{ + c: a.c, + networkID: networkID, + } +} + +// List list all routers in networks +// See more: https://docs.netbird.io/api/routers/networks#list-all-network-routers +func (a *NetworkRoutersAPI) List(ctx context.Context) ([]api.NetworkRouter, error) { + resp, err := a.c.newRequest(ctx, "GET", "/api/networks/"+a.networkID+"/routers", nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[[]api.NetworkRouter](resp) + return ret, err +} + +// Get get network router info +// See more: https://docs.netbird.io/api/routers/networks#retrieve-a-network-router +func (a *NetworkRoutersAPI) Get(ctx context.Context, networkRouterID string) (*api.NetworkRouter, error) { + resp, err := a.c.newRequest(ctx, "GET", "/api/networks/"+a.networkID+"/routers/"+networkRouterID, nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.NetworkRouter](resp) + return &ret, err +} + +// Create create new network router +// See more: https://docs.netbird.io/api/routers/networks#create-a-network-router +func (a *NetworkRoutersAPI) Create(ctx context.Context, request api.PostApiNetworksNetworkIdRoutersJSONRequestBody) (*api.NetworkRouter, error) { + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + resp, err := a.c.newRequest(ctx, "POST", "/api/networks/"+a.networkID+"/routers", bytes.NewReader(requestBytes)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.NetworkRouter](resp) + return &ret, err +} + +// Update update network router +// See more: https://docs.netbird.io/api/routers/networks#update-a-network-router +func (a *NetworkRoutersAPI) Update(ctx context.Context, networkRouterID string, request api.PutApiNetworksNetworkIdRoutersRouterIdJSONRequestBody) (*api.NetworkRouter, error) { + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + resp, err := a.c.newRequest(ctx, "PUT", "/api/networks/"+a.networkID+"/routers/"+networkRouterID, bytes.NewReader(requestBytes)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.NetworkRouter](resp) + return &ret, err +} + +// Delete delete network router +// See more: https://docs.netbird.io/api/routers/networks#delete-a-network-router +func (a *NetworkRoutersAPI) Delete(ctx context.Context, networkRouterID string) error { + resp, err := a.c.newRequest(ctx, "DELETE", "/api/networks/"+a.networkID+"/routers/"+networkRouterID, nil) + if err != nil { + return err + } + defer resp.Body.Close() + + return nil +} diff --git a/management/client/rest/networks_test.go b/management/client/rest/networks_test.go new file mode 100644 index 000000000..934c55380 --- /dev/null +++ b/management/client/rest/networks_test.go @@ -0,0 +1,589 @@ +package rest + +import ( + "context" + "encoding/json" + "io" + "net/http" + "testing" + + "github.com/netbirdio/netbird/management/server/http/api" + "github.com/netbirdio/netbird/management/server/http/util" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var ( + testNetwork = api.Network{ + Id: "Test", + Name: "wow", + } + + testNetworkResource = api.NetworkResource{ + Description: ptr("meaw"), + Id: "awa", + } + + testNetworkRouter = api.NetworkRouter{ + Id: "ouch", + } +) + +func TestNetworks_List_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/networks", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal([]api.Network{testNetwork}) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Networks.List(context.Background()) + require.NoError(t, err) + assert.Len(t, ret, 1) + assert.Equal(t, testNetwork, ret[0]) + }) +} + +func TestNetworks_List_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/networks", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Networks.List(context.Background()) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} + +func TestNetworks_Get_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/networks/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(testNetwork) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Networks.Get(context.Background(), "Test") + require.NoError(t, err) + assert.Equal(t, testNetwork, *ret) + }) +} + +func TestNetworks_Get_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/networks/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Networks.Get(context.Background(), "Test") + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} + +func TestNetworks_Create_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/networks", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "POST", r.Method) + reqBytes, err := io.ReadAll(r.Body) + require.NoError(t, err) + var req api.PostApiNetworksJSONRequestBody + err = json.Unmarshal(reqBytes, &req) + require.NoError(t, err) + assert.Equal(t, "weaw", req.Name) + retBytes, _ := json.Marshal(testNetwork) + _, err = w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Networks.Create(context.Background(), api.PostApiNetworksJSONRequestBody{ + Name: "weaw", + }) + require.NoError(t, err) + assert.Equal(t, testNetwork, *ret) + }) +} + +func TestNetworks_Create_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/networks", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Networks.Create(context.Background(), api.PostApiNetworksJSONRequestBody{ + Name: "weaw", + }) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Nil(t, ret) + }) +} + +func TestNetworks_Update_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/networks/Test", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "PUT", r.Method) + reqBytes, err := io.ReadAll(r.Body) + require.NoError(t, err) + var req api.PutApiNetworksNetworkIdJSONRequestBody + err = json.Unmarshal(reqBytes, &req) + require.NoError(t, err) + assert.Equal(t, "weaw", req.Name) + retBytes, _ := json.Marshal(testNetwork) + _, err = w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Networks.Update(context.Background(), "Test", api.PutApiNetworksNetworkIdJSONRequestBody{ + Name: "weaw", + }) + require.NoError(t, err) + assert.Equal(t, testNetwork, *ret) + }) +} + +func TestNetworks_Update_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/networks/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Networks.Update(context.Background(), "Test", api.PutApiNetworksNetworkIdJSONRequestBody{ + Name: "weaw", + }) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Nil(t, ret) + }) +} + +func TestNetworks_Delete_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/networks/Test", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "DELETE", r.Method) + w.WriteHeader(200) + }) + err := c.Networks.Delete(context.Background(), "Test") + require.NoError(t, err) + }) +} + +func TestNetworks_Delete_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/networks/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404}) + w.WriteHeader(404) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + err := c.Networks.Delete(context.Background(), "Test") + assert.Error(t, err) + assert.Equal(t, "Not found", err.Error()) + }) +} + +func TestNetworks_Integration(t *testing.T) { + withBlackBoxServer(t, func(c *Client) { + network, err := c.Networks.Create(context.Background(), api.NetworkRequest{ + Description: ptr("TestNetwork"), + Name: "Test", + }) + assert.NoError(t, err) + assert.Equal(t, "Test", network.Name) + + networks, err := c.Networks.List(context.Background()) + assert.NoError(t, err) + assert.Empty(t, networks) + + network, err = c.Networks.Update(context.Background(), "TestID", api.NetworkRequest{ + Description: ptr("TestNetwork?"), + Name: "Test", + }) + + assert.NoError(t, err) + assert.Equal(t, "TestNetwork?", *network.Description) + + err = c.Networks.Delete(context.Background(), "TestID") + assert.NoError(t, err) + }) +} + +func TestNetworkResources_List_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/networks/Meow/resources", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal([]api.NetworkResource{testNetworkResource}) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Networks.Resources("Meow").List(context.Background()) + require.NoError(t, err) + assert.Len(t, ret, 1) + assert.Equal(t, testNetworkResource, ret[0]) + }) +} + +func TestNetworkResources_List_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/networks/Meow/resources", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Networks.Resources("Meow").List(context.Background()) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} + +func TestNetworkResources_Get_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/networks/Meow/resources/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(testNetworkResource) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Networks.Resources("Meow").Get(context.Background(), "Test") + require.NoError(t, err) + assert.Equal(t, testNetworkResource, *ret) + }) +} + +func TestNetworkResources_Get_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/networks/Meow/resources/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Networks.Resources("Meow").Get(context.Background(), "Test") + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} + +func TestNetworkResources_Create_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/networks/Meow/resources", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "POST", r.Method) + reqBytes, err := io.ReadAll(r.Body) + require.NoError(t, err) + var req api.PostApiNetworksNetworkIdResourcesJSONRequestBody + err = json.Unmarshal(reqBytes, &req) + require.NoError(t, err) + assert.Equal(t, "weaw", req.Name) + retBytes, _ := json.Marshal(testNetworkResource) + _, err = w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Networks.Resources("Meow").Create(context.Background(), api.PostApiNetworksNetworkIdResourcesJSONRequestBody{ + Name: "weaw", + }) + require.NoError(t, err) + assert.Equal(t, testNetworkResource, *ret) + }) +} + +func TestNetworkResources_Create_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/networks/Meow/resources", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Networks.Resources("Meow").Create(context.Background(), api.PostApiNetworksNetworkIdResourcesJSONRequestBody{ + Name: "weaw", + }) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Nil(t, ret) + }) +} + +func TestNetworkResources_Update_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/networks/Meow/resources/Test", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "PUT", r.Method) + reqBytes, err := io.ReadAll(r.Body) + require.NoError(t, err) + var req api.PutApiNetworksNetworkIdResourcesResourceIdJSONRequestBody + err = json.Unmarshal(reqBytes, &req) + require.NoError(t, err) + assert.Equal(t, "weaw", req.Name) + retBytes, _ := json.Marshal(testNetworkResource) + _, err = w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Networks.Resources("Meow").Update(context.Background(), "Test", api.PutApiNetworksNetworkIdResourcesResourceIdJSONRequestBody{ + Name: "weaw", + }) + require.NoError(t, err) + assert.Equal(t, testNetworkResource, *ret) + }) +} + +func TestNetworkResources_Update_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/networks/Meow/resources/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Networks.Resources("Meow").Update(context.Background(), "Test", api.PutApiNetworksNetworkIdResourcesResourceIdJSONRequestBody{ + Name: "weaw", + }) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Nil(t, ret) + }) +} + +func TestNetworkResources_Delete_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/networks/Meow/resources/Test", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "DELETE", r.Method) + w.WriteHeader(200) + }) + err := c.Networks.Resources("Meow").Delete(context.Background(), "Test") + require.NoError(t, err) + }) +} + +func TestNetworkResources_Delete_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/networks/Meow/resources/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404}) + w.WriteHeader(404) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + err := c.Networks.Resources("Meow").Delete(context.Background(), "Test") + assert.Error(t, err) + assert.Equal(t, "Not found", err.Error()) + }) +} + +func TestNetworkResources_Integration(t *testing.T) { + withBlackBoxServer(t, func(c *Client) { + _, err := c.Networks.Resources("TestNetwork").Create(context.Background(), api.NetworkResourceRequest{ + Address: "test.com", + Description: ptr("Description"), + Enabled: false, + Groups: []string{"test"}, + Name: "test", + }) + assert.NoError(t, err) + + _, err = c.Networks.Resources("TestNetwork").List(context.Background()) + assert.NoError(t, err) + + _, err = c.Networks.Resources("TestNetwork").Get(context.Background(), "TestResource") + assert.NoError(t, err) + + _, err = c.Networks.Resources("TestNetwork").Update(context.Background(), "TestResource", api.NetworkResourceRequest{ + Address: "testnt.com", + }) + assert.NoError(t, err) + + err = c.Networks.Resources("TestNetwork").Delete(context.Background(), "TestResource") + assert.NoError(t, err) + }) +} + +func TestNetworkRouters_List_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/networks/Meow/routers", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal([]api.NetworkRouter{testNetworkRouter}) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Networks.Routers("Meow").List(context.Background()) + require.NoError(t, err) + assert.Len(t, ret, 1) + assert.Equal(t, testNetworkRouter, ret[0]) + }) +} + +func TestNetworkRouters_List_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/networks/Meow/routers", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Networks.Routers("Meow").List(context.Background()) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} + +func TestNetworkRouters_Get_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/networks/Meow/routers/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(testNetworkRouter) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Networks.Routers("Meow").Get(context.Background(), "Test") + require.NoError(t, err) + assert.Equal(t, testNetworkRouter, *ret) + }) +} + +func TestNetworkRouters_Get_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/networks/Meow/routers/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Networks.Routers("Meow").Get(context.Background(), "Test") + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} + +func TestNetworkRouters_Create_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/networks/Meow/routers", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "POST", r.Method) + reqBytes, err := io.ReadAll(r.Body) + require.NoError(t, err) + var req api.PostApiNetworksNetworkIdRoutersJSONRequestBody + err = json.Unmarshal(reqBytes, &req) + require.NoError(t, err) + assert.Equal(t, "test", *req.Peer) + retBytes, _ := json.Marshal(testNetworkRouter) + _, err = w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Networks.Routers("Meow").Create(context.Background(), api.PostApiNetworksNetworkIdRoutersJSONRequestBody{ + Peer: ptr("test"), + }) + require.NoError(t, err) + assert.Equal(t, testNetworkRouter, *ret) + }) +} + +func TestNetworkRouters_Create_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/networks/Meow/routers", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Networks.Routers("Meow").Create(context.Background(), api.PostApiNetworksNetworkIdRoutersJSONRequestBody{ + Peer: ptr("test"), + }) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Nil(t, ret) + }) +} + +func TestNetworkRouters_Update_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/networks/Meow/routers/Test", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "PUT", r.Method) + reqBytes, err := io.ReadAll(r.Body) + require.NoError(t, err) + var req api.PutApiNetworksNetworkIdRoutersRouterIdJSONRequestBody + err = json.Unmarshal(reqBytes, &req) + require.NoError(t, err) + assert.Equal(t, "test", *req.Peer) + retBytes, _ := json.Marshal(testNetworkRouter) + _, err = w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Networks.Routers("Meow").Update(context.Background(), "Test", api.PutApiNetworksNetworkIdRoutersRouterIdJSONRequestBody{ + Peer: ptr("test"), + }) + require.NoError(t, err) + assert.Equal(t, testNetworkRouter, *ret) + }) +} + +func TestNetworkRouters_Update_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/networks/Meow/routers/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Networks.Routers("Meow").Update(context.Background(), "Test", api.PutApiNetworksNetworkIdRoutersRouterIdJSONRequestBody{ + Peer: ptr("test"), + }) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Nil(t, ret) + }) +} + +func TestNetworkRouters_Delete_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/networks/Meow/routers/Test", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "DELETE", r.Method) + w.WriteHeader(200) + }) + err := c.Networks.Routers("Meow").Delete(context.Background(), "Test") + require.NoError(t, err) + }) +} + +func TestNetworkRouters_Delete_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/networks/Meow/routers/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404}) + w.WriteHeader(404) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + err := c.Networks.Routers("Meow").Delete(context.Background(), "Test") + assert.Error(t, err) + assert.Equal(t, "Not found", err.Error()) + }) +} + +func TestNetworkRouters_Integration(t *testing.T) { + withBlackBoxServer(t, func(c *Client) { + _, err := c.Networks.Routers("TestNetwork").Create(context.Background(), api.NetworkRouterRequest{ + Enabled: false, + Masquerade: false, + Metric: 9999, + PeerGroups: ptr([]string{"test"}), + }) + assert.NoError(t, err) + + _, err = c.Networks.Routers("TestNetwork").List(context.Background()) + assert.NoError(t, err) + + _, err = c.Networks.Routers("TestNetwork").Get(context.Background(), "TestRouter") + assert.NoError(t, err) + + _, err = c.Networks.Routers("TestNetwork").Update(context.Background(), "TestRouter", api.NetworkRouterRequest{ + Enabled: true, + }) + assert.NoError(t, err) + + err = c.Networks.Routers("TestNetwork").Delete(context.Background(), "TestRouter") + assert.NoError(t, err) + }) +} diff --git a/management/client/rest/peers.go b/management/client/rest/peers.go new file mode 100644 index 000000000..9d35f013c --- /dev/null +++ b/management/client/rest/peers.go @@ -0,0 +1,78 @@ +package rest + +import ( + "bytes" + "context" + "encoding/json" + + "github.com/netbirdio/netbird/management/server/http/api" +) + +// PeersAPI APIs for peers, do not use directly +type PeersAPI struct { + c *Client +} + +// List list all peers +// See more: https://docs.netbird.io/api/resources/peers#list-all-peers +func (a *PeersAPI) List(ctx context.Context) ([]api.Peer, error) { + resp, err := a.c.newRequest(ctx, "GET", "/api/peers", nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[[]api.Peer](resp) + return ret, err +} + +// Get retrieve a peer +// See more: https://docs.netbird.io/api/resources/peers#retrieve-a-peer +func (a *PeersAPI) Get(ctx context.Context, peerID string) (*api.Peer, error) { + resp, err := a.c.newRequest(ctx, "GET", "/api/peers/"+peerID, nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.Peer](resp) + return &ret, err +} + +// Update update information for a peer +// See more: https://docs.netbird.io/api/resources/peers#update-a-peer +func (a *PeersAPI) Update(ctx context.Context, peerID string, request api.PutApiPeersPeerIdJSONRequestBody) (*api.Peer, error) { + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + resp, err := a.c.newRequest(ctx, "PUT", "/api/peers/"+peerID, bytes.NewReader(requestBytes)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.Peer](resp) + return &ret, err +} + +// Delete delete a peer +// See more: https://docs.netbird.io/api/resources/peers#delete-a-peer +func (a *PeersAPI) Delete(ctx context.Context, peerID string) error { + resp, err := a.c.newRequest(ctx, "DELETE", "/api/peers/"+peerID, nil) + if err != nil { + return err + } + defer resp.Body.Close() + + return nil +} + +// ListAccessiblePeers list all peers that the specified peer can connect to within the network +// See more: https://docs.netbird.io/api/resources/peers#list-accessible-peers +func (a *PeersAPI) ListAccessiblePeers(ctx context.Context, peerID string) ([]api.Peer, error) { + resp, err := a.c.newRequest(ctx, "GET", "/api/peers/"+peerID+"/accessible-peers", nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[[]api.Peer](resp) + return ret, err +} diff --git a/management/client/rest/peers_test.go b/management/client/rest/peers_test.go new file mode 100644 index 000000000..216ee990c --- /dev/null +++ b/management/client/rest/peers_test.go @@ -0,0 +1,203 @@ +package rest + +import ( + "context" + "encoding/json" + "io" + "net/http" + "testing" + + "github.com/netbirdio/netbird/management/server/http/api" + "github.com/netbirdio/netbird/management/server/http/util" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var ( + testPeer = api.Peer{ + ApprovalRequired: false, + Connected: false, + ConnectionIp: "127.0.0.1", + DnsLabel: "test", + Id: "Test", + } +) + +func TestPeers_List_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/peers", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal([]api.Peer{testPeer}) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Peers.List(context.Background()) + require.NoError(t, err) + assert.Len(t, ret, 1) + assert.Equal(t, testPeer, ret[0]) + }) +} + +func TestPeers_List_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/peers", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Peers.List(context.Background()) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} + +func TestPeers_Get_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/peers/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(testPeer) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Peers.Get(context.Background(), "Test") + require.NoError(t, err) + assert.Equal(t, testPeer, *ret) + }) +} + +func TestPeers_Get_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/peers/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Peers.Get(context.Background(), "Test") + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} + +func TestPeers_Update_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/peers/Test", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "PUT", r.Method) + reqBytes, err := io.ReadAll(r.Body) + require.NoError(t, err) + var req api.PutApiPeersPeerIdJSONRequestBody + err = json.Unmarshal(reqBytes, &req) + require.NoError(t, err) + assert.Equal(t, true, req.InactivityExpirationEnabled) + retBytes, _ := json.Marshal(testPeer) + _, err = w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Peers.Update(context.Background(), "Test", api.PutApiPeersPeerIdJSONRequestBody{ + InactivityExpirationEnabled: true, + }) + require.NoError(t, err) + assert.Equal(t, testPeer, *ret) + }) +} + +func TestPeers_Update_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/peers/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Peers.Update(context.Background(), "Test", api.PutApiPeersPeerIdJSONRequestBody{ + InactivityExpirationEnabled: false, + }) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Nil(t, ret) + }) +} + +func TestPeers_Delete_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/peers/Test", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "DELETE", r.Method) + w.WriteHeader(200) + }) + err := c.Peers.Delete(context.Background(), "Test") + require.NoError(t, err) + }) +} + +func TestPeers_Delete_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/peers/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404}) + w.WriteHeader(404) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + err := c.Peers.Delete(context.Background(), "Test") + assert.Error(t, err) + assert.Equal(t, "Not found", err.Error()) + }) +} + +func TestPeers_ListAccessiblePeers_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/peers/Test/accessible-peers", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal([]api.Peer{testPeer}) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Peers.ListAccessiblePeers(context.Background(), "Test") + require.NoError(t, err) + assert.Len(t, ret, 1) + assert.Equal(t, testPeer, ret[0]) + }) +} + +func TestPeers_ListAccessiblePeers_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/peers/Test/accessible-peers", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Peers.ListAccessiblePeers(context.Background(), "Test") + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} + +func TestPeers_Integration(t *testing.T) { + withBlackBoxServer(t, func(c *Client) { + peers, err := c.Peers.List(context.Background()) + require.NoError(t, err) + require.NotEmpty(t, peers) + + peer, err := c.Peers.Get(context.Background(), peers[0].Id) + require.NoError(t, err) + assert.Equal(t, peers[0].Id, peer.Id) + + peer, err = c.Peers.Update(context.Background(), peer.Id, api.PeerRequest{ + LoginExpirationEnabled: true, + Name: "Test", + SshEnabled: false, + ApprovalRequired: ptr(false), + InactivityExpirationEnabled: false, + }) + require.NoError(t, err) + assert.Equal(t, true, peer.LoginExpirationEnabled) + + accessiblePeers, err := c.Peers.ListAccessiblePeers(context.Background(), peer.Id) + require.NoError(t, err) + assert.Empty(t, accessiblePeers) + + err = c.Peers.Delete(context.Background(), peer.Id) + require.NoError(t, err) + }) +} diff --git a/management/client/rest/policies.go b/management/client/rest/policies.go new file mode 100644 index 000000000..be6abafaf --- /dev/null +++ b/management/client/rest/policies.go @@ -0,0 +1,82 @@ +package rest + +import ( + "bytes" + "context" + "encoding/json" + + "github.com/netbirdio/netbird/management/server/http/api" +) + +// PoliciesAPI APIs for Policies, do not use directly +type PoliciesAPI struct { + c *Client +} + +// List list all policies +// See more: https://docs.netbird.io/api/resources/policies#list-all-policies +func (a *PoliciesAPI) List(ctx context.Context) ([]api.Policy, error) { + resp, err := a.c.newRequest(ctx, "GET", "/api/policies", nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[[]api.Policy](resp) + return ret, err +} + +// Get get policy info +// See more: https://docs.netbird.io/api/resources/policies#retrieve-a-policy +func (a *PoliciesAPI) Get(ctx context.Context, policyID string) (*api.Policy, error) { + resp, err := a.c.newRequest(ctx, "GET", "/api/policies/"+policyID, nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.Policy](resp) + return &ret, err +} + +// Create create new policy +// See more: https://docs.netbird.io/api/resources/policies#create-a-policy +func (a *PoliciesAPI) Create(ctx context.Context, request api.PostApiPoliciesJSONRequestBody) (*api.Policy, error) { + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + resp, err := a.c.newRequest(ctx, "POST", "/api/policies", bytes.NewReader(requestBytes)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.Policy](resp) + return &ret, err +} + +// Update update policy info +// See more: https://docs.netbird.io/api/resources/policies#update-a-policy +func (a *PoliciesAPI) Update(ctx context.Context, policyID string, request api.PutApiPoliciesPolicyIdJSONRequestBody) (*api.Policy, error) { + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + resp, err := a.c.newRequest(ctx, "PUT", "/api/policies/"+policyID, bytes.NewReader(requestBytes)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.Policy](resp) + return &ret, err +} + +// Delete delete policy +// See more: https://docs.netbird.io/api/resources/policies#delete-a-policy +func (a *PoliciesAPI) Delete(ctx context.Context, policyID string) error { + resp, err := a.c.newRequest(ctx, "DELETE", "/api/policies/"+policyID, nil) + if err != nil { + return err + } + defer resp.Body.Close() + + return nil +} diff --git a/management/client/rest/policies_test.go b/management/client/rest/policies_test.go new file mode 100644 index 000000000..f7fc6ff10 --- /dev/null +++ b/management/client/rest/policies_test.go @@ -0,0 +1,236 @@ +package rest + +import ( + "context" + "encoding/json" + "io" + "net/http" + "testing" + + "github.com/netbirdio/netbird/management/server/http/api" + "github.com/netbirdio/netbird/management/server/http/util" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var ( + testPolicy = api.Policy{ + Name: "wow", + Id: ptr("Test"), + Enabled: false, + } +) + +func TestPolicies_List_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/policies", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal([]api.Policy{testPolicy}) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Policies.List(context.Background()) + require.NoError(t, err) + assert.Len(t, ret, 1) + assert.Equal(t, testPolicy, ret[0]) + }) +} + +func TestPolicies_List_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/policies", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Policies.List(context.Background()) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} + +func TestPolicies_Get_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/policies/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(testPolicy) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Policies.Get(context.Background(), "Test") + require.NoError(t, err) + assert.Equal(t, testPolicy, *ret) + }) +} + +func TestPolicies_Get_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/policies/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Policies.Get(context.Background(), "Test") + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} + +func TestPolicies_Create_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/policies", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "POST", r.Method) + reqBytes, err := io.ReadAll(r.Body) + require.NoError(t, err) + var req api.PutApiPoliciesPolicyIdJSONRequestBody + err = json.Unmarshal(reqBytes, &req) + require.NoError(t, err) + assert.Equal(t, "weaw", req.Name) + retBytes, _ := json.Marshal(testPolicy) + _, err = w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Policies.Create(context.Background(), api.PostApiPoliciesJSONRequestBody{ + Name: "weaw", + }) + require.NoError(t, err) + assert.Equal(t, testPolicy, *ret) + }) +} + +func TestPolicies_Create_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/policies", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Policies.Create(context.Background(), api.PostApiPoliciesJSONRequestBody{ + Name: "weaw", + }) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Nil(t, ret) + }) +} + +func TestPolicies_Update_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/policies/Test", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "PUT", r.Method) + reqBytes, err := io.ReadAll(r.Body) + require.NoError(t, err) + var req api.PutApiPoliciesPolicyIdJSONRequestBody + err = json.Unmarshal(reqBytes, &req) + require.NoError(t, err) + assert.Equal(t, "weaw", req.Name) + retBytes, _ := json.Marshal(testPolicy) + _, err = w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Policies.Update(context.Background(), "Test", api.PutApiPoliciesPolicyIdJSONRequestBody{ + Name: "weaw", + }) + require.NoError(t, err) + assert.Equal(t, testPolicy, *ret) + }) +} + +func TestPolicies_Update_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/policies/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Policies.Update(context.Background(), "Test", api.PutApiPoliciesPolicyIdJSONRequestBody{ + Name: "weaw", + }) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Nil(t, ret) + }) +} + +func TestPolicies_Delete_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/policies/Test", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "DELETE", r.Method) + w.WriteHeader(200) + }) + err := c.Policies.Delete(context.Background(), "Test") + require.NoError(t, err) + }) +} + +func TestPolicies_Delete_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/policies/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404}) + w.WriteHeader(404) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + err := c.Policies.Delete(context.Background(), "Test") + assert.Error(t, err) + assert.Equal(t, "Not found", err.Error()) + }) +} + +func TestPolicies_Integration(t *testing.T) { + withBlackBoxServer(t, func(c *Client) { + policies, err := c.Policies.List(context.Background()) + require.NoError(t, err) + require.NotEmpty(t, policies) + + policy, err := c.Policies.Get(context.Background(), *policies[0].Id) + require.NoError(t, err) + assert.Equal(t, *policies[0].Id, *policy.Id) + + policy, err = c.Policies.Update(context.Background(), *policy.Id, api.PolicyCreate{ + Description: ptr("Test Policy"), + Enabled: false, + Name: "Test", + Rules: []api.PolicyRuleUpdate{ + { + Action: api.PolicyRuleUpdateAction(policy.Rules[0].Action), + Bidirectional: true, + Description: ptr("Test Policy"), + Sources: ptr([]string{(*policy.Rules[0].Sources)[0].Id}), + Destinations: ptr([]string{(*policy.Rules[0].Destinations)[0].Id}), + Enabled: false, + Protocol: api.PolicyRuleUpdateProtocolAll, + }, + }, + SourcePostureChecks: nil, + }) + require.NoError(t, err) + assert.Equal(t, "Test Policy", *policy.Rules[0].Description) + + policy, err = c.Policies.Create(context.Background(), api.PolicyUpdate{ + Description: ptr("Test Policy 2"), + Enabled: false, + Name: "Test", + Rules: []api.PolicyRuleUpdate{ + { + Action: api.PolicyRuleUpdateAction(policy.Rules[0].Action), + Bidirectional: true, + Description: ptr("Test Policy 2"), + Sources: ptr([]string{(*policy.Rules[0].Sources)[0].Id}), + Destinations: ptr([]string{(*policy.Rules[0].Destinations)[0].Id}), + Enabled: false, + Protocol: api.PolicyRuleUpdateProtocolAll, + }, + }, + SourcePostureChecks: nil, + }) + require.NoError(t, err) + + err = c.Policies.Delete(context.Background(), *policy.Id) + require.NoError(t, err) + }) +} diff --git a/management/client/rest/posturechecks.go b/management/client/rest/posturechecks.go new file mode 100644 index 000000000..950d17ba0 --- /dev/null +++ b/management/client/rest/posturechecks.go @@ -0,0 +1,82 @@ +package rest + +import ( + "bytes" + "context" + "encoding/json" + + "github.com/netbirdio/netbird/management/server/http/api" +) + +// PostureChecksAPI APIs for PostureChecks, do not use directly +type PostureChecksAPI struct { + c *Client +} + +// List list all posture checks +// See more: https://docs.netbird.io/api/resources/posture-checks#list-all-posture-checks +func (a *PostureChecksAPI) List(ctx context.Context) ([]api.PostureCheck, error) { + resp, err := a.c.newRequest(ctx, "GET", "/api/posture-checks", nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[[]api.PostureCheck](resp) + return ret, err +} + +// Get get posture check info +// See more: https://docs.netbird.io/api/resources/posture-checks#retrieve-a-posture-check +func (a *PostureChecksAPI) Get(ctx context.Context, postureCheckID string) (*api.PostureCheck, error) { + resp, err := a.c.newRequest(ctx, "GET", "/api/posture-checks/"+postureCheckID, nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.PostureCheck](resp) + return &ret, err +} + +// Create create new posture check +// See more: https://docs.netbird.io/api/resources/posture-checks#create-a-posture-check +func (a *PostureChecksAPI) Create(ctx context.Context, request api.PostApiPostureChecksJSONRequestBody) (*api.PostureCheck, error) { + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + resp, err := a.c.newRequest(ctx, "POST", "/api/posture-checks", bytes.NewReader(requestBytes)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.PostureCheck](resp) + return &ret, err +} + +// Update update posture check info +// See more: https://docs.netbird.io/api/resources/posture-checks#update-a-posture-check +func (a *PostureChecksAPI) Update(ctx context.Context, postureCheckID string, request api.PutApiPostureChecksPostureCheckIdJSONRequestBody) (*api.PostureCheck, error) { + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + resp, err := a.c.newRequest(ctx, "PUT", "/api/posture-checks/"+postureCheckID, bytes.NewReader(requestBytes)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.PostureCheck](resp) + return &ret, err +} + +// Delete delete posture check +// See more: https://docs.netbird.io/api/resources/posture-checks#delete-a-posture-check +func (a *PostureChecksAPI) Delete(ctx context.Context, postureCheckID string) error { + resp, err := a.c.newRequest(ctx, "DELETE", "/api/posture-checks/"+postureCheckID, nil) + if err != nil { + return err + } + defer resp.Body.Close() + + return nil +} diff --git a/management/client/rest/posturechecks_test.go b/management/client/rest/posturechecks_test.go new file mode 100644 index 000000000..6fefc0140 --- /dev/null +++ b/management/client/rest/posturechecks_test.go @@ -0,0 +1,228 @@ +package rest + +import ( + "context" + "encoding/json" + "io" + "net/http" + "testing" + + "github.com/netbirdio/netbird/management/server/http/api" + "github.com/netbirdio/netbird/management/server/http/util" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var ( + testPostureCheck = api.PostureCheck{ + Id: "Test", + Name: "wow", + } +) + +func TestPostureChecks_List_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/posture-checks", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal([]api.PostureCheck{testPostureCheck}) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.PostureChecks.List(context.Background()) + require.NoError(t, err) + assert.Len(t, ret, 1) + assert.Equal(t, testPostureCheck, ret[0]) + }) +} + +func TestPostureChecks_List_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/posture-checks", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.PostureChecks.List(context.Background()) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} + +func TestPostureChecks_Get_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/posture-checks/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(testPostureCheck) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.PostureChecks.Get(context.Background(), "Test") + require.NoError(t, err) + assert.Equal(t, testPostureCheck, *ret) + }) +} + +func TestPostureChecks_Get_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/posture-checks/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.PostureChecks.Get(context.Background(), "Test") + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} + +func TestPostureChecks_Create_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/posture-checks", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "POST", r.Method) + reqBytes, err := io.ReadAll(r.Body) + require.NoError(t, err) + var req api.PostureCheckUpdate + err = json.Unmarshal(reqBytes, &req) + require.NoError(t, err) + assert.Equal(t, "weaw", req.Name) + retBytes, _ := json.Marshal(testPostureCheck) + _, err = w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.PostureChecks.Create(context.Background(), api.PostureCheckUpdate{ + Name: "weaw", + }) + require.NoError(t, err) + assert.Equal(t, testPostureCheck, *ret) + }) +} + +func TestPostureChecks_Create_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/posture-checks", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.PostureChecks.Create(context.Background(), api.PostureCheckUpdate{ + Name: "weaw", + }) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Nil(t, ret) + }) +} + +func TestPostureChecks_Update_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/posture-checks/Test", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "PUT", r.Method) + reqBytes, err := io.ReadAll(r.Body) + require.NoError(t, err) + var req api.PostureCheckUpdate + err = json.Unmarshal(reqBytes, &req) + require.NoError(t, err) + assert.Equal(t, "weaw", req.Name) + retBytes, _ := json.Marshal(testPostureCheck) + _, err = w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.PostureChecks.Update(context.Background(), "Test", api.PostureCheckUpdate{ + Name: "weaw", + }) + require.NoError(t, err) + assert.Equal(t, testPostureCheck, *ret) + }) +} + +func TestPostureChecks_Update_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/posture-checks/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.PostureChecks.Update(context.Background(), "Test", api.PostureCheckUpdate{ + Name: "weaw", + }) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Nil(t, ret) + }) +} + +func TestPostureChecks_Delete_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/posture-checks/Test", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "DELETE", r.Method) + w.WriteHeader(200) + }) + err := c.PostureChecks.Delete(context.Background(), "Test") + require.NoError(t, err) + }) +} + +func TestPostureChecks_Delete_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/posture-checks/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404}) + w.WriteHeader(404) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + err := c.PostureChecks.Delete(context.Background(), "Test") + assert.Error(t, err) + assert.Equal(t, "Not found", err.Error()) + }) +} + +func TestPostureChecks_Integration(t *testing.T) { + withBlackBoxServer(t, func(c *Client) { + check, err := c.PostureChecks.Create(context.Background(), api.PostureCheckUpdate{ + Name: "Test", + Description: "Testing", + Checks: &api.Checks{ + OsVersionCheck: &api.OSVersionCheck{ + Windows: &api.MinKernelVersionCheck{ + MinKernelVersion: "0.0.0", + }, + }, + }, + }) + require.NoError(t, err) + assert.Equal(t, "Test", check.Name) + + checks, err := c.PostureChecks.List(context.Background()) + require.NoError(t, err) + assert.Len(t, checks, 1) + + check, err = c.PostureChecks.Update(context.Background(), check.Id, api.PostureCheckUpdate{ + Name: "Tests", + Description: "Testings", + Checks: &api.Checks{ + GeoLocationCheck: &api.GeoLocationCheck{ + Action: api.GeoLocationCheckActionAllow, Locations: []api.Location{ + { + CityName: ptr("Cairo"), + CountryCode: "EG", + }, + }, + }, + }, + }) + + require.NoError(t, err) + assert.Equal(t, "Testings", *check.Description) + + check, err = c.PostureChecks.Get(context.Background(), check.Id) + require.NoError(t, err) + assert.Equal(t, "Tests", check.Name) + + err = c.PostureChecks.Delete(context.Background(), check.Id) + require.NoError(t, err) + }) +} diff --git a/management/client/rest/routes.go b/management/client/rest/routes.go new file mode 100644 index 000000000..bccbb8847 --- /dev/null +++ b/management/client/rest/routes.go @@ -0,0 +1,82 @@ +package rest + +import ( + "bytes" + "context" + "encoding/json" + + "github.com/netbirdio/netbird/management/server/http/api" +) + +// RoutesAPI APIs for Routes, do not use directly +type RoutesAPI struct { + c *Client +} + +// List list all routes +// See more: https://docs.netbird.io/api/resources/routes#list-all-routes +func (a *RoutesAPI) List(ctx context.Context) ([]api.Route, error) { + resp, err := a.c.newRequest(ctx, "GET", "/api/routes", nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[[]api.Route](resp) + return ret, err +} + +// Get get route info +// See more: https://docs.netbird.io/api/resources/routes#retrieve-a-route +func (a *RoutesAPI) Get(ctx context.Context, routeID string) (*api.Route, error) { + resp, err := a.c.newRequest(ctx, "GET", "/api/routes/"+routeID, nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.Route](resp) + return &ret, err +} + +// Create create new route +// See more: https://docs.netbird.io/api/resources/routes#create-a-route +func (a *RoutesAPI) Create(ctx context.Context, request api.PostApiRoutesJSONRequestBody) (*api.Route, error) { + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + resp, err := a.c.newRequest(ctx, "POST", "/api/routes", bytes.NewReader(requestBytes)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.Route](resp) + return &ret, err +} + +// Update update route info +// See more: https://docs.netbird.io/api/resources/routes#update-a-route +func (a *RoutesAPI) Update(ctx context.Context, routeID string, request api.PutApiRoutesRouteIdJSONRequestBody) (*api.Route, error) { + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + resp, err := a.c.newRequest(ctx, "PUT", "/api/routes/"+routeID, bytes.NewReader(requestBytes)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.Route](resp) + return &ret, err +} + +// Delete delete route +// See more: https://docs.netbird.io/api/resources/routes#delete-a-route +func (a *RoutesAPI) Delete(ctx context.Context, routeID string) error { + resp, err := a.c.newRequest(ctx, "DELETE", "/api/routes/"+routeID, nil) + if err != nil { + return err + } + defer resp.Body.Close() + + return nil +} diff --git a/management/client/rest/routes_test.go b/management/client/rest/routes_test.go new file mode 100644 index 000000000..123bd41d4 --- /dev/null +++ b/management/client/rest/routes_test.go @@ -0,0 +1,226 @@ +package rest + +import ( + "context" + "encoding/json" + "io" + "net/http" + "testing" + + "github.com/netbirdio/netbird/management/server/http/api" + "github.com/netbirdio/netbird/management/server/http/util" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var ( + testRoute = api.Route{ + Id: "Test", + Domains: ptr([]string{"google.com"}), + } +) + +func TestRoutes_List_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/routes", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal([]api.Route{testRoute}) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Routes.List(context.Background()) + require.NoError(t, err) + assert.Len(t, ret, 1) + assert.Equal(t, testRoute, ret[0]) + }) +} + +func TestRoutes_List_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/routes", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Routes.List(context.Background()) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} + +func TestRoutes_Get_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/routes/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(testRoute) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Routes.Get(context.Background(), "Test") + require.NoError(t, err) + assert.Equal(t, testRoute, *ret) + }) +} + +func TestRoutes_Get_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/routes/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Routes.Get(context.Background(), "Test") + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} + +func TestRoutes_Create_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/routes", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "POST", r.Method) + reqBytes, err := io.ReadAll(r.Body) + require.NoError(t, err) + var req api.PostApiRoutesJSONRequestBody + err = json.Unmarshal(reqBytes, &req) + require.NoError(t, err) + assert.Equal(t, "meow", req.Description) + retBytes, _ := json.Marshal(testRoute) + _, err = w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Routes.Create(context.Background(), api.PostApiRoutesJSONRequestBody{ + Description: "meow", + }) + require.NoError(t, err) + assert.Equal(t, testRoute, *ret) + }) +} + +func TestRoutes_Create_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/routes", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Routes.Create(context.Background(), api.PostApiRoutesJSONRequestBody{ + Description: "meow", + }) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Nil(t, ret) + }) +} + +func TestRoutes_Update_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/routes/Test", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "PUT", r.Method) + reqBytes, err := io.ReadAll(r.Body) + require.NoError(t, err) + var req api.PutApiRoutesRouteIdJSONRequestBody + err = json.Unmarshal(reqBytes, &req) + require.NoError(t, err) + assert.Equal(t, "meow", req.Description) + retBytes, _ := json.Marshal(testRoute) + _, err = w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Routes.Update(context.Background(), "Test", api.PutApiRoutesRouteIdJSONRequestBody{ + Description: "meow", + }) + require.NoError(t, err) + assert.Equal(t, testRoute, *ret) + }) +} + +func TestRoutes_Update_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/routes/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Routes.Update(context.Background(), "Test", api.PutApiRoutesRouteIdJSONRequestBody{ + Description: "meow", + }) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Nil(t, ret) + }) +} + +func TestRoutes_Delete_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/routes/Test", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "DELETE", r.Method) + w.WriteHeader(200) + }) + err := c.Routes.Delete(context.Background(), "Test") + require.NoError(t, err) + }) +} + +func TestRoutes_Delete_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/routes/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404}) + w.WriteHeader(404) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + err := c.Routes.Delete(context.Background(), "Test") + assert.Error(t, err) + assert.Equal(t, "Not found", err.Error()) + }) +} + +func TestRoutes_Integration(t *testing.T) { + withBlackBoxServer(t, func(c *Client) { + route, err := c.Routes.Create(context.Background(), api.RouteRequest{ + Description: "Meow", + Enabled: false, + Groups: []string{"cs1tnh0hhcjnqoiuebeg"}, + PeerGroups: ptr([]string{"cs1tnh0hhcjnqoiuebeg"}), + Domains: ptr([]string{"google.com"}), + Masquerade: true, + Metric: 9999, + KeepRoute: false, + NetworkId: "Test", + }) + + require.NoError(t, err) + assert.Equal(t, "Test", route.NetworkId) + + routes, err := c.Routes.List(context.Background()) + require.NoError(t, err) + assert.Len(t, routes, 1) + + route, err = c.Routes.Update(context.Background(), route.Id, api.RouteRequest{ + Description: "Testings", + Enabled: false, + Groups: []string{"cs1tnh0hhcjnqoiuebeg"}, + PeerGroups: ptr([]string{"cs1tnh0hhcjnqoiuebeg"}), + Domains: ptr([]string{"google.com"}), + Masquerade: true, + Metric: 9999, + KeepRoute: false, + NetworkId: "Tests", + }) + + require.NoError(t, err) + assert.Equal(t, "Testings", route.Description) + + route, err = c.Routes.Get(context.Background(), route.Id) + require.NoError(t, err) + assert.Equal(t, "Tests", route.NetworkId) + + err = c.Routes.Delete(context.Background(), route.Id) + require.NoError(t, err) + }) +} diff --git a/management/client/rest/setupkeys.go b/management/client/rest/setupkeys.go new file mode 100644 index 000000000..645614fcf --- /dev/null +++ b/management/client/rest/setupkeys.go @@ -0,0 +1,82 @@ +package rest + +import ( + "bytes" + "context" + "encoding/json" + + "github.com/netbirdio/netbird/management/server/http/api" +) + +// SetupKeysAPI APIs for Setup keys, do not use directly +type SetupKeysAPI struct { + c *Client +} + +// List list all setup keys +// See more: https://docs.netbird.io/api/resources/setup-keys#list-all-setup-keys +func (a *SetupKeysAPI) List(ctx context.Context) ([]api.SetupKey, error) { + resp, err := a.c.newRequest(ctx, "GET", "/api/setup-keys", nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[[]api.SetupKey](resp) + return ret, err +} + +// Get get setup key info +// See more: https://docs.netbird.io/api/resources/setup-keys#retrieve-a-setup-key +func (a *SetupKeysAPI) Get(ctx context.Context, setupKeyID string) (*api.SetupKey, error) { + resp, err := a.c.newRequest(ctx, "GET", "/api/setup-keys/"+setupKeyID, nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.SetupKey](resp) + return &ret, err +} + +// Create generate new Setup Key +// See more: https://docs.netbird.io/api/resources/setup-keys#create-a-setup-key +func (a *SetupKeysAPI) Create(ctx context.Context, request api.PostApiSetupKeysJSONRequestBody) (*api.SetupKeyClear, error) { + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + resp, err := a.c.newRequest(ctx, "POST", "/api/setup-keys", bytes.NewReader(requestBytes)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.SetupKeyClear](resp) + return &ret, err +} + +// Update generate new Setup Key +// See more: https://docs.netbird.io/api/resources/setup-keys#update-a-setup-key +func (a *SetupKeysAPI) Update(ctx context.Context, setupKeyID string, request api.PutApiSetupKeysKeyIdJSONRequestBody) (*api.SetupKey, error) { + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + resp, err := a.c.newRequest(ctx, "PUT", "/api/setup-keys/"+setupKeyID, bytes.NewReader(requestBytes)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.SetupKey](resp) + return &ret, err +} + +// Delete delete setup key +// See more: https://docs.netbird.io/api/resources/setup-keys#delete-a-setup-key +func (a *SetupKeysAPI) Delete(ctx context.Context, setupKeyID string) error { + resp, err := a.c.newRequest(ctx, "DELETE", "/api/setup-keys/"+setupKeyID, nil) + if err != nil { + return err + } + defer resp.Body.Close() + + return nil +} diff --git a/management/client/rest/setupkeys_test.go b/management/client/rest/setupkeys_test.go new file mode 100644 index 000000000..82c3d1fc8 --- /dev/null +++ b/management/client/rest/setupkeys_test.go @@ -0,0 +1,227 @@ +package rest + +import ( + "context" + "encoding/json" + "io" + "net/http" + "testing" + + "github.com/netbirdio/netbird/management/server/http/api" + "github.com/netbirdio/netbird/management/server/http/util" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var ( + testSetupKey = api.SetupKey{ + Id: "Test", + Name: "wow", + AutoGroups: []string{"meow"}, + Ephemeral: true, + } + + testSteupKeyGenerated = api.SetupKeyClear{ + Id: "Test", + Name: "wow", + AutoGroups: []string{"meow"}, + Ephemeral: true, + Key: "shhh", + } +) + +func TestSetupKeys_List_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/setup-keys", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal([]api.SetupKey{testSetupKey}) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.SetupKeys.List(context.Background()) + require.NoError(t, err) + assert.Len(t, ret, 1) + assert.Equal(t, testSetupKey, ret[0]) + }) +} + +func TestSetupKeys_List_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/setup-keys", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.SetupKeys.List(context.Background()) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} + +func TestSetupKeys_Get_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/setup-keys/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(testSetupKey) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.SetupKeys.Get(context.Background(), "Test") + require.NoError(t, err) + assert.Equal(t, testSetupKey, *ret) + }) +} + +func TestSetupKeys_Get_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/setup-keys/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.SetupKeys.Get(context.Background(), "Test") + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} + +func TestSetupKeys_Create_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/setup-keys", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "POST", r.Method) + reqBytes, err := io.ReadAll(r.Body) + require.NoError(t, err) + var req api.PostApiSetupKeysJSONRequestBody + err = json.Unmarshal(reqBytes, &req) + require.NoError(t, err) + assert.Equal(t, 5, req.ExpiresIn) + retBytes, _ := json.Marshal(testSteupKeyGenerated) + _, err = w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.SetupKeys.Create(context.Background(), api.PostApiSetupKeysJSONRequestBody{ + ExpiresIn: 5, + }) + require.NoError(t, err) + assert.Equal(t, testSteupKeyGenerated, *ret) + }) +} + +func TestSetupKeys_Create_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/setup-keys", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.SetupKeys.Create(context.Background(), api.PostApiSetupKeysJSONRequestBody{ + ExpiresIn: 5, + }) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Nil(t, ret) + }) +} + +func TestSetupKeys_Update_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/setup-keys/Test", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "PUT", r.Method) + reqBytes, err := io.ReadAll(r.Body) + require.NoError(t, err) + var req api.PutApiSetupKeysKeyIdJSONRequestBody + err = json.Unmarshal(reqBytes, &req) + require.NoError(t, err) + assert.Equal(t, true, req.Revoked) + retBytes, _ := json.Marshal(testSetupKey) + _, err = w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.SetupKeys.Update(context.Background(), "Test", api.PutApiSetupKeysKeyIdJSONRequestBody{ + Revoked: true, + }) + require.NoError(t, err) + assert.Equal(t, testSetupKey, *ret) + }) +} + +func TestSetupKeys_Update_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/setup-keys/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.SetupKeys.Update(context.Background(), "Test", api.PutApiSetupKeysKeyIdJSONRequestBody{ + Revoked: true, + }) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Nil(t, ret) + }) +} + +func TestSetupKeys_Delete_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/setup-keys/Test", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "DELETE", r.Method) + w.WriteHeader(200) + }) + err := c.SetupKeys.Delete(context.Background(), "Test") + require.NoError(t, err) + }) +} + +func TestSetupKeys_Delete_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/setup-keys/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404}) + w.WriteHeader(404) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + err := c.SetupKeys.Delete(context.Background(), "Test") + assert.Error(t, err) + assert.Equal(t, "Not found", err.Error()) + }) +} + +func TestSetupKeys_Integration(t *testing.T) { + withBlackBoxServer(t, func(c *Client) { + group, err := c.Groups.Create(context.Background(), api.GroupRequest{ + Name: "Test", + }) + require.NoError(t, err) + + skClear, err := c.SetupKeys.Create(context.Background(), api.CreateSetupKeyRequest{ + AutoGroups: []string{group.Id}, + Ephemeral: ptr(false), + Name: "test", + Type: "reusable", + }) + + require.NoError(t, err) + assert.Equal(t, true, skClear.Valid) + + keys, err := c.SetupKeys.List(context.Background()) + require.NoError(t, err) + assert.Len(t, keys, 2) + + sk, err := c.SetupKeys.Update(context.Background(), skClear.Id, api.SetupKeyRequest{ + Revoked: true, + AutoGroups: []string{group.Id}, + }) + require.NoError(t, err) + + sk, err = c.SetupKeys.Get(context.Background(), sk.Id) + require.NoError(t, err) + assert.Equal(t, false, sk.Valid) + + err = c.SetupKeys.Delete(context.Background(), sk.Id) + require.NoError(t, err) + }) +} diff --git a/management/client/rest/tokens.go b/management/client/rest/tokens.go new file mode 100644 index 000000000..3275bea81 --- /dev/null +++ b/management/client/rest/tokens.go @@ -0,0 +1,66 @@ +package rest + +import ( + "bytes" + "context" + "encoding/json" + + "github.com/netbirdio/netbird/management/server/http/api" +) + +// TokensAPI APIs for PATs, do not use directly +type TokensAPI struct { + c *Client +} + +// List list user tokens +// See more: https://docs.netbird.io/api/resources/tokens#list-all-tokens +func (a *TokensAPI) List(ctx context.Context, userID string) ([]api.PersonalAccessToken, error) { + resp, err := a.c.newRequest(ctx, "GET", "/api/users/"+userID+"/tokens", nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[[]api.PersonalAccessToken](resp) + return ret, err +} + +// Get get user token info +// See more: https://docs.netbird.io/api/resources/tokens#retrieve-a-token +func (a *TokensAPI) Get(ctx context.Context, userID, tokenID string) (*api.PersonalAccessToken, error) { + resp, err := a.c.newRequest(ctx, "GET", "/api/users/"+userID+"/tokens/"+tokenID, nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.PersonalAccessToken](resp) + return &ret, err +} + +// Create generate new PAT for user +// See more: https://docs.netbird.io/api/resources/tokens#create-a-token +func (a *TokensAPI) Create(ctx context.Context, userID string, request api.PostApiUsersUserIdTokensJSONRequestBody) (*api.PersonalAccessTokenGenerated, error) { + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + resp, err := a.c.newRequest(ctx, "POST", "/api/users/"+userID+"/tokens", bytes.NewReader(requestBytes)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.PersonalAccessTokenGenerated](resp) + return &ret, err +} + +// Delete delete user token +// See more: https://docs.netbird.io/api/resources/tokens#delete-a-token +func (a *TokensAPI) Delete(ctx context.Context, userID, tokenID string) error { + resp, err := a.c.newRequest(ctx, "DELETE", "/api/users/"+userID+"/tokens/"+tokenID, nil) + if err != nil { + return err + } + defer resp.Body.Close() + + return nil +} diff --git a/management/client/rest/tokens_test.go b/management/client/rest/tokens_test.go new file mode 100644 index 000000000..478fae93e --- /dev/null +++ b/management/client/rest/tokens_test.go @@ -0,0 +1,175 @@ +package rest + +import ( + "context" + "encoding/json" + "io" + "net/http" + "testing" + "time" + + "github.com/netbirdio/netbird/management/server/http/api" + "github.com/netbirdio/netbird/management/server/http/util" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var ( + testToken = api.PersonalAccessToken{ + Id: "Test", + CreatedAt: time.Time{}, + CreatedBy: "meow", + ExpirationDate: time.Time{}, + LastUsed: nil, + Name: "wow", + } + + testTokenGenerated = api.PersonalAccessTokenGenerated{ + PersonalAccessToken: testToken, + PlainToken: "shhh", + } +) + +func TestTokens_List_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/users/meow/tokens", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal([]api.PersonalAccessToken{testToken}) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Tokens.List(context.Background(), "meow") + require.NoError(t, err) + assert.Len(t, ret, 1) + assert.Equal(t, testToken, ret[0]) + }) +} + +func TestTokens_List_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/users/meow/tokens", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Tokens.List(context.Background(), "meow") + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} + +func TestTokens_Get_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/users/meow/tokens/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(testToken) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Tokens.Get(context.Background(), "meow", "Test") + require.NoError(t, err) + assert.Equal(t, testToken, *ret) + }) +} + +func TestTokens_Get_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/users/meow/tokens/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Tokens.Get(context.Background(), "meow", "Test") + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} + +func TestTokens_Create_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/users/meow/tokens", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "POST", r.Method) + reqBytes, err := io.ReadAll(r.Body) + require.NoError(t, err) + var req api.PostApiUsersUserIdTokensJSONRequestBody + err = json.Unmarshal(reqBytes, &req) + require.NoError(t, err) + assert.Equal(t, 5, req.ExpiresIn) + retBytes, _ := json.Marshal(testTokenGenerated) + _, err = w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Tokens.Create(context.Background(), "meow", api.PostApiUsersUserIdTokensJSONRequestBody{ + ExpiresIn: 5, + }) + require.NoError(t, err) + assert.Equal(t, testTokenGenerated, *ret) + }) +} + +func TestTokens_Create_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/users/meow/tokens", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Tokens.Create(context.Background(), "meow", api.PostApiUsersUserIdTokensJSONRequestBody{ + ExpiresIn: 5, + }) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Nil(t, ret) + }) +} + +func TestTokens_Delete_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/users/meow/tokens/Test", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "DELETE", r.Method) + w.WriteHeader(200) + }) + err := c.Tokens.Delete(context.Background(), "meow", "Test") + require.NoError(t, err) + }) +} + +func TestTokens_Delete_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/users/meow/tokens/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404}) + w.WriteHeader(404) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + err := c.Tokens.Delete(context.Background(), "meow", "Test") + assert.Error(t, err) + assert.Equal(t, "Not found", err.Error()) + }) +} + +func TestTokens_Integration(t *testing.T) { + withBlackBoxServer(t, func(c *Client) { + tokenClear, err := c.Tokens.Create(context.Background(), "a23efe53-63fb-11ec-90d6-0242ac120003", api.PersonalAccessTokenRequest{ + Name: "Test", + ExpiresIn: 365, + }) + + require.NoError(t, err) + assert.Equal(t, "Test", tokenClear.PersonalAccessToken.Name) + + tokens, err := c.Tokens.List(context.Background(), "a23efe53-63fb-11ec-90d6-0242ac120003") + require.NoError(t, err) + assert.Len(t, tokens, 2) + + token, err := c.Tokens.Get(context.Background(), "a23efe53-63fb-11ec-90d6-0242ac120003", tokenClear.PersonalAccessToken.Id) + require.NoError(t, err) + assert.Equal(t, "Test", token.Name) + + err = c.Tokens.Delete(context.Background(), "a23efe53-63fb-11ec-90d6-0242ac120003", token.Id) + require.NoError(t, err) + }) +} diff --git a/management/client/rest/users.go b/management/client/rest/users.go new file mode 100644 index 000000000..372bcee45 --- /dev/null +++ b/management/client/rest/users.go @@ -0,0 +1,82 @@ +package rest + +import ( + "bytes" + "context" + "encoding/json" + + "github.com/netbirdio/netbird/management/server/http/api" +) + +// UsersAPI APIs for users, do not use directly +type UsersAPI struct { + c *Client +} + +// List list all users, only returns one user always +// See more: https://docs.netbird.io/api/resources/users#list-all-users +func (a *UsersAPI) List(ctx context.Context) ([]api.User, error) { + resp, err := a.c.newRequest(ctx, "GET", "/api/users", nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[[]api.User](resp) + return ret, err +} + +// Create create user +// See more: https://docs.netbird.io/api/resources/users#create-a-user +func (a *UsersAPI) Create(ctx context.Context, request api.PostApiUsersJSONRequestBody) (*api.User, error) { + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + resp, err := a.c.newRequest(ctx, "POST", "/api/users", bytes.NewReader(requestBytes)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.User](resp) + return &ret, err +} + +// Update update user settings +// See more: https://docs.netbird.io/api/resources/users#update-a-user +func (a *UsersAPI) Update(ctx context.Context, userID string, request api.PutApiUsersUserIdJSONRequestBody) (*api.User, error) { + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + resp, err := a.c.newRequest(ctx, "PUT", "/api/users/"+userID, bytes.NewReader(requestBytes)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.User](resp) + return &ret, err +} + +// Delete delete user +// See more: https://docs.netbird.io/api/resources/users#delete-a-user +func (a *UsersAPI) Delete(ctx context.Context, userID string) error { + resp, err := a.c.newRequest(ctx, "DELETE", "/api/users/"+userID, nil) + if err != nil { + return err + } + defer resp.Body.Close() + + return nil +} + +// ResendInvitation resend user invitation +// See more: https://docs.netbird.io/api/resources/users#resend-user-invitation +func (a *UsersAPI) ResendInvitation(ctx context.Context, userID string) error { + resp, err := a.c.newRequest(ctx, "POST", "/api/users/"+userID+"/invite", nil) + if err != nil { + return err + } + defer resp.Body.Close() + + return nil +} diff --git a/management/client/rest/users_test.go b/management/client/rest/users_test.go new file mode 100644 index 000000000..aaec3bf42 --- /dev/null +++ b/management/client/rest/users_test.go @@ -0,0 +1,222 @@ +package rest + +import ( + "context" + "encoding/json" + "io" + "net/http" + "testing" + "time" + + "github.com/netbirdio/netbird/management/server/http/api" + "github.com/netbirdio/netbird/management/server/http/util" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var ( + testUser = api.User{ + Id: "Test", + AutoGroups: []string{"test-group"}, + Email: "test@test.com", + IsBlocked: false, + IsCurrent: ptr(false), + IsServiceUser: ptr(false), + Issued: ptr("api"), + LastLogin: &time.Time{}, + Name: "M. Essam", + Permissions: &api.UserPermissions{ + DashboardView: ptr(api.UserPermissionsDashboardViewFull), + }, + Role: "user", + Status: api.UserStatusActive, + } +) + +func TestUsers_List_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/users", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal([]api.User{testUser}) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Users.List(context.Background()) + require.NoError(t, err) + assert.Len(t, ret, 1) + assert.Equal(t, testUser, ret[0]) + }) +} + +func TestUsers_List_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/users", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Users.List(context.Background()) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} + +func TestUsers_Create_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/users", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "POST", r.Method) + reqBytes, err := io.ReadAll(r.Body) + require.NoError(t, err) + var req api.PostApiUsersJSONRequestBody + err = json.Unmarshal(reqBytes, &req) + require.NoError(t, err) + assert.Equal(t, []string{"meow"}, req.AutoGroups) + retBytes, _ := json.Marshal(testUser) + _, err = w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Users.Create(context.Background(), api.PostApiUsersJSONRequestBody{ + AutoGroups: []string{"meow"}, + }) + require.NoError(t, err) + assert.Equal(t, testUser, *ret) + }) +} + +func TestUsers_Create_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/users", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Users.Create(context.Background(), api.PostApiUsersJSONRequestBody{ + AutoGroups: []string{"meow"}, + }) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Nil(t, ret) + }) +} + +func TestUsers_Update_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/users/Test", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "PUT", r.Method) + reqBytes, err := io.ReadAll(r.Body) + require.NoError(t, err) + var req api.PutApiUsersUserIdJSONRequestBody + err = json.Unmarshal(reqBytes, &req) + require.NoError(t, err) + assert.Equal(t, true, req.IsBlocked) + retBytes, _ := json.Marshal(testUser) + _, err = w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Users.Update(context.Background(), "Test", api.PutApiUsersUserIdJSONRequestBody{ + IsBlocked: true, + }) + require.NoError(t, err) + assert.Equal(t, testUser, *ret) + }) + +} + +func TestUsers_Update_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/users/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Users.Update(context.Background(), "Test", api.PutApiUsersUserIdJSONRequestBody{ + IsBlocked: true, + }) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Nil(t, ret) + }) +} + +func TestUsers_Delete_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/users/Test", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "DELETE", r.Method) + w.WriteHeader(200) + }) + err := c.Users.Delete(context.Background(), "Test") + require.NoError(t, err) + }) +} + +func TestUsers_Delete_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/users/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404}) + w.WriteHeader(404) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + err := c.Users.Delete(context.Background(), "Test") + assert.Error(t, err) + assert.Equal(t, "Not found", err.Error()) + }) +} + +func TestUsers_ResendInvitation_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/users/Test/invite", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "POST", r.Method) + w.WriteHeader(200) + }) + err := c.Users.ResendInvitation(context.Background(), "Test") + require.NoError(t, err) + }) +} + +func TestUsers_ResendInvitation_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/users/Test/invite", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404}) + w.WriteHeader(404) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + err := c.Users.ResendInvitation(context.Background(), "Test") + assert.Error(t, err) + assert.Equal(t, "Not found", err.Error()) + }) +} + +func TestUsers_Integration(t *testing.T) { + withBlackBoxServer(t, func(c *Client) { + user, err := c.Users.Create(context.Background(), api.UserCreateRequest{ + AutoGroups: []string{}, + Email: ptr("test@example.com"), + IsServiceUser: true, + Name: ptr("Nobody"), + Role: "user", + }) + + require.NoError(t, err) + assert.Equal(t, "Nobody", user.Name) + + users, err := c.Users.List(context.Background()) + require.NoError(t, err) + assert.NotEmpty(t, users) + + user, err = c.Users.Update(context.Background(), user.Id, api.UserRequest{ + AutoGroups: []string{}, + Role: "admin", + }) + + require.NoError(t, err) + assert.Equal(t, "admin", user.Role) + + err = c.Users.Delete(context.Background(), user.Id) + require.NoError(t, err) + }) +} diff --git a/management/server/testdata/store.sql b/management/server/testdata/store.sql index 17f029713..1c0767bde 100644 --- a/management/server/testdata/store.sql +++ b/management/server/testdata/store.sql @@ -19,6 +19,7 @@ CREATE INDEX `idx_accounts_domain` ON `accounts`(`domain`); CREATE INDEX `idx_setup_keys_account_id` ON `setup_keys`(`account_id`); CREATE INDEX `idx_peers_key` ON `peers`(`key`); CREATE INDEX `idx_peers_account_id` ON `peers`(`account_id`); +CREATE INDEX `idx_peers_account_id_ip` ON `peers`(`account_id`,`ip`); CREATE INDEX `idx_users_account_id` ON `users`(`account_id`); CREATE INDEX `idx_personal_access_tokens_user_id` ON `personal_access_tokens`(`user_id`); CREATE INDEX `idx_groups_account_id` ON `groups`(`account_id`); @@ -39,8 +40,11 @@ CREATE INDEX `idx_networks_account_id` ON `networks`(`account_id`); INSERT INTO accounts VALUES('bf1c8084-ba50-4ce7-9439-34653001fc3b','','2024-10-02 16:03:06.778746+02:00','test.com','private',1,'af1c8024-ha40-4ce2-9418-34653101fc3c','{"IP":"100.64.0.0","Mask":"//8AAA=="}','',0,'[]',0,86400000000000,0,0,0,'',NULL,NULL,NULL); INSERT INTO "groups" VALUES('cs1tnh0hhcjnqoiuebeg','bf1c8084-ba50-4ce7-9439-34653001fc3b','All','api','[]',0,''); INSERT INTO setup_keys VALUES('','bf1c8084-ba50-4ce7-9439-34653001fc3b','A2C8E62B-38F5-4553-B31E-DD66C696CEBB','Default key','reusable','2021-08-19 20:46:20.005936822+02:00','2321-09-18 20:46:20.005936822+02:00','2021-08-19 20:46:20.005936822+02:00',0,0,NULL,'["cs1tnh0hhcjnqoiuebeg"]',0,0); +INSERT INTO users VALUES('a23efe53-63fb-11ec-90d6-0242ac120003','bf1c8084-ba50-4ce7-9439-34653001fc3b','owner',0,0,'','[]',0,NULL,'2024-10-02 16:03:06.779156+02:00','api',0,''); INSERT INTO users VALUES('edafee4e-63fb-11ec-90d6-0242ac120003','bf1c8084-ba50-4ce7-9439-34653001fc3b','admin',0,0,'','[]',0,NULL,'2024-10-02 16:03:06.779156+02:00','api',0,''); INSERT INTO users VALUES('f4f6d672-63fb-11ec-90d6-0242ac120003','bf1c8084-ba50-4ce7-9439-34653001fc3b','user',0,0,'','[]',0,NULL,'2024-10-02 16:03:06.779156+02:00','api',0,''); +-- Unhashed PAT is "nbp_apTmlmUXHSC4PKmHwtIZNaGr8eqcVI2gMURp" +INSERT INTO personal_access_tokens VALUES('9dj38s35-63fb-11ec-90d6-0242ac120004','a23efe53-63fb-11ec-90d6-0242ac120003','','smJvzexPcQ3NRezrVDUmF++0XqvFvXzx8Rsn2y9r1z0=','5023-02-27 00:00:00+00:00','user','2023-01-01 00:00:00+00:00','2023-02-01 00:00:00+00:00'); INSERT INTO personal_access_tokens VALUES('9dj38s35-63fb-11ec-90d6-0242ac120003','f4f6d672-63fb-11ec-90d6-0242ac120003','','SoMeHaShEdToKeN','2023-02-27 00:00:00+00:00','user','2023-01-01 00:00:00+00:00','2023-02-01 00:00:00+00:00'); INSERT INTO installations VALUES(1,''); INSERT INTO policies VALUES('cs1tnh0hhcjnqoiuebf0','bf1c8084-ba50-4ce7-9439-34653001fc3b','Default','This is a default rule that allows connections between all the resources',1,'[]'); @@ -48,3 +52,4 @@ INSERT INTO policy_rules VALUES('cs387mkv2d4bgq41b6n0','cs1tnh0hhcjnqoiuebf0','D INSERT INTO network_routers VALUES('ctc20ji7qv9ck2sebc80','ct286bi7qv930dsrrug0','bf1c8084-ba50-4ce7-9439-34653001fc3b','cs1tnh0hhcjnqoiuebeg',NULL,0,0); INSERT INTO network_resources VALUES ('ctc4nci7qv9061u6ilfg','ct286bi7qv930dsrrug0','bf1c8084-ba50-4ce7-9439-34653001fc3b','Host','192.168.1.1'); INSERT INTO networks VALUES('ct286bi7qv930dsrrug0','bf1c8084-ba50-4ce7-9439-34653001fc3b','Test Network','Test Network'); +INSERT INTO peers VALUES('ct286bi7qv930dsrrug0','bf1c8084-ba50-4ce7-9439-34653001fc3b','','','192.168.0.0','','','','','','','','','','','','','','','','','test','test','2023-01-01 00:00:00+00:00',0,0,0,'a23efe53-63fb-11ec-90d6-0242ac120003','',0,0,'2023-01-01 00:00:00+00:00','2023-01-01 00:00:00+00:00',0,'','','',0); From 0125cd97d8d8d68ce3638ebae1e8618008a6acfb Mon Sep 17 00:00:00 2001 From: hakansa <43675540+hakansa@users.noreply.github.com> Date: Tue, 4 Feb 2025 18:17:59 +0300 Subject: [PATCH 42/92] [client] use embedded root CA if system certpool is empty (#3272) * Implement custom TLS certificate handling with fallback to embedded roots --- client/internal/auth/device_flow.go | 17 ++++++++++++ relay/client/dialer/ws/ws.go | 12 +++++++++ relay/tls/client_dev.go | 16 ++++++++++- relay/tls/client_prod.go | 16 ++++++++++- util/embeddedroots/embeddedroots.go | 42 +++++++++++++++++++++++++++++ util/embeddedroots/isrg-root-x1.pem | 31 +++++++++++++++++++++ util/embeddedroots/isrg-root-x2.pem | 14 ++++++++++ util/grpc/dialer.go | 18 ++++++++++--- 8 files changed, 160 insertions(+), 6 deletions(-) create mode 100644 util/embeddedroots/embeddedroots.go create mode 100644 util/embeddedroots/isrg-root-x1.pem create mode 100644 util/embeddedroots/isrg-root-x2.pem diff --git a/client/internal/auth/device_flow.go b/client/internal/auth/device_flow.go index 87d00de5e..da4f16c8d 100644 --- a/client/internal/auth/device_flow.go +++ b/client/internal/auth/device_flow.go @@ -2,6 +2,8 @@ package auth import ( "context" + "crypto/tls" + "crypto/x509" "encoding/json" "errors" "fmt" @@ -11,7 +13,10 @@ import ( "strings" "time" + log "github.com/sirupsen/logrus" + "github.com/netbirdio/netbird/client/internal" + "github.com/netbirdio/netbird/util/embeddedroots" ) // HostedGrantType grant type for device flow on Hosted @@ -56,6 +61,18 @@ func NewDeviceAuthorizationFlow(config internal.DeviceAuthProviderConfig) (*Devi httpTransport := http.DefaultTransport.(*http.Transport).Clone() httpTransport.MaxIdleConns = 5 + certPool, err := x509.SystemCertPool() + if err != nil || certPool == nil { + log.Debugf("System cert pool not available; falling back to embedded cert, error: %v", err) + certPool = embeddedroots.Get() + } else { + log.Debug("Using system certificate pool.") + } + + httpTransport.TLSClientConfig = &tls.Config{ + RootCAs: certPool, + } + httpClient := &http.Client{ Timeout: 10 * time.Second, Transport: httpTransport, diff --git a/relay/client/dialer/ws/ws.go b/relay/client/dialer/ws/ws.go index df91a66d4..2adbd2451 100644 --- a/relay/client/dialer/ws/ws.go +++ b/relay/client/dialer/ws/ws.go @@ -2,6 +2,8 @@ package ws import ( "context" + "crypto/tls" + "crypto/x509" "errors" "fmt" "net" @@ -13,6 +15,7 @@ import ( "nhooyr.io/websocket" "github.com/netbirdio/netbird/relay/server/listener/ws" + "github.com/netbirdio/netbird/util/embeddedroots" nbnet "github.com/netbirdio/netbird/util/net" ) @@ -66,10 +69,19 @@ func prepareURL(address string) (string, error) { func httpClientNbDialer() *http.Client { customDialer := nbnet.NewDialer() + certPool, err := x509.SystemCertPool() + if err != nil || certPool == nil { + log.Debugf("System cert pool not available; falling back to embedded cert, error: %v", err) + certPool = embeddedroots.Get() + } + customTransport := &http.Transport{ DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { return customDialer.DialContext(ctx, network, addr) }, + TLSClientConfig: &tls.Config{ + RootCAs: certPool, + }, } return &http.Client{ diff --git a/relay/tls/client_dev.go b/relay/tls/client_dev.go index f6b8290a0..52e5535c5 100644 --- a/relay/tls/client_dev.go +++ b/relay/tls/client_dev.go @@ -2,11 +2,25 @@ package tls -import "crypto/tls" +import ( + "crypto/tls" + "crypto/x509" + + log "github.com/sirupsen/logrus" + + "github.com/netbirdio/netbird/util/embeddedroots" +) func ClientQUICTLSConfig() *tls.Config { + certPool, err := x509.SystemCertPool() + if err != nil || certPool == nil { + log.Debugf("System cert pool not available; falling back to embedded cert, error: %v", err) + certPool = embeddedroots.Get() + } + return &tls.Config{ InsecureSkipVerify: true, // Debug mode allows insecure connections NextProtos: []string{nbalpn}, // Ensure this matches the server's ALPN + RootCAs: certPool, } } diff --git a/relay/tls/client_prod.go b/relay/tls/client_prod.go index 686093a37..62e218bc3 100644 --- a/relay/tls/client_prod.go +++ b/relay/tls/client_prod.go @@ -2,10 +2,24 @@ package tls -import "crypto/tls" +import ( + "crypto/tls" + "crypto/x509" + + log "github.com/sirupsen/logrus" + + "github.com/netbirdio/netbird/util/embeddedroots" +) func ClientQUICTLSConfig() *tls.Config { + certPool, err := x509.SystemCertPool() + if err != nil || certPool == nil { + log.Debugf("System cert pool not available; falling back to embedded cert, error: %v", err) + certPool = embeddedroots.Get() + } + return &tls.Config{ NextProtos: []string{nbalpn}, + RootCAs: certPool, } } diff --git a/util/embeddedroots/embeddedroots.go b/util/embeddedroots/embeddedroots.go new file mode 100644 index 000000000..d205f5b69 --- /dev/null +++ b/util/embeddedroots/embeddedroots.go @@ -0,0 +1,42 @@ +package embeddedroots + +import ( + "crypto/x509" + _ "embed" + "sync" +) + +func Get() *x509.CertPool { + rootsVar.load() + return rootsVar.p +} + +type roots struct { + once sync.Once + p *x509.CertPool +} + +var rootsVar roots + +func (r *roots) load() { + r.once.Do(func() { + p := x509.NewCertPool() + p.AppendCertsFromPEM([]byte(isrgRootX1RootPEM)) + p.AppendCertsFromPEM([]byte(isrgRootX2RootPEM)) + r.p = p + }) +} + +// Subject: O = Internet Security Research Group, CN = ISRG Root X1 +// Key type: RSA 4096 +// Validity: until 2030-06-04 (generated 2015-06-04) +// +//go:embed isrg-root-x1.pem +var isrgRootX1RootPEM string + +// Subject: O = Internet Security Research Group, CN = ISRG Root X2 +// Key type: ECDSA P-384 +// Validity: until 2035-09-04 (generated 2020-09-04) +// +//go:embed isrg-root-x2.pem +var isrgRootX2RootPEM string diff --git a/util/embeddedroots/isrg-root-x1.pem b/util/embeddedroots/isrg-root-x1.pem new file mode 100644 index 000000000..57d4a3766 --- /dev/null +++ b/util/embeddedroots/isrg-root-x1.pem @@ -0,0 +1,31 @@ +-----BEGIN CERTIFICATE----- +MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw +TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh +cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4 +WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu +ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY +MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc +h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+ +0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U +A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW +T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH +B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC +B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv +KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn +OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn +jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw +qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI +rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq +hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL +ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ +3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK +NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5 +ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur +TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC +jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc +oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq +4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA +mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d +emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc= +-----END CERTIFICATE----- \ No newline at end of file diff --git a/util/embeddedroots/isrg-root-x2.pem b/util/embeddedroots/isrg-root-x2.pem new file mode 100644 index 000000000..7d903edc9 --- /dev/null +++ b/util/embeddedroots/isrg-root-x2.pem @@ -0,0 +1,14 @@ +-----BEGIN CERTIFICATE----- +MIICGzCCAaGgAwIBAgIQQdKd0XLq7qeAwSxs6S+HUjAKBggqhkjOPQQDAzBPMQsw +CQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFyY2gg +R3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBYMjAeFw0yMDA5MDQwMDAwMDBaFw00 +MDA5MTcxNjAwMDBaME8xCzAJBgNVBAYTAlVTMSkwJwYDVQQKEyBJbnRlcm5ldCBT +ZWN1cml0eSBSZXNlYXJjaCBHcm91cDEVMBMGA1UEAxMMSVNSRyBSb290IFgyMHYw +EAYHKoZIzj0CAQYFK4EEACIDYgAEzZvVn4CDCuwJSvMWSj5cz3es3mcFDR0HttwW ++1qLFNvicWDEukWVEYmO6gbf9yoWHKS5xcUy4APgHoIYOIvXRdgKam7mAHf7AlF9 +ItgKbppbd9/w+kHsOdx1ymgHDB/qo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0T +AQH/BAUwAwEB/zAdBgNVHQ4EFgQUfEKWrt5LSDv6kviejM9ti6lyN5UwCgYIKoZI +zj0EAwMDaAAwZQIwe3lORlCEwkSHRhtFcP9Ymd70/aTSVaYgLXTWNLxBo1BfASdW +tL4ndQavEi51mI38AjEAi/V3bNTIZargCyzuFJ0nN6T5U6VR5CmD1/iQMVtCnwr1 +/q4AaOeMSQ+2b1tbFfLn +-----END CERTIFICATE----- diff --git a/util/grpc/dialer.go b/util/grpc/dialer.go index 4fbffe342..83a11c65d 100644 --- a/util/grpc/dialer.go +++ b/util/grpc/dialer.go @@ -3,14 +3,16 @@ package grpc import ( "context" "crypto/tls" + "crypto/x509" "fmt" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" "net" "os/user" "runtime" "time" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "github.com/cenkalti/backoff/v4" log "github.com/sirupsen/logrus" "google.golang.org/grpc" @@ -18,6 +20,7 @@ import ( "google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/keepalive" + "github.com/netbirdio/netbird/util/embeddedroots" nbnet "github.com/netbirdio/netbird/util/net" ) @@ -57,9 +60,16 @@ func Backoff(ctx context.Context) backoff.BackOff { func CreateConnection(addr string, tlsEnabled bool) (*grpc.ClientConn, error) { transportOption := grpc.WithTransportCredentials(insecure.NewCredentials()) - if tlsEnabled { - transportOption = grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{})) + certPool, err := x509.SystemCertPool() + if err != nil || certPool == nil { + log.Debugf("System cert pool not available; falling back to embedded cert, error: %v", err) + certPool = embeddedroots.Get() + } + + transportOption = grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{ + RootCAs: certPool, + })) } connCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second) From 97d498c59cc1d0f86cb82e5b8e1e89a44365b772 Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Wed, 5 Feb 2025 16:49:41 +0100 Subject: [PATCH 43/92] [misc, client, management] Replace Wiretrustee with Netbird (#3267) --- .github/workflows/release.yml | 2 +- AUTHORS | 2 +- CONTRIBUTOR_LICENSE_AGREEMENT.md | 48 +- LICENSE | 4 +- client/iface/configurer/name.go | 2 +- client/iface/configurer/name_darwin.go | 2 +- client/iface/device/device_android.go | 2 +- client/iface/device/device_ios.go | 2 +- client/internal/connect.go | 16 +- client/internal/engine.go | 4 +- client/netbird.wxs | 4 +- client/proto/daemon.pb.go | 4 +- client/proto/daemon.proto | 2 +- client/system/info.go | 2 +- client/system/info_android.go | 2 +- client/system/info_darwin.go | 2 +- client/system/info_freebsd.go | 22 +- client/system/info_ios.go | 2 +- client/system/info_linux.go | 2 +- client/system/info_test.go | 2 +- client/system/info_windows.go | 2 +- management/client/client_test.go | 24 +- management/client/grpc.go | 28 +- management/proto/management.pb.go | 934 ++++++++++----------- management/proto/management.proto | 22 +- management/server/config.go | 4 +- management/server/grpcserver.go | 16 +- management/server/management_proto_test.go | 98 +-- management/server/management_test.go | 42 +- management/server/peer_test.go | 14 +- management/server/testdata/management.json | 8 +- management/server/token_mgr.go | 6 +- management/server/token_mgr_test.go | 8 +- release_files/darwin-ui-installer.sh | 12 +- release_files/install.sh | 12 +- signal/README.md | 2 +- 36 files changed, 679 insertions(+), 681 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8f267ebdd..04874bdf4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,7 +12,7 @@ env: SIGN_PIPE_VER: "v0.0.18" GORELEASER_VER: "v2.3.2" PRODUCT_NAME: "NetBird" - COPYRIGHT: "Wiretrustee UG (haftungsbeschreankt)" + COPYRIGHT: "NetBird GmbH" concurrency: group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref || github.actor_id }} diff --git a/AUTHORS b/AUTHORS index f2b228766..f39620acc 100644 --- a/AUTHORS +++ b/AUTHORS @@ -1,3 +1,3 @@ Mikhail Bragin (https://github.com/braginini) Maycon Santos (https://github.com/mlsmaycon) -Wiretrustee UG (haftungsbeschränkt) +NetBird GmbH diff --git a/CONTRIBUTOR_LICENSE_AGREEMENT.md b/CONTRIBUTOR_LICENSE_AGREEMENT.md index c47eb813d..89e011ec1 100644 --- a/CONTRIBUTOR_LICENSE_AGREEMENT.md +++ b/CONTRIBUTOR_LICENSE_AGREEMENT.md @@ -3,10 +3,10 @@ We are incredibly thankful for the contributions we receive from the community. We require our external contributors to sign a Contributor License Agreement ("CLA") in order to ensure that our projects remain licensed under Free and Open Source licenses such -as BSD-3 while allowing Wiretrustee to build a sustainable business. +as BSD-3 while allowing NetBird to build a sustainable business. -Wiretrustee is committed to having a true Open Source Software ("OSS") license for -our software. A CLA enables Wiretrustee to safely commercialize our products +NetBird is committed to having a true Open Source Software ("OSS") license for +our software. A CLA enables NetBird to safely commercialize our products while keeping a standard OSS license with all the rights that license grants to users: the ability to use the project in their own projects or businesses, to republish modified source, or to completely fork the project. @@ -20,11 +20,11 @@ This is a human-readable summary of (and not a substitute for) the full agreemen This highlights only some of key terms of the CLA. It has no legal value and you should carefully review all the terms of the actual CLA before agreeing. -
  • Grant of copyright license. You give Wiretrustee permission to use your copyrighted work +
  • Grant of copyright license. You give NetBird permission to use your copyrighted work in commercial products.
  • -
  • Grant of patent license. If your contributed work uses a patent, you give Wiretrustee a +
  • Grant of patent license. If your contributed work uses a patent, you give NetBird a license to use that patent including within commercial products. You also agree that you have permission to grant this license.
  • @@ -45,7 +45,7 @@ more. # Why require a CLA? Agreeing to a CLA explicitly states that you are entitled to provide a contribution, that you cannot withdraw permission -to use your contribution at a later date, and that Wiretrustee has permission to use your contribution in our commercial +to use your contribution at a later date, and that NetBird has permission to use your contribution in our commercial products. This removes any ambiguities or uncertainties caused by not having a CLA and allows users and customers to confidently @@ -65,25 +65,25 @@ Follow the steps given by the bot to sign the CLA. This will require you to log information from your account) and to fill in a few additional details such as your name and email address. We will only use this information for CLA tracking; none of your submitted information will be used for marketing purposes. -You only have to sign the CLA once. Once you've signed the CLA, future contributions to any Wiretrustee project will not +You only have to sign the CLA once. Once you've signed the CLA, future contributions to any NetBird project will not require you to sign again. # Legal Terms and Agreement -In order to clarify the intellectual property license granted with Contributions from any person or entity, Wiretrustee -UG (haftungsbeschränkt) ("Wiretrustee") must have a Contributor License Agreement ("CLA") on file that has been signed +In order to clarify the intellectual property license granted with Contributions from any person or entity, NetBird +GmbH ("NetBird") must have a Contributor License Agreement ("CLA") on file that has been signed by each Contributor, indicating agreement to the license terms below. This license does not change your rights to use your own Contributions for any other purpose. You accept and agree to the following terms and conditions for Your present and future Contributions submitted to -Wiretrustee. Except for the license granted herein to Wiretrustee and recipients of software distributed by Wiretrustee, +NetBird. Except for the license granted herein to NetBird and recipients of software distributed by NetBird, You reserve all right, title, and interest in and to Your Contributions. 1. Definitions. ``` "You" (or "Your") shall mean the copyright owner or legal entity authorized by the copyright owner - that is making this Agreement with Wiretrustee. For legal entities, the entity making a Contribution and all other + that is making this Agreement with NetBird. For legal entities, the entity making a Contribution and all other entities that control, are controlled by, or are under common control with that entity are considered to be a single Contributor. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty @@ -91,23 +91,23 @@ You reserve all right, title, and interest in and to Your Contributions. ``` ``` "Contribution" shall mean any original work of authorship, including any modifications or additions to - an existing work, that is or previously has been intentionally submitted by You to Wiretrustee for inclusion in, - or documentation of, any of the products owned or managed by Wiretrustee (the "Work"). + an existing work, that is or previously has been intentionally submitted by You to NetBird for inclusion in, + or documentation of, any of the products owned or managed by NetBird (the "Work"). For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication - sent to Wiretrustee or its representatives, including but not limited to communication on electronic mailing lists, + sent to NetBird or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, - Wiretrustee for the purpose of discussing and improving the Work, but excluding communication that is conspicuously + NetBird for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by You as "Not a Contribution." ``` -2. Grant of Copyright License. Subject to the terms and conditions of this Agreement, You hereby grant to Wiretrustee - and to recipients of software distributed by Wiretrustee a perpetual, worldwide, non-exclusive, no-charge, +2. Grant of Copyright License. Subject to the terms and conditions of this Agreement, You hereby grant to NetBird + and to recipients of software distributed by NetBird a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, sublicense, and distribute Your Contributions and such derivative works. -3. Grant of Patent License. Subject to the terms and conditions of this Agreement, You hereby grant to Wiretrustee and - to recipients of software distributed by Wiretrustee a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +3. Grant of Patent License. Subject to the terms and conditions of this Agreement, You hereby grant to NetBird and + to recipients of software distributed by NetBird a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by You that are necessarily infringed by Your Contribution(s) alone or by combination of Your Contribution(s) with the Work to which @@ -121,8 +121,8 @@ You reserve all right, title, and interest in and to Your Contributions. intellectual property that you create that includes your Contributions, you represent that you have received permission to make Contributions on behalf of that employer, that you will have received permission from your current and future employers for all future Contributions, that your applicable employer has waived such rights for all of - your current and future Contributions to Wiretrustee, or that your employer has executed a separate Corporate CLA - with Wiretrustee. + your current and future Contributions to NetBird, or that your employer has executed a separate Corporate CLA + with NetBird. 5. You represent that each of Your Contributions is Your original creation (see section 7 for submissions on behalf of @@ -138,11 +138,11 @@ You reserve all right, title, and interest in and to Your Contributions. MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. -7. Should You wish to submit work that is not Your original creation, You may submit it to Wiretrustee separately from +7. Should You wish to submit work that is not Your original creation, You may submit it to NetBird separately from any Contribution, identifying the complete details of its source and of any license or other restriction (including, but not limited to, related patents, trademarks, and license agreements) of which you are personally aware, and conspicuously marking the work as "Submitted on behalf of a third-party: [named here]". -8. You agree to notify Wiretrustee of any facts or circumstances of which you become aware that would make these - representations inaccurate in any respect. \ No newline at end of file +8. You agree to notify NetBird of any facts or circumstances of which you become aware that would make these + representations inaccurate in any respect. diff --git a/LICENSE b/LICENSE index bddb455d5..7cba76dfd 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ BSD 3-Clause License -Copyright (c) 2022 Wiretrustee UG (haftungsbeschränkt) & AUTHORS +Copyright (c) 2022 NetBird GmbH & AUTHORS Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: @@ -10,4 +10,4 @@ Redistribution and use in source and binary forms, with or without modification, 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/client/iface/configurer/name.go b/client/iface/configurer/name.go index e2133d0ea..3b9abc0e8 100644 --- a/client/iface/configurer/name.go +++ b/client/iface/configurer/name.go @@ -2,5 +2,5 @@ package configurer -// WgInterfaceDefault is a default interface name of Wiretrustee +// WgInterfaceDefault is a default interface name of Netbird const WgInterfaceDefault = "wt0" diff --git a/client/iface/configurer/name_darwin.go b/client/iface/configurer/name_darwin.go index 034ce388d..eaf04fc2d 100644 --- a/client/iface/configurer/name_darwin.go +++ b/client/iface/configurer/name_darwin.go @@ -2,5 +2,5 @@ package configurer -// WgInterfaceDefault is a default interface name of Wiretrustee +// WgInterfaceDefault is a default interface name of Netbird const WgInterfaceDefault = "utun100" diff --git a/client/iface/device/device_android.go b/client/iface/device/device_android.go index fac2ba63d..772722b83 100644 --- a/client/iface/device/device_android.go +++ b/client/iface/device/device_android.go @@ -63,7 +63,7 @@ func (t *WGTunDevice) Create(routes []string, dns string, searchDomains []string t.filteredDevice = newDeviceFilter(tunDevice) log.Debugf("attaching to interface %v", name) - t.device = device.NewDevice(t.filteredDevice, t.iceBind, device.NewLogger(wgLogLevel(), "[wiretrustee] ")) + t.device = device.NewDevice(t.filteredDevice, t.iceBind, device.NewLogger(wgLogLevel(), "[netbird] ")) // without this property mobile devices can discover remote endpoints if the configured one was wrong. // this helps with support for the older NetBird clients that had a hardcoded direct mode // t.device.DisableSomeRoamingForBrokenMobileSemantics() diff --git a/client/iface/device/device_ios.go b/client/iface/device/device_ios.go index b9591e0b8..cdabd2c85 100644 --- a/client/iface/device/device_ios.go +++ b/client/iface/device/device_ios.go @@ -64,7 +64,7 @@ func (t *TunDevice) Create() (WGConfigurer, error) { t.filteredDevice = newDeviceFilter(tunDevice) log.Debug("Attaching to interface") - t.device = device.NewDevice(t.filteredDevice, t.iceBind, device.NewLogger(wgLogLevel(), "[wiretrustee] ")) + t.device = device.NewDevice(t.filteredDevice, t.iceBind, device.NewLogger(wgLogLevel(), "[netbird] ")) // without this property mobile devices can discover remote endpoints if the configured one was wrong. // this helps with support for the older NetBird clients that had a hardcoded direct mode // t.device.DisableSomeRoamingForBrokenMobileSemantics() diff --git a/client/internal/connect.go b/client/internal/connect.go index 3e3f04f17..ddd10e5cd 100644 --- a/client/internal/connect.go +++ b/client/internal/connect.go @@ -177,7 +177,7 @@ func (c *ConnectClient) run(mobileDependency MobileDependency, runningChan chan } }() - // connect (just a connection, no stream yet) and login to Management Service to get an initial global Wiretrustee config + // connect (just a connection, no stream yet) and login to Management Service to get an initial global Netbird config loginResp, err := loginToManagement(engineCtx, mgmClient, publicSSHKey, c.config) if err != nil { log.Debug(err) @@ -199,8 +199,8 @@ func (c *ConnectClient) run(mobileDependency MobileDependency, runningChan chan c.statusRecorder.UpdateLocalPeerState(localPeerState) signalURL := fmt.Sprintf("%s://%s", - strings.ToLower(loginResp.GetWiretrusteeConfig().GetSignal().GetProtocol().String()), - loginResp.GetWiretrusteeConfig().GetSignal().GetUri(), + strings.ToLower(loginResp.GetNetbirdConfig().GetSignal().GetProtocol().String()), + loginResp.GetNetbirdConfig().GetSignal().GetUri(), ) c.statusRecorder.UpdateSignalAddress(signalURL) @@ -211,8 +211,8 @@ func (c *ConnectClient) run(mobileDependency MobileDependency, runningChan chan c.statusRecorder.MarkSignalDisconnected(err) }() - // with the global Wiretrustee config in hand connect (just a connection, no stream yet) Signal - signalClient, err := connectToSignal(engineCtx, loginResp.GetWiretrusteeConfig(), myPrivateKey) + // with the global Netbird config in hand connect (just a connection, no stream yet) Signal + signalClient, err := connectToSignal(engineCtx, loginResp.GetNetbirdConfig(), myPrivateKey) if err != nil { log.Error(err) return wrapErr(err) @@ -311,7 +311,7 @@ func (c *ConnectClient) run(mobileDependency MobileDependency, runningChan chan } func parseRelayInfo(loginResp *mgmProto.LoginResponse) ([]string, *hmac.Token) { - relayCfg := loginResp.GetWiretrusteeConfig().GetRelay() + relayCfg := loginResp.GetNetbirdConfig().GetRelay() if relayCfg == nil { return nil, nil } @@ -440,7 +440,7 @@ func createEngineConfig(key wgtypes.Key, config *Config, peerConfig *mgmProto.Pe } // connectToSignal creates Signal Service client and established a connection -func connectToSignal(ctx context.Context, wtConfig *mgmProto.WiretrusteeConfig, ourPrivateKey wgtypes.Key) (*signal.GrpcClient, error) { +func connectToSignal(ctx context.Context, wtConfig *mgmProto.NetbirdConfig, ourPrivateKey wgtypes.Key) (*signal.GrpcClient, error) { var sigTLSEnabled bool if wtConfig.Signal.Protocol == mgmProto.HostConfig_HTTPS { sigTLSEnabled = true @@ -457,7 +457,7 @@ func connectToSignal(ctx context.Context, wtConfig *mgmProto.WiretrusteeConfig, return signalClient, nil } -// loginToManagement creates Management Services client, establishes a connection, logs-in and gets a global Wiretrustee config (signal, turn, stun hosts, etc) +// loginToManagement creates Management Services client, establishes a connection, logs-in and gets a global Netbird config (signal, turn, stun hosts, etc) func loginToManagement(ctx context.Context, client mgm.Client, pubSSHKey []byte, config *Config) (*mgmProto.LoginResponse, error) { serverPublicKey, err := client.GetServerPublicKey() diff --git a/client/internal/engine.go b/client/internal/engine.go index 4f69adfa6..7f7cdf376 100644 --- a/client/internal/engine.go +++ b/client/internal/engine.go @@ -608,8 +608,8 @@ func (e *Engine) handleSync(update *mgmProto.SyncResponse) error { e.syncMsgMux.Lock() defer e.syncMsgMux.Unlock() - if update.GetWiretrusteeConfig() != nil { - wCfg := update.GetWiretrusteeConfig() + if update.GetNetbirdConfig() != nil { + wCfg := update.GetNetbirdConfig() err := e.updateTURNs(wCfg.GetTurns()) if err != nil { return fmt.Errorf("update TURNs: %w", err) diff --git a/client/netbird.wxs b/client/netbird.wxs index 0e2be7b3c..ee9ab667f 100644 --- a/client/netbird.wxs +++ b/client/netbird.wxs @@ -1,6 +1,6 @@ - @@ -75,4 +75,4 @@ - \ No newline at end of file + diff --git a/client/proto/daemon.pb.go b/client/proto/daemon.pb.go index 413f94a54..30f7473cd 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 v4.23.4 +// protoc v4.24.3 // source: daemon.proto package proto @@ -92,7 +92,7 @@ type LoginRequest struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - // setupKey wiretrustee setup key. + // setupKey netbird setup key. SetupKey string `protobuf:"bytes,1,opt,name=setupKey,proto3" json:"setupKey,omitempty"` // This is the old PreSharedKey field which will be deprecated in favor of optionalPreSharedKey field that is defined as optional // to allow clearing of preshared key while being able to persist in the config file. diff --git a/client/proto/daemon.proto b/client/proto/daemon.proto index b626276de..8db3add08 100644 --- a/client/proto/daemon.proto +++ b/client/proto/daemon.proto @@ -61,7 +61,7 @@ service DaemonService { message LoginRequest { - // setupKey wiretrustee setup key. + // setupKey netbird setup key. string setupKey = 1; // This is the old PreSharedKey field which will be deprecated in favor of optionalPreSharedKey field that is defined as optional diff --git a/client/system/info.go b/client/system/info.go index 4ab4292ae..d83e9509a 100644 --- a/client/system/info.go +++ b/client/system/info.go @@ -50,7 +50,7 @@ type Info struct { OSVersion string Hostname string CPUs int - WiretrusteeVersion string + NetbirdVersion string UIVersion string KernelVersion string NetworkAddresses []NetworkAddress diff --git a/client/system/info_android.go b/client/system/info_android.go index 2d44a6f52..56fe0741d 100644 --- a/client/system/info_android.go +++ b/client/system/info_android.go @@ -36,7 +36,7 @@ func GetInfo(ctx context.Context) *Info { OSVersion: osVersion(), Hostname: extractDeviceName(ctx, "android"), CPUs: runtime.NumCPU(), - WiretrusteeVersion: version.NetbirdVersion(), + NetbirdVersion: version.NetbirdVersion(), UIVersion: extractUIVersion(ctx), KernelVersion: kernelVersion, SystemSerialNumber: serial(), diff --git a/client/system/info_darwin.go b/client/system/info_darwin.go index 13b0a446b..f105ada60 100644 --- a/client/system/info_darwin.go +++ b/client/system/info_darwin.go @@ -63,7 +63,7 @@ func GetInfo(ctx context.Context) *Info { systemHostname, _ := os.Hostname() gio.Hostname = extractDeviceName(ctx, systemHostname) - gio.WiretrusteeVersion = version.NetbirdVersion() + gio.NetbirdVersion = version.NetbirdVersion() gio.UIVersion = extractUserAgent(ctx) return gio diff --git a/client/system/info_freebsd.go b/client/system/info_freebsd.go index 454e58a0b..bed6711de 100644 --- a/client/system/info_freebsd.go +++ b/client/system/info_freebsd.go @@ -39,17 +39,17 @@ func GetInfo(ctx context.Context) *Info { systemHostname, _ := os.Hostname() return &Info{ - GoOS: runtime.GOOS, - Kernel: osInfo[0], - Platform: runtime.GOARCH, - OS: osName, - OSVersion: osVersion, - Hostname: extractDeviceName(ctx, systemHostname), - CPUs: runtime.NumCPU(), - WiretrusteeVersion: version.NetbirdVersion(), - UIVersion: extractUserAgent(ctx), - KernelVersion: osInfo[1], - Environment: env, + GoOS: runtime.GOOS, + Kernel: osInfo[0], + Platform: runtime.GOARCH, + OS: osName, + OSVersion: osVersion, + Hostname: extractDeviceName(ctx, systemHostname), + CPUs: runtime.NumCPU(), + NetbirdVersion: version.NetbirdVersion(), + UIVersion: extractUserAgent(ctx), + KernelVersion: osInfo[1], + Environment: env, } } diff --git a/client/system/info_ios.go b/client/system/info_ios.go index 3dbf50e1e..897ec0a35 100644 --- a/client/system/info_ios.go +++ b/client/system/info_ios.go @@ -19,7 +19,7 @@ func GetInfo(ctx context.Context) *Info { gio := &Info{Kernel: sysName, OSVersion: swVersion, Platform: "unknown", OS: sysName, GoOS: runtime.GOOS, CPUs: runtime.NumCPU(), KernelVersion: swVersion} gio.Hostname = extractDeviceName(ctx, "hostname") - gio.WiretrusteeVersion = version.NetbirdVersion() + gio.NetbirdVersion = version.NetbirdVersion() gio.UIVersion = extractUserAgent(ctx) return gio diff --git a/client/system/info_linux.go b/client/system/info_linux.go index bfc77be19..9bfc82009 100644 --- a/client/system/info_linux.go +++ b/client/system/info_linux.go @@ -61,7 +61,7 @@ func GetInfo(ctx context.Context) *Info { Hostname: extractDeviceName(ctx, systemHostname), GoOS: runtime.GOOS, CPUs: runtime.NumCPU(), - WiretrusteeVersion: version.NetbirdVersion(), + NetbirdVersion: version.NetbirdVersion(), UIVersion: extractUserAgent(ctx), KernelVersion: osInfo[1], NetworkAddresses: addrs, diff --git a/client/system/info_test.go b/client/system/info_test.go index f44219d9e..27821f3c5 100644 --- a/client/system/info_test.go +++ b/client/system/info_test.go @@ -11,7 +11,7 @@ import ( func Test_LocalWTVersion(t *testing.T) { got := GetInfo(context.TODO()) want := "development" - assert.Equal(t, want, got.WiretrusteeVersion) + assert.Equal(t, want, got.NetbirdVersion) } func Test_UIVersion(t *testing.T) { diff --git a/client/system/info_windows.go b/client/system/info_windows.go index f3f387f28..6f05ded20 100644 --- a/client/system/info_windows.go +++ b/client/system/info_windows.go @@ -64,7 +64,7 @@ func GetInfo(ctx context.Context) *Info { systemHostname, _ := os.Hostname() gio.Hostname = extractDeviceName(ctx, systemHostname) - gio.WiretrusteeVersion = version.NetbirdVersion() + gio.NetbirdVersion = version.NetbirdVersion() gio.UIVersion = extractUserAgent(ctx) return gio diff --git a/management/client/client_test.go b/management/client/client_test.go index 8bd8af8d2..3e498a5ea 100644 --- a/management/client/client_test.go +++ b/management/client/client_test.go @@ -273,8 +273,8 @@ func TestClient_Sync(t *testing.T) { if resp.GetPeerConfig() == nil { t.Error("expecting non nil PeerConfig got nil") } - if resp.GetWiretrusteeConfig() == nil { - t.Error("expecting non nil WiretrusteeConfig got nil") + if resp.GetNetbirdConfig() == nil { + t.Error("expecting non nil NetbirdConfig got nil") } if len(resp.GetRemotePeers()) != 1 { t.Errorf("expecting RemotePeers size %d got %d", 1, len(resp.GetRemotePeers())) @@ -366,15 +366,15 @@ func Test_SystemMetaDataFromClient(t *testing.T) { } expectedMeta := &mgmtProto.PeerSystemMeta{ - Hostname: info.Hostname, - GoOS: info.GoOS, - Kernel: info.Kernel, - Platform: info.Platform, - OS: info.OS, - Core: info.OSVersion, - OSVersion: info.OSVersion, - WiretrusteeVersion: info.WiretrusteeVersion, - KernelVersion: info.KernelVersion, + Hostname: info.Hostname, + GoOS: info.GoOS, + Kernel: info.Kernel, + Platform: info.Platform, + OS: info.OS, + Core: info.OSVersion, + OSVersion: info.OSVersion, + NetbirdVersion: info.NetbirdVersion, + KernelVersion: info.KernelVersion, NetworkAddresses: protoNetAddr, SysSerialNumber: info.SystemSerialNumber, @@ -417,7 +417,7 @@ func isEqual(a, b *mgmtProto.PeerSystemMeta) bool { a.GetPlatform() == b.GetPlatform() && a.GetOS() == b.GetOS() && a.GetOSVersion() == b.GetOSVersion() && - a.GetWiretrusteeVersion() == b.GetWiretrusteeVersion() && + a.GetNetbirdVersion() == b.GetNetbirdVersion() && a.GetUiVersion() == b.GetUiVersion() && a.GetSysSerialNumber() == b.GetSysSerialNumber() && a.GetSysProductName() == b.GetSysProductName() && diff --git a/management/client/grpc.go b/management/client/grpc.go index 9a9c603df..53f66da18 100644 --- a/management/client/grpc.go +++ b/management/client/grpc.go @@ -521,20 +521,20 @@ func infoToMetaData(info *system.Info) *proto.PeerSystemMeta { } return &proto.PeerSystemMeta{ - Hostname: info.Hostname, - GoOS: info.GoOS, - OS: info.OS, - Core: info.OSVersion, - OSVersion: info.OSVersion, - Platform: info.Platform, - Kernel: info.Kernel, - WiretrusteeVersion: info.WiretrusteeVersion, - UiVersion: info.UIVersion, - KernelVersion: info.KernelVersion, - NetworkAddresses: addresses, - SysSerialNumber: info.SystemSerialNumber, - SysManufacturer: info.SystemManufacturer, - SysProductName: info.SystemProductName, + Hostname: info.Hostname, + GoOS: info.GoOS, + OS: info.OS, + Core: info.OSVersion, + OSVersion: info.OSVersion, + Platform: info.Platform, + Kernel: info.Kernel, + NetbirdVersion: info.NetbirdVersion, + UiVersion: info.UIVersion, + KernelVersion: info.KernelVersion, + NetworkAddresses: addresses, + SysSerialNumber: info.SystemSerialNumber, + SysManufacturer: info.SystemManufacturer, + SysProductName: info.SystemProductName, Environment: &proto.Environment{ Cloud: info.Environment.Cloud, Platform: info.Environment.Platform, diff --git a/management/proto/management.pb.go b/management/proto/management.pb.go index ae6559675..a654a6365 100644 --- a/management/proto/management.pb.go +++ b/management/proto/management.pb.go @@ -278,7 +278,7 @@ type EncryptedMessage struct { WgPubKey string `protobuf:"bytes,1,opt,name=wgPubKey,proto3" json:"wgPubKey,omitempty"` // encrypted message Body Body []byte `protobuf:"bytes,2,opt,name=body,proto3" json:"body,omitempty"` - // Version of the Wiretrustee Management Service protocol + // Version of the Netbird Management Service protocol Version int32 `protobuf:"varint,3,opt,name=version,proto3" json:"version,omitempty"` } @@ -383,14 +383,14 @@ func (x *SyncRequest) GetMeta() *PeerSystemMeta { return nil } -// SyncResponse represents a state that should be applied to the local peer (e.g. Wiretrustee servers config as well as local peer and remote peers configs) +// SyncResponse represents a state that should be applied to the local peer (e.g. Netbird servers config as well as local peer and remote peers configs) type SyncResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // Global config - WiretrusteeConfig *WiretrusteeConfig `protobuf:"bytes,1,opt,name=wiretrusteeConfig,proto3" json:"wiretrusteeConfig,omitempty"` + NetbirdConfig *NetbirdConfig `protobuf:"bytes,1,opt,name=netbirdConfig,proto3" json:"netbirdConfig,omitempty"` // Deprecated. Use NetworkMap.PeerConfig PeerConfig *PeerConfig `protobuf:"bytes,2,opt,name=peerConfig,proto3" json:"peerConfig,omitempty"` // Deprecated. Use NetworkMap.RemotePeerConfig @@ -435,9 +435,9 @@ func (*SyncResponse) Descriptor() ([]byte, []int) { return file_management_proto_rawDescGZIP(), []int{2} } -func (x *SyncResponse) GetWiretrusteeConfig() *WiretrusteeConfig { +func (x *SyncResponse) GetNetbirdConfig() *NetbirdConfig { if x != nil { - return x.WiretrusteeConfig + return x.NetbirdConfig } return nil } @@ -885,23 +885,23 @@ type PeerSystemMeta struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Hostname string `protobuf:"bytes,1,opt,name=hostname,proto3" json:"hostname,omitempty"` - GoOS string `protobuf:"bytes,2,opt,name=goOS,proto3" json:"goOS,omitempty"` - Kernel string `protobuf:"bytes,3,opt,name=kernel,proto3" json:"kernel,omitempty"` - Core string `protobuf:"bytes,4,opt,name=core,proto3" json:"core,omitempty"` - Platform string `protobuf:"bytes,5,opt,name=platform,proto3" json:"platform,omitempty"` - OS string `protobuf:"bytes,6,opt,name=OS,proto3" json:"OS,omitempty"` - WiretrusteeVersion string `protobuf:"bytes,7,opt,name=wiretrusteeVersion,proto3" json:"wiretrusteeVersion,omitempty"` - UiVersion string `protobuf:"bytes,8,opt,name=uiVersion,proto3" json:"uiVersion,omitempty"` - KernelVersion string `protobuf:"bytes,9,opt,name=kernelVersion,proto3" json:"kernelVersion,omitempty"` - OSVersion string `protobuf:"bytes,10,opt,name=OSVersion,proto3" json:"OSVersion,omitempty"` - NetworkAddresses []*NetworkAddress `protobuf:"bytes,11,rep,name=networkAddresses,proto3" json:"networkAddresses,omitempty"` - SysSerialNumber string `protobuf:"bytes,12,opt,name=sysSerialNumber,proto3" json:"sysSerialNumber,omitempty"` - SysProductName string `protobuf:"bytes,13,opt,name=sysProductName,proto3" json:"sysProductName,omitempty"` - SysManufacturer string `protobuf:"bytes,14,opt,name=sysManufacturer,proto3" json:"sysManufacturer,omitempty"` - Environment *Environment `protobuf:"bytes,15,opt,name=environment,proto3" json:"environment,omitempty"` - Files []*File `protobuf:"bytes,16,rep,name=files,proto3" json:"files,omitempty"` - Flags *Flags `protobuf:"bytes,17,opt,name=flags,proto3" json:"flags,omitempty"` + Hostname string `protobuf:"bytes,1,opt,name=hostname,proto3" json:"hostname,omitempty"` + GoOS string `protobuf:"bytes,2,opt,name=goOS,proto3" json:"goOS,omitempty"` + Kernel string `protobuf:"bytes,3,opt,name=kernel,proto3" json:"kernel,omitempty"` + Core string `protobuf:"bytes,4,opt,name=core,proto3" json:"core,omitempty"` + Platform string `protobuf:"bytes,5,opt,name=platform,proto3" json:"platform,omitempty"` + OS string `protobuf:"bytes,6,opt,name=OS,proto3" json:"OS,omitempty"` + NetbirdVersion string `protobuf:"bytes,7,opt,name=netbirdVersion,proto3" json:"netbirdVersion,omitempty"` + UiVersion string `protobuf:"bytes,8,opt,name=uiVersion,proto3" json:"uiVersion,omitempty"` + KernelVersion string `protobuf:"bytes,9,opt,name=kernelVersion,proto3" json:"kernelVersion,omitempty"` + OSVersion string `protobuf:"bytes,10,opt,name=OSVersion,proto3" json:"OSVersion,omitempty"` + NetworkAddresses []*NetworkAddress `protobuf:"bytes,11,rep,name=networkAddresses,proto3" json:"networkAddresses,omitempty"` + SysSerialNumber string `protobuf:"bytes,12,opt,name=sysSerialNumber,proto3" json:"sysSerialNumber,omitempty"` + SysProductName string `protobuf:"bytes,13,opt,name=sysProductName,proto3" json:"sysProductName,omitempty"` + SysManufacturer string `protobuf:"bytes,14,opt,name=sysManufacturer,proto3" json:"sysManufacturer,omitempty"` + Environment *Environment `protobuf:"bytes,15,opt,name=environment,proto3" json:"environment,omitempty"` + Files []*File `protobuf:"bytes,16,rep,name=files,proto3" json:"files,omitempty"` + Flags *Flags `protobuf:"bytes,17,opt,name=flags,proto3" json:"flags,omitempty"` } func (x *PeerSystemMeta) Reset() { @@ -978,9 +978,9 @@ func (x *PeerSystemMeta) GetOS() string { return "" } -func (x *PeerSystemMeta) GetWiretrusteeVersion() string { +func (x *PeerSystemMeta) GetNetbirdVersion() string { if x != nil { - return x.WiretrusteeVersion + return x.NetbirdVersion } return "" } @@ -1061,7 +1061,7 @@ type LoginResponse struct { unknownFields protoimpl.UnknownFields // Global config - WiretrusteeConfig *WiretrusteeConfig `protobuf:"bytes,1,opt,name=wiretrusteeConfig,proto3" json:"wiretrusteeConfig,omitempty"` + NetbirdConfig *NetbirdConfig `protobuf:"bytes,1,opt,name=netbirdConfig,proto3" json:"netbirdConfig,omitempty"` // Peer local config PeerConfig *PeerConfig `protobuf:"bytes,2,opt,name=peerConfig,proto3" json:"peerConfig,omitempty"` // Posture checks to be evaluated by client @@ -1100,9 +1100,9 @@ func (*LoginResponse) Descriptor() ([]byte, []int) { return file_management_proto_rawDescGZIP(), []int{10} } -func (x *LoginResponse) GetWiretrusteeConfig() *WiretrusteeConfig { +func (x *LoginResponse) GetNetbirdConfig() *NetbirdConfig { if x != nil { - return x.WiretrusteeConfig + return x.NetbirdConfig } return nil } @@ -1130,7 +1130,7 @@ type ServerKeyResponse struct { Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` // Key expiration timestamp after which the key should be fetched again by the client ExpiresAt *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=expiresAt,proto3" json:"expiresAt,omitempty"` - // Version of the Wiretrustee Management Service protocol + // Version of the Netbird Management Service protocol Version int32 `protobuf:"varint,3,opt,name=version,proto3" json:"version,omitempty"` } @@ -1225,8 +1225,8 @@ func (*Empty) Descriptor() ([]byte, []int) { return file_management_proto_rawDescGZIP(), []int{12} } -// WiretrusteeConfig is a common configuration of any Wiretrustee peer. It contains STUN, TURN, Signal and Management servers configurations -type WiretrusteeConfig struct { +// NetbirdConfig is a common configuration of any Netbird peer. It contains STUN, TURN, Signal and Management servers configurations +type NetbirdConfig struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields @@ -1240,8 +1240,8 @@ type WiretrusteeConfig struct { Relay *RelayConfig `protobuf:"bytes,4,opt,name=relay,proto3" json:"relay,omitempty"` } -func (x *WiretrusteeConfig) Reset() { - *x = WiretrusteeConfig{} +func (x *NetbirdConfig) Reset() { + *x = NetbirdConfig{} if protoimpl.UnsafeEnabled { mi := &file_management_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -1249,13 +1249,13 @@ func (x *WiretrusteeConfig) Reset() { } } -func (x *WiretrusteeConfig) String() string { +func (x *NetbirdConfig) String() string { return protoimpl.X.MessageStringOf(x) } -func (*WiretrusteeConfig) ProtoMessage() {} +func (*NetbirdConfig) ProtoMessage() {} -func (x *WiretrusteeConfig) ProtoReflect() protoreflect.Message { +func (x *NetbirdConfig) ProtoReflect() protoreflect.Message { mi := &file_management_proto_msgTypes[13] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -1267,33 +1267,33 @@ func (x *WiretrusteeConfig) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use WiretrusteeConfig.ProtoReflect.Descriptor instead. -func (*WiretrusteeConfig) Descriptor() ([]byte, []int) { +// Deprecated: Use NetbirdConfig.ProtoReflect.Descriptor instead. +func (*NetbirdConfig) Descriptor() ([]byte, []int) { return file_management_proto_rawDescGZIP(), []int{13} } -func (x *WiretrusteeConfig) GetStuns() []*HostConfig { +func (x *NetbirdConfig) GetStuns() []*HostConfig { if x != nil { return x.Stuns } return nil } -func (x *WiretrusteeConfig) GetTurns() []*ProtectedHostConfig { +func (x *NetbirdConfig) GetTurns() []*ProtectedHostConfig { if x != nil { return x.Turns } return nil } -func (x *WiretrusteeConfig) GetSignal() *HostConfig { +func (x *NetbirdConfig) GetSignal() *HostConfig { if x != nil { return x.Signal } return nil } -func (x *WiretrusteeConfig) GetRelay() *RelayConfig { +func (x *NetbirdConfig) GetRelay() *RelayConfig { if x != nil { return x.Relay } @@ -1306,7 +1306,7 @@ type HostConfig struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - // URI of the resource e.g. turns://stun.wiretrustee.com:4430 or signal.wiretrustee.com:10000 + // URI of the resource e.g. turns://stun.netbird.io:4430 or signal.netbird.io:10000 Uri string `protobuf:"bytes,1,opt,name=uri,proto3" json:"uri,omitempty"` Protocol HostConfig_Protocol `protobuf:"varint,2,opt,name=protocol,proto3,enum=management.HostConfig_Protocol" json:"protocol,omitempty"` } @@ -1492,9 +1492,9 @@ type PeerConfig struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - // Peer's virtual IP address within the Wiretrustee VPN (a Wireguard address config) + // Peer's virtual IP address within the Netbird VPN (a Wireguard address config) Address string `protobuf:"bytes,1,opt,name=address,proto3" json:"address,omitempty"` - // Wiretrustee DNS server (a Wireguard DNS config) + // Netbird DNS server (a Wireguard DNS config) Dns string `protobuf:"bytes,2,opt,name=dns,proto3" json:"dns,omitempty"` // SSHConfig of the peer. SshConfig *SSHConfig `protobuf:"bytes,3,opt,name=sshConfig,proto3" json:"sshConfig,omitempty"` @@ -3067,125 +3067,123 @@ var file_management_proto_rawDesc = []byte{ 0x0b, 0x53, 0x79, 0x6e, 0x63, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2e, 0x0a, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x53, 0x79, 0x73, 0x74, - 0x65, 0x6d, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x22, 0xe7, 0x02, 0x0a, - 0x0c, 0x53, 0x79, 0x6e, 0x63, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4b, 0x0a, - 0x11, 0x77, 0x69, 0x72, 0x65, 0x74, 0x72, 0x75, 0x73, 0x74, 0x65, 0x65, 0x43, 0x6f, 0x6e, 0x66, - 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, - 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x57, 0x69, 0x72, 0x65, 0x74, 0x72, 0x75, 0x73, 0x74, 0x65, - 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x11, 0x77, 0x69, 0x72, 0x65, 0x74, 0x72, 0x75, - 0x73, 0x74, 0x65, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x36, 0x0a, 0x0a, 0x70, 0x65, - 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, - 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x65, 0x65, 0x72, - 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0a, 0x70, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, - 0x69, 0x67, 0x12, 0x3e, 0x0a, 0x0b, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, - 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, - 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x43, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0b, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, - 0x72, 0x73, 0x12, 0x2e, 0x0a, 0x12, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, - 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x12, - 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, - 0x74, 0x79, 0x12, 0x36, 0x0a, 0x0a, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, - 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, - 0x65, 0x6e, 0x74, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x52, 0x0a, - 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x12, 0x2a, 0x0a, 0x06, 0x43, 0x68, - 0x65, 0x63, 0x6b, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x6d, 0x61, 0x6e, - 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x52, 0x06, - 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x22, 0x41, 0x0a, 0x0f, 0x53, 0x79, 0x6e, 0x63, 0x4d, 0x65, - 0x74, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2e, 0x0a, 0x04, 0x6d, 0x65, 0x74, - 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, - 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x4d, - 0x65, 0x74, 0x61, 0x52, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x22, 0xa8, 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, 0x2e, 0x0a, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, - 0x74, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x4d, 0x65, 0x74, 0x61, - 0x52, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x12, 0x1a, 0x0a, 0x08, 0x6a, 0x77, 0x74, 0x54, 0x6f, 0x6b, - 0x65, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6a, 0x77, 0x74, 0x54, 0x6f, 0x6b, - 0x65, 0x6e, 0x12, 0x30, 0x0a, 0x08, 0x70, 0x65, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x73, 0x18, 0x04, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, - 0x74, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x73, 0x52, 0x08, 0x70, 0x65, 0x65, 0x72, - 0x4b, 0x65, 0x79, 0x73, 0x22, 0x44, 0x0a, 0x08, 0x50, 0x65, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x73, - 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x73, 0x68, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x0c, 0x52, 0x09, 0x73, 0x73, 0x68, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x12, 0x1a, - 0x0a, 0x08, 0x77, 0x67, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, - 0x52, 0x08, 0x77, 0x67, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x22, 0x3f, 0x0a, 0x0b, 0x45, 0x6e, - 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x6c, 0x6f, - 0x75, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x12, - 0x1a, 0x0a, 0x08, 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x08, 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x22, 0x5c, 0x0a, 0x04, 0x46, - 0x69, 0x6c, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x78, 0x69, 0x73, 0x74, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x05, 0x65, 0x78, 0x69, 0x73, 0x74, 0x12, 0x2a, 0x0a, - 0x10, 0x70, 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x49, 0x73, 0x52, 0x75, 0x6e, 0x6e, 0x69, 0x6e, - 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x70, 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, - 0x49, 0x73, 0x52, 0x75, 0x6e, 0x6e, 0x69, 0x6e, 0x67, 0x22, 0xbf, 0x02, 0x0a, 0x05, 0x46, 0x6c, - 0x61, 0x67, 0x73, 0x12, 0x2a, 0x0a, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, - 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x72, - 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, - 0x30, 0x0a, 0x13, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, 0x65, 0x72, 0x6d, - 0x69, 0x73, 0x73, 0x69, 0x76, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x13, 0x72, 0x6f, - 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x76, - 0x65, 0x12, 0x2a, 0x0a, 0x10, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x53, 0x53, 0x48, 0x41, 0x6c, - 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x73, 0x65, 0x72, - 0x76, 0x65, 0x72, 0x53, 0x53, 0x48, 0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x12, 0x30, 0x0a, - 0x13, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, 0x6f, - 0x75, 0x74, 0x65, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x13, 0x64, 0x69, 0x73, 0x61, - 0x62, 0x6c, 0x65, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x12, - 0x30, 0x0a, 0x13, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, - 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x13, 0x64, 0x69, - 0x73, 0x61, 0x62, 0x6c, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x6f, 0x75, 0x74, 0x65, - 0x73, 0x12, 0x1e, 0x0a, 0x0a, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x44, 0x4e, 0x53, 0x18, - 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x44, 0x4e, - 0x53, 0x12, 0x28, 0x0a, 0x0f, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x46, 0x69, 0x72, 0x65, - 0x77, 0x61, 0x6c, 0x6c, 0x18, 0x07, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, 0x64, 0x69, 0x73, 0x61, - 0x62, 0x6c, 0x65, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x22, 0xfa, 0x04, 0x0a, 0x0e, - 0x50, 0x65, 0x65, 0x72, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x4d, 0x65, 0x74, 0x61, 0x12, 0x1a, - 0x0a, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x67, 0x6f, - 0x4f, 0x53, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x67, 0x6f, 0x4f, 0x53, 0x12, 0x16, - 0x0a, 0x06, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, - 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, 0x72, 0x65, 0x18, 0x04, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x63, 0x6f, 0x72, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x6c, - 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x6c, - 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x12, 0x0e, 0x0a, 0x02, 0x4f, 0x53, 0x18, 0x06, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x02, 0x4f, 0x53, 0x12, 0x2e, 0x0a, 0x12, 0x77, 0x69, 0x72, 0x65, 0x74, 0x72, - 0x75, 0x73, 0x74, 0x65, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x07, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x12, 0x77, 0x69, 0x72, 0x65, 0x74, 0x72, 0x75, 0x73, 0x74, 0x65, 0x65, 0x56, - 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1c, 0x0a, 0x09, 0x75, 0x69, 0x56, 0x65, 0x72, 0x73, - 0x69, 0x6f, 0x6e, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x75, 0x69, 0x56, 0x65, 0x72, - 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x24, 0x0a, 0x0d, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x56, 0x65, - 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6b, 0x65, 0x72, - 0x6e, 0x65, 0x6c, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1c, 0x0a, 0x09, 0x4f, 0x53, - 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x4f, - 0x53, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x46, 0x0a, 0x10, 0x6e, 0x65, 0x74, 0x77, - 0x6f, 0x72, 0x6b, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x18, 0x0b, 0x20, 0x03, - 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, - 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x52, 0x10, - 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, - 0x12, 0x28, 0x0a, 0x0f, 0x73, 0x79, 0x73, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x4e, 0x75, 0x6d, - 0x62, 0x65, 0x72, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x73, 0x79, 0x73, 0x53, 0x65, - 0x72, 0x69, 0x61, 0x6c, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x12, 0x26, 0x0a, 0x0e, 0x73, 0x79, - 0x73, 0x50, 0x72, 0x6f, 0x64, 0x75, 0x63, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x0d, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x0e, 0x73, 0x79, 0x73, 0x50, 0x72, 0x6f, 0x64, 0x75, 0x63, 0x74, 0x4e, 0x61, - 0x6d, 0x65, 0x12, 0x28, 0x0a, 0x0f, 0x73, 0x79, 0x73, 0x4d, 0x61, 0x6e, 0x75, 0x66, 0x61, 0x63, - 0x74, 0x75, 0x72, 0x65, 0x72, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x73, 0x79, 0x73, - 0x4d, 0x61, 0x6e, 0x75, 0x66, 0x61, 0x63, 0x74, 0x75, 0x72, 0x65, 0x72, 0x12, 0x39, 0x0a, 0x0b, - 0x65, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x18, 0x0f, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x17, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, - 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x0b, 0x65, 0x6e, 0x76, 0x69, - 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x26, 0x0a, 0x05, 0x66, 0x69, 0x6c, 0x65, 0x73, - 0x18, 0x10, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, - 0x65, 0x6e, 0x74, 0x2e, 0x46, 0x69, 0x6c, 0x65, 0x52, 0x05, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x12, - 0x27, 0x0a, 0x05, 0x66, 0x6c, 0x61, 0x67, 0x73, 0x18, 0x11, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, - 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x46, 0x6c, 0x61, 0x67, - 0x73, 0x52, 0x05, 0x66, 0x6c, 0x61, 0x67, 0x73, 0x22, 0xc0, 0x01, 0x0a, 0x0d, 0x4c, 0x6f, 0x67, - 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4b, 0x0a, 0x11, 0x77, 0x69, - 0x72, 0x65, 0x74, 0x72, 0x75, 0x73, 0x74, 0x65, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, - 0x6e, 0x74, 0x2e, 0x57, 0x69, 0x72, 0x65, 0x74, 0x72, 0x75, 0x73, 0x74, 0x65, 0x65, 0x43, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x52, 0x11, 0x77, 0x69, 0x72, 0x65, 0x74, 0x72, 0x75, 0x73, 0x74, 0x65, - 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x36, 0x0a, 0x0a, 0x70, 0x65, 0x65, 0x72, 0x43, + 0x65, 0x6d, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x22, 0xdb, 0x02, 0x0a, + 0x0c, 0x53, 0x79, 0x6e, 0x63, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3f, 0x0a, + 0x0d, 0x6e, 0x65, 0x74, 0x62, 0x69, 0x72, 0x64, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, + 0x74, 0x2e, 0x4e, 0x65, 0x74, 0x62, 0x69, 0x72, 0x64, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, + 0x0d, 0x6e, 0x65, 0x74, 0x62, 0x69, 0x72, 0x64, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x36, + 0x0a, 0x0a, 0x70, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, + 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0a, 0x70, 0x65, 0x65, 0x72, + 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x3e, 0x0a, 0x0b, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, + 0x50, 0x65, 0x65, 0x72, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x6d, 0x61, + 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, + 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0b, 0x72, 0x65, 0x6d, 0x6f, 0x74, + 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x12, 0x2e, 0x0a, 0x12, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, + 0x50, 0x65, 0x65, 0x72, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x18, 0x04, 0x20, 0x01, + 0x28, 0x08, 0x52, 0x12, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x49, + 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x36, 0x0a, 0x0a, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, + 0x6b, 0x4d, 0x61, 0x70, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, + 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, + 0x61, 0x70, 0x52, 0x0a, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x12, 0x2a, + 0x0a, 0x06, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, + 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x68, 0x65, 0x63, + 0x6b, 0x73, 0x52, 0x06, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x22, 0x41, 0x0a, 0x0f, 0x53, 0x79, + 0x6e, 0x63, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2e, 0x0a, + 0x04, 0x6d, 0x65, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61, + 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x53, 0x79, 0x73, + 0x74, 0x65, 0x6d, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x22, 0xa8, 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, 0x2e, 0x0a, 0x04, 0x6d, 0x65, + 0x74, 0x61, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, + 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, + 0x4d, 0x65, 0x74, 0x61, 0x52, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x12, 0x1a, 0x0a, 0x08, 0x6a, 0x77, + 0x74, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6a, 0x77, + 0x74, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x30, 0x0a, 0x08, 0x70, 0x65, 0x65, 0x72, 0x4b, 0x65, + 0x79, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, + 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x73, 0x52, 0x08, + 0x70, 0x65, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x73, 0x22, 0x44, 0x0a, 0x08, 0x50, 0x65, 0x65, 0x72, + 0x4b, 0x65, 0x79, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x73, 0x68, 0x50, 0x75, 0x62, 0x4b, 0x65, + 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x73, 0x73, 0x68, 0x50, 0x75, 0x62, 0x4b, + 0x65, 0x79, 0x12, 0x1a, 0x0a, 0x08, 0x77, 0x67, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x77, 0x67, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x22, 0x3f, + 0x0a, 0x0b, 0x45, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x14, 0x0a, + 0x05, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x63, 0x6c, + 0x6f, 0x75, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x22, + 0x5c, 0x0a, 0x04, 0x46, 0x69, 0x6c, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x12, 0x14, 0x0a, 0x05, 0x65, + 0x78, 0x69, 0x73, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x05, 0x65, 0x78, 0x69, 0x73, + 0x74, 0x12, 0x2a, 0x0a, 0x10, 0x70, 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x49, 0x73, 0x52, 0x75, + 0x6e, 0x6e, 0x69, 0x6e, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x70, 0x72, 0x6f, + 0x63, 0x65, 0x73, 0x73, 0x49, 0x73, 0x52, 0x75, 0x6e, 0x6e, 0x69, 0x6e, 0x67, 0x22, 0xbf, 0x02, + 0x0a, 0x05, 0x46, 0x6c, 0x61, 0x67, 0x73, 0x12, 0x2a, 0x0a, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, + 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x08, 0x52, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, + 0x6c, 0x65, 0x64, 0x12, 0x30, 0x0a, 0x13, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, + 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x76, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, + 0x52, 0x13, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, 0x65, 0x72, 0x6d, 0x69, + 0x73, 0x73, 0x69, 0x76, 0x65, 0x12, 0x2a, 0x0a, 0x10, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x53, + 0x53, 0x48, 0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, + 0x10, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x53, 0x53, 0x48, 0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x65, + 0x64, 0x12, 0x30, 0x0a, 0x13, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x43, 0x6c, 0x69, 0x65, + 0x6e, 0x74, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x13, + 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, 0x6f, 0x75, + 0x74, 0x65, 0x73, 0x12, 0x30, 0x0a, 0x13, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x53, 0x65, + 0x72, 0x76, 0x65, 0x72, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, + 0x52, 0x13, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, + 0x6f, 0x75, 0x74, 0x65, 0x73, 0x12, 0x1e, 0x0a, 0x0a, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, + 0x44, 0x4e, 0x53, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x64, 0x69, 0x73, 0x61, 0x62, + 0x6c, 0x65, 0x44, 0x4e, 0x53, 0x12, 0x28, 0x0a, 0x0f, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, + 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x18, 0x07, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, + 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x22, + 0xf2, 0x04, 0x0a, 0x0e, 0x50, 0x65, 0x65, 0x72, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x4d, 0x65, + 0x74, 0x61, 0x12, 0x1a, 0x0a, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, + 0x0a, 0x04, 0x67, 0x6f, 0x4f, 0x53, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x67, 0x6f, + 0x4f, 0x53, 0x12, 0x16, 0x0a, 0x06, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x06, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, + 0x72, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x63, 0x6f, 0x72, 0x65, 0x12, 0x1a, + 0x0a, 0x08, 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x08, 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x12, 0x0e, 0x0a, 0x02, 0x4f, 0x53, + 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x4f, 0x53, 0x12, 0x26, 0x0a, 0x0e, 0x6e, 0x65, + 0x74, 0x62, 0x69, 0x72, 0x64, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x07, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0e, 0x6e, 0x65, 0x74, 0x62, 0x69, 0x72, 0x64, 0x56, 0x65, 0x72, 0x73, 0x69, + 0x6f, 0x6e, 0x12, 0x1c, 0x0a, 0x09, 0x75, 0x69, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, + 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x75, 0x69, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, + 0x12, 0x24, 0x0a, 0x0d, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, + 0x6e, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x56, + 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1c, 0x0a, 0x09, 0x4f, 0x53, 0x56, 0x65, 0x72, 0x73, + 0x69, 0x6f, 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x4f, 0x53, 0x56, 0x65, 0x72, + 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x46, 0x0a, 0x10, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x41, + 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x18, 0x0b, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, + 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x4e, 0x65, 0x74, 0x77, + 0x6f, 0x72, 0x6b, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x52, 0x10, 0x6e, 0x65, 0x74, 0x77, + 0x6f, 0x72, 0x6b, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x12, 0x28, 0x0a, 0x0f, + 0x73, 0x79, 0x73, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x18, + 0x0c, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x73, 0x79, 0x73, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, + 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x12, 0x26, 0x0a, 0x0e, 0x73, 0x79, 0x73, 0x50, 0x72, 0x6f, + 0x64, 0x75, 0x63, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, + 0x73, 0x79, 0x73, 0x50, 0x72, 0x6f, 0x64, 0x75, 0x63, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x28, + 0x0a, 0x0f, 0x73, 0x79, 0x73, 0x4d, 0x61, 0x6e, 0x75, 0x66, 0x61, 0x63, 0x74, 0x75, 0x72, 0x65, + 0x72, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x73, 0x79, 0x73, 0x4d, 0x61, 0x6e, 0x75, + 0x66, 0x61, 0x63, 0x74, 0x75, 0x72, 0x65, 0x72, 0x12, 0x39, 0x0a, 0x0b, 0x65, 0x6e, 0x76, 0x69, + 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, + 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x76, 0x69, 0x72, + 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x0b, 0x65, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, + 0x65, 0x6e, 0x74, 0x12, 0x26, 0x0a, 0x05, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x18, 0x10, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, + 0x46, 0x69, 0x6c, 0x65, 0x52, 0x05, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x12, 0x27, 0x0a, 0x05, 0x66, + 0x6c, 0x61, 0x67, 0x73, 0x18, 0x11, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x6d, 0x61, 0x6e, + 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x46, 0x6c, 0x61, 0x67, 0x73, 0x52, 0x05, 0x66, + 0x6c, 0x61, 0x67, 0x73, 0x22, 0xb4, 0x01, 0x0a, 0x0d, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3f, 0x0a, 0x0d, 0x6e, 0x65, 0x74, 0x62, 0x69, 0x72, + 0x64, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, + 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x4e, 0x65, 0x74, 0x62, 0x69, + 0x72, 0x64, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0d, 0x6e, 0x65, 0x74, 0x62, 0x69, 0x72, + 0x64, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x36, 0x0a, 0x0a, 0x70, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0a, 0x70, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, @@ -3200,308 +3198,308 @@ var file_management_proto_rawDesc = []byte{ 0x70, 0x52, 0x09, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x41, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x07, 0x0a, 0x05, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, - 0xd7, 0x01, 0x0a, 0x11, 0x57, 0x69, 0x72, 0x65, 0x74, 0x72, 0x75, 0x73, 0x74, 0x65, 0x65, 0x43, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x2c, 0x0a, 0x05, 0x73, 0x74, 0x75, 0x6e, 0x73, 0x18, 0x01, - 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, - 0x74, 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x05, 0x73, 0x74, - 0x75, 0x6e, 0x73, 0x12, 0x35, 0x0a, 0x05, 0x74, 0x75, 0x72, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x03, - 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, - 0x50, 0x72, 0x6f, 0x74, 0x65, 0x63, 0x74, 0x65, 0x64, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x52, 0x05, 0x74, 0x75, 0x72, 0x6e, 0x73, 0x12, 0x2e, 0x0a, 0x06, 0x73, 0x69, - 0x67, 0x6e, 0x61, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, + 0xd3, 0x01, 0x0a, 0x0d, 0x4e, 0x65, 0x74, 0x62, 0x69, 0x72, 0x64, 0x43, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x12, 0x2c, 0x0a, 0x05, 0x73, 0x74, 0x75, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x48, 0x6f, + 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x05, 0x73, 0x74, 0x75, 0x6e, 0x73, 0x12, + 0x35, 0x0a, 0x05, 0x74, 0x75, 0x72, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, + 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x72, 0x6f, 0x74, + 0x65, 0x63, 0x74, 0x65, 0x64, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, + 0x05, 0x74, 0x75, 0x72, 0x6e, 0x73, 0x12, 0x2e, 0x0a, 0x06, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, + 0x65, 0x6e, 0x74, 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x06, + 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x12, 0x2d, 0x0a, 0x05, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, + 0x6e, 0x74, 0x2e, 0x52, 0x65, 0x6c, 0x61, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x05, + 0x72, 0x65, 0x6c, 0x61, 0x79, 0x22, 0x98, 0x01, 0x0a, 0x0a, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x69, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x03, 0x75, 0x72, 0x69, 0x12, 0x3b, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, + 0x6f, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1f, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, + 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, + 0x2e, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x63, 0x6f, 0x6c, 0x22, 0x3b, 0x0a, 0x08, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, + 0x07, 0x0a, 0x03, 0x55, 0x44, 0x50, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x54, 0x43, 0x50, 0x10, + 0x01, 0x12, 0x08, 0x0a, 0x04, 0x48, 0x54, 0x54, 0x50, 0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, 0x48, + 0x54, 0x54, 0x50, 0x53, 0x10, 0x03, 0x12, 0x08, 0x0a, 0x04, 0x44, 0x54, 0x4c, 0x53, 0x10, 0x04, + 0x22, 0x6d, 0x0a, 0x0b, 0x52, 0x65, 0x6c, 0x61, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, + 0x12, 0x0a, 0x04, 0x75, 0x72, 0x6c, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x04, 0x75, + 0x72, 0x6c, 0x73, 0x12, 0x22, 0x0a, 0x0c, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x50, 0x61, 0x79, 0x6c, + 0x6f, 0x61, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x74, 0x6f, 0x6b, 0x65, 0x6e, + 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x12, 0x26, 0x0a, 0x0e, 0x74, 0x6f, 0x6b, 0x65, 0x6e, + 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0e, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x22, + 0x7d, 0x0a, 0x13, 0x50, 0x72, 0x6f, 0x74, 0x65, 0x63, 0x74, 0x65, 0x64, 0x48, 0x6f, 0x73, 0x74, + 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x36, 0x0a, 0x0a, 0x68, 0x6f, 0x73, 0x74, 0x43, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, - 0x69, 0x67, 0x52, 0x06, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x12, 0x2d, 0x0a, 0x05, 0x72, 0x65, - 0x6c, 0x61, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x6d, 0x61, 0x6e, 0x61, - 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x65, 0x6c, 0x61, 0x79, 0x43, 0x6f, 0x6e, 0x66, - 0x69, 0x67, 0x52, 0x05, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x22, 0x98, 0x01, 0x0a, 0x0a, 0x48, 0x6f, - 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x69, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x69, 0x12, 0x3b, 0x0a, 0x08, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1f, 0x2e, 0x6d, - 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x52, 0x08, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x22, 0x3b, 0x0a, 0x08, 0x50, 0x72, 0x6f, 0x74, 0x6f, - 0x63, 0x6f, 0x6c, 0x12, 0x07, 0x0a, 0x03, 0x55, 0x44, 0x50, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, - 0x54, 0x43, 0x50, 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x48, 0x54, 0x54, 0x50, 0x10, 0x02, 0x12, - 0x09, 0x0a, 0x05, 0x48, 0x54, 0x54, 0x50, 0x53, 0x10, 0x03, 0x12, 0x08, 0x0a, 0x04, 0x44, 0x54, - 0x4c, 0x53, 0x10, 0x04, 0x22, 0x6d, 0x0a, 0x0b, 0x52, 0x65, 0x6c, 0x61, 0x79, 0x43, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x72, 0x6c, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, - 0x09, 0x52, 0x04, 0x75, 0x72, 0x6c, 0x73, 0x12, 0x22, 0x0a, 0x0c, 0x74, 0x6f, 0x6b, 0x65, 0x6e, - 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x74, - 0x6f, 0x6b, 0x65, 0x6e, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x12, 0x26, 0x0a, 0x0e, 0x74, - 0x6f, 0x6b, 0x65, 0x6e, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x18, 0x03, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x0e, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, - 0x75, 0x72, 0x65, 0x22, 0x7d, 0x0a, 0x13, 0x50, 0x72, 0x6f, 0x74, 0x65, 0x63, 0x74, 0x65, 0x64, - 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x36, 0x0a, 0x0a, 0x68, 0x6f, - 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, - 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x48, 0x6f, 0x73, 0x74, - 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0a, 0x68, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, - 0x69, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, - 0x72, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, - 0x72, 0x64, 0x22, 0xcb, 0x01, 0x0a, 0x0a, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, - 0x67, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x64, - 0x6e, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x64, 0x6e, 0x73, 0x12, 0x33, 0x0a, - 0x09, 0x73, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x15, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x53, - 0x48, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x09, 0x73, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, - 0x69, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x12, 0x48, 0x0a, 0x1f, 0x52, 0x6f, 0x75, 0x74, 0x69, 0x6e, - 0x67, 0x50, 0x65, 0x65, 0x72, 0x44, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x75, 0x74, 0x69, - 0x6f, 0x6e, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, - 0x1f, 0x52, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, 0x50, 0x65, 0x65, 0x72, 0x44, 0x6e, 0x73, 0x52, - 0x65, 0x73, 0x6f, 0x6c, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, - 0x22, 0xf3, 0x04, 0x0a, 0x0a, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x12, - 0x16, 0x0a, 0x06, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, - 0x06, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x12, 0x36, 0x0a, 0x0a, 0x70, 0x65, 0x65, 0x72, 0x43, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, - 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x52, 0x0a, 0x70, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, - 0x3e, 0x0a, 0x0b, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x18, 0x03, - 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, - 0x74, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, - 0x69, 0x67, 0x52, 0x0b, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x12, - 0x2e, 0x0a, 0x12, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x49, 0x73, - 0x45, 0x6d, 0x70, 0x74, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x12, 0x72, 0x65, 0x6d, - 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, - 0x29, 0x0a, 0x06, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, - 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x6f, 0x75, - 0x74, 0x65, 0x52, 0x06, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x12, 0x33, 0x0a, 0x09, 0x44, 0x4e, - 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, - 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x44, 0x4e, 0x53, 0x43, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x52, 0x09, 0x44, 0x4e, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, - 0x40, 0x0a, 0x0c, 0x6f, 0x66, 0x66, 0x6c, 0x69, 0x6e, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x18, - 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, - 0x6e, 0x74, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x52, 0x0c, 0x6f, 0x66, 0x66, 0x6c, 0x69, 0x6e, 0x65, 0x50, 0x65, 0x65, 0x72, - 0x73, 0x12, 0x3e, 0x0a, 0x0d, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, - 0x65, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, - 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, - 0x6c, 0x65, 0x52, 0x0d, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, - 0x73, 0x12, 0x32, 0x0a, 0x14, 0x66, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, - 0x65, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, + 0x69, 0x67, 0x52, 0x0a, 0x68, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, + 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x75, 0x73, + 0x65, 0x72, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, 0xcb, + 0x01, 0x0a, 0x0a, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x18, 0x0a, + 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, + 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x64, 0x6e, 0x73, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x64, 0x6e, 0x73, 0x12, 0x33, 0x0a, 0x09, 0x73, 0x73, 0x68, + 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6d, + 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x53, 0x48, 0x43, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x52, 0x09, 0x73, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, + 0x0a, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x71, + 0x64, 0x6e, 0x12, 0x48, 0x0a, 0x1f, 0x52, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, 0x50, 0x65, 0x65, + 0x72, 0x44, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, + 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x1f, 0x52, 0x6f, 0x75, + 0x74, 0x69, 0x6e, 0x67, 0x50, 0x65, 0x65, 0x72, 0x44, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x6f, 0x6c, + 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x22, 0xf3, 0x04, 0x0a, + 0x0a, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x12, 0x16, 0x0a, 0x06, 0x53, + 0x65, 0x72, 0x69, 0x61, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x53, 0x65, 0x72, + 0x69, 0x61, 0x6c, 0x12, 0x36, 0x0a, 0x0a, 0x70, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, + 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, + 0x0a, 0x70, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x3e, 0x0a, 0x0b, 0x72, + 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x65, + 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0b, + 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x12, 0x2e, 0x0a, 0x12, 0x72, + 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, + 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x12, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, + 0x65, 0x65, 0x72, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x29, 0x0a, 0x06, 0x52, + 0x6f, 0x75, 0x74, 0x65, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x6d, 0x61, + 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x52, 0x06, + 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x12, 0x33, 0x0a, 0x09, 0x44, 0x4e, 0x53, 0x43, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6d, 0x61, 0x6e, 0x61, + 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x44, 0x4e, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, + 0x52, 0x09, 0x44, 0x4e, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x40, 0x0a, 0x0c, 0x6f, + 0x66, 0x66, 0x6c, 0x69, 0x6e, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, + 0x0b, 0x32, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, + 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, + 0x0c, 0x6f, 0x66, 0x66, 0x6c, 0x69, 0x6e, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x12, 0x3e, 0x0a, + 0x0d, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x18, 0x08, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, + 0x74, 0x2e, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x52, 0x0d, + 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x12, 0x32, 0x0a, 0x14, 0x66, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x49, 0x73, - 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x4f, 0x0a, 0x13, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x46, - 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x18, 0x0a, 0x20, 0x03, - 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, - 0x52, 0x6f, 0x75, 0x74, 0x65, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, - 0x65, 0x52, 0x13, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, - 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x12, 0x3e, 0x0a, 0x1a, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, - 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x49, 0x73, 0x45, - 0x6d, 0x70, 0x74, 0x79, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x08, 0x52, 0x1a, 0x72, 0x6f, 0x75, 0x74, - 0x65, 0x73, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x49, - 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x97, 0x01, 0x0a, 0x10, 0x52, 0x65, 0x6d, 0x6f, 0x74, - 0x65, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1a, 0x0a, 0x08, 0x77, - 0x67, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x77, - 0x67, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x12, 0x1e, 0x0a, 0x0a, 0x61, 0x6c, 0x6c, 0x6f, 0x77, - 0x65, 0x64, 0x49, 0x70, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, 0x61, 0x6c, 0x6c, - 0x6f, 0x77, 0x65, 0x64, 0x49, 0x70, 0x73, 0x12, 0x33, 0x0a, 0x09, 0x73, 0x73, 0x68, 0x43, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6d, 0x61, 0x6e, - 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x53, 0x48, 0x43, 0x6f, 0x6e, 0x66, 0x69, - 0x67, 0x52, 0x09, 0x73, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, 0x0a, 0x04, - 0x66, 0x71, 0x64, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x71, 0x64, 0x6e, - 0x22, 0x49, 0x0a, 0x09, 0x53, 0x53, 0x48, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1e, 0x0a, - 0x0a, 0x73, 0x73, 0x68, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x08, 0x52, 0x0a, 0x73, 0x73, 0x68, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x1c, 0x0a, - 0x09, 0x73, 0x73, 0x68, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, - 0x52, 0x09, 0x73, 0x73, 0x68, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x22, 0x20, 0x0a, 0x1e, 0x44, - 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xbf, 0x01, - 0x0a, 0x17, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x12, 0x48, 0x0a, 0x08, 0x50, 0x72, 0x6f, - 0x76, 0x69, 0x64, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x2c, 0x2e, 0x6d, 0x61, - 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, - 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, - 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x52, 0x08, 0x50, 0x72, 0x6f, 0x76, 0x69, - 0x64, 0x65, 0x72, 0x12, 0x42, 0x0a, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61, - 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, - 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, - 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0x16, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x76, 0x69, - 0x64, 0x65, 0x72, 0x12, 0x0a, 0x0a, 0x06, 0x48, 0x4f, 0x53, 0x54, 0x45, 0x44, 0x10, 0x00, 0x22, - 0x1e, 0x0a, 0x1c, 0x50, 0x4b, 0x43, 0x45, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, - 0x5b, 0x0a, 0x15, 0x50, 0x4b, 0x43, 0x45, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x12, 0x42, 0x0a, 0x0e, 0x50, 0x72, 0x6f, 0x76, - 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x72, - 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0e, 0x50, 0x72, - 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0xea, 0x02, 0x0a, - 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, - 0x1a, 0x0a, 0x08, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x08, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x44, 0x12, 0x22, 0x0a, 0x0c, 0x43, - 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x0c, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x12, - 0x16, 0x0a, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x41, 0x75, 0x64, 0x69, 0x65, - 0x6e, 0x63, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x41, 0x75, 0x64, 0x69, 0x65, - 0x6e, 0x63, 0x65, 0x12, 0x2e, 0x0a, 0x12, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, - 0x68, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x12, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x45, 0x6e, 0x64, 0x70, 0x6f, - 0x69, 0x6e, 0x74, 0x12, 0x24, 0x0a, 0x0d, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x45, 0x6e, 0x64, 0x70, - 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x54, 0x6f, 0x6b, 0x65, - 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x53, 0x63, 0x6f, - 0x70, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x12, - 0x1e, 0x0a, 0x0a, 0x55, 0x73, 0x65, 0x49, 0x44, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x08, 0x20, - 0x01, 0x28, 0x08, 0x52, 0x0a, 0x55, 0x73, 0x65, 0x49, 0x44, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, - 0x34, 0x0a, 0x15, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x15, - 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x64, - 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x22, 0x0a, 0x0c, 0x52, 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, - 0x74, 0x55, 0x52, 0x4c, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0c, 0x52, 0x65, 0x64, - 0x69, 0x72, 0x65, 0x63, 0x74, 0x55, 0x52, 0x4c, 0x73, 0x22, 0xed, 0x01, 0x0a, 0x05, 0x52, 0x6f, - 0x75, 0x74, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x02, 0x49, 0x44, 0x12, 0x18, 0x0a, 0x07, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x12, 0x20, 0x0a, - 0x0b, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x54, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x03, 0x52, 0x0b, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x54, 0x79, 0x70, 0x65, 0x12, - 0x12, 0x0a, 0x04, 0x50, 0x65, 0x65, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x50, - 0x65, 0x65, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x18, 0x05, 0x20, - 0x01, 0x28, 0x03, 0x52, 0x06, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x12, 0x1e, 0x0a, 0x0a, 0x4d, - 0x61, 0x73, 0x71, 0x75, 0x65, 0x72, 0x61, 0x64, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, - 0x0a, 0x4d, 0x61, 0x73, 0x71, 0x75, 0x65, 0x72, 0x61, 0x64, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x4e, - 0x65, 0x74, 0x49, 0x44, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x4e, 0x65, 0x74, 0x49, - 0x44, 0x12, 0x18, 0x0a, 0x07, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x08, 0x20, 0x03, - 0x28, 0x09, 0x52, 0x07, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x6b, - 0x65, 0x65, 0x70, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, - 0x6b, 0x65, 0x65, 0x70, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x22, 0xb4, 0x01, 0x0a, 0x09, 0x44, 0x4e, - 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x24, 0x0a, 0x0d, 0x53, 0x65, 0x72, 0x76, 0x69, - 0x63, 0x65, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, - 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x47, 0x0a, - 0x10, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, - 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, - 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, - 0x72, 0x6f, 0x75, 0x70, 0x52, 0x10, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, - 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x12, 0x38, 0x0a, 0x0b, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, - 0x5a, 0x6f, 0x6e, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, - 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a, - 0x6f, 0x6e, 0x65, 0x52, 0x0b, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, 0x73, - 0x22, 0x58, 0x0a, 0x0a, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, 0x12, 0x16, - 0x0a, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, - 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x32, 0x0a, 0x07, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, - 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, - 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x52, 0x65, 0x63, 0x6f, 0x72, - 0x64, 0x52, 0x07, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x22, 0x74, 0x0a, 0x0c, 0x53, 0x69, - 0x6d, 0x70, 0x6c, 0x65, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, - 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x12, - 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x54, 0x79, - 0x70, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x05, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x54, 0x54, 0x4c, 0x18, - 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x03, 0x54, 0x54, 0x4c, 0x12, 0x14, 0x0a, 0x05, 0x52, 0x44, - 0x61, 0x74, 0x61, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x52, 0x44, 0x61, 0x74, 0x61, - 0x22, 0xb3, 0x01, 0x0a, 0x0f, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, - 0x72, 0x6f, 0x75, 0x70, 0x12, 0x38, 0x0a, 0x0b, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, - 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, - 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, - 0x72, 0x52, 0x0b, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x12, 0x18, - 0x0a, 0x07, 0x50, 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, - 0x07, 0x50, 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x44, 0x6f, 0x6d, 0x61, - 0x69, 0x6e, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x44, 0x6f, 0x6d, 0x61, 0x69, - 0x6e, 0x73, 0x12, 0x32, 0x0a, 0x14, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x44, 0x6f, 0x6d, 0x61, - 0x69, 0x6e, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, - 0x52, 0x14, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x45, - 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x22, 0x48, 0x0a, 0x0a, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, - 0x72, 0x76, 0x65, 0x72, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x02, 0x49, 0x50, 0x12, 0x16, 0x0a, 0x06, 0x4e, 0x53, 0x54, 0x79, 0x70, 0x65, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x4e, 0x53, 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, - 0x50, 0x6f, 0x72, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x50, 0x6f, 0x72, 0x74, - 0x22, 0x8b, 0x02, 0x0a, 0x0c, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, - 0x65, 0x12, 0x16, 0x0a, 0x06, 0x50, 0x65, 0x65, 0x72, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x06, 0x50, 0x65, 0x65, 0x72, 0x49, 0x50, 0x12, 0x37, 0x0a, 0x09, 0x44, 0x69, 0x72, - 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x19, 0x2e, 0x6d, - 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x44, 0x69, - 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x09, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, - 0x6f, 0x6e, 0x12, 0x2e, 0x0a, 0x06, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x0e, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, - 0x52, 0x75, 0x6c, 0x65, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x41, 0x63, 0x74, 0x69, - 0x6f, 0x6e, 0x12, 0x34, 0x0a, 0x08, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x04, - 0x20, 0x01, 0x28, 0x0e, 0x32, 0x18, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, - 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x52, 0x08, - 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x6f, 0x72, 0x74, - 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x30, 0x0a, 0x08, - 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, - 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x6f, 0x72, 0x74, - 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x08, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x22, 0x38, - 0x0a, 0x0e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, - 0x12, 0x14, 0x0a, 0x05, 0x6e, 0x65, 0x74, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x05, 0x6e, 0x65, 0x74, 0x49, 0x50, 0x12, 0x10, 0x0a, 0x03, 0x6d, 0x61, 0x63, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x03, 0x6d, 0x61, 0x63, 0x22, 0x1e, 0x0a, 0x06, 0x43, 0x68, 0x65, 0x63, - 0x6b, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, - 0x09, 0x52, 0x05, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x22, 0x96, 0x01, 0x0a, 0x08, 0x50, 0x6f, 0x72, - 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x14, 0x0a, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x0d, 0x48, 0x00, 0x52, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x12, 0x32, 0x0a, 0x05, 0x72, - 0x61, 0x6e, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e, - 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, - 0x2e, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x48, 0x00, 0x52, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x1a, - 0x2f, 0x0a, 0x05, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x72, - 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x12, 0x10, - 0x0a, 0x03, 0x65, 0x6e, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x03, 0x65, 0x6e, 0x64, - 0x42, 0x0f, 0x0a, 0x0d, 0x70, 0x6f, 0x72, 0x74, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, - 0x6e, 0x22, 0xd1, 0x02, 0x0a, 0x11, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x46, 0x69, 0x72, 0x65, 0x77, - 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x22, 0x0a, 0x0c, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0c, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x12, 0x2e, 0x0a, 0x06, 0x61, - 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x16, 0x2e, 0x6d, 0x61, - 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x41, 0x63, 0x74, - 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x20, 0x0a, 0x0b, 0x64, - 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x0b, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x34, 0x0a, - 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, - 0x18, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, - 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x63, 0x6f, 0x6c, 0x12, 0x30, 0x0a, 0x08, 0x70, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x18, - 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, - 0x6e, 0x74, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x08, 0x70, 0x6f, 0x72, - 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x1c, 0x0a, 0x09, 0x69, 0x73, 0x44, 0x79, 0x6e, 0x61, 0x6d, - 0x69, 0x63, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x69, 0x73, 0x44, 0x79, 0x6e, 0x61, - 0x6d, 0x69, 0x63, 0x12, 0x18, 0x0a, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x07, - 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x26, 0x0a, - 0x0e, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, - 0x08, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0e, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x50, 0x72, 0x6f, - 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x2a, 0x4c, 0x0a, 0x0c, 0x52, 0x75, 0x6c, 0x65, 0x50, 0x72, 0x6f, - 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, - 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x41, 0x4c, 0x4c, 0x10, 0x01, 0x12, 0x07, 0x0a, 0x03, 0x54, - 0x43, 0x50, 0x10, 0x02, 0x12, 0x07, 0x0a, 0x03, 0x55, 0x44, 0x50, 0x10, 0x03, 0x12, 0x08, 0x0a, - 0x04, 0x49, 0x43, 0x4d, 0x50, 0x10, 0x04, 0x12, 0x0a, 0x0a, 0x06, 0x43, 0x55, 0x53, 0x54, 0x4f, - 0x4d, 0x10, 0x05, 0x2a, 0x20, 0x0a, 0x0d, 0x52, 0x75, 0x6c, 0x65, 0x44, 0x69, 0x72, 0x65, 0x63, - 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x06, 0x0a, 0x02, 0x49, 0x4e, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, - 0x4f, 0x55, 0x54, 0x10, 0x01, 0x2a, 0x22, 0x0a, 0x0a, 0x52, 0x75, 0x6c, 0x65, 0x41, 0x63, 0x74, - 0x69, 0x6f, 0x6e, 0x12, 0x0a, 0x0a, 0x06, 0x41, 0x43, 0x43, 0x45, 0x50, 0x54, 0x10, 0x00, 0x12, - 0x08, 0x0a, 0x04, 0x44, 0x52, 0x4f, 0x50, 0x10, 0x01, 0x32, 0x90, 0x04, 0x0a, 0x11, 0x4d, 0x61, - 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, - 0x45, 0x0a, 0x05, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, - 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, - 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, - 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, - 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12, 0x46, 0x0a, 0x04, 0x53, 0x79, 0x6e, 0x63, 0x12, 0x1c, - 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, - 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, - 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, - 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x30, 0x01, 0x12, 0x42, - 0x0a, 0x0c, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x12, 0x11, - 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, - 0x79, 0x1a, 0x1d, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, - 0x65, 0x72, 0x76, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x22, 0x00, 0x12, 0x33, 0x0a, 0x09, 0x69, 0x73, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x79, 0x12, - 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, - 0x74, 0x79, 0x1a, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, - 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x5a, 0x0a, 0x1a, 0x47, 0x65, 0x74, 0x44, 0x65, + 0x45, 0x6d, 0x70, 0x74, 0x79, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x14, 0x66, 0x69, 0x72, + 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, + 0x79, 0x12, 0x4f, 0x0a, 0x13, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x46, 0x69, 0x72, 0x65, 0x77, + 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, + 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x6f, 0x75, 0x74, + 0x65, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x52, 0x13, 0x72, + 0x6f, 0x75, 0x74, 0x65, 0x73, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, + 0x65, 0x73, 0x12, 0x3e, 0x0a, 0x1a, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x46, 0x69, 0x72, 0x65, + 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, + 0x18, 0x0b, 0x20, 0x01, 0x28, 0x08, 0x52, 0x1a, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x46, 0x69, + 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, + 0x74, 0x79, 0x22, 0x97, 0x01, 0x0a, 0x10, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, + 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1a, 0x0a, 0x08, 0x77, 0x67, 0x50, 0x75, 0x62, + 0x4b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x77, 0x67, 0x50, 0x75, 0x62, + 0x4b, 0x65, 0x79, 0x12, 0x1e, 0x0a, 0x0a, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x49, 0x70, + 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, + 0x49, 0x70, 0x73, 0x12, 0x33, 0x0a, 0x09, 0x73, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, + 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x53, 0x48, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x09, 0x73, + 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x71, 0x64, 0x6e, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x22, 0x49, 0x0a, 0x09, + 0x53, 0x53, 0x48, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1e, 0x0a, 0x0a, 0x73, 0x73, 0x68, + 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x73, + 0x73, 0x68, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x73, 0x68, + 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x73, 0x73, + 0x68, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x22, 0x20, 0x0a, 0x1e, 0x44, 0x65, 0x76, 0x69, 0x63, + 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, + 0x6f, 0x77, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xbf, 0x01, 0x0a, 0x17, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, - 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, - 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, - 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, - 0x65, 0x22, 0x00, 0x12, 0x58, 0x0a, 0x18, 0x47, 0x65, 0x74, 0x50, 0x4b, 0x43, 0x45, 0x41, 0x75, - 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x12, + 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x12, 0x48, 0x0a, 0x08, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, + 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x2c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, + 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, + 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x2e, 0x70, 0x72, 0x6f, + 0x76, 0x69, 0x64, 0x65, 0x72, 0x52, 0x08, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, + 0x42, 0x0a, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, + 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x52, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x22, 0x16, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, + 0x0a, 0x0a, 0x06, 0x48, 0x4f, 0x53, 0x54, 0x45, 0x44, 0x10, 0x00, 0x22, 0x1e, 0x0a, 0x1c, 0x50, + 0x4b, 0x43, 0x45, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x46, 0x6c, 0x6f, 0x77, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x5b, 0x0a, 0x15, 0x50, + 0x4b, 0x43, 0x45, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x46, 0x6c, 0x6f, 0x77, 0x12, 0x42, 0x0a, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, + 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, + 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, + 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, + 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0xea, 0x02, 0x0a, 0x0e, 0x50, 0x72, 0x6f, + 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1a, 0x0a, 0x08, 0x43, + 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x43, + 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x44, 0x12, 0x22, 0x0a, 0x0c, 0x43, 0x6c, 0x69, 0x65, 0x6e, + 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x43, + 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x44, + 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x44, 0x6f, 0x6d, + 0x61, 0x69, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x41, 0x75, 0x64, 0x69, 0x65, 0x6e, 0x63, 0x65, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x41, 0x75, 0x64, 0x69, 0x65, 0x6e, 0x63, 0x65, 0x12, + 0x2e, 0x0a, 0x12, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x45, 0x6e, 0x64, + 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x44, 0x65, 0x76, + 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, + 0x24, 0x0a, 0x0d, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, + 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x45, 0x6e, 0x64, + 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x18, 0x07, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x55, + 0x73, 0x65, 0x49, 0x44, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, + 0x0a, 0x55, 0x73, 0x65, 0x49, 0x44, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x34, 0x0a, 0x15, 0x41, + 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x64, 0x70, + 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x15, 0x41, 0x75, 0x74, 0x68, + 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, + 0x74, 0x12, 0x22, 0x0a, 0x0c, 0x52, 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x55, 0x52, 0x4c, + 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0c, 0x52, 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, + 0x74, 0x55, 0x52, 0x4c, 0x73, 0x22, 0xed, 0x01, 0x0a, 0x05, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x12, + 0x0e, 0x0a, 0x02, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x44, 0x12, + 0x18, 0x0a, 0x07, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x07, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x12, 0x20, 0x0a, 0x0b, 0x4e, 0x65, 0x74, + 0x77, 0x6f, 0x72, 0x6b, 0x54, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, + 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x50, + 0x65, 0x65, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x50, 0x65, 0x65, 0x72, 0x12, + 0x16, 0x0a, 0x06, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, + 0x06, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x12, 0x1e, 0x0a, 0x0a, 0x4d, 0x61, 0x73, 0x71, 0x75, + 0x65, 0x72, 0x61, 0x64, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x4d, 0x61, 0x73, + 0x71, 0x75, 0x65, 0x72, 0x61, 0x64, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x4e, 0x65, 0x74, 0x49, 0x44, + 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x4e, 0x65, 0x74, 0x49, 0x44, 0x12, 0x18, 0x0a, + 0x07, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, + 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x6b, 0x65, 0x65, 0x70, 0x52, + 0x6f, 0x75, 0x74, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x6b, 0x65, 0x65, 0x70, + 0x52, 0x6f, 0x75, 0x74, 0x65, 0x22, 0xb4, 0x01, 0x0a, 0x09, 0x44, 0x4e, 0x53, 0x43, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x12, 0x24, 0x0a, 0x0d, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x45, 0x6e, + 0x61, 0x62, 0x6c, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x53, 0x65, 0x72, 0x76, + 0x69, 0x63, 0x65, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x47, 0x0a, 0x10, 0x4e, 0x61, 0x6d, + 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x18, 0x02, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, + 0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, + 0x52, 0x10, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, + 0x70, 0x73, 0x12, 0x38, 0x0a, 0x0b, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, + 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, + 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, 0x52, + 0x0b, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, 0x73, 0x22, 0x58, 0x0a, 0x0a, + 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x44, 0x6f, + 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x44, 0x6f, 0x6d, 0x61, + 0x69, 0x6e, 0x12, 0x32, 0x0a, 0x07, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x18, 0x02, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, + 0x2e, 0x53, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x52, 0x07, 0x52, + 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x22, 0x74, 0x0a, 0x0c, 0x53, 0x69, 0x6d, 0x70, 0x6c, 0x65, + 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x54, 0x79, + 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12, 0x14, + 0x0a, 0x05, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x43, + 0x6c, 0x61, 0x73, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x54, 0x54, 0x4c, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x03, 0x52, 0x03, 0x54, 0x54, 0x4c, 0x12, 0x14, 0x0a, 0x05, 0x52, 0x44, 0x61, 0x74, 0x61, 0x18, + 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x52, 0x44, 0x61, 0x74, 0x61, 0x22, 0xb3, 0x01, 0x0a, + 0x0f, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, + 0x12, 0x38, 0x0a, 0x0b, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x18, + 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, + 0x6e, 0x74, 0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x0b, 0x4e, + 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x50, 0x72, + 0x69, 0x6d, 0x61, 0x72, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x50, 0x72, 0x69, + 0x6d, 0x61, 0x72, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, + 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x32, + 0x0a, 0x14, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x45, + 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x14, 0x53, 0x65, + 0x61, 0x72, 0x63, 0x68, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, + 0x65, 0x64, 0x22, 0x48, 0x0a, 0x0a, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, + 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x50, + 0x12, 0x16, 0x0a, 0x06, 0x4e, 0x53, 0x54, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, + 0x52, 0x06, 0x4e, 0x53, 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x6f, 0x72, 0x74, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x22, 0x8b, 0x02, 0x0a, + 0x0c, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x16, 0x0a, + 0x06, 0x50, 0x65, 0x65, 0x72, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x50, + 0x65, 0x65, 0x72, 0x49, 0x50, 0x12, 0x37, 0x0a, 0x09, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, + 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x19, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, + 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x52, 0x09, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x2e, + 0x0a, 0x06, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x16, + 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, + 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x34, + 0x0a, 0x08, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, + 0x32, 0x18, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, + 0x6c, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x52, 0x08, 0x50, 0x72, 0x6f, 0x74, + 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x05, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x30, 0x0a, 0x08, 0x50, 0x6f, 0x72, 0x74, + 0x49, 0x6e, 0x66, 0x6f, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x6d, 0x61, 0x6e, + 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, + 0x52, 0x08, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x22, 0x38, 0x0a, 0x0e, 0x4e, 0x65, + 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x14, 0x0a, 0x05, + 0x6e, 0x65, 0x74, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6e, 0x65, 0x74, + 0x49, 0x50, 0x12, 0x10, 0x0a, 0x03, 0x6d, 0x61, 0x63, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x03, 0x6d, 0x61, 0x63, 0x22, 0x1e, 0x0a, 0x06, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x12, 0x14, + 0x0a, 0x05, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x46, + 0x69, 0x6c, 0x65, 0x73, 0x22, 0x96, 0x01, 0x0a, 0x08, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, + 0x6f, 0x12, 0x14, 0x0a, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x48, + 0x00, 0x52, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x12, 0x32, 0x0a, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, + 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x2e, 0x52, 0x61, 0x6e, + 0x67, 0x65, 0x48, 0x00, 0x52, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x1a, 0x2f, 0x0a, 0x05, 0x52, + 0x61, 0x6e, 0x67, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x0d, 0x52, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x65, 0x6e, + 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x03, 0x65, 0x6e, 0x64, 0x42, 0x0f, 0x0a, 0x0d, + 0x70, 0x6f, 0x72, 0x74, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0xd1, 0x02, + 0x0a, 0x11, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, + 0x75, 0x6c, 0x65, 0x12, 0x22, 0x0a, 0x0c, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x61, 0x6e, + 0x67, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0c, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x12, 0x2e, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, + 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, + 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, + 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x74, 0x69, + 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, + 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x34, 0x0a, 0x08, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x18, 0x2e, 0x6d, 0x61, + 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x50, 0x72, 0x6f, + 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, + 0x30, 0x0a, 0x08, 0x70, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x18, 0x05, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x14, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, + 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x08, 0x70, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, + 0x6f, 0x12, 0x1c, 0x0a, 0x09, 0x69, 0x73, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x18, 0x06, + 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x69, 0x73, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x12, + 0x18, 0x0a, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x09, + 0x52, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x26, 0x0a, 0x0e, 0x63, 0x75, 0x73, + 0x74, 0x6f, 0x6d, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x08, 0x20, 0x01, 0x28, + 0x0d, 0x52, 0x0e, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, + 0x6c, 0x2a, 0x4c, 0x0a, 0x0c, 0x52, 0x75, 0x6c, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, + 0x6c, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x07, + 0x0a, 0x03, 0x41, 0x4c, 0x4c, 0x10, 0x01, 0x12, 0x07, 0x0a, 0x03, 0x54, 0x43, 0x50, 0x10, 0x02, + 0x12, 0x07, 0x0a, 0x03, 0x55, 0x44, 0x50, 0x10, 0x03, 0x12, 0x08, 0x0a, 0x04, 0x49, 0x43, 0x4d, + 0x50, 0x10, 0x04, 0x12, 0x0a, 0x0a, 0x06, 0x43, 0x55, 0x53, 0x54, 0x4f, 0x4d, 0x10, 0x05, 0x2a, + 0x20, 0x0a, 0x0d, 0x52, 0x75, 0x6c, 0x65, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, + 0x12, 0x06, 0x0a, 0x02, 0x49, 0x4e, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x4f, 0x55, 0x54, 0x10, + 0x01, 0x2a, 0x22, 0x0a, 0x0a, 0x52, 0x75, 0x6c, 0x65, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, + 0x0a, 0x0a, 0x06, 0x41, 0x43, 0x43, 0x45, 0x50, 0x54, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x44, + 0x52, 0x4f, 0x50, 0x10, 0x01, 0x32, 0x90, 0x04, 0x0a, 0x11, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, + 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x45, 0x0a, 0x05, 0x4c, + 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, + 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, + 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, + 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, + 0x22, 0x00, 0x12, 0x46, 0x0a, 0x04, 0x53, 0x79, 0x6e, 0x63, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, + 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, + 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, + 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, + 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x30, 0x01, 0x12, 0x42, 0x0a, 0x0c, 0x47, 0x65, + 0x74, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x12, 0x11, 0x2e, 0x6d, 0x61, 0x6e, + 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1d, 0x2e, + 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, + 0x72, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x33, + 0x0a, 0x09, 0x69, 0x73, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x79, 0x12, 0x11, 0x2e, 0x6d, 0x61, + 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x11, + 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, + 0x79, 0x22, 0x00, 0x12, 0x5a, 0x0a, 0x1a, 0x47, 0x65, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, + 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, + 0x77, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, + 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, - 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, - 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, - 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12, 0x3d, 0x0a, - 0x08, 0x53, 0x79, 0x6e, 0x63, 0x4d, 0x65, 0x74, 0x61, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, + 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12, + 0x58, 0x0a, 0x18, 0x47, 0x65, 0x74, 0x50, 0x4b, 0x43, 0x45, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, + 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x12, 0x1c, 0x2e, 0x6d, 0x61, + 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, + 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, - 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, - 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x42, 0x08, 0x5a, 0x06, - 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12, 0x3d, 0x0a, 0x08, 0x53, 0x79, 0x6e, + 0x63, 0x4d, 0x65, 0x74, 0x61, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, + 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, + 0x61, 0x67, 0x65, 0x1a, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, + 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x42, 0x08, 0x5a, 0x06, 0x2f, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -3537,7 +3535,7 @@ var file_management_proto_goTypes = []interface{}{ (*LoginResponse)(nil), // 15: management.LoginResponse (*ServerKeyResponse)(nil), // 16: management.ServerKeyResponse (*Empty)(nil), // 17: management.Empty - (*WiretrusteeConfig)(nil), // 18: management.WiretrusteeConfig + (*NetbirdConfig)(nil), // 18: management.NetbirdConfig (*HostConfig)(nil), // 19: management.HostConfig (*RelayConfig)(nil), // 20: management.RelayConfig (*ProtectedHostConfig)(nil), // 21: management.ProtectedHostConfig @@ -3566,7 +3564,7 @@ var file_management_proto_goTypes = []interface{}{ } var file_management_proto_depIdxs = []int32{ 14, // 0: management.SyncRequest.meta:type_name -> management.PeerSystemMeta - 18, // 1: management.SyncResponse.wiretrusteeConfig:type_name -> management.WiretrusteeConfig + 18, // 1: management.SyncResponse.netbirdConfig:type_name -> management.NetbirdConfig 22, // 2: management.SyncResponse.peerConfig:type_name -> management.PeerConfig 24, // 3: management.SyncResponse.remotePeers:type_name -> management.RemotePeerConfig 23, // 4: management.SyncResponse.NetworkMap:type_name -> management.NetworkMap @@ -3578,14 +3576,14 @@ var file_management_proto_depIdxs = []int32{ 11, // 10: management.PeerSystemMeta.environment:type_name -> management.Environment 12, // 11: management.PeerSystemMeta.files:type_name -> management.File 13, // 12: management.PeerSystemMeta.flags:type_name -> management.Flags - 18, // 13: management.LoginResponse.wiretrusteeConfig:type_name -> management.WiretrusteeConfig + 18, // 13: management.LoginResponse.netbirdConfig:type_name -> management.NetbirdConfig 22, // 14: management.LoginResponse.peerConfig:type_name -> management.PeerConfig 39, // 15: management.LoginResponse.Checks:type_name -> management.Checks 43, // 16: management.ServerKeyResponse.expiresAt:type_name -> google.protobuf.Timestamp - 19, // 17: management.WiretrusteeConfig.stuns:type_name -> management.HostConfig - 21, // 18: management.WiretrusteeConfig.turns:type_name -> management.ProtectedHostConfig - 19, // 19: management.WiretrusteeConfig.signal:type_name -> management.HostConfig - 20, // 20: management.WiretrusteeConfig.relay:type_name -> management.RelayConfig + 19, // 17: management.NetbirdConfig.stuns:type_name -> management.HostConfig + 21, // 18: management.NetbirdConfig.turns:type_name -> management.ProtectedHostConfig + 19, // 19: management.NetbirdConfig.signal:type_name -> management.HostConfig + 20, // 20: management.NetbirdConfig.relay:type_name -> management.RelayConfig 3, // 21: management.HostConfig.protocol:type_name -> management.HostConfig.Protocol 19, // 22: management.ProtectedHostConfig.hostConfig:type_name -> management.HostConfig 25, // 23: management.PeerConfig.sshConfig:type_name -> management.SSHConfig @@ -3796,7 +3794,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[13].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*WiretrusteeConfig); i { + switch v := v.(*NetbirdConfig); i { case 0: return &v.state case 1: diff --git a/management/proto/management.proto b/management/proto/management.proto index 9db66ec4d..b75d3f956 100644 --- a/management/proto/management.proto +++ b/management/proto/management.proto @@ -52,7 +52,7 @@ message EncryptedMessage { // encrypted message Body bytes body = 2; - // Version of the Wiretrustee Management Service protocol + // Version of the Netbird Management Service protocol int32 version = 3; } @@ -61,11 +61,11 @@ message SyncRequest { PeerSystemMeta meta = 1; } -// SyncResponse represents a state that should be applied to the local peer (e.g. Wiretrustee servers config as well as local peer and remote peers configs) +// SyncResponse represents a state that should be applied to the local peer (e.g. Netbird servers config as well as local peer and remote peers configs) message SyncResponse { // Global config - WiretrusteeConfig wiretrusteeConfig = 1; + NetbirdConfig netbirdConfig = 1; // Deprecated. Use NetworkMap.PeerConfig PeerConfig peerConfig = 2; @@ -146,7 +146,7 @@ message PeerSystemMeta { string core = 4; string platform = 5; string OS = 6; - string wiretrusteeVersion = 7; + string netbirdVersion = 7; string uiVersion = 8; string kernelVersion = 9; string OSVersion = 10; @@ -161,7 +161,7 @@ message PeerSystemMeta { message LoginResponse { // Global config - WiretrusteeConfig wiretrusteeConfig = 1; + NetbirdConfig netbirdConfig = 1; // Peer local config PeerConfig peerConfig = 2; // Posture checks to be evaluated by client @@ -173,14 +173,14 @@ message ServerKeyResponse { string key = 1; // Key expiration timestamp after which the key should be fetched again by the client google.protobuf.Timestamp expiresAt = 2; - // Version of the Wiretrustee Management Service protocol + // Version of the Netbird Management Service protocol int32 version = 3; } message Empty {} -// WiretrusteeConfig is a common configuration of any Wiretrustee peer. It contains STUN, TURN, Signal and Management servers configurations -message WiretrusteeConfig { +// NetbirdConfig is a common configuration of any Netbird peer. It contains STUN, TURN, Signal and Management servers configurations +message NetbirdConfig { // a list of STUN servers repeated HostConfig stuns = 1; // a list of TURN servers @@ -194,7 +194,7 @@ message WiretrusteeConfig { // HostConfig describes connection properties of some server (e.g. STUN, Signal, Management) message HostConfig { - // URI of the resource e.g. turns://stun.wiretrustee.com:4430 or signal.wiretrustee.com:10000 + // URI of the resource e.g. turns://stun.netbird.io:4430 or signal.netbird.io:10000 string uri = 1; Protocol protocol = 2; @@ -224,9 +224,9 @@ message ProtectedHostConfig { // PeerConfig represents a configuration of a "our" peer. // The properties are used to configure local Wireguard message PeerConfig { - // Peer's virtual IP address within the Wiretrustee VPN (a Wireguard address config) + // Peer's virtual IP address within the Netbird VPN (a Wireguard address config) string address = 1; - // Wiretrustee DNS server (a Wireguard DNS config) + // Netbird DNS server (a Wireguard DNS config) string dns = 2; // SSHConfig of the peer. diff --git a/management/server/config.go b/management/server/config.go index f3555b92b..397b5f0e6 100644 --- a/management/server/config.go +++ b/management/server/config.go @@ -106,10 +106,10 @@ type HttpServerConfig struct { ExtraAuthAudience string } -// Host represents a Wiretrustee host (e.g. STUN, TURN, Signal) +// Host represents a Netbird host (e.g. STUN, TURN, Signal) type Host struct { Proto Protocol - // URI e.g. turns://stun.wiretrustee.com:4430 or signal.wiretrustee.com:10000 + // URI e.g. turns://stun.netbird.io:4430 or signal.netbird.io:10000 URI string Username string Password string diff --git a/management/server/grpcserver.go b/management/server/grpcserver.go index a21dcd5b8..eec109ee9 100644 --- a/management/server/grpcserver.go +++ b/management/server/grpcserver.go @@ -381,7 +381,7 @@ func extractPeerMeta(ctx context.Context, meta *proto.PeerSystemMeta) nbpeer.Pee Platform: meta.GetPlatform(), OS: meta.GetOS(), OSVersion: osVersion, - WtVersion: meta.GetWiretrusteeVersion(), + WtVersion: meta.GetNetbirdVersion(), UIVersion: meta.GetUiVersion(), KernelVersion: meta.GetKernelVersion(), NetworkAddresses: networkAddresses, @@ -489,9 +489,9 @@ func (s *GRPCServer) Login(ctx context.Context, req *proto.EncryptedMessage) (*p // if peer has reached this point then it has logged in loginResp := &proto.LoginResponse{ - WiretrusteeConfig: toWiretrusteeConfig(s.config, nil, relayToken), - PeerConfig: toPeerConfig(peer, netMap.Network, s.accountManager.GetDNSDomain(), false), - Checks: toProtocolChecks(ctx, postureChecks), + NetbirdConfig: toNetbirdConfig(s.config, nil, relayToken), + PeerConfig: toPeerConfig(peer, netMap.Network, s.accountManager.GetDNSDomain(), false), + Checks: toProtocolChecks(ctx, postureChecks), } encryptedResp, err := encryption.EncryptMessage(peerKey, s.wgKey, loginResp) if err != nil { @@ -547,7 +547,7 @@ func ToResponseProto(configProto Protocol) proto.HostConfig_Protocol { } } -func toWiretrusteeConfig(config *Config, turnCredentials *Token, relayToken *Token) *proto.WiretrusteeConfig { +func toNetbirdConfig(config *Config, turnCredentials *Token, relayToken *Token) *proto.NetbirdConfig { if config == nil { return nil } @@ -595,7 +595,7 @@ func toWiretrusteeConfig(config *Config, turnCredentials *Token, relayToken *Tok } } - return &proto.WiretrusteeConfig{ + return &proto.NetbirdConfig{ Stuns: stuns, Turns: turns, Signal: &proto.HostConfig{ @@ -619,8 +619,8 @@ func toPeerConfig(peer *nbpeer.Peer, network *types.Network, dnsName string, dns func toSyncResponse(ctx context.Context, config *Config, peer *nbpeer.Peer, turnCredentials *Token, relayCredentials *Token, networkMap *types.NetworkMap, dnsName string, checks []*posture.Checks, dnsCache *DNSConfigCache, dnsResolutionOnRoutingPeerEnbled bool) *proto.SyncResponse { response := &proto.SyncResponse{ - WiretrusteeConfig: toWiretrusteeConfig(config, turnCredentials, relayCredentials), - PeerConfig: toPeerConfig(peer, networkMap.Network, dnsName, dnsResolutionOnRoutingPeerEnbled), + NetbirdConfig: toNetbirdConfig(config, turnCredentials, relayCredentials), + PeerConfig: toPeerConfig(peer, networkMap.Network, dnsName, dnsResolutionOnRoutingPeerEnbled), NetworkMap: &proto.NetworkMap{ Serial: networkMap.Network.CurrentSerial(), Routes: toProtocolRoutes(networkMap.Routes), diff --git a/management/server/management_proto_test.go b/management/server/management_proto_test.go index 0df2462f4..bcdf75b8c 100644 --- a/management/server/management_proto_test.go +++ b/management/server/management_proto_test.go @@ -94,7 +94,7 @@ func Test_SyncProtocol(t *testing.T) { mgmtServer, _, mgmtAddr, cleanup, err := startManagementForTest(t, "testdata/store_with_expired_peers.sql", &Config{ Stuns: []*Host{{ Proto: "udp", - URI: "stun:stun.wiretrustee.com:3468", + URI: "stun:stun.netbird.io:3468", }}, TURNConfig: &TURNConfig{ TimeBasedCredentials: false, @@ -102,12 +102,12 @@ func Test_SyncProtocol(t *testing.T) { Secret: "whatever", Turns: []*Host{{ Proto: "udp", - URI: "turn:stun.wiretrustee.com:3468", + URI: "turn:stun.netbird.io:3468", }}, }, Signal: &Host{ Proto: "http", - URI: "signal.wiretrustee.com:10000", + URI: "signal.netbird.io:10000", }, Datadir: dir, HttpConfig: nil, @@ -173,64 +173,64 @@ func Test_SyncProtocol(t *testing.T) { return } - wiretrusteeConfig := syncResp.GetWiretrusteeConfig() - if wiretrusteeConfig == nil { - t.Fatal("expecting SyncResponse to have non-nil WiretrusteeConfig") + netbirdConfig := syncResp.GetNetbirdConfig() + if netbirdConfig == nil { + t.Fatal("expecting SyncResponse to have non-nil NetbirdConfig") } - if wiretrusteeConfig.GetSignal() == nil { - t.Fatal("expecting SyncResponse to have WiretrusteeConfig with non-nil Signal config") + if netbirdConfig.GetSignal() == nil { + t.Fatal("expecting SyncResponse to have NetbirdConfig with non-nil Signal config") } expectedSignalConfig := &mgmtProto.HostConfig{ - Uri: "signal.wiretrustee.com:10000", + Uri: "signal.netbird.io:10000", Protocol: mgmtProto.HostConfig_HTTP, } - if wiretrusteeConfig.GetSignal().GetUri() != expectedSignalConfig.GetUri() { - t.Fatalf("expecting SyncResponse to have WiretrusteeConfig with expected Signal URI: %v, actual: %v", + if netbirdConfig.GetSignal().GetUri() != expectedSignalConfig.GetUri() { + t.Fatalf("expecting SyncResponse to have NetbirdConfig with expected Signal URI: %v, actual: %v", expectedSignalConfig.GetUri(), - wiretrusteeConfig.GetSignal().GetUri()) + netbirdConfig.GetSignal().GetUri()) } - if wiretrusteeConfig.GetSignal().GetProtocol() != expectedSignalConfig.GetProtocol() { - t.Fatalf("expecting SyncResponse to have WiretrusteeConfig with expected Signal Protocol: %v, actual: %v", + if netbirdConfig.GetSignal().GetProtocol() != expectedSignalConfig.GetProtocol() { + t.Fatalf("expecting SyncResponse to have NetbirdConfig with expected Signal Protocol: %v, actual: %v", expectedSignalConfig.GetProtocol().String(), - wiretrusteeConfig.GetSignal().GetProtocol()) + netbirdConfig.GetSignal().GetProtocol()) } expectedStunsConfig := &mgmtProto.HostConfig{ - Uri: "stun:stun.wiretrustee.com:3468", + Uri: "stun:stun.netbird.io:3468", Protocol: mgmtProto.HostConfig_UDP, } - if wiretrusteeConfig.GetStuns()[0].GetUri() != expectedStunsConfig.GetUri() { - t.Fatalf("expecting SyncResponse to have WiretrusteeConfig with expected STUN URI: %v, actual: %v", + if netbirdConfig.GetStuns()[0].GetUri() != expectedStunsConfig.GetUri() { + t.Fatalf("expecting SyncResponse to have NetbirdConfig with expected STUN URI: %v, actual: %v", expectedStunsConfig.GetUri(), - wiretrusteeConfig.GetStuns()[0].GetUri()) + netbirdConfig.GetStuns()[0].GetUri()) } - if wiretrusteeConfig.GetStuns()[0].GetProtocol() != expectedStunsConfig.GetProtocol() { - t.Fatalf("expecting SyncResponse to have WiretrusteeConfig with expected STUN Protocol: %v, actual: %v", + if netbirdConfig.GetStuns()[0].GetProtocol() != expectedStunsConfig.GetProtocol() { + t.Fatalf("expecting SyncResponse to have NetbirdConfig with expected STUN Protocol: %v, actual: %v", expectedStunsConfig.GetProtocol(), - wiretrusteeConfig.GetStuns()[0].GetProtocol()) + netbirdConfig.GetStuns()[0].GetProtocol()) } expectedTRUNHost := &mgmtProto.HostConfig{ - Uri: "turn:stun.wiretrustee.com:3468", + Uri: "turn:stun.netbird.io:3468", Protocol: mgmtProto.HostConfig_UDP, } - if wiretrusteeConfig.GetTurns()[0].GetHostConfig().GetUri() != expectedTRUNHost.GetUri() { - t.Fatalf("expecting SyncResponse to have WiretrusteeConfig with expected TURN URI: %v, actual: %v", + if netbirdConfig.GetTurns()[0].GetHostConfig().GetUri() != expectedTRUNHost.GetUri() { + t.Fatalf("expecting SyncResponse to have NetbirdConfig with expected TURN URI: %v, actual: %v", expectedTRUNHost.GetUri(), - wiretrusteeConfig.GetTurns()[0].GetHostConfig().GetUri()) + netbirdConfig.GetTurns()[0].GetHostConfig().GetUri()) } - if wiretrusteeConfig.GetTurns()[0].GetHostConfig().GetProtocol() != expectedTRUNHost.GetProtocol() { - t.Fatalf("expecting SyncResponse to have WiretrusteeConfig with expected TURN Protocol: %v, actual: %v", + if netbirdConfig.GetTurns()[0].GetHostConfig().GetProtocol() != expectedTRUNHost.GetProtocol() { + t.Fatalf("expecting SyncResponse to have NetbirdConfig with expected TURN Protocol: %v, actual: %v", expectedTRUNHost.GetProtocol().String(), - wiretrusteeConfig.GetTurns()[0].GetHostConfig().GetProtocol()) + netbirdConfig.GetTurns()[0].GetHostConfig().GetProtocol()) } // ensure backward compatibility @@ -285,13 +285,13 @@ func loginPeerWithValidSetupKey(key wgtypes.Key, client mgmtProto.ManagementServ } meta := &mgmtProto.PeerSystemMeta{ - Hostname: key.PublicKey().String(), - GoOS: runtime.GOOS, - OS: runtime.GOOS, - Core: "core", - Platform: "platform", - Kernel: "kernel", - WiretrusteeVersion: "", + Hostname: key.PublicKey().String(), + GoOS: runtime.GOOS, + OS: runtime.GOOS, + Core: "core", + Platform: "platform", + Kernel: "kernel", + NetbirdVersion: "", } message, err := encryption.EncryptMessage(*serverKey, key, &mgmtProto.LoginRequest{SetupKey: TestValidSetupKey, Meta: meta}) if err != nil { @@ -498,7 +498,7 @@ func testSyncStatusRace(t *testing.T) { mgmtServer, am, mgmtAddr, cleanup, err := startManagementForTest(t, "testdata/store_with_expired_peers.sql", &Config{ Stuns: []*Host{{ Proto: "udp", - URI: "stun:stun.wiretrustee.com:3468", + URI: "stun:stun.netbird.io:3468", }}, TURNConfig: &TURNConfig{ TimeBasedCredentials: false, @@ -506,12 +506,12 @@ func testSyncStatusRace(t *testing.T) { Secret: "whatever", Turns: []*Host{{ Proto: "udp", - URI: "turn:stun.wiretrustee.com:3468", + URI: "turn:stun.netbird.io:3468", }}, }, Signal: &Host{ Proto: "http", - URI: "signal.wiretrustee.com:10000", + URI: "signal.netbird.io:10000", }, Datadir: dir, HttpConfig: nil, @@ -670,7 +670,7 @@ func Test_LoginPerformance(t *testing.T) { mgmtServer, am, _, cleanup, err := startManagementForTest(t, "testdata/store_with_expired_peers.sql", &Config{ Stuns: []*Host{{ Proto: "udp", - URI: "stun:stun.wiretrustee.com:3468", + URI: "stun:stun.netbird.io:3468", }}, TURNConfig: &TURNConfig{ TimeBasedCredentials: false, @@ -678,12 +678,12 @@ func Test_LoginPerformance(t *testing.T) { Secret: "whatever", Turns: []*Host{{ Proto: "udp", - URI: "turn:stun.wiretrustee.com:3468", + URI: "turn:stun.netbird.io:3468", }}, }, Signal: &Host{ Proto: "http", - URI: "signal.wiretrustee.com:10000", + URI: "signal.netbird.io:10000", }, Datadir: dir, HttpConfig: nil, @@ -730,13 +730,13 @@ func Test_LoginPerformance(t *testing.T) { } meta := &mgmtProto.PeerSystemMeta{ - Hostname: key.PublicKey().String(), - GoOS: runtime.GOOS, - OS: runtime.GOOS, - Core: "core", - Platform: "platform", - Kernel: "kernel", - WiretrusteeVersion: "", + Hostname: key.PublicKey().String(), + GoOS: runtime.GOOS, + OS: runtime.GOOS, + Core: "core", + Platform: "platform", + Kernel: "kernel", + NetbirdVersion: "", } peerLogin := PeerLogin{ diff --git a/management/server/management_test.go b/management/server/management_test.go index cfa2c138f..43a6e40d5 100644 --- a/management/server/management_test.go +++ b/management/server/management_test.go @@ -47,7 +47,7 @@ var _ = Describe("Management service", func() { level, _ := log.ParseLevel("Debug") log.SetLevel(level) var err error - dataDir, err = os.MkdirTemp("", "wiretrustee_mgmt_test_tmp_*") + dataDir, err = os.MkdirTemp("", "netbird_mgmt_test_tmp_*") Expect(err).NotTo(HaveOccurred()) var listener net.Listener @@ -109,23 +109,23 @@ var _ = Describe("Management service", func() { Expect(err).NotTo(HaveOccurred()) expectedSignalConfig := &mgmtProto.HostConfig{ - Uri: "signal.wiretrustee.com:10000", + Uri: "signal.netbird.io:10000", Protocol: mgmtProto.HostConfig_HTTP, } expectedStunsConfig := &mgmtProto.HostConfig{ - Uri: "stun:stun.wiretrustee.com:3468", + Uri: "stun:stun.netbird.io:3468", Protocol: mgmtProto.HostConfig_UDP, } expectedTRUNHost := &mgmtProto.HostConfig{ - Uri: "turn:stun.wiretrustee.com:3468", + Uri: "turn:stun.netbird.io:3468", Protocol: mgmtProto.HostConfig_UDP, } - Expect(resp.WiretrusteeConfig.Signal).To(BeEquivalentTo(expectedSignalConfig)) - Expect(resp.WiretrusteeConfig.Stuns).To(ConsistOf(expectedStunsConfig)) + Expect(resp.NetbirdConfig.Signal).To(BeEquivalentTo(expectedSignalConfig)) + Expect(resp.NetbirdConfig.Stuns).To(ConsistOf(expectedStunsConfig)) // TURN validation is special because credentials are dynamically generated - Expect(resp.WiretrusteeConfig.Turns).To(HaveLen(1)) - actualTURN := resp.WiretrusteeConfig.Turns[0] + Expect(resp.NetbirdConfig.Turns).To(HaveLen(1)) + actualTURN := resp.NetbirdConfig.Turns[0] Expect(len(actualTURN.User) > 0).To(BeTrue()) Expect(actualTURN.HostConfig).To(BeEquivalentTo(expectedTRUNHost)) Expect(len(resp.NetworkMap.OfflinePeers) == 0).To(BeTrue()) @@ -286,25 +286,25 @@ var _ = Describe("Management service", func() { Expect(err).NotTo(HaveOccurred()) expectedSignalConfig := &mgmtProto.HostConfig{ - Uri: "signal.wiretrustee.com:10000", + Uri: "signal.netbird.io:10000", Protocol: mgmtProto.HostConfig_HTTP, } expectedStunsConfig := &mgmtProto.HostConfig{ - Uri: "stun:stun.wiretrustee.com:3468", + Uri: "stun:stun.netbird.io:3468", Protocol: mgmtProto.HostConfig_UDP, } expectedTurnsConfig := &mgmtProto.ProtectedHostConfig{ HostConfig: &mgmtProto.HostConfig{ - Uri: "turn:stun.wiretrustee.com:3468", + Uri: "turn:stun.netbird.io:3468", Protocol: mgmtProto.HostConfig_UDP, }, User: "some_user", Password: "some_password", } - Expect(decryptedResp.GetWiretrusteeConfig().Signal).To(BeEquivalentTo(expectedSignalConfig)) - Expect(decryptedResp.GetWiretrusteeConfig().Stuns).To(ConsistOf(expectedStunsConfig)) - Expect(decryptedResp.GetWiretrusteeConfig().Turns).To(ConsistOf(expectedTurnsConfig)) + Expect(decryptedResp.GetNetbirdConfig().Signal).To(BeEquivalentTo(expectedSignalConfig)) + Expect(decryptedResp.GetNetbirdConfig().Stuns).To(ConsistOf(expectedStunsConfig)) + Expect(decryptedResp.GetNetbirdConfig().Turns).To(ConsistOf(expectedTurnsConfig)) }) }) }) @@ -449,13 +449,13 @@ func loginPeerWithValidSetupKey(serverPubKey wgtypes.Key, key wgtypes.Key, clien defer GinkgoRecover() meta := &mgmtProto.PeerSystemMeta{ - Hostname: key.PublicKey().String(), - GoOS: runtime.GOOS, - OS: runtime.GOOS, - Core: "core", - Platform: "platform", - Kernel: "kernel", - WiretrusteeVersion: "", + Hostname: key.PublicKey().String(), + GoOS: runtime.GOOS, + OS: runtime.GOOS, + Core: "core", + Platform: "platform", + Kernel: "kernel", + NetbirdVersion: "", } message, err := encryption.EncryptMessage(serverPubKey, key, &mgmtProto.LoginRequest{SetupKey: ValidSetupKey, Meta: meta}) Expect(err).NotTo(HaveOccurred()) diff --git a/management/server/peer_test.go b/management/server/peer_test.go index 40f8d15d5..a0417c996 100644 --- a/management/server/peer_test.go +++ b/management/server/peer_test.go @@ -1099,13 +1099,13 @@ func TestToSyncResponse(t *testing.T) { assert.Equal(t, "192.168.1.1/24", response.PeerConfig.Address) assert.Equal(t, "peer1.example.com", response.PeerConfig.Fqdn) assert.Equal(t, true, response.PeerConfig.SshConfig.SshEnabled) - // assert wiretrustee config - assert.Equal(t, "signal.uri", response.WiretrusteeConfig.Signal.Uri) - assert.Equal(t, proto.HostConfig_HTTPS, response.WiretrusteeConfig.Signal.GetProtocol()) - assert.Equal(t, "stun.uri", response.WiretrusteeConfig.Stuns[0].Uri) - assert.Equal(t, "turn.uri", response.WiretrusteeConfig.Turns[0].HostConfig.GetUri()) - assert.Equal(t, "turn-user", response.WiretrusteeConfig.Turns[0].User) - assert.Equal(t, "turn-pass", response.WiretrusteeConfig.Turns[0].Password) + // assert netbird config + assert.Equal(t, "signal.uri", response.NetbirdConfig.Signal.Uri) + assert.Equal(t, proto.HostConfig_HTTPS, response.NetbirdConfig.Signal.GetProtocol()) + assert.Equal(t, "stun.uri", response.NetbirdConfig.Stuns[0].Uri) + assert.Equal(t, "turn.uri", response.NetbirdConfig.Turns[0].HostConfig.GetUri()) + assert.Equal(t, "turn-user", response.NetbirdConfig.Turns[0].User) + assert.Equal(t, "turn-pass", response.NetbirdConfig.Turns[0].Password) // assert RemotePeers assert.Equal(t, 1, len(response.RemotePeers)) assert.Equal(t, "192.168.1.2/32", response.RemotePeers[0].AllowedIps[0]) diff --git a/management/server/testdata/management.json b/management/server/testdata/management.json index d29491118..f797a7d2b 100644 --- a/management/server/testdata/management.json +++ b/management/server/testdata/management.json @@ -2,7 +2,7 @@ "Stuns": [ { "Proto": "udp", - "URI": "stun:stun.wiretrustee.com:3468", + "URI": "stun:stun.netbird.io:3468", "Username": "", "Password": null } @@ -11,7 +11,7 @@ "Turns": [ { "Proto": "udp", - "URI": "turn:stun.wiretrustee.com:3468", + "URI": "turn:stun.netbird.io:3468", "Username": "some_user", "Password": "some_password" } @@ -22,7 +22,7 @@ }, "Signal": { "Proto": "http", - "URI": "signal.wiretrustee.com:10000", + "URI": "signal.netbird.io:10000", "Username": "", "Password": null }, @@ -44,4 +44,4 @@ "GrantType": "client_credentials" } } -} \ No newline at end of file +} diff --git a/management/server/token_mgr.go b/management/server/token_mgr.go index fd67fa3e3..ec8aae47e 100644 --- a/management/server/token_mgr.go +++ b/management/server/token_mgr.go @@ -199,7 +199,7 @@ func (m *TimeBasedAuthSecretsManager) pushNewTURNAndRelayTokens(ctx context.Cont } update := &proto.SyncResponse{ - WiretrusteeConfig: &proto.WiretrusteeConfig{ + NetbirdConfig: &proto.NetbirdConfig{ Turns: turns, }, } @@ -208,7 +208,7 @@ func (m *TimeBasedAuthSecretsManager) pushNewTURNAndRelayTokens(ctx context.Cont if m.relayCfg != nil { token, err := m.GenerateRelayToken() if err == nil { - update.WiretrusteeConfig.Relay = &proto.RelayConfig{ + update.NetbirdConfig.Relay = &proto.RelayConfig{ Urls: m.relayCfg.Addresses, TokenPayload: token.Payload, TokenSignature: token.Signature, @@ -228,7 +228,7 @@ func (m *TimeBasedAuthSecretsManager) pushNewRelayTokens(ctx context.Context, pe } update := &proto.SyncResponse{ - WiretrusteeConfig: &proto.WiretrusteeConfig{ + NetbirdConfig: &proto.NetbirdConfig{ Relay: &proto.RelayConfig{ Urls: m.relayCfg.Addresses, TokenPayload: string(relayToken.Payload), diff --git a/management/server/token_mgr_test.go b/management/server/token_mgr_test.go index 2aafb9f68..f2b056d8f 100644 --- a/management/server/token_mgr_test.go +++ b/management/server/token_mgr_test.go @@ -18,7 +18,7 @@ import ( var TurnTestHost = &Host{ Proto: UDP, - URI: "turn:turn.wiretrustee.com:77777", + URI: "turn:turn.netbird.io:77777", Username: "username", Password: "", } @@ -124,7 +124,7 @@ loop: var firstRelayUpdate, secondRelayUpdate *proto.RelayConfig for _, update := range updates { - if turns := update.Update.GetWiretrusteeConfig().GetTurns(); len(turns) > 0 { + if turns := update.Update.GetNetbirdConfig().GetTurns(); len(turns) > 0 { turnUpdates++ if turnUpdates == 1 { firstTurnUpdate = turns[0] @@ -132,9 +132,9 @@ loop: secondTurnUpdate = turns[0] } } - if relay := update.Update.GetWiretrusteeConfig().GetRelay(); relay != nil { + if relay := update.Update.GetNetbirdConfig().GetRelay(); relay != nil { // avoid updating on turn updates since they also send relay credentials - if update.Update.GetWiretrusteeConfig().GetTurns() == nil { + if update.Update.GetNetbirdConfig().GetTurns() == nil { relayUpdates++ if relayUpdates == 1 { firstRelayUpdate = relay diff --git a/release_files/darwin-ui-installer.sh b/release_files/darwin-ui-installer.sh index 5179f02d6..de331730f 100644 --- a/release_files/darwin-ui-installer.sh +++ b/release_files/darwin-ui-installer.sh @@ -2,13 +2,13 @@ export PATH=$PATH:/usr/local/bin:/opt/homebrew/bin -# check if wiretrustee is installed -WT_BIN=$(which wiretrustee) -if [ -n "$WT_BIN" ] +# check if netbird is installed +NB_BIN=$(which netbird) +if [ -n "$NB_BIN" ] then - echo "Stopping and uninstalling Wiretrustee daemon" - wiretrustee service stop || true - wiretrustee service uninstall || true + echo "Stopping and uninstalling Netbird daemon" + netbird service stop || true + netbird service uninstall || true fi # check if netbird is installed diff --git a/release_files/install.sh b/release_files/install.sh index bb917c39a..459645c58 100755 --- a/release_files/install.sh +++ b/release_files/install.sh @@ -263,16 +263,16 @@ install_netbird() { add_aur_repo ;; brew) - # Remove Wiretrustee if it had been installed using Homebrew before - if brew ls --versions wiretrustee >/dev/null 2>&1; then - echo "Removing existing wiretrustee client" + # Remove Netbird if it had been installed using Homebrew before + if brew ls --versions netbird >/dev/null 2>&1; then + echo "Removing existing netbird client" # Stop and uninstall daemon service: - wiretrustee service stop - wiretrustee service uninstall + netbird service stop + netbird service uninstall # Unlik the app - brew unlink wiretrustee + brew unlink netbird fi brew install netbirdio/tap/netbird diff --git a/signal/README.md b/signal/README.md index 9e3207cfa..0033eaf90 100644 --- a/signal/README.md +++ b/signal/README.md @@ -60,7 +60,7 @@ subdomain sub.mydomain.com). ```bash # create a volume -docker volume create wiretrustee-signal +docker volume create netbird-signal # run the docker container docker run -d --name netbird-signal \ -p 10000:10000 \ From 125b5e2b163a5c3bd1ac3ca5d99a5c04972748fb Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Wed, 5 Feb 2025 18:55:42 +0100 Subject: [PATCH 44/92] [client] Fix acl empty port range detection (#3285) --- client/internal/acl/manager.go | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/client/internal/acl/manager.go b/client/internal/acl/manager.go index 9ec0bb031..a015e0a49 100644 --- a/client/internal/acl/manager.go +++ b/client/internal/acl/manager.go @@ -268,7 +268,7 @@ func (d *DefaultManager) protoRuleToFirewallRule( } var port *firewall.Port - if r.PortInfo != nil { + if !portInfoEmpty(r.PortInfo) { port = convertPortInfo(r.PortInfo) } else if r.Port != "" { // old version of management, single port @@ -305,6 +305,22 @@ func (d *DefaultManager) protoRuleToFirewallRule( return ruleID, rules, nil } +func portInfoEmpty(portInfo *mgmProto.PortInfo) bool { + if portInfo == nil { + return true + } + + switch portInfo.GetPortSelection().(type) { + case *mgmProto.PortInfo_Port: + return portInfo.GetPort() == 0 + case *mgmProto.PortInfo_Range_: + r := portInfo.GetRange() + return r == nil || r.Start == 0 || r.End == 0 + default: + return true + } +} + func (d *DefaultManager) addInRules( ip net.IP, protocol firewall.Protocol, From fe370e7d8f6feef4bd6aecb584c514eb09b1b679 Mon Sep 17 00:00:00 2001 From: Christian Stewart Date: Wed, 5 Feb 2025 14:03:53 -0800 Subject: [PATCH 45/92] [relay] Use new upstream for nhooyr.io/websocket package (#3287) The nhooyr.io/websocket package was renamed to github.com/coder/websocket when the project was transferred to "coder" as the new maintainer. Use the new import path and update go.mod and go.sum accordingly. Signed-off-by: Christian Stewart --- go.mod | 2 +- go.sum | 4 ++-- relay/client/dialer/ws/conn.go | 2 +- relay/client/dialer/ws/ws.go | 2 +- relay/server/listener/ws/conn.go | 2 +- relay/server/listener/ws/listener.go | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/go.mod b/go.mod index 13adeff09..77d570662 100644 --- a/go.mod +++ b/go.mod @@ -36,6 +36,7 @@ require ( github.com/c-robinson/iplib v1.0.3 github.com/caddyserver/certmagic v0.21.3 github.com/cilium/ebpf v0.15.0 + github.com/coder/websocket v1.8.12 github.com/coreos/go-iptables v0.7.0 github.com/creack/pty v1.1.18 github.com/davecgh/go-spew v1.1.1 @@ -101,7 +102,6 @@ require ( gorm.io/driver/postgres v1.5.7 gorm.io/driver/sqlite v1.5.7 gorm.io/gorm v1.25.12 - nhooyr.io/websocket v1.8.11 ) require ( diff --git a/go.sum b/go.sum index e3670b99e..4b9e90eba 100644 --- a/go.sum +++ b/go.sum @@ -137,6 +137,8 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo= +github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= github.com/containerd/containerd v1.7.16 h1:7Zsfe8Fkj4Wi2My6DXGQ87hiqIrmOXolm72ZEkFU5Mg= github.com/containerd/containerd v1.7.16/go.mod h1:NL49g7A/Fui7ccmxV6zkBWwqMgmMxFWzujYCc+JLt7k= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= @@ -1264,8 +1266,6 @@ k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8 k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E= -nhooyr.io/websocket v1.8.11 h1:f/qXNc2/3DpoSZkHt1DQu6rj4zGC8JmkkLkWss0MgN0= -nhooyr.io/websocket v1.8.11/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/relay/client/dialer/ws/conn.go b/relay/client/dialer/ws/conn.go index 74bcafd82..0086b702b 100644 --- a/relay/client/dialer/ws/conn.go +++ b/relay/client/dialer/ws/conn.go @@ -6,7 +6,7 @@ import ( "net" "time" - "nhooyr.io/websocket" + "github.com/coder/websocket" ) type Conn struct { diff --git a/relay/client/dialer/ws/ws.go b/relay/client/dialer/ws/ws.go index 2adbd2451..b007e24bb 100644 --- a/relay/client/dialer/ws/ws.go +++ b/relay/client/dialer/ws/ws.go @@ -12,7 +12,7 @@ import ( "strings" log "github.com/sirupsen/logrus" - "nhooyr.io/websocket" + "github.com/coder/websocket" "github.com/netbirdio/netbird/relay/server/listener/ws" "github.com/netbirdio/netbird/util/embeddedroots" diff --git a/relay/server/listener/ws/conn.go b/relay/server/listener/ws/conn.go index 12e721fdb..3466b2abd 100644 --- a/relay/server/listener/ws/conn.go +++ b/relay/server/listener/ws/conn.go @@ -9,7 +9,7 @@ import ( "time" log "github.com/sirupsen/logrus" - "nhooyr.io/websocket" + "github.com/coder/websocket" ) const ( diff --git a/relay/server/listener/ws/listener.go b/relay/server/listener/ws/listener.go index 0eb244c77..4597669dc 100644 --- a/relay/server/listener/ws/listener.go +++ b/relay/server/listener/ws/listener.go @@ -9,7 +9,7 @@ import ( "net/http" log "github.com/sirupsen/logrus" - "nhooyr.io/websocket" + "github.com/coder/websocket" ) // URLPath is the path for the websocket connection. From e00a280329f77e9572ec43b0005366916b9bcce7 Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Wed, 5 Feb 2025 23:04:52 +0100 Subject: [PATCH 46/92] [client] Fix grouping of peer ACLs with different port ranges (#3289) --- client/internal/acl/manager.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/internal/acl/manager.go b/client/internal/acl/manager.go index a015e0a49..31173a5f7 100644 --- a/client/internal/acl/manager.go +++ b/client/internal/acl/manager.go @@ -507,7 +507,7 @@ func (d *DefaultManager) squashAcceptRules( // getRuleGroupingSelector takes all rule properties except IP address to build selector func (d *DefaultManager) getRuleGroupingSelector(rule *mgmProto.FirewallRule) string { - return fmt.Sprintf("%v:%v:%v:%s", strconv.Itoa(int(rule.Direction)), rule.Action, rule.Protocol, rule.Port) + return fmt.Sprintf("%v:%v:%v:%s:%v", strconv.Itoa(int(rule.Direction)), rule.Action, rule.Protocol, rule.Port, rule.PortInfo) } func (d *DefaultManager) rollBack(newRulePairs map[id.RuleID][]firewall.Rule) { From ca9aca9b19b82d2976099c92d9ec7d9de8eb44c4 Mon Sep 17 00:00:00 2001 From: Zoltan Papp Date: Thu, 6 Feb 2025 10:20:31 +0100 Subject: [PATCH 47/92] Fix nil pointer exception when load empty list and try to cast it (#3282) --- client/internal/engine.go | 3 +-- client/internal/peer/ice/StunTurn.go | 22 ++++++++++++++++++++++ client/internal/peer/ice/StunTurn_test.go | 13 +++++++++++++ client/internal/peer/ice/agent.go | 3 +-- client/internal/peer/ice/config.go | 4 +--- 5 files changed, 38 insertions(+), 7 deletions(-) create mode 100644 client/internal/peer/ice/StunTurn.go create mode 100644 client/internal/peer/ice/StunTurn_test.go diff --git a/client/internal/engine.go b/client/internal/engine.go index 7f7cdf376..335729d92 100644 --- a/client/internal/engine.go +++ b/client/internal/engine.go @@ -13,7 +13,6 @@ import ( "sort" "strings" "sync" - "sync/atomic" "time" "github.com/hashicorp/go-multierror" @@ -146,7 +145,7 @@ type Engine struct { STUNs []*stun.URI // TURNs is a list of STUN servers used by ICE TURNs []*stun.URI - stunTurn atomic.Value + stunTurn icemaker.StunTurn clientCtx context.Context clientCancel context.CancelFunc diff --git a/client/internal/peer/ice/StunTurn.go b/client/internal/peer/ice/StunTurn.go new file mode 100644 index 000000000..63ee8c713 --- /dev/null +++ b/client/internal/peer/ice/StunTurn.go @@ -0,0 +1,22 @@ +package ice + +import ( + "sync/atomic" + + "github.com/pion/stun/v2" +) + +type StunTurn atomic.Value + +func (s *StunTurn) Load() []*stun.URI { + v := (*atomic.Value)(s).Load() + if v == nil { + return nil + } + + return v.([]*stun.URI) +} + +func (s *StunTurn) Store(value []*stun.URI) { + (*atomic.Value)(s).Store(value) +} diff --git a/client/internal/peer/ice/StunTurn_test.go b/client/internal/peer/ice/StunTurn_test.go new file mode 100644 index 000000000..7233afa6c --- /dev/null +++ b/client/internal/peer/ice/StunTurn_test.go @@ -0,0 +1,13 @@ +package ice + +import ( + "testing" +) + +func TestStunTurn_LoadEmpty(t *testing.T) { + var stStunTurn StunTurn + got := stStunTurn.Load() + if len(got) != 0 { + t.Errorf("StunTurn.Load() = %v, want %v", got, nil) + } +} diff --git a/client/internal/peer/ice/agent.go b/client/internal/peer/ice/agent.go index dc4750f24..af9e60f2d 100644 --- a/client/internal/peer/ice/agent.go +++ b/client/internal/peer/ice/agent.go @@ -5,7 +5,6 @@ import ( "github.com/pion/ice/v3" "github.com/pion/randutil" - "github.com/pion/stun/v2" log "github.com/sirupsen/logrus" "github.com/netbirdio/netbird/client/internal/stdnet" @@ -39,7 +38,7 @@ func NewAgent(iFaceDiscover stdnet.ExternalIFaceDiscover, config Config, candida agentConfig := &ice.AgentConfig{ MulticastDNSMode: ice.MulticastDNSModeDisabled, NetworkTypes: []ice.NetworkType{ice.NetworkTypeUDP4, ice.NetworkTypeUDP6}, - Urls: config.StunTurn.Load().([]*stun.URI), + Urls: config.StunTurn.Load(), CandidateTypes: candidateTypes, InterfaceFilter: stdnet.InterfaceFilter(config.InterfaceBlackList), UDPMux: config.UDPMux, diff --git a/client/internal/peer/ice/config.go b/client/internal/peer/ice/config.go index 8abc842f0..dd854a605 100644 --- a/client/internal/peer/ice/config.go +++ b/client/internal/peer/ice/config.go @@ -1,14 +1,12 @@ package ice import ( - "sync/atomic" - "github.com/pion/ice/v3" ) type Config struct { // StunTurn is a list of STUN and TURN URLs - StunTurn *atomic.Value // []*stun.URI + StunTurn *StunTurn // []*stun.URI // InterfaceBlackList is a list of machine interfaces that should be filtered out by ICE Candidate gathering // (e.g. if eth0 is in the list, host candidate of this interface won't be used) From cee4aeea9e48376707974f301f280b3d813131a9 Mon Sep 17 00:00:00 2001 From: Pascal Fischer <32096965+pascal-fischer@users.noreply.github.com> Date: Thu, 6 Feb 2025 13:36:57 +0100 Subject: [PATCH 48/92] [management] Check groups when counting peers on networks list (#3284) --- management/server/http/handlers/networks/handler.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/management/server/http/handlers/networks/handler.go b/management/server/http/handlers/networks/handler.go index 316b93611..f716348d6 100644 --- a/management/server/http/handlers/networks/handler.go +++ b/management/server/http/handlers/networks/handler.go @@ -7,6 +7,7 @@ import ( "net/http" "github.com/gorilla/mux" + log "github.com/sirupsen/logrus" s "github.com/netbirdio/netbird/management/server" "github.com/netbirdio/netbird/management/server/groups" @@ -281,7 +282,12 @@ func (h *handler) collectIDsInNetwork(ctx context.Context, accountID, userID, ne } if len(router.PeerGroups) > 0 { for _, groupID := range router.PeerGroups { - peerCounter += len(groups[groupID].Peers) + group, ok := groups[groupID] + if !ok { + log.WithContext(ctx).Warnf("group %s not found", groupID) + continue + } + peerCounter += len(group.Peers) } } } From b7af53ea40b74f52ac57ec874690e93bd2521abb Mon Sep 17 00:00:00 2001 From: Pascal Fischer <32096965+pascal-fischer@users.noreply.github.com> Date: Fri, 7 Feb 2025 13:51:17 +0100 Subject: [PATCH 49/92] [management] add logs for grpc API (#3298) --- management/server/grpcserver.go | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/management/server/grpcserver.go b/management/server/grpcserver.go index eec109ee9..e8e0c422e 100644 --- a/management/server/grpcserver.go +++ b/management/server/grpcserver.go @@ -15,6 +15,7 @@ import ( log "github.com/sirupsen/logrus" "golang.zx2c4.com/wireguard/wgctrl/wgtypes" "google.golang.org/grpc/codes" + "google.golang.org/grpc/peer" "google.golang.org/grpc/status" "github.com/netbirdio/netbird/encryption" @@ -114,6 +115,18 @@ func NewServer( } func (s *GRPCServer) GetServerKey(ctx context.Context, req *proto.Empty) (*proto.ServerKeyResponse, error) { + ip := "" + p, ok := peer.FromContext(ctx) + if ok { + ip = p.Addr.String() + } + + log.WithContext(ctx).Tracef("GetServerKey request from %s", ip) + start := time.Now() + defer func() { + log.WithContext(ctx).Tracef("GetServerKey from %s took %v", ip, time.Since(start)) + }() + // todo introduce something more meaningful with the key expiration/rotation if s.appMetrics != nil { s.appMetrics.GRPCMetrics().CountGetKeyRequest() @@ -717,6 +730,12 @@ func (s *GRPCServer) sendInitialSync(ctx context.Context, peerKey wgtypes.Key, p // This is used for initiating an Oauth 2 device authorization grant flow // which will be used by our clients to Login func (s *GRPCServer) GetDeviceAuthorizationFlow(ctx context.Context, req *proto.EncryptedMessage) (*proto.EncryptedMessage, error) { + log.WithContext(ctx).Tracef("GetDeviceAuthorizationFlow request for pubKey: %s", req.WgPubKey) + start := time.Now() + defer func() { + log.WithContext(ctx).Tracef("GetDeviceAuthorizationFlow for pubKey: %s took %v", req.WgPubKey, time.Since(start)) + }() + peerKey, err := wgtypes.ParseKey(req.GetWgPubKey()) if err != nil { errMSG := fmt.Sprintf("error while parsing peer's Wireguard public key %s on GetDeviceAuthorizationFlow request.", req.WgPubKey) @@ -769,6 +788,12 @@ func (s *GRPCServer) GetDeviceAuthorizationFlow(ctx context.Context, req *proto. // This is used for initiating an Oauth 2 pkce authorization grant flow // which will be used by our clients to Login func (s *GRPCServer) GetPKCEAuthorizationFlow(ctx context.Context, req *proto.EncryptedMessage) (*proto.EncryptedMessage, error) { + log.WithContext(ctx).Tracef("GetPKCEAuthorizationFlow request for pubKey: %s", req.WgPubKey) + start := time.Now() + defer func() { + log.WithContext(ctx).Tracef("GetPKCEAuthorizationFlow for pubKey %s took %v", req.WgPubKey, time.Since(start)) + }() + peerKey, err := wgtypes.ParseKey(req.GetWgPubKey()) if err != nil { errMSG := fmt.Sprintf("error while parsing peer's Wireguard public key %s on GetPKCEAuthorizationFlow request.", req.WgPubKey) From 05415f72ec827835edd211f332a235f5b8e02ba6 Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Fri, 7 Feb 2025 14:11:53 +0100 Subject: [PATCH 50/92] [client] Add experimental support for userspace routing (#3134) --- client/Dockerfile-rootless | 1 + client/cmd/trace.go | 137 +++ client/firewall/create.go | 4 +- client/firewall/create_linux.go | 14 +- client/firewall/iface.go | 4 + client/firewall/iptables/manager_linux.go | 5 + client/firewall/iptables/router_linux.go | 11 +- client/firewall/manager/firewall.go | 2 + client/firewall/nftables/manager_linux.go | 5 + .../firewall/nftables/manager_linux_test.go | 17 +- client/firewall/nftables/router_linux.go | 8 +- client/firewall/uspfilter/allow_netbird.go | 23 +- .../uspfilter/allow_netbird_windows.go | 20 +- client/firewall/uspfilter/common/iface.go | 16 + client/firewall/uspfilter/conntrack/common.go | 21 +- .../uspfilter/conntrack/common_test.go | 36 +- client/firewall/uspfilter/conntrack/icmp.go | 27 +- .../firewall/uspfilter/conntrack/icmp_test.go | 4 +- client/firewall/uspfilter/conntrack/tcp.go | 41 +- .../firewall/uspfilter/conntrack/tcp_test.go | 14 +- client/firewall/uspfilter/conntrack/udp.go | 19 +- .../firewall/uspfilter/conntrack/udp_test.go | 12 +- .../firewall/uspfilter/forwarder/endpoint.go | 81 ++ .../firewall/uspfilter/forwarder/forwarder.go | 166 +++ client/firewall/uspfilter/forwarder/icmp.go | 109 ++ client/firewall/uspfilter/forwarder/tcp.go | 90 ++ client/firewall/uspfilter/forwarder/udp.go | 288 +++++ client/firewall/uspfilter/localip.go | 134 +++ client/firewall/uspfilter/localip_test.go | 270 +++++ client/firewall/uspfilter/log/log.go | 196 ++++ client/firewall/uspfilter/log/ringbuffer.go | 85 ++ client/firewall/uspfilter/rule.go | 22 +- client/firewall/uspfilter/tracer.go | 390 +++++++ client/firewall/uspfilter/uspfilter.go | 516 +++++++-- .../uspfilter/uspfilter_bench_test.go | 122 +- .../uspfilter/uspfilter_filter_test.go | 1014 +++++++++++++++++ client/firewall/uspfilter/uspfilter_test.go | 66 +- client/iface/device.go | 3 + client/iface/device/device_darwin.go | 5 + client/iface/device/device_kernel_unix.go | 6 + client/iface/device/device_netstack.go | 5 + client/iface/device/device_usp_unix.go | 5 + client/iface/device/device_windows.go | 5 + client/iface/device_android.go | 3 + client/iface/iface.go | 7 + client/iface/iface_moc.go | 9 +- client/iface/iwginterface.go | 2 + client/iface/iwginterface_windows.go | 2 + client/internal/acl/manager_test.go | 6 +- client/internal/acl/mocks/iface_mapper.go | 30 + client/internal/dns/server_test.go | 2 +- client/internal/engine.go | 29 +- client/internal/routemanager/manager.go | 5 - client/proto/daemon.pb.go | 690 ++++++++--- client/proto/daemon.proto | 35 + client/proto/daemon_grpc.pb.go | 36 + client/server/debug.go | 17 + client/server/trace.go | 123 ++ go.mod | 4 +- go.sum | 8 +- 60 files changed, 4652 insertions(+), 375 deletions(-) create mode 100644 client/cmd/trace.go create mode 100644 client/firewall/uspfilter/common/iface.go create mode 100644 client/firewall/uspfilter/forwarder/endpoint.go create mode 100644 client/firewall/uspfilter/forwarder/forwarder.go create mode 100644 client/firewall/uspfilter/forwarder/icmp.go create mode 100644 client/firewall/uspfilter/forwarder/tcp.go create mode 100644 client/firewall/uspfilter/forwarder/udp.go create mode 100644 client/firewall/uspfilter/localip.go create mode 100644 client/firewall/uspfilter/localip_test.go create mode 100644 client/firewall/uspfilter/log/log.go create mode 100644 client/firewall/uspfilter/log/ringbuffer.go create mode 100644 client/firewall/uspfilter/tracer.go create mode 100644 client/firewall/uspfilter/uspfilter_filter_test.go create mode 100644 client/server/trace.go diff --git a/client/Dockerfile-rootless b/client/Dockerfile-rootless index 62bcaf964..78314ba12 100644 --- a/client/Dockerfile-rootless +++ b/client/Dockerfile-rootless @@ -9,6 +9,7 @@ USER netbird:netbird ENV NB_FOREGROUND_MODE=true ENV NB_USE_NETSTACK_MODE=true +ENV NB_ENABLE_NETSTACK_LOCAL_FORWARDING=true ENV NB_CONFIG=config.json ENV NB_DAEMON_ADDR=unix://netbird.sock ENV NB_DISABLE_DNS=true diff --git a/client/cmd/trace.go b/client/cmd/trace.go new file mode 100644 index 000000000..b2ff1f1b5 --- /dev/null +++ b/client/cmd/trace.go @@ -0,0 +1,137 @@ +package cmd + +import ( + "fmt" + "math/rand" + "strings" + + "github.com/spf13/cobra" + "google.golang.org/grpc/status" + + "github.com/netbirdio/netbird/client/proto" +) + +var traceCmd = &cobra.Command{ + Use: "trace ", + Short: "Trace a packet through the firewall", + Example: ` + netbird debug trace in 192.168.1.10 10.10.0.2 -p tcp --sport 12345 --dport 443 --syn --ack + netbird debug trace out 10.10.0.1 8.8.8.8 -p udp --dport 53 + netbird debug trace in 10.10.0.2 10.10.0.1 -p icmp --type 8 --code 0 + netbird debug trace in 100.64.1.1 self -p tcp --dport 80`, + Args: cobra.ExactArgs(3), + RunE: tracePacket, +} + +func init() { + debugCmd.AddCommand(traceCmd) + + traceCmd.Flags().StringP("protocol", "p", "tcp", "Protocol (tcp/udp/icmp)") + traceCmd.Flags().Uint16("sport", 0, "Source port") + traceCmd.Flags().Uint16("dport", 0, "Destination port") + traceCmd.Flags().Uint8("icmp-type", 0, "ICMP type") + traceCmd.Flags().Uint8("icmp-code", 0, "ICMP code") + traceCmd.Flags().Bool("syn", false, "TCP SYN flag") + traceCmd.Flags().Bool("ack", false, "TCP ACK flag") + traceCmd.Flags().Bool("fin", false, "TCP FIN flag") + traceCmd.Flags().Bool("rst", false, "TCP RST flag") + traceCmd.Flags().Bool("psh", false, "TCP PSH flag") + traceCmd.Flags().Bool("urg", false, "TCP URG flag") +} + +func tracePacket(cmd *cobra.Command, args []string) error { + direction := strings.ToLower(args[0]) + if direction != "in" && direction != "out" { + return fmt.Errorf("invalid direction: use 'in' or 'out'") + } + + protocol := cmd.Flag("protocol").Value.String() + if protocol != "tcp" && protocol != "udp" && protocol != "icmp" { + return fmt.Errorf("invalid protocol: use tcp/udp/icmp") + } + + sport, err := cmd.Flags().GetUint16("sport") + if err != nil { + return fmt.Errorf("invalid source port: %v", err) + } + dport, err := cmd.Flags().GetUint16("dport") + if err != nil { + return fmt.Errorf("invalid destination port: %v", err) + } + + // For TCP/UDP, generate random ephemeral port (49152-65535) if not specified + if protocol != "icmp" { + if sport == 0 { + sport = uint16(rand.Intn(16383) + 49152) + } + if dport == 0 { + dport = uint16(rand.Intn(16383) + 49152) + } + } + + var tcpFlags *proto.TCPFlags + if protocol == "tcp" { + syn, _ := cmd.Flags().GetBool("syn") + ack, _ := cmd.Flags().GetBool("ack") + fin, _ := cmd.Flags().GetBool("fin") + rst, _ := cmd.Flags().GetBool("rst") + psh, _ := cmd.Flags().GetBool("psh") + urg, _ := cmd.Flags().GetBool("urg") + + tcpFlags = &proto.TCPFlags{ + Syn: syn, + Ack: ack, + Fin: fin, + Rst: rst, + Psh: psh, + Urg: urg, + } + } + + icmpType, _ := cmd.Flags().GetUint32("icmp-type") + icmpCode, _ := cmd.Flags().GetUint32("icmp-code") + + conn, err := getClient(cmd) + if err != nil { + return err + } + defer conn.Close() + + client := proto.NewDaemonServiceClient(conn) + resp, err := client.TracePacket(cmd.Context(), &proto.TracePacketRequest{ + SourceIp: args[1], + DestinationIp: args[2], + Protocol: protocol, + SourcePort: uint32(sport), + DestinationPort: uint32(dport), + Direction: direction, + TcpFlags: tcpFlags, + IcmpType: &icmpType, + IcmpCode: &icmpCode, + }) + if err != nil { + return fmt.Errorf("trace failed: %v", status.Convert(err).Message()) + } + + printTrace(cmd, args[1], args[2], protocol, sport, dport, resp) + return nil +} + +func printTrace(cmd *cobra.Command, src, dst, proto string, sport, dport uint16, resp *proto.TracePacketResponse) { + cmd.Printf("Packet trace %s:%d -> %s:%d (%s)\n\n", src, sport, dst, dport, strings.ToUpper(proto)) + + for _, stage := range resp.Stages { + if stage.ForwardingDetails != nil { + cmd.Printf("%s: %s [%s]\n", stage.Name, stage.Message, *stage.ForwardingDetails) + } else { + cmd.Printf("%s: %s\n", stage.Name, stage.Message) + } + } + + disposition := map[bool]string{ + true: "\033[32mALLOWED\033[0m", // Green + false: "\033[31mDENIED\033[0m", // Red + }[resp.FinalDisposition] + + cmd.Printf("\nFinal disposition: %s\n", disposition) +} diff --git a/client/firewall/create.go b/client/firewall/create.go index 9466f4b4d..37ea5ceb3 100644 --- a/client/firewall/create.go +++ b/client/firewall/create.go @@ -14,13 +14,13 @@ import ( ) // NewFirewall creates a firewall manager instance -func NewFirewall(iface IFaceMapper, _ *statemanager.Manager) (firewall.Manager, error) { +func NewFirewall(iface IFaceMapper, _ *statemanager.Manager, disableServerRoutes bool) (firewall.Manager, error) { if !iface.IsUserspaceBind() { return nil, fmt.Errorf("not implemented for this OS: %s", runtime.GOOS) } // use userspace packet filtering firewall - fm, err := uspfilter.Create(iface) + fm, err := uspfilter.Create(iface, disableServerRoutes) if err != nil { return nil, err } diff --git a/client/firewall/create_linux.go b/client/firewall/create_linux.go index 076d08ec2..be1b37916 100644 --- a/client/firewall/create_linux.go +++ b/client/firewall/create_linux.go @@ -33,12 +33,12 @@ const SKIP_NFTABLES_ENV = "NB_SKIP_NFTABLES_CHECK" // FWType is the type for the firewall type type FWType int -func NewFirewall(iface IFaceMapper, stateManager *statemanager.Manager) (firewall.Manager, error) { +func NewFirewall(iface IFaceMapper, stateManager *statemanager.Manager, disableServerRoutes bool) (firewall.Manager, error) { // on the linux system we try to user nftables or iptables // in any case, because we need to allow netbird interface traffic // so we use AllowNetbird traffic from these firewall managers // for the userspace packet filtering firewall - fm, err := createNativeFirewall(iface, stateManager) + fm, err := createNativeFirewall(iface, stateManager, disableServerRoutes) if !iface.IsUserspaceBind() { return fm, err @@ -47,10 +47,10 @@ func NewFirewall(iface IFaceMapper, stateManager *statemanager.Manager) (firewal if err != nil { log.Warnf("failed to create native firewall: %v. Proceeding with userspace", err) } - return createUserspaceFirewall(iface, fm) + return createUserspaceFirewall(iface, fm, disableServerRoutes) } -func createNativeFirewall(iface IFaceMapper, stateManager *statemanager.Manager) (firewall.Manager, error) { +func createNativeFirewall(iface IFaceMapper, stateManager *statemanager.Manager, routes bool) (firewall.Manager, error) { fm, err := createFW(iface) if err != nil { return nil, fmt.Errorf("create firewall: %s", err) @@ -77,12 +77,12 @@ func createFW(iface IFaceMapper) (firewall.Manager, error) { } } -func createUserspaceFirewall(iface IFaceMapper, fm firewall.Manager) (firewall.Manager, error) { +func createUserspaceFirewall(iface IFaceMapper, fm firewall.Manager, disableServerRoutes bool) (firewall.Manager, error) { var errUsp error if fm != nil { - fm, errUsp = uspfilter.CreateWithNativeFirewall(iface, fm) + fm, errUsp = uspfilter.CreateWithNativeFirewall(iface, fm, disableServerRoutes) } else { - fm, errUsp = uspfilter.Create(iface) + fm, errUsp = uspfilter.Create(iface, disableServerRoutes) } if errUsp != nil { diff --git a/client/firewall/iface.go b/client/firewall/iface.go index f349f9210..d842abaa1 100644 --- a/client/firewall/iface.go +++ b/client/firewall/iface.go @@ -1,6 +1,8 @@ package firewall import ( + wgdevice "golang.zx2c4.com/wireguard/device" + "github.com/netbirdio/netbird/client/iface/device" ) @@ -10,4 +12,6 @@ type IFaceMapper interface { Address() device.WGAddress IsUserspaceBind() bool SetFilter(device.PacketFilter) error + GetDevice() *device.FilteredDevice + GetWGDevice() *wgdevice.Device } diff --git a/client/firewall/iptables/manager_linux.go b/client/firewall/iptables/manager_linux.go index 75f082fc4..679f288e3 100644 --- a/client/firewall/iptables/manager_linux.go +++ b/client/firewall/iptables/manager_linux.go @@ -213,6 +213,11 @@ func (m *Manager) AllowNetbird() error { // Flush doesn't need to be implemented for this manager func (m *Manager) Flush() error { return nil } +// SetLogLevel sets the log level for the firewall manager +func (m *Manager) SetLogLevel(log.Level) { + // not supported +} + func getConntrackEstablished() []string { return []string{"-m", "conntrack", "--ctstate", "RELATED,ESTABLISHED", "-j", "ACCEPT"} } diff --git a/client/firewall/iptables/router_linux.go b/client/firewall/iptables/router_linux.go index a47d3ffe6..6522daa3f 100644 --- a/client/firewall/iptables/router_linux.go +++ b/client/firewall/iptables/router_linux.go @@ -135,7 +135,16 @@ func (r *router) AddRouteFiltering( } rule := genRouteFilteringRuleSpec(params) - if err := r.iptablesClient.Append(tableFilter, chainRTFWD, rule...); err != nil { + // Insert DROP rules at the beginning, append ACCEPT rules at the end + var err error + if action == firewall.ActionDrop { + // after the established rule + err = r.iptablesClient.Insert(tableFilter, chainRTFWD, 2, rule...) + } else { + err = r.iptablesClient.Append(tableFilter, chainRTFWD, rule...) + } + + if err != nil { return nil, fmt.Errorf("add route rule: %v", err) } diff --git a/client/firewall/manager/firewall.go b/client/firewall/manager/firewall.go index f46e5eb5d..de25ff1f1 100644 --- a/client/firewall/manager/firewall.go +++ b/client/firewall/manager/firewall.go @@ -99,6 +99,8 @@ type Manager interface { // Flush the changes to firewall controller Flush() error + + SetLogLevel(log.Level) } func GenKey(format string, pair RouterPair) string { diff --git a/client/firewall/nftables/manager_linux.go b/client/firewall/nftables/manager_linux.go index a78626dbc..4fe52bd53 100644 --- a/client/firewall/nftables/manager_linux.go +++ b/client/firewall/nftables/manager_linux.go @@ -318,6 +318,11 @@ func (m *Manager) cleanupNetbirdTables() error { return nil } +// SetLogLevel sets the log level for the firewall manager +func (m *Manager) SetLogLevel(log.Level) { + // not supported +} + // Flush rule/chain/set operations from the buffer // // Method also get all rules after flush and refreshes handle values in the rulesets diff --git a/client/firewall/nftables/manager_linux_test.go b/client/firewall/nftables/manager_linux_test.go index 8d693725a..eaa8ef1f5 100644 --- a/client/firewall/nftables/manager_linux_test.go +++ b/client/firewall/nftables/manager_linux_test.go @@ -107,7 +107,7 @@ func TestNftablesManager(t *testing.T) { Kind: expr.VerdictAccept, }, } - require.ElementsMatch(t, rules[0].Exprs, expectedExprs1, "expected the same expressions") + compareExprsIgnoringCounters(t, rules[0].Exprs, expectedExprs1) ipToAdd, _ := netip.AddrFromSlice(ip) add := ipToAdd.Unmap() @@ -307,3 +307,18 @@ func TestNftablesManagerCompatibilityWithIptables(t *testing.T) { stdout, stderr = runIptablesSave(t) verifyIptablesOutput(t, stdout, stderr) } + +func compareExprsIgnoringCounters(t *testing.T, got, want []expr.Any) { + t.Helper() + require.Equal(t, len(got), len(want), "expression count mismatch") + + for i := range got { + if _, isCounter := got[i].(*expr.Counter); isCounter { + _, wantIsCounter := want[i].(*expr.Counter) + require.True(t, wantIsCounter, "expected Counter at index %d", i) + continue + } + + require.Equal(t, got[i], want[i], "expression mismatch at index %d", i) + } +} diff --git a/client/firewall/nftables/router_linux.go b/client/firewall/nftables/router_linux.go index 19734673b..92f81f39c 100644 --- a/client/firewall/nftables/router_linux.go +++ b/client/firewall/nftables/router_linux.go @@ -233,7 +233,13 @@ func (r *router) AddRouteFiltering( UserData: []byte(ruleKey), } - rule = r.conn.AddRule(rule) + // Insert DROP rules at the beginning, append ACCEPT rules at the end + if action == firewall.ActionDrop { + // TODO: Insert after the established rule + rule = r.conn.InsertRule(rule) + } else { + rule = r.conn.AddRule(rule) + } log.Tracef("Adding route rule %s", spew.Sdump(rule)) if err := r.conn.Flush(); err != nil { diff --git a/client/firewall/uspfilter/allow_netbird.go b/client/firewall/uspfilter/allow_netbird.go index cc0792255..03f23f5e6 100644 --- a/client/firewall/uspfilter/allow_netbird.go +++ b/client/firewall/uspfilter/allow_netbird.go @@ -3,6 +3,11 @@ package uspfilter import ( + "context" + "time" + + log "github.com/sirupsen/logrus" + "github.com/netbirdio/netbird/client/firewall/uspfilter/conntrack" "github.com/netbirdio/netbird/client/internal/statemanager" ) @@ -17,17 +22,29 @@ func (m *Manager) Reset(stateManager *statemanager.Manager) error { if m.udpTracker != nil { m.udpTracker.Close() - m.udpTracker = conntrack.NewUDPTracker(conntrack.DefaultUDPTimeout) + m.udpTracker = conntrack.NewUDPTracker(conntrack.DefaultUDPTimeout, m.logger) } if m.icmpTracker != nil { m.icmpTracker.Close() - m.icmpTracker = conntrack.NewICMPTracker(conntrack.DefaultICMPTimeout) + m.icmpTracker = conntrack.NewICMPTracker(conntrack.DefaultICMPTimeout, m.logger) } if m.tcpTracker != nil { m.tcpTracker.Close() - m.tcpTracker = conntrack.NewTCPTracker(conntrack.DefaultTCPTimeout) + m.tcpTracker = conntrack.NewTCPTracker(conntrack.DefaultTCPTimeout, m.logger) + } + + if m.forwarder != nil { + m.forwarder.Stop() + } + + if m.logger != nil { + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + if err := m.logger.Stop(ctx); err != nil { + log.Errorf("failed to shutdown logger: %v", err) + } } if m.nativeFirewall != nil { diff --git a/client/firewall/uspfilter/allow_netbird_windows.go b/client/firewall/uspfilter/allow_netbird_windows.go index 0d55d6268..379585978 100644 --- a/client/firewall/uspfilter/allow_netbird_windows.go +++ b/client/firewall/uspfilter/allow_netbird_windows.go @@ -1,9 +1,11 @@ package uspfilter import ( + "context" "fmt" "os/exec" "syscall" + "time" log "github.com/sirupsen/logrus" @@ -29,17 +31,29 @@ func (m *Manager) Reset(*statemanager.Manager) error { if m.udpTracker != nil { m.udpTracker.Close() - m.udpTracker = conntrack.NewUDPTracker(conntrack.DefaultUDPTimeout) + m.udpTracker = conntrack.NewUDPTracker(conntrack.DefaultUDPTimeout, m.logger) } if m.icmpTracker != nil { m.icmpTracker.Close() - m.icmpTracker = conntrack.NewICMPTracker(conntrack.DefaultICMPTimeout) + m.icmpTracker = conntrack.NewICMPTracker(conntrack.DefaultICMPTimeout, m.logger) } if m.tcpTracker != nil { m.tcpTracker.Close() - m.tcpTracker = conntrack.NewTCPTracker(conntrack.DefaultTCPTimeout) + m.tcpTracker = conntrack.NewTCPTracker(conntrack.DefaultTCPTimeout, m.logger) + } + + if m.forwarder != nil { + m.forwarder.Stop() + } + + if m.logger != nil { + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + if err := m.logger.Stop(ctx); err != nil { + log.Errorf("failed to shutdown logger: %v", err) + } } if !isWindowsFirewallReachable() { diff --git a/client/firewall/uspfilter/common/iface.go b/client/firewall/uspfilter/common/iface.go new file mode 100644 index 000000000..d44e79509 --- /dev/null +++ b/client/firewall/uspfilter/common/iface.go @@ -0,0 +1,16 @@ +package common + +import ( + wgdevice "golang.zx2c4.com/wireguard/device" + + "github.com/netbirdio/netbird/client/iface" + "github.com/netbirdio/netbird/client/iface/device" +) + +// IFaceMapper defines subset methods of interface required for manager +type IFaceMapper interface { + SetFilter(device.PacketFilter) error + Address() iface.WGAddress + GetWGDevice() *wgdevice.Device + GetDevice() *device.FilteredDevice +} diff --git a/client/firewall/uspfilter/conntrack/common.go b/client/firewall/uspfilter/conntrack/common.go index e459bc75a..f5f502540 100644 --- a/client/firewall/uspfilter/conntrack/common.go +++ b/client/firewall/uspfilter/conntrack/common.go @@ -10,12 +10,11 @@ import ( // BaseConnTrack provides common fields and locking for all connection types type BaseConnTrack struct { - SourceIP net.IP - DestIP net.IP - SourcePort uint16 - DestPort uint16 - lastSeen atomic.Int64 // Unix nano for atomic access - established atomic.Bool + SourceIP net.IP + DestIP net.IP + SourcePort uint16 + DestPort uint16 + lastSeen atomic.Int64 // Unix nano for atomic access } // these small methods will be inlined by the compiler @@ -25,16 +24,6 @@ func (b *BaseConnTrack) UpdateLastSeen() { b.lastSeen.Store(time.Now().UnixNano()) } -// IsEstablished safely checks if connection is established -func (b *BaseConnTrack) IsEstablished() bool { - return b.established.Load() -} - -// SetEstablished safely sets the established state -func (b *BaseConnTrack) SetEstablished(state bool) { - b.established.Store(state) -} - // GetLastSeen safely gets the last seen timestamp func (b *BaseConnTrack) GetLastSeen() time.Time { return time.Unix(0, b.lastSeen.Load()) diff --git a/client/firewall/uspfilter/conntrack/common_test.go b/client/firewall/uspfilter/conntrack/common_test.go index 72d006def..81fa64b19 100644 --- a/client/firewall/uspfilter/conntrack/common_test.go +++ b/client/firewall/uspfilter/conntrack/common_test.go @@ -3,8 +3,14 @@ package conntrack import ( "net" "testing" + + "github.com/sirupsen/logrus" + + "github.com/netbirdio/netbird/client/firewall/uspfilter/log" ) +var logger = log.NewFromLogrus(logrus.StandardLogger()) + func BenchmarkIPOperations(b *testing.B) { b.Run("MakeIPAddr", func(b *testing.B) { ip := net.ParseIP("192.168.1.1") @@ -34,37 +40,11 @@ func BenchmarkIPOperations(b *testing.B) { }) } -func BenchmarkAtomicOperations(b *testing.B) { - conn := &BaseConnTrack{} - b.Run("UpdateLastSeen", func(b *testing.B) { - for i := 0; i < b.N; i++ { - conn.UpdateLastSeen() - } - }) - - b.Run("IsEstablished", func(b *testing.B) { - for i := 0; i < b.N; i++ { - _ = conn.IsEstablished() - } - }) - - b.Run("SetEstablished", func(b *testing.B) { - for i := 0; i < b.N; i++ { - conn.SetEstablished(i%2 == 0) - } - }) - - b.Run("GetLastSeen", func(b *testing.B) { - for i := 0; i < b.N; i++ { - _ = conn.GetLastSeen() - } - }) -} // Memory pressure tests func BenchmarkMemoryPressure(b *testing.B) { b.Run("TCPHighLoad", func(b *testing.B) { - tracker := NewTCPTracker(DefaultTCPTimeout) + tracker := NewTCPTracker(DefaultTCPTimeout, logger) defer tracker.Close() // Generate different IPs @@ -89,7 +69,7 @@ func BenchmarkMemoryPressure(b *testing.B) { }) b.Run("UDPHighLoad", func(b *testing.B) { - tracker := NewUDPTracker(DefaultUDPTimeout) + tracker := NewUDPTracker(DefaultUDPTimeout, logger) defer tracker.Close() // Generate different IPs diff --git a/client/firewall/uspfilter/conntrack/icmp.go b/client/firewall/uspfilter/conntrack/icmp.go index e0a971678..25cd9e87d 100644 --- a/client/firewall/uspfilter/conntrack/icmp.go +++ b/client/firewall/uspfilter/conntrack/icmp.go @@ -6,6 +6,8 @@ import ( "time" "github.com/google/gopacket/layers" + + nblog "github.com/netbirdio/netbird/client/firewall/uspfilter/log" ) const ( @@ -33,6 +35,7 @@ type ICMPConnTrack struct { // ICMPTracker manages ICMP connection states type ICMPTracker struct { + logger *nblog.Logger connections map[ICMPConnKey]*ICMPConnTrack timeout time.Duration cleanupTicker *time.Ticker @@ -42,12 +45,13 @@ type ICMPTracker struct { } // NewICMPTracker creates a new ICMP connection tracker -func NewICMPTracker(timeout time.Duration) *ICMPTracker { +func NewICMPTracker(timeout time.Duration, logger *nblog.Logger) *ICMPTracker { if timeout == 0 { timeout = DefaultICMPTimeout } tracker := &ICMPTracker{ + logger: logger, connections: make(map[ICMPConnKey]*ICMPConnTrack), timeout: timeout, cleanupTicker: time.NewTicker(ICMPCleanupInterval), @@ -62,7 +66,6 @@ func NewICMPTracker(timeout time.Duration) *ICMPTracker { // TrackOutbound records an outbound ICMP Echo Request func (t *ICMPTracker) TrackOutbound(srcIP net.IP, dstIP net.IP, id uint16, seq uint16) { key := makeICMPKey(srcIP, dstIP, id, seq) - now := time.Now().UnixNano() t.mutex.Lock() conn, exists := t.connections[key] @@ -80,24 +83,19 @@ func (t *ICMPTracker) TrackOutbound(srcIP net.IP, dstIP net.IP, id uint16, seq u ID: id, Sequence: seq, } - conn.lastSeen.Store(now) - conn.established.Store(true) + conn.UpdateLastSeen() t.connections[key] = conn + + t.logger.Trace("New ICMP connection %v", key) } t.mutex.Unlock() - conn.lastSeen.Store(now) + conn.UpdateLastSeen() } // IsValidInbound checks if an inbound ICMP Echo Reply matches a tracked request func (t *ICMPTracker) IsValidInbound(srcIP net.IP, dstIP net.IP, id uint16, seq uint16, icmpType uint8) bool { - switch icmpType { - case uint8(layers.ICMPv4TypeDestinationUnreachable), - uint8(layers.ICMPv4TypeTimeExceeded): - return true - case uint8(layers.ICMPv4TypeEchoReply): - // continue processing - default: + if icmpType != uint8(layers.ICMPv4TypeEchoReply) { return false } @@ -115,8 +113,7 @@ func (t *ICMPTracker) IsValidInbound(srcIP net.IP, dstIP net.IP, id uint16, seq return false } - return conn.IsEstablished() && - ValidateIPs(MakeIPAddr(srcIP), conn.DestIP) && + return ValidateIPs(MakeIPAddr(srcIP), conn.DestIP) && ValidateIPs(MakeIPAddr(dstIP), conn.SourceIP) && conn.ID == id && conn.Sequence == seq @@ -141,6 +138,8 @@ func (t *ICMPTracker) cleanup() { t.ipPool.Put(conn.SourceIP) t.ipPool.Put(conn.DestIP) delete(t.connections, key) + + t.logger.Debug("Removed ICMP connection %v (timeout)", key) } } } diff --git a/client/firewall/uspfilter/conntrack/icmp_test.go b/client/firewall/uspfilter/conntrack/icmp_test.go index 21176e719..32553c836 100644 --- a/client/firewall/uspfilter/conntrack/icmp_test.go +++ b/client/firewall/uspfilter/conntrack/icmp_test.go @@ -7,7 +7,7 @@ import ( func BenchmarkICMPTracker(b *testing.B) { b.Run("TrackOutbound", func(b *testing.B) { - tracker := NewICMPTracker(DefaultICMPTimeout) + tracker := NewICMPTracker(DefaultICMPTimeout, logger) defer tracker.Close() srcIP := net.ParseIP("192.168.1.1") @@ -20,7 +20,7 @@ func BenchmarkICMPTracker(b *testing.B) { }) b.Run("IsValidInbound", func(b *testing.B) { - tracker := NewICMPTracker(DefaultICMPTimeout) + tracker := NewICMPTracker(DefaultICMPTimeout, logger) defer tracker.Close() srcIP := net.ParseIP("192.168.1.1") diff --git a/client/firewall/uspfilter/conntrack/tcp.go b/client/firewall/uspfilter/conntrack/tcp.go index a7968dc73..7c12e8ad0 100644 --- a/client/firewall/uspfilter/conntrack/tcp.go +++ b/client/firewall/uspfilter/conntrack/tcp.go @@ -5,7 +5,10 @@ package conntrack import ( "net" "sync" + "sync/atomic" "time" + + nblog "github.com/netbirdio/netbird/client/firewall/uspfilter/log" ) const ( @@ -61,12 +64,24 @@ type TCPConnKey struct { // TCPConnTrack represents a TCP connection state type TCPConnTrack struct { BaseConnTrack - State TCPState + State TCPState + established atomic.Bool sync.RWMutex } +// IsEstablished safely checks if connection is established +func (t *TCPConnTrack) IsEstablished() bool { + return t.established.Load() +} + +// SetEstablished safely sets the established state +func (t *TCPConnTrack) SetEstablished(state bool) { + t.established.Store(state) +} + // TCPTracker manages TCP connection states type TCPTracker struct { + logger *nblog.Logger connections map[ConnKey]*TCPConnTrack mutex sync.RWMutex cleanupTicker *time.Ticker @@ -76,8 +91,9 @@ type TCPTracker struct { } // NewTCPTracker creates a new TCP connection tracker -func NewTCPTracker(timeout time.Duration) *TCPTracker { +func NewTCPTracker(timeout time.Duration, logger *nblog.Logger) *TCPTracker { tracker := &TCPTracker{ + logger: logger, connections: make(map[ConnKey]*TCPConnTrack), cleanupTicker: time.NewTicker(TCPCleanupInterval), done: make(chan struct{}), @@ -93,7 +109,6 @@ func NewTCPTracker(timeout time.Duration) *TCPTracker { func (t *TCPTracker) TrackOutbound(srcIP net.IP, dstIP net.IP, srcPort uint16, dstPort uint16, flags uint8) { // Create key before lock key := makeConnKey(srcIP, dstIP, srcPort, dstPort) - now := time.Now().UnixNano() t.mutex.Lock() conn, exists := t.connections[key] @@ -113,9 +128,11 @@ func (t *TCPTracker) TrackOutbound(srcIP net.IP, dstIP net.IP, srcPort uint16, d }, State: TCPStateNew, } - conn.lastSeen.Store(now) + conn.UpdateLastSeen() conn.established.Store(false) t.connections[key] = conn + + t.logger.Trace("New TCP connection: %s:%d -> %s:%d", srcIP, srcPort, dstIP, dstPort) } t.mutex.Unlock() @@ -123,7 +140,7 @@ func (t *TCPTracker) TrackOutbound(srcIP net.IP, dstIP net.IP, srcPort uint16, d conn.Lock() t.updateState(conn, flags, true) conn.Unlock() - conn.lastSeen.Store(now) + conn.UpdateLastSeen() } // IsValidInbound checks if an inbound TCP packet matches a tracked connection @@ -171,6 +188,9 @@ func (t *TCPTracker) updateState(conn *TCPConnTrack, flags uint8, isOutbound boo if flags&TCPRst != 0 { conn.State = TCPStateClosed conn.SetEstablished(false) + + t.logger.Trace("TCP connection reset: %s:%d -> %s:%d", + conn.SourceIP, conn.SourcePort, conn.DestIP, conn.DestPort) return } @@ -227,6 +247,9 @@ func (t *TCPTracker) updateState(conn *TCPConnTrack, flags uint8, isOutbound boo if flags&TCPAck != 0 { conn.State = TCPStateTimeWait // Keep established = false from previous state + + t.logger.Trace("TCP connection closed (simultaneous) - %s:%d -> %s:%d", + conn.SourceIP, conn.SourcePort, conn.DestIP, conn.DestPort) } case TCPStateCloseWait: @@ -237,11 +260,17 @@ func (t *TCPTracker) updateState(conn *TCPConnTrack, flags uint8, isOutbound boo case TCPStateLastAck: if flags&TCPAck != 0 { conn.State = TCPStateClosed + + t.logger.Trace("TCP connection gracefully closed: %s:%d -> %s:%d", + conn.SourceIP, conn.SourcePort, conn.DestIP, conn.DestPort) } case TCPStateTimeWait: // Stay in TIME-WAIT for 2MSL before transitioning to closed // This is handled by the cleanup routine + + t.logger.Trace("TCP connection completed - %s:%d -> %s:%d", + conn.SourceIP, conn.SourcePort, conn.DestIP, conn.DestPort) } } @@ -318,6 +347,8 @@ func (t *TCPTracker) cleanup() { t.ipPool.Put(conn.SourceIP) t.ipPool.Put(conn.DestIP) delete(t.connections, key) + + t.logger.Trace("Cleaned up TCP connection: %s:%d -> %s:%d", conn.SourceIP, conn.SourcePort, conn.DestIP, conn.DestPort) } } } diff --git a/client/firewall/uspfilter/conntrack/tcp_test.go b/client/firewall/uspfilter/conntrack/tcp_test.go index 6c8f82423..5f4c43915 100644 --- a/client/firewall/uspfilter/conntrack/tcp_test.go +++ b/client/firewall/uspfilter/conntrack/tcp_test.go @@ -9,7 +9,7 @@ import ( ) func TestTCPStateMachine(t *testing.T) { - tracker := NewTCPTracker(DefaultTCPTimeout) + tracker := NewTCPTracker(DefaultTCPTimeout, logger) defer tracker.Close() srcIP := net.ParseIP("100.64.0.1") @@ -154,7 +154,7 @@ func TestTCPStateMachine(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Helper() - tracker = NewTCPTracker(DefaultTCPTimeout) + tracker = NewTCPTracker(DefaultTCPTimeout, logger) tt.test(t) }) } @@ -162,7 +162,7 @@ func TestTCPStateMachine(t *testing.T) { } func TestRSTHandling(t *testing.T) { - tracker := NewTCPTracker(DefaultTCPTimeout) + tracker := NewTCPTracker(DefaultTCPTimeout, logger) defer tracker.Close() srcIP := net.ParseIP("100.64.0.1") @@ -233,7 +233,7 @@ func establishConnection(t *testing.T, tracker *TCPTracker, srcIP, dstIP net.IP, func BenchmarkTCPTracker(b *testing.B) { b.Run("TrackOutbound", func(b *testing.B) { - tracker := NewTCPTracker(DefaultTCPTimeout) + tracker := NewTCPTracker(DefaultTCPTimeout, logger) defer tracker.Close() srcIP := net.ParseIP("192.168.1.1") @@ -246,7 +246,7 @@ func BenchmarkTCPTracker(b *testing.B) { }) b.Run("IsValidInbound", func(b *testing.B) { - tracker := NewTCPTracker(DefaultTCPTimeout) + tracker := NewTCPTracker(DefaultTCPTimeout, logger) defer tracker.Close() srcIP := net.ParseIP("192.168.1.1") @@ -264,7 +264,7 @@ func BenchmarkTCPTracker(b *testing.B) { }) b.Run("ConcurrentAccess", func(b *testing.B) { - tracker := NewTCPTracker(DefaultTCPTimeout) + tracker := NewTCPTracker(DefaultTCPTimeout, logger) defer tracker.Close() srcIP := net.ParseIP("192.168.1.1") @@ -287,7 +287,7 @@ func BenchmarkTCPTracker(b *testing.B) { // Benchmark connection cleanup func BenchmarkCleanup(b *testing.B) { b.Run("TCPCleanup", func(b *testing.B) { - tracker := NewTCPTracker(100 * time.Millisecond) // Short timeout for testing + tracker := NewTCPTracker(100*time.Millisecond, logger) // Short timeout for testing defer tracker.Close() // Pre-populate with expired connections diff --git a/client/firewall/uspfilter/conntrack/udp.go b/client/firewall/uspfilter/conntrack/udp.go index a969a4e84..e73465e31 100644 --- a/client/firewall/uspfilter/conntrack/udp.go +++ b/client/firewall/uspfilter/conntrack/udp.go @@ -4,6 +4,8 @@ import ( "net" "sync" "time" + + nblog "github.com/netbirdio/netbird/client/firewall/uspfilter/log" ) const ( @@ -20,6 +22,7 @@ type UDPConnTrack struct { // UDPTracker manages UDP connection states type UDPTracker struct { + logger *nblog.Logger connections map[ConnKey]*UDPConnTrack timeout time.Duration cleanupTicker *time.Ticker @@ -29,12 +32,13 @@ type UDPTracker struct { } // NewUDPTracker creates a new UDP connection tracker -func NewUDPTracker(timeout time.Duration) *UDPTracker { +func NewUDPTracker(timeout time.Duration, logger *nblog.Logger) *UDPTracker { if timeout == 0 { timeout = DefaultUDPTimeout } tracker := &UDPTracker{ + logger: logger, connections: make(map[ConnKey]*UDPConnTrack), timeout: timeout, cleanupTicker: time.NewTicker(UDPCleanupInterval), @@ -49,7 +53,6 @@ func NewUDPTracker(timeout time.Duration) *UDPTracker { // TrackOutbound records an outbound UDP connection func (t *UDPTracker) TrackOutbound(srcIP net.IP, dstIP net.IP, srcPort uint16, dstPort uint16) { key := makeConnKey(srcIP, dstIP, srcPort, dstPort) - now := time.Now().UnixNano() t.mutex.Lock() conn, exists := t.connections[key] @@ -67,13 +70,14 @@ func (t *UDPTracker) TrackOutbound(srcIP net.IP, dstIP net.IP, srcPort uint16, d DestPort: dstPort, }, } - conn.lastSeen.Store(now) - conn.established.Store(true) + conn.UpdateLastSeen() t.connections[key] = conn + + t.logger.Trace("New UDP connection: %v", conn) } t.mutex.Unlock() - conn.lastSeen.Store(now) + conn.UpdateLastSeen() } // IsValidInbound checks if an inbound packet matches a tracked connection @@ -92,8 +96,7 @@ func (t *UDPTracker) IsValidInbound(srcIP net.IP, dstIP net.IP, srcPort uint16, return false } - return conn.IsEstablished() && - ValidateIPs(MakeIPAddr(srcIP), conn.DestIP) && + return ValidateIPs(MakeIPAddr(srcIP), conn.DestIP) && ValidateIPs(MakeIPAddr(dstIP), conn.SourceIP) && conn.DestPort == srcPort && conn.SourcePort == dstPort @@ -120,6 +123,8 @@ func (t *UDPTracker) cleanup() { t.ipPool.Put(conn.SourceIP) t.ipPool.Put(conn.DestIP) delete(t.connections, key) + + t.logger.Trace("Removed UDP connection %v (timeout)", conn) } } } diff --git a/client/firewall/uspfilter/conntrack/udp_test.go b/client/firewall/uspfilter/conntrack/udp_test.go index 671721890..fa83ee356 100644 --- a/client/firewall/uspfilter/conntrack/udp_test.go +++ b/client/firewall/uspfilter/conntrack/udp_test.go @@ -29,7 +29,7 @@ func TestNewUDPTracker(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - tracker := NewUDPTracker(tt.timeout) + tracker := NewUDPTracker(tt.timeout, logger) assert.NotNil(t, tracker) assert.Equal(t, tt.wantTimeout, tracker.timeout) assert.NotNil(t, tracker.connections) @@ -40,7 +40,7 @@ func TestNewUDPTracker(t *testing.T) { } func TestUDPTracker_TrackOutbound(t *testing.T) { - tracker := NewUDPTracker(DefaultUDPTimeout) + tracker := NewUDPTracker(DefaultUDPTimeout, logger) defer tracker.Close() srcIP := net.ParseIP("192.168.1.2") @@ -58,12 +58,11 @@ func TestUDPTracker_TrackOutbound(t *testing.T) { assert.True(t, conn.DestIP.Equal(dstIP)) assert.Equal(t, srcPort, conn.SourcePort) assert.Equal(t, dstPort, conn.DestPort) - assert.True(t, conn.IsEstablished()) assert.WithinDuration(t, time.Now(), conn.GetLastSeen(), 1*time.Second) } func TestUDPTracker_IsValidInbound(t *testing.T) { - tracker := NewUDPTracker(1 * time.Second) + tracker := NewUDPTracker(1*time.Second, logger) defer tracker.Close() srcIP := net.ParseIP("192.168.1.2") @@ -162,6 +161,7 @@ func TestUDPTracker_Cleanup(t *testing.T) { cleanupTicker: time.NewTicker(cleanupInterval), done: make(chan struct{}), ipPool: NewPreallocatedIPs(), + logger: logger, } // Start cleanup routine @@ -211,7 +211,7 @@ func TestUDPTracker_Cleanup(t *testing.T) { func BenchmarkUDPTracker(b *testing.B) { b.Run("TrackOutbound", func(b *testing.B) { - tracker := NewUDPTracker(DefaultUDPTimeout) + tracker := NewUDPTracker(DefaultUDPTimeout, logger) defer tracker.Close() srcIP := net.ParseIP("192.168.1.1") @@ -224,7 +224,7 @@ func BenchmarkUDPTracker(b *testing.B) { }) b.Run("IsValidInbound", func(b *testing.B) { - tracker := NewUDPTracker(DefaultUDPTimeout) + tracker := NewUDPTracker(DefaultUDPTimeout, logger) defer tracker.Close() srcIP := net.ParseIP("192.168.1.1") diff --git a/client/firewall/uspfilter/forwarder/endpoint.go b/client/firewall/uspfilter/forwarder/endpoint.go new file mode 100644 index 000000000..e8a265c94 --- /dev/null +++ b/client/firewall/uspfilter/forwarder/endpoint.go @@ -0,0 +1,81 @@ +package forwarder + +import ( + wgdevice "golang.zx2c4.com/wireguard/device" + "gvisor.dev/gvisor/pkg/tcpip" + "gvisor.dev/gvisor/pkg/tcpip/header" + "gvisor.dev/gvisor/pkg/tcpip/stack" + + nblog "github.com/netbirdio/netbird/client/firewall/uspfilter/log" +) + +// endpoint implements stack.LinkEndpoint and handles integration with the wireguard device +type endpoint struct { + logger *nblog.Logger + dispatcher stack.NetworkDispatcher + device *wgdevice.Device + mtu uint32 +} + +func (e *endpoint) Attach(dispatcher stack.NetworkDispatcher) { + e.dispatcher = dispatcher +} + +func (e *endpoint) IsAttached() bool { + return e.dispatcher != nil +} + +func (e *endpoint) MTU() uint32 { + return e.mtu +} + +func (e *endpoint) Capabilities() stack.LinkEndpointCapabilities { + return stack.CapabilityNone +} + +func (e *endpoint) MaxHeaderLength() uint16 { + return 0 +} + +func (e *endpoint) LinkAddress() tcpip.LinkAddress { + return "" +} + +func (e *endpoint) WritePackets(pkts stack.PacketBufferList) (int, tcpip.Error) { + var written int + for _, pkt := range pkts.AsSlice() { + netHeader := header.IPv4(pkt.NetworkHeader().View().AsSlice()) + + data := stack.PayloadSince(pkt.NetworkHeader()) + if data == nil { + continue + } + + // Send the packet through WireGuard + address := netHeader.DestinationAddress() + err := e.device.CreateOutboundPacket(data.AsSlice(), address.AsSlice()) + if err != nil { + e.logger.Error("CreateOutboundPacket: %v", err) + continue + } + written++ + } + + return written, nil +} + +func (e *endpoint) Wait() { + // not required +} + +func (e *endpoint) ARPHardwareType() header.ARPHardwareType { + return header.ARPHardwareNone +} + +func (e *endpoint) AddHeader(*stack.PacketBuffer) { + // not required +} + +func (e *endpoint) ParseHeader(*stack.PacketBuffer) bool { + return true +} diff --git a/client/firewall/uspfilter/forwarder/forwarder.go b/client/firewall/uspfilter/forwarder/forwarder.go new file mode 100644 index 000000000..4ed152b79 --- /dev/null +++ b/client/firewall/uspfilter/forwarder/forwarder.go @@ -0,0 +1,166 @@ +package forwarder + +import ( + "context" + "fmt" + "net" + "runtime" + + log "github.com/sirupsen/logrus" + "gvisor.dev/gvisor/pkg/buffer" + "gvisor.dev/gvisor/pkg/tcpip" + "gvisor.dev/gvisor/pkg/tcpip/header" + "gvisor.dev/gvisor/pkg/tcpip/network/ipv4" + "gvisor.dev/gvisor/pkg/tcpip/stack" + "gvisor.dev/gvisor/pkg/tcpip/transport/icmp" + "gvisor.dev/gvisor/pkg/tcpip/transport/tcp" + "gvisor.dev/gvisor/pkg/tcpip/transport/udp" + + "github.com/netbirdio/netbird/client/firewall/uspfilter/common" + nblog "github.com/netbirdio/netbird/client/firewall/uspfilter/log" +) + +const ( + defaultReceiveWindow = 32768 + defaultMaxInFlight = 1024 + iosReceiveWindow = 16384 + iosMaxInFlight = 256 +) + +type Forwarder struct { + logger *nblog.Logger + stack *stack.Stack + endpoint *endpoint + udpForwarder *udpForwarder + ctx context.Context + cancel context.CancelFunc + ip net.IP + netstack bool +} + +func New(iface common.IFaceMapper, logger *nblog.Logger, netstack bool) (*Forwarder, error) { + s := stack.New(stack.Options{ + NetworkProtocols: []stack.NetworkProtocolFactory{ipv4.NewProtocol}, + TransportProtocols: []stack.TransportProtocolFactory{ + tcp.NewProtocol, + udp.NewProtocol, + icmp.NewProtocol4, + }, + HandleLocal: false, + }) + + mtu, err := iface.GetDevice().MTU() + if err != nil { + return nil, fmt.Errorf("get MTU: %w", err) + } + nicID := tcpip.NICID(1) + endpoint := &endpoint{ + logger: logger, + device: iface.GetWGDevice(), + mtu: uint32(mtu), + } + + if err := s.CreateNIC(nicID, endpoint); err != nil { + return nil, fmt.Errorf("failed to create NIC: %v", err) + } + + ones, _ := iface.Address().Network.Mask.Size() + protoAddr := tcpip.ProtocolAddress{ + Protocol: ipv4.ProtocolNumber, + AddressWithPrefix: tcpip.AddressWithPrefix{ + Address: tcpip.AddrFromSlice(iface.Address().IP.To4()), + PrefixLen: ones, + }, + } + + if err := s.AddProtocolAddress(nicID, protoAddr, stack.AddressProperties{}); err != nil { + return nil, fmt.Errorf("failed to add protocol address: %s", err) + } + + defaultSubnet, err := tcpip.NewSubnet( + tcpip.AddrFrom4([4]byte{0, 0, 0, 0}), + tcpip.MaskFromBytes([]byte{0, 0, 0, 0}), + ) + if err != nil { + return nil, fmt.Errorf("creating default subnet: %w", err) + } + + if err := s.SetPromiscuousMode(nicID, true); err != nil { + return nil, fmt.Errorf("set promiscuous mode: %s", err) + } + if err := s.SetSpoofing(nicID, true); err != nil { + return nil, fmt.Errorf("set spoofing: %s", err) + } + + s.SetRouteTable([]tcpip.Route{ + { + Destination: defaultSubnet, + NIC: nicID, + }, + }) + + ctx, cancel := context.WithCancel(context.Background()) + f := &Forwarder{ + logger: logger, + stack: s, + endpoint: endpoint, + udpForwarder: newUDPForwarder(mtu, logger), + ctx: ctx, + cancel: cancel, + netstack: netstack, + ip: iface.Address().IP, + } + + receiveWindow := defaultReceiveWindow + maxInFlight := defaultMaxInFlight + if runtime.GOOS == "ios" { + receiveWindow = iosReceiveWindow + maxInFlight = iosMaxInFlight + } + + tcpForwarder := tcp.NewForwarder(s, receiveWindow, maxInFlight, f.handleTCP) + s.SetTransportProtocolHandler(tcp.ProtocolNumber, tcpForwarder.HandlePacket) + + udpForwarder := udp.NewForwarder(s, f.handleUDP) + s.SetTransportProtocolHandler(udp.ProtocolNumber, udpForwarder.HandlePacket) + + s.SetTransportProtocolHandler(icmp.ProtocolNumber4, f.handleICMP) + + log.Debugf("forwarder: Initialization complete with NIC %d", nicID) + return f, nil +} + +func (f *Forwarder) InjectIncomingPacket(payload []byte) error { + if len(payload) < header.IPv4MinimumSize { + return fmt.Errorf("packet too small: %d bytes", len(payload)) + } + + pkt := stack.NewPacketBuffer(stack.PacketBufferOptions{ + Payload: buffer.MakeWithData(payload), + }) + defer pkt.DecRef() + + if f.endpoint.dispatcher != nil { + f.endpoint.dispatcher.DeliverNetworkPacket(ipv4.ProtocolNumber, pkt) + } + return nil +} + +// Stop gracefully shuts down the forwarder +func (f *Forwarder) Stop() { + f.cancel() + + if f.udpForwarder != nil { + f.udpForwarder.Stop() + } + + f.stack.Close() + f.stack.Wait() +} + +func (f *Forwarder) determineDialAddr(addr tcpip.Address) net.IP { + if f.netstack && f.ip.Equal(addr.AsSlice()) { + return net.IPv4(127, 0, 0, 1) + } + return addr.AsSlice() +} diff --git a/client/firewall/uspfilter/forwarder/icmp.go b/client/firewall/uspfilter/forwarder/icmp.go new file mode 100644 index 000000000..14cdc37be --- /dev/null +++ b/client/firewall/uspfilter/forwarder/icmp.go @@ -0,0 +1,109 @@ +package forwarder + +import ( + "context" + "net" + "time" + + "gvisor.dev/gvisor/pkg/tcpip/header" + "gvisor.dev/gvisor/pkg/tcpip/stack" +) + +// handleICMP handles ICMP packets from the network stack +func (f *Forwarder) handleICMP(id stack.TransportEndpointID, pkt stack.PacketBufferPtr) bool { + ctx, cancel := context.WithTimeout(f.ctx, 5*time.Second) + defer cancel() + + lc := net.ListenConfig{} + // TODO: support non-root + conn, err := lc.ListenPacket(ctx, "ip4:icmp", "0.0.0.0") + if err != nil { + f.logger.Error("Failed to create ICMP socket for %v: %v", id, err) + + // This will make netstack reply on behalf of the original destination, that's ok for now + return false + } + defer func() { + if err := conn.Close(); err != nil { + f.logger.Debug("Failed to close ICMP socket: %v", err) + } + }() + + dstIP := f.determineDialAddr(id.LocalAddress) + dst := &net.IPAddr{IP: dstIP} + + // Get the complete ICMP message (header + data) + fullPacket := stack.PayloadSince(pkt.TransportHeader()) + payload := fullPacket.AsSlice() + + icmpHdr := header.ICMPv4(pkt.TransportHeader().View().AsSlice()) + + // For Echo Requests, send and handle response + switch icmpHdr.Type() { + case header.ICMPv4Echo: + return f.handleEchoResponse(icmpHdr, payload, dst, conn, id) + case header.ICMPv4EchoReply: + // dont process our own replies + return true + default: + } + + // For other ICMP types (Time Exceeded, Destination Unreachable, etc) + _, err = conn.WriteTo(payload, dst) + if err != nil { + f.logger.Error("Failed to write ICMP packet for %v: %v", id, err) + return true + } + + f.logger.Trace("Forwarded ICMP packet %v type=%v code=%v", + id, icmpHdr.Type(), icmpHdr.Code()) + + return true +} + +func (f *Forwarder) handleEchoResponse(icmpHdr header.ICMPv4, payload []byte, dst *net.IPAddr, conn net.PacketConn, id stack.TransportEndpointID) bool { + if _, err := conn.WriteTo(payload, dst); err != nil { + f.logger.Error("Failed to write ICMP packet for %v: %v", id, err) + return true + } + + f.logger.Trace("Forwarded ICMP packet %v type=%v code=%v", + id, icmpHdr.Type(), icmpHdr.Code()) + + if err := conn.SetReadDeadline(time.Now().Add(5 * time.Second)); err != nil { + f.logger.Error("Failed to set read deadline for ICMP response: %v", err) + return true + } + + response := make([]byte, f.endpoint.mtu) + n, _, err := conn.ReadFrom(response) + if err != nil { + if !isTimeout(err) { + f.logger.Error("Failed to read ICMP response: %v", err) + } + return true + } + + ipHdr := make([]byte, header.IPv4MinimumSize) + ip := header.IPv4(ipHdr) + ip.Encode(&header.IPv4Fields{ + TotalLength: uint16(header.IPv4MinimumSize + n), + TTL: 64, + Protocol: uint8(header.ICMPv4ProtocolNumber), + SrcAddr: id.LocalAddress, + DstAddr: id.RemoteAddress, + }) + ip.SetChecksum(^ip.CalculateChecksum()) + + fullPacket := make([]byte, 0, len(ipHdr)+n) + fullPacket = append(fullPacket, ipHdr...) + fullPacket = append(fullPacket, response[:n]...) + + if err := f.InjectIncomingPacket(fullPacket); err != nil { + f.logger.Error("Failed to inject ICMP response: %v", err) + return true + } + + f.logger.Trace("Forwarded ICMP echo reply for %v", id) + return true +} diff --git a/client/firewall/uspfilter/forwarder/tcp.go b/client/firewall/uspfilter/forwarder/tcp.go new file mode 100644 index 000000000..6d7cf3b6a --- /dev/null +++ b/client/firewall/uspfilter/forwarder/tcp.go @@ -0,0 +1,90 @@ +package forwarder + +import ( + "context" + "fmt" + "io" + "net" + + "gvisor.dev/gvisor/pkg/tcpip" + "gvisor.dev/gvisor/pkg/tcpip/adapters/gonet" + "gvisor.dev/gvisor/pkg/tcpip/stack" + "gvisor.dev/gvisor/pkg/tcpip/transport/tcp" + "gvisor.dev/gvisor/pkg/waiter" +) + +// handleTCP is called by the TCP forwarder for new connections. +func (f *Forwarder) handleTCP(r *tcp.ForwarderRequest) { + id := r.ID() + + dialAddr := fmt.Sprintf("%s:%d", f.determineDialAddr(id.LocalAddress), id.LocalPort) + + outConn, err := (&net.Dialer{}).DialContext(f.ctx, "tcp", dialAddr) + if err != nil { + r.Complete(true) + f.logger.Trace("forwarder: dial error for %v: %v", id, err) + return + } + + // Create wait queue for blocking syscalls + wq := waiter.Queue{} + + ep, epErr := r.CreateEndpoint(&wq) + if epErr != nil { + f.logger.Error("forwarder: failed to create TCP endpoint: %v", epErr) + if err := outConn.Close(); err != nil { + f.logger.Debug("forwarder: outConn close error: %v", err) + } + r.Complete(true) + return + } + + // Complete the handshake + r.Complete(false) + + inConn := gonet.NewTCPConn(&wq, ep) + + f.logger.Trace("forwarder: established TCP connection %v", id) + + go f.proxyTCP(id, inConn, outConn, ep) +} + +func (f *Forwarder) proxyTCP(id stack.TransportEndpointID, inConn *gonet.TCPConn, outConn net.Conn, ep tcpip.Endpoint) { + defer func() { + if err := inConn.Close(); err != nil { + f.logger.Debug("forwarder: inConn close error: %v", err) + } + if err := outConn.Close(); err != nil { + f.logger.Debug("forwarder: outConn close error: %v", err) + } + ep.Close() + }() + + // Create context for managing the proxy goroutines + ctx, cancel := context.WithCancel(f.ctx) + defer cancel() + + errChan := make(chan error, 2) + + go func() { + _, err := io.Copy(outConn, inConn) + errChan <- err + }() + + go func() { + _, err := io.Copy(inConn, outConn) + errChan <- err + }() + + select { + case <-ctx.Done(): + f.logger.Trace("forwarder: tearing down TCP connection %v due to context done", id) + return + case err := <-errChan: + if err != nil && !isClosedError(err) { + f.logger.Error("proxyTCP: copy error: %v", err) + } + f.logger.Trace("forwarder: tearing down TCP connection %v", id) + return + } +} diff --git a/client/firewall/uspfilter/forwarder/udp.go b/client/firewall/uspfilter/forwarder/udp.go new file mode 100644 index 000000000..97e4662fd --- /dev/null +++ b/client/firewall/uspfilter/forwarder/udp.go @@ -0,0 +1,288 @@ +package forwarder + +import ( + "context" + "errors" + "fmt" + "net" + "sync" + "sync/atomic" + "time" + + "gvisor.dev/gvisor/pkg/tcpip" + "gvisor.dev/gvisor/pkg/tcpip/adapters/gonet" + "gvisor.dev/gvisor/pkg/tcpip/stack" + "gvisor.dev/gvisor/pkg/tcpip/transport/udp" + "gvisor.dev/gvisor/pkg/waiter" + + nblog "github.com/netbirdio/netbird/client/firewall/uspfilter/log" +) + +const ( + udpTimeout = 30 * time.Second +) + +type udpPacketConn struct { + conn *gonet.UDPConn + outConn net.Conn + lastSeen atomic.Int64 + cancel context.CancelFunc + ep tcpip.Endpoint +} + +type udpForwarder struct { + sync.RWMutex + logger *nblog.Logger + conns map[stack.TransportEndpointID]*udpPacketConn + bufPool sync.Pool + ctx context.Context + cancel context.CancelFunc +} + +type idleConn struct { + id stack.TransportEndpointID + conn *udpPacketConn +} + +func newUDPForwarder(mtu int, logger *nblog.Logger) *udpForwarder { + ctx, cancel := context.WithCancel(context.Background()) + f := &udpForwarder{ + logger: logger, + conns: make(map[stack.TransportEndpointID]*udpPacketConn), + ctx: ctx, + cancel: cancel, + bufPool: sync.Pool{ + New: func() any { + b := make([]byte, mtu) + return &b + }, + }, + } + go f.cleanup() + return f +} + +// Stop stops the UDP forwarder and all active connections +func (f *udpForwarder) Stop() { + f.cancel() + + f.Lock() + defer f.Unlock() + + for id, conn := range f.conns { + conn.cancel() + if err := conn.conn.Close(); err != nil { + f.logger.Debug("forwarder: UDP conn close error for %v: %v", id, err) + } + if err := conn.outConn.Close(); err != nil { + f.logger.Debug("forwarder: UDP outConn close error for %v: %v", id, err) + } + + conn.ep.Close() + delete(f.conns, id) + } +} + +// cleanup periodically removes idle UDP connections +func (f *udpForwarder) cleanup() { + ticker := time.NewTicker(time.Minute) + defer ticker.Stop() + + for { + select { + case <-f.ctx.Done(): + return + case <-ticker.C: + var idleConns []idleConn + + f.RLock() + for id, conn := range f.conns { + if conn.getIdleDuration() > udpTimeout { + idleConns = append(idleConns, idleConn{id, conn}) + } + } + f.RUnlock() + + for _, idle := range idleConns { + idle.conn.cancel() + if err := idle.conn.conn.Close(); err != nil { + f.logger.Debug("forwarder: UDP conn close error for %v: %v", idle.id, err) + } + if err := idle.conn.outConn.Close(); err != nil { + f.logger.Debug("forwarder: UDP outConn close error for %v: %v", idle.id, err) + } + + idle.conn.ep.Close() + + f.Lock() + delete(f.conns, idle.id) + f.Unlock() + + f.logger.Trace("forwarder: cleaned up idle UDP connection %v", idle.id) + } + } + } +} + +// handleUDP is called by the UDP forwarder for new packets +func (f *Forwarder) handleUDP(r *udp.ForwarderRequest) { + if f.ctx.Err() != nil { + f.logger.Trace("forwarder: context done, dropping UDP packet") + return + } + + id := r.ID() + + f.udpForwarder.RLock() + _, exists := f.udpForwarder.conns[id] + f.udpForwarder.RUnlock() + if exists { + f.logger.Trace("forwarder: existing UDP connection for %v", id) + return + } + + dstAddr := fmt.Sprintf("%s:%d", f.determineDialAddr(id.LocalAddress), id.LocalPort) + outConn, err := (&net.Dialer{}).DialContext(f.ctx, "udp", dstAddr) + if err != nil { + f.logger.Debug("forwarder: UDP dial error for %v: %v", id, err) + // TODO: Send ICMP error message + return + } + + // Create wait queue for blocking syscalls + wq := waiter.Queue{} + ep, epErr := r.CreateEndpoint(&wq) + if epErr != nil { + f.logger.Debug("forwarder: failed to create UDP endpoint: %v", epErr) + if err := outConn.Close(); err != nil { + f.logger.Debug("forwarder: UDP outConn close error for %v: %v", id, err) + } + return + } + + inConn := gonet.NewUDPConn(f.stack, &wq, ep) + connCtx, connCancel := context.WithCancel(f.ctx) + + pConn := &udpPacketConn{ + conn: inConn, + outConn: outConn, + cancel: connCancel, + ep: ep, + } + pConn.updateLastSeen() + + f.udpForwarder.Lock() + // Double-check no connection was created while we were setting up + if _, exists := f.udpForwarder.conns[id]; exists { + f.udpForwarder.Unlock() + pConn.cancel() + if err := inConn.Close(); err != nil { + f.logger.Debug("forwarder: UDP inConn close error for %v: %v", id, err) + } + if err := outConn.Close(); err != nil { + f.logger.Debug("forwarder: UDP outConn close error for %v: %v", id, err) + } + return + } + f.udpForwarder.conns[id] = pConn + f.udpForwarder.Unlock() + + f.logger.Trace("forwarder: established UDP connection to %v", id) + go f.proxyUDP(connCtx, pConn, id, ep) +} + +func (f *Forwarder) proxyUDP(ctx context.Context, pConn *udpPacketConn, id stack.TransportEndpointID, ep tcpip.Endpoint) { + defer func() { + pConn.cancel() + if err := pConn.conn.Close(); err != nil { + f.logger.Debug("forwarder: UDP inConn close error for %v: %v", id, err) + } + if err := pConn.outConn.Close(); err != nil { + f.logger.Debug("forwarder: UDP outConn close error for %v: %v", id, err) + } + + ep.Close() + + f.udpForwarder.Lock() + delete(f.udpForwarder.conns, id) + f.udpForwarder.Unlock() + }() + + errChan := make(chan error, 2) + + go func() { + errChan <- pConn.copy(ctx, pConn.conn, pConn.outConn, &f.udpForwarder.bufPool, "outbound->inbound") + }() + + go func() { + errChan <- pConn.copy(ctx, pConn.outConn, pConn.conn, &f.udpForwarder.bufPool, "inbound->outbound") + }() + + select { + case <-ctx.Done(): + f.logger.Trace("forwarder: tearing down UDP connection %v due to context done", id) + return + case err := <-errChan: + if err != nil && !isClosedError(err) { + f.logger.Error("proxyUDP: copy error: %v", err) + } + f.logger.Trace("forwarder: tearing down UDP connection %v", id) + return + } +} + +func (c *udpPacketConn) updateLastSeen() { + c.lastSeen.Store(time.Now().UnixNano()) +} + +func (c *udpPacketConn) getIdleDuration() time.Duration { + lastSeen := time.Unix(0, c.lastSeen.Load()) + return time.Since(lastSeen) +} + +func (c *udpPacketConn) copy(ctx context.Context, dst net.Conn, src net.Conn, bufPool *sync.Pool, direction string) error { + bufp := bufPool.Get().(*[]byte) + defer bufPool.Put(bufp) + buffer := *bufp + + if err := src.SetReadDeadline(time.Now().Add(udpTimeout)); err != nil { + return fmt.Errorf("set read deadline: %w", err) + } + if err := src.SetWriteDeadline(time.Now().Add(udpTimeout)); err != nil { + return fmt.Errorf("set write deadline: %w", err) + } + + for { + select { + case <-ctx.Done(): + return ctx.Err() + default: + n, err := src.Read(buffer) + if err != nil { + if isTimeout(err) { + continue + } + return fmt.Errorf("read from %s: %w", direction, err) + } + + _, err = dst.Write(buffer[:n]) + if err != nil { + return fmt.Errorf("write to %s: %w", direction, err) + } + + c.updateLastSeen() + } + } +} + +func isClosedError(err error) bool { + return errors.Is(err, net.ErrClosed) || errors.Is(err, context.Canceled) +} + +func isTimeout(err error) bool { + var netErr net.Error + if errors.As(err, &netErr) { + return netErr.Timeout() + } + return false +} diff --git a/client/firewall/uspfilter/localip.go b/client/firewall/uspfilter/localip.go new file mode 100644 index 000000000..7664b65d5 --- /dev/null +++ b/client/firewall/uspfilter/localip.go @@ -0,0 +1,134 @@ +package uspfilter + +import ( + "fmt" + "net" + "sync" + + log "github.com/sirupsen/logrus" + + "github.com/netbirdio/netbird/client/firewall/uspfilter/common" +) + +type localIPManager struct { + mu sync.RWMutex + + // Use bitmap for IPv4 (32 bits * 2^16 = 256KB memory) + ipv4Bitmap [1 << 16]uint32 +} + +func newLocalIPManager() *localIPManager { + return &localIPManager{} +} + +func (m *localIPManager) setBitmapBit(ip net.IP) { + ipv4 := ip.To4() + if ipv4 == nil { + return + } + high := (uint16(ipv4[0]) << 8) | uint16(ipv4[1]) + low := (uint16(ipv4[2]) << 8) | uint16(ipv4[3]) + m.ipv4Bitmap[high] |= 1 << (low % 32) +} + +func (m *localIPManager) checkBitmapBit(ip net.IP) bool { + ipv4 := ip.To4() + if ipv4 == nil { + return false + } + high := (uint16(ipv4[0]) << 8) | uint16(ipv4[1]) + low := (uint16(ipv4[2]) << 8) | uint16(ipv4[3]) + return (m.ipv4Bitmap[high] & (1 << (low % 32))) != 0 +} + +func (m *localIPManager) processIP(ip net.IP, newIPv4Bitmap *[1 << 16]uint32, ipv4Set map[string]struct{}, ipv4Addresses *[]string) error { + if ipv4 := ip.To4(); ipv4 != nil { + high := (uint16(ipv4[0]) << 8) | uint16(ipv4[1]) + low := (uint16(ipv4[2]) << 8) | uint16(ipv4[3]) + if int(high) >= len(*newIPv4Bitmap) { + return fmt.Errorf("invalid IPv4 address: %s", ip) + } + ipStr := ip.String() + if _, exists := ipv4Set[ipStr]; !exists { + ipv4Set[ipStr] = struct{}{} + *ipv4Addresses = append(*ipv4Addresses, ipStr) + newIPv4Bitmap[high] |= 1 << (low % 32) + } + } + return nil +} + +func (m *localIPManager) processInterface(iface net.Interface, newIPv4Bitmap *[1 << 16]uint32, ipv4Set map[string]struct{}, ipv4Addresses *[]string) { + addrs, err := iface.Addrs() + if err != nil { + log.Debugf("get addresses for interface %s failed: %v", iface.Name, err) + return + } + + for _, addr := range addrs { + var ip net.IP + switch v := addr.(type) { + case *net.IPNet: + ip = v.IP + case *net.IPAddr: + ip = v.IP + default: + continue + } + + if err := m.processIP(ip, newIPv4Bitmap, ipv4Set, ipv4Addresses); err != nil { + log.Debugf("process IP failed: %v", err) + } + } +} + +func (m *localIPManager) UpdateLocalIPs(iface common.IFaceMapper) (err error) { + defer func() { + if r := recover(); r != nil { + err = fmt.Errorf("panic: %v", r) + } + }() + + var newIPv4Bitmap [1 << 16]uint32 + ipv4Set := make(map[string]struct{}) + var ipv4Addresses []string + + // 127.0.0.0/8 + high := uint16(127) << 8 + for i := uint16(0); i < 256; i++ { + newIPv4Bitmap[high|i] = 0xffffffff + } + + if iface != nil { + if err := m.processIP(iface.Address().IP, &newIPv4Bitmap, ipv4Set, &ipv4Addresses); err != nil { + return err + } + } + + interfaces, err := net.Interfaces() + if err != nil { + log.Warnf("failed to get interfaces: %v", err) + } else { + for _, intf := range interfaces { + m.processInterface(intf, &newIPv4Bitmap, ipv4Set, &ipv4Addresses) + } + } + + m.mu.Lock() + m.ipv4Bitmap = newIPv4Bitmap + m.mu.Unlock() + + log.Debugf("Local IPv4 addresses: %v", ipv4Addresses) + return nil +} + +func (m *localIPManager) IsLocalIP(ip net.IP) bool { + m.mu.RLock() + defer m.mu.RUnlock() + + if ipv4 := ip.To4(); ipv4 != nil { + return m.checkBitmapBit(ipv4) + } + + return false +} diff --git a/client/firewall/uspfilter/localip_test.go b/client/firewall/uspfilter/localip_test.go new file mode 100644 index 000000000..02f41bf4f --- /dev/null +++ b/client/firewall/uspfilter/localip_test.go @@ -0,0 +1,270 @@ +package uspfilter + +import ( + "net" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/netbirdio/netbird/client/iface" +) + +func TestLocalIPManager(t *testing.T) { + tests := []struct { + name string + setupAddr iface.WGAddress + testIP net.IP + expected bool + }{ + { + name: "Localhost range", + setupAddr: iface.WGAddress{ + IP: net.ParseIP("192.168.1.1"), + Network: &net.IPNet{ + IP: net.ParseIP("192.168.1.0"), + Mask: net.CIDRMask(24, 32), + }, + }, + testIP: net.ParseIP("127.0.0.2"), + expected: true, + }, + { + name: "Localhost standard address", + setupAddr: iface.WGAddress{ + IP: net.ParseIP("192.168.1.1"), + Network: &net.IPNet{ + IP: net.ParseIP("192.168.1.0"), + Mask: net.CIDRMask(24, 32), + }, + }, + testIP: net.ParseIP("127.0.0.1"), + expected: true, + }, + { + name: "Localhost range edge", + setupAddr: iface.WGAddress{ + IP: net.ParseIP("192.168.1.1"), + Network: &net.IPNet{ + IP: net.ParseIP("192.168.1.0"), + Mask: net.CIDRMask(24, 32), + }, + }, + testIP: net.ParseIP("127.255.255.255"), + expected: true, + }, + { + name: "Local IP matches", + setupAddr: iface.WGAddress{ + IP: net.ParseIP("192.168.1.1"), + Network: &net.IPNet{ + IP: net.ParseIP("192.168.1.0"), + Mask: net.CIDRMask(24, 32), + }, + }, + testIP: net.ParseIP("192.168.1.1"), + expected: true, + }, + { + name: "Local IP doesn't match", + setupAddr: iface.WGAddress{ + IP: net.ParseIP("192.168.1.1"), + Network: &net.IPNet{ + IP: net.ParseIP("192.168.1.0"), + Mask: net.CIDRMask(24, 32), + }, + }, + testIP: net.ParseIP("192.168.1.2"), + expected: false, + }, + { + name: "IPv6 address", + setupAddr: iface.WGAddress{ + IP: net.ParseIP("fe80::1"), + Network: &net.IPNet{ + IP: net.ParseIP("fe80::"), + Mask: net.CIDRMask(64, 128), + }, + }, + testIP: net.ParseIP("fe80::1"), + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + manager := newLocalIPManager() + + mock := &IFaceMock{ + AddressFunc: func() iface.WGAddress { + return tt.setupAddr + }, + } + + err := manager.UpdateLocalIPs(mock) + require.NoError(t, err) + + result := manager.IsLocalIP(tt.testIP) + require.Equal(t, tt.expected, result) + }) + } +} + +func TestLocalIPManager_AllInterfaces(t *testing.T) { + manager := newLocalIPManager() + mock := &IFaceMock{} + + // Get actual local interfaces + interfaces, err := net.Interfaces() + require.NoError(t, err) + + var tests []struct { + ip string + expected bool + } + + // Add all local interface IPs to test cases + for _, iface := range interfaces { + addrs, err := iface.Addrs() + require.NoError(t, err) + + for _, addr := range addrs { + var ip net.IP + switch v := addr.(type) { + case *net.IPNet: + ip = v.IP + case *net.IPAddr: + ip = v.IP + default: + continue + } + + if ip4 := ip.To4(); ip4 != nil { + tests = append(tests, struct { + ip string + expected bool + }{ + ip: ip4.String(), + expected: true, + }) + } + } + } + + // Add some external IPs as negative test cases + externalIPs := []string{ + "8.8.8.8", + "1.1.1.1", + "208.67.222.222", + } + for _, ip := range externalIPs { + tests = append(tests, struct { + ip string + expected bool + }{ + ip: ip, + expected: false, + }) + } + + require.NotEmpty(t, tests, "No test cases generated") + + err = manager.UpdateLocalIPs(mock) + require.NoError(t, err) + + t.Logf("Testing %d IPs", len(tests)) + for _, tt := range tests { + t.Run(tt.ip, func(t *testing.T) { + result := manager.IsLocalIP(net.ParseIP(tt.ip)) + require.Equal(t, tt.expected, result, "IP: %s", tt.ip) + }) + } +} + +// MapImplementation is a version using map[string]struct{} +type MapImplementation struct { + localIPs map[string]struct{} +} + +func BenchmarkIPChecks(b *testing.B) { + interfaces := make([]net.IP, 16) + for i := range interfaces { + interfaces[i] = net.IPv4(10, 0, byte(i>>8), byte(i)) + } + + // Setup bitmap version + bitmapManager := &localIPManager{ + ipv4Bitmap: [1 << 16]uint32{}, + } + for _, ip := range interfaces[:8] { // Add half of IPs + bitmapManager.setBitmapBit(ip) + } + + // Setup map version + mapManager := &MapImplementation{ + localIPs: make(map[string]struct{}), + } + for _, ip := range interfaces[:8] { + mapManager.localIPs[ip.String()] = struct{}{} + } + + b.Run("Bitmap_Hit", func(b *testing.B) { + ip := interfaces[4] + b.ResetTimer() + for i := 0; i < b.N; i++ { + bitmapManager.checkBitmapBit(ip) + } + }) + + b.Run("Bitmap_Miss", func(b *testing.B) { + ip := interfaces[12] + b.ResetTimer() + for i := 0; i < b.N; i++ { + bitmapManager.checkBitmapBit(ip) + } + }) + + b.Run("Map_Hit", func(b *testing.B) { + ip := interfaces[4] + b.ResetTimer() + for i := 0; i < b.N; i++ { + // nolint:gosimple + _, _ = mapManager.localIPs[ip.String()] + } + }) + + b.Run("Map_Miss", func(b *testing.B) { + ip := interfaces[12] + b.ResetTimer() + for i := 0; i < b.N; i++ { + // nolint:gosimple + _, _ = mapManager.localIPs[ip.String()] + } + }) +} + +func BenchmarkWGPosition(b *testing.B) { + wgIP := net.ParseIP("10.10.0.1") + + // Create two managers - one checks WG IP first, other checks it last + b.Run("WG_First", func(b *testing.B) { + bm := &localIPManager{ipv4Bitmap: [1 << 16]uint32{}} + bm.setBitmapBit(wgIP) + b.ResetTimer() + for i := 0; i < b.N; i++ { + bm.checkBitmapBit(wgIP) + } + }) + + b.Run("WG_Last", func(b *testing.B) { + bm := &localIPManager{ipv4Bitmap: [1 << 16]uint32{}} + // Fill with other IPs first + for i := 0; i < 15; i++ { + bm.setBitmapBit(net.IPv4(10, 0, byte(i>>8), byte(i))) + } + bm.setBitmapBit(wgIP) // Add WG IP last + b.ResetTimer() + for i := 0; i < b.N; i++ { + bm.checkBitmapBit(wgIP) + } + }) +} diff --git a/client/firewall/uspfilter/log/log.go b/client/firewall/uspfilter/log/log.go new file mode 100644 index 000000000..984b6ad08 --- /dev/null +++ b/client/firewall/uspfilter/log/log.go @@ -0,0 +1,196 @@ +// Package logger provides a high-performance, non-blocking logger for userspace networking +package log + +import ( + "context" + "fmt" + "io" + "sync" + "sync/atomic" + "time" + + log "github.com/sirupsen/logrus" +) + +const ( + maxBatchSize = 1024 * 16 // 16KB max batch size + maxMessageSize = 1024 * 2 // 2KB per message + bufferSize = 1024 * 256 // 256KB ring buffer + defaultFlushInterval = 2 * time.Second +) + +// Level represents log severity +type Level uint32 + +const ( + LevelPanic Level = iota + LevelFatal + LevelError + LevelWarn + LevelInfo + LevelDebug + LevelTrace +) + +var levelStrings = map[Level]string{ + LevelPanic: "PANC", + LevelFatal: "FATL", + LevelError: "ERRO", + LevelWarn: "WARN", + LevelInfo: "INFO", + LevelDebug: "DEBG", + LevelTrace: "TRAC", +} + +// Logger is a high-performance, non-blocking logger +type Logger struct { + output io.Writer + level atomic.Uint32 + buffer *ringBuffer + shutdown chan struct{} + closeOnce sync.Once + wg sync.WaitGroup + + // Reusable buffer pool for formatting messages + bufPool sync.Pool +} + +func NewFromLogrus(logrusLogger *log.Logger) *Logger { + l := &Logger{ + output: logrusLogger.Out, + buffer: newRingBuffer(bufferSize), + shutdown: make(chan struct{}), + bufPool: sync.Pool{ + New: func() interface{} { + // Pre-allocate buffer for message formatting + b := make([]byte, 0, maxMessageSize) + return &b + }, + }, + } + logrusLevel := logrusLogger.GetLevel() + l.level.Store(uint32(logrusLevel)) + level := levelStrings[Level(logrusLevel)] + log.Debugf("New uspfilter logger created with loglevel %v", level) + + l.wg.Add(1) + go l.worker() + + return l +} + +func (l *Logger) SetLevel(level Level) { + l.level.Store(uint32(level)) + + log.Debugf("Set uspfilter logger loglevel to %v", levelStrings[level]) +} + +func (l *Logger) formatMessage(buf *[]byte, level Level, format string, args ...interface{}) { + *buf = (*buf)[:0] + + // Timestamp + *buf = time.Now().AppendFormat(*buf, "2006-01-02T15:04:05-07:00") + *buf = append(*buf, ' ') + + // Level + *buf = append(*buf, levelStrings[level]...) + *buf = append(*buf, ' ') + + // Message + if len(args) > 0 { + *buf = append(*buf, fmt.Sprintf(format, args...)...) + } else { + *buf = append(*buf, format...) + } + + *buf = append(*buf, '\n') +} + +func (l *Logger) log(level Level, format string, args ...interface{}) { + bufp := l.bufPool.Get().(*[]byte) + l.formatMessage(bufp, level, format, args...) + + if len(*bufp) > maxMessageSize { + *bufp = (*bufp)[:maxMessageSize] + } + _, _ = l.buffer.Write(*bufp) + + l.bufPool.Put(bufp) +} + +func (l *Logger) Error(format string, args ...interface{}) { + if l.level.Load() >= uint32(LevelError) { + l.log(LevelError, format, args...) + } +} + +func (l *Logger) Warn(format string, args ...interface{}) { + if l.level.Load() >= uint32(LevelWarn) { + l.log(LevelWarn, format, args...) + } +} + +func (l *Logger) Info(format string, args ...interface{}) { + if l.level.Load() >= uint32(LevelInfo) { + l.log(LevelInfo, format, args...) + } +} + +func (l *Logger) Debug(format string, args ...interface{}) { + if l.level.Load() >= uint32(LevelDebug) { + l.log(LevelDebug, format, args...) + } +} + +func (l *Logger) Trace(format string, args ...interface{}) { + if l.level.Load() >= uint32(LevelTrace) { + l.log(LevelTrace, format, args...) + } +} + +// worker periodically flushes the buffer +func (l *Logger) worker() { + defer l.wg.Done() + + ticker := time.NewTicker(defaultFlushInterval) + defer ticker.Stop() + + buf := make([]byte, 0, maxBatchSize) + + for { + select { + case <-l.shutdown: + return + case <-ticker.C: + // Read accumulated messages + n, _ := l.buffer.Read(buf[:cap(buf)]) + if n == 0 { + continue + } + + // Write batch + _, _ = l.output.Write(buf[:n]) + } + } +} + +// Stop gracefully shuts down the logger +func (l *Logger) Stop(ctx context.Context) error { + done := make(chan struct{}) + + l.closeOnce.Do(func() { + close(l.shutdown) + }) + + go func() { + l.wg.Wait() + close(done) + }() + + select { + case <-ctx.Done(): + return ctx.Err() + case <-done: + return nil + } +} diff --git a/client/firewall/uspfilter/log/ringbuffer.go b/client/firewall/uspfilter/log/ringbuffer.go new file mode 100644 index 000000000..dbc8f1289 --- /dev/null +++ b/client/firewall/uspfilter/log/ringbuffer.go @@ -0,0 +1,85 @@ +package log + +import "sync" + +// ringBuffer is a simple ring buffer implementation +type ringBuffer struct { + buf []byte + size int + r, w int64 // Read and write positions + mu sync.Mutex +} + +func newRingBuffer(size int) *ringBuffer { + return &ringBuffer{ + buf: make([]byte, size), + size: size, + } +} + +func (r *ringBuffer) Write(p []byte) (n int, err error) { + if len(p) == 0 { + return 0, nil + } + + r.mu.Lock() + defer r.mu.Unlock() + + if len(p) > r.size { + p = p[:r.size] + } + + n = len(p) + + // Write data, handling wrap-around + pos := int(r.w % int64(r.size)) + writeLen := min(len(p), r.size-pos) + copy(r.buf[pos:], p[:writeLen]) + + // If we have more data and need to wrap around + if writeLen < len(p) { + copy(r.buf, p[writeLen:]) + } + + // Update write position + r.w += int64(n) + + return n, nil +} + +func (r *ringBuffer) Read(p []byte) (n int, err error) { + r.mu.Lock() + defer r.mu.Unlock() + + if r.w == r.r { + return 0, nil + } + + // Calculate available data accounting for wraparound + available := int(r.w - r.r) + if available < 0 { + available += r.size + } + available = min(available, r.size) + + // Limit read to buffer size + toRead := min(available, len(p)) + if toRead == 0 { + return 0, nil + } + + // Read data, handling wrap-around + pos := int(r.r % int64(r.size)) + readLen := min(toRead, r.size-pos) + n = copy(p, r.buf[pos:pos+readLen]) + + // If we need more data and need to wrap around + if readLen < toRead { + n += copy(p[readLen:toRead], r.buf[:toRead-readLen]) + } + + // Update read position + r.r += int64(n) + + return n, nil +} diff --git a/client/firewall/uspfilter/rule.go b/client/firewall/uspfilter/rule.go index c59d4b264..6a4415f73 100644 --- a/client/firewall/uspfilter/rule.go +++ b/client/firewall/uspfilter/rule.go @@ -2,14 +2,15 @@ package uspfilter import ( "net" + "net/netip" "github.com/google/gopacket" firewall "github.com/netbirdio/netbird/client/firewall/manager" ) -// Rule to handle management of rules -type Rule struct { +// PeerRule to handle management of rules +type PeerRule struct { id string ip net.IP ipLayer gopacket.LayerType @@ -24,6 +25,21 @@ type Rule struct { } // GetRuleID returns the rule id -func (r *Rule) GetRuleID() string { +func (r *PeerRule) GetRuleID() string { + return r.id +} + +type RouteRule struct { + id string + sources []netip.Prefix + destination netip.Prefix + proto firewall.Protocol + srcPort *firewall.Port + dstPort *firewall.Port + action firewall.Action +} + +// GetRuleID returns the rule id +func (r *RouteRule) GetRuleID() string { return r.id } diff --git a/client/firewall/uspfilter/tracer.go b/client/firewall/uspfilter/tracer.go new file mode 100644 index 000000000..a4c653b3b --- /dev/null +++ b/client/firewall/uspfilter/tracer.go @@ -0,0 +1,390 @@ +package uspfilter + +import ( + "fmt" + "net" + "time" + + "github.com/google/gopacket" + "github.com/google/gopacket/layers" + + fw "github.com/netbirdio/netbird/client/firewall/manager" + "github.com/netbirdio/netbird/client/firewall/uspfilter/conntrack" +) + +type PacketStage int + +const ( + StageReceived PacketStage = iota + StageConntrack + StagePeerACL + StageRouting + StageRouteACL + StageForwarding + StageCompleted +) + +const msgProcessingCompleted = "Processing completed" + +func (s PacketStage) String() string { + return map[PacketStage]string{ + StageReceived: "Received", + StageConntrack: "Connection Tracking", + StagePeerACL: "Peer ACL", + StageRouting: "Routing", + StageRouteACL: "Route ACL", + StageForwarding: "Forwarding", + StageCompleted: "Completed", + }[s] +} + +type ForwarderAction struct { + Action string + RemoteAddr string + Error error +} + +type TraceResult struct { + Timestamp time.Time + Stage PacketStage + Message string + Allowed bool + ForwarderAction *ForwarderAction +} + +type PacketTrace struct { + SourceIP net.IP + DestinationIP net.IP + Protocol string + SourcePort uint16 + DestinationPort uint16 + Direction fw.RuleDirection + Results []TraceResult +} + +type TCPState struct { + SYN bool + ACK bool + FIN bool + RST bool + PSH bool + URG bool +} + +type PacketBuilder struct { + SrcIP net.IP + DstIP net.IP + Protocol fw.Protocol + SrcPort uint16 + DstPort uint16 + ICMPType uint8 + ICMPCode uint8 + Direction fw.RuleDirection + PayloadSize int + TCPState *TCPState +} + +func (t *PacketTrace) AddResult(stage PacketStage, message string, allowed bool) { + t.Results = append(t.Results, TraceResult{ + Timestamp: time.Now(), + Stage: stage, + Message: message, + Allowed: allowed, + }) +} + +func (t *PacketTrace) AddResultWithForwarder(stage PacketStage, message string, allowed bool, action *ForwarderAction) { + t.Results = append(t.Results, TraceResult{ + Timestamp: time.Now(), + Stage: stage, + Message: message, + Allowed: allowed, + ForwarderAction: action, + }) +} + +func (p *PacketBuilder) Build() ([]byte, error) { + ip := p.buildIPLayer() + pktLayers := []gopacket.SerializableLayer{ip} + + transportLayer, err := p.buildTransportLayer(ip) + if err != nil { + return nil, err + } + pktLayers = append(pktLayers, transportLayer...) + + if p.PayloadSize > 0 { + payload := make([]byte, p.PayloadSize) + pktLayers = append(pktLayers, gopacket.Payload(payload)) + } + + return serializePacket(pktLayers) +} + +func (p *PacketBuilder) buildIPLayer() *layers.IPv4 { + return &layers.IPv4{ + Version: 4, + TTL: 64, + Protocol: layers.IPProtocol(getIPProtocolNumber(p.Protocol)), + SrcIP: p.SrcIP, + DstIP: p.DstIP, + } +} + +func (p *PacketBuilder) buildTransportLayer(ip *layers.IPv4) ([]gopacket.SerializableLayer, error) { + switch p.Protocol { + case "tcp": + return p.buildTCPLayer(ip) + case "udp": + return p.buildUDPLayer(ip) + case "icmp": + return p.buildICMPLayer() + default: + return nil, fmt.Errorf("unsupported protocol: %s", p.Protocol) + } +} + +func (p *PacketBuilder) buildTCPLayer(ip *layers.IPv4) ([]gopacket.SerializableLayer, error) { + tcp := &layers.TCP{ + SrcPort: layers.TCPPort(p.SrcPort), + DstPort: layers.TCPPort(p.DstPort), + Window: 65535, + SYN: p.TCPState != nil && p.TCPState.SYN, + ACK: p.TCPState != nil && p.TCPState.ACK, + FIN: p.TCPState != nil && p.TCPState.FIN, + RST: p.TCPState != nil && p.TCPState.RST, + PSH: p.TCPState != nil && p.TCPState.PSH, + URG: p.TCPState != nil && p.TCPState.URG, + } + if err := tcp.SetNetworkLayerForChecksum(ip); err != nil { + return nil, fmt.Errorf("set network layer for TCP checksum: %w", err) + } + return []gopacket.SerializableLayer{tcp}, nil +} + +func (p *PacketBuilder) buildUDPLayer(ip *layers.IPv4) ([]gopacket.SerializableLayer, error) { + udp := &layers.UDP{ + SrcPort: layers.UDPPort(p.SrcPort), + DstPort: layers.UDPPort(p.DstPort), + } + if err := udp.SetNetworkLayerForChecksum(ip); err != nil { + return nil, fmt.Errorf("set network layer for UDP checksum: %w", err) + } + return []gopacket.SerializableLayer{udp}, nil +} + +func (p *PacketBuilder) buildICMPLayer() ([]gopacket.SerializableLayer, error) { + icmp := &layers.ICMPv4{ + TypeCode: layers.CreateICMPv4TypeCode(p.ICMPType, p.ICMPCode), + } + if p.ICMPType == layers.ICMPv4TypeEchoRequest || p.ICMPType == layers.ICMPv4TypeEchoReply { + icmp.Id = uint16(1) + icmp.Seq = uint16(1) + } + return []gopacket.SerializableLayer{icmp}, nil +} + +func serializePacket(layers []gopacket.SerializableLayer) ([]byte, error) { + buf := gopacket.NewSerializeBuffer() + opts := gopacket.SerializeOptions{ + ComputeChecksums: true, + FixLengths: true, + } + if err := gopacket.SerializeLayers(buf, opts, layers...); err != nil { + return nil, fmt.Errorf("serialize packet: %w", err) + } + return buf.Bytes(), nil +} + +func getIPProtocolNumber(protocol fw.Protocol) int { + switch protocol { + case fw.ProtocolTCP: + return int(layers.IPProtocolTCP) + case fw.ProtocolUDP: + return int(layers.IPProtocolUDP) + case fw.ProtocolICMP: + return int(layers.IPProtocolICMPv4) + default: + return 0 + } +} + +func (m *Manager) TracePacketFromBuilder(builder *PacketBuilder) (*PacketTrace, error) { + packetData, err := builder.Build() + if err != nil { + return nil, fmt.Errorf("build packet: %w", err) + } + + return m.TracePacket(packetData, builder.Direction), nil +} + +func (m *Manager) TracePacket(packetData []byte, direction fw.RuleDirection) *PacketTrace { + + d := m.decoders.Get().(*decoder) + defer m.decoders.Put(d) + + trace := &PacketTrace{Direction: direction} + + // Initial packet decoding + if err := d.parser.DecodeLayers(packetData, &d.decoded); err != nil { + trace.AddResult(StageReceived, fmt.Sprintf("Failed to decode packet: %v", err), false) + return trace + } + + // Extract base packet info + srcIP, dstIP := m.extractIPs(d) + trace.SourceIP = srcIP + trace.DestinationIP = dstIP + + // Determine protocol and ports + switch d.decoded[1] { + case layers.LayerTypeTCP: + trace.Protocol = "TCP" + trace.SourcePort = uint16(d.tcp.SrcPort) + trace.DestinationPort = uint16(d.tcp.DstPort) + case layers.LayerTypeUDP: + trace.Protocol = "UDP" + trace.SourcePort = uint16(d.udp.SrcPort) + trace.DestinationPort = uint16(d.udp.DstPort) + case layers.LayerTypeICMPv4: + trace.Protocol = "ICMP" + } + + trace.AddResult(StageReceived, fmt.Sprintf("Received %s packet: %s:%d -> %s:%d", + trace.Protocol, srcIP, trace.SourcePort, dstIP, trace.DestinationPort), true) + + if direction == fw.RuleDirectionOUT { + return m.traceOutbound(packetData, trace) + } + + return m.traceInbound(packetData, trace, d, srcIP, dstIP) +} + +func (m *Manager) traceInbound(packetData []byte, trace *PacketTrace, d *decoder, srcIP net.IP, dstIP net.IP) *PacketTrace { + if m.stateful && m.handleConntrackState(trace, d, srcIP, dstIP) { + return trace + } + + if m.handleLocalDelivery(trace, packetData, d, srcIP, dstIP) { + return trace + } + + if !m.handleRouting(trace) { + return trace + } + + if m.nativeRouter { + return m.handleNativeRouter(trace) + } + + return m.handleRouteACLs(trace, d, srcIP, dstIP) +} + +func (m *Manager) handleConntrackState(trace *PacketTrace, d *decoder, srcIP, dstIP net.IP) bool { + allowed := m.isValidTrackedConnection(d, srcIP, dstIP) + msg := "No existing connection found" + if allowed { + msg = m.buildConntrackStateMessage(d) + trace.AddResult(StageConntrack, msg, true) + trace.AddResult(StageCompleted, "Packet allowed by connection tracking", true) + return true + } + trace.AddResult(StageConntrack, msg, false) + return false +} + +func (m *Manager) buildConntrackStateMessage(d *decoder) string { + msg := "Matched existing connection state" + switch d.decoded[1] { + case layers.LayerTypeTCP: + flags := getTCPFlags(&d.tcp) + msg += fmt.Sprintf(" (TCP Flags: SYN=%v ACK=%v RST=%v FIN=%v)", + flags&conntrack.TCPSyn != 0, + flags&conntrack.TCPAck != 0, + flags&conntrack.TCPRst != 0, + flags&conntrack.TCPFin != 0) + case layers.LayerTypeICMPv4: + msg += fmt.Sprintf(" (ICMP ID=%d, Seq=%d)", d.icmp4.Id, d.icmp4.Seq) + } + return msg +} + +func (m *Manager) handleLocalDelivery(trace *PacketTrace, packetData []byte, d *decoder, srcIP, dstIP net.IP) bool { + if !m.localForwarding { + trace.AddResult(StageRouting, "Local forwarding disabled", false) + trace.AddResult(StageCompleted, "Packet dropped - local forwarding disabled", false) + return true + } + + trace.AddResult(StageRouting, "Packet destined for local delivery", true) + blocked := m.peerACLsBlock(srcIP, packetData, m.incomingRules, d) + + msg := "Allowed by peer ACL rules" + if blocked { + msg = "Blocked by peer ACL rules" + } + trace.AddResult(StagePeerACL, msg, !blocked) + + if m.netstack { + m.addForwardingResult(trace, "proxy-local", "127.0.0.1", !blocked) + } + + trace.AddResult(StageCompleted, msgProcessingCompleted, !blocked) + return true +} + +func (m *Manager) handleRouting(trace *PacketTrace) bool { + if !m.routingEnabled { + trace.AddResult(StageRouting, "Routing disabled", false) + trace.AddResult(StageCompleted, "Packet dropped - routing disabled", false) + return false + } + trace.AddResult(StageRouting, "Routing enabled, checking ACLs", true) + return true +} + +func (m *Manager) handleNativeRouter(trace *PacketTrace) *PacketTrace { + trace.AddResult(StageRouteACL, "Using native router, skipping ACL checks", true) + trace.AddResult(StageForwarding, "Forwarding via native router", true) + trace.AddResult(StageCompleted, msgProcessingCompleted, true) + return trace +} + +func (m *Manager) handleRouteACLs(trace *PacketTrace, d *decoder, srcIP, dstIP net.IP) *PacketTrace { + proto := getProtocolFromPacket(d) + srcPort, dstPort := getPortsFromPacket(d) + allowed := m.routeACLsPass(srcIP, dstIP, proto, srcPort, dstPort) + + msg := "Allowed by route ACLs" + if !allowed { + msg = "Blocked by route ACLs" + } + trace.AddResult(StageRouteACL, msg, allowed) + + if allowed && m.forwarder != nil { + m.addForwardingResult(trace, "proxy-remote", fmt.Sprintf("%s:%d", dstIP, dstPort), true) + } + + trace.AddResult(StageCompleted, msgProcessingCompleted, allowed) + return trace +} + +func (m *Manager) addForwardingResult(trace *PacketTrace, action, remoteAddr string, allowed bool) { + fwdAction := &ForwarderAction{ + Action: action, + RemoteAddr: remoteAddr, + } + trace.AddResultWithForwarder(StageForwarding, + fmt.Sprintf("Forwarding to %s", fwdAction.Action), allowed, fwdAction) +} + +func (m *Manager) traceOutbound(packetData []byte, trace *PacketTrace) *PacketTrace { + // will create or update the connection state + dropped := m.processOutgoingHooks(packetData) + if dropped { + trace.AddResult(StageCompleted, "Packet dropped by outgoing hook", false) + } else { + trace.AddResult(StageCompleted, "Packet allowed (outgoing)", true) + } + return trace +} diff --git a/client/firewall/uspfilter/uspfilter.go b/client/firewall/uspfilter/uspfilter.go index 757249b2d..889e4cbb1 100644 --- a/client/firewall/uspfilter/uspfilter.go +++ b/client/firewall/uspfilter/uspfilter.go @@ -1,11 +1,14 @@ package uspfilter import ( + "errors" "fmt" "net" "net/netip" "os" + "slices" "strconv" + "strings" "sync" "github.com/google/gopacket" @@ -14,28 +17,48 @@ import ( log "github.com/sirupsen/logrus" firewall "github.com/netbirdio/netbird/client/firewall/manager" + "github.com/netbirdio/netbird/client/firewall/uspfilter/common" "github.com/netbirdio/netbird/client/firewall/uspfilter/conntrack" - "github.com/netbirdio/netbird/client/iface" - "github.com/netbirdio/netbird/client/iface/device" + "github.com/netbirdio/netbird/client/firewall/uspfilter/forwarder" + nblog "github.com/netbirdio/netbird/client/firewall/uspfilter/log" + "github.com/netbirdio/netbird/client/iface/netstack" "github.com/netbirdio/netbird/client/internal/statemanager" ) const layerTypeAll = 0 -const EnvDisableConntrack = "NB_DISABLE_CONNTRACK" +const ( + // EnvDisableConntrack disables the stateful filter, replies to outbound traffic won't be allowed. + EnvDisableConntrack = "NB_DISABLE_CONNTRACK" -var ( - errRouteNotSupported = fmt.Errorf("route not supported with userspace firewall") + // EnvDisableUserspaceRouting disables userspace routing, to-be-routed packets will be dropped. + EnvDisableUserspaceRouting = "NB_DISABLE_USERSPACE_ROUTING" + + // EnvForceUserspaceRouter forces userspace routing even if native routing is available. + EnvForceUserspaceRouter = "NB_FORCE_USERSPACE_ROUTER" + + // EnvEnableNetstackLocalForwarding enables forwarding of local traffic to the native stack when running netstack + // Leaving this on by default introduces a security risk as sockets on listening on localhost only will be accessible + EnvEnableNetstackLocalForwarding = "NB_ENABLE_NETSTACK_LOCAL_FORWARDING" ) -// IFaceMapper defines subset methods of interface required for manager -type IFaceMapper interface { - SetFilter(device.PacketFilter) error - Address() iface.WGAddress -} - // RuleSet is a set of rules grouped by a string key -type RuleSet map[string]Rule +type RuleSet map[string]PeerRule + +type RouteRules []RouteRule + +func (r RouteRules) Sort() { + slices.SortStableFunc(r, func(a, b RouteRule) int { + // Deny rules come first + if a.action == firewall.ActionDrop && b.action != firewall.ActionDrop { + return -1 + } + if a.action != firewall.ActionDrop && b.action == firewall.ActionDrop { + return 1 + } + return strings.Compare(a.id, b.id) + }) +} // Manager userspace firewall manager type Manager struct { @@ -43,17 +66,32 @@ type Manager struct { outgoingRules map[string]RuleSet // incomingRules is used for filtering and hooks incomingRules map[string]RuleSet + routeRules RouteRules wgNetwork *net.IPNet decoders sync.Pool - wgIface IFaceMapper + wgIface common.IFaceMapper nativeFirewall firewall.Manager mutex sync.RWMutex - stateful bool + // indicates whether we forward packets not destined for ourselves + routingEnabled bool + // indicates whether we leave forwarding and filtering to the native firewall + nativeRouter bool + // indicates whether we track outbound connections + stateful bool + // indicates whether wireguards runs in netstack mode + netstack bool + // indicates whether we forward local traffic to the native stack + localForwarding bool + + localipmanager *localIPManager + udpTracker *conntrack.UDPTracker icmpTracker *conntrack.ICMPTracker tcpTracker *conntrack.TCPTracker + forwarder *forwarder.Forwarder + logger *nblog.Logger } // decoder for packages @@ -70,22 +108,32 @@ type decoder struct { } // Create userspace firewall manager constructor -func Create(iface IFaceMapper) (*Manager, error) { - return create(iface) +func Create(iface common.IFaceMapper, disableServerRoutes bool) (*Manager, error) { + return create(iface, nil, disableServerRoutes) } -func CreateWithNativeFirewall(iface IFaceMapper, nativeFirewall firewall.Manager) (*Manager, error) { - mgr, err := create(iface) +func CreateWithNativeFirewall(iface common.IFaceMapper, nativeFirewall firewall.Manager, disableServerRoutes bool) (*Manager, error) { + if nativeFirewall == nil { + return nil, errors.New("native firewall is nil") + } + + mgr, err := create(iface, nativeFirewall, disableServerRoutes) if err != nil { return nil, err } - mgr.nativeFirewall = nativeFirewall return mgr, nil } -func create(iface IFaceMapper) (*Manager, error) { - disableConntrack, _ := strconv.ParseBool(os.Getenv(EnvDisableConntrack)) +func create(iface common.IFaceMapper, nativeFirewall firewall.Manager, disableServerRoutes bool) (*Manager, error) { + disableConntrack, err := strconv.ParseBool(os.Getenv(EnvDisableConntrack)) + if err != nil { + log.Warnf("failed to parse %s: %v", EnvDisableConntrack, err) + } + enableLocalForwarding, err := strconv.ParseBool(os.Getenv(EnvEnableNetstackLocalForwarding)) + if err != nil { + log.Warnf("failed to parse %s: %v", EnvEnableNetstackLocalForwarding, err) + } m := &Manager{ decoders: sync.Pool{ @@ -101,52 +149,161 @@ func create(iface IFaceMapper) (*Manager, error) { return d }, }, - outgoingRules: make(map[string]RuleSet), - incomingRules: make(map[string]RuleSet), - wgIface: iface, - stateful: !disableConntrack, + nativeFirewall: nativeFirewall, + outgoingRules: make(map[string]RuleSet), + incomingRules: make(map[string]RuleSet), + wgIface: iface, + localipmanager: newLocalIPManager(), + routingEnabled: false, + stateful: !disableConntrack, + logger: nblog.NewFromLogrus(log.StandardLogger()), + netstack: netstack.IsEnabled(), + // default true for non-netstack, for netstack only if explicitly enabled + localForwarding: !netstack.IsEnabled() || enableLocalForwarding, + } + + if err := m.localipmanager.UpdateLocalIPs(iface); err != nil { + return nil, fmt.Errorf("update local IPs: %w", err) } // Only initialize trackers if stateful mode is enabled if disableConntrack { log.Info("conntrack is disabled") } else { - m.udpTracker = conntrack.NewUDPTracker(conntrack.DefaultUDPTimeout) - m.icmpTracker = conntrack.NewICMPTracker(conntrack.DefaultICMPTimeout) - m.tcpTracker = conntrack.NewTCPTracker(conntrack.DefaultTCPTimeout) + m.udpTracker = conntrack.NewUDPTracker(conntrack.DefaultUDPTimeout, m.logger) + m.icmpTracker = conntrack.NewICMPTracker(conntrack.DefaultICMPTimeout, m.logger) + m.tcpTracker = conntrack.NewTCPTracker(conntrack.DefaultTCPTimeout, m.logger) + } + + m.determineRouting(iface, disableServerRoutes) + + if err := m.blockInvalidRouted(iface); err != nil { + log.Errorf("failed to block invalid routed traffic: %v", err) } if err := iface.SetFilter(m); err != nil { - return nil, err + return nil, fmt.Errorf("set filter: %w", err) } return m, nil } +func (m *Manager) blockInvalidRouted(iface common.IFaceMapper) error { + if m.forwarder == nil { + return nil + } + wgPrefix, err := netip.ParsePrefix(iface.Address().Network.String()) + if err != nil { + return fmt.Errorf("parse wireguard network: %w", err) + } + log.Debugf("blocking invalid routed traffic for %s", wgPrefix) + + if _, err := m.AddRouteFiltering( + []netip.Prefix{netip.PrefixFrom(netip.IPv4Unspecified(), 0)}, + wgPrefix, + firewall.ProtocolALL, + nil, + nil, + firewall.ActionDrop, + ); err != nil { + return fmt.Errorf("block wg nte : %w", err) + } + + // TODO: Block networks that we're a client of + + return nil +} + +func (m *Manager) determineRouting(iface common.IFaceMapper, disableServerRoutes bool) { + disableUspRouting, _ := strconv.ParseBool(os.Getenv(EnvDisableUserspaceRouting)) + forceUserspaceRouter, _ := strconv.ParseBool(os.Getenv(EnvForceUserspaceRouter)) + + switch { + case disableUspRouting: + m.routingEnabled = false + m.nativeRouter = false + log.Info("userspace routing is disabled") + + case disableServerRoutes: + // if server routes are disabled we will let packets pass to the native stack + m.routingEnabled = true + m.nativeRouter = true + + log.Info("server routes are disabled") + + case forceUserspaceRouter: + m.routingEnabled = true + m.nativeRouter = false + + log.Info("userspace routing is forced") + + case !m.netstack && m.nativeFirewall != nil && m.nativeFirewall.IsServerRouteSupported(): + // if the OS supports routing natively, then we don't need to filter/route ourselves + // netstack mode won't support native routing as there is no interface + + m.routingEnabled = true + m.nativeRouter = true + + log.Info("native routing is enabled") + + default: + m.routingEnabled = true + m.nativeRouter = false + + log.Info("userspace routing enabled by default") + } + + // netstack needs the forwarder for local traffic + if m.netstack && m.localForwarding || + m.routingEnabled && !m.nativeRouter { + + m.initForwarder(iface) + } +} + +// initForwarder initializes the forwarder, it disables routing on errors +func (m *Manager) initForwarder(iface common.IFaceMapper) { + // Only supported in userspace mode as we need to inject packets back into wireguard directly + intf := iface.GetWGDevice() + if intf == nil { + log.Info("forwarding not supported") + m.routingEnabled = false + return + } + + forwarder, err := forwarder.New(iface, m.logger, m.netstack) + if err != nil { + log.Errorf("failed to create forwarder: %v", err) + m.routingEnabled = false + return + } + + m.forwarder = forwarder +} + func (m *Manager) Init(*statemanager.Manager) error { return nil } func (m *Manager) IsServerRouteSupported() bool { - if m.nativeFirewall == nil { - return false - } else { - return true - } + return m.nativeFirewall != nil || m.routingEnabled && m.forwarder != nil } func (m *Manager) AddNatRule(pair firewall.RouterPair) error { - if m.nativeFirewall == nil { - return errRouteNotSupported + if m.nativeRouter && m.nativeFirewall != nil { + return m.nativeFirewall.AddNatRule(pair) } - return m.nativeFirewall.AddNatRule(pair) + + // userspace routed packets are always SNATed to the inbound direction + // TODO: implement outbound SNAT + return nil } // RemoveNatRule removes a routing firewall rule func (m *Manager) RemoveNatRule(pair firewall.RouterPair) error { - if m.nativeFirewall == nil { - return errRouteNotSupported + if m.nativeRouter && m.nativeFirewall != nil { + return m.nativeFirewall.RemoveNatRule(pair) } - return m.nativeFirewall.RemoveNatRule(pair) + return nil } // AddPeerFiltering rule to the firewall @@ -162,7 +319,7 @@ func (m *Manager) AddPeerFiltering( _ string, comment string, ) ([]firewall.Rule, error) { - r := Rule{ + r := PeerRule{ id: uuid.New().String(), ip: ip, ipLayer: layers.LayerTypeIPv6, @@ -205,18 +362,56 @@ func (m *Manager) AddPeerFiltering( return []firewall.Rule{&r}, nil } -func (m *Manager) AddRouteFiltering(sources []netip.Prefix, destination netip.Prefix, proto firewall.Protocol, sPort *firewall.Port, dPort *firewall.Port, action firewall.Action) (firewall.Rule, error) { - if m.nativeFirewall == nil { - return nil, errRouteNotSupported +func (m *Manager) AddRouteFiltering( + sources []netip.Prefix, + destination netip.Prefix, + proto firewall.Protocol, + sPort *firewall.Port, + dPort *firewall.Port, + action firewall.Action, +) (firewall.Rule, error) { + if m.nativeRouter && m.nativeFirewall != nil { + return m.nativeFirewall.AddRouteFiltering(sources, destination, proto, sPort, dPort, action) } - return m.nativeFirewall.AddRouteFiltering(sources, destination, proto, sPort, dPort, action) + + m.mutex.Lock() + defer m.mutex.Unlock() + + ruleID := uuid.New().String() + rule := RouteRule{ + id: ruleID, + sources: sources, + destination: destination, + proto: proto, + srcPort: sPort, + dstPort: dPort, + action: action, + } + + m.routeRules = append(m.routeRules, rule) + m.routeRules.Sort() + + return &rule, nil } func (m *Manager) DeleteRouteRule(rule firewall.Rule) error { - if m.nativeFirewall == nil { - return errRouteNotSupported + if m.nativeRouter && m.nativeFirewall != nil { + return m.nativeFirewall.DeleteRouteRule(rule) } - return m.nativeFirewall.DeleteRouteRule(rule) + + m.mutex.Lock() + defer m.mutex.Unlock() + + ruleID := rule.GetRuleID() + idx := slices.IndexFunc(m.routeRules, func(r RouteRule) bool { + return r.id == ruleID + }) + if idx < 0 { + return fmt.Errorf("route rule not found: %s", ruleID) + } + + m.routeRules = slices.Delete(m.routeRules, idx, idx+1) + return nil } // DeletePeerRule from the firewall by rule definition @@ -224,7 +419,7 @@ func (m *Manager) DeletePeerRule(rule firewall.Rule) error { m.mutex.Lock() defer m.mutex.Unlock() - r, ok := rule.(*Rule) + r, ok := rule.(*PeerRule) if !ok { return fmt.Errorf("delete rule: invalid rule type: %T", rule) } @@ -255,10 +450,14 @@ func (m *Manager) DropOutgoing(packetData []byte) bool { // DropIncoming filter incoming packets func (m *Manager) DropIncoming(packetData []byte) bool { - return m.dropFilter(packetData, m.incomingRules) + return m.dropFilter(packetData) +} + +// UpdateLocalIPs updates the list of local IPs +func (m *Manager) UpdateLocalIPs() error { + return m.localipmanager.UpdateLocalIPs(m.wgIface) } -// processOutgoingHooks processes UDP hooks for outgoing packets and tracks TCP/UDP/ICMP func (m *Manager) processOutgoingHooks(packetData []byte) bool { m.mutex.RLock() defer m.mutex.RUnlock() @@ -279,18 +478,11 @@ func (m *Manager) processOutgoingHooks(packetData []byte) bool { return false } - // Always process UDP hooks - if d.decoded[1] == layers.LayerTypeUDP { - // Track UDP state only if enabled - if m.stateful { - m.trackUDPOutbound(d, srcIP, dstIP) - } - return m.checkUDPHooks(d, dstIP, packetData) - } - - // Track other protocols only if stateful mode is enabled + // Track all protocols if stateful mode is enabled if m.stateful { switch d.decoded[1] { + case layers.LayerTypeUDP: + m.trackUDPOutbound(d, srcIP, dstIP) case layers.LayerTypeTCP: m.trackTCPOutbound(d, srcIP, dstIP) case layers.LayerTypeICMPv4: @@ -298,6 +490,11 @@ func (m *Manager) processOutgoingHooks(packetData []byte) bool { } } + // Process UDP hooks even if stateful mode is disabled + if d.decoded[1] == layers.LayerTypeUDP { + return m.checkUDPHooks(d, dstIP, packetData) + } + return false } @@ -379,10 +576,9 @@ func (m *Manager) trackICMPOutbound(d *decoder, srcIP, dstIP net.IP) { } } -// dropFilter implements filtering logic for incoming packets -func (m *Manager) dropFilter(packetData []byte, rules map[string]RuleSet) bool { - // TODO: Disable router if --disable-server-router is set - +// dropFilter implements filtering logic for incoming packets. +// If it returns true, the packet should be dropped. +func (m *Manager) dropFilter(packetData []byte) bool { m.mutex.RLock() defer m.mutex.RUnlock() @@ -390,25 +586,120 @@ func (m *Manager) dropFilter(packetData []byte, rules map[string]RuleSet) bool { defer m.decoders.Put(d) if !m.isValidPacket(d, packetData) { + m.logger.Trace("Invalid packet structure") return true } srcIP, dstIP := m.extractIPs(d) if srcIP == nil { - log.Errorf("unknown layer: %v", d.decoded[0]) + m.logger.Error("Unknown network layer: %v", d.decoded[0]) return true } - if !m.isWireguardTraffic(srcIP, dstIP) { - return false - } - - // Check connection state only if enabled + // For all inbound traffic, first check if it matches a tracked connection. + // This must happen before any other filtering because the packets are statefully tracked. if m.stateful && m.isValidTrackedConnection(d, srcIP, dstIP) { return false } - return m.applyRules(srcIP, packetData, rules, d) + if m.localipmanager.IsLocalIP(dstIP) { + return m.handleLocalTraffic(d, srcIP, dstIP, packetData) + } + + return m.handleRoutedTraffic(d, srcIP, dstIP, packetData) +} + +// handleLocalTraffic handles local traffic. +// If it returns true, the packet should be dropped. +func (m *Manager) handleLocalTraffic(d *decoder, srcIP, dstIP net.IP, packetData []byte) bool { + if !m.localForwarding { + m.logger.Trace("Dropping local packet (local forwarding disabled): src=%s dst=%s", srcIP, dstIP) + return true + } + + if m.peerACLsBlock(srcIP, packetData, m.incomingRules, d) { + m.logger.Trace("Dropping local packet (ACL denied): src=%s dst=%s", + srcIP, dstIP) + return true + } + + // if running in netstack mode we need to pass this to the forwarder + if m.netstack { + m.handleNetstackLocalTraffic(packetData) + + // don't process this packet further + return true + } + + return false +} +func (m *Manager) handleNetstackLocalTraffic(packetData []byte) { + if m.forwarder == nil { + return + } + + if err := m.forwarder.InjectIncomingPacket(packetData); err != nil { + m.logger.Error("Failed to inject local packet: %v", err) + } +} + +// handleRoutedTraffic handles routed traffic. +// If it returns true, the packet should be dropped. +func (m *Manager) handleRoutedTraffic(d *decoder, srcIP, dstIP net.IP, packetData []byte) bool { + // Drop if routing is disabled + if !m.routingEnabled { + m.logger.Trace("Dropping routed packet (routing disabled): src=%s dst=%s", + srcIP, dstIP) + return true + } + + // Pass to native stack if native router is enabled or forced + if m.nativeRouter { + return false + } + + // Get protocol and ports for route ACL check + proto := getProtocolFromPacket(d) + srcPort, dstPort := getPortsFromPacket(d) + + // Check route ACLs + if !m.routeACLsPass(srcIP, dstIP, proto, srcPort, dstPort) { + m.logger.Trace("Dropping routed packet (ACL denied): src=%s:%d dst=%s:%d proto=%v", + srcIP, srcPort, dstIP, dstPort, proto) + return true + } + + // Let forwarder handle the packet if it passed route ACLs + if err := m.forwarder.InjectIncomingPacket(packetData); err != nil { + m.logger.Error("Failed to inject incoming packet: %v", err) + } + + // Forwarded packets shouldn't reach the native stack, hence they won't be visible in a packet capture + return true +} + +func getProtocolFromPacket(d *decoder) firewall.Protocol { + switch d.decoded[1] { + case layers.LayerTypeTCP: + return firewall.ProtocolTCP + case layers.LayerTypeUDP: + return firewall.ProtocolUDP + case layers.LayerTypeICMPv4, layers.LayerTypeICMPv6: + return firewall.ProtocolICMP + default: + return firewall.ProtocolALL + } +} + +func getPortsFromPacket(d *decoder) (srcPort, dstPort uint16) { + switch d.decoded[1] { + case layers.LayerTypeTCP: + return uint16(d.tcp.SrcPort), uint16(d.tcp.DstPort) + case layers.LayerTypeUDP: + return uint16(d.udp.SrcPort), uint16(d.udp.DstPort) + default: + return 0, 0 + } } func (m *Manager) isValidPacket(d *decoder, packetData []byte) bool { @@ -424,10 +715,6 @@ func (m *Manager) isValidPacket(d *decoder, packetData []byte) bool { return true } -func (m *Manager) isWireguardTraffic(srcIP, dstIP net.IP) bool { - return m.wgNetwork.Contains(srcIP) && m.wgNetwork.Contains(dstIP) -} - func (m *Manager) isValidTrackedConnection(d *decoder, srcIP, dstIP net.IP) bool { switch d.decoded[1] { case layers.LayerTypeTCP: @@ -462,7 +749,22 @@ func (m *Manager) isValidTrackedConnection(d *decoder, srcIP, dstIP net.IP) bool return false } -func (m *Manager) applyRules(srcIP net.IP, packetData []byte, rules map[string]RuleSet, d *decoder) bool { +// isSpecialICMP returns true if the packet is a special ICMP packet that should be allowed +func (m *Manager) isSpecialICMP(d *decoder) bool { + if d.decoded[1] != layers.LayerTypeICMPv4 { + return false + } + + icmpType := d.icmp4.TypeCode.Type() + return icmpType == layers.ICMPv4TypeDestinationUnreachable || + icmpType == layers.ICMPv4TypeTimeExceeded +} + +func (m *Manager) peerACLsBlock(srcIP net.IP, packetData []byte, rules map[string]RuleSet, d *decoder) bool { + if m.isSpecialICMP(d) { + return false + } + if filter, ok := validateRule(srcIP, packetData, rules[srcIP.String()], d); ok { return filter } @@ -496,7 +798,7 @@ func portsMatch(rulePort *firewall.Port, packetPort uint16) bool { return false } -func validateRule(ip net.IP, packetData []byte, rules map[string]Rule, d *decoder) (bool, bool) { +func validateRule(ip net.IP, packetData []byte, rules map[string]PeerRule, d *decoder) (bool, bool) { payloadLayer := d.decoded[1] for _, rule := range rules { if rule.matchByIP && !ip.Equal(rule.ip) { @@ -533,6 +835,51 @@ func validateRule(ip net.IP, packetData []byte, rules map[string]Rule, d *decode return false, false } +// routeACLsPass returns treu if the packet is allowed by the route ACLs +func (m *Manager) routeACLsPass(srcIP, dstIP net.IP, proto firewall.Protocol, srcPort, dstPort uint16) bool { + m.mutex.RLock() + defer m.mutex.RUnlock() + + srcAddr := netip.AddrFrom4([4]byte(srcIP.To4())) + dstAddr := netip.AddrFrom4([4]byte(dstIP.To4())) + + for _, rule := range m.routeRules { + if m.ruleMatches(rule, srcAddr, dstAddr, proto, srcPort, dstPort) { + return rule.action == firewall.ActionAccept + } + } + return false +} + +func (m *Manager) ruleMatches(rule RouteRule, srcAddr, dstAddr netip.Addr, proto firewall.Protocol, srcPort, dstPort uint16) bool { + if !rule.destination.Contains(dstAddr) { + return false + } + + sourceMatched := false + for _, src := range rule.sources { + if src.Contains(srcAddr) { + sourceMatched = true + break + } + } + if !sourceMatched { + return false + } + + if rule.proto != firewall.ProtocolALL && rule.proto != proto { + return false + } + + if proto == firewall.ProtocolTCP || proto == firewall.ProtocolUDP { + if !portsMatch(rule.srcPort, srcPort) || !portsMatch(rule.dstPort, dstPort) { + return false + } + } + + return true +} + // SetNetwork of the wireguard interface to which filtering applied func (m *Manager) SetNetwork(network *net.IPNet) { m.wgNetwork = network @@ -544,7 +891,7 @@ func (m *Manager) SetNetwork(network *net.IPNet) { func (m *Manager) AddUDPPacketHook( in bool, ip net.IP, dPort uint16, hook func([]byte) bool, ) string { - r := Rule{ + r := PeerRule{ id: uuid.New().String(), ip: ip, protoLayer: layers.LayerTypeUDP, @@ -561,12 +908,12 @@ func (m *Manager) AddUDPPacketHook( m.mutex.Lock() if in { if _, ok := m.incomingRules[r.ip.String()]; !ok { - m.incomingRules[r.ip.String()] = make(map[string]Rule) + m.incomingRules[r.ip.String()] = make(map[string]PeerRule) } m.incomingRules[r.ip.String()][r.id] = r } else { if _, ok := m.outgoingRules[r.ip.String()]; !ok { - m.outgoingRules[r.ip.String()] = make(map[string]Rule) + m.outgoingRules[r.ip.String()] = make(map[string]PeerRule) } m.outgoingRules[r.ip.String()][r.id] = r } @@ -599,3 +946,10 @@ func (m *Manager) RemovePacketHook(hookID string) error { } return fmt.Errorf("hook with given id not found") } + +// SetLogLevel sets the log level for the firewall manager +func (m *Manager) SetLogLevel(level log.Level) { + if m.logger != nil { + m.logger.SetLevel(nblog.Level(level)) + } +} diff --git a/client/firewall/uspfilter/uspfilter_bench_test.go b/client/firewall/uspfilter/uspfilter_bench_test.go index 46bc4439d..875bb2425 100644 --- a/client/firewall/uspfilter/uspfilter_bench_test.go +++ b/client/firewall/uspfilter/uspfilter_bench_test.go @@ -1,9 +1,12 @@ +//go:build uspbench + package uspfilter import ( "fmt" "math/rand" "net" + "net/netip" "os" "strings" "testing" @@ -155,7 +158,7 @@ func BenchmarkCoreFiltering(b *testing.B) { // Create manager and basic setup manager, _ := Create(&IFaceMock{ SetFilterFunc: func(device.PacketFilter) error { return nil }, - }) + }, false) defer b.Cleanup(func() { require.NoError(b, manager.Reset(nil)) }) @@ -185,7 +188,7 @@ func BenchmarkCoreFiltering(b *testing.B) { // Measure inbound packet processing b.ResetTimer() for i := 0; i < b.N; i++ { - manager.dropFilter(inbound, manager.incomingRules) + manager.dropFilter(inbound) } }) } @@ -200,7 +203,7 @@ func BenchmarkStateScaling(b *testing.B) { b.Run(fmt.Sprintf("conns_%d", count), func(b *testing.B) { manager, _ := Create(&IFaceMock{ SetFilterFunc: func(device.PacketFilter) error { return nil }, - }) + }, false) b.Cleanup(func() { require.NoError(b, manager.Reset(nil)) }) @@ -228,7 +231,7 @@ func BenchmarkStateScaling(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - manager.dropFilter(testIn, manager.incomingRules) + manager.dropFilter(testIn) } }) } @@ -248,7 +251,7 @@ func BenchmarkEstablishmentOverhead(b *testing.B) { b.Run(sc.name, func(b *testing.B) { manager, _ := Create(&IFaceMock{ SetFilterFunc: func(device.PacketFilter) error { return nil }, - }) + }, false) b.Cleanup(func() { require.NoError(b, manager.Reset(nil)) }) @@ -269,7 +272,7 @@ func BenchmarkEstablishmentOverhead(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - manager.dropFilter(inbound, manager.incomingRules) + manager.dropFilter(inbound) } }) } @@ -447,7 +450,7 @@ func BenchmarkRoutedNetworkReturn(b *testing.B) { b.Run(sc.name, func(b *testing.B) { manager, _ := Create(&IFaceMock{ SetFilterFunc: func(device.PacketFilter) error { return nil }, - }) + }, false) b.Cleanup(func() { require.NoError(b, manager.Reset(nil)) }) @@ -472,7 +475,7 @@ func BenchmarkRoutedNetworkReturn(b *testing.B) { manager.processOutgoingHooks(syn) // SYN-ACK synack := generateTCPPacketWithFlags(b, dstIP, srcIP, 80, 1024, uint16(conntrack.TCPSyn|conntrack.TCPAck)) - manager.dropFilter(synack, manager.incomingRules) + manager.dropFilter(synack) // ACK ack := generateTCPPacketWithFlags(b, srcIP, dstIP, 1024, 80, uint16(conntrack.TCPAck)) manager.processOutgoingHooks(ack) @@ -481,7 +484,7 @@ func BenchmarkRoutedNetworkReturn(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - manager.dropFilter(inbound, manager.incomingRules) + manager.dropFilter(inbound) } }) } @@ -574,7 +577,7 @@ func BenchmarkLongLivedConnections(b *testing.B) { manager, _ := Create(&IFaceMock{ SetFilterFunc: func(device.PacketFilter) error { return nil }, - }) + }, false) defer b.Cleanup(func() { require.NoError(b, manager.Reset(nil)) }) @@ -618,7 +621,7 @@ func BenchmarkLongLivedConnections(b *testing.B) { // SYN-ACK synack := generateTCPPacketWithFlags(b, dstIPs[i], srcIPs[i], 80, uint16(1024+i), uint16(conntrack.TCPSyn|conntrack.TCPAck)) - manager.dropFilter(synack, manager.incomingRules) + manager.dropFilter(synack) // ACK ack := generateTCPPacketWithFlags(b, srcIPs[i], dstIPs[i], @@ -646,7 +649,7 @@ func BenchmarkLongLivedConnections(b *testing.B) { // First outbound data manager.processOutgoingHooks(outPackets[connIdx]) // Then inbound response - this is what we're actually measuring - manager.dropFilter(inPackets[connIdx], manager.incomingRules) + manager.dropFilter(inPackets[connIdx]) } }) } @@ -665,7 +668,7 @@ func BenchmarkShortLivedConnections(b *testing.B) { manager, _ := Create(&IFaceMock{ SetFilterFunc: func(device.PacketFilter) error { return nil }, - }) + }, false) defer b.Cleanup(func() { require.NoError(b, manager.Reset(nil)) }) @@ -754,17 +757,17 @@ func BenchmarkShortLivedConnections(b *testing.B) { // Connection establishment manager.processOutgoingHooks(p.syn) - manager.dropFilter(p.synAck, manager.incomingRules) + manager.dropFilter(p.synAck) manager.processOutgoingHooks(p.ack) // Data transfer manager.processOutgoingHooks(p.request) - manager.dropFilter(p.response, manager.incomingRules) + manager.dropFilter(p.response) // Connection teardown manager.processOutgoingHooks(p.finClient) - manager.dropFilter(p.ackServer, manager.incomingRules) - manager.dropFilter(p.finServer, manager.incomingRules) + manager.dropFilter(p.ackServer) + manager.dropFilter(p.finServer) manager.processOutgoingHooks(p.ackClient) } }) @@ -784,7 +787,7 @@ func BenchmarkParallelLongLivedConnections(b *testing.B) { manager, _ := Create(&IFaceMock{ SetFilterFunc: func(device.PacketFilter) error { return nil }, - }) + }, false) defer b.Cleanup(func() { require.NoError(b, manager.Reset(nil)) }) @@ -825,7 +828,7 @@ func BenchmarkParallelLongLivedConnections(b *testing.B) { synack := generateTCPPacketWithFlags(b, dstIPs[i], srcIPs[i], 80, uint16(1024+i), uint16(conntrack.TCPSyn|conntrack.TCPAck)) - manager.dropFilter(synack, manager.incomingRules) + manager.dropFilter(synack) ack := generateTCPPacketWithFlags(b, srcIPs[i], dstIPs[i], uint16(1024+i), 80, uint16(conntrack.TCPAck)) @@ -852,7 +855,7 @@ func BenchmarkParallelLongLivedConnections(b *testing.B) { // Simulate bidirectional traffic manager.processOutgoingHooks(outPackets[connIdx]) - manager.dropFilter(inPackets[connIdx], manager.incomingRules) + manager.dropFilter(inPackets[connIdx]) } }) }) @@ -872,7 +875,7 @@ func BenchmarkParallelShortLivedConnections(b *testing.B) { manager, _ := Create(&IFaceMock{ SetFilterFunc: func(device.PacketFilter) error { return nil }, - }) + }, false) defer b.Cleanup(func() { require.NoError(b, manager.Reset(nil)) }) @@ -949,15 +952,15 @@ func BenchmarkParallelShortLivedConnections(b *testing.B) { // Full connection lifecycle manager.processOutgoingHooks(p.syn) - manager.dropFilter(p.synAck, manager.incomingRules) + manager.dropFilter(p.synAck) manager.processOutgoingHooks(p.ack) manager.processOutgoingHooks(p.request) - manager.dropFilter(p.response, manager.incomingRules) + manager.dropFilter(p.response) manager.processOutgoingHooks(p.finClient) - manager.dropFilter(p.ackServer, manager.incomingRules) - manager.dropFilter(p.finServer, manager.incomingRules) + manager.dropFilter(p.ackServer) + manager.dropFilter(p.finServer) manager.processOutgoingHooks(p.ackClient) } }) @@ -996,3 +999,72 @@ func generateTCPPacketWithFlags(b *testing.B, srcIP, dstIP net.IP, srcPort, dstP require.NoError(b, gopacket.SerializeLayers(buf, opts, ipv4, tcp, gopacket.Payload("test"))) return buf.Bytes() } + +func BenchmarkRouteACLs(b *testing.B) { + manager := setupRoutedManager(b, "10.10.0.100/16") + + // Add several route rules to simulate real-world scenario + rules := []struct { + sources []netip.Prefix + dest netip.Prefix + proto fw.Protocol + port *fw.Port + }{ + { + sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")}, + dest: netip.MustParsePrefix("192.168.1.0/24"), + proto: fw.ProtocolTCP, + port: &fw.Port{Values: []uint16{80, 443}}, + }, + { + sources: []netip.Prefix{ + netip.MustParsePrefix("172.16.0.0/12"), + netip.MustParsePrefix("10.0.0.0/8"), + }, + dest: netip.MustParsePrefix("0.0.0.0/0"), + proto: fw.ProtocolICMP, + }, + { + sources: []netip.Prefix{netip.MustParsePrefix("0.0.0.0/0")}, + dest: netip.MustParsePrefix("192.168.0.0/16"), + proto: fw.ProtocolUDP, + port: &fw.Port{Values: []uint16{53}}, + }, + } + + for _, r := range rules { + _, err := manager.AddRouteFiltering( + r.sources, + r.dest, + r.proto, + nil, + r.port, + fw.ActionAccept, + ) + if err != nil { + b.Fatal(err) + } + } + + // Test cases that exercise different matching scenarios + cases := []struct { + srcIP string + dstIP string + proto fw.Protocol + dstPort uint16 + }{ + {"100.10.0.1", "192.168.1.100", fw.ProtocolTCP, 443}, // Match first rule + {"172.16.0.1", "8.8.8.8", fw.ProtocolICMP, 0}, // Match second rule + {"1.1.1.1", "192.168.1.53", fw.ProtocolUDP, 53}, // Match third rule + {"192.168.1.1", "10.0.0.1", fw.ProtocolTCP, 8080}, // No match + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + for _, tc := range cases { + srcIP := net.ParseIP(tc.srcIP) + dstIP := net.ParseIP(tc.dstIP) + manager.routeACLsPass(srcIP, dstIP, tc.proto, 0, tc.dstPort) + } + } +} diff --git a/client/firewall/uspfilter/uspfilter_filter_test.go b/client/firewall/uspfilter/uspfilter_filter_test.go new file mode 100644 index 000000000..d7aebb1aa --- /dev/null +++ b/client/firewall/uspfilter/uspfilter_filter_test.go @@ -0,0 +1,1014 @@ +package uspfilter + +import ( + "net" + "net/netip" + "testing" + + "github.com/golang/mock/gomock" + "github.com/google/gopacket" + "github.com/google/gopacket/layers" + "github.com/stretchr/testify/require" + wgdevice "golang.zx2c4.com/wireguard/device" + + fw "github.com/netbirdio/netbird/client/firewall/manager" + "github.com/netbirdio/netbird/client/iface" + "github.com/netbirdio/netbird/client/iface/device" + "github.com/netbirdio/netbird/client/iface/mocks" +) + +func TestPeerACLFiltering(t *testing.T) { + localIP := net.ParseIP("100.10.0.100") + wgNet := &net.IPNet{ + IP: net.ParseIP("100.10.0.0"), + Mask: net.CIDRMask(16, 32), + } + + ifaceMock := &IFaceMock{ + SetFilterFunc: func(device.PacketFilter) error { return nil }, + AddressFunc: func() iface.WGAddress { + return iface.WGAddress{ + IP: localIP, + Network: wgNet, + } + }, + } + + manager, err := Create(ifaceMock, false) + require.NoError(t, err) + require.NotNil(t, manager) + + t.Cleanup(func() { + require.NoError(t, manager.Reset(nil)) + }) + + manager.wgNetwork = wgNet + + err = manager.UpdateLocalIPs() + require.NoError(t, err) + + testCases := []struct { + name string + srcIP string + dstIP string + proto fw.Protocol + srcPort uint16 + dstPort uint16 + ruleIP string + ruleProto fw.Protocol + ruleSrcPort *fw.Port + ruleDstPort *fw.Port + ruleAction fw.Action + shouldBeBlocked bool + }{ + { + name: "Allow TCP traffic from WG peer", + srcIP: "100.10.0.1", + dstIP: "100.10.0.100", + proto: fw.ProtocolTCP, + srcPort: 12345, + dstPort: 443, + ruleIP: "100.10.0.1", + ruleProto: fw.ProtocolTCP, + ruleDstPort: &fw.Port{Values: []uint16{443}}, + ruleAction: fw.ActionAccept, + shouldBeBlocked: false, + }, + { + name: "Allow UDP traffic from WG peer", + srcIP: "100.10.0.1", + dstIP: "100.10.0.100", + proto: fw.ProtocolUDP, + srcPort: 12345, + dstPort: 53, + ruleIP: "100.10.0.1", + ruleProto: fw.ProtocolUDP, + ruleDstPort: &fw.Port{Values: []uint16{53}}, + ruleAction: fw.ActionAccept, + shouldBeBlocked: false, + }, + { + name: "Allow ICMP traffic from WG peer", + srcIP: "100.10.0.1", + dstIP: "100.10.0.100", + proto: fw.ProtocolICMP, + ruleIP: "100.10.0.1", + ruleProto: fw.ProtocolICMP, + ruleAction: fw.ActionAccept, + shouldBeBlocked: false, + }, + { + name: "Allow all traffic from WG peer", + srcIP: "100.10.0.1", + dstIP: "100.10.0.100", + proto: fw.ProtocolTCP, + srcPort: 12345, + dstPort: 443, + ruleIP: "100.10.0.1", + ruleProto: fw.ProtocolALL, + ruleAction: fw.ActionAccept, + shouldBeBlocked: false, + }, + { + name: "Allow traffic from non-WG source", + srcIP: "192.168.1.1", + dstIP: "100.10.0.100", + proto: fw.ProtocolTCP, + srcPort: 12345, + dstPort: 443, + ruleIP: "192.168.1.1", + ruleProto: fw.ProtocolTCP, + ruleDstPort: &fw.Port{Values: []uint16{443}}, + ruleAction: fw.ActionAccept, + shouldBeBlocked: false, + }, + { + name: "Allow all traffic with 0.0.0.0 rule", + srcIP: "100.10.0.1", + dstIP: "100.10.0.100", + proto: fw.ProtocolTCP, + srcPort: 12345, + dstPort: 443, + ruleIP: "0.0.0.0", + ruleProto: fw.ProtocolALL, + ruleAction: fw.ActionAccept, + shouldBeBlocked: false, + }, + { + name: "Allow TCP traffic within port range", + srcIP: "100.10.0.1", + dstIP: "100.10.0.100", + proto: fw.ProtocolTCP, + srcPort: 12345, + dstPort: 8080, + ruleIP: "100.10.0.1", + ruleProto: fw.ProtocolTCP, + ruleDstPort: &fw.Port{IsRange: true, Values: []uint16{8000, 8100}}, + ruleAction: fw.ActionAccept, + shouldBeBlocked: false, + }, + { + name: "Block TCP traffic outside port range", + srcIP: "100.10.0.1", + dstIP: "100.10.0.100", + proto: fw.ProtocolTCP, + srcPort: 12345, + dstPort: 7999, + ruleIP: "100.10.0.1", + ruleProto: fw.ProtocolTCP, + ruleDstPort: &fw.Port{IsRange: true, Values: []uint16{8000, 8100}}, + ruleAction: fw.ActionAccept, + shouldBeBlocked: true, + }, + { + name: "Allow TCP traffic with source port range", + srcIP: "100.10.0.1", + dstIP: "100.10.0.100", + proto: fw.ProtocolTCP, + srcPort: 32100, + dstPort: 443, + ruleIP: "100.10.0.1", + ruleProto: fw.ProtocolTCP, + ruleSrcPort: &fw.Port{IsRange: true, Values: []uint16{32000, 33000}}, + ruleDstPort: &fw.Port{Values: []uint16{443}}, + ruleAction: fw.ActionAccept, + shouldBeBlocked: false, + }, + { + name: "Block TCP traffic outside source port range", + srcIP: "100.10.0.1", + dstIP: "100.10.0.100", + proto: fw.ProtocolTCP, + srcPort: 31999, + dstPort: 443, + ruleIP: "100.10.0.1", + ruleProto: fw.ProtocolTCP, + ruleSrcPort: &fw.Port{IsRange: true, Values: []uint16{32000, 33000}}, + ruleDstPort: &fw.Port{Values: []uint16{443}}, + ruleAction: fw.ActionAccept, + shouldBeBlocked: true, + }, + } + + t.Run("Implicit DROP (no rules)", func(t *testing.T) { + packet := createTestPacket(t, "100.10.0.1", "100.10.0.100", fw.ProtocolTCP, 12345, 443) + isDropped := manager.DropIncoming(packet) + require.True(t, isDropped, "Packet should be dropped when no rules exist") + }) + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + rules, err := manager.AddPeerFiltering( + net.ParseIP(tc.ruleIP), + tc.ruleProto, + tc.ruleSrcPort, + tc.ruleDstPort, + tc.ruleAction, + "", + tc.name, + ) + require.NoError(t, err) + require.NotEmpty(t, rules) + + t.Cleanup(func() { + for _, rule := range rules { + require.NoError(t, manager.DeletePeerRule(rule)) + } + }) + + packet := createTestPacket(t, tc.srcIP, tc.dstIP, tc.proto, tc.srcPort, tc.dstPort) + isDropped := manager.DropIncoming(packet) + require.Equal(t, tc.shouldBeBlocked, isDropped) + }) + } +} + +func createTestPacket(t *testing.T, srcIP, dstIP string, proto fw.Protocol, srcPort, dstPort uint16) []byte { + t.Helper() + + buf := gopacket.NewSerializeBuffer() + opts := gopacket.SerializeOptions{ + ComputeChecksums: true, + FixLengths: true, + } + + ipLayer := &layers.IPv4{ + Version: 4, + TTL: 64, + SrcIP: net.ParseIP(srcIP), + DstIP: net.ParseIP(dstIP), + } + + var err error + switch proto { + case fw.ProtocolTCP: + ipLayer.Protocol = layers.IPProtocolTCP + tcp := &layers.TCP{ + SrcPort: layers.TCPPort(srcPort), + DstPort: layers.TCPPort(dstPort), + } + err = tcp.SetNetworkLayerForChecksum(ipLayer) + require.NoError(t, err) + err = gopacket.SerializeLayers(buf, opts, ipLayer, tcp) + + case fw.ProtocolUDP: + ipLayer.Protocol = layers.IPProtocolUDP + udp := &layers.UDP{ + SrcPort: layers.UDPPort(srcPort), + DstPort: layers.UDPPort(dstPort), + } + err = udp.SetNetworkLayerForChecksum(ipLayer) + require.NoError(t, err) + err = gopacket.SerializeLayers(buf, opts, ipLayer, udp) + + case fw.ProtocolICMP: + ipLayer.Protocol = layers.IPProtocolICMPv4 + icmp := &layers.ICMPv4{ + TypeCode: layers.CreateICMPv4TypeCode(layers.ICMPv4TypeEchoRequest, 0), + } + err = gopacket.SerializeLayers(buf, opts, ipLayer, icmp) + + default: + err = gopacket.SerializeLayers(buf, opts, ipLayer) + } + + require.NoError(t, err) + return buf.Bytes() +} + +func setupRoutedManager(tb testing.TB, network string) *Manager { + tb.Helper() + + ctrl := gomock.NewController(tb) + dev := mocks.NewMockDevice(ctrl) + dev.EXPECT().MTU().Return(1500, nil).AnyTimes() + + localIP, wgNet, err := net.ParseCIDR(network) + require.NoError(tb, err) + + ifaceMock := &IFaceMock{ + SetFilterFunc: func(device.PacketFilter) error { return nil }, + AddressFunc: func() iface.WGAddress { + return iface.WGAddress{ + IP: localIP, + Network: wgNet, + } + }, + GetDeviceFunc: func() *device.FilteredDevice { + return &device.FilteredDevice{Device: dev} + }, + GetWGDeviceFunc: func() *wgdevice.Device { + return &wgdevice.Device{} + }, + } + + manager, err := Create(ifaceMock, false) + require.NoError(tb, err) + require.NotNil(tb, manager) + require.True(tb, manager.routingEnabled) + require.False(tb, manager.nativeRouter) + + tb.Cleanup(func() { + require.NoError(tb, manager.Reset(nil)) + }) + + return manager +} + +func TestRouteACLFiltering(t *testing.T) { + manager := setupRoutedManager(t, "10.10.0.100/16") + + type rule struct { + sources []netip.Prefix + dest netip.Prefix + proto fw.Protocol + srcPort *fw.Port + dstPort *fw.Port + action fw.Action + } + + testCases := []struct { + name string + srcIP string + dstIP string + proto fw.Protocol + srcPort uint16 + dstPort uint16 + rule rule + shouldPass bool + }{ + { + name: "Allow TCP with specific source and destination", + srcIP: "100.10.0.1", + dstIP: "192.168.1.100", + proto: fw.ProtocolTCP, + srcPort: 12345, + dstPort: 443, + rule: rule{ + sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")}, + dest: netip.MustParsePrefix("192.168.1.0/24"), + proto: fw.ProtocolTCP, + dstPort: &fw.Port{Values: []uint16{443}}, + action: fw.ActionAccept, + }, + shouldPass: true, + }, + { + name: "Allow any source to specific destination", + srcIP: "172.16.0.1", + dstIP: "192.168.1.100", + proto: fw.ProtocolTCP, + srcPort: 12345, + dstPort: 443, + rule: rule{ + sources: []netip.Prefix{netip.MustParsePrefix("0.0.0.0/0")}, + dest: netip.MustParsePrefix("192.168.1.0/24"), + proto: fw.ProtocolTCP, + dstPort: &fw.Port{Values: []uint16{443}}, + action: fw.ActionAccept, + }, + shouldPass: true, + }, + { + name: "Allow any source to any destination", + srcIP: "172.16.0.1", + dstIP: "203.0.113.100", + proto: fw.ProtocolTCP, + srcPort: 12345, + dstPort: 443, + rule: rule{ + sources: []netip.Prefix{netip.MustParsePrefix("0.0.0.0/0")}, + dest: netip.MustParsePrefix("0.0.0.0/0"), + proto: fw.ProtocolTCP, + dstPort: &fw.Port{Values: []uint16{443}}, + action: fw.ActionAccept, + }, + shouldPass: true, + }, + { + name: "Allow UDP DNS traffic", + srcIP: "100.10.0.1", + dstIP: "192.168.1.53", + proto: fw.ProtocolUDP, + srcPort: 54321, + dstPort: 53, + rule: rule{ + sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")}, + dest: netip.MustParsePrefix("192.168.1.0/24"), + proto: fw.ProtocolUDP, + dstPort: &fw.Port{Values: []uint16{53}}, + action: fw.ActionAccept, + }, + shouldPass: true, + }, + { + name: "Allow ICMP to any destination", + srcIP: "100.10.0.1", + dstIP: "8.8.8.8", + proto: fw.ProtocolICMP, + rule: rule{ + sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")}, + dest: netip.MustParsePrefix("0.0.0.0/0"), + proto: fw.ProtocolICMP, + action: fw.ActionAccept, + }, + shouldPass: true, + }, + { + name: "Allow all protocols but specific port", + srcIP: "100.10.0.1", + dstIP: "192.168.1.100", + proto: fw.ProtocolTCP, + srcPort: 12345, + dstPort: 80, + rule: rule{ + sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")}, + dest: netip.MustParsePrefix("192.168.1.0/24"), + proto: fw.ProtocolALL, + dstPort: &fw.Port{Values: []uint16{80}}, + action: fw.ActionAccept, + }, + shouldPass: true, + }, + { + name: "Implicit deny - wrong destination port", + srcIP: "100.10.0.1", + dstIP: "192.168.1.100", + proto: fw.ProtocolTCP, + srcPort: 12345, + dstPort: 8080, + rule: rule{ + sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")}, + dest: netip.MustParsePrefix("192.168.1.0/24"), + proto: fw.ProtocolTCP, + dstPort: &fw.Port{Values: []uint16{80}}, + action: fw.ActionAccept, + }, + shouldPass: false, + }, + { + name: "Implicit deny - wrong protocol", + srcIP: "100.10.0.1", + dstIP: "192.168.1.100", + proto: fw.ProtocolUDP, + srcPort: 12345, + dstPort: 80, + rule: rule{ + sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")}, + dest: netip.MustParsePrefix("192.168.1.0/24"), + proto: fw.ProtocolTCP, + dstPort: &fw.Port{Values: []uint16{80}}, + action: fw.ActionAccept, + }, + shouldPass: false, + }, + { + name: "Implicit deny - wrong source network", + srcIP: "172.16.0.1", + dstIP: "192.168.1.100", + proto: fw.ProtocolTCP, + srcPort: 12345, + dstPort: 80, + rule: rule{ + sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")}, + dest: netip.MustParsePrefix("192.168.1.0/24"), + proto: fw.ProtocolTCP, + dstPort: &fw.Port{Values: []uint16{80}}, + action: fw.ActionAccept, + }, + shouldPass: false, + }, + { + name: "Source port match", + srcIP: "100.10.0.1", + dstIP: "192.168.1.100", + proto: fw.ProtocolTCP, + srcPort: 12345, + dstPort: 80, + rule: rule{ + sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")}, + dest: netip.MustParsePrefix("192.168.1.0/24"), + proto: fw.ProtocolTCP, + srcPort: &fw.Port{Values: []uint16{12345}}, + action: fw.ActionAccept, + }, + shouldPass: true, + }, + { + name: "Multiple source networks", + srcIP: "172.16.0.1", + dstIP: "192.168.1.100", + proto: fw.ProtocolTCP, + srcPort: 12345, + dstPort: 80, + rule: rule{ + sources: []netip.Prefix{ + netip.MustParsePrefix("100.10.0.0/16"), + netip.MustParsePrefix("172.16.0.0/16"), + }, + dest: netip.MustParsePrefix("192.168.1.0/24"), + proto: fw.ProtocolTCP, + dstPort: &fw.Port{Values: []uint16{80}}, + action: fw.ActionAccept, + }, + shouldPass: true, + }, + { + name: "Allow ALL protocol without ports", + srcIP: "100.10.0.1", + dstIP: "192.168.1.100", + proto: fw.ProtocolICMP, + rule: rule{ + sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")}, + dest: netip.MustParsePrefix("192.168.1.0/24"), + proto: fw.ProtocolALL, + action: fw.ActionAccept, + }, + shouldPass: true, + }, + { + name: "Allow ALL protocol with specific ports", + srcIP: "100.10.0.1", + dstIP: "192.168.1.100", + proto: fw.ProtocolTCP, + srcPort: 12345, + dstPort: 80, + rule: rule{ + sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")}, + dest: netip.MustParsePrefix("192.168.1.0/24"), + proto: fw.ProtocolALL, + dstPort: &fw.Port{Values: []uint16{80}}, + action: fw.ActionAccept, + }, + shouldPass: true, + }, + { + name: "Multiple source networks with mismatched protocol", + srcIP: "172.16.0.1", + dstIP: "192.168.1.100", + // Should not match TCP rule + proto: fw.ProtocolUDP, + srcPort: 12345, + dstPort: 80, + rule: rule{ + sources: []netip.Prefix{ + netip.MustParsePrefix("100.10.0.0/16"), + netip.MustParsePrefix("172.16.0.0/16"), + }, + dest: netip.MustParsePrefix("192.168.1.0/24"), + proto: fw.ProtocolTCP, + dstPort: &fw.Port{Values: []uint16{80}}, + action: fw.ActionAccept, + }, + shouldPass: false, + }, + { + name: "Allow multiple destination ports", + srcIP: "100.10.0.1", + dstIP: "192.168.1.100", + proto: fw.ProtocolTCP, + srcPort: 12345, + dstPort: 8080, + rule: rule{ + sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")}, + dest: netip.MustParsePrefix("192.168.1.0/24"), + proto: fw.ProtocolTCP, + dstPort: &fw.Port{Values: []uint16{80, 8080, 443}}, + action: fw.ActionAccept, + }, + shouldPass: true, + }, + { + name: "Allow multiple source ports", + srcIP: "100.10.0.1", + dstIP: "192.168.1.100", + proto: fw.ProtocolTCP, + srcPort: 12345, + dstPort: 80, + rule: rule{ + sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")}, + dest: netip.MustParsePrefix("192.168.1.0/24"), + proto: fw.ProtocolTCP, + srcPort: &fw.Port{Values: []uint16{12345, 12346, 12347}}, + action: fw.ActionAccept, + }, + shouldPass: true, + }, + { + name: "Allow ALL protocol with both src and dst ports", + srcIP: "100.10.0.1", + dstIP: "192.168.1.100", + proto: fw.ProtocolTCP, + srcPort: 12345, + dstPort: 80, + rule: rule{ + sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")}, + dest: netip.MustParsePrefix("192.168.1.0/24"), + proto: fw.ProtocolALL, + srcPort: &fw.Port{Values: []uint16{12345}}, + dstPort: &fw.Port{Values: []uint16{80}}, + action: fw.ActionAccept, + }, + shouldPass: true, + }, + { + name: "Port Range - Within Range", + srcIP: "100.10.0.1", + dstIP: "192.168.1.100", + proto: fw.ProtocolTCP, + srcPort: 12345, + dstPort: 8080, + rule: rule{ + sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")}, + dest: netip.MustParsePrefix("192.168.1.0/24"), + proto: fw.ProtocolTCP, + dstPort: &fw.Port{ + IsRange: true, + Values: []uint16{8000, 8100}, + }, + action: fw.ActionAccept, + }, + shouldPass: true, + }, + { + name: "Port Range - Outside Range", + srcIP: "100.10.0.1", + dstIP: "192.168.1.100", + proto: fw.ProtocolTCP, + srcPort: 12345, + dstPort: 7999, + rule: rule{ + sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")}, + dest: netip.MustParsePrefix("192.168.1.0/24"), + proto: fw.ProtocolTCP, + dstPort: &fw.Port{ + IsRange: true, + Values: []uint16{8000, 8100}, + }, + action: fw.ActionAccept, + }, + shouldPass: false, + }, + { + name: "Source Port Range - Within Range", + srcIP: "100.10.0.1", + dstIP: "192.168.1.100", + proto: fw.ProtocolTCP, + srcPort: 32100, + dstPort: 80, + rule: rule{ + sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")}, + dest: netip.MustParsePrefix("192.168.1.0/24"), + proto: fw.ProtocolTCP, + srcPort: &fw.Port{ + IsRange: true, + Values: []uint16{32000, 33000}, + }, + action: fw.ActionAccept, + }, + shouldPass: true, + }, + { + name: "Mixed Port Specification - Range and Single", + srcIP: "100.10.0.1", + dstIP: "192.168.1.100", + proto: fw.ProtocolTCP, + srcPort: 32100, + dstPort: 443, + rule: rule{ + sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")}, + dest: netip.MustParsePrefix("192.168.1.0/24"), + proto: fw.ProtocolTCP, + srcPort: &fw.Port{ + IsRange: true, + Values: []uint16{32000, 33000}, + }, + dstPort: &fw.Port{ + Values: []uint16{443}, + }, + action: fw.ActionAccept, + }, + shouldPass: true, + }, + { + name: "Edge Case - Port at Range Boundary", + srcIP: "100.10.0.1", + dstIP: "192.168.1.100", + proto: fw.ProtocolTCP, + srcPort: 12345, + dstPort: 8100, + rule: rule{ + sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")}, + dest: netip.MustParsePrefix("192.168.1.0/24"), + proto: fw.ProtocolTCP, + dstPort: &fw.Port{ + IsRange: true, + Values: []uint16{8000, 8100}, + }, + action: fw.ActionAccept, + }, + shouldPass: true, + }, + { + name: "UDP Port Range", + srcIP: "100.10.0.1", + dstIP: "192.168.1.100", + proto: fw.ProtocolUDP, + srcPort: 12345, + dstPort: 5060, + rule: rule{ + sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")}, + dest: netip.MustParsePrefix("192.168.1.0/24"), + proto: fw.ProtocolUDP, + dstPort: &fw.Port{ + IsRange: true, + Values: []uint16{5060, 5070}, + }, + action: fw.ActionAccept, + }, + shouldPass: true, + }, + { + name: "ALL Protocol with Port Range", + srcIP: "100.10.0.1", + dstIP: "192.168.1.100", + proto: fw.ProtocolTCP, + srcPort: 12345, + dstPort: 8080, + rule: rule{ + sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")}, + dest: netip.MustParsePrefix("192.168.1.0/24"), + proto: fw.ProtocolALL, + dstPort: &fw.Port{ + IsRange: true, + Values: []uint16{8000, 8100}, + }, + action: fw.ActionAccept, + }, + shouldPass: true, + }, + { + name: "Drop TCP traffic to specific destination", + srcIP: "100.10.0.1", + dstIP: "192.168.1.100", + proto: fw.ProtocolTCP, + srcPort: 12345, + dstPort: 443, + rule: rule{ + sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")}, + dest: netip.MustParsePrefix("192.168.1.0/24"), + proto: fw.ProtocolTCP, + dstPort: &fw.Port{Values: []uint16{443}}, + action: fw.ActionDrop, + }, + shouldPass: false, + }, + { + name: "Drop all traffic to specific destination", + srcIP: "100.10.0.1", + dstIP: "192.168.1.100", + proto: fw.ProtocolTCP, + srcPort: 12345, + dstPort: 80, + rule: rule{ + sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")}, + dest: netip.MustParsePrefix("192.168.1.0/24"), + proto: fw.ProtocolALL, + action: fw.ActionDrop, + }, + shouldPass: false, + }, + { + name: "Drop traffic from multiple source networks", + srcIP: "172.16.0.1", + dstIP: "192.168.1.100", + proto: fw.ProtocolTCP, + srcPort: 12345, + dstPort: 80, + rule: rule{ + sources: []netip.Prefix{ + netip.MustParsePrefix("100.10.0.0/16"), + netip.MustParsePrefix("172.16.0.0/16"), + }, + dest: netip.MustParsePrefix("192.168.1.0/24"), + proto: fw.ProtocolTCP, + dstPort: &fw.Port{Values: []uint16{80}}, + action: fw.ActionDrop, + }, + shouldPass: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + rule, err := manager.AddRouteFiltering( + tc.rule.sources, + tc.rule.dest, + tc.rule.proto, + tc.rule.srcPort, + tc.rule.dstPort, + tc.rule.action, + ) + require.NoError(t, err) + require.NotNil(t, rule) + + t.Cleanup(func() { + require.NoError(t, manager.DeleteRouteRule(rule)) + }) + + srcIP := net.ParseIP(tc.srcIP) + dstIP := net.ParseIP(tc.dstIP) + + // testing routeACLsPass only and not DropIncoming, as routed packets are dropped after being passed + // to the forwarder + isAllowed := manager.routeACLsPass(srcIP, dstIP, tc.proto, tc.srcPort, tc.dstPort) + require.Equal(t, tc.shouldPass, isAllowed) + }) + } +} + +func TestRouteACLOrder(t *testing.T) { + manager := setupRoutedManager(t, "10.10.0.100/16") + + type testCase struct { + name string + rules []struct { + sources []netip.Prefix + dest netip.Prefix + proto fw.Protocol + srcPort *fw.Port + dstPort *fw.Port + action fw.Action + } + packets []struct { + srcIP string + dstIP string + proto fw.Protocol + srcPort uint16 + dstPort uint16 + shouldPass bool + } + } + + testCases := []testCase{ + { + name: "Drop rules take precedence over accept", + rules: []struct { + sources []netip.Prefix + dest netip.Prefix + proto fw.Protocol + srcPort *fw.Port + dstPort *fw.Port + action fw.Action + }{ + { + // Accept rule added first + sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")}, + dest: netip.MustParsePrefix("192.168.1.0/24"), + proto: fw.ProtocolTCP, + dstPort: &fw.Port{Values: []uint16{80, 443}}, + action: fw.ActionAccept, + }, + { + // Drop rule added second but should be evaluated first + sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")}, + dest: netip.MustParsePrefix("192.168.1.0/24"), + proto: fw.ProtocolTCP, + dstPort: &fw.Port{Values: []uint16{443}}, + action: fw.ActionDrop, + }, + }, + packets: []struct { + srcIP string + dstIP string + proto fw.Protocol + srcPort uint16 + dstPort uint16 + shouldPass bool + }{ + { + // Should be dropped by the drop rule + srcIP: "100.10.0.1", + dstIP: "192.168.1.100", + proto: fw.ProtocolTCP, + srcPort: 12345, + dstPort: 443, + shouldPass: false, + }, + { + // Should be allowed by the accept rule (port 80 not in drop rule) + srcIP: "100.10.0.1", + dstIP: "192.168.1.100", + proto: fw.ProtocolTCP, + srcPort: 12345, + dstPort: 80, + shouldPass: true, + }, + }, + }, + { + name: "Multiple drop rules take precedence", + rules: []struct { + sources []netip.Prefix + dest netip.Prefix + proto fw.Protocol + srcPort *fw.Port + dstPort *fw.Port + action fw.Action + }{ + { + // Accept all + sources: []netip.Prefix{netip.MustParsePrefix("0.0.0.0/0")}, + dest: netip.MustParsePrefix("0.0.0.0/0"), + proto: fw.ProtocolALL, + action: fw.ActionAccept, + }, + { + // Drop specific port + sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")}, + dest: netip.MustParsePrefix("192.168.1.0/24"), + proto: fw.ProtocolTCP, + dstPort: &fw.Port{Values: []uint16{443}}, + action: fw.ActionDrop, + }, + { + // Drop different port + sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")}, + dest: netip.MustParsePrefix("192.168.1.0/24"), + proto: fw.ProtocolTCP, + dstPort: &fw.Port{Values: []uint16{80}}, + action: fw.ActionDrop, + }, + }, + packets: []struct { + srcIP string + dstIP string + proto fw.Protocol + srcPort uint16 + dstPort uint16 + shouldPass bool + }{ + { + // Should be dropped by first drop rule + srcIP: "100.10.0.1", + dstIP: "192.168.1.100", + proto: fw.ProtocolTCP, + srcPort: 12345, + dstPort: 443, + shouldPass: false, + }, + { + // Should be dropped by second drop rule + srcIP: "100.10.0.1", + dstIP: "192.168.1.100", + proto: fw.ProtocolTCP, + srcPort: 12345, + dstPort: 80, + shouldPass: false, + }, + { + // Should be allowed by the accept rule (different port) + srcIP: "100.10.0.1", + dstIP: "192.168.1.100", + proto: fw.ProtocolTCP, + srcPort: 12345, + dstPort: 8080, + shouldPass: true, + }, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + var rules []fw.Rule + for _, r := range tc.rules { + rule, err := manager.AddRouteFiltering( + r.sources, + r.dest, + r.proto, + r.srcPort, + r.dstPort, + r.action, + ) + require.NoError(t, err) + require.NotNil(t, rule) + rules = append(rules, rule) + } + + t.Cleanup(func() { + for _, rule := range rules { + require.NoError(t, manager.DeleteRouteRule(rule)) + } + }) + + for i, p := range tc.packets { + srcIP := net.ParseIP(p.srcIP) + dstIP := net.ParseIP(p.dstIP) + + isAllowed := manager.routeACLsPass(srcIP, dstIP, p.proto, p.srcPort, p.dstPort) + require.Equal(t, p.shouldPass, isAllowed, "packet %d failed", i) + } + }) + } +} diff --git a/client/firewall/uspfilter/uspfilter_test.go b/client/firewall/uspfilter/uspfilter_test.go index 9d795de69..089bf8f55 100644 --- a/client/firewall/uspfilter/uspfilter_test.go +++ b/client/firewall/uspfilter/uspfilter_test.go @@ -9,17 +9,38 @@ import ( "github.com/google/gopacket" "github.com/google/gopacket/layers" + "github.com/sirupsen/logrus" "github.com/stretchr/testify/require" + wgdevice "golang.zx2c4.com/wireguard/device" fw "github.com/netbirdio/netbird/client/firewall/manager" "github.com/netbirdio/netbird/client/firewall/uspfilter/conntrack" + "github.com/netbirdio/netbird/client/firewall/uspfilter/log" "github.com/netbirdio/netbird/client/iface" "github.com/netbirdio/netbird/client/iface/device" ) +var logger = log.NewFromLogrus(logrus.StandardLogger()) + type IFaceMock struct { - SetFilterFunc func(device.PacketFilter) error - AddressFunc func() iface.WGAddress + SetFilterFunc func(device.PacketFilter) error + AddressFunc func() iface.WGAddress + GetWGDeviceFunc func() *wgdevice.Device + GetDeviceFunc func() *device.FilteredDevice +} + +func (i *IFaceMock) GetWGDevice() *wgdevice.Device { + if i.GetWGDeviceFunc == nil { + return nil + } + return i.GetWGDeviceFunc() +} + +func (i *IFaceMock) GetDevice() *device.FilteredDevice { + if i.GetDeviceFunc == nil { + return nil + } + return i.GetDeviceFunc() } func (i *IFaceMock) SetFilter(iface device.PacketFilter) error { @@ -41,7 +62,7 @@ func TestManagerCreate(t *testing.T) { SetFilterFunc: func(device.PacketFilter) error { return nil }, } - m, err := Create(ifaceMock) + m, err := Create(ifaceMock, false) if err != nil { t.Errorf("failed to create Manager: %v", err) return @@ -61,7 +82,7 @@ func TestManagerAddPeerFiltering(t *testing.T) { }, } - m, err := Create(ifaceMock) + m, err := Create(ifaceMock, false) if err != nil { t.Errorf("failed to create Manager: %v", err) return @@ -95,7 +116,7 @@ func TestManagerDeleteRule(t *testing.T) { SetFilterFunc: func(device.PacketFilter) error { return nil }, } - m, err := Create(ifaceMock) + m, err := Create(ifaceMock, false) if err != nil { t.Errorf("failed to create Manager: %v", err) return @@ -166,12 +187,12 @@ func TestAddUDPPacketHook(t *testing.T) { t.Run(tt.name, func(t *testing.T) { manager, err := Create(&IFaceMock{ SetFilterFunc: func(device.PacketFilter) error { return nil }, - }) + }, false) require.NoError(t, err) manager.AddUDPPacketHook(tt.in, tt.ip, tt.dPort, tt.hook) - var addedRule Rule + var addedRule PeerRule if tt.in { if len(manager.incomingRules[tt.ip.String()]) != 1 { t.Errorf("expected 1 incoming rule, got %d", len(manager.incomingRules)) @@ -215,7 +236,7 @@ func TestManagerReset(t *testing.T) { SetFilterFunc: func(device.PacketFilter) error { return nil }, } - m, err := Create(ifaceMock) + m, err := Create(ifaceMock, false) if err != nil { t.Errorf("failed to create Manager: %v", err) return @@ -247,9 +268,18 @@ func TestManagerReset(t *testing.T) { func TestNotMatchByIP(t *testing.T) { ifaceMock := &IFaceMock{ SetFilterFunc: func(device.PacketFilter) error { return nil }, + AddressFunc: func() iface.WGAddress { + return iface.WGAddress{ + IP: net.ParseIP("100.10.0.100"), + Network: &net.IPNet{ + IP: net.ParseIP("100.10.0.0"), + Mask: net.CIDRMask(16, 32), + }, + } + }, } - m, err := Create(ifaceMock) + m, err := Create(ifaceMock, false) if err != nil { t.Errorf("failed to create Manager: %v", err) return @@ -298,7 +328,7 @@ func TestNotMatchByIP(t *testing.T) { return } - if m.dropFilter(buf.Bytes(), m.incomingRules) { + if m.dropFilter(buf.Bytes()) { t.Errorf("expected packet to be accepted") return } @@ -317,7 +347,7 @@ func TestRemovePacketHook(t *testing.T) { } // creating manager instance - manager, err := Create(iface) + manager, err := Create(iface, false) if err != nil { t.Fatalf("Failed to create Manager: %s", err) } @@ -363,7 +393,7 @@ func TestRemovePacketHook(t *testing.T) { func TestProcessOutgoingHooks(t *testing.T) { manager, err := Create(&IFaceMock{ SetFilterFunc: func(device.PacketFilter) error { return nil }, - }) + }, false) require.NoError(t, err) manager.wgNetwork = &net.IPNet{ @@ -371,7 +401,7 @@ func TestProcessOutgoingHooks(t *testing.T) { Mask: net.CIDRMask(16, 32), } manager.udpTracker.Close() - manager.udpTracker = conntrack.NewUDPTracker(100 * time.Millisecond) + manager.udpTracker = conntrack.NewUDPTracker(100*time.Millisecond, logger) defer func() { require.NoError(t, manager.Reset(nil)) }() @@ -449,7 +479,7 @@ func TestUSPFilterCreatePerformance(t *testing.T) { ifaceMock := &IFaceMock{ SetFilterFunc: func(device.PacketFilter) error { return nil }, } - manager, err := Create(ifaceMock) + manager, err := Create(ifaceMock, false) require.NoError(t, err) time.Sleep(time.Second) @@ -476,7 +506,7 @@ func TestUSPFilterCreatePerformance(t *testing.T) { func TestStatefulFirewall_UDPTracking(t *testing.T) { manager, err := Create(&IFaceMock{ SetFilterFunc: func(device.PacketFilter) error { return nil }, - }) + }, false) require.NoError(t, err) manager.wgNetwork = &net.IPNet{ @@ -485,7 +515,7 @@ func TestStatefulFirewall_UDPTracking(t *testing.T) { } manager.udpTracker.Close() // Close the existing tracker - manager.udpTracker = conntrack.NewUDPTracker(200 * time.Millisecond) + manager.udpTracker = conntrack.NewUDPTracker(200*time.Millisecond, logger) manager.decoders = sync.Pool{ New: func() any { d := &decoder{ @@ -606,7 +636,7 @@ func TestStatefulFirewall_UDPTracking(t *testing.T) { for _, cp := range checkPoints { time.Sleep(cp.sleep) - drop = manager.dropFilter(inboundBuf.Bytes(), manager.incomingRules) + drop = manager.dropFilter(inboundBuf.Bytes()) require.Equal(t, cp.shouldAllow, !drop, cp.description) // If the connection should still be valid, verify it exists @@ -677,7 +707,7 @@ func TestStatefulFirewall_UDPTracking(t *testing.T) { require.NoError(t, err) // Verify the invalid packet is dropped - drop = manager.dropFilter(testBuf.Bytes(), manager.incomingRules) + drop = manager.dropFilter(testBuf.Bytes()) require.True(t, drop, tc.description) }) } diff --git a/client/iface/device.go b/client/iface/device.go index 0d4e69145..2a170adfb 100644 --- a/client/iface/device.go +++ b/client/iface/device.go @@ -3,6 +3,8 @@ package iface import ( + wgdevice "golang.zx2c4.com/wireguard/device" + "github.com/netbirdio/netbird/client/iface/bind" "github.com/netbirdio/netbird/client/iface/device" ) @@ -15,4 +17,5 @@ type WGTunDevice interface { DeviceName() string Close() error FilteredDevice() *device.FilteredDevice + Device() *wgdevice.Device } diff --git a/client/iface/device/device_darwin.go b/client/iface/device/device_darwin.go index b5a128bc1..fe7ed1752 100644 --- a/client/iface/device/device_darwin.go +++ b/client/iface/device/device_darwin.go @@ -117,6 +117,11 @@ func (t *TunDevice) FilteredDevice() *FilteredDevice { return t.filteredDevice } +// Device returns the wireguard device +func (t *TunDevice) Device() *device.Device { + return t.device +} + // assignAddr Adds IP address to the tunnel interface and network route based on the range provided func (t *TunDevice) assignAddr() error { cmd := exec.Command("ifconfig", t.name, "inet", t.address.IP.String(), t.address.IP.String()) diff --git a/client/iface/device/device_kernel_unix.go b/client/iface/device/device_kernel_unix.go index 0dfed4d90..3314b576b 100644 --- a/client/iface/device/device_kernel_unix.go +++ b/client/iface/device/device_kernel_unix.go @@ -9,6 +9,7 @@ import ( "github.com/pion/transport/v3" log "github.com/sirupsen/logrus" + "golang.zx2c4.com/wireguard/device" "github.com/netbirdio/netbird/client/iface/bind" "github.com/netbirdio/netbird/client/iface/configurer" @@ -151,6 +152,11 @@ func (t *TunKernelDevice) DeviceName() string { return t.name } +// Device returns the wireguard device, not applicable for kernel devices +func (t *TunKernelDevice) Device() *device.Device { + return nil +} + func (t *TunKernelDevice) FilteredDevice() *FilteredDevice { return nil } diff --git a/client/iface/device/device_netstack.go b/client/iface/device/device_netstack.go index f5d39e9e0..c7d297187 100644 --- a/client/iface/device/device_netstack.go +++ b/client/iface/device/device_netstack.go @@ -117,3 +117,8 @@ func (t *TunNetstackDevice) DeviceName() string { func (t *TunNetstackDevice) FilteredDevice() *FilteredDevice { return t.filteredDevice } + +// Device returns the wireguard device +func (t *TunNetstackDevice) Device() *device.Device { + return t.device +} diff --git a/client/iface/device/device_usp_unix.go b/client/iface/device/device_usp_unix.go index 3562f312d..4ac87aecb 100644 --- a/client/iface/device/device_usp_unix.go +++ b/client/iface/device/device_usp_unix.go @@ -124,6 +124,11 @@ func (t *USPDevice) FilteredDevice() *FilteredDevice { return t.filteredDevice } +// Device returns the wireguard device +func (t *USPDevice) Device() *device.Device { + return t.device +} + // assignAddr Adds IP address to the tunnel interface func (t *USPDevice) assignAddr() error { link := newWGLink(t.name) diff --git a/client/iface/device/device_windows.go b/client/iface/device/device_windows.go index 86968d06d..e603d7696 100644 --- a/client/iface/device/device_windows.go +++ b/client/iface/device/device_windows.go @@ -150,6 +150,11 @@ func (t *TunDevice) FilteredDevice() *FilteredDevice { return t.filteredDevice } +// Device returns the wireguard device +func (t *TunDevice) Device() *device.Device { + return t.device +} + func (t *TunDevice) GetInterfaceGUIDString() (string, error) { if t.nativeTunDevice == nil { return "", fmt.Errorf("interface has not been initialized yet") diff --git a/client/iface/device_android.go b/client/iface/device_android.go index 3d15080ff..028f6fa7d 100644 --- a/client/iface/device_android.go +++ b/client/iface/device_android.go @@ -1,6 +1,8 @@ package iface import ( + wgdevice "golang.zx2c4.com/wireguard/device" + "github.com/netbirdio/netbird/client/iface/bind" "github.com/netbirdio/netbird/client/iface/device" ) @@ -13,4 +15,5 @@ type WGTunDevice interface { DeviceName() string Close() error FilteredDevice() *device.FilteredDevice + Device() *wgdevice.Device } diff --git a/client/iface/iface.go b/client/iface/iface.go index 1fb9c2691..64219975f 100644 --- a/client/iface/iface.go +++ b/client/iface/iface.go @@ -11,6 +11,8 @@ import ( log "github.com/sirupsen/logrus" "golang.zx2c4.com/wireguard/wgctrl/wgtypes" + wgdevice "golang.zx2c4.com/wireguard/device" + "github.com/netbirdio/netbird/client/errors" "github.com/netbirdio/netbird/client/iface/bind" "github.com/netbirdio/netbird/client/iface/configurer" @@ -203,6 +205,11 @@ func (w *WGIface) GetDevice() *device.FilteredDevice { return w.tun.FilteredDevice() } +// GetWGDevice returns the WireGuard device +func (w *WGIface) GetWGDevice() *wgdevice.Device { + return w.tun.Device() +} + // GetStats returns the last handshake time, rx and tx bytes for the given peer func (w *WGIface) GetStats(peerKey string) (configurer.WGStats, error) { return w.configurer.GetStats(peerKey) diff --git a/client/iface/iface_moc.go b/client/iface/iface_moc.go index d91a7224f..5f57bc821 100644 --- a/client/iface/iface_moc.go +++ b/client/iface/iface_moc.go @@ -4,6 +4,7 @@ import ( "net" "time" + wgdevice "golang.zx2c4.com/wireguard/device" "golang.zx2c4.com/wireguard/wgctrl/wgtypes" "github.com/netbirdio/netbird/client/iface/bind" @@ -29,6 +30,7 @@ type MockWGIface struct { SetFilterFunc func(filter device.PacketFilter) error GetFilterFunc func() device.PacketFilter GetDeviceFunc func() *device.FilteredDevice + GetWGDeviceFunc func() *wgdevice.Device GetStatsFunc func(peerKey string) (configurer.WGStats, error) GetInterfaceGUIDStringFunc func() (string, error) GetProxyFunc func() wgproxy.Proxy @@ -102,11 +104,14 @@ func (m *MockWGIface) GetDevice() *device.FilteredDevice { return m.GetDeviceFunc() } +func (m *MockWGIface) GetWGDevice() *wgdevice.Device { + return m.GetWGDeviceFunc() +} + func (m *MockWGIface) GetStats(peerKey string) (configurer.WGStats, error) { return m.GetStatsFunc(peerKey) } func (m *MockWGIface) GetProxy() wgproxy.Proxy { - //TODO implement me - panic("implement me") + return m.GetProxyFunc() } diff --git a/client/iface/iwginterface.go b/client/iface/iwginterface.go index f5ab29539..472ab45f9 100644 --- a/client/iface/iwginterface.go +++ b/client/iface/iwginterface.go @@ -6,6 +6,7 @@ import ( "net" "time" + wgdevice "golang.zx2c4.com/wireguard/device" "golang.zx2c4.com/wireguard/wgctrl/wgtypes" "github.com/netbirdio/netbird/client/iface/bind" @@ -32,5 +33,6 @@ type IWGIface interface { SetFilter(filter device.PacketFilter) error GetFilter() device.PacketFilter GetDevice() *device.FilteredDevice + GetWGDevice() *wgdevice.Device GetStats(peerKey string) (configurer.WGStats, error) } diff --git a/client/iface/iwginterface_windows.go b/client/iface/iwginterface_windows.go index 96eec52a5..c9183cafd 100644 --- a/client/iface/iwginterface_windows.go +++ b/client/iface/iwginterface_windows.go @@ -4,6 +4,7 @@ import ( "net" "time" + wgdevice "golang.zx2c4.com/wireguard/device" "golang.zx2c4.com/wireguard/wgctrl/wgtypes" "github.com/netbirdio/netbird/client/iface/bind" @@ -30,6 +31,7 @@ type IWGIface interface { SetFilter(filter device.PacketFilter) error GetFilter() device.PacketFilter GetDevice() *device.FilteredDevice + GetWGDevice() *wgdevice.Device GetStats(peerKey string) (configurer.WGStats, error) GetInterfaceGUIDString() (string, error) } diff --git a/client/internal/acl/manager_test.go b/client/internal/acl/manager_test.go index 6049b4f48..217dbce9f 100644 --- a/client/internal/acl/manager_test.go +++ b/client/internal/acl/manager_test.go @@ -49,9 +49,10 @@ func TestDefaultManager(t *testing.T) { IP: ip, Network: network, }).AnyTimes() + ifaceMock.EXPECT().GetWGDevice().Return(nil).AnyTimes() // we receive one rule from the management so for testing purposes ignore it - fw, err := firewall.NewFirewall(ifaceMock, nil) + fw, err := firewall.NewFirewall(ifaceMock, nil, false) if err != nil { t.Errorf("create firewall: %v", err) return @@ -342,9 +343,10 @@ func TestDefaultManagerEnableSSHRules(t *testing.T) { IP: ip, Network: network, }).AnyTimes() + ifaceMock.EXPECT().GetWGDevice().Return(nil).AnyTimes() // we receive one rule from the management so for testing purposes ignore it - fw, err := firewall.NewFirewall(ifaceMock, nil) + fw, err := firewall.NewFirewall(ifaceMock, nil, false) if err != nil { t.Errorf("create firewall: %v", err) return diff --git a/client/internal/acl/mocks/iface_mapper.go b/client/internal/acl/mocks/iface_mapper.go index 3ed12b6dd..08aa4fd5a 100644 --- a/client/internal/acl/mocks/iface_mapper.go +++ b/client/internal/acl/mocks/iface_mapper.go @@ -8,6 +8,8 @@ import ( reflect "reflect" gomock "github.com/golang/mock/gomock" + wgdevice "golang.zx2c4.com/wireguard/device" + iface "github.com/netbirdio/netbird/client/iface" "github.com/netbirdio/netbird/client/iface/device" ) @@ -90,3 +92,31 @@ func (mr *MockIFaceMapperMockRecorder) SetFilter(arg0 interface{}) *gomock.Call mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetFilter", reflect.TypeOf((*MockIFaceMapper)(nil).SetFilter), arg0) } + +// GetDevice mocks base method. +func (m *MockIFaceMapper) GetDevice() *device.FilteredDevice { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetDevice") + ret0, _ := ret[0].(*device.FilteredDevice) + return ret0 +} + +// GetDevice indicates an expected call of GetDevice. +func (mr *MockIFaceMapperMockRecorder) GetDevice() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDevice", reflect.TypeOf((*MockIFaceMapper)(nil).GetDevice)) +} + +// GetWGDevice mocks base method. +func (m *MockIFaceMapper) GetWGDevice() *wgdevice.Device { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetWGDevice") + ret0, _ := ret[0].(*wgdevice.Device) + return ret0 +} + +// GetWGDevice indicates an expected call of GetWGDevice. +func (mr *MockIFaceMapperMockRecorder) GetWGDevice() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWGDevice", reflect.TypeOf((*MockIFaceMapper)(nil).GetWGDevice)) +} diff --git a/client/internal/dns/server_test.go b/client/internal/dns/server_test.go index c166820c4..14ff1bb71 100644 --- a/client/internal/dns/server_test.go +++ b/client/internal/dns/server_test.go @@ -849,7 +849,7 @@ func createWgInterfaceWithBind(t *testing.T) (*iface.WGIface, error) { return nil, err } - pf, err := uspfilter.Create(wgIface) + pf, err := uspfilter.Create(wgIface, false) if err != nil { t.Fatalf("failed to create uspfilter: %v", err) return nil, err diff --git a/client/internal/engine.go b/client/internal/engine.go index 335729d92..14e0d348f 100644 --- a/client/internal/engine.go +++ b/client/internal/engine.go @@ -42,13 +42,13 @@ import ( "github.com/netbirdio/netbird/client/internal/routemanager" "github.com/netbirdio/netbird/client/internal/routemanager/systemops" "github.com/netbirdio/netbird/client/internal/statemanager" + "github.com/netbirdio/netbird/management/domain" semaphoregroup "github.com/netbirdio/netbird/util/semaphore-group" nbssh "github.com/netbirdio/netbird/client/ssh" "github.com/netbirdio/netbird/client/system" nbdns "github.com/netbirdio/netbird/dns" mgm "github.com/netbirdio/netbird/management/client" - "github.com/netbirdio/netbird/management/domain" mgmProto "github.com/netbirdio/netbird/management/proto" auth "github.com/netbirdio/netbird/relay/auth/hmac" relayClient "github.com/netbirdio/netbird/relay/client" @@ -193,6 +193,10 @@ type Peer struct { WgAllowedIps string } +type localIpUpdater interface { + UpdateLocalIPs() error +} + // NewEngine creates a new Connection Engine with probes attached func NewEngine( clientCtx context.Context, @@ -433,7 +437,7 @@ func (e *Engine) createFirewall() error { } var err error - e.firewall, err = firewall.NewFirewall(e.wgInterface, e.stateManager) + e.firewall, err = firewall.NewFirewall(e.wgInterface, e.stateManager, e.config.DisableServerRoutes) if err != nil || e.firewall == nil { log.Errorf("failed creating firewall manager: %s", err) return nil @@ -883,6 +887,14 @@ func (e *Engine) updateNetworkMap(networkMap *mgmProto.NetworkMap) error { e.acl.ApplyFiltering(networkMap) } + if e.firewall != nil { + if localipfw, ok := e.firewall.(localIpUpdater); ok { + if err := localipfw.UpdateLocalIPs(); err != nil { + log.Errorf("failed to update local IPs: %v", err) + } + } + } + // DNS forwarder dnsRouteFeatureFlag := toDNSFeatureFlag(networkMap) dnsRouteDomains := toRouteDomains(e.config.WgPrivateKey.PublicKey().String(), networkMap.GetRoutes()) @@ -1446,6 +1458,11 @@ func (e *Engine) GetRouteManager() routemanager.Manager { return e.routeManager } +// GetFirewallManager returns the firewall manager +func (e *Engine) GetFirewallManager() manager.Manager { + return e.firewall +} + func findIPFromInterfaceName(ifaceName string) (net.IP, error) { iface, err := net.InterfaceByName(ifaceName) if err != nil { @@ -1657,6 +1674,14 @@ func (e *Engine) GetLatestNetworkMap() (*mgmProto.NetworkMap, error) { return nm, nil } +// GetWgAddr returns the wireguard address +func (e *Engine) GetWgAddr() net.IP { + if e.wgInterface == nil { + return nil + } + return e.wgInterface.Address().IP +} + // updateDNSForwarder start or stop the DNS forwarder based on the domains and the feature flag func (e *Engine) updateDNSForwarder(enabled bool, domains []string) { if !enabled { diff --git a/client/internal/routemanager/manager.go b/client/internal/routemanager/manager.go index 6f73fb166..34bd67893 100644 --- a/client/internal/routemanager/manager.go +++ b/client/internal/routemanager/manager.go @@ -422,11 +422,6 @@ func (m *DefaultManager) classifyRoutes(newRoutes []*route.Route) (map[route.ID] haID := newRoute.GetHAUniqueID() if newRoute.Peer == m.pubKey { ownNetworkIDs[haID] = true - // only linux is supported for now - if runtime.GOOS != "linux" { - log.Warnf("received a route to manage, but agent doesn't support router mode on %s OS", runtime.GOOS) - continue - } newServerRoutesMap[newRoute.ID] = newRoute } } diff --git a/client/proto/daemon.pb.go b/client/proto/daemon.pb.go index 30f7473cd..c9651efed 100644 --- a/client/proto/daemon.pb.go +++ b/client/proto/daemon.pb.go @@ -2571,6 +2571,330 @@ func (*SetNetworkMapPersistenceResponse) Descriptor() ([]byte, []int) { return file_daemon_proto_rawDescGZIP(), []int{39} } +type TCPFlags struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Syn bool `protobuf:"varint,1,opt,name=syn,proto3" json:"syn,omitempty"` + Ack bool `protobuf:"varint,2,opt,name=ack,proto3" json:"ack,omitempty"` + Fin bool `protobuf:"varint,3,opt,name=fin,proto3" json:"fin,omitempty"` + Rst bool `protobuf:"varint,4,opt,name=rst,proto3" json:"rst,omitempty"` + Psh bool `protobuf:"varint,5,opt,name=psh,proto3" json:"psh,omitempty"` + Urg bool `protobuf:"varint,6,opt,name=urg,proto3" json:"urg,omitempty"` +} + +func (x *TCPFlags) Reset() { + *x = TCPFlags{} + if protoimpl.UnsafeEnabled { + mi := &file_daemon_proto_msgTypes[40] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *TCPFlags) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TCPFlags) ProtoMessage() {} + +func (x *TCPFlags) ProtoReflect() protoreflect.Message { + mi := &file_daemon_proto_msgTypes[40] + 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 TCPFlags.ProtoReflect.Descriptor instead. +func (*TCPFlags) Descriptor() ([]byte, []int) { + return file_daemon_proto_rawDescGZIP(), []int{40} +} + +func (x *TCPFlags) GetSyn() bool { + if x != nil { + return x.Syn + } + return false +} + +func (x *TCPFlags) GetAck() bool { + if x != nil { + return x.Ack + } + return false +} + +func (x *TCPFlags) GetFin() bool { + if x != nil { + return x.Fin + } + return false +} + +func (x *TCPFlags) GetRst() bool { + if x != nil { + return x.Rst + } + return false +} + +func (x *TCPFlags) GetPsh() bool { + if x != nil { + return x.Psh + } + return false +} + +func (x *TCPFlags) GetUrg() bool { + if x != nil { + return x.Urg + } + return false +} + +type TracePacketRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + SourceIp string `protobuf:"bytes,1,opt,name=source_ip,json=sourceIp,proto3" json:"source_ip,omitempty"` + DestinationIp string `protobuf:"bytes,2,opt,name=destination_ip,json=destinationIp,proto3" json:"destination_ip,omitempty"` + Protocol string `protobuf:"bytes,3,opt,name=protocol,proto3" json:"protocol,omitempty"` + SourcePort uint32 `protobuf:"varint,4,opt,name=source_port,json=sourcePort,proto3" json:"source_port,omitempty"` + DestinationPort uint32 `protobuf:"varint,5,opt,name=destination_port,json=destinationPort,proto3" json:"destination_port,omitempty"` + Direction string `protobuf:"bytes,6,opt,name=direction,proto3" json:"direction,omitempty"` + TcpFlags *TCPFlags `protobuf:"bytes,7,opt,name=tcp_flags,json=tcpFlags,proto3,oneof" json:"tcp_flags,omitempty"` + IcmpType *uint32 `protobuf:"varint,8,opt,name=icmp_type,json=icmpType,proto3,oneof" json:"icmp_type,omitempty"` + IcmpCode *uint32 `protobuf:"varint,9,opt,name=icmp_code,json=icmpCode,proto3,oneof" json:"icmp_code,omitempty"` +} + +func (x *TracePacketRequest) Reset() { + *x = TracePacketRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_daemon_proto_msgTypes[41] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *TracePacketRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TracePacketRequest) ProtoMessage() {} + +func (x *TracePacketRequest) ProtoReflect() protoreflect.Message { + mi := &file_daemon_proto_msgTypes[41] + 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 TracePacketRequest.ProtoReflect.Descriptor instead. +func (*TracePacketRequest) Descriptor() ([]byte, []int) { + return file_daemon_proto_rawDescGZIP(), []int{41} +} + +func (x *TracePacketRequest) GetSourceIp() string { + if x != nil { + return x.SourceIp + } + return "" +} + +func (x *TracePacketRequest) GetDestinationIp() string { + if x != nil { + return x.DestinationIp + } + return "" +} + +func (x *TracePacketRequest) GetProtocol() string { + if x != nil { + return x.Protocol + } + return "" +} + +func (x *TracePacketRequest) GetSourcePort() uint32 { + if x != nil { + return x.SourcePort + } + return 0 +} + +func (x *TracePacketRequest) GetDestinationPort() uint32 { + if x != nil { + return x.DestinationPort + } + return 0 +} + +func (x *TracePacketRequest) GetDirection() string { + if x != nil { + return x.Direction + } + return "" +} + +func (x *TracePacketRequest) GetTcpFlags() *TCPFlags { + if x != nil { + return x.TcpFlags + } + return nil +} + +func (x *TracePacketRequest) GetIcmpType() uint32 { + if x != nil && x.IcmpType != nil { + return *x.IcmpType + } + return 0 +} + +func (x *TracePacketRequest) GetIcmpCode() uint32 { + if x != nil && x.IcmpCode != nil { + return *x.IcmpCode + } + return 0 +} + +type TraceStage struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"` + Allowed bool `protobuf:"varint,3,opt,name=allowed,proto3" json:"allowed,omitempty"` + ForwardingDetails *string `protobuf:"bytes,4,opt,name=forwarding_details,json=forwardingDetails,proto3,oneof" json:"forwarding_details,omitempty"` +} + +func (x *TraceStage) Reset() { + *x = TraceStage{} + if protoimpl.UnsafeEnabled { + mi := &file_daemon_proto_msgTypes[42] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *TraceStage) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TraceStage) ProtoMessage() {} + +func (x *TraceStage) ProtoReflect() protoreflect.Message { + mi := &file_daemon_proto_msgTypes[42] + 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 TraceStage.ProtoReflect.Descriptor instead. +func (*TraceStage) Descriptor() ([]byte, []int) { + return file_daemon_proto_rawDescGZIP(), []int{42} +} + +func (x *TraceStage) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *TraceStage) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +func (x *TraceStage) GetAllowed() bool { + if x != nil { + return x.Allowed + } + return false +} + +func (x *TraceStage) GetForwardingDetails() string { + if x != nil && x.ForwardingDetails != nil { + return *x.ForwardingDetails + } + return "" +} + +type TracePacketResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Stages []*TraceStage `protobuf:"bytes,1,rep,name=stages,proto3" json:"stages,omitempty"` + FinalDisposition bool `protobuf:"varint,2,opt,name=final_disposition,json=finalDisposition,proto3" json:"final_disposition,omitempty"` +} + +func (x *TracePacketResponse) Reset() { + *x = TracePacketResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_daemon_proto_msgTypes[43] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *TracePacketResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TracePacketResponse) ProtoMessage() {} + +func (x *TracePacketResponse) ProtoReflect() protoreflect.Message { + mi := &file_daemon_proto_msgTypes[43] + 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 TracePacketResponse.ProtoReflect.Descriptor instead. +func (*TracePacketResponse) Descriptor() ([]byte, []int) { + return file_daemon_proto_rawDescGZIP(), []int{43} +} + +func (x *TracePacketResponse) GetStages() []*TraceStage { + if x != nil { + return x.Stages + } + return nil +} + +func (x *TracePacketResponse) GetFinalDisposition() bool { + if x != nil { + return x.FinalDisposition + } + return false +} + var File_daemon_proto protoreflect.FileDescriptor var file_daemon_proto_rawDesc = []byte{ @@ -2920,87 +3244,141 @@ var file_daemon_proto_rawDesc = []byte{ 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x22, 0x22, 0x0a, 0x20, 0x53, 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x50, 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x2a, 0x62, 0x0a, 0x08, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, - 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, - 0x50, 0x41, 0x4e, 0x49, 0x43, 0x10, 0x01, 0x12, 0x09, 0x0a, 0x05, 0x46, 0x41, 0x54, 0x41, 0x4c, - 0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x03, 0x12, 0x08, 0x0a, - 0x04, 0x57, 0x41, 0x52, 0x4e, 0x10, 0x04, 0x12, 0x08, 0x0a, 0x04, 0x49, 0x4e, 0x46, 0x4f, 0x10, - 0x05, 0x12, 0x09, 0x0a, 0x05, 0x44, 0x45, 0x42, 0x55, 0x47, 0x10, 0x06, 0x12, 0x09, 0x0a, 0x05, - 0x54, 0x52, 0x41, 0x43, 0x45, 0x10, 0x07, 0x32, 0x93, 0x09, 0x0a, 0x0d, 0x44, 0x61, 0x65, 0x6d, - 0x6f, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x36, 0x0a, 0x05, 0x4c, 0x6f, 0x67, - 0x69, 0x6e, 0x12, 0x14, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x67, 0x69, - 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x15, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, - 0x6e, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, - 0x00, 0x12, 0x4b, 0x0a, 0x0c, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, - 0x6e, 0x12, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x57, 0x61, 0x69, 0x74, 0x53, - 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, + 0x6e, 0x73, 0x65, 0x22, 0x76, 0x0a, 0x08, 0x54, 0x43, 0x50, 0x46, 0x6c, 0x61, 0x67, 0x73, 0x12, + 0x10, 0x0a, 0x03, 0x73, 0x79, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x73, 0x79, + 0x6e, 0x12, 0x10, 0x0a, 0x03, 0x61, 0x63, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, + 0x61, 0x63, 0x6b, 0x12, 0x10, 0x0a, 0x03, 0x66, 0x69, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, + 0x52, 0x03, 0x66, 0x69, 0x6e, 0x12, 0x10, 0x0a, 0x03, 0x72, 0x73, 0x74, 0x18, 0x04, 0x20, 0x01, + 0x28, 0x08, 0x52, 0x03, 0x72, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x70, 0x73, 0x68, 0x18, 0x05, + 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x70, 0x73, 0x68, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x67, + 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x75, 0x72, 0x67, 0x22, 0x80, 0x03, 0x0a, 0x12, + 0x54, 0x72, 0x61, 0x63, 0x65, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x69, 0x70, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x70, 0x12, + 0x25, 0x0a, 0x0e, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, + 0x70, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x70, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, + 0x6f, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, + 0x6f, 0x6c, 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x70, 0x6f, 0x72, + 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0a, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x50, + 0x6f, 0x72, 0x74, 0x12, 0x29, 0x0a, 0x10, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x5f, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0f, 0x64, + 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x1c, + 0x0a, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x32, 0x0a, 0x09, + 0x74, 0x63, 0x70, 0x5f, 0x66, 0x6c, 0x61, 0x67, 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x10, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x54, 0x43, 0x50, 0x46, 0x6c, 0x61, 0x67, + 0x73, 0x48, 0x00, 0x52, 0x08, 0x74, 0x63, 0x70, 0x46, 0x6c, 0x61, 0x67, 0x73, 0x88, 0x01, 0x01, + 0x12, 0x20, 0x0a, 0x09, 0x69, 0x63, 0x6d, 0x70, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x08, 0x20, + 0x01, 0x28, 0x0d, 0x48, 0x01, 0x52, 0x08, 0x69, 0x63, 0x6d, 0x70, 0x54, 0x79, 0x70, 0x65, 0x88, + 0x01, 0x01, 0x12, 0x20, 0x0a, 0x09, 0x69, 0x63, 0x6d, 0x70, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, + 0x09, 0x20, 0x01, 0x28, 0x0d, 0x48, 0x02, 0x52, 0x08, 0x69, 0x63, 0x6d, 0x70, 0x43, 0x6f, 0x64, + 0x65, 0x88, 0x01, 0x01, 0x42, 0x0c, 0x0a, 0x0a, 0x5f, 0x74, 0x63, 0x70, 0x5f, 0x66, 0x6c, 0x61, + 0x67, 0x73, 0x42, 0x0c, 0x0a, 0x0a, 0x5f, 0x69, 0x63, 0x6d, 0x70, 0x5f, 0x74, 0x79, 0x70, 0x65, + 0x42, 0x0c, 0x0a, 0x0a, 0x5f, 0x69, 0x63, 0x6d, 0x70, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x22, 0x9f, + 0x01, 0x0a, 0x0a, 0x54, 0x72, 0x61, 0x63, 0x65, 0x53, 0x74, 0x61, 0x67, 0x65, 0x12, 0x12, 0x0a, + 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, + 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x61, + 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x61, 0x6c, + 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x12, 0x32, 0x0a, 0x12, 0x66, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, + 0x69, 0x6e, 0x67, 0x5f, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x09, 0x48, 0x00, 0x52, 0x11, 0x66, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x44, + 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x88, 0x01, 0x01, 0x42, 0x15, 0x0a, 0x13, 0x5f, 0x66, 0x6f, + 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x5f, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, + 0x22, 0x6e, 0x0a, 0x13, 0x54, 0x72, 0x61, 0x63, 0x65, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2a, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x67, 0x65, + 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, + 0x2e, 0x54, 0x72, 0x61, 0x63, 0x65, 0x53, 0x74, 0x61, 0x67, 0x65, 0x52, 0x06, 0x73, 0x74, 0x61, + 0x67, 0x65, 0x73, 0x12, 0x2b, 0x0a, 0x11, 0x66, 0x69, 0x6e, 0x61, 0x6c, 0x5f, 0x64, 0x69, 0x73, + 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, + 0x66, 0x69, 0x6e, 0x61, 0x6c, 0x44, 0x69, 0x73, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, + 0x2a, 0x62, 0x0a, 0x08, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x0b, 0x0a, 0x07, + 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x50, 0x41, 0x4e, + 0x49, 0x43, 0x10, 0x01, 0x12, 0x09, 0x0a, 0x05, 0x46, 0x41, 0x54, 0x41, 0x4c, 0x10, 0x02, 0x12, + 0x09, 0x0a, 0x05, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x03, 0x12, 0x08, 0x0a, 0x04, 0x57, 0x41, + 0x52, 0x4e, 0x10, 0x04, 0x12, 0x08, 0x0a, 0x04, 0x49, 0x4e, 0x46, 0x4f, 0x10, 0x05, 0x12, 0x09, + 0x0a, 0x05, 0x44, 0x45, 0x42, 0x55, 0x47, 0x10, 0x06, 0x12, 0x09, 0x0a, 0x05, 0x54, 0x52, 0x41, + 0x43, 0x45, 0x10, 0x07, 0x32, 0xdd, 0x09, 0x0a, 0x0d, 0x44, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x53, + 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x36, 0x0a, 0x05, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, + 0x14, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x15, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, + 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x4b, + 0x0a, 0x0c, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, - 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x2d, - 0x0a, 0x02, 0x55, 0x70, 0x12, 0x11, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x55, 0x70, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, - 0x2e, 0x55, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x39, 0x0a, - 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x15, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, - 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, - 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x33, 0x0a, 0x04, 0x44, 0x6f, 0x77, 0x6e, - 0x12, 0x13, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x6f, 0x77, 0x6e, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, - 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x42, 0x0a, - 0x09, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x18, 0x2e, 0x64, 0x61, 0x65, - 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, - 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, - 0x00, 0x12, 0x4b, 0x0a, 0x0c, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, - 0x73, 0x12, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4e, - 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, + 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x64, 0x61, + 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, + 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x2d, 0x0a, 0x02, 0x55, + 0x70, 0x12, 0x11, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x55, 0x70, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x55, 0x70, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x39, 0x0a, 0x06, 0x53, 0x74, + 0x61, 0x74, 0x75, 0x73, 0x12, 0x15, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x74, + 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x64, 0x61, + 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x33, 0x0a, 0x04, 0x44, 0x6f, 0x77, 0x6e, 0x12, 0x13, 0x2e, + 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x6f, 0x77, 0x6e, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x42, 0x0a, 0x09, 0x47, 0x65, + 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x18, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, + 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x19, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x4b, + 0x0a, 0x0c, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x12, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x65, 0x74, 0x77, - 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x51, - 0x0a, 0x0e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, - 0x12, 0x1d, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, - 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x1e, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, - 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, - 0x00, 0x12, 0x53, 0x0a, 0x10, 0x44, 0x65, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, - 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x12, 0x1d, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, - 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, - 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x0b, 0x44, 0x65, 0x62, 0x75, 0x67, 0x42, - 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, - 0x65, 0x62, 0x75, 0x67, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x62, 0x75, 0x67, - 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, - 0x12, 0x48, 0x0a, 0x0b, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, - 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, - 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, - 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x0b, 0x53, 0x65, - 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, - 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, - 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x22, 0x00, 0x12, 0x45, 0x0a, 0x0a, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, - 0x65, 0x73, 0x12, 0x19, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x69, 0x73, 0x74, - 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, - 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, - 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x45, 0x0a, 0x0a, 0x43, - 0x6c, 0x65, 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x19, 0x2e, 0x64, 0x61, 0x65, 0x6d, - 0x6f, 0x6e, 0x2e, 0x43, 0x6c, 0x65, 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x43, 0x6c, - 0x65, 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x22, 0x00, 0x12, 0x48, 0x0a, 0x0b, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, - 0x65, 0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, - 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, - 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x74, 0x61, - 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x6f, 0x0a, 0x18, - 0x53, 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x50, 0x65, 0x72, - 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x27, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, - 0x6e, 0x2e, 0x53, 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x50, - 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x28, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x74, 0x4e, 0x65, - 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x50, 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, - 0x6e, 0x63, 0x65, 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, + 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x64, 0x61, + 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, + 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x51, 0x0a, 0x0e, 0x53, + 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x12, 0x1d, 0x2e, + 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, + 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x64, + 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, + 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x53, + 0x0a, 0x10, 0x44, 0x65, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, + 0x6b, 0x73, 0x12, 0x1d, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x6c, 0x65, + 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x1e, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x6c, 0x65, 0x63, + 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x0b, 0x44, 0x65, 0x62, 0x75, 0x67, 0x42, 0x75, 0x6e, 0x64, + 0x6c, 0x65, 0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x62, 0x75, + 0x67, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, + 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x62, 0x75, 0x67, 0x42, 0x75, 0x6e, + 0x64, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a, + 0x0b, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x1a, 0x2e, 0x64, + 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, + 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, + 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x0b, 0x53, 0x65, 0x74, 0x4c, 0x6f, + 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, + 0x53, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x74, 0x4c, + 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, + 0x00, 0x12, 0x45, 0x0a, 0x0a, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x12, + 0x19, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, 0x61, + 0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x64, 0x61, 0x65, + 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x45, 0x0a, 0x0a, 0x43, 0x6c, 0x65, 0x61, + 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x19, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, + 0x43, 0x6c, 0x65, 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x43, 0x6c, 0x65, 0x61, 0x6e, + 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, + 0x48, 0x0a, 0x0b, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x1a, + 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x74, + 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x65, + 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x6f, 0x0a, 0x18, 0x53, 0x65, 0x74, + 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x50, 0x65, 0x72, 0x73, 0x69, 0x73, + 0x74, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x27, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, + 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x50, 0x65, 0x72, 0x73, + 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x28, + 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, + 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x50, 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x0b, 0x54, 0x72, + 0x61, 0x63, 0x65, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, + 0x6f, 0x6e, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x65, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x54, + 0x72, 0x61, 0x63, 0x65, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 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 ( @@ -3016,7 +3394,7 @@ func file_daemon_proto_rawDescGZIP() []byte { } var file_daemon_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_daemon_proto_msgTypes = make([]protoimpl.MessageInfo, 41) +var file_daemon_proto_msgTypes = make([]protoimpl.MessageInfo, 45) var file_daemon_proto_goTypes = []interface{}{ (LogLevel)(0), // 0: daemon.LogLevel (*LoginRequest)(nil), // 1: daemon.LoginRequest @@ -3059,16 +3437,20 @@ var file_daemon_proto_goTypes = []interface{}{ (*DeleteStateResponse)(nil), // 38: daemon.DeleteStateResponse (*SetNetworkMapPersistenceRequest)(nil), // 39: daemon.SetNetworkMapPersistenceRequest (*SetNetworkMapPersistenceResponse)(nil), // 40: daemon.SetNetworkMapPersistenceResponse - nil, // 41: daemon.Network.ResolvedIPsEntry - (*durationpb.Duration)(nil), // 42: google.protobuf.Duration - (*timestamppb.Timestamp)(nil), // 43: google.protobuf.Timestamp + (*TCPFlags)(nil), // 41: daemon.TCPFlags + (*TracePacketRequest)(nil), // 42: daemon.TracePacketRequest + (*TraceStage)(nil), // 43: daemon.TraceStage + (*TracePacketResponse)(nil), // 44: daemon.TracePacketResponse + nil, // 45: daemon.Network.ResolvedIPsEntry + (*durationpb.Duration)(nil), // 46: google.protobuf.Duration + (*timestamppb.Timestamp)(nil), // 47: google.protobuf.Timestamp } var file_daemon_proto_depIdxs = []int32{ - 42, // 0: daemon.LoginRequest.dnsRouteInterval:type_name -> google.protobuf.Duration + 46, // 0: daemon.LoginRequest.dnsRouteInterval:type_name -> google.protobuf.Duration 19, // 1: daemon.StatusResponse.fullStatus:type_name -> daemon.FullStatus - 43, // 2: daemon.PeerState.connStatusUpdate:type_name -> google.protobuf.Timestamp - 43, // 3: daemon.PeerState.lastWireguardHandshake:type_name -> google.protobuf.Timestamp - 42, // 4: daemon.PeerState.latency:type_name -> google.protobuf.Duration + 47, // 2: daemon.PeerState.connStatusUpdate:type_name -> google.protobuf.Timestamp + 47, // 3: daemon.PeerState.lastWireguardHandshake:type_name -> google.protobuf.Timestamp + 46, // 4: daemon.PeerState.latency:type_name -> google.protobuf.Duration 16, // 5: daemon.FullStatus.managementState:type_name -> daemon.ManagementState 15, // 6: daemon.FullStatus.signalState:type_name -> daemon.SignalState 14, // 7: daemon.FullStatus.localPeerState:type_name -> daemon.LocalPeerState @@ -3076,48 +3458,52 @@ var file_daemon_proto_depIdxs = []int32{ 17, // 9: daemon.FullStatus.relays:type_name -> daemon.RelayState 18, // 10: daemon.FullStatus.dns_servers:type_name -> daemon.NSGroupState 25, // 11: daemon.ListNetworksResponse.routes:type_name -> daemon.Network - 41, // 12: daemon.Network.resolvedIPs:type_name -> daemon.Network.ResolvedIPsEntry + 45, // 12: daemon.Network.resolvedIPs:type_name -> daemon.Network.ResolvedIPsEntry 0, // 13: daemon.GetLogLevelResponse.level:type_name -> daemon.LogLevel 0, // 14: daemon.SetLogLevelRequest.level:type_name -> daemon.LogLevel 32, // 15: daemon.ListStatesResponse.states:type_name -> daemon.State - 24, // 16: daemon.Network.ResolvedIPsEntry.value:type_name -> daemon.IPList - 1, // 17: daemon.DaemonService.Login:input_type -> daemon.LoginRequest - 3, // 18: daemon.DaemonService.WaitSSOLogin:input_type -> daemon.WaitSSOLoginRequest - 5, // 19: daemon.DaemonService.Up:input_type -> daemon.UpRequest - 7, // 20: daemon.DaemonService.Status:input_type -> daemon.StatusRequest - 9, // 21: daemon.DaemonService.Down:input_type -> daemon.DownRequest - 11, // 22: daemon.DaemonService.GetConfig:input_type -> daemon.GetConfigRequest - 20, // 23: daemon.DaemonService.ListNetworks:input_type -> daemon.ListNetworksRequest - 22, // 24: daemon.DaemonService.SelectNetworks:input_type -> daemon.SelectNetworksRequest - 22, // 25: daemon.DaemonService.DeselectNetworks:input_type -> daemon.SelectNetworksRequest - 26, // 26: daemon.DaemonService.DebugBundle:input_type -> daemon.DebugBundleRequest - 28, // 27: daemon.DaemonService.GetLogLevel:input_type -> daemon.GetLogLevelRequest - 30, // 28: daemon.DaemonService.SetLogLevel:input_type -> daemon.SetLogLevelRequest - 33, // 29: daemon.DaemonService.ListStates:input_type -> daemon.ListStatesRequest - 35, // 30: daemon.DaemonService.CleanState:input_type -> daemon.CleanStateRequest - 37, // 31: daemon.DaemonService.DeleteState:input_type -> daemon.DeleteStateRequest - 39, // 32: daemon.DaemonService.SetNetworkMapPersistence:input_type -> daemon.SetNetworkMapPersistenceRequest - 2, // 33: daemon.DaemonService.Login:output_type -> daemon.LoginResponse - 4, // 34: daemon.DaemonService.WaitSSOLogin:output_type -> daemon.WaitSSOLoginResponse - 6, // 35: daemon.DaemonService.Up:output_type -> daemon.UpResponse - 8, // 36: daemon.DaemonService.Status:output_type -> daemon.StatusResponse - 10, // 37: daemon.DaemonService.Down:output_type -> daemon.DownResponse - 12, // 38: daemon.DaemonService.GetConfig:output_type -> daemon.GetConfigResponse - 21, // 39: daemon.DaemonService.ListNetworks:output_type -> daemon.ListNetworksResponse - 23, // 40: daemon.DaemonService.SelectNetworks:output_type -> daemon.SelectNetworksResponse - 23, // 41: daemon.DaemonService.DeselectNetworks:output_type -> daemon.SelectNetworksResponse - 27, // 42: daemon.DaemonService.DebugBundle:output_type -> daemon.DebugBundleResponse - 29, // 43: daemon.DaemonService.GetLogLevel:output_type -> daemon.GetLogLevelResponse - 31, // 44: daemon.DaemonService.SetLogLevel:output_type -> daemon.SetLogLevelResponse - 34, // 45: daemon.DaemonService.ListStates:output_type -> daemon.ListStatesResponse - 36, // 46: daemon.DaemonService.CleanState:output_type -> daemon.CleanStateResponse - 38, // 47: daemon.DaemonService.DeleteState:output_type -> daemon.DeleteStateResponse - 40, // 48: daemon.DaemonService.SetNetworkMapPersistence:output_type -> daemon.SetNetworkMapPersistenceResponse - 33, // [33:49] is the sub-list for method output_type - 17, // [17:33] is the sub-list for method input_type - 17, // [17:17] is the sub-list for extension type_name - 17, // [17:17] is the sub-list for extension extendee - 0, // [0:17] is the sub-list for field type_name + 41, // 16: daemon.TracePacketRequest.tcp_flags:type_name -> daemon.TCPFlags + 43, // 17: daemon.TracePacketResponse.stages:type_name -> daemon.TraceStage + 24, // 18: daemon.Network.ResolvedIPsEntry.value:type_name -> daemon.IPList + 1, // 19: daemon.DaemonService.Login:input_type -> daemon.LoginRequest + 3, // 20: daemon.DaemonService.WaitSSOLogin:input_type -> daemon.WaitSSOLoginRequest + 5, // 21: daemon.DaemonService.Up:input_type -> daemon.UpRequest + 7, // 22: daemon.DaemonService.Status:input_type -> daemon.StatusRequest + 9, // 23: daemon.DaemonService.Down:input_type -> daemon.DownRequest + 11, // 24: daemon.DaemonService.GetConfig:input_type -> daemon.GetConfigRequest + 20, // 25: daemon.DaemonService.ListNetworks:input_type -> daemon.ListNetworksRequest + 22, // 26: daemon.DaemonService.SelectNetworks:input_type -> daemon.SelectNetworksRequest + 22, // 27: daemon.DaemonService.DeselectNetworks:input_type -> daemon.SelectNetworksRequest + 26, // 28: daemon.DaemonService.DebugBundle:input_type -> daemon.DebugBundleRequest + 28, // 29: daemon.DaemonService.GetLogLevel:input_type -> daemon.GetLogLevelRequest + 30, // 30: daemon.DaemonService.SetLogLevel:input_type -> daemon.SetLogLevelRequest + 33, // 31: daemon.DaemonService.ListStates:input_type -> daemon.ListStatesRequest + 35, // 32: daemon.DaemonService.CleanState:input_type -> daemon.CleanStateRequest + 37, // 33: daemon.DaemonService.DeleteState:input_type -> daemon.DeleteStateRequest + 39, // 34: daemon.DaemonService.SetNetworkMapPersistence:input_type -> daemon.SetNetworkMapPersistenceRequest + 42, // 35: daemon.DaemonService.TracePacket:input_type -> daemon.TracePacketRequest + 2, // 36: daemon.DaemonService.Login:output_type -> daemon.LoginResponse + 4, // 37: daemon.DaemonService.WaitSSOLogin:output_type -> daemon.WaitSSOLoginResponse + 6, // 38: daemon.DaemonService.Up:output_type -> daemon.UpResponse + 8, // 39: daemon.DaemonService.Status:output_type -> daemon.StatusResponse + 10, // 40: daemon.DaemonService.Down:output_type -> daemon.DownResponse + 12, // 41: daemon.DaemonService.GetConfig:output_type -> daemon.GetConfigResponse + 21, // 42: daemon.DaemonService.ListNetworks:output_type -> daemon.ListNetworksResponse + 23, // 43: daemon.DaemonService.SelectNetworks:output_type -> daemon.SelectNetworksResponse + 23, // 44: daemon.DaemonService.DeselectNetworks:output_type -> daemon.SelectNetworksResponse + 27, // 45: daemon.DaemonService.DebugBundle:output_type -> daemon.DebugBundleResponse + 29, // 46: daemon.DaemonService.GetLogLevel:output_type -> daemon.GetLogLevelResponse + 31, // 47: daemon.DaemonService.SetLogLevel:output_type -> daemon.SetLogLevelResponse + 34, // 48: daemon.DaemonService.ListStates:output_type -> daemon.ListStatesResponse + 36, // 49: daemon.DaemonService.CleanState:output_type -> daemon.CleanStateResponse + 38, // 50: daemon.DaemonService.DeleteState:output_type -> daemon.DeleteStateResponse + 40, // 51: daemon.DaemonService.SetNetworkMapPersistence:output_type -> daemon.SetNetworkMapPersistenceResponse + 44, // 52: daemon.DaemonService.TracePacket:output_type -> daemon.TracePacketResponse + 36, // [36:53] is the sub-list for method output_type + 19, // [19:36] is the sub-list for method input_type + 19, // [19:19] is the sub-list for extension type_name + 19, // [19:19] is the sub-list for extension extendee + 0, // [0:19] is the sub-list for field type_name } func init() { file_daemon_proto_init() } @@ -3606,15 +3992,65 @@ func file_daemon_proto_init() { return nil } } + file_daemon_proto_msgTypes[40].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*TCPFlags); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_daemon_proto_msgTypes[41].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*TracePacketRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_daemon_proto_msgTypes[42].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*TraceStage); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_daemon_proto_msgTypes[43].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*TracePacketResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } } file_daemon_proto_msgTypes[0].OneofWrappers = []interface{}{} + file_daemon_proto_msgTypes[41].OneofWrappers = []interface{}{} + file_daemon_proto_msgTypes[42].OneofWrappers = []interface{}{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_daemon_proto_rawDesc, NumEnums: 1, - NumMessages: 41, + NumMessages: 45, NumExtensions: 0, NumServices: 1, }, diff --git a/client/proto/daemon.proto b/client/proto/daemon.proto index 8db3add08..412449076 100644 --- a/client/proto/daemon.proto +++ b/client/proto/daemon.proto @@ -57,6 +57,8 @@ service DaemonService { // SetNetworkMapPersistence enables or disables network map persistence rpc SetNetworkMapPersistence(SetNetworkMapPersistenceRequest) returns (SetNetworkMapPersistenceResponse) {} + + rpc TracePacket(TracePacketRequest) returns (TracePacketResponse) {} } @@ -356,3 +358,36 @@ message SetNetworkMapPersistenceRequest { } message SetNetworkMapPersistenceResponse {} + +message TCPFlags { + bool syn = 1; + bool ack = 2; + bool fin = 3; + bool rst = 4; + bool psh = 5; + bool urg = 6; +} + +message TracePacketRequest { + string source_ip = 1; + string destination_ip = 2; + string protocol = 3; + uint32 source_port = 4; + uint32 destination_port = 5; + string direction = 6; + optional TCPFlags tcp_flags = 7; + optional uint32 icmp_type = 8; + optional uint32 icmp_code = 9; +} + +message TraceStage { + string name = 1; + string message = 2; + bool allowed = 3; + optional string forwarding_details = 4; +} + +message TracePacketResponse { + repeated TraceStage stages = 1; + bool final_disposition = 2; +} diff --git a/client/proto/daemon_grpc.pb.go b/client/proto/daemon_grpc.pb.go index 39424aee9..9dcb543a8 100644 --- a/client/proto/daemon_grpc.pb.go +++ b/client/proto/daemon_grpc.pb.go @@ -51,6 +51,7 @@ type DaemonServiceClient interface { DeleteState(ctx context.Context, in *DeleteStateRequest, opts ...grpc.CallOption) (*DeleteStateResponse, error) // SetNetworkMapPersistence enables or disables network map persistence SetNetworkMapPersistence(ctx context.Context, in *SetNetworkMapPersistenceRequest, opts ...grpc.CallOption) (*SetNetworkMapPersistenceResponse, error) + TracePacket(ctx context.Context, in *TracePacketRequest, opts ...grpc.CallOption) (*TracePacketResponse, error) } type daemonServiceClient struct { @@ -205,6 +206,15 @@ func (c *daemonServiceClient) SetNetworkMapPersistence(ctx context.Context, in * return out, nil } +func (c *daemonServiceClient) TracePacket(ctx context.Context, in *TracePacketRequest, opts ...grpc.CallOption) (*TracePacketResponse, error) { + out := new(TracePacketResponse) + err := c.cc.Invoke(ctx, "/daemon.DaemonService/TracePacket", 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 @@ -242,6 +252,7 @@ type DaemonServiceServer interface { DeleteState(context.Context, *DeleteStateRequest) (*DeleteStateResponse, error) // SetNetworkMapPersistence enables or disables network map persistence SetNetworkMapPersistence(context.Context, *SetNetworkMapPersistenceRequest) (*SetNetworkMapPersistenceResponse, error) + TracePacket(context.Context, *TracePacketRequest) (*TracePacketResponse, error) mustEmbedUnimplementedDaemonServiceServer() } @@ -297,6 +308,9 @@ func (UnimplementedDaemonServiceServer) DeleteState(context.Context, *DeleteStat func (UnimplementedDaemonServiceServer) SetNetworkMapPersistence(context.Context, *SetNetworkMapPersistenceRequest) (*SetNetworkMapPersistenceResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method SetNetworkMapPersistence not implemented") } +func (UnimplementedDaemonServiceServer) TracePacket(context.Context, *TracePacketRequest) (*TracePacketResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method TracePacket not implemented") +} func (UnimplementedDaemonServiceServer) mustEmbedUnimplementedDaemonServiceServer() {} // UnsafeDaemonServiceServer may be embedded to opt out of forward compatibility for this service. @@ -598,6 +612,24 @@ func _DaemonService_SetNetworkMapPersistence_Handler(srv interface{}, ctx contex return interceptor(ctx, in, info, handler) } +func _DaemonService_TracePacket_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(TracePacketRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DaemonServiceServer).TracePacket(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/daemon.DaemonService/TracePacket", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DaemonServiceServer).TracePacket(ctx, req.(*TracePacketRequest)) + } + 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) @@ -669,6 +701,10 @@ var DaemonService_ServiceDesc = grpc.ServiceDesc{ MethodName: "SetNetworkMapPersistence", Handler: _DaemonService_SetNetworkMapPersistence_Handler, }, + { + MethodName: "TracePacket", + Handler: _DaemonService_TracePacket_Handler, + }, }, Streams: []grpc.StreamDesc{}, Metadata: "daemon.proto", diff --git a/client/server/debug.go b/client/server/debug.go index a37195b29..749220d62 100644 --- a/client/server/debug.go +++ b/client/server/debug.go @@ -538,7 +538,24 @@ func (s *Server) SetLogLevel(_ context.Context, req *proto.SetLogLevelRequest) ( } log.SetLevel(level) + + if s.connectClient == nil { + return nil, fmt.Errorf("connect client not initialized") + } + engine := s.connectClient.Engine() + if engine == nil { + return nil, fmt.Errorf("engine not initialized") + } + + fwManager := engine.GetFirewallManager() + if fwManager == nil { + return nil, fmt.Errorf("firewall manager not initialized") + } + + fwManager.SetLogLevel(level) + log.Infof("Log level set to %s", level.String()) + return &proto.SetLogLevelResponse{}, nil } diff --git a/client/server/trace.go b/client/server/trace.go new file mode 100644 index 000000000..66b83d8cf --- /dev/null +++ b/client/server/trace.go @@ -0,0 +1,123 @@ +package server + +import ( + "context" + "fmt" + "net" + + fw "github.com/netbirdio/netbird/client/firewall/manager" + "github.com/netbirdio/netbird/client/firewall/uspfilter" + "github.com/netbirdio/netbird/client/proto" +) + +type packetTracer interface { + TracePacketFromBuilder(builder *uspfilter.PacketBuilder) (*uspfilter.PacketTrace, error) +} + +func (s *Server) TracePacket(_ context.Context, req *proto.TracePacketRequest) (*proto.TracePacketResponse, error) { + s.mutex.Lock() + defer s.mutex.Unlock() + + if s.connectClient == nil { + return nil, fmt.Errorf("connect client not initialized") + } + engine := s.connectClient.Engine() + if engine == nil { + return nil, fmt.Errorf("engine not initialized") + } + + fwManager := engine.GetFirewallManager() + if fwManager == nil { + return nil, fmt.Errorf("firewall manager not initialized") + } + + tracer, ok := fwManager.(packetTracer) + if !ok { + return nil, fmt.Errorf("firewall manager does not support packet tracing") + } + + srcIP := net.ParseIP(req.GetSourceIp()) + if req.GetSourceIp() == "self" { + srcIP = engine.GetWgAddr() + } + + dstIP := net.ParseIP(req.GetDestinationIp()) + if req.GetDestinationIp() == "self" { + dstIP = engine.GetWgAddr() + } + + if srcIP == nil || dstIP == nil { + return nil, fmt.Errorf("invalid IP address") + } + + var tcpState *uspfilter.TCPState + if flags := req.GetTcpFlags(); flags != nil { + tcpState = &uspfilter.TCPState{ + SYN: flags.GetSyn(), + ACK: flags.GetAck(), + FIN: flags.GetFin(), + RST: flags.GetRst(), + PSH: flags.GetPsh(), + URG: flags.GetUrg(), + } + } + + var dir fw.RuleDirection + switch req.GetDirection() { + case "in": + dir = fw.RuleDirectionIN + case "out": + dir = fw.RuleDirectionOUT + default: + return nil, fmt.Errorf("invalid direction") + } + + var protocol fw.Protocol + switch req.GetProtocol() { + case "tcp": + protocol = fw.ProtocolTCP + case "udp": + protocol = fw.ProtocolUDP + case "icmp": + protocol = fw.ProtocolICMP + default: + return nil, fmt.Errorf("invalid protocolcol") + } + + builder := &uspfilter.PacketBuilder{ + SrcIP: srcIP, + DstIP: dstIP, + Protocol: protocol, + SrcPort: uint16(req.GetSourcePort()), + DstPort: uint16(req.GetDestinationPort()), + Direction: dir, + TCPState: tcpState, + ICMPType: uint8(req.GetIcmpType()), + ICMPCode: uint8(req.GetIcmpCode()), + } + trace, err := tracer.TracePacketFromBuilder(builder) + if err != nil { + return nil, fmt.Errorf("trace packet: %w", err) + } + + resp := &proto.TracePacketResponse{} + + for _, result := range trace.Results { + stage := &proto.TraceStage{ + Name: result.Stage.String(), + Message: result.Message, + Allowed: result.Allowed, + } + if result.ForwarderAction != nil { + details := fmt.Sprintf("%s to %s", result.ForwarderAction.Action, result.ForwarderAction.RemoteAddr) + stage.ForwardingDetails = &details + } + resp.Stages = append(resp.Stages, stage) + } + + if len(trace.Results) > 0 { + resp.FinalDisposition = trace.Results[len(trace.Results)-1].Allowed + } + + return resp, nil +} diff --git a/go.mod b/go.mod index 77d570662..3e1208e5a 100644 --- a/go.mod +++ b/go.mod @@ -102,6 +102,7 @@ require ( gorm.io/driver/postgres v1.5.7 gorm.io/driver/sqlite v1.5.7 gorm.io/gorm v1.25.12 + gvisor.dev/gvisor v0.0.0-20231020174304-b8a429915ff1 ) require ( @@ -237,7 +238,6 @@ require ( gopkg.in/square/go-jose.v2 v2.6.0 // indirect gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637 // indirect - gvisor.dev/gvisor v0.0.0-20231020174304-db3d49b921f9 // indirect k8s.io/apimachinery v0.26.2 // indirect ) @@ -245,7 +245,7 @@ replace github.com/kardianos/service => github.com/netbirdio/service v0.0.0-2024 replace github.com/getlantern/systray => github.com/netbirdio/systray v0.0.0-20231030152038-ef1ed2a27949 -replace golang.zx2c4.com/wireguard => github.com/netbirdio/wireguard-go v0.0.0-20241125150134-f9cdce5e32e9 +replace golang.zx2c4.com/wireguard => github.com/netbirdio/wireguard-go v0.0.0-20241230120307-6a676aebaaf6 replace github.com/cloudflare/circl => github.com/cunicu/circl v0.0.0-20230801113412-fec58fc7b5f6 diff --git a/go.sum b/go.sum index 4b9e90eba..54b77dbee 100644 --- a/go.sum +++ b/go.sum @@ -535,8 +535,8 @@ github.com/netbirdio/service v0.0.0-20240911161631-f62744f42502 h1:3tHlFmhTdX9ax github.com/netbirdio/service v0.0.0-20240911161631-f62744f42502/go.mod h1:CIMRFEJVL+0DS1a3Nx06NaMn4Dz63Ng6O7dl0qH0zVM= github.com/netbirdio/signal-dispatcher/dispatcher v0.0.0-20241010133937-e0df50df217d h1:bRq5TKgC7Iq20pDiuC54yXaWnAVeS5PdGpSokFTlR28= github.com/netbirdio/signal-dispatcher/dispatcher v0.0.0-20241010133937-e0df50df217d/go.mod h1:5/sjFmLb8O96B5737VCqhHyGRzNFIaN/Bu7ZodXc3qQ= -github.com/netbirdio/wireguard-go v0.0.0-20241125150134-f9cdce5e32e9 h1:Pu/7EukijT09ynHUOzQYW7cC3M/BKU8O4qyN/TvTGoY= -github.com/netbirdio/wireguard-go v0.0.0-20241125150134-f9cdce5e32e9/go.mod h1:tkCQ4FQXmpAgYVh++1cq16/dH4QJtmvpRv19DWGAHSA= +github.com/netbirdio/wireguard-go v0.0.0-20241230120307-6a676aebaaf6 h1:X5h5QgP7uHAv78FWgHV8+WYLjHxK9v3ilkVXT1cpCrQ= +github.com/netbirdio/wireguard-go v0.0.0-20241230120307-6a676aebaaf6/go.mod h1:tkCQ4FQXmpAgYVh++1cq16/dH4QJtmvpRv19DWGAHSA= github.com/nicksnyder/go-i18n/v2 v2.4.0 h1:3IcvPOAvnCKwNm0TB0dLDTuawWEj+ax/RERNC+diLMM= github.com/nicksnyder/go-i18n/v2 v2.4.0/go.mod h1:nxYSZE9M0bf3Y70gPQjN9ha7XNHX7gMc814+6wVyEI4= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= @@ -1250,8 +1250,8 @@ gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= gotest.tools/v3 v3.5.0 h1:Ljk6PdHdOhAb5aDMWXjDLMMhph+BpztA4v1QdqEW2eY= gotest.tools/v3 v3.5.0/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= -gvisor.dev/gvisor v0.0.0-20231020174304-db3d49b921f9 h1:sCEaoA7ZmkuFwa2IR61pl4+RYZPwCJOiaSYT0k+BRf8= -gvisor.dev/gvisor v0.0.0-20231020174304-db3d49b921f9/go.mod h1:8hmigyCdYtw5xJGfQDJzSH5Ju8XEIDBnpyi8+O6GRt8= +gvisor.dev/gvisor v0.0.0-20231020174304-b8a429915ff1 h1:qDCwdCWECGnwQSQC01Dpnp09fRHxJs9PbktotUqG+hs= +gvisor.dev/gvisor v0.0.0-20231020174304-b8a429915ff1/go.mod h1:8hmigyCdYtw5xJGfQDJzSH5Ju8XEIDBnpyi8+O6GRt8= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= From 58b2eb4b92254ccd4bb921d95f1662d6bd474e2e Mon Sep 17 00:00:00 2001 From: ransomware <4thel00z@gmail.com> Date: Fri, 7 Feb 2025 15:05:41 +0100 Subject: [PATCH 51/92] [signal] Fix context propagation in signal server (#3251) --- signal/server/signal.go | 97 ++++++++++++++++++++++------------------- 1 file changed, 51 insertions(+), 46 deletions(-) diff --git a/signal/server/signal.go b/signal/server/signal.go index 305fd052b..abc1c367b 100644 --- a/signal/server/signal.go +++ b/signal/server/signal.go @@ -52,13 +52,13 @@ func NewServer(ctx context.Context, meter metric.Meter) (*Server, error) { return nil, fmt.Errorf("creating app metrics: %v", err) } - dispatcher, err := dispatcher.NewDispatcher(ctx, meter) + d, err := dispatcher.NewDispatcher(ctx, meter) if err != nil { return nil, fmt.Errorf("creating dispatcher: %v", err) } s := &Server{ - dispatcher: dispatcher, + dispatcher: d, registry: peer.NewRegistry(appMetrics), metrics: appMetrics, } @@ -75,7 +75,7 @@ func (s *Server) Send(ctx context.Context, msg *proto.EncryptedMessage) (*proto. return &proto.EncryptedMessage{}, nil } - return s.dispatcher.SendMessage(context.Background(), msg) + return s.dispatcher.SendMessage(ctx, msg) } // ConnectStream connects to the exchange stream @@ -98,76 +98,81 @@ func (s *Server) ConnectStream(stream proto.SignalExchange_ConnectStreamServer) log.Debugf("peer connected [%s] [streamID %d] ", p.Id, p.StreamID) for { - // read incoming messages - msg, err := stream.Recv() - if err == io.EOF { - break - } else if err != nil { - return err - } + select { + case <-stream.Context().Done(): + log.Debugf("stream closed for peer [%s] [streamID %d] due to context cancellation", p.Id, p.StreamID) + return stream.Context().Err() + default: + // read incoming messages + msg, err := stream.Recv() + if err == io.EOF { + break + } else if err != nil { + return err + } - log.Debugf("Received a response from peer [%s] to peer [%s]", msg.Key, msg.RemoteKey) + log.Debugf("Received a response from peer [%s] to peer [%s]", msg.Key, msg.RemoteKey) - _, err = s.dispatcher.SendMessage(stream.Context(), msg) - if err != nil { - log.Debugf("error while sending message from peer [%s] to peer [%s] %v", msg.Key, msg.RemoteKey, err) + _, err = s.dispatcher.SendMessage(stream.Context(), msg) + if err != nil { + log.Debugf("error while sending message from peer [%s] to peer [%s] %v", msg.Key, msg.RemoteKey, err) + } } } - - <-stream.Context().Done() - return stream.Context().Err() } func (s *Server) RegisterPeer(stream proto.SignalExchange_ConnectStreamServer) (*peer.Peer, error) { log.Debugf("registering new peer") - if meta, hasMeta := metadata.FromIncomingContext(stream.Context()); hasMeta { - if id, found := meta[proto.HeaderId]; found { - p := peer.NewPeer(id[0], stream) - - s.registry.Register(p) - s.dispatcher.ListenForMessages(stream.Context(), p.Id, s.forwardMessageToPeer) - - return p, nil - } else { - s.metrics.RegistrationFailures.Add(stream.Context(), 1, metric.WithAttributes(attribute.String(labelError, labelErrorMissingId))) - return nil, status.Errorf(codes.FailedPrecondition, "missing connection header: "+proto.HeaderId) - } - } else { + meta, hasMeta := metadata.FromIncomingContext(stream.Context()) + if !hasMeta { s.metrics.RegistrationFailures.Add(stream.Context(), 1, metric.WithAttributes(attribute.String(labelError, labelErrorMissingMeta))) return nil, status.Errorf(codes.FailedPrecondition, "missing connection stream meta") } + + id, found := meta[proto.HeaderId] + if !found { + s.metrics.RegistrationFailures.Add(stream.Context(), 1, metric.WithAttributes(attribute.String(labelError, labelErrorMissingId))) + return nil, status.Errorf(codes.FailedPrecondition, "missing connection header: %s", proto.HeaderId) + } + + p := peer.NewPeer(id[0], stream) + s.registry.Register(p) + s.dispatcher.ListenForMessages(stream.Context(), p.Id, s.forwardMessageToPeer) + return p, nil } func (s *Server) DeregisterPeer(p *peer.Peer) { log.Debugf("peer disconnected [%s] [streamID %d] ", p.Id, p.StreamID) s.registry.Deregister(p) - s.metrics.PeerConnectionDuration.Record(p.Stream.Context(), int64(time.Since(p.RegisteredAt).Seconds())) } func (s *Server) forwardMessageToPeer(ctx context.Context, msg *proto.EncryptedMessage) { log.Debugf("forwarding a new message from peer [%s] to peer [%s]", msg.Key, msg.RemoteKey) - getRegistrationStart := time.Now() // lookup the target peer where the message is going to - if dstPeer, found := s.registry.Get(msg.RemoteKey); found { - s.metrics.GetRegistrationDelay.Record(ctx, float64(time.Since(getRegistrationStart).Nanoseconds())/1e6, metric.WithAttributes(attribute.String(labelType, labelTypeStream), attribute.String(labelRegistrationStatus, labelRegistrationFound))) - start := time.Now() - // forward the message to the target peer - if err := dstPeer.Stream.Send(msg); err != nil { - log.Warnf("error while forwarding message from peer [%s] to peer [%s] %v", msg.Key, msg.RemoteKey, err) - // todo respond to the sender? - s.metrics.MessageForwardFailures.Add(ctx, 1, metric.WithAttributes(attribute.String(labelType, labelTypeError))) - } else { - // in milliseconds - s.metrics.MessageForwardLatency.Record(ctx, float64(time.Since(start).Nanoseconds())/1e6, metric.WithAttributes(attribute.String(labelType, labelTypeStream))) - s.metrics.MessagesForwarded.Add(ctx, 1) - } - } else { + dstPeer, found := s.registry.Get(msg.RemoteKey) + + if !found { s.metrics.GetRegistrationDelay.Record(ctx, float64(time.Since(getRegistrationStart).Nanoseconds())/1e6, metric.WithAttributes(attribute.String(labelType, labelTypeStream), attribute.String(labelRegistrationStatus, labelRegistrationNotFound))) s.metrics.MessageForwardFailures.Add(ctx, 1, metric.WithAttributes(attribute.String(labelType, labelTypeNotConnected))) log.Debugf("message from peer [%s] can't be forwarded to peer [%s] because destination peer is not connected", msg.Key, msg.RemoteKey) // todo respond to the sender? } + + s.metrics.GetRegistrationDelay.Record(ctx, float64(time.Since(getRegistrationStart).Nanoseconds())/1e6, metric.WithAttributes(attribute.String(labelType, labelTypeStream), attribute.String(labelRegistrationStatus, labelRegistrationFound))) + start := time.Now() + + // forward the message to the target peer + if err := dstPeer.Stream.Send(msg); err != nil { + log.Warnf("error while forwarding message from peer [%s] to peer [%s] %v", msg.Key, msg.RemoteKey, err) + // todo respond to the sender? + s.metrics.MessageForwardFailures.Add(ctx, 1, metric.WithAttributes(attribute.String(labelType, labelTypeError))) + return + } + + // in milliseconds + s.metrics.MessageForwardLatency.Record(ctx, float64(time.Since(start).Nanoseconds())/1e6, metric.WithAttributes(attribute.String(labelType, labelTypeStream))) + s.metrics.MessagesForwarded.Add(ctx, 1) } From 5953b43ead2e0059807c3d295a2f9b324f0773b3 Mon Sep 17 00:00:00 2001 From: Zoltan Papp Date: Mon, 10 Feb 2025 10:32:50 +0100 Subject: [PATCH 52/92] [client, relay] Fix/wg watch (#3261) Fix WireGuard watcher related issues - Fix race handling between TURN and Relayed reconnection - Move the WgWatcher logic to separate struct - Handle timeouts in a more defensive way - Fix initial Relay client reconnection to the home server --- client/internal/peer/conn.go | 49 +++----- client/internal/peer/guard/guard.go | 36 ++---- client/internal/peer/wg_watcher.go | 154 ++++++++++++++++++++++++ client/internal/peer/wg_watcher_test.go | 98 +++++++++++++++ client/internal/peer/worker_ice.go | 17 +-- client/internal/peer/worker_relay.go | 127 ++++--------------- relay/client/client.go | 18 --- relay/client/guard.go | 71 +++++++---- relay/client/manager.go | 10 +- 9 files changed, 365 insertions(+), 215 deletions(-) create mode 100644 client/internal/peer/wg_watcher.go create mode 100644 client/internal/peer/wg_watcher_test.go diff --git a/client/internal/peer/conn.go b/client/internal/peer/conn.go index b8cb2582f..7caafa53d 100644 --- a/client/internal/peer/conn.go +++ b/client/internal/peer/conn.go @@ -32,8 +32,8 @@ const ( defaultWgKeepAlive = 25 * time.Second connPriorityRelay ConnPriority = 1 - connPriorityICETurn ConnPriority = 1 - connPriorityICEP2P ConnPriority = 2 + connPriorityICETurn ConnPriority = 2 + connPriorityICEP2P ConnPriority = 3 ) type WgConfig struct { @@ -66,14 +66,6 @@ type ConnConfig struct { ICEConfig icemaker.Config } -type WorkerCallbacks struct { - OnRelayReadyCallback func(info RelayConnInfo) - OnRelayStatusChanged func(ConnStatus) - - OnICEConnReadyCallback func(ConnPriority, ICEConnInfo) - OnICEStatusChanged func(ConnStatus) -} - type Conn struct { log *log.Entry mu sync.Mutex @@ -135,21 +127,11 @@ func NewConn(engineCtx context.Context, config ConnConfig, statusRecorder *Statu semaphore: semaphore, } - rFns := WorkerRelayCallbacks{ - OnConnReady: conn.relayConnectionIsReady, - OnDisconnected: conn.onWorkerRelayStateDisconnected, - } - - wFns := WorkerICECallbacks{ - OnConnReady: conn.iCEConnectionIsReady, - OnStatusChanged: conn.onWorkerICEStateDisconnected, - } - ctrl := isController(config) - conn.workerRelay = NewWorkerRelay(connLog, ctrl, config, relayManager, rFns) + conn.workerRelay = NewWorkerRelay(connLog, ctrl, config, conn, relayManager) relayIsSupportedLocally := conn.workerRelay.RelayIsSupportedLocally() - conn.workerICE, err = NewWorkerICE(ctx, connLog, config, signaler, iFaceDiscover, statusRecorder, relayIsSupportedLocally, wFns) + conn.workerICE, err = NewWorkerICE(ctx, connLog, config, conn, signaler, iFaceDiscover, statusRecorder, relayIsSupportedLocally) if err != nil { return nil, err } @@ -304,7 +286,7 @@ func (conn *Conn) GetKey() string { } // configureConnection starts proxying traffic from/to local Wireguard and sets connection status to StatusConnected -func (conn *Conn) iCEConnectionIsReady(priority ConnPriority, iceConnInfo ICEConnInfo) { +func (conn *Conn) onICEConnectionIsReady(priority ConnPriority, iceConnInfo ICEConnInfo) { conn.mu.Lock() defer conn.mu.Unlock() @@ -376,7 +358,7 @@ func (conn *Conn) iCEConnectionIsReady(priority ConnPriority, iceConnInfo ICECon } // todo review to make sense to handle connecting and disconnected status also? -func (conn *Conn) onWorkerICEStateDisconnected(newState ConnStatus) { +func (conn *Conn) onICEStateDisconnected() { conn.mu.Lock() defer conn.mu.Unlock() @@ -384,7 +366,7 @@ func (conn *Conn) onWorkerICEStateDisconnected(newState ConnStatus) { return } - conn.log.Tracef("ICE connection state changed to %s", newState) + conn.log.Tracef("ICE connection state changed to disconnected") if conn.wgProxyICE != nil { if err := conn.wgProxyICE.CloseConn(); err != nil { @@ -404,10 +386,11 @@ func (conn *Conn) onWorkerICEStateDisconnected(newState ConnStatus) { conn.currentConnPriority = connPriorityRelay } - changed := conn.statusICE.Get() != newState && newState != StatusConnecting - conn.statusICE.Set(newState) - - conn.guard.SetICEConnDisconnected(changed) + changed := conn.statusICE.Get() != StatusDisconnected + if changed { + conn.guard.SetICEConnDisconnected() + } + conn.statusICE.Set(StatusDisconnected) peerState := State{ PubKey: conn.config.Key, @@ -422,7 +405,7 @@ func (conn *Conn) onWorkerICEStateDisconnected(newState ConnStatus) { } } -func (conn *Conn) relayConnectionIsReady(rci RelayConnInfo) { +func (conn *Conn) onRelayConnectionIsReady(rci RelayConnInfo) { conn.mu.Lock() defer conn.mu.Unlock() @@ -474,7 +457,7 @@ func (conn *Conn) relayConnectionIsReady(rci RelayConnInfo) { conn.doOnConnected(rci.rosenpassPubKey, rci.rosenpassAddr) } -func (conn *Conn) onWorkerRelayStateDisconnected() { +func (conn *Conn) onRelayDisconnected() { conn.mu.Lock() defer conn.mu.Unlock() @@ -497,8 +480,10 @@ func (conn *Conn) onWorkerRelayStateDisconnected() { } changed := conn.statusRelay.Get() != StatusDisconnected + if changed { + conn.guard.SetRelayedConnDisconnected() + } conn.statusRelay.Set(StatusDisconnected) - conn.guard.SetRelayedConnDisconnected(changed) peerState := State{ PubKey: conn.config.Key, diff --git a/client/internal/peer/guard/guard.go b/client/internal/peer/guard/guard.go index bf3527a62..1fc2b4a4a 100644 --- a/client/internal/peer/guard/guard.go +++ b/client/internal/peer/guard/guard.go @@ -29,8 +29,8 @@ type Guard struct { isConnectedOnAllWay isConnectedFunc timeout time.Duration srWatcher *SRWatcher - relayedConnDisconnected chan bool - iCEConnDisconnected chan bool + relayedConnDisconnected chan struct{} + iCEConnDisconnected chan struct{} } func NewGuard(log *log.Entry, isController bool, isConnectedFn isConnectedFunc, timeout time.Duration, srWatcher *SRWatcher) *Guard { @@ -41,8 +41,8 @@ func NewGuard(log *log.Entry, isController bool, isConnectedFn isConnectedFunc, isConnectedOnAllWay: isConnectedFn, timeout: timeout, srWatcher: srWatcher, - relayedConnDisconnected: make(chan bool, 1), - iCEConnDisconnected: make(chan bool, 1), + relayedConnDisconnected: make(chan struct{}, 1), + iCEConnDisconnected: make(chan struct{}, 1), } } @@ -54,16 +54,16 @@ func (g *Guard) Start(ctx context.Context) { } } -func (g *Guard) SetRelayedConnDisconnected(changed bool) { +func (g *Guard) SetRelayedConnDisconnected() { select { - case g.relayedConnDisconnected <- changed: + case g.relayedConnDisconnected <- struct{}{}: default: } } -func (g *Guard) SetICEConnDisconnected(changed bool) { +func (g *Guard) SetICEConnDisconnected() { select { - case g.iCEConnDisconnected <- changed: + case g.iCEConnDisconnected <- struct{}{}: default: } } @@ -96,19 +96,13 @@ func (g *Guard) reconnectLoopWithRetry(ctx context.Context) { g.triggerOfferSending() } - case changed := <-g.relayedConnDisconnected: - if !changed { - continue - } + case <-g.relayedConnDisconnected: g.log.Debugf("Relay connection changed, reset reconnection ticker") ticker.Stop() ticker = g.prepareExponentTicker(ctx) tickerChannel = ticker.C - case changed := <-g.iCEConnDisconnected: - if !changed { - continue - } + case <-g.iCEConnDisconnected: g.log.Debugf("ICE connection changed, reset reconnection ticker") ticker.Stop() ticker = g.prepareExponentTicker(ctx) @@ -138,16 +132,10 @@ func (g *Guard) listenForDisconnectEvents(ctx context.Context) { g.log.Infof("start listen for reconnect events...") for { select { - case changed := <-g.relayedConnDisconnected: - if !changed { - continue - } + case <-g.relayedConnDisconnected: g.log.Debugf("Relay connection changed, triggering reconnect") g.triggerOfferSending() - case changed := <-g.iCEConnDisconnected: - if !changed { - continue - } + case <-g.iCEConnDisconnected: g.log.Debugf("ICE state changed, try to send new offer") g.triggerOfferSending() case <-srReconnectedChan: diff --git a/client/internal/peer/wg_watcher.go b/client/internal/peer/wg_watcher.go new file mode 100644 index 000000000..6670c6517 --- /dev/null +++ b/client/internal/peer/wg_watcher.go @@ -0,0 +1,154 @@ +package peer + +import ( + "context" + "sync" + "time" + + log "github.com/sirupsen/logrus" + + "github.com/netbirdio/netbird/client/iface/configurer" +) + +const ( + wgHandshakePeriod = 3 * time.Minute +) + +var ( + wgHandshakeOvertime = 30 * time.Second // allowed delay in network + checkPeriod = wgHandshakePeriod + wgHandshakeOvertime +) + +type WGInterfaceStater interface { + GetStats(key string) (configurer.WGStats, error) +} + +type WGWatcher struct { + log *log.Entry + wgIfaceStater WGInterfaceStater + peerKey string + + ctx context.Context + ctxCancel context.CancelFunc + ctxLock sync.Mutex + waitGroup sync.WaitGroup +} + +func NewWGWatcher(log *log.Entry, wgIfaceStater WGInterfaceStater, peerKey string) *WGWatcher { + return &WGWatcher{ + log: log, + wgIfaceStater: wgIfaceStater, + peerKey: peerKey, + } +} + +// EnableWgWatcher starts the WireGuard watcher. If it is already enabled, it will return immediately and do nothing. +func (w *WGWatcher) EnableWgWatcher(parentCtx context.Context, onDisconnectedFn func()) { + w.log.Debugf("enable WireGuard watcher") + w.ctxLock.Lock() + defer w.ctxLock.Unlock() + + if w.ctx != nil && w.ctx.Err() == nil { + w.log.Errorf("WireGuard watcher already enabled") + return + } + + ctx, ctxCancel := context.WithCancel(parentCtx) + w.ctx = ctx + w.ctxCancel = ctxCancel + + initialHandshake, err := w.wgState() + if err != nil { + w.log.Warnf("failed to read initial wg stats: %v", err) + } + + w.waitGroup.Add(1) + go w.periodicHandshakeCheck(ctx, ctxCancel, onDisconnectedFn, initialHandshake) +} + +// DisableWgWatcher stops the WireGuard watcher and wait for the watcher to exit +func (w *WGWatcher) DisableWgWatcher() { + w.ctxLock.Lock() + defer w.ctxLock.Unlock() + + if w.ctxCancel == nil { + return + } + + w.log.Debugf("disable WireGuard watcher") + + w.ctxCancel() + w.ctxCancel = nil + w.waitGroup.Wait() +} + +// wgStateCheck help to check the state of the WireGuard handshake and relay connection +func (w *WGWatcher) periodicHandshakeCheck(ctx context.Context, ctxCancel context.CancelFunc, onDisconnectedFn func(), initialHandshake time.Time) { + w.log.Infof("WireGuard watcher started") + defer w.waitGroup.Done() + + timer := time.NewTimer(wgHandshakeOvertime) + defer timer.Stop() + defer ctxCancel() + + lastHandshake := initialHandshake + + for { + select { + case <-timer.C: + handshake, ok := w.handshakeCheck(lastHandshake) + if !ok { + onDisconnectedFn() + return + } + lastHandshake = *handshake + + resetTime := time.Until(handshake.Add(checkPeriod)) + timer.Reset(resetTime) + + w.log.Debugf("WireGuard watcher reset timer: %v", resetTime) + case <-ctx.Done(): + w.log.Infof("WireGuard watcher stopped") + return + } + } +} + +// handshakeCheck checks the WireGuard handshake and return the new handshake time if it is different from the previous one +func (w *WGWatcher) handshakeCheck(lastHandshake time.Time) (*time.Time, bool) { + handshake, err := w.wgState() + if err != nil { + w.log.Errorf("failed to read wg stats: %v", err) + return nil, false + } + + w.log.Tracef("previous handshake, handshake: %v, %v", lastHandshake, handshake) + + // the current know handshake did not change + if handshake.Equal(lastHandshake) { + w.log.Warnf("WireGuard handshake timed out, closing relay connection: %v", handshake) + return nil, false + } + + // in case if the machine is suspended, the handshake time will be in the past + if handshake.Add(checkPeriod).Before(time.Now()) { + w.log.Warnf("WireGuard handshake timed out, closing relay connection: %v", handshake) + return nil, false + } + + // error handling for handshake time in the future + if handshake.After(time.Now()) { + w.log.Warnf("WireGuard handshake is in the future, closing relay connection: %v", handshake) + return nil, false + } + + return &handshake, true +} + +func (w *WGWatcher) wgState() (time.Time, error) { + wgState, err := w.wgIfaceStater.GetStats(w.peerKey) + if err != nil { + return time.Time{}, err + } + return wgState.LastHandshake, nil +} diff --git a/client/internal/peer/wg_watcher_test.go b/client/internal/peer/wg_watcher_test.go new file mode 100644 index 000000000..a5b9026ad --- /dev/null +++ b/client/internal/peer/wg_watcher_test.go @@ -0,0 +1,98 @@ +package peer + +import ( + "context" + "testing" + "time" + + log "github.com/sirupsen/logrus" + + "github.com/netbirdio/netbird/client/iface/configurer" +) + +type MocWgIface struct { + initial bool + lastHandshake time.Time + stop bool +} + +func (m *MocWgIface) GetStats(key string) (configurer.WGStats, error) { + if !m.initial { + m.initial = true + return configurer.WGStats{}, nil + } + + if !m.stop { + m.lastHandshake = time.Now() + } + + stats := configurer.WGStats{ + LastHandshake: m.lastHandshake, + } + + return stats, nil +} + +func (m *MocWgIface) disconnect() { + m.stop = true +} + +func TestWGWatcher_EnableWgWatcher(t *testing.T) { + checkPeriod = 5 * time.Second + wgHandshakeOvertime = 1 * time.Second + + mlog := log.WithField("peer", "tet") + mocWgIface := &MocWgIface{} + watcher := NewWGWatcher(mlog, mocWgIface, "") + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + onDisconnected := make(chan struct{}, 1) + watcher.EnableWgWatcher(ctx, func() { + mlog.Infof("onDisconnectedFn") + onDisconnected <- struct{}{} + }) + + // wait for initial reading + time.Sleep(2 * time.Second) + mocWgIface.disconnect() + + select { + case <-onDisconnected: + case <-time.After(10 * time.Second): + t.Errorf("timeout") + } + watcher.DisableWgWatcher() +} + +func TestWGWatcher_ReEnable(t *testing.T) { + checkPeriod = 5 * time.Second + wgHandshakeOvertime = 1 * time.Second + + mlog := log.WithField("peer", "tet") + mocWgIface := &MocWgIface{} + watcher := NewWGWatcher(mlog, mocWgIface, "") + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + onDisconnected := make(chan struct{}, 1) + + watcher.EnableWgWatcher(ctx, func() {}) + watcher.DisableWgWatcher() + + watcher.EnableWgWatcher(ctx, func() { + onDisconnected <- struct{}{} + }) + + time.Sleep(2 * time.Second) + mocWgIface.disconnect() + + select { + case <-onDisconnected: + case <-time.After(10 * time.Second): + t.Errorf("timeout") + } + watcher.DisableWgWatcher() +} diff --git a/client/internal/peer/worker_ice.go b/client/internal/peer/worker_ice.go index 008318492..7dd84a98e 100644 --- a/client/internal/peer/worker_ice.go +++ b/client/internal/peer/worker_ice.go @@ -31,20 +31,15 @@ type ICEConnInfo struct { RelayedOnLocal bool } -type WorkerICECallbacks struct { - OnConnReady func(ConnPriority, ICEConnInfo) - OnStatusChanged func(ConnStatus) -} - type WorkerICE struct { ctx context.Context log *log.Entry config ConnConfig + conn *Conn signaler *Signaler iFaceDiscover stdnet.ExternalIFaceDiscover statusRecorder *Status hasRelayOnLocally bool - conn WorkerICECallbacks agent *ice.Agent muxAgent sync.Mutex @@ -60,16 +55,16 @@ type WorkerICE struct { lastKnownState ice.ConnectionState } -func NewWorkerICE(ctx context.Context, log *log.Entry, config ConnConfig, signaler *Signaler, ifaceDiscover stdnet.ExternalIFaceDiscover, statusRecorder *Status, hasRelayOnLocally bool, callBacks WorkerICECallbacks) (*WorkerICE, error) { +func NewWorkerICE(ctx context.Context, log *log.Entry, config ConnConfig, conn *Conn, signaler *Signaler, ifaceDiscover stdnet.ExternalIFaceDiscover, statusRecorder *Status, hasRelayOnLocally bool) (*WorkerICE, error) { w := &WorkerICE{ ctx: ctx, log: log, config: config, + conn: conn, signaler: signaler, iFaceDiscover: ifaceDiscover, statusRecorder: statusRecorder, hasRelayOnLocally: hasRelayOnLocally, - conn: callBacks, } localUfrag, localPwd, err := icemaker.GenerateICECredentials() @@ -154,8 +149,8 @@ func (w *WorkerICE) OnNewOffer(remoteOfferAnswer *OfferAnswer) { Relayed: isRelayed(pair), RelayedOnLocal: isRelayCandidate(pair.Local), } - w.log.Debugf("on ICE conn read to use ready") - go w.conn.OnConnReady(selectedPriority(pair), ci) + w.log.Debugf("on ICE conn is ready to use") + go w.conn.onICEConnectionIsReady(selectedPriority(pair), ci) } // OnRemoteCandidate Handles ICE connection Candidate provided by the remote peer. @@ -220,7 +215,7 @@ func (w *WorkerICE) reCreateAgent(agentCancel context.CancelFunc, candidates []i case ice.ConnectionStateFailed, ice.ConnectionStateDisconnected: if w.lastKnownState != ice.ConnectionStateDisconnected { w.lastKnownState = ice.ConnectionStateDisconnected - w.conn.OnStatusChanged(StatusDisconnected) + w.conn.onICEStateDisconnected() } w.closeAgent(agentCancel) default: diff --git a/client/internal/peer/worker_relay.go b/client/internal/peer/worker_relay.go index c22dcdeda..56c19cd1e 100644 --- a/client/internal/peer/worker_relay.go +++ b/client/internal/peer/worker_relay.go @@ -6,52 +6,41 @@ import ( "net" "sync" "sync/atomic" - "time" log "github.com/sirupsen/logrus" relayClient "github.com/netbirdio/netbird/relay/client" ) -var ( - wgHandshakePeriod = 3 * time.Minute - wgHandshakeOvertime = 30 * time.Second -) - type RelayConnInfo struct { relayedConn net.Conn rosenpassPubKey []byte rosenpassAddr string } -type WorkerRelayCallbacks struct { - OnConnReady func(RelayConnInfo) - OnDisconnected func() -} - type WorkerRelay struct { log *log.Entry isController bool config ConnConfig + conn *Conn relayManager relayClient.ManagerService - callBacks WorkerRelayCallbacks - relayedConn net.Conn - relayLock sync.Mutex - ctxWgWatch context.Context - ctxCancelWgWatch context.CancelFunc - ctxLock sync.Mutex + relayedConn net.Conn + relayLock sync.Mutex relaySupportedOnRemotePeer atomic.Bool + + wgWatcher *WGWatcher } -func NewWorkerRelay(log *log.Entry, ctrl bool, config ConnConfig, relayManager relayClient.ManagerService, callbacks WorkerRelayCallbacks) *WorkerRelay { +func NewWorkerRelay(log *log.Entry, ctrl bool, config ConnConfig, conn *Conn, relayManager relayClient.ManagerService) *WorkerRelay { r := &WorkerRelay{ log: log, isController: ctrl, config: config, + conn: conn, relayManager: relayManager, - callBacks: callbacks, + wgWatcher: NewWGWatcher(log, config.WgConfig.WgInterface, config.Key), } return r } @@ -87,7 +76,7 @@ func (w *WorkerRelay) OnNewOffer(remoteOfferAnswer *OfferAnswer) { w.relayedConn = relayedConn w.relayLock.Unlock() - err = w.relayManager.AddCloseListener(srv, w.onRelayMGDisconnected) + err = w.relayManager.AddCloseListener(srv, w.onRelayClientDisconnected) if err != nil { log.Errorf("failed to add close listener: %s", err) _ = relayedConn.Close() @@ -95,7 +84,7 @@ func (w *WorkerRelay) OnNewOffer(remoteOfferAnswer *OfferAnswer) { } w.log.Debugf("peer conn opened via Relay: %s", srv) - go w.callBacks.OnConnReady(RelayConnInfo{ + go w.conn.onRelayConnectionIsReady(RelayConnInfo{ relayedConn: relayedConn, rosenpassPubKey: remoteOfferAnswer.RosenpassPubKey, rosenpassAddr: remoteOfferAnswer.RosenpassAddr, @@ -103,32 +92,11 @@ func (w *WorkerRelay) OnNewOffer(remoteOfferAnswer *OfferAnswer) { } func (w *WorkerRelay) EnableWgWatcher(ctx context.Context) { - w.log.Debugf("enable WireGuard watcher") - w.ctxLock.Lock() - defer w.ctxLock.Unlock() - - if w.ctxWgWatch != nil && w.ctxWgWatch.Err() == nil { - return - } - - ctx, ctxCancel := context.WithCancel(ctx) - w.ctxWgWatch = ctx - w.ctxCancelWgWatch = ctxCancel - - w.wgStateCheck(ctx, ctxCancel) + w.wgWatcher.EnableWgWatcher(ctx, w.onWGDisconnected) } func (w *WorkerRelay) DisableWgWatcher() { - w.ctxLock.Lock() - defer w.ctxLock.Unlock() - - if w.ctxCancelWgWatch == nil { - return - } - - w.log.Debugf("disable WireGuard watcher") - - w.ctxCancelWgWatch() + w.wgWatcher.DisableWgWatcher() } func (w *WorkerRelay) RelayInstanceAddress() (string, error) { @@ -150,57 +118,17 @@ func (w *WorkerRelay) CloseConn() { return } - err := w.relayedConn.Close() - if err != nil { + if err := w.relayedConn.Close(); err != nil { w.log.Warnf("failed to close relay connection: %v", err) } } -// wgStateCheck help to check the state of the WireGuard handshake and relay connection -func (w *WorkerRelay) wgStateCheck(ctx context.Context, ctxCancel context.CancelFunc) { - w.log.Debugf("WireGuard watcher started") - lastHandshake, err := w.wgState() - if err != nil { - w.log.Warnf("failed to read wg stats: %v", err) - lastHandshake = time.Time{} - } - - go func(lastHandshake time.Time) { - timer := time.NewTimer(wgHandshakeOvertime) - defer timer.Stop() - defer ctxCancel() - - for { - select { - case <-timer.C: - handshake, err := w.wgState() - if err != nil { - w.log.Errorf("failed to read wg stats: %v", err) - timer.Reset(wgHandshakeOvertime) - continue - } - - w.log.Tracef("previous handshake, handshake: %v, %v", lastHandshake, handshake) - - if handshake.Equal(lastHandshake) { - w.log.Infof("WireGuard handshake timed out, closing relay connection: %v", handshake) - w.relayLock.Lock() - _ = w.relayedConn.Close() - w.relayLock.Unlock() - w.callBacks.OnDisconnected() - return - } - - resetTime := time.Until(handshake.Add(wgHandshakePeriod + wgHandshakeOvertime)) - lastHandshake = handshake - timer.Reset(resetTime) - case <-ctx.Done(): - w.log.Debugf("WireGuard watcher stopped") - return - } - } - }(lastHandshake) +func (w *WorkerRelay) onWGDisconnected() { + w.relayLock.Lock() + _ = w.relayedConn.Close() + w.relayLock.Unlock() + w.conn.onRelayDisconnected() } func (w *WorkerRelay) isRelaySupported(answer *OfferAnswer) bool { @@ -217,20 +145,7 @@ func (w *WorkerRelay) preferredRelayServer(myRelayAddress, remoteRelayAddress st return remoteRelayAddress } -func (w *WorkerRelay) wgState() (time.Time, error) { - wgState, err := w.config.WgConfig.WgInterface.GetStats(w.config.Key) - if err != nil { - return time.Time{}, err - } - return wgState.LastHandshake, nil -} - -func (w *WorkerRelay) onRelayMGDisconnected() { - w.ctxLock.Lock() - defer w.ctxLock.Unlock() - - if w.ctxCancelWgWatch != nil { - w.ctxCancelWgWatch() - } - go w.callBacks.OnDisconnected() +func (w *WorkerRelay) onRelayClientDisconnected() { + w.wgWatcher.DisableWgWatcher() + go w.conn.onRelayDisconnected() } diff --git a/relay/client/client.go b/relay/client/client.go index 3c23b70d2..9e7e54393 100644 --- a/relay/client/client.go +++ b/relay/client/client.go @@ -141,7 +141,6 @@ type Client struct { muInstanceURL sync.Mutex onDisconnectListener func(string) - onConnectedListener func() listenerMutex sync.Mutex } @@ -190,7 +189,6 @@ func (c *Client) Connect() error { c.wgReadLoop.Add(1) go c.readLoop(c.relayConn) - go c.notifyConnected() return nil } @@ -238,12 +236,6 @@ func (c *Client) SetOnDisconnectListener(fn func(string)) { c.onDisconnectListener = fn } -func (c *Client) SetOnConnectedListener(fn func()) { - c.listenerMutex.Lock() - defer c.listenerMutex.Unlock() - c.onConnectedListener = fn -} - // HasConns returns true if there are connections. func (c *Client) HasConns() bool { c.mu.Lock() @@ -559,16 +551,6 @@ func (c *Client) notifyDisconnected() { go c.onDisconnectListener(c.connectionURL) } -func (c *Client) notifyConnected() { - c.listenerMutex.Lock() - defer c.listenerMutex.Unlock() - - if c.onConnectedListener == nil { - return - } - go c.onConnectedListener() -} - func (c *Client) writeCloseMsg() { msg := messages.MarshalCloseMsg() _, err := c.relayConn.Write(msg) diff --git a/relay/client/guard.go b/relay/client/guard.go index b971363a8..554330ea3 100644 --- a/relay/client/guard.go +++ b/relay/client/guard.go @@ -14,8 +14,9 @@ var ( // Guard manage the reconnection tries to the Relay server in case of disconnection event. type Guard struct { - // OnNewRelayClient is a channel that is used to notify the relay client about a new relay client instance. + // OnNewRelayClient is a channel that is used to notify the relay manager about a new relay client instance. OnNewRelayClient chan *Client + OnReconnected chan struct{} serverPicker *ServerPicker } @@ -23,6 +24,7 @@ type Guard struct { func NewGuard(sp *ServerPicker) *Guard { g := &Guard{ OnNewRelayClient: make(chan *Client, 1), + OnReconnected: make(chan struct{}, 1), serverPicker: sp, } return g @@ -39,14 +41,13 @@ func NewGuard(sp *ServerPicker) *Guard { // - relayClient: The relay client instance that was disconnected. // todo prevent multiple reconnection instances. In the current usage it should not happen, but it is better to prevent func (g *Guard) StartReconnectTrys(ctx context.Context, relayClient *Client) { - if relayClient == nil { - goto RETRY - } - if g.isServerURLStillValid(relayClient) && g.quickReconnect(ctx, relayClient) { + // try to reconnect to the same server + if ok := g.tryToQuickReconnect(ctx, relayClient); ok { + g.notifyReconnected() return } -RETRY: + // start a ticker to pick a new server ticker := exponentTicker(ctx) defer ticker.Stop() @@ -64,6 +65,28 @@ RETRY: } } +func (g *Guard) tryToQuickReconnect(parentCtx context.Context, rc *Client) bool { + if rc == nil { + return false + } + + if !g.isServerURLStillValid(rc) { + return false + } + + if cancelled := waiteBeforeRetry(parentCtx); !cancelled { + return false + } + + log.Infof("try to reconnect to Relay server: %s", rc.connectionURL) + + if err := rc.Connect(); err != nil { + log.Errorf("failed to reconnect to relay server: %s", err) + return false + } + return true +} + func (g *Guard) retry(ctx context.Context) error { log.Infof("try to pick up a new Relay server") relayClient, err := g.serverPicker.PickServer(ctx) @@ -78,23 +101,6 @@ func (g *Guard) retry(ctx context.Context) error { return nil } -func (g *Guard) quickReconnect(parentCtx context.Context, rc *Client) bool { - ctx, cancel := context.WithTimeout(parentCtx, 1500*time.Millisecond) - defer cancel() - <-ctx.Done() - - if parentCtx.Err() != nil { - return false - } - log.Infof("try to reconnect to Relay server: %s", rc.connectionURL) - - if err := rc.Connect(); err != nil { - log.Errorf("failed to reconnect to relay server: %s", err) - return false - } - return true -} - func (g *Guard) drainRelayClientChan() { select { case <-g.OnNewRelayClient: @@ -111,6 +117,13 @@ func (g *Guard) isServerURLStillValid(rc *Client) bool { return false } +func (g *Guard) notifyReconnected() { + select { + case g.OnReconnected <- struct{}{}: + default: + } +} + func exponentTicker(ctx context.Context) *backoff.Ticker { bo := backoff.WithContext(&backoff.ExponentialBackOff{ InitialInterval: 2 * time.Second, @@ -121,3 +134,15 @@ func exponentTicker(ctx context.Context) *backoff.Ticker { return backoff.NewTicker(bo) } + +func waiteBeforeRetry(ctx context.Context) bool { + timer := time.NewTimer(1500 * time.Millisecond) + defer timer.Stop() + + select { + case <-timer.C: + return true + case <-ctx.Done(): + return false + } +} diff --git a/relay/client/manager.go b/relay/client/manager.go index d847bb879..26b113050 100644 --- a/relay/client/manager.go +++ b/relay/client/manager.go @@ -165,6 +165,9 @@ func (m *Manager) Ready() bool { } func (m *Manager) SetOnReconnectedListener(f func()) { + m.listenerLock.Lock() + defer m.listenerLock.Unlock() + m.onReconnectedListenerFn = f } @@ -284,6 +287,9 @@ func (m *Manager) openConnVia(serverAddress, peerKey string) (net.Conn, error) { } func (m *Manager) onServerConnected() { + m.listenerLock.Lock() + defer m.listenerLock.Unlock() + if m.onReconnectedListenerFn == nil { return } @@ -304,8 +310,11 @@ func (m *Manager) onServerDisconnected(serverAddress string) { func (m *Manager) listenGuardEvent(ctx context.Context) { for { select { + case <-m.reconnectGuard.OnReconnected: + m.onServerConnected() case rc := <-m.reconnectGuard.OnNewRelayClient: m.storeClient(rc) + m.onServerConnected() case <-ctx.Done(): return } @@ -317,7 +326,6 @@ func (m *Manager) storeClient(client *Client) { defer m.relayClientMu.Unlock() m.relayClient = client - m.relayClient.SetOnConnectedListener(m.onServerConnected) m.relayClient.SetOnDisconnectListener(m.onServerDisconnected) } From 488b697479e95db88bca281022e965af192527ed Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Mon, 10 Feb 2025 18:13:34 +0100 Subject: [PATCH 53/92] [client] Support dns upstream failover for nameserver groups with same match domain (#3178) --- client/internal/dns/handler_chain.go | 24 +- client/internal/dns/handler_chain_test.go | 26 +- client/internal/dns/local.go | 7 +- client/internal/dns/server.go | 241 ++++++---- client/internal/dns/server_test.go | 525 +++++++++++++++++++++- client/internal/dns/upstream.go | 59 ++- client/internal/dns/upstream_android.go | 3 +- client/internal/dns/upstream_general.go | 3 +- client/internal/dns/upstream_ios.go | 3 +- client/internal/dns/upstream_test.go | 38 +- client/internal/peer/status.go | 4 +- 11 files changed, 747 insertions(+), 186 deletions(-) diff --git a/client/internal/dns/handler_chain.go b/client/internal/dns/handler_chain.go index 673f410e2..3286daabf 100644 --- a/client/internal/dns/handler_chain.go +++ b/client/internal/dns/handler_chain.go @@ -12,7 +12,7 @@ import ( const ( PriorityDNSRoute = 100 PriorityMatchDomain = 50 - PriorityDefault = 0 + PriorityDefault = 1 ) type SubdomainMatcher interface { @@ -26,7 +26,6 @@ type HandlerEntry struct { Pattern string OrigPattern string IsWildcard bool - StopHandler handlerWithStop MatchSubdomains bool } @@ -64,7 +63,7 @@ func (w *ResponseWriterChain) GetOrigPattern() string { } // AddHandler adds a new handler to the chain, replacing any existing handler with the same pattern and priority -func (c *HandlerChain) AddHandler(pattern string, handler dns.Handler, priority int, stopHandler handlerWithStop) { +func (c *HandlerChain) AddHandler(pattern string, handler dns.Handler, priority int) { c.mu.Lock() defer c.mu.Unlock() @@ -78,9 +77,6 @@ func (c *HandlerChain) AddHandler(pattern string, handler dns.Handler, priority // First remove any existing handler with same pattern (case-insensitive) and priority for i := len(c.handlers) - 1; i >= 0; i-- { if strings.EqualFold(c.handlers[i].OrigPattern, origPattern) && c.handlers[i].Priority == priority { - if c.handlers[i].StopHandler != nil { - c.handlers[i].StopHandler.stop() - } c.handlers = append(c.handlers[:i], c.handlers[i+1:]...) break } @@ -101,7 +97,6 @@ func (c *HandlerChain) AddHandler(pattern string, handler dns.Handler, priority Pattern: pattern, OrigPattern: origPattern, IsWildcard: isWildcard, - StopHandler: stopHandler, MatchSubdomains: matchSubdomains, } @@ -142,9 +137,6 @@ func (c *HandlerChain) RemoveHandler(pattern string, priority int) { for i := len(c.handlers) - 1; i >= 0; i-- { entry := c.handlers[i] if strings.EqualFold(entry.OrigPattern, pattern) && entry.Priority == priority { - if entry.StopHandler != nil { - entry.StopHandler.stop() - } c.handlers = append(c.handlers[:i], c.handlers[i+1:]...) return } @@ -180,8 +172,8 @@ func (c *HandlerChain) ServeDNS(w dns.ResponseWriter, r *dns.Msg) { if log.IsLevelEnabled(log.TraceLevel) { log.Tracef("current handlers (%d):", len(handlers)) for _, h := range handlers { - log.Tracef(" - pattern: domain=%s original: domain=%s wildcard=%v priority=%d", - h.Pattern, h.OrigPattern, h.IsWildcard, h.Priority) + log.Tracef(" - pattern: domain=%s original: domain=%s wildcard=%v match_subdomain=%v priority=%d", + h.Pattern, h.OrigPattern, h.IsWildcard, h.MatchSubdomains, h.Priority) } } @@ -206,13 +198,13 @@ func (c *HandlerChain) ServeDNS(w dns.ResponseWriter, r *dns.Msg) { } if !matched { - log.Tracef("trying domain match: request: domain=%s pattern: domain=%s wildcard=%v match_subdomain=%v matched=false", - qname, entry.OrigPattern, entry.MatchSubdomains, entry.IsWildcard) + log.Tracef("trying domain match: request: domain=%s pattern: domain=%s wildcard=%v match_subdomain=%v priority=%d matched=false", + qname, entry.OrigPattern, entry.MatchSubdomains, entry.IsWildcard, entry.Priority) continue } - log.Tracef("handler matched: request: domain=%s pattern: domain=%s wildcard=%v match_subdomain=%v", - qname, entry.OrigPattern, entry.IsWildcard, entry.MatchSubdomains) + log.Tracef("handler matched: request: domain=%s pattern: domain=%s wildcard=%v match_subdomain=%v priority=%d", + qname, entry.OrigPattern, entry.IsWildcard, entry.MatchSubdomains, entry.Priority) chainWriter := &ResponseWriterChain{ ResponseWriter: w, diff --git a/client/internal/dns/handler_chain_test.go b/client/internal/dns/handler_chain_test.go index d04bfbbb3..8c66446ee 100644 --- a/client/internal/dns/handler_chain_test.go +++ b/client/internal/dns/handler_chain_test.go @@ -21,9 +21,9 @@ func TestHandlerChain_ServeDNS_Priorities(t *testing.T) { dnsRouteHandler := &nbdns.MockHandler{} // Setup handlers with different priorities - chain.AddHandler("example.com.", defaultHandler, nbdns.PriorityDefault, nil) - chain.AddHandler("example.com.", matchDomainHandler, nbdns.PriorityMatchDomain, nil) - chain.AddHandler("example.com.", dnsRouteHandler, nbdns.PriorityDNSRoute, nil) + chain.AddHandler("example.com.", defaultHandler, nbdns.PriorityDefault) + chain.AddHandler("example.com.", matchDomainHandler, nbdns.PriorityMatchDomain) + chain.AddHandler("example.com.", dnsRouteHandler, nbdns.PriorityDNSRoute) // Create test request r := new(dns.Msg) @@ -138,7 +138,7 @@ func TestHandlerChain_ServeDNS_DomainMatching(t *testing.T) { pattern = "*." + tt.handlerDomain[2:] } - chain.AddHandler(pattern, handler, nbdns.PriorityDefault, nil) + chain.AddHandler(pattern, handler, nbdns.PriorityDefault) r := new(dns.Msg) r.SetQuestion(tt.queryDomain, dns.TypeA) @@ -253,7 +253,7 @@ func TestHandlerChain_ServeDNS_OverlappingDomains(t *testing.T) { handler.On("ServeDNS", mock.Anything, mock.Anything).Maybe() } - chain.AddHandler(tt.handlers[i].pattern, handler, tt.handlers[i].priority, nil) + chain.AddHandler(tt.handlers[i].pattern, handler, tt.handlers[i].priority) } // Create and execute request @@ -280,9 +280,9 @@ func TestHandlerChain_ServeDNS_ChainContinuation(t *testing.T) { handler3 := &nbdns.MockHandler{} // Add handlers in priority order - chain.AddHandler("example.com.", handler1, nbdns.PriorityDNSRoute, nil) - chain.AddHandler("example.com.", handler2, nbdns.PriorityMatchDomain, nil) - chain.AddHandler("example.com.", handler3, nbdns.PriorityDefault, nil) + chain.AddHandler("example.com.", handler1, nbdns.PriorityDNSRoute) + chain.AddHandler("example.com.", handler2, nbdns.PriorityMatchDomain) + chain.AddHandler("example.com.", handler3, nbdns.PriorityDefault) // Create test request r := new(dns.Msg) @@ -416,7 +416,7 @@ func TestHandlerChain_PriorityDeregistration(t *testing.T) { if op.action == "add" { handler := &nbdns.MockHandler{} handlers[op.priority] = handler - chain.AddHandler(op.pattern, handler, op.priority, nil) + chain.AddHandler(op.pattern, handler, op.priority) } else { chain.RemoveHandler(op.pattern, op.priority) } @@ -471,9 +471,9 @@ func TestHandlerChain_MultiPriorityHandling(t *testing.T) { r.SetQuestion(testQuery, dns.TypeA) // Add handlers in mixed order - chain.AddHandler(testDomain, defaultHandler, nbdns.PriorityDefault, nil) - chain.AddHandler(testDomain, routeHandler, nbdns.PriorityDNSRoute, nil) - chain.AddHandler(testDomain, matchHandler, nbdns.PriorityMatchDomain, nil) + chain.AddHandler(testDomain, defaultHandler, nbdns.PriorityDefault) + chain.AddHandler(testDomain, routeHandler, nbdns.PriorityDNSRoute) + chain.AddHandler(testDomain, matchHandler, nbdns.PriorityMatchDomain) // Test 1: Initial state with all three handlers w := &nbdns.ResponseWriterChain{ResponseWriter: &mockResponseWriter{}} @@ -653,7 +653,7 @@ func TestHandlerChain_CaseSensitivity(t *testing.T) { handler = mockHandler } - chain.AddHandler(pattern, handler, h.priority, nil) + chain.AddHandler(pattern, handler, h.priority) } // Execute request diff --git a/client/internal/dns/local.go b/client/internal/dns/local.go index 9a78d4d50..1fe88f750 100644 --- a/client/internal/dns/local.go +++ b/client/internal/dns/local.go @@ -29,10 +29,15 @@ func (d *localResolver) String() string { return fmt.Sprintf("local resolver [%d records]", len(d.registeredMap)) } +// ID returns the unique handler ID +func (d *localResolver) id() handlerID { + return "local-resolver" +} + // ServeDNS handles a DNS request func (d *localResolver) ServeDNS(w dns.ResponseWriter, r *dns.Msg) { if len(r.Question) > 0 { - log.Tracef("received question: domain=%s type=%v class=%v", r.Question[0].Name, r.Question[0].Qtype, r.Question[0].Qclass) + log.Tracef("received local question: domain=%s type=%v class=%v", r.Question[0].Name, r.Question[0].Qtype, r.Question[0].Qclass) } replyMessage := &dns.Msg{} diff --git a/client/internal/dns/server.go b/client/internal/dns/server.go index 1fe913fd9..f714f9857 100644 --- a/client/internal/dns/server.go +++ b/client/internal/dns/server.go @@ -5,7 +5,6 @@ import ( "fmt" "net/netip" "runtime" - "strings" "sync" "github.com/miekg/dns" @@ -42,7 +41,12 @@ type Server interface { ProbeAvailability() } -type registeredHandlerMap map[string]handlerWithStop +type handlerID string + +type nsGroupsByDomain struct { + domain string + groups []*nbdns.NameServerGroup +} // DefaultServer dns server object type DefaultServer struct { @@ -52,7 +56,6 @@ type DefaultServer struct { mux sync.Mutex service service dnsMuxMap registeredHandlerMap - handlerPriorities map[string]int localResolver *localResolver wgInterface WGIface hostManager hostManager @@ -77,14 +80,17 @@ type handlerWithStop interface { dns.Handler stop() probeAvailability() + id() handlerID } -type muxUpdate struct { +type handlerWrapper struct { domain string handler handlerWithStop priority int } +type registeredHandlerMap map[handlerID]handlerWrapper + // NewDefaultServer returns a new dns server func NewDefaultServer( ctx context.Context, @@ -158,13 +164,12 @@ func newDefaultServer( ) *DefaultServer { ctx, stop := context.WithCancel(ctx) defaultServer := &DefaultServer{ - ctx: ctx, - ctxCancel: stop, - disableSys: disableSys, - service: dnsService, - handlerChain: NewHandlerChain(), - dnsMuxMap: make(registeredHandlerMap), - handlerPriorities: make(map[string]int), + ctx: ctx, + ctxCancel: stop, + disableSys: disableSys, + service: dnsService, + handlerChain: NewHandlerChain(), + dnsMuxMap: make(registeredHandlerMap), localResolver: &localResolver{ registeredMap: make(registrationMap), }, @@ -192,8 +197,7 @@ func (s *DefaultServer) registerHandler(domains []string, handler dns.Handler, p log.Warn("skipping empty domain") continue } - s.handlerChain.AddHandler(domain, handler, priority, nil) - s.handlerPriorities[domain] = priority + s.handlerChain.AddHandler(domain, handler, priority) s.service.RegisterMux(nbdns.NormalizeZone(domain), s.handlerChain) } } @@ -209,14 +213,15 @@ func (s *DefaultServer) deregisterHandler(domains []string, priority int) { log.Debugf("deregistering handler %v with priority %d", domains, priority) for _, domain := range domains { + if domain == "" { + log.Warn("skipping empty domain") + continue + } + s.handlerChain.RemoveHandler(domain, priority) // Only deregister from service if no handlers remain if !s.handlerChain.HasHandlers(domain) { - if domain == "" { - log.Warn("skipping empty domain") - continue - } s.service.DeregisterMux(nbdns.NormalizeZone(domain)) } } @@ -283,14 +288,24 @@ func (s *DefaultServer) Stop() { // OnUpdatedHostDNSServer update the DNS servers addresses for root zones // It will be applied if the mgm server do not enforce DNS settings for root zone + func (s *DefaultServer) OnUpdatedHostDNSServer(hostsDnsList []string) { s.hostsDNSHolder.set(hostsDnsList) - _, ok := s.dnsMuxMap[nbdns.RootZone] - if ok { + // Check if there's any root handler + var hasRootHandler bool + for _, handler := range s.dnsMuxMap { + if handler.domain == nbdns.RootZone { + hasRootHandler = true + break + } + } + + if hasRootHandler { log.Debugf("on new host DNS config but skip to apply it") return } + log.Debugf("update host DNS settings: %+v", hostsDnsList) s.addHostRootZone() } @@ -364,7 +379,7 @@ func (s *DefaultServer) ProbeAvailability() { go func(mux handlerWithStop) { defer wg.Done() mux.probeAvailability() - }(mux) + }(mux.handler) } wg.Wait() } @@ -419,8 +434,8 @@ func (s *DefaultServer) applyConfiguration(update nbdns.Config) error { return nil } -func (s *DefaultServer) buildLocalHandlerUpdate(customZones []nbdns.CustomZone) ([]muxUpdate, map[string]nbdns.SimpleRecord, error) { - var muxUpdates []muxUpdate +func (s *DefaultServer) buildLocalHandlerUpdate(customZones []nbdns.CustomZone) ([]handlerWrapper, map[string]nbdns.SimpleRecord, error) { + var muxUpdates []handlerWrapper localRecords := make(map[string]nbdns.SimpleRecord, 0) for _, customZone := range customZones { @@ -428,7 +443,7 @@ func (s *DefaultServer) buildLocalHandlerUpdate(customZones []nbdns.CustomZone) return nil, nil, fmt.Errorf("received an empty list of records") } - muxUpdates = append(muxUpdates, muxUpdate{ + muxUpdates = append(muxUpdates, handlerWrapper{ domain: customZone.Domain, handler: s.localResolver, priority: PriorityMatchDomain, @@ -446,15 +461,59 @@ func (s *DefaultServer) buildLocalHandlerUpdate(customZones []nbdns.CustomZone) return muxUpdates, localRecords, nil } -func (s *DefaultServer) buildUpstreamHandlerUpdate(nameServerGroups []*nbdns.NameServerGroup) ([]muxUpdate, error) { +func (s *DefaultServer) buildUpstreamHandlerUpdate(nameServerGroups []*nbdns.NameServerGroup) ([]handlerWrapper, error) { + var muxUpdates []handlerWrapper - var muxUpdates []muxUpdate for _, nsGroup := range nameServerGroups { if len(nsGroup.NameServers) == 0 { log.Warn("received a nameserver group with empty nameserver list") continue } + if !nsGroup.Primary && len(nsGroup.Domains) == 0 { + return nil, fmt.Errorf("received a non primary nameserver group with an empty domain list") + } + + for _, domain := range nsGroup.Domains { + if domain == "" { + return nil, fmt.Errorf("received a nameserver group with an empty domain element") + } + } + } + + groupedNS := groupNSGroupsByDomain(nameServerGroups) + + for _, domainGroup := range groupedNS { + basePriority := PriorityMatchDomain + if domainGroup.domain == nbdns.RootZone { + basePriority = PriorityDefault + } + + updates, err := s.createHandlersForDomainGroup(domainGroup, basePriority) + if err != nil { + return nil, err + } + muxUpdates = append(muxUpdates, updates...) + } + + return muxUpdates, nil +} + +func (s *DefaultServer) createHandlersForDomainGroup(domainGroup nsGroupsByDomain, basePriority int) ([]handlerWrapper, error) { + var muxUpdates []handlerWrapper + + for i, nsGroup := range domainGroup.groups { + // Decrement priority by handler index (0, 1, 2, ...) to avoid conflicts + priority := basePriority - i + + // Check if we're about to overlap with the next priority tier + if basePriority == PriorityMatchDomain && priority <= PriorityDefault { + log.Warnf("too many handlers for domain=%s, would overlap with default priority tier (diff=%d). Skipping remaining handlers", + domainGroup.domain, PriorityMatchDomain-PriorityDefault) + break + } + + log.Debugf("creating handler for domain=%s with priority=%d", domainGroup.domain, priority) handler, err := newUpstreamResolver( s.ctx, s.wgInterface.Name(), @@ -462,10 +521,12 @@ func (s *DefaultServer) buildUpstreamHandlerUpdate(nameServerGroups []*nbdns.Nam s.wgInterface.Address().Network, s.statusRecorder, s.hostsDNSHolder, + domainGroup.domain, ) if err != nil { - return nil, fmt.Errorf("unable to create a new upstream resolver, error: %v", err) + return nil, fmt.Errorf("create upstream resolver: %v", err) } + for _, ns := range nsGroup.NameServers { if ns.NSType != nbdns.UDPNameServerType { log.Warnf("skipping nameserver %s with type %s, this peer supports only %s", @@ -489,78 +550,47 @@ func (s *DefaultServer) buildUpstreamHandlerUpdate(nameServerGroups []*nbdns.Nam // after some period defined by upstream it tries to reactivate self by calling this hook // everything we need here is just to re-apply current configuration because it already // contains this upstream settings (temporal deactivation not removed it) - handler.deactivate, handler.reactivate = s.upstreamCallbacks(nsGroup, handler) + handler.deactivate, handler.reactivate = s.upstreamCallbacks(nsGroup, handler, priority) - if nsGroup.Primary { - muxUpdates = append(muxUpdates, muxUpdate{ - domain: nbdns.RootZone, - handler: handler, - priority: PriorityDefault, - }) - continue - } - - if len(nsGroup.Domains) == 0 { - handler.stop() - return nil, fmt.Errorf("received a non primary nameserver group with an empty domain list") - } - - for _, domain := range nsGroup.Domains { - if domain == "" { - handler.stop() - return nil, fmt.Errorf("received a nameserver group with an empty domain element") - } - muxUpdates = append(muxUpdates, muxUpdate{ - domain: domain, - handler: handler, - priority: PriorityMatchDomain, - }) - } + muxUpdates = append(muxUpdates, handlerWrapper{ + domain: domainGroup.domain, + handler: handler, + priority: priority, + }) } return muxUpdates, nil } -func (s *DefaultServer) updateMux(muxUpdates []muxUpdate) { - muxUpdateMap := make(registeredHandlerMap) - handlersByPriority := make(map[string]int) - - var isContainRootUpdate bool - - // First register new handlers - for _, update := range muxUpdates { - s.registerHandler([]string{update.domain}, update.handler, update.priority) - muxUpdateMap[update.domain] = update.handler - handlersByPriority[update.domain] = update.priority - - if existingHandler, ok := s.dnsMuxMap[update.domain]; ok { - existingHandler.stop() - } - - if update.domain == nbdns.RootZone { - isContainRootUpdate = true - } +func (s *DefaultServer) updateMux(muxUpdates []handlerWrapper) { + // this will introduce a short period of time when the server is not able to handle DNS requests + for _, existing := range s.dnsMuxMap { + s.deregisterHandler([]string{existing.domain}, existing.priority) + existing.handler.stop() } - // Then deregister old handlers not in the update - for key, existingHandler := range s.dnsMuxMap { - _, found := muxUpdateMap[key] - if !found { - if !isContainRootUpdate && key == nbdns.RootZone { + muxUpdateMap := make(registeredHandlerMap) + var containsRootUpdate bool + + for _, update := range muxUpdates { + if update.domain == nbdns.RootZone { + containsRootUpdate = true + } + s.registerHandler([]string{update.domain}, update.handler, update.priority) + muxUpdateMap[update.handler.id()] = update + } + + // If there's no root update and we had a root handler, restore it + if !containsRootUpdate { + for _, existing := range s.dnsMuxMap { + if existing.domain == nbdns.RootZone { s.addHostRootZone() - existingHandler.stop() - } else { - existingHandler.stop() - // Deregister with the priority that was used to register - if oldPriority, ok := s.handlerPriorities[key]; ok { - s.deregisterHandler([]string{key}, oldPriority) - } + break } } } s.dnsMuxMap = muxUpdateMap - s.handlerPriorities = handlersByPriority } func (s *DefaultServer) updateLocalResolver(update map[string]nbdns.SimpleRecord) { @@ -593,6 +623,7 @@ func getNSHostPort(ns nbdns.NameServer) string { func (s *DefaultServer) upstreamCallbacks( nsGroup *nbdns.NameServerGroup, handler dns.Handler, + priority int, ) (deactivate func(error), reactivate func()) { var removeIndex map[string]int deactivate = func(err error) { @@ -609,13 +640,13 @@ func (s *DefaultServer) upstreamCallbacks( if nsGroup.Primary { removeIndex[nbdns.RootZone] = -1 s.currentConfig.RouteAll = false - s.deregisterHandler([]string{nbdns.RootZone}, PriorityDefault) + s.deregisterHandler([]string{nbdns.RootZone}, priority) } for i, item := range s.currentConfig.Domains { if _, found := removeIndex[item.Domain]; found { s.currentConfig.Domains[i].Disabled = true - s.deregisterHandler([]string{item.Domain}, PriorityMatchDomain) + s.deregisterHandler([]string{item.Domain}, priority) removeIndex[item.Domain] = i } } @@ -635,8 +666,8 @@ func (s *DefaultServer) upstreamCallbacks( } s.updateNSState(nsGroup, err, false) - } + reactivate = func() { s.mux.Lock() defer s.mux.Unlock() @@ -646,7 +677,7 @@ func (s *DefaultServer) upstreamCallbacks( continue } s.currentConfig.Domains[i].Disabled = false - s.registerHandler([]string{domain}, handler, PriorityMatchDomain) + s.registerHandler([]string{domain}, handler, priority) } l := log.WithField("nameservers", nsGroup.NameServers) @@ -654,7 +685,7 @@ func (s *DefaultServer) upstreamCallbacks( if nsGroup.Primary { s.currentConfig.RouteAll = true - s.registerHandler([]string{nbdns.RootZone}, handler, PriorityDefault) + s.registerHandler([]string{nbdns.RootZone}, handler, priority) } if s.hostManager != nil { @@ -676,6 +707,7 @@ func (s *DefaultServer) addHostRootZone() { s.wgInterface.Address().Network, s.statusRecorder, s.hostsDNSHolder, + nbdns.RootZone, ) if err != nil { log.Errorf("unable to create a new upstream resolver, error: %v", err) @@ -732,5 +764,34 @@ func generateGroupKey(nsGroup *nbdns.NameServerGroup) string { for _, ns := range nsGroup.NameServers { servers = append(servers, fmt.Sprintf("%s:%d", ns.IP, ns.Port)) } - return fmt.Sprintf("%s_%s_%s", nsGroup.ID, nsGroup.Name, strings.Join(servers, ",")) + return fmt.Sprintf("%v_%v", servers, nsGroup.Domains) +} + +// groupNSGroupsByDomain groups nameserver groups by their match domains +func groupNSGroupsByDomain(nsGroups []*nbdns.NameServerGroup) []nsGroupsByDomain { + domainMap := make(map[string][]*nbdns.NameServerGroup) + + for _, group := range nsGroups { + if group.Primary { + domainMap[nbdns.RootZone] = append(domainMap[nbdns.RootZone], group) + continue + } + + for _, domain := range group.Domains { + if domain == "" { + continue + } + domainMap[domain] = append(domainMap[domain], group) + } + } + + var result []nsGroupsByDomain + for domain, groups := range domainMap { + result = append(result, nsGroupsByDomain{ + domain: domain, + groups: groups, + }) + } + + return result } diff --git a/client/internal/dns/server_test.go b/client/internal/dns/server_test.go index 14ff1bb71..db49f96a2 100644 --- a/client/internal/dns/server_test.go +++ b/client/internal/dns/server_test.go @@ -13,6 +13,7 @@ import ( "github.com/golang/mock/gomock" "github.com/miekg/dns" log "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "golang.zx2c4.com/wireguard/wgctrl/wgtypes" @@ -88,6 +89,18 @@ func init() { formatter.SetTextFormatter(log.StandardLogger()) } +func generateDummyHandler(domain string, servers []nbdns.NameServer) *upstreamResolverBase { + var srvs []string + for _, srv := range servers { + srvs = append(srvs, getNSHostPort(srv)) + } + return &upstreamResolverBase{ + domain: domain, + upstreamServers: srvs, + cancel: func() {}, + } +} + func TestUpdateDNSServer(t *testing.T) { nameServers := []nbdns.NameServer{ { @@ -140,15 +153,37 @@ func TestUpdateDNSServer(t *testing.T) { }, }, }, - expectedUpstreamMap: registeredHandlerMap{"netbird.io": dummyHandler, "netbird.cloud": dummyHandler, nbdns.RootZone: dummyHandler}, - expectedLocalMap: registrationMap{buildRecordKey(zoneRecords[0].Name, 1, 1): struct{}{}}, + expectedUpstreamMap: registeredHandlerMap{ + generateDummyHandler("netbird.io", nameServers).id(): handlerWrapper{ + domain: "netbird.io", + handler: dummyHandler, + priority: PriorityMatchDomain, + }, + dummyHandler.id(): handlerWrapper{ + domain: "netbird.cloud", + handler: dummyHandler, + priority: PriorityMatchDomain, + }, + generateDummyHandler(".", nameServers).id(): handlerWrapper{ + domain: nbdns.RootZone, + handler: dummyHandler, + priority: PriorityDefault, + }, + }, + expectedLocalMap: registrationMap{buildRecordKey(zoneRecords[0].Name, 1, 1): struct{}{}}, }, { - name: "New Config Should Succeed", - initLocalMap: registrationMap{"netbird.cloud": struct{}{}}, - initUpstreamMap: registeredHandlerMap{buildRecordKey(zoneRecords[0].Name, 1, 1): dummyHandler}, - initSerial: 0, - inputSerial: 1, + name: "New Config Should Succeed", + initLocalMap: registrationMap{"netbird.cloud": struct{}{}}, + initUpstreamMap: registeredHandlerMap{ + generateDummyHandler(zoneRecords[0].Name, nameServers).id(): handlerWrapper{ + domain: buildRecordKey(zoneRecords[0].Name, 1, 1), + handler: dummyHandler, + priority: PriorityMatchDomain, + }, + }, + initSerial: 0, + inputSerial: 1, inputUpdate: nbdns.Config{ ServiceEnable: true, CustomZones: []nbdns.CustomZone{ @@ -164,8 +199,19 @@ func TestUpdateDNSServer(t *testing.T) { }, }, }, - expectedUpstreamMap: registeredHandlerMap{"netbird.io": dummyHandler, "netbird.cloud": dummyHandler}, - expectedLocalMap: registrationMap{buildRecordKey(zoneRecords[0].Name, 1, 1): struct{}{}}, + expectedUpstreamMap: registeredHandlerMap{ + generateDummyHandler("netbird.io", nameServers).id(): handlerWrapper{ + domain: "netbird.io", + handler: dummyHandler, + priority: PriorityMatchDomain, + }, + "local-resolver": handlerWrapper{ + domain: "netbird.cloud", + handler: dummyHandler, + priority: PriorityMatchDomain, + }, + }, + expectedLocalMap: registrationMap{buildRecordKey(zoneRecords[0].Name, 1, 1): struct{}{}}, }, { name: "Smaller Config Serial Should Be Skipped", @@ -242,9 +288,15 @@ func TestUpdateDNSServer(t *testing.T) { shouldFail: true, }, { - name: "Empty Config Should Succeed and Clean Maps", - initLocalMap: registrationMap{"netbird.cloud": struct{}{}}, - initUpstreamMap: registeredHandlerMap{zoneRecords[0].Name: dummyHandler}, + name: "Empty Config Should Succeed and Clean Maps", + initLocalMap: registrationMap{"netbird.cloud": struct{}{}}, + initUpstreamMap: registeredHandlerMap{ + generateDummyHandler(zoneRecords[0].Name, nameServers).id(): handlerWrapper{ + domain: zoneRecords[0].Name, + handler: dummyHandler, + priority: PriorityMatchDomain, + }, + }, initSerial: 0, inputSerial: 1, inputUpdate: nbdns.Config{ServiceEnable: true}, @@ -252,9 +304,15 @@ func TestUpdateDNSServer(t *testing.T) { expectedLocalMap: make(registrationMap), }, { - name: "Disabled Service Should clean map", - initLocalMap: registrationMap{"netbird.cloud": struct{}{}}, - initUpstreamMap: registeredHandlerMap{zoneRecords[0].Name: dummyHandler}, + name: "Disabled Service Should clean map", + initLocalMap: registrationMap{"netbird.cloud": struct{}{}}, + initUpstreamMap: registeredHandlerMap{ + generateDummyHandler(zoneRecords[0].Name, nameServers).id(): handlerWrapper{ + domain: zoneRecords[0].Name, + handler: dummyHandler, + priority: PriorityMatchDomain, + }, + }, initSerial: 0, inputSerial: 1, inputUpdate: nbdns.Config{ServiceEnable: false}, @@ -421,7 +479,13 @@ func TestDNSFakeResolverHandleUpdates(t *testing.T) { } }() - dnsServer.dnsMuxMap = registeredHandlerMap{zoneRecords[0].Name: &localResolver{}} + dnsServer.dnsMuxMap = registeredHandlerMap{ + "id1": handlerWrapper{ + domain: zoneRecords[0].Name, + handler: &localResolver{}, + priority: PriorityMatchDomain, + }, + } dnsServer.localResolver.registeredMap = registrationMap{"netbird.cloud": struct{}{}} dnsServer.updateSerial = 0 @@ -562,9 +626,8 @@ func TestDNSServerUpstreamDeactivateCallback(t *testing.T) { localResolver: &localResolver{ registeredMap: make(registrationMap), }, - handlerChain: NewHandlerChain(), - handlerPriorities: make(map[string]int), - hostManager: hostManager, + handlerChain: NewHandlerChain(), + hostManager: hostManager, currentConfig: HostDNSConfig{ Domains: []DomainConfig{ {false, "domain0", false}, @@ -593,7 +656,7 @@ func TestDNSServerUpstreamDeactivateCallback(t *testing.T) { NameServers: []nbdns.NameServer{ {IP: netip.MustParseAddr("8.8.0.0"), NSType: nbdns.UDPNameServerType, Port: 53}, }, - }, nil) + }, nil, 0) deactivate(nil) expected := "domain0,domain2" @@ -903,8 +966,8 @@ func TestHandlerChain_DomainPriorities(t *testing.T) { Subdomains: true, } - chain.AddHandler("example.com.", dnsRouteHandler, PriorityDNSRoute, nil) - chain.AddHandler("example.com.", upstreamHandler, PriorityMatchDomain, nil) + chain.AddHandler("example.com.", dnsRouteHandler, PriorityDNSRoute) + chain.AddHandler("example.com.", upstreamHandler, PriorityMatchDomain) testCases := []struct { name string @@ -959,3 +1022,421 @@ func TestHandlerChain_DomainPriorities(t *testing.T) { }) } } + +type mockHandler struct { + Id string +} + +func (m *mockHandler) ServeDNS(dns.ResponseWriter, *dns.Msg) {} +func (m *mockHandler) stop() {} +func (m *mockHandler) probeAvailability() {} +func (m *mockHandler) id() handlerID { return handlerID(m.Id) } + +type mockService struct{} + +func (m *mockService) Listen() error { return nil } +func (m *mockService) Stop() {} +func (m *mockService) RuntimeIP() string { return "127.0.0.1" } +func (m *mockService) RuntimePort() int { return 53 } +func (m *mockService) RegisterMux(string, dns.Handler) {} +func (m *mockService) DeregisterMux(string) {} + +func TestDefaultServer_UpdateMux(t *testing.T) { + baseMatchHandlers := registeredHandlerMap{ + "upstream-group1": { + domain: "example.com", + handler: &mockHandler{ + Id: "upstream-group1", + }, + priority: PriorityMatchDomain, + }, + "upstream-group2": { + domain: "example.com", + handler: &mockHandler{ + Id: "upstream-group2", + }, + priority: PriorityMatchDomain - 1, + }, + } + + baseRootHandlers := registeredHandlerMap{ + "upstream-root1": { + domain: ".", + handler: &mockHandler{ + Id: "upstream-root1", + }, + priority: PriorityDefault, + }, + "upstream-root2": { + domain: ".", + handler: &mockHandler{ + Id: "upstream-root2", + }, + priority: PriorityDefault - 1, + }, + } + + baseMixedHandlers := registeredHandlerMap{ + "upstream-group1": { + domain: "example.com", + handler: &mockHandler{ + Id: "upstream-group1", + }, + priority: PriorityMatchDomain, + }, + "upstream-group2": { + domain: "example.com", + handler: &mockHandler{ + Id: "upstream-group2", + }, + priority: PriorityMatchDomain - 1, + }, + "upstream-other": { + domain: "other.com", + handler: &mockHandler{ + Id: "upstream-other", + }, + priority: PriorityMatchDomain, + }, + } + + tests := []struct { + name string + initialHandlers registeredHandlerMap + updates []handlerWrapper + expectedHandlers map[string]string // map[handlerID]domain + description string + }{ + { + name: "Remove group1 from update", + initialHandlers: baseMatchHandlers, + updates: []handlerWrapper{ + // Only group2 remains + { + domain: "example.com", + handler: &mockHandler{ + Id: "upstream-group2", + }, + priority: PriorityMatchDomain - 1, + }, + }, + expectedHandlers: map[string]string{ + "upstream-group2": "example.com", + }, + description: "When group1 is not included in the update, it should be removed while group2 remains", + }, + { + name: "Remove group2 from update", + initialHandlers: baseMatchHandlers, + updates: []handlerWrapper{ + // Only group1 remains + { + domain: "example.com", + handler: &mockHandler{ + Id: "upstream-group1", + }, + priority: PriorityMatchDomain, + }, + }, + expectedHandlers: map[string]string{ + "upstream-group1": "example.com", + }, + description: "When group2 is not included in the update, it should be removed while group1 remains", + }, + { + name: "Add group3 in first position", + initialHandlers: baseMatchHandlers, + updates: []handlerWrapper{ + // Add group3 with highest priority + { + domain: "example.com", + handler: &mockHandler{ + Id: "upstream-group3", + }, + priority: PriorityMatchDomain + 1, + }, + // Keep existing groups with their original priorities + { + domain: "example.com", + handler: &mockHandler{ + Id: "upstream-group1", + }, + priority: PriorityMatchDomain, + }, + { + domain: "example.com", + handler: &mockHandler{ + Id: "upstream-group2", + }, + priority: PriorityMatchDomain - 1, + }, + }, + expectedHandlers: map[string]string{ + "upstream-group1": "example.com", + "upstream-group2": "example.com", + "upstream-group3": "example.com", + }, + description: "When adding group3 with highest priority, it should be first in chain while maintaining existing groups", + }, + { + name: "Add group3 in last position", + initialHandlers: baseMatchHandlers, + updates: []handlerWrapper{ + // Keep existing groups with their original priorities + { + domain: "example.com", + handler: &mockHandler{ + Id: "upstream-group1", + }, + priority: PriorityMatchDomain, + }, + { + domain: "example.com", + handler: &mockHandler{ + Id: "upstream-group2", + }, + priority: PriorityMatchDomain - 1, + }, + // Add group3 with lowest priority + { + domain: "example.com", + handler: &mockHandler{ + Id: "upstream-group3", + }, + priority: PriorityMatchDomain - 2, + }, + }, + expectedHandlers: map[string]string{ + "upstream-group1": "example.com", + "upstream-group2": "example.com", + "upstream-group3": "example.com", + }, + description: "When adding group3 with lowest priority, it should be last in chain while maintaining existing groups", + }, + // Root zone tests + { + name: "Remove root1 from update", + initialHandlers: baseRootHandlers, + updates: []handlerWrapper{ + { + domain: ".", + handler: &mockHandler{ + Id: "upstream-root2", + }, + priority: PriorityDefault - 1, + }, + }, + expectedHandlers: map[string]string{ + "upstream-root2": ".", + }, + description: "When root1 is not included in the update, it should be removed while root2 remains", + }, + { + name: "Remove root2 from update", + initialHandlers: baseRootHandlers, + updates: []handlerWrapper{ + { + domain: ".", + handler: &mockHandler{ + Id: "upstream-root1", + }, + priority: PriorityDefault, + }, + }, + expectedHandlers: map[string]string{ + "upstream-root1": ".", + }, + description: "When root2 is not included in the update, it should be removed while root1 remains", + }, + { + name: "Add root3 in first position", + initialHandlers: baseRootHandlers, + updates: []handlerWrapper{ + { + domain: ".", + handler: &mockHandler{ + Id: "upstream-root3", + }, + priority: PriorityDefault + 1, + }, + { + domain: ".", + handler: &mockHandler{ + Id: "upstream-root1", + }, + priority: PriorityDefault, + }, + { + domain: ".", + handler: &mockHandler{ + Id: "upstream-root2", + }, + priority: PriorityDefault - 1, + }, + }, + expectedHandlers: map[string]string{ + "upstream-root1": ".", + "upstream-root2": ".", + "upstream-root3": ".", + }, + description: "When adding root3 with highest priority, it should be first in chain while maintaining existing root handlers", + }, + { + name: "Add root3 in last position", + initialHandlers: baseRootHandlers, + updates: []handlerWrapper{ + { + domain: ".", + handler: &mockHandler{ + Id: "upstream-root1", + }, + priority: PriorityDefault, + }, + { + domain: ".", + handler: &mockHandler{ + Id: "upstream-root2", + }, + priority: PriorityDefault - 1, + }, + { + domain: ".", + handler: &mockHandler{ + Id: "upstream-root3", + }, + priority: PriorityDefault - 2, + }, + }, + expectedHandlers: map[string]string{ + "upstream-root1": ".", + "upstream-root2": ".", + "upstream-root3": ".", + }, + description: "When adding root3 with lowest priority, it should be last in chain while maintaining existing root handlers", + }, + // Mixed domain tests + { + name: "Update with mixed domains - remove one of duplicate domain", + initialHandlers: baseMixedHandlers, + updates: []handlerWrapper{ + { + domain: "example.com", + handler: &mockHandler{ + Id: "upstream-group1", + }, + priority: PriorityMatchDomain, + }, + { + domain: "other.com", + handler: &mockHandler{ + Id: "upstream-other", + }, + priority: PriorityMatchDomain, + }, + }, + expectedHandlers: map[string]string{ + "upstream-group1": "example.com", + "upstream-other": "other.com", + }, + description: "When updating mixed domains, should correctly handle removal of one duplicate while maintaining other domains", + }, + { + name: "Update with mixed domains - add new domain", + initialHandlers: baseMixedHandlers, + updates: []handlerWrapper{ + { + domain: "example.com", + handler: &mockHandler{ + Id: "upstream-group1", + }, + priority: PriorityMatchDomain, + }, + { + domain: "example.com", + handler: &mockHandler{ + Id: "upstream-group2", + }, + priority: PriorityMatchDomain - 1, + }, + { + domain: "other.com", + handler: &mockHandler{ + Id: "upstream-other", + }, + priority: PriorityMatchDomain, + }, + { + domain: "new.com", + handler: &mockHandler{ + Id: "upstream-new", + }, + priority: PriorityMatchDomain, + }, + }, + expectedHandlers: map[string]string{ + "upstream-group1": "example.com", + "upstream-group2": "example.com", + "upstream-other": "other.com", + "upstream-new": "new.com", + }, + description: "When updating mixed domains, should maintain existing duplicates and add new domain", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + server := &DefaultServer{ + dnsMuxMap: tt.initialHandlers, + handlerChain: NewHandlerChain(), + service: &mockService{}, + } + + // Perform the update + server.updateMux(tt.updates) + + // Verify the results + assert.Equal(t, len(tt.expectedHandlers), len(server.dnsMuxMap), + "Number of handlers after update doesn't match expected") + + // Check each expected handler + for id, expectedDomain := range tt.expectedHandlers { + handler, exists := server.dnsMuxMap[handlerID(id)] + assert.True(t, exists, "Expected handler %s not found", id) + if exists { + assert.Equal(t, expectedDomain, handler.domain, + "Domain mismatch for handler %s", id) + } + } + + // Verify no unexpected handlers exist + for handlerID := range server.dnsMuxMap { + _, expected := tt.expectedHandlers[string(handlerID)] + assert.True(t, expected, "Unexpected handler found: %s", handlerID) + } + + // Verify the handlerChain state and order + previousPriority := 0 + for _, chainEntry := range server.handlerChain.handlers { + // Verify priority order + if previousPriority > 0 { + assert.True(t, chainEntry.Priority <= previousPriority, + "Handlers in chain not properly ordered by priority") + } + previousPriority = chainEntry.Priority + + // Verify handler exists in mux + foundInMux := false + for _, muxEntry := range server.dnsMuxMap { + if chainEntry.Handler == muxEntry.handler && + chainEntry.Priority == muxEntry.priority && + chainEntry.Pattern == dns.Fqdn(muxEntry.domain) { + foundInMux = true + break + } + } + assert.True(t, foundInMux, + "Handler in chain not found in dnsMuxMap") + } + }) + } +} diff --git a/client/internal/dns/upstream.go b/client/internal/dns/upstream.go index f0aa12b65..4c69a173d 100644 --- a/client/internal/dns/upstream.go +++ b/client/internal/dns/upstream.go @@ -2,9 +2,13 @@ package dns import ( "context" + "crypto/sha256" + "encoding/hex" "errors" "fmt" "net" + "slices" + "strings" "sync" "sync/atomic" "time" @@ -40,6 +44,7 @@ type upstreamResolverBase struct { cancel context.CancelFunc upstreamClient upstreamClient upstreamServers []string + domain string disabled bool failsCount atomic.Int32 successCount atomic.Int32 @@ -53,12 +58,13 @@ type upstreamResolverBase struct { statusRecorder *peer.Status } -func newUpstreamResolverBase(ctx context.Context, statusRecorder *peer.Status) *upstreamResolverBase { +func newUpstreamResolverBase(ctx context.Context, statusRecorder *peer.Status, domain string) *upstreamResolverBase { ctx, cancel := context.WithCancel(ctx) return &upstreamResolverBase{ ctx: ctx, cancel: cancel, + domain: domain, upstreamTimeout: upstreamTimeout, reactivatePeriod: reactivatePeriod, failsTillDeact: failsTillDeact, @@ -71,6 +77,17 @@ func (u *upstreamResolverBase) String() string { return fmt.Sprintf("upstream %v", u.upstreamServers) } +// ID returns the unique handler ID +func (u *upstreamResolverBase) id() handlerID { + servers := slices.Clone(u.upstreamServers) + slices.Sort(servers) + + hash := sha256.New() + hash.Write([]byte(u.domain + ":")) + hash.Write([]byte(strings.Join(servers, ","))) + return handlerID("upstream-" + hex.EncodeToString(hash.Sum(nil)[:8])) +} + func (u *upstreamResolverBase) MatchSubdomains() bool { return true } @@ -87,7 +104,7 @@ func (u *upstreamResolverBase) ServeDNS(w dns.ResponseWriter, r *dns.Msg) { u.checkUpstreamFails(err) }() - log.WithField("question", r.Question[0]).Trace("received an upstream question") + log.Tracef("received upstream question: domain=%s type=%v class=%v", r.Question[0].Name, r.Question[0].Qtype, r.Question[0].Qclass) // set the AuthenticatedData flag and the EDNS0 buffer size to 4096 bytes to support larger dns records if r.Extra == nil { r.SetEdns0(4096, false) @@ -96,6 +113,7 @@ func (u *upstreamResolverBase) ServeDNS(w dns.ResponseWriter, r *dns.Msg) { select { case <-u.ctx.Done(): + log.Tracef("%s has been stopped", u) return default: } @@ -112,41 +130,36 @@ func (u *upstreamResolverBase) ServeDNS(w dns.ResponseWriter, r *dns.Msg) { if err != nil { if errors.Is(err, context.DeadlineExceeded) || isTimeout(err) { - log.WithError(err).WithField("upstream", upstream). - Warn("got an error while connecting to upstream") + log.Warnf("upstream %s timed out for question domain=%s", upstream, r.Question[0].Name) continue } - u.failsCount.Add(1) - log.WithError(err).WithField("upstream", upstream). - Error("got other error while querying the upstream") - return + log.Warnf("failed to query upstream %s for question domain=%s: %s", upstream, r.Question[0].Name, err) + continue } - if rm == nil { - log.WithError(err).WithField("upstream", upstream). - Warn("no response from upstream") - return - } - // those checks need to be independent of each other due to memory address issues - if !rm.Response { - log.WithError(err).WithField("upstream", upstream). - Warn("no response from upstream") - return + if rm == nil || !rm.Response { + log.Warnf("no response from upstream %s for question domain=%s", upstream, r.Question[0].Name) + continue } u.successCount.Add(1) - log.Tracef("took %s to query the upstream %s", t, upstream) + log.Tracef("took %s to query the upstream %s for question domain=%s", t, upstream, r.Question[0].Name) - err = w.WriteMsg(rm) - if err != nil { - log.WithError(err).Error("got an error while writing the upstream resolver response") + if err = w.WriteMsg(rm); err != nil { + log.Errorf("failed to write DNS response for question domain=%s: %s", r.Question[0].Name, err) } // count the fails only if they happen sequentially u.failsCount.Store(0) return } u.failsCount.Add(1) - log.Error("all queries to the upstream nameservers failed with timeout") + log.Errorf("all queries to the %s failed for question domain=%s", u, r.Question[0].Name) + + m := new(dns.Msg) + m.SetRcode(r, dns.RcodeServerFailure) + if err := w.WriteMsg(m); err != nil { + log.Errorf("failed to write error response for %s for question domain=%s: %s", u, r.Question[0].Name, err) + } } // checkUpstreamFails counts fails and disables or enables upstream resolving diff --git a/client/internal/dns/upstream_android.go b/client/internal/dns/upstream_android.go index 36ea05e44..a9e46ca02 100644 --- a/client/internal/dns/upstream_android.go +++ b/client/internal/dns/upstream_android.go @@ -27,8 +27,9 @@ func newUpstreamResolver( _ *net.IPNet, statusRecorder *peer.Status, hostsDNSHolder *hostsDNSHolder, + domain string, ) (*upstreamResolver, error) { - upstreamResolverBase := newUpstreamResolverBase(ctx, statusRecorder) + upstreamResolverBase := newUpstreamResolverBase(ctx, statusRecorder, domain) c := &upstreamResolver{ upstreamResolverBase: upstreamResolverBase, hostsDNSHolder: hostsDNSHolder, diff --git a/client/internal/dns/upstream_general.go b/client/internal/dns/upstream_general.go index a29350f8c..51acbf7a6 100644 --- a/client/internal/dns/upstream_general.go +++ b/client/internal/dns/upstream_general.go @@ -23,8 +23,9 @@ func newUpstreamResolver( _ *net.IPNet, statusRecorder *peer.Status, _ *hostsDNSHolder, + domain string, ) (*upstreamResolver, error) { - upstreamResolverBase := newUpstreamResolverBase(ctx, statusRecorder) + upstreamResolverBase := newUpstreamResolverBase(ctx, statusRecorder, domain) nonIOS := &upstreamResolver{ upstreamResolverBase: upstreamResolverBase, } diff --git a/client/internal/dns/upstream_ios.go b/client/internal/dns/upstream_ios.go index 60ed79d87..7d3301e14 100644 --- a/client/internal/dns/upstream_ios.go +++ b/client/internal/dns/upstream_ios.go @@ -30,8 +30,9 @@ func newUpstreamResolver( net *net.IPNet, statusRecorder *peer.Status, _ *hostsDNSHolder, + domain string, ) (*upstreamResolverIOS, error) { - upstreamResolverBase := newUpstreamResolverBase(ctx, statusRecorder) + upstreamResolverBase := newUpstreamResolverBase(ctx, statusRecorder, domain) ios := &upstreamResolverIOS{ upstreamResolverBase: upstreamResolverBase, diff --git a/client/internal/dns/upstream_test.go b/client/internal/dns/upstream_test.go index c1251dcc1..c5adc0858 100644 --- a/client/internal/dns/upstream_test.go +++ b/client/internal/dns/upstream_test.go @@ -20,6 +20,7 @@ func TestUpstreamResolver_ServeDNS(t *testing.T) { timeout time.Duration cancelCTX bool expectedAnswer string + acceptNXDomain bool }{ { name: "Should Resolve A Record", @@ -36,11 +37,11 @@ func TestUpstreamResolver_ServeDNS(t *testing.T) { expectedAnswer: "1.1.1.1", }, { - name: "Should Not Resolve If Can't Connect To Both Servers", - inputMSG: new(dns.Msg).SetQuestion("one.one.one.one.", dns.TypeA), - InputServers: []string{"8.0.0.0:53", "8.0.0.1:53"}, - timeout: 200 * time.Millisecond, - responseShouldBeNil: true, + name: "Should Not Resolve If Can't Connect To Both Servers", + inputMSG: new(dns.Msg).SetQuestion("one.one.one.one.", dns.TypeA), + InputServers: []string{"8.0.0.0:53", "8.0.0.1:53"}, + timeout: 200 * time.Millisecond, + acceptNXDomain: true, }, { name: "Should Not Resolve If Parent Context Is Canceled", @@ -51,14 +52,11 @@ func TestUpstreamResolver_ServeDNS(t *testing.T) { responseShouldBeNil: true, }, } - // should resolve if first upstream times out - // should not write when both fails - // should not resolve if parent context is canceled for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { ctx, cancel := context.WithCancel(context.TODO()) - resolver, _ := newUpstreamResolver(ctx, "", net.IP{}, &net.IPNet{}, nil, nil) + resolver, _ := newUpstreamResolver(ctx, "", net.IP{}, &net.IPNet{}, nil, nil, ".") resolver.upstreamServers = testCase.InputServers resolver.upstreamTimeout = testCase.timeout if testCase.cancelCTX { @@ -84,16 +82,22 @@ func TestUpstreamResolver_ServeDNS(t *testing.T) { t.Fatalf("should write a response message") } - foundAnswer := false - for _, answer := range responseMSG.Answer { - if strings.Contains(answer.String(), testCase.expectedAnswer) { - foundAnswer = true - break - } + if testCase.acceptNXDomain && responseMSG.Rcode == dns.RcodeNameError { + return } - if !foundAnswer { - t.Errorf("couldn't find the required answer, %s, in the dns response", testCase.expectedAnswer) + if testCase.expectedAnswer != "" { + foundAnswer := false + for _, answer := range responseMSG.Answer { + if strings.Contains(answer.String(), testCase.expectedAnswer) { + foundAnswer = true + break + } + } + + if !foundAnswer { + t.Errorf("couldn't find the required answer, %s, in the dns response", testCase.expectedAnswer) + } } }) } diff --git a/client/internal/peer/status.go b/client/internal/peer/status.go index 0df2a2e81..311ddbd7f 100644 --- a/client/internal/peer/status.go +++ b/client/internal/peer/status.go @@ -721,7 +721,9 @@ func (d *Status) GetRelayStates() []relay.ProbeResult { func (d *Status) GetDNSStates() []NSGroupState { d.mux.Lock() defer d.mux.Unlock() - return d.nsGroupStates + + // shallow copy is good enough, as slices fields are currently not updated + return slices.Clone(d.nsGroupStates) } func (d *Status) GetResolvedDomainsStates() map[domain.Domain]ResolvedDomainInfo { From 44407a158a9416e076e5c9ca615134c42c64a036 Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Tue, 11 Feb 2025 12:42:04 +0100 Subject: [PATCH 54/92] [client] Fix dns handler chain test (#3307) --- client/internal/dns/handler_chain_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/internal/dns/handler_chain_test.go b/client/internal/dns/handler_chain_test.go index 8c66446ee..94aa987af 100644 --- a/client/internal/dns/handler_chain_test.go +++ b/client/internal/dns/handler_chain_test.go @@ -795,7 +795,7 @@ func TestHandlerChain_DomainSpecificityOrdering(t *testing.T) { if op.action == "add" { handler := &nbdns.MockSubdomainHandler{Subdomains: op.subdomain} handlers[op.pattern] = handler - chain.AddHandler(op.pattern, handler, op.priority, nil) + chain.AddHandler(op.pattern, handler, op.priority) } else { chain.RemoveHandler(op.pattern, op.priority) } From 18f84f0df5e978f0bfb4435ca3d9282b8ee9ac31 Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Tue, 11 Feb 2025 13:09:17 +0100 Subject: [PATCH 55/92] [client] Check for fwmark support and use fallback routing if not supported (#3220) --- client/iface/configurer/usp.go | 2 +- client/internal/connect.go | 3 + client/internal/routemanager/manager.go | 11 +- .../routemanager/systemops/systemops_linux.go | 29 +---- .../systemops/systemops_unix_test.go | 1 + util/grpc/dialer.go | 1 - util/net/env.go | 21 ++-- util/net/env_generic.go | 12 ++ util/net/env_linux.go | 119 ++++++++++++++++++ util/net/net_linux.go | 20 +-- 10 files changed, 163 insertions(+), 56 deletions(-) create mode 100644 util/net/env_generic.go create mode 100644 util/net/env_linux.go diff --git a/client/iface/configurer/usp.go b/client/iface/configurer/usp.go index 21d65ab2a..391269dd0 100644 --- a/client/iface/configurer/usp.go +++ b/client/iface/configurer/usp.go @@ -362,7 +362,7 @@ func toWgUserspaceString(wgCfg wgtypes.Config) string { } func getFwmark() int { - if runtime.GOOS == "linux" && !nbnet.CustomRoutingDisabled() { + if nbnet.AdvancedRouting() { return nbnet.NetbirdFwmark } return 0 diff --git a/client/internal/connect.go b/client/internal/connect.go index ddd10e5cd..a0d585ffe 100644 --- a/client/internal/connect.go +++ b/client/internal/connect.go @@ -31,6 +31,7 @@ import ( relayClient "github.com/netbirdio/netbird/relay/client" signal "github.com/netbirdio/netbird/signal/client" "github.com/netbirdio/netbird/util" + nbnet "github.com/netbirdio/netbird/util/net" "github.com/netbirdio/netbird/version" ) @@ -109,6 +110,8 @@ func (c *ConnectClient) run(mobileDependency MobileDependency, runningChan chan log.Infof("starting NetBird client version %s on %s/%s", version.NetbirdVersion(), runtime.GOOS, runtime.GOARCH) + nbnet.Init() + backOff := &backoff.ExponentialBackOff{ InitialInterval: time.Second, RandomizationFactor: 1, diff --git a/client/internal/routemanager/manager.go b/client/internal/routemanager/manager.go index 34bd67893..9c7f1f6fa 100644 --- a/client/internal/routemanager/manager.go +++ b/client/internal/routemanager/manager.go @@ -113,13 +113,14 @@ func NewManager(config ManagerConfig) *DefaultManager { disableServerRoutes: config.DisableServerRoutes, } + useNoop := netstack.IsEnabled() || config.DisableClientRoutes + dm.setupRefCounters(useNoop) + // don't proceed with client routes if it is disabled if config.DisableClientRoutes { return dm } - dm.setupRefCounters() - if runtime.GOOS == "android" { cr := dm.initialClientRoutes(config.InitialRoutes) dm.notifier.SetInitialClientRoutes(cr) @@ -127,7 +128,7 @@ func NewManager(config ManagerConfig) *DefaultManager { return dm } -func (m *DefaultManager) setupRefCounters() { +func (m *DefaultManager) setupRefCounters(useNoop bool) { m.routeRefCounter = refcounter.New( func(prefix netip.Prefix, _ struct{}) (struct{}, error) { return struct{}{}, m.sysOps.AddVPNRoute(prefix, m.wgInterface.ToInterface()) @@ -137,7 +138,7 @@ func (m *DefaultManager) setupRefCounters() { }, ) - if netstack.IsEnabled() { + if useNoop { m.routeRefCounter = refcounter.New( func(netip.Prefix, struct{}) (struct{}, error) { return struct{}{}, refcounter.ErrIgnore @@ -449,7 +450,7 @@ func (m *DefaultManager) initialClientRoutes(initialRoutes []*route.Route) []*ro } func isRouteSupported(route *route.Route) bool { - if !nbnet.CustomRoutingDisabled() || route.IsDynamic() { + if netstack.IsEnabled() || !nbnet.CustomRoutingDisabled() || route.IsDynamic() { return true } diff --git a/client/internal/routemanager/systemops/systemops_linux.go b/client/internal/routemanager/systemops/systemops_linux.go index 1da92cc80..d724cb1a7 100644 --- a/client/internal/routemanager/systemops/systemops_linux.go +++ b/client/internal/routemanager/systemops/systemops_linux.go @@ -53,20 +53,6 @@ type ruleParams struct { description string } -// isLegacy determines whether to use the legacy routing setup -func isLegacy() bool { - return os.Getenv("NB_USE_LEGACY_ROUTING") == "true" || nbnet.CustomRoutingDisabled() || nbnet.SkipSocketMark() -} - -// setIsLegacy sets the legacy routing setup -func setIsLegacy(b bool) { - if b { - os.Setenv("NB_USE_LEGACY_ROUTING", "true") - } else { - os.Unsetenv("NB_USE_LEGACY_ROUTING") - } -} - func getSetupRules() []ruleParams { return []ruleParams{ {100, -1, syscall.RT_TABLE_MAIN, netlink.FAMILY_V4, false, 0, "rule with suppress prefixlen v4"}, @@ -87,7 +73,7 @@ func getSetupRules() []ruleParams { // This table is where a default route or other specific routes received from the management server are configured, // enabling VPN connectivity. func (r *SysOps) SetupRouting(initAddresses []net.IP, stateManager *statemanager.Manager) (_ nbnet.AddHookFunc, _ nbnet.RemoveHookFunc, err error) { - if isLegacy() { + if !nbnet.AdvancedRouting() { log.Infof("Using legacy routing setup") return r.setupRefCounter(initAddresses, stateManager) } @@ -103,11 +89,6 @@ func (r *SysOps) SetupRouting(initAddresses []net.IP, stateManager *statemanager rules := getSetupRules() for _, rule := range rules { if err := addRule(rule); err != nil { - if errors.Is(err, syscall.EOPNOTSUPP) { - log.Warnf("Rule operations are not supported, falling back to the legacy routing setup") - setIsLegacy(true) - return r.setupRefCounter(initAddresses, stateManager) - } return nil, nil, fmt.Errorf("%s: %w", rule.description, err) } } @@ -130,7 +111,7 @@ func (r *SysOps) SetupRouting(initAddresses []net.IP, stateManager *statemanager // It systematically removes the three rules and any associated routing table entries to ensure a clean state. // The function uses error aggregation to report any errors encountered during the cleanup process. func (r *SysOps) CleanupRouting(stateManager *statemanager.Manager) error { - if isLegacy() { + if !nbnet.AdvancedRouting() { return r.cleanupRefCounter(stateManager) } @@ -168,7 +149,7 @@ func (r *SysOps) removeFromRouteTable(prefix netip.Prefix, nexthop Nexthop) erro } func (r *SysOps) AddVPNRoute(prefix netip.Prefix, intf *net.Interface) error { - if isLegacy() { + if !nbnet.AdvancedRouting() { return r.genericAddVPNRoute(prefix, intf) } @@ -191,7 +172,7 @@ func (r *SysOps) AddVPNRoute(prefix netip.Prefix, intf *net.Interface) error { } func (r *SysOps) RemoveVPNRoute(prefix netip.Prefix, intf *net.Interface) error { - if isLegacy() { + if !nbnet.AdvancedRouting() { return r.genericRemoveVPNRoute(prefix, intf) } @@ -504,7 +485,7 @@ func getAddressFamily(prefix netip.Prefix) int { } func hasSeparateRouting() ([]netip.Prefix, error) { - if isLegacy() { + if !nbnet.AdvancedRouting() { return GetRoutesFromTable() } return nil, ErrRoutingIsSeparate diff --git a/client/internal/routemanager/systemops/systemops_unix_test.go b/client/internal/routemanager/systemops/systemops_unix_test.go index a6000d963..d88c1ab6b 100644 --- a/client/internal/routemanager/systemops/systemops_unix_test.go +++ b/client/internal/routemanager/systemops/systemops_unix_test.go @@ -85,6 +85,7 @@ var testCases = []testCase{ } func TestRouting(t *testing.T) { + nbnet.Init() for _, tc := range testCases { // todo resolve test execution on freebsd if runtime.GOOS == "freebsd" { diff --git a/util/grpc/dialer.go b/util/grpc/dialer.go index 83a11c65d..f6d6d2f04 100644 --- a/util/grpc/dialer.go +++ b/util/grpc/dialer.go @@ -40,7 +40,6 @@ func WithCustomDialer() grpc.DialOption { } } - log.Debug("Using nbnet.NewDialer()") conn, err := nbnet.NewDialer().DialContext(ctx, "tcp", addr) if err != nil { log.Errorf("Failed to dial: %s", err) diff --git a/util/net/env.go b/util/net/env.go index 099da39b7..32425665d 100644 --- a/util/net/env.go +++ b/util/net/env.go @@ -2,6 +2,7 @@ package net import ( "os" + "strconv" log "github.com/sirupsen/logrus" @@ -10,20 +11,24 @@ import ( const ( envDisableCustomRouting = "NB_DISABLE_CUSTOM_ROUTING" - envSkipSocketMark = "NB_SKIP_SOCKET_MARK" ) +// CustomRoutingDisabled returns true if custom routing is disabled. +// This will fall back to the operation mode before the exit node functionality was implemented. +// In particular exclusion routes won't be set up and all dialers and listeners will use net.Dial and net.Listen, respectively. func CustomRoutingDisabled() bool { if netstack.IsEnabled() { return true } - return os.Getenv(envDisableCustomRouting) == "true" -} -func SkipSocketMark() bool { - if skipSocketMark := os.Getenv(envSkipSocketMark); skipSocketMark == "true" { - log.Infof("%s is set to true, skipping SO_MARK", envSkipSocketMark) - return true + var customRoutingDisabled bool + if val := os.Getenv(envDisableCustomRouting); val != "" { + var err error + customRoutingDisabled, err = strconv.ParseBool(val) + if err != nil { + log.Warnf("failed to parse %s: %v", envDisableCustomRouting, err) + } } - return false + + return customRoutingDisabled } diff --git a/util/net/env_generic.go b/util/net/env_generic.go new file mode 100644 index 000000000..6d142a838 --- /dev/null +++ b/util/net/env_generic.go @@ -0,0 +1,12 @@ +//go:build !linux || android + +package net + +func Init() { + // nothing to do on non-linux +} + +func AdvancedRouting() bool { + // non-linux currently doesn't support advanced routing + return false +} diff --git a/util/net/env_linux.go b/util/net/env_linux.go new file mode 100644 index 000000000..124bf64de --- /dev/null +++ b/util/net/env_linux.go @@ -0,0 +1,119 @@ +//go:build linux && !android + +package net + +import ( + "errors" + "os" + "strconv" + "syscall" + "time" + + log "github.com/sirupsen/logrus" + "github.com/vishvananda/netlink" + + "github.com/netbirdio/netbird/client/iface/netstack" +) + +const ( + // these have the same effect, skip socket env supported for backward compatibility + envSkipSocketMark = "NB_SKIP_SOCKET_MARK" + envUseLegacyRouting = "NB_USE_LEGACY_ROUTING" +) + +var advancedRoutingSupported bool + +func Init() { + advancedRoutingSupported = checkAdvancedRoutingSupport() +} + +func AdvancedRouting() bool { + return advancedRoutingSupported +} + +func checkAdvancedRoutingSupport() bool { + var err error + + var legacyRouting bool + if val := os.Getenv(envUseLegacyRouting); val != "" { + legacyRouting, err = strconv.ParseBool(val) + if err != nil { + log.Warnf("failed to parse %s: %v", envUseLegacyRouting, err) + } + } + + var skipSocketMark bool + if val := os.Getenv(envSkipSocketMark); val != "" { + skipSocketMark, err = strconv.ParseBool(val) + if err != nil { + log.Warnf("failed to parse %s: %v", envSkipSocketMark, err) + } + } + + // requested to disable advanced routing + if legacyRouting || skipSocketMark || + // envCustomRoutingDisabled disables the custom dialers. + // There is no point in using advanced routing without those, as they set up fwmarks on the sockets. + CustomRoutingDisabled() || + // netstack mode doesn't need routing at all + netstack.IsEnabled() { + + log.Info("advanced routing has been requested to be disabled") + return false + } + + if !CheckFwmarkSupport() || !CheckRuleOperationsSupport() { + log.Warn("system doesn't support required routing features, falling back to legacy routing") + return false + } + + log.Info("system supports advanced routing") + + return true +} + +func CheckFwmarkSupport() bool { + // temporarily enable advanced routing to check fwmarks are supported + old := advancedRoutingSupported + advancedRoutingSupported = true + defer func() { + advancedRoutingSupported = old + }() + + dialer := NewDialer() + dialer.Timeout = 100 * time.Millisecond + + conn, err := dialer.Dial("udp", "127.0.0.1:9") + if err != nil { + log.Warnf("failed to dial with fwmark: %v", err) + return false + } + if err := conn.Close(); err != nil { + log.Warnf("failed to close connection: %v", err) + + } + + return true +} + +func CheckRuleOperationsSupport() bool { + rule := netlink.NewRule() + // low precedence, semi-random + rule.Priority = 32321 + rule.Table = syscall.RT_TABLE_MAIN + rule.Family = netlink.FAMILY_V4 + + if err := netlink.RuleAdd(rule); err != nil { + if errors.Is(err, syscall.EOPNOTSUPP) { + log.Warn("IP rule operations are not supported") + return false + } + log.Warnf("failed to test rule support: %v", err) + return false + } + + if err := netlink.RuleDel(rule); err != nil { + log.Warnf("failed to delete test rule: %v", err) + } + return true +} diff --git a/util/net/net_linux.go b/util/net/net_linux.go index fc486ebd4..eae483a26 100644 --- a/util/net/net_linux.go +++ b/util/net/net_linux.go @@ -5,13 +5,11 @@ package net import ( "fmt" "syscall" - - log "github.com/sirupsen/logrus" ) // SetSocketMark sets the SO_MARK option on the given socket connection func SetSocketMark(conn syscall.Conn) error { - if isSocketMarkDisabled() { + if !AdvancedRouting() { return nil } @@ -25,7 +23,7 @@ func SetSocketMark(conn syscall.Conn) error { // SetSocketOpt sets the SO_MARK option on the given file descriptor func SetSocketOpt(fd int) error { - if isSocketMarkDisabled() { + if !AdvancedRouting() { return nil } @@ -36,7 +34,7 @@ func setRawSocketMark(conn syscall.RawConn) error { var setErr error err := conn.Control(func(fd uintptr) { - if isSocketMarkDisabled() { + if !AdvancedRouting() { return } setErr = setSocketOptInt(int(fd)) @@ -55,15 +53,3 @@ func setRawSocketMark(conn syscall.RawConn) error { func setSocketOptInt(fd int) error { return syscall.SetsockoptInt(fd, syscall.SOL_SOCKET, syscall.SO_MARK, NetbirdFwmark) } - -func isSocketMarkDisabled() bool { - if CustomRoutingDisabled() { - log.Infof("Custom routing is disabled, skipping SO_MARK") - return true - } - - if SkipSocketMark() { - return true - } - return false -} From b41de7fcd1ed6a38022e9e7d9acdb642e57f50a6 Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Wed, 12 Feb 2025 11:10:49 +0100 Subject: [PATCH 56/92] [client] Enable userspace forwarder conditionally (#3309) * Enable userspace forwarder conditionally * Move disable/enable logic --- client/firewall/iptables/manager_linux.go | 8 + client/firewall/manager/firewall.go | 4 + client/firewall/nftables/manager_linux.go | 8 + client/firewall/uspfilter/uspfilter.go | 154 +++++++++++++----- .../uspfilter/uspfilter_filter_test.go | 1 + client/internal/routemanager/manager.go | 12 +- .../routemanager/server_nonandroid.go | 12 +- 7 files changed, 145 insertions(+), 54 deletions(-) diff --git a/client/firewall/iptables/manager_linux.go b/client/firewall/iptables/manager_linux.go index 679f288e3..929e8a656 100644 --- a/client/firewall/iptables/manager_linux.go +++ b/client/firewall/iptables/manager_linux.go @@ -218,6 +218,14 @@ func (m *Manager) SetLogLevel(log.Level) { // not supported } +func (m *Manager) EnableRouting() error { + return nil +} + +func (m *Manager) DisableRouting() error { + return nil +} + func getConntrackEstablished() []string { return []string{"-m", "conntrack", "--ctstate", "RELATED,ESTABLISHED", "-j", "ACCEPT"} } diff --git a/client/firewall/manager/firewall.go b/client/firewall/manager/firewall.go index de25ff1f1..d007e20a5 100644 --- a/client/firewall/manager/firewall.go +++ b/client/firewall/manager/firewall.go @@ -101,6 +101,10 @@ type Manager interface { Flush() error SetLogLevel(log.Level) + + EnableRouting() error + + DisableRouting() error } func GenKey(format string, pair RouterPair) string { diff --git a/client/firewall/nftables/manager_linux.go b/client/firewall/nftables/manager_linux.go index 4fe52bd53..de68f3291 100644 --- a/client/firewall/nftables/manager_linux.go +++ b/client/firewall/nftables/manager_linux.go @@ -323,6 +323,14 @@ func (m *Manager) SetLogLevel(log.Level) { // not supported } +func (m *Manager) EnableRouting() error { + return nil +} + +func (m *Manager) DisableRouting() error { + return nil +} + // Flush rule/chain/set operations from the buffer // // Method also get all rules after flush and refreshes handle values in the rulesets diff --git a/client/firewall/uspfilter/uspfilter.go b/client/firewall/uspfilter/uspfilter.go index 889e4cbb1..5bb225ccd 100644 --- a/client/firewall/uspfilter/uspfilter.go +++ b/client/firewall/uspfilter/uspfilter.go @@ -74,6 +74,8 @@ type Manager struct { mutex sync.RWMutex + // indicates whether server routes are disabled + disableServerRoutes bool // indicates whether we forward packets not destined for ourselves routingEnabled bool // indicates whether we leave forwarding and filtering to the native firewall @@ -125,15 +127,27 @@ func CreateWithNativeFirewall(iface common.IFaceMapper, nativeFirewall firewall. return mgr, nil } +func parseCreateEnv() (bool, bool) { + var disableConntrack, enableLocalForwarding bool + var err error + if val := os.Getenv(EnvDisableConntrack); val != "" { + disableConntrack, err = strconv.ParseBool(val) + if err != nil { + log.Warnf("failed to parse %s: %v", EnvDisableConntrack, err) + } + } + if val := os.Getenv(EnvEnableNetstackLocalForwarding); val != "" { + enableLocalForwarding, err = strconv.ParseBool(val) + if err != nil { + log.Warnf("failed to parse %s: %v", EnvEnableNetstackLocalForwarding, err) + } + } + + return disableConntrack, enableLocalForwarding +} + func create(iface common.IFaceMapper, nativeFirewall firewall.Manager, disableServerRoutes bool) (*Manager, error) { - disableConntrack, err := strconv.ParseBool(os.Getenv(EnvDisableConntrack)) - if err != nil { - log.Warnf("failed to parse %s: %v", EnvDisableConntrack, err) - } - enableLocalForwarding, err := strconv.ParseBool(os.Getenv(EnvEnableNetstackLocalForwarding)) - if err != nil { - log.Warnf("failed to parse %s: %v", EnvEnableNetstackLocalForwarding, err) - } + disableConntrack, enableLocalForwarding := parseCreateEnv() m := &Manager{ decoders: sync.Pool{ @@ -149,15 +163,16 @@ func create(iface common.IFaceMapper, nativeFirewall firewall.Manager, disableSe return d }, }, - nativeFirewall: nativeFirewall, - outgoingRules: make(map[string]RuleSet), - incomingRules: make(map[string]RuleSet), - wgIface: iface, - localipmanager: newLocalIPManager(), - routingEnabled: false, - stateful: !disableConntrack, - logger: nblog.NewFromLogrus(log.StandardLogger()), - netstack: netstack.IsEnabled(), + nativeFirewall: nativeFirewall, + outgoingRules: make(map[string]RuleSet), + incomingRules: make(map[string]RuleSet), + wgIface: iface, + localipmanager: newLocalIPManager(), + disableServerRoutes: disableServerRoutes, + routingEnabled: false, + stateful: !disableConntrack, + logger: nblog.NewFromLogrus(log.StandardLogger()), + netstack: netstack.IsEnabled(), // default true for non-netstack, for netstack only if explicitly enabled localForwarding: !netstack.IsEnabled() || enableLocalForwarding, } @@ -166,7 +181,6 @@ func create(iface common.IFaceMapper, nativeFirewall firewall.Manager, disableSe return nil, fmt.Errorf("update local IPs: %w", err) } - // Only initialize trackers if stateful mode is enabled if disableConntrack { log.Info("conntrack is disabled") } else { @@ -175,7 +189,12 @@ func create(iface common.IFaceMapper, nativeFirewall firewall.Manager, disableSe m.tcpTracker = conntrack.NewTCPTracker(conntrack.DefaultTCPTimeout, m.logger) } - m.determineRouting(iface, disableServerRoutes) + // netstack needs the forwarder for local traffic + if m.netstack && m.localForwarding { + if err := m.initForwarder(); err != nil { + log.Errorf("failed to initialize forwarder: %v", err) + } + } if err := m.blockInvalidRouted(iface); err != nil { log.Errorf("failed to block invalid routed traffic: %v", err) @@ -213,9 +232,21 @@ func (m *Manager) blockInvalidRouted(iface common.IFaceMapper) error { return nil } -func (m *Manager) determineRouting(iface common.IFaceMapper, disableServerRoutes bool) { - disableUspRouting, _ := strconv.ParseBool(os.Getenv(EnvDisableUserspaceRouting)) - forceUserspaceRouter, _ := strconv.ParseBool(os.Getenv(EnvForceUserspaceRouter)) +func (m *Manager) determineRouting() error { + var disableUspRouting, forceUserspaceRouter bool + var err error + if val := os.Getenv(EnvDisableUserspaceRouting); val != "" { + disableUspRouting, err = strconv.ParseBool(val) + if err != nil { + log.Warnf("failed to parse %s: %v", EnvDisableUserspaceRouting, err) + } + } + if val := os.Getenv(EnvForceUserspaceRouter); val != "" { + forceUserspaceRouter, err = strconv.ParseBool(val) + if err != nil { + log.Warnf("failed to parse %s: %v", EnvForceUserspaceRouter, err) + } + } switch { case disableUspRouting: @@ -223,7 +254,7 @@ func (m *Manager) determineRouting(iface common.IFaceMapper, disableServerRoutes m.nativeRouter = false log.Info("userspace routing is disabled") - case disableServerRoutes: + case m.disableServerRoutes: // if server routes are disabled we will let packets pass to the native stack m.routingEnabled = true m.nativeRouter = true @@ -252,32 +283,37 @@ func (m *Manager) determineRouting(iface common.IFaceMapper, disableServerRoutes log.Info("userspace routing enabled by default") } - // netstack needs the forwarder for local traffic - if m.netstack && m.localForwarding || - m.routingEnabled && !m.nativeRouter { - - m.initForwarder(iface) + if m.routingEnabled && !m.nativeRouter { + return m.initForwarder() } + + return nil } // initForwarder initializes the forwarder, it disables routing on errors -func (m *Manager) initForwarder(iface common.IFaceMapper) { - // Only supported in userspace mode as we need to inject packets back into wireguard directly - intf := iface.GetWGDevice() - if intf == nil { - log.Info("forwarding not supported") - m.routingEnabled = false - return +func (m *Manager) initForwarder() error { + if m.forwarder != nil { + return nil } - forwarder, err := forwarder.New(iface, m.logger, m.netstack) - if err != nil { - log.Errorf("failed to create forwarder: %v", err) + // Only supported in userspace mode as we need to inject packets back into wireguard directly + intf := m.wgIface.GetWGDevice() + if intf == nil { m.routingEnabled = false - return + return errors.New("forwarding not supported") + } + + forwarder, err := forwarder.New(m.wgIface, m.logger, m.netstack) + if err != nil { + m.routingEnabled = false + return fmt.Errorf("create forwarder: %w", err) } m.forwarder = forwarder + + log.Debug("forwarder initialized") + + return nil } func (m *Manager) Init(*statemanager.Manager) error { @@ -285,7 +321,7 @@ func (m *Manager) Init(*statemanager.Manager) error { } func (m *Manager) IsServerRouteSupported() bool { - return m.nativeFirewall != nil || m.routingEnabled && m.forwarder != nil + return true } func (m *Manager) AddNatRule(pair firewall.RouterPair) error { @@ -586,7 +622,6 @@ func (m *Manager) dropFilter(packetData []byte) bool { defer m.decoders.Put(d) if !m.isValidPacket(d, packetData) { - m.logger.Trace("Invalid packet structure") return true } @@ -658,11 +693,9 @@ func (m *Manager) handleRoutedTraffic(d *decoder, srcIP, dstIP net.IP, packetDat return false } - // Get protocol and ports for route ACL check proto := getProtocolFromPacket(d) srcPort, dstPort := getPortsFromPacket(d) - // Check route ACLs if !m.routeACLsPass(srcIP, dstIP, proto, srcPort, dstPort) { m.logger.Trace("Dropping routed packet (ACL denied): src=%s:%d dst=%s:%d proto=%v", srcIP, srcPort, dstIP, dstPort, proto) @@ -704,12 +737,12 @@ func getPortsFromPacket(d *decoder) (srcPort, dstPort uint16) { func (m *Manager) isValidPacket(d *decoder, packetData []byte) bool { if err := d.parser.DecodeLayers(packetData, &d.decoded); err != nil { - log.Tracef("couldn't decode layer, err: %s", err) + m.logger.Trace("couldn't decode packet, err: %s", err) return false } if len(d.decoded) < 2 { - log.Tracef("not enough levels in network packet") + m.logger.Trace("packet doesn't have network and transport layers") return false } return true @@ -953,3 +986,34 @@ func (m *Manager) SetLogLevel(level log.Level) { m.logger.SetLevel(nblog.Level(level)) } } + +func (m *Manager) EnableRouting() error { + m.mutex.Lock() + defer m.mutex.Unlock() + + return m.determineRouting() +} + +func (m *Manager) DisableRouting() error { + m.mutex.Lock() + defer m.mutex.Unlock() + + if m.forwarder == nil { + return nil + } + + m.routingEnabled = false + m.nativeRouter = false + + // don't stop forwarder if in use by netstack + if m.netstack && m.localForwarding { + return nil + } + + m.forwarder.Stop() + m.forwarder = nil + + log.Debug("forwarder stopped") + + return nil +} diff --git a/client/firewall/uspfilter/uspfilter_filter_test.go b/client/firewall/uspfilter/uspfilter_filter_test.go index d7aebb1aa..9a1456d00 100644 --- a/client/firewall/uspfilter/uspfilter_filter_test.go +++ b/client/firewall/uspfilter/uspfilter_filter_test.go @@ -303,6 +303,7 @@ func setupRoutedManager(tb testing.TB, network string) *Manager { } manager, err := Create(ifaceMock, false) + require.NoError(tb, manager.EnableRouting()) require.NoError(tb, err) require.NotNil(tb, manager) require.True(tb, manager.routingEnabled) diff --git a/client/internal/routemanager/manager.go b/client/internal/routemanager/manager.go index 9c7f1f6fa..52de0948b 100644 --- a/client/internal/routemanager/manager.go +++ b/client/internal/routemanager/manager.go @@ -286,15 +286,15 @@ func (m *DefaultManager) UpdateRoutes(updateSerial uint64, newRoutes []*route.Ro m.updateClientNetworks(updateSerial, filteredClientRoutes) m.notifier.OnNewRoutes(filteredClientRoutes) } + m.clientRoutes = newClientRoutesIDMap - if m.serverRouter != nil { - err := m.serverRouter.updateRoutes(newServerRoutesMap) - if err != nil { - return err - } + if m.serverRouter == nil { + return nil } - m.clientRoutes = newClientRoutesIDMap + if err := m.serverRouter.updateRoutes(newServerRoutesMap); err != nil { + return fmt.Errorf("update routes: %w", err) + } return nil } diff --git a/client/internal/routemanager/server_nonandroid.go b/client/internal/routemanager/server_nonandroid.go index b60cb318e..4690e3f0e 100644 --- a/client/internal/routemanager/server_nonandroid.go +++ b/client/internal/routemanager/server_nonandroid.go @@ -71,9 +71,15 @@ func (m *serverRouter) updateRoutes(routesMap map[route.ID]*route.Route) error { } if len(m.routes) > 0 { - err := systemops.EnableIPForwarding() - if err != nil { - return err + if err := systemops.EnableIPForwarding(); err != nil { + return fmt.Errorf("enable ip forwarding: %w", err) + } + if err := m.firewall.EnableRouting(); err != nil { + return fmt.Errorf("enable routing: %w", err) + } + } else { + if err := m.firewall.DisableRouting(); err != nil { + return fmt.Errorf("disable routing: %w", err) } } From d48edb983787dd1221d83f17aea9972daba4437c Mon Sep 17 00:00:00 2001 From: Pedro Maia Costa <550684+pnmcosta@users.noreply.github.com> Date: Wed, 12 Feb 2025 11:16:51 +0000 Subject: [PATCH 57/92] fix integration tests (#3311) --- management/client/rest/accounts_test.go | 29 +++++--- management/client/rest/client_test.go | 14 ++-- management/client/rest/dns_test.go | 41 ++++++----- management/client/rest/events_test.go | 17 +++-- management/client/rest/geo_test.go | 21 ++++-- management/client/rest/groups_test.go | 33 +++++---- management/client/rest/networks_test.go | 77 +++++++++++--------- management/client/rest/peers_test.go | 33 +++++---- management/client/rest/policies_test.go | 33 +++++---- management/client/rest/posturechecks_test.go | 33 +++++---- management/client/rest/routes_test.go | 33 +++++---- management/client/rest/setupkeys_test.go | 33 +++++---- management/client/rest/tokens_test.go | 29 +++++--- management/client/rest/users_test.go | 33 +++++---- 14 files changed, 264 insertions(+), 195 deletions(-) diff --git a/management/client/rest/accounts_test.go b/management/client/rest/accounts_test.go index 3c1925fbc..621228261 100644 --- a/management/client/rest/accounts_test.go +++ b/management/client/rest/accounts_test.go @@ -1,4 +1,7 @@ -package rest +//go:build integration +// +build integration + +package rest_test import ( "context" @@ -7,10 +10,12 @@ import ( "net/http" "testing" - "github.com/netbirdio/netbird/management/server/http/api" - "github.com/netbirdio/netbird/management/server/http/util" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/netbirdio/netbird/management/client/rest" + "github.com/netbirdio/netbird/management/server/http/api" + "github.com/netbirdio/netbird/management/server/http/util" ) var ( @@ -33,7 +38,7 @@ var ( ) func TestAccounts_List_200(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/accounts", func(w http.ResponseWriter, r *http.Request) { retBytes, _ := json.Marshal([]api.Account{testAccount}) _, err := w.Write(retBytes) @@ -47,7 +52,7 @@ func TestAccounts_List_200(t *testing.T) { } func TestAccounts_List_Err(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/accounts", func(w http.ResponseWriter, r *http.Request) { retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) w.WriteHeader(400) @@ -62,7 +67,7 @@ func TestAccounts_List_Err(t *testing.T) { } func TestAccounts_Update_200(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/accounts/Test", func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "PUT", r.Method) reqBytes, err := io.ReadAll(r.Body) @@ -87,7 +92,7 @@ func TestAccounts_Update_200(t *testing.T) { } func TestAccounts_Update_Err(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/accounts/Test", func(w http.ResponseWriter, r *http.Request) { retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) w.WriteHeader(400) @@ -106,7 +111,7 @@ func TestAccounts_Update_Err(t *testing.T) { } func TestAccounts_Delete_200(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/accounts/Test", func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "DELETE", r.Method) w.WriteHeader(200) @@ -117,7 +122,7 @@ func TestAccounts_Delete_200(t *testing.T) { } func TestAccounts_Delete_Err(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/accounts/Test", func(w http.ResponseWriter, r *http.Request) { retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404}) w.WriteHeader(404) @@ -131,7 +136,7 @@ func TestAccounts_Delete_Err(t *testing.T) { } func TestAccounts_Integration_List(t *testing.T) { - withBlackBoxServer(t, func(c *Client) { + withBlackBoxServer(t, func(c *rest.Client) { accounts, err := c.Accounts.List(context.Background()) require.NoError(t, err) assert.Len(t, accounts, 1) @@ -141,7 +146,7 @@ func TestAccounts_Integration_List(t *testing.T) { } func TestAccounts_Integration_Update(t *testing.T) { - withBlackBoxServer(t, func(c *Client) { + withBlackBoxServer(t, func(c *rest.Client) { accounts, err := c.Accounts.List(context.Background()) require.NoError(t, err) assert.Len(t, accounts, 1) @@ -157,7 +162,7 @@ func TestAccounts_Integration_Update(t *testing.T) { // Account deletion on MySQL and PostgreSQL databases causes unknown errors // func TestAccounts_Integration_Delete(t *testing.T) { -// withBlackBoxServer(t, func(c *Client) { +// withBlackBoxServer(t, func(c *rest.Client) { // accounts, err := c.Accounts.List(context.Background()) // require.NoError(t, err) // assert.Len(t, accounts, 1) diff --git a/management/client/rest/client_test.go b/management/client/rest/client_test.go index a42b12fa3..70e6c73e1 100644 --- a/management/client/rest/client_test.go +++ b/management/client/rest/client_test.go @@ -1,18 +1,22 @@ -package rest +//go:build integration +// +build integration + +package rest_test import ( "net/http" "net/http/httptest" "testing" + "github.com/netbirdio/netbird/management/client/rest" "github.com/netbirdio/netbird/management/server/http/testing/testing_tools" ) -func withMockClient(callback func(*Client, *http.ServeMux)) { +func withMockClient(callback func(*rest.Client, *http.ServeMux)) { mux := &http.ServeMux{} server := httptest.NewServer(mux) defer server.Close() - c := New(server.URL, "ABC") + c := rest.New(server.URL, "ABC") callback(c, mux) } @@ -20,11 +24,11 @@ func ptr[T any, PT *T](x T) PT { return &x } -func withBlackBoxServer(t *testing.T, callback func(*Client)) { +func withBlackBoxServer(t *testing.T, callback func(*rest.Client)) { t.Helper() handler, _, _ := testing_tools.BuildApiBlackBoxWithDBState(t, "../../server/testdata/store.sql", nil, false) server := httptest.NewServer(handler) defer server.Close() - c := New(server.URL, "nbp_apTmlmUXHSC4PKmHwtIZNaGr8eqcVI2gMURp") + c := rest.New(server.URL, "nbp_apTmlmUXHSC4PKmHwtIZNaGr8eqcVI2gMURp") callback(c) } diff --git a/management/client/rest/dns_test.go b/management/client/rest/dns_test.go index d2c00549c..0d57d63d7 100644 --- a/management/client/rest/dns_test.go +++ b/management/client/rest/dns_test.go @@ -1,4 +1,7 @@ -package rest +//go:build integration +// +build integration + +package rest_test import ( "context" @@ -7,10 +10,12 @@ import ( "net/http" "testing" - "github.com/netbirdio/netbird/management/server/http/api" - "github.com/netbirdio/netbird/management/server/http/util" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/netbirdio/netbird/management/client/rest" + "github.com/netbirdio/netbird/management/server/http/api" + "github.com/netbirdio/netbird/management/server/http/util" ) var ( @@ -25,7 +30,7 @@ var ( ) func TestDNSNameserverGroup_List_200(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/dns/nameservers", func(w http.ResponseWriter, r *http.Request) { retBytes, _ := json.Marshal([]api.NameserverGroup{testNameserverGroup}) _, err := w.Write(retBytes) @@ -39,7 +44,7 @@ func TestDNSNameserverGroup_List_200(t *testing.T) { } func TestDNSNameserverGroup_List_Err(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/dns/nameservers", func(w http.ResponseWriter, r *http.Request) { retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) w.WriteHeader(400) @@ -54,7 +59,7 @@ func TestDNSNameserverGroup_List_Err(t *testing.T) { } func TestDNSNameserverGroup_Get_200(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/dns/nameservers/Test", func(w http.ResponseWriter, r *http.Request) { retBytes, _ := json.Marshal(testNameserverGroup) _, err := w.Write(retBytes) @@ -67,7 +72,7 @@ func TestDNSNameserverGroup_Get_200(t *testing.T) { } func TestDNSNameserverGroup_Get_Err(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/dns/nameservers/Test", func(w http.ResponseWriter, r *http.Request) { retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) w.WriteHeader(400) @@ -82,7 +87,7 @@ func TestDNSNameserverGroup_Get_Err(t *testing.T) { } func TestDNSNameserverGroup_Create_200(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/dns/nameservers", func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "POST", r.Method) reqBytes, err := io.ReadAll(r.Body) @@ -104,7 +109,7 @@ func TestDNSNameserverGroup_Create_200(t *testing.T) { } func TestDNSNameserverGroup_Create_Err(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/dns/nameservers", func(w http.ResponseWriter, r *http.Request) { retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) w.WriteHeader(400) @@ -121,7 +126,7 @@ func TestDNSNameserverGroup_Create_Err(t *testing.T) { } func TestDNSNameserverGroup_Update_200(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/dns/nameservers/Test", func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "PUT", r.Method) reqBytes, err := io.ReadAll(r.Body) @@ -143,7 +148,7 @@ func TestDNSNameserverGroup_Update_200(t *testing.T) { } func TestDNSNameserverGroup_Update_Err(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/dns/nameservers/Test", func(w http.ResponseWriter, r *http.Request) { retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) w.WriteHeader(400) @@ -160,7 +165,7 @@ func TestDNSNameserverGroup_Update_Err(t *testing.T) { } func TestDNSNameserverGroup_Delete_200(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/dns/nameservers/Test", func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "DELETE", r.Method) w.WriteHeader(200) @@ -171,7 +176,7 @@ func TestDNSNameserverGroup_Delete_200(t *testing.T) { } func TestDNSNameserverGroup_Delete_Err(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/dns/nameservers/Test", func(w http.ResponseWriter, r *http.Request) { retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404}) w.WriteHeader(404) @@ -185,7 +190,7 @@ func TestDNSNameserverGroup_Delete_Err(t *testing.T) { } func TestDNSSettings_Get_200(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/dns/settings", func(w http.ResponseWriter, r *http.Request) { retBytes, _ := json.Marshal(testSettings) _, err := w.Write(retBytes) @@ -198,7 +203,7 @@ func TestDNSSettings_Get_200(t *testing.T) { } func TestDNSSettings_Get_Err(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/dns/settings", func(w http.ResponseWriter, r *http.Request) { retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) w.WriteHeader(400) @@ -213,7 +218,7 @@ func TestDNSSettings_Get_Err(t *testing.T) { } func TestDNSSettings_Update_200(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/dns/settings", func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "PUT", r.Method) reqBytes, err := io.ReadAll(r.Body) @@ -235,7 +240,7 @@ func TestDNSSettings_Update_200(t *testing.T) { } func TestDNSSettings_Update_Err(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/dns/settings", func(w http.ResponseWriter, r *http.Request) { retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) w.WriteHeader(400) @@ -267,7 +272,7 @@ func TestDNS_Integration(t *testing.T) { Primary: true, SearchDomainsEnabled: false, } - withBlackBoxServer(t, func(c *Client) { + withBlackBoxServer(t, func(c *rest.Client) { // Create nsGroup, err := c.DNS.CreateNameserverGroup(context.Background(), nsGroupReq) require.NoError(t, err) diff --git a/management/client/rest/events_test.go b/management/client/rest/events_test.go index 515c227e6..2589193a2 100644 --- a/management/client/rest/events_test.go +++ b/management/client/rest/events_test.go @@ -1,4 +1,7 @@ -package rest +//go:build integration +// +build integration + +package rest_test import ( "context" @@ -6,10 +9,12 @@ import ( "net/http" "testing" - "github.com/netbirdio/netbird/management/server/http/api" - "github.com/netbirdio/netbird/management/server/http/util" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/netbirdio/netbird/management/client/rest" + "github.com/netbirdio/netbird/management/server/http/api" + "github.com/netbirdio/netbird/management/server/http/util" ) var ( @@ -20,7 +25,7 @@ var ( ) func TestEvents_List_200(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/events", func(w http.ResponseWriter, r *http.Request) { retBytes, _ := json.Marshal([]api.Event{testEvent}) _, err := w.Write(retBytes) @@ -34,7 +39,7 @@ func TestEvents_List_200(t *testing.T) { } func TestEvents_List_Err(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/events", func(w http.ResponseWriter, r *http.Request) { retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) w.WriteHeader(400) @@ -49,7 +54,7 @@ func TestEvents_List_Err(t *testing.T) { } func TestEvents_Integration(t *testing.T) { - withBlackBoxServer(t, func(c *Client) { + withBlackBoxServer(t, func(c *rest.Client) { // Do something that would trigger any event _, err := c.SetupKeys.Create(context.Background(), api.CreateSetupKeyRequest{ Ephemeral: ptr(true), diff --git a/management/client/rest/geo_test.go b/management/client/rest/geo_test.go index dd42ecba8..d24405094 100644 --- a/management/client/rest/geo_test.go +++ b/management/client/rest/geo_test.go @@ -1,4 +1,7 @@ -package rest +//go:build integration +// +build integration + +package rest_test import ( "context" @@ -6,10 +9,12 @@ import ( "net/http" "testing" - "github.com/netbirdio/netbird/management/server/http/api" - "github.com/netbirdio/netbird/management/server/http/util" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/netbirdio/netbird/management/client/rest" + "github.com/netbirdio/netbird/management/server/http/api" + "github.com/netbirdio/netbird/management/server/http/util" ) var ( @@ -25,7 +30,7 @@ var ( ) func TestGeo_ListCountries_200(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/locations/countries", func(w http.ResponseWriter, r *http.Request) { retBytes, _ := json.Marshal([]api.Country{testCountry}) _, err := w.Write(retBytes) @@ -39,7 +44,7 @@ func TestGeo_ListCountries_200(t *testing.T) { } func TestGeo_ListCountries_Err(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/locations/countries", func(w http.ResponseWriter, r *http.Request) { retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) w.WriteHeader(400) @@ -54,7 +59,7 @@ func TestGeo_ListCountries_Err(t *testing.T) { } func TestGeo_ListCountryCities_200(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/locations/countries/Test/cities", func(w http.ResponseWriter, r *http.Request) { retBytes, _ := json.Marshal([]api.City{testCity}) _, err := w.Write(retBytes) @@ -68,7 +73,7 @@ func TestGeo_ListCountryCities_200(t *testing.T) { } func TestGeo_ListCountryCities_Err(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/locations/countries/Test/cities", func(w http.ResponseWriter, r *http.Request) { retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) w.WriteHeader(400) @@ -84,7 +89,7 @@ func TestGeo_ListCountryCities_Err(t *testing.T) { func TestGeo_Integration(t *testing.T) { // Blackbox is initialized with empty GeoLocations - withBlackBoxServer(t, func(c *Client) { + withBlackBoxServer(t, func(c *rest.Client) { countries, err := c.GeoLocation.ListCountries(context.Background()) require.NoError(t, err) assert.Empty(t, countries) diff --git a/management/client/rest/groups_test.go b/management/client/rest/groups_test.go index ac534437d..d6a5410e0 100644 --- a/management/client/rest/groups_test.go +++ b/management/client/rest/groups_test.go @@ -1,4 +1,7 @@ -package rest +//go:build integration +// +build integration + +package rest_test import ( "context" @@ -7,10 +10,12 @@ import ( "net/http" "testing" - "github.com/netbirdio/netbird/management/server/http/api" - "github.com/netbirdio/netbird/management/server/http/util" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/netbirdio/netbird/management/client/rest" + "github.com/netbirdio/netbird/management/server/http/api" + "github.com/netbirdio/netbird/management/server/http/util" ) var ( @@ -22,7 +27,7 @@ var ( ) func TestGroups_List_200(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/groups", func(w http.ResponseWriter, r *http.Request) { retBytes, _ := json.Marshal([]api.Group{testGroup}) _, err := w.Write(retBytes) @@ -36,7 +41,7 @@ func TestGroups_List_200(t *testing.T) { } func TestGroups_List_Err(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/groups", func(w http.ResponseWriter, r *http.Request) { retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) w.WriteHeader(400) @@ -51,7 +56,7 @@ func TestGroups_List_Err(t *testing.T) { } func TestGroups_Get_200(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/groups/Test", func(w http.ResponseWriter, r *http.Request) { retBytes, _ := json.Marshal(testGroup) _, err := w.Write(retBytes) @@ -64,7 +69,7 @@ func TestGroups_Get_200(t *testing.T) { } func TestGroups_Get_Err(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/groups/Test", func(w http.ResponseWriter, r *http.Request) { retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) w.WriteHeader(400) @@ -79,7 +84,7 @@ func TestGroups_Get_Err(t *testing.T) { } func TestGroups_Create_200(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/groups", func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "POST", r.Method) reqBytes, err := io.ReadAll(r.Body) @@ -101,7 +106,7 @@ func TestGroups_Create_200(t *testing.T) { } func TestGroups_Create_Err(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/groups", func(w http.ResponseWriter, r *http.Request) { retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) w.WriteHeader(400) @@ -118,7 +123,7 @@ func TestGroups_Create_Err(t *testing.T) { } func TestGroups_Update_200(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/groups/Test", func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "PUT", r.Method) reqBytes, err := io.ReadAll(r.Body) @@ -140,7 +145,7 @@ func TestGroups_Update_200(t *testing.T) { } func TestGroups_Update_Err(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/groups/Test", func(w http.ResponseWriter, r *http.Request) { retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) w.WriteHeader(400) @@ -157,7 +162,7 @@ func TestGroups_Update_Err(t *testing.T) { } func TestGroups_Delete_200(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/groups/Test", func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "DELETE", r.Method) w.WriteHeader(200) @@ -168,7 +173,7 @@ func TestGroups_Delete_200(t *testing.T) { } func TestGroups_Delete_Err(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/groups/Test", func(w http.ResponseWriter, r *http.Request) { retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404}) w.WriteHeader(404) @@ -182,7 +187,7 @@ func TestGroups_Delete_Err(t *testing.T) { } func TestGroups_Integration(t *testing.T) { - withBlackBoxServer(t, func(c *Client) { + withBlackBoxServer(t, func(c *rest.Client) { groups, err := c.Groups.List(context.Background()) require.NoError(t, err) assert.Len(t, groups, 1) diff --git a/management/client/rest/networks_test.go b/management/client/rest/networks_test.go index 934c55380..0772d7540 100644 --- a/management/client/rest/networks_test.go +++ b/management/client/rest/networks_test.go @@ -1,4 +1,7 @@ -package rest +//go:build integration +// +build integration + +package rest_test import ( "context" @@ -7,10 +10,12 @@ import ( "net/http" "testing" - "github.com/netbirdio/netbird/management/server/http/api" - "github.com/netbirdio/netbird/management/server/http/util" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/netbirdio/netbird/management/client/rest" + "github.com/netbirdio/netbird/management/server/http/api" + "github.com/netbirdio/netbird/management/server/http/util" ) var ( @@ -30,7 +35,7 @@ var ( ) func TestNetworks_List_200(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/networks", func(w http.ResponseWriter, r *http.Request) { retBytes, _ := json.Marshal([]api.Network{testNetwork}) _, err := w.Write(retBytes) @@ -44,7 +49,7 @@ func TestNetworks_List_200(t *testing.T) { } func TestNetworks_List_Err(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/networks", func(w http.ResponseWriter, r *http.Request) { retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) w.WriteHeader(400) @@ -59,7 +64,7 @@ func TestNetworks_List_Err(t *testing.T) { } func TestNetworks_Get_200(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/networks/Test", func(w http.ResponseWriter, r *http.Request) { retBytes, _ := json.Marshal(testNetwork) _, err := w.Write(retBytes) @@ -72,7 +77,7 @@ func TestNetworks_Get_200(t *testing.T) { } func TestNetworks_Get_Err(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/networks/Test", func(w http.ResponseWriter, r *http.Request) { retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) w.WriteHeader(400) @@ -87,7 +92,7 @@ func TestNetworks_Get_Err(t *testing.T) { } func TestNetworks_Create_200(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/networks", func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "POST", r.Method) reqBytes, err := io.ReadAll(r.Body) @@ -109,7 +114,7 @@ func TestNetworks_Create_200(t *testing.T) { } func TestNetworks_Create_Err(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/networks", func(w http.ResponseWriter, r *http.Request) { retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) w.WriteHeader(400) @@ -126,7 +131,7 @@ func TestNetworks_Create_Err(t *testing.T) { } func TestNetworks_Update_200(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/networks/Test", func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "PUT", r.Method) reqBytes, err := io.ReadAll(r.Body) @@ -148,7 +153,7 @@ func TestNetworks_Update_200(t *testing.T) { } func TestNetworks_Update_Err(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/networks/Test", func(w http.ResponseWriter, r *http.Request) { retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) w.WriteHeader(400) @@ -165,7 +170,7 @@ func TestNetworks_Update_Err(t *testing.T) { } func TestNetworks_Delete_200(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/networks/Test", func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "DELETE", r.Method) w.WriteHeader(200) @@ -176,7 +181,7 @@ func TestNetworks_Delete_200(t *testing.T) { } func TestNetworks_Delete_Err(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/networks/Test", func(w http.ResponseWriter, r *http.Request) { retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404}) w.WriteHeader(404) @@ -190,7 +195,7 @@ func TestNetworks_Delete_Err(t *testing.T) { } func TestNetworks_Integration(t *testing.T) { - withBlackBoxServer(t, func(c *Client) { + withBlackBoxServer(t, func(c *rest.Client) { network, err := c.Networks.Create(context.Background(), api.NetworkRequest{ Description: ptr("TestNetwork"), Name: "Test", @@ -216,7 +221,7 @@ func TestNetworks_Integration(t *testing.T) { } func TestNetworkResources_List_200(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/networks/Meow/resources", func(w http.ResponseWriter, r *http.Request) { retBytes, _ := json.Marshal([]api.NetworkResource{testNetworkResource}) _, err := w.Write(retBytes) @@ -230,7 +235,7 @@ func TestNetworkResources_List_200(t *testing.T) { } func TestNetworkResources_List_Err(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/networks/Meow/resources", func(w http.ResponseWriter, r *http.Request) { retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) w.WriteHeader(400) @@ -245,7 +250,7 @@ func TestNetworkResources_List_Err(t *testing.T) { } func TestNetworkResources_Get_200(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/networks/Meow/resources/Test", func(w http.ResponseWriter, r *http.Request) { retBytes, _ := json.Marshal(testNetworkResource) _, err := w.Write(retBytes) @@ -258,7 +263,7 @@ func TestNetworkResources_Get_200(t *testing.T) { } func TestNetworkResources_Get_Err(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/networks/Meow/resources/Test", func(w http.ResponseWriter, r *http.Request) { retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) w.WriteHeader(400) @@ -273,7 +278,7 @@ func TestNetworkResources_Get_Err(t *testing.T) { } func TestNetworkResources_Create_200(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/networks/Meow/resources", func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "POST", r.Method) reqBytes, err := io.ReadAll(r.Body) @@ -295,7 +300,7 @@ func TestNetworkResources_Create_200(t *testing.T) { } func TestNetworkResources_Create_Err(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/networks/Meow/resources", func(w http.ResponseWriter, r *http.Request) { retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) w.WriteHeader(400) @@ -312,7 +317,7 @@ func TestNetworkResources_Create_Err(t *testing.T) { } func TestNetworkResources_Update_200(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/networks/Meow/resources/Test", func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "PUT", r.Method) reqBytes, err := io.ReadAll(r.Body) @@ -334,7 +339,7 @@ func TestNetworkResources_Update_200(t *testing.T) { } func TestNetworkResources_Update_Err(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/networks/Meow/resources/Test", func(w http.ResponseWriter, r *http.Request) { retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) w.WriteHeader(400) @@ -351,7 +356,7 @@ func TestNetworkResources_Update_Err(t *testing.T) { } func TestNetworkResources_Delete_200(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/networks/Meow/resources/Test", func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "DELETE", r.Method) w.WriteHeader(200) @@ -362,7 +367,7 @@ func TestNetworkResources_Delete_200(t *testing.T) { } func TestNetworkResources_Delete_Err(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/networks/Meow/resources/Test", func(w http.ResponseWriter, r *http.Request) { retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404}) w.WriteHeader(404) @@ -376,7 +381,7 @@ func TestNetworkResources_Delete_Err(t *testing.T) { } func TestNetworkResources_Integration(t *testing.T) { - withBlackBoxServer(t, func(c *Client) { + withBlackBoxServer(t, func(c *rest.Client) { _, err := c.Networks.Resources("TestNetwork").Create(context.Background(), api.NetworkResourceRequest{ Address: "test.com", Description: ptr("Description"), @@ -403,7 +408,7 @@ func TestNetworkResources_Integration(t *testing.T) { } func TestNetworkRouters_List_200(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/networks/Meow/routers", func(w http.ResponseWriter, r *http.Request) { retBytes, _ := json.Marshal([]api.NetworkRouter{testNetworkRouter}) _, err := w.Write(retBytes) @@ -417,7 +422,7 @@ func TestNetworkRouters_List_200(t *testing.T) { } func TestNetworkRouters_List_Err(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/networks/Meow/routers", func(w http.ResponseWriter, r *http.Request) { retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) w.WriteHeader(400) @@ -432,7 +437,7 @@ func TestNetworkRouters_List_Err(t *testing.T) { } func TestNetworkRouters_Get_200(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/networks/Meow/routers/Test", func(w http.ResponseWriter, r *http.Request) { retBytes, _ := json.Marshal(testNetworkRouter) _, err := w.Write(retBytes) @@ -445,7 +450,7 @@ func TestNetworkRouters_Get_200(t *testing.T) { } func TestNetworkRouters_Get_Err(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/networks/Meow/routers/Test", func(w http.ResponseWriter, r *http.Request) { retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) w.WriteHeader(400) @@ -460,7 +465,7 @@ func TestNetworkRouters_Get_Err(t *testing.T) { } func TestNetworkRouters_Create_200(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/networks/Meow/routers", func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "POST", r.Method) reqBytes, err := io.ReadAll(r.Body) @@ -482,7 +487,7 @@ func TestNetworkRouters_Create_200(t *testing.T) { } func TestNetworkRouters_Create_Err(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/networks/Meow/routers", func(w http.ResponseWriter, r *http.Request) { retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) w.WriteHeader(400) @@ -499,7 +504,7 @@ func TestNetworkRouters_Create_Err(t *testing.T) { } func TestNetworkRouters_Update_200(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/networks/Meow/routers/Test", func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "PUT", r.Method) reqBytes, err := io.ReadAll(r.Body) @@ -521,7 +526,7 @@ func TestNetworkRouters_Update_200(t *testing.T) { } func TestNetworkRouters_Update_Err(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/networks/Meow/routers/Test", func(w http.ResponseWriter, r *http.Request) { retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) w.WriteHeader(400) @@ -538,7 +543,7 @@ func TestNetworkRouters_Update_Err(t *testing.T) { } func TestNetworkRouters_Delete_200(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/networks/Meow/routers/Test", func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "DELETE", r.Method) w.WriteHeader(200) @@ -549,7 +554,7 @@ func TestNetworkRouters_Delete_200(t *testing.T) { } func TestNetworkRouters_Delete_Err(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/networks/Meow/routers/Test", func(w http.ResponseWriter, r *http.Request) { retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404}) w.WriteHeader(404) @@ -563,7 +568,7 @@ func TestNetworkRouters_Delete_Err(t *testing.T) { } func TestNetworkRouters_Integration(t *testing.T) { - withBlackBoxServer(t, func(c *Client) { + withBlackBoxServer(t, func(c *rest.Client) { _, err := c.Networks.Routers("TestNetwork").Create(context.Background(), api.NetworkRouterRequest{ Enabled: false, Masquerade: false, diff --git a/management/client/rest/peers_test.go b/management/client/rest/peers_test.go index 216ee990c..4c5cd1e60 100644 --- a/management/client/rest/peers_test.go +++ b/management/client/rest/peers_test.go @@ -1,4 +1,7 @@ -package rest +//go:build integration +// +build integration + +package rest_test import ( "context" @@ -7,10 +10,12 @@ import ( "net/http" "testing" - "github.com/netbirdio/netbird/management/server/http/api" - "github.com/netbirdio/netbird/management/server/http/util" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/netbirdio/netbird/management/client/rest" + "github.com/netbirdio/netbird/management/server/http/api" + "github.com/netbirdio/netbird/management/server/http/util" ) var ( @@ -24,7 +29,7 @@ var ( ) func TestPeers_List_200(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/peers", func(w http.ResponseWriter, r *http.Request) { retBytes, _ := json.Marshal([]api.Peer{testPeer}) _, err := w.Write(retBytes) @@ -38,7 +43,7 @@ func TestPeers_List_200(t *testing.T) { } func TestPeers_List_Err(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/peers", func(w http.ResponseWriter, r *http.Request) { retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) w.WriteHeader(400) @@ -53,7 +58,7 @@ func TestPeers_List_Err(t *testing.T) { } func TestPeers_Get_200(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/peers/Test", func(w http.ResponseWriter, r *http.Request) { retBytes, _ := json.Marshal(testPeer) _, err := w.Write(retBytes) @@ -66,7 +71,7 @@ func TestPeers_Get_200(t *testing.T) { } func TestPeers_Get_Err(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/peers/Test", func(w http.ResponseWriter, r *http.Request) { retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) w.WriteHeader(400) @@ -81,7 +86,7 @@ func TestPeers_Get_Err(t *testing.T) { } func TestPeers_Update_200(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/peers/Test", func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "PUT", r.Method) reqBytes, err := io.ReadAll(r.Body) @@ -103,7 +108,7 @@ func TestPeers_Update_200(t *testing.T) { } func TestPeers_Update_Err(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/peers/Test", func(w http.ResponseWriter, r *http.Request) { retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) w.WriteHeader(400) @@ -120,7 +125,7 @@ func TestPeers_Update_Err(t *testing.T) { } func TestPeers_Delete_200(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/peers/Test", func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "DELETE", r.Method) w.WriteHeader(200) @@ -131,7 +136,7 @@ func TestPeers_Delete_200(t *testing.T) { } func TestPeers_Delete_Err(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/peers/Test", func(w http.ResponseWriter, r *http.Request) { retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404}) w.WriteHeader(404) @@ -145,7 +150,7 @@ func TestPeers_Delete_Err(t *testing.T) { } func TestPeers_ListAccessiblePeers_200(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/peers/Test/accessible-peers", func(w http.ResponseWriter, r *http.Request) { retBytes, _ := json.Marshal([]api.Peer{testPeer}) _, err := w.Write(retBytes) @@ -159,7 +164,7 @@ func TestPeers_ListAccessiblePeers_200(t *testing.T) { } func TestPeers_ListAccessiblePeers_Err(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/peers/Test/accessible-peers", func(w http.ResponseWriter, r *http.Request) { retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) w.WriteHeader(400) @@ -174,7 +179,7 @@ func TestPeers_ListAccessiblePeers_Err(t *testing.T) { } func TestPeers_Integration(t *testing.T) { - withBlackBoxServer(t, func(c *Client) { + withBlackBoxServer(t, func(c *rest.Client) { peers, err := c.Peers.List(context.Background()) require.NoError(t, err) require.NotEmpty(t, peers) diff --git a/management/client/rest/policies_test.go b/management/client/rest/policies_test.go index f7fc6ff10..5792048df 100644 --- a/management/client/rest/policies_test.go +++ b/management/client/rest/policies_test.go @@ -1,4 +1,7 @@ -package rest +//go:build integration +// +build integration + +package rest_test import ( "context" @@ -7,10 +10,12 @@ import ( "net/http" "testing" - "github.com/netbirdio/netbird/management/server/http/api" - "github.com/netbirdio/netbird/management/server/http/util" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/netbirdio/netbird/management/client/rest" + "github.com/netbirdio/netbird/management/server/http/api" + "github.com/netbirdio/netbird/management/server/http/util" ) var ( @@ -22,7 +27,7 @@ var ( ) func TestPolicies_List_200(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/policies", func(w http.ResponseWriter, r *http.Request) { retBytes, _ := json.Marshal([]api.Policy{testPolicy}) _, err := w.Write(retBytes) @@ -36,7 +41,7 @@ func TestPolicies_List_200(t *testing.T) { } func TestPolicies_List_Err(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/policies", func(w http.ResponseWriter, r *http.Request) { retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) w.WriteHeader(400) @@ -51,7 +56,7 @@ func TestPolicies_List_Err(t *testing.T) { } func TestPolicies_Get_200(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/policies/Test", func(w http.ResponseWriter, r *http.Request) { retBytes, _ := json.Marshal(testPolicy) _, err := w.Write(retBytes) @@ -64,7 +69,7 @@ func TestPolicies_Get_200(t *testing.T) { } func TestPolicies_Get_Err(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/policies/Test", func(w http.ResponseWriter, r *http.Request) { retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) w.WriteHeader(400) @@ -79,7 +84,7 @@ func TestPolicies_Get_Err(t *testing.T) { } func TestPolicies_Create_200(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/policies", func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "POST", r.Method) reqBytes, err := io.ReadAll(r.Body) @@ -101,7 +106,7 @@ func TestPolicies_Create_200(t *testing.T) { } func TestPolicies_Create_Err(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/policies", func(w http.ResponseWriter, r *http.Request) { retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) w.WriteHeader(400) @@ -118,7 +123,7 @@ func TestPolicies_Create_Err(t *testing.T) { } func TestPolicies_Update_200(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/policies/Test", func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "PUT", r.Method) reqBytes, err := io.ReadAll(r.Body) @@ -140,7 +145,7 @@ func TestPolicies_Update_200(t *testing.T) { } func TestPolicies_Update_Err(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/policies/Test", func(w http.ResponseWriter, r *http.Request) { retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) w.WriteHeader(400) @@ -157,7 +162,7 @@ func TestPolicies_Update_Err(t *testing.T) { } func TestPolicies_Delete_200(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/policies/Test", func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "DELETE", r.Method) w.WriteHeader(200) @@ -168,7 +173,7 @@ func TestPolicies_Delete_200(t *testing.T) { } func TestPolicies_Delete_Err(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/policies/Test", func(w http.ResponseWriter, r *http.Request) { retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404}) w.WriteHeader(404) @@ -182,7 +187,7 @@ func TestPolicies_Delete_Err(t *testing.T) { } func TestPolicies_Integration(t *testing.T) { - withBlackBoxServer(t, func(c *Client) { + withBlackBoxServer(t, func(c *rest.Client) { policies, err := c.Policies.List(context.Background()) require.NoError(t, err) require.NotEmpty(t, policies) diff --git a/management/client/rest/posturechecks_test.go b/management/client/rest/posturechecks_test.go index 6fefc0140..a891d6ac9 100644 --- a/management/client/rest/posturechecks_test.go +++ b/management/client/rest/posturechecks_test.go @@ -1,4 +1,7 @@ -package rest +//go:build integration +// +build integration + +package rest_test import ( "context" @@ -7,10 +10,12 @@ import ( "net/http" "testing" - "github.com/netbirdio/netbird/management/server/http/api" - "github.com/netbirdio/netbird/management/server/http/util" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/netbirdio/netbird/management/client/rest" + "github.com/netbirdio/netbird/management/server/http/api" + "github.com/netbirdio/netbird/management/server/http/util" ) var ( @@ -21,7 +26,7 @@ var ( ) func TestPostureChecks_List_200(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/posture-checks", func(w http.ResponseWriter, r *http.Request) { retBytes, _ := json.Marshal([]api.PostureCheck{testPostureCheck}) _, err := w.Write(retBytes) @@ -35,7 +40,7 @@ func TestPostureChecks_List_200(t *testing.T) { } func TestPostureChecks_List_Err(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/posture-checks", func(w http.ResponseWriter, r *http.Request) { retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) w.WriteHeader(400) @@ -50,7 +55,7 @@ func TestPostureChecks_List_Err(t *testing.T) { } func TestPostureChecks_Get_200(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/posture-checks/Test", func(w http.ResponseWriter, r *http.Request) { retBytes, _ := json.Marshal(testPostureCheck) _, err := w.Write(retBytes) @@ -63,7 +68,7 @@ func TestPostureChecks_Get_200(t *testing.T) { } func TestPostureChecks_Get_Err(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/posture-checks/Test", func(w http.ResponseWriter, r *http.Request) { retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) w.WriteHeader(400) @@ -78,7 +83,7 @@ func TestPostureChecks_Get_Err(t *testing.T) { } func TestPostureChecks_Create_200(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/posture-checks", func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "POST", r.Method) reqBytes, err := io.ReadAll(r.Body) @@ -100,7 +105,7 @@ func TestPostureChecks_Create_200(t *testing.T) { } func TestPostureChecks_Create_Err(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/posture-checks", func(w http.ResponseWriter, r *http.Request) { retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) w.WriteHeader(400) @@ -117,7 +122,7 @@ func TestPostureChecks_Create_Err(t *testing.T) { } func TestPostureChecks_Update_200(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/posture-checks/Test", func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "PUT", r.Method) reqBytes, err := io.ReadAll(r.Body) @@ -139,7 +144,7 @@ func TestPostureChecks_Update_200(t *testing.T) { } func TestPostureChecks_Update_Err(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/posture-checks/Test", func(w http.ResponseWriter, r *http.Request) { retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) w.WriteHeader(400) @@ -156,7 +161,7 @@ func TestPostureChecks_Update_Err(t *testing.T) { } func TestPostureChecks_Delete_200(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/posture-checks/Test", func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "DELETE", r.Method) w.WriteHeader(200) @@ -167,7 +172,7 @@ func TestPostureChecks_Delete_200(t *testing.T) { } func TestPostureChecks_Delete_Err(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/posture-checks/Test", func(w http.ResponseWriter, r *http.Request) { retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404}) w.WriteHeader(404) @@ -181,7 +186,7 @@ func TestPostureChecks_Delete_Err(t *testing.T) { } func TestPostureChecks_Integration(t *testing.T) { - withBlackBoxServer(t, func(c *Client) { + withBlackBoxServer(t, func(c *rest.Client) { check, err := c.PostureChecks.Create(context.Background(), api.PostureCheckUpdate{ Name: "Test", Description: "Testing", diff --git a/management/client/rest/routes_test.go b/management/client/rest/routes_test.go index 123bd41d4..1c698a7fb 100644 --- a/management/client/rest/routes_test.go +++ b/management/client/rest/routes_test.go @@ -1,4 +1,7 @@ -package rest +//go:build integration +// +build integration + +package rest_test import ( "context" @@ -7,10 +10,12 @@ import ( "net/http" "testing" - "github.com/netbirdio/netbird/management/server/http/api" - "github.com/netbirdio/netbird/management/server/http/util" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/netbirdio/netbird/management/client/rest" + "github.com/netbirdio/netbird/management/server/http/api" + "github.com/netbirdio/netbird/management/server/http/util" ) var ( @@ -21,7 +26,7 @@ var ( ) func TestRoutes_List_200(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/routes", func(w http.ResponseWriter, r *http.Request) { retBytes, _ := json.Marshal([]api.Route{testRoute}) _, err := w.Write(retBytes) @@ -35,7 +40,7 @@ func TestRoutes_List_200(t *testing.T) { } func TestRoutes_List_Err(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/routes", func(w http.ResponseWriter, r *http.Request) { retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) w.WriteHeader(400) @@ -50,7 +55,7 @@ func TestRoutes_List_Err(t *testing.T) { } func TestRoutes_Get_200(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/routes/Test", func(w http.ResponseWriter, r *http.Request) { retBytes, _ := json.Marshal(testRoute) _, err := w.Write(retBytes) @@ -63,7 +68,7 @@ func TestRoutes_Get_200(t *testing.T) { } func TestRoutes_Get_Err(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/routes/Test", func(w http.ResponseWriter, r *http.Request) { retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) w.WriteHeader(400) @@ -78,7 +83,7 @@ func TestRoutes_Get_Err(t *testing.T) { } func TestRoutes_Create_200(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/routes", func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "POST", r.Method) reqBytes, err := io.ReadAll(r.Body) @@ -100,7 +105,7 @@ func TestRoutes_Create_200(t *testing.T) { } func TestRoutes_Create_Err(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/routes", func(w http.ResponseWriter, r *http.Request) { retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) w.WriteHeader(400) @@ -117,7 +122,7 @@ func TestRoutes_Create_Err(t *testing.T) { } func TestRoutes_Update_200(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/routes/Test", func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "PUT", r.Method) reqBytes, err := io.ReadAll(r.Body) @@ -139,7 +144,7 @@ func TestRoutes_Update_200(t *testing.T) { } func TestRoutes_Update_Err(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/routes/Test", func(w http.ResponseWriter, r *http.Request) { retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) w.WriteHeader(400) @@ -156,7 +161,7 @@ func TestRoutes_Update_Err(t *testing.T) { } func TestRoutes_Delete_200(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/routes/Test", func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "DELETE", r.Method) w.WriteHeader(200) @@ -167,7 +172,7 @@ func TestRoutes_Delete_200(t *testing.T) { } func TestRoutes_Delete_Err(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/routes/Test", func(w http.ResponseWriter, r *http.Request) { retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404}) w.WriteHeader(404) @@ -181,7 +186,7 @@ func TestRoutes_Delete_Err(t *testing.T) { } func TestRoutes_Integration(t *testing.T) { - withBlackBoxServer(t, func(c *Client) { + withBlackBoxServer(t, func(c *rest.Client) { route, err := c.Routes.Create(context.Background(), api.RouteRequest{ Description: "Meow", Enabled: false, diff --git a/management/client/rest/setupkeys_test.go b/management/client/rest/setupkeys_test.go index 82c3d1fc8..8edce8428 100644 --- a/management/client/rest/setupkeys_test.go +++ b/management/client/rest/setupkeys_test.go @@ -1,4 +1,7 @@ -package rest +//go:build integration +// +build integration + +package rest_test import ( "context" @@ -7,10 +10,12 @@ import ( "net/http" "testing" - "github.com/netbirdio/netbird/management/server/http/api" - "github.com/netbirdio/netbird/management/server/http/util" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/netbirdio/netbird/management/client/rest" + "github.com/netbirdio/netbird/management/server/http/api" + "github.com/netbirdio/netbird/management/server/http/util" ) var ( @@ -31,7 +36,7 @@ var ( ) func TestSetupKeys_List_200(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/setup-keys", func(w http.ResponseWriter, r *http.Request) { retBytes, _ := json.Marshal([]api.SetupKey{testSetupKey}) _, err := w.Write(retBytes) @@ -45,7 +50,7 @@ func TestSetupKeys_List_200(t *testing.T) { } func TestSetupKeys_List_Err(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/setup-keys", func(w http.ResponseWriter, r *http.Request) { retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) w.WriteHeader(400) @@ -60,7 +65,7 @@ func TestSetupKeys_List_Err(t *testing.T) { } func TestSetupKeys_Get_200(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/setup-keys/Test", func(w http.ResponseWriter, r *http.Request) { retBytes, _ := json.Marshal(testSetupKey) _, err := w.Write(retBytes) @@ -73,7 +78,7 @@ func TestSetupKeys_Get_200(t *testing.T) { } func TestSetupKeys_Get_Err(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/setup-keys/Test", func(w http.ResponseWriter, r *http.Request) { retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) w.WriteHeader(400) @@ -88,7 +93,7 @@ func TestSetupKeys_Get_Err(t *testing.T) { } func TestSetupKeys_Create_200(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/setup-keys", func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "POST", r.Method) reqBytes, err := io.ReadAll(r.Body) @@ -110,7 +115,7 @@ func TestSetupKeys_Create_200(t *testing.T) { } func TestSetupKeys_Create_Err(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/setup-keys", func(w http.ResponseWriter, r *http.Request) { retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) w.WriteHeader(400) @@ -127,7 +132,7 @@ func TestSetupKeys_Create_Err(t *testing.T) { } func TestSetupKeys_Update_200(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/setup-keys/Test", func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "PUT", r.Method) reqBytes, err := io.ReadAll(r.Body) @@ -149,7 +154,7 @@ func TestSetupKeys_Update_200(t *testing.T) { } func TestSetupKeys_Update_Err(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/setup-keys/Test", func(w http.ResponseWriter, r *http.Request) { retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) w.WriteHeader(400) @@ -166,7 +171,7 @@ func TestSetupKeys_Update_Err(t *testing.T) { } func TestSetupKeys_Delete_200(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/setup-keys/Test", func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "DELETE", r.Method) w.WriteHeader(200) @@ -177,7 +182,7 @@ func TestSetupKeys_Delete_200(t *testing.T) { } func TestSetupKeys_Delete_Err(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/setup-keys/Test", func(w http.ResponseWriter, r *http.Request) { retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404}) w.WriteHeader(404) @@ -191,7 +196,7 @@ func TestSetupKeys_Delete_Err(t *testing.T) { } func TestSetupKeys_Integration(t *testing.T) { - withBlackBoxServer(t, func(c *Client) { + withBlackBoxServer(t, func(c *rest.Client) { group, err := c.Groups.Create(context.Background(), api.GroupRequest{ Name: "Test", }) diff --git a/management/client/rest/tokens_test.go b/management/client/rest/tokens_test.go index 478fae93e..eea55d22f 100644 --- a/management/client/rest/tokens_test.go +++ b/management/client/rest/tokens_test.go @@ -1,4 +1,7 @@ -package rest +//go:build integration +// +build integration + +package rest_test import ( "context" @@ -8,10 +11,12 @@ import ( "testing" "time" - "github.com/netbirdio/netbird/management/server/http/api" - "github.com/netbirdio/netbird/management/server/http/util" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/netbirdio/netbird/management/client/rest" + "github.com/netbirdio/netbird/management/server/http/api" + "github.com/netbirdio/netbird/management/server/http/util" ) var ( @@ -31,7 +36,7 @@ var ( ) func TestTokens_List_200(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/users/meow/tokens", func(w http.ResponseWriter, r *http.Request) { retBytes, _ := json.Marshal([]api.PersonalAccessToken{testToken}) _, err := w.Write(retBytes) @@ -45,7 +50,7 @@ func TestTokens_List_200(t *testing.T) { } func TestTokens_List_Err(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/users/meow/tokens", func(w http.ResponseWriter, r *http.Request) { retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) w.WriteHeader(400) @@ -60,7 +65,7 @@ func TestTokens_List_Err(t *testing.T) { } func TestTokens_Get_200(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/users/meow/tokens/Test", func(w http.ResponseWriter, r *http.Request) { retBytes, _ := json.Marshal(testToken) _, err := w.Write(retBytes) @@ -73,7 +78,7 @@ func TestTokens_Get_200(t *testing.T) { } func TestTokens_Get_Err(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/users/meow/tokens/Test", func(w http.ResponseWriter, r *http.Request) { retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) w.WriteHeader(400) @@ -88,7 +93,7 @@ func TestTokens_Get_Err(t *testing.T) { } func TestTokens_Create_200(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/users/meow/tokens", func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "POST", r.Method) reqBytes, err := io.ReadAll(r.Body) @@ -110,7 +115,7 @@ func TestTokens_Create_200(t *testing.T) { } func TestTokens_Create_Err(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/users/meow/tokens", func(w http.ResponseWriter, r *http.Request) { retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) w.WriteHeader(400) @@ -127,7 +132,7 @@ func TestTokens_Create_Err(t *testing.T) { } func TestTokens_Delete_200(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/users/meow/tokens/Test", func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "DELETE", r.Method) w.WriteHeader(200) @@ -138,7 +143,7 @@ func TestTokens_Delete_200(t *testing.T) { } func TestTokens_Delete_Err(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/users/meow/tokens/Test", func(w http.ResponseWriter, r *http.Request) { retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404}) w.WriteHeader(404) @@ -152,7 +157,7 @@ func TestTokens_Delete_Err(t *testing.T) { } func TestTokens_Integration(t *testing.T) { - withBlackBoxServer(t, func(c *Client) { + withBlackBoxServer(t, func(c *rest.Client) { tokenClear, err := c.Tokens.Create(context.Background(), "a23efe53-63fb-11ec-90d6-0242ac120003", api.PersonalAccessTokenRequest{ Name: "Test", ExpiresIn: 365, diff --git a/management/client/rest/users_test.go b/management/client/rest/users_test.go index aaec3bf42..2ff8a0327 100644 --- a/management/client/rest/users_test.go +++ b/management/client/rest/users_test.go @@ -1,4 +1,7 @@ -package rest +//go:build integration +// +build integration + +package rest_test import ( "context" @@ -8,10 +11,12 @@ import ( "testing" "time" - "github.com/netbirdio/netbird/management/server/http/api" - "github.com/netbirdio/netbird/management/server/http/util" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/netbirdio/netbird/management/client/rest" + "github.com/netbirdio/netbird/management/server/http/api" + "github.com/netbirdio/netbird/management/server/http/util" ) var ( @@ -34,7 +39,7 @@ var ( ) func TestUsers_List_200(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/users", func(w http.ResponseWriter, r *http.Request) { retBytes, _ := json.Marshal([]api.User{testUser}) _, err := w.Write(retBytes) @@ -48,7 +53,7 @@ func TestUsers_List_200(t *testing.T) { } func TestUsers_List_Err(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/users", func(w http.ResponseWriter, r *http.Request) { retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) w.WriteHeader(400) @@ -63,7 +68,7 @@ func TestUsers_List_Err(t *testing.T) { } func TestUsers_Create_200(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/users", func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "POST", r.Method) reqBytes, err := io.ReadAll(r.Body) @@ -85,7 +90,7 @@ func TestUsers_Create_200(t *testing.T) { } func TestUsers_Create_Err(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/users", func(w http.ResponseWriter, r *http.Request) { retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) w.WriteHeader(400) @@ -102,7 +107,7 @@ func TestUsers_Create_Err(t *testing.T) { } func TestUsers_Update_200(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/users/Test", func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "PUT", r.Method) reqBytes, err := io.ReadAll(r.Body) @@ -125,7 +130,7 @@ func TestUsers_Update_200(t *testing.T) { } func TestUsers_Update_Err(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/users/Test", func(w http.ResponseWriter, r *http.Request) { retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) w.WriteHeader(400) @@ -142,7 +147,7 @@ func TestUsers_Update_Err(t *testing.T) { } func TestUsers_Delete_200(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/users/Test", func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "DELETE", r.Method) w.WriteHeader(200) @@ -153,7 +158,7 @@ func TestUsers_Delete_200(t *testing.T) { } func TestUsers_Delete_Err(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/users/Test", func(w http.ResponseWriter, r *http.Request) { retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404}) w.WriteHeader(404) @@ -167,7 +172,7 @@ func TestUsers_Delete_Err(t *testing.T) { } func TestUsers_ResendInvitation_200(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/users/Test/invite", func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "POST", r.Method) w.WriteHeader(200) @@ -178,7 +183,7 @@ func TestUsers_ResendInvitation_200(t *testing.T) { } func TestUsers_ResendInvitation_Err(t *testing.T) { - withMockClient(func(c *Client, mux *http.ServeMux) { + withMockClient(func(c *rest.Client, mux *http.ServeMux) { mux.HandleFunc("/api/users/Test/invite", func(w http.ResponseWriter, r *http.Request) { retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404}) w.WriteHeader(404) @@ -192,7 +197,7 @@ func TestUsers_ResendInvitation_Err(t *testing.T) { } func TestUsers_Integration(t *testing.T) { - withBlackBoxServer(t, func(c *Client) { + withBlackBoxServer(t, func(c *rest.Client) { user, err := c.Users.Create(context.Background(), api.UserCreateRequest{ AutoGroups: []string{}, Email: ptr("test@example.com"), From a930c2aecf415bba1233e7b7aaa77f0d39ee6709 Mon Sep 17 00:00:00 2001 From: Zoltan Papp Date: Thu, 13 Feb 2025 15:48:10 +0100 Subject: [PATCH 58/92] Fix priority handling (#3313) --- client/internal/peer/conn.go | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/client/internal/peer/conn.go b/client/internal/peer/conn.go index 7caafa53d..8bbea6a2b 100644 --- a/client/internal/peer/conn.go +++ b/client/internal/peer/conn.go @@ -2,6 +2,7 @@ package peer import ( "context" + "fmt" "math/rand" "net" "os" @@ -28,9 +29,25 @@ import ( type ConnPriority int +func (cp ConnPriority) String() string { + switch cp { + case connPriorityNone: + return "None" + case connPriorityRelay: + return "PriorityRelay" + case connPriorityICETurn: + return "PriorityICETurn" + case connPriorityICEP2P: + return "PriorityICEP2P" + default: + return fmt.Sprintf("ConnPriority(%d)", cp) + } +} + const ( defaultWgKeepAlive = 25 * time.Second + connPriorityNone ConnPriority = 0 connPriorityRelay ConnPriority = 1 connPriorityICETurn ConnPriority = 2 connPriorityICEP2P ConnPriority = 3 @@ -299,9 +316,10 @@ func (conn *Conn) onICEConnectionIsReady(priority ConnPriority, iceConnInfo ICEC return } - conn.log.Debugf("ICE connection is ready") - + // this never should happen, because Relay is the lower priority and ICE always close the deprecated connection before upgrade + // todo consider to remove this check if conn.currentConnPriority > priority { + conn.log.Infof("current connection priority (%s) is higher than the new one (%s), do not upgrade connection", conn.currentConnPriority, priority) conn.statusICE.Set(StatusConnected) conn.updateIceState(iceConnInfo) return @@ -357,7 +375,6 @@ func (conn *Conn) onICEConnectionIsReady(priority ConnPriority, iceConnInfo ICEC conn.doOnConnected(iceConnInfo.RosenpassPubKey, iceConnInfo.RosenpassAddr) } -// todo review to make sense to handle connecting and disconnected status also? func (conn *Conn) onICEStateDisconnected() { conn.mu.Lock() defer conn.mu.Unlock() @@ -376,7 +393,7 @@ func (conn *Conn) onICEStateDisconnected() { // switch back to relay connection if conn.isReadyToUpgrade() { - conn.log.Debugf("ICE disconnected, set Relay to active connection") + conn.log.Infof("ICE disconnected, set Relay to active connection") conn.wgProxyRelay.Work() if err := conn.configureWGEndpoint(conn.wgProxyRelay.EndpointAddr()); err != nil { @@ -384,6 +401,9 @@ func (conn *Conn) onICEStateDisconnected() { } conn.workerRelay.EnableWgWatcher(conn.ctx) conn.currentConnPriority = connPriorityRelay + } else { + conn.log.Infof("ICE disconnected, do not switch to Relay. Reset priority to: %s", connPriorityNone.String()) + conn.currentConnPriority = connPriorityNone } changed := conn.statusICE.Get() != StatusDisconnected @@ -427,7 +447,7 @@ func (conn *Conn) onRelayConnectionIsReady(rci RelayConnInfo) { conn.log.Infof("created new wgProxy for relay connection: %s", wgProxy.EndpointAddr().String()) if conn.iceP2PIsActive() { - conn.log.Debugf("do not switch to relay because current priority is: %v", conn.currentConnPriority) + conn.log.Debugf("do not switch to relay because current priority is: %s", conn.currentConnPriority.String()) conn.setRelayedProxy(wgProxy) conn.statusRelay.Set(StatusConnected) conn.updateRelayStatus(rci.relayedConn.RemoteAddr().String(), rci.rosenpassPubKey) From c4a6dafd272ca29364d5da342e333cead23c7888 Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Thu, 13 Feb 2025 18:17:18 +0100 Subject: [PATCH 59/92] [client] Use GPO DNS Policy Config to configure DNS if present (#3319) --- client/internal/dns/host_windows.go | 250 +++++++++++------- .../internal/dns/unclean_shutdown_windows.go | 7 +- 2 files changed, 155 insertions(+), 102 deletions(-) diff --git a/client/internal/dns/host_windows.go b/client/internal/dns/host_windows.go index 7ecca8a41..0cd078472 100644 --- a/client/internal/dns/host_windows.go +++ b/client/internal/dns/host_windows.go @@ -1,35 +1,51 @@ package dns import ( + "errors" "fmt" "io" "strings" + "syscall" + "github.com/hashicorp/go-multierror" log "github.com/sirupsen/logrus" "golang.org/x/sys/windows/registry" + nberrors "github.com/netbirdio/netbird/client/errors" "github.com/netbirdio/netbird/client/internal/statemanager" ) +var ( + userenv = syscall.NewLazyDLL("userenv.dll") + + // https://learn.microsoft.com/en-us/windows/win32/api/userenv/nf-userenv-refreshpolicyex + refreshPolicyExFn = userenv.NewProc("RefreshPolicyEx") +) + const ( - dnsPolicyConfigMatchPath = `SYSTEM\CurrentControlSet\Services\Dnscache\Parameters\DnsPolicyConfig\NetBird-Match` + dnsPolicyConfigMatchPath = `SYSTEM\CurrentControlSet\Services\Dnscache\Parameters\DnsPolicyConfig\NetBird-Match` + gpoDnsPolicyRoot = `SOFTWARE\Policies\Microsoft\Windows NT\DNSClient` + gpoDnsPolicyConfigMatchPath = gpoDnsPolicyRoot + `\DnsPolicyConfig\NetBird-Match` + dnsPolicyConfigVersionKey = "Version" dnsPolicyConfigVersionValue = 2 dnsPolicyConfigNameKey = "Name" dnsPolicyConfigGenericDNSServersKey = "GenericDNSServers" dnsPolicyConfigConfigOptionsKey = "ConfigOptions" dnsPolicyConfigConfigOptionsValue = 0x8 -) -const ( interfaceConfigPath = `SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\Interfaces` interfaceConfigNameServerKey = "NameServer" interfaceConfigSearchListKey = "SearchList" + + // RP_FORCE: Reapply all policies even if no policy change was detected + rpForce = 0x1 ) type registryConfigurator struct { guid string routingAll bool + gpo bool } func newHostManager(wgInterface WGIface) (*registryConfigurator, error) { @@ -37,12 +53,20 @@ func newHostManager(wgInterface WGIface) (*registryConfigurator, error) { if err != nil { return nil, err } - return newHostManagerWithGuid(guid) -} -func newHostManagerWithGuid(guid string) (*registryConfigurator, error) { + var useGPO bool + k, err := registry.OpenKey(registry.LOCAL_MACHINE, gpoDnsPolicyRoot, registry.QUERY_VALUE) + if err != nil { + log.Debugf("failed to open GPO DNS policy root: %v", err) + } else { + closer(k) + useGPO = true + log.Infof("detected GPO DNS policy configuration, using policy store") + } + return ®istryConfigurator{ guid: guid, + gpo: useGPO, }, nil } @@ -51,30 +75,23 @@ func (r *registryConfigurator) supportCustomPort() bool { } func (r *registryConfigurator) applyDNSConfig(config HostDNSConfig, stateManager *statemanager.Manager) error { - var err error if config.RouteAll { - err = r.addDNSSetupForAll(config.ServerIP) - if err != nil { + if err := r.addDNSSetupForAll(config.ServerIP); err != nil { return fmt.Errorf("add dns setup: %w", err) } } else if r.routingAll { - err = r.deleteInterfaceRegistryKeyProperty(interfaceConfigNameServerKey) - if err != nil { + if err := r.deleteInterfaceRegistryKeyProperty(interfaceConfigNameServerKey); err != nil { return fmt.Errorf("delete interface registry key property: %w", err) } r.routingAll = false log.Infof("removed %s as main DNS forwarder for this peer", config.ServerIP) } - if err := stateManager.UpdateState(&ShutdownState{Guid: r.guid}); err != nil { + if err := stateManager.UpdateState(&ShutdownState{Guid: r.guid, GPO: r.gpo}); err != nil { log.Errorf("failed to update shutdown state: %s", err) } - var ( - searchDomains []string - matchDomains []string - ) - + var searchDomains, matchDomains []string for _, dConf := range config.Domains { if dConf.Disabled { continue @@ -86,16 +103,16 @@ func (r *registryConfigurator) applyDNSConfig(config HostDNSConfig, stateManager } if len(matchDomains) != 0 { - err = r.addDNSMatchPolicy(matchDomains, config.ServerIP) + if err := r.addDNSMatchPolicy(matchDomains, config.ServerIP); err != nil { + return fmt.Errorf("add dns match policy: %w", err) + } } else { - err = removeRegistryKeyFromDNSPolicyConfig(dnsPolicyConfigMatchPath) - } - if err != nil { - return fmt.Errorf("add dns match policy: %w", err) + if err := r.removeDNSMatchPolicies(); err != nil { + return fmt.Errorf("remove dns match policies: %w", err) + } } - err = r.updateSearchDomains(searchDomains) - if err != nil { + if err := r.updateSearchDomains(searchDomains); err != nil { return fmt.Errorf("update search domains: %w", err) } @@ -103,9 +120,8 @@ func (r *registryConfigurator) applyDNSConfig(config HostDNSConfig, stateManager } func (r *registryConfigurator) addDNSSetupForAll(ip string) error { - err := r.setInterfaceRegistryKeyStringValue(interfaceConfigNameServerKey, ip) - if err != nil { - return fmt.Errorf("adding dns setup for all failed with error: %w", err) + if err := r.setInterfaceRegistryKeyStringValue(interfaceConfigNameServerKey, ip); err != nil { + return fmt.Errorf("adding dns setup for all failed: %w", err) } r.routingAll = true log.Infof("configured %s:53 as main DNS forwarder for this peer", ip) @@ -113,64 +129,54 @@ func (r *registryConfigurator) addDNSSetupForAll(ip string) error { } func (r *registryConfigurator) addDNSMatchPolicy(domains []string, ip string) error { - _, err := registry.OpenKey(registry.LOCAL_MACHINE, dnsPolicyConfigMatchPath, registry.QUERY_VALUE) - if err == nil { - err = registry.DeleteKey(registry.LOCAL_MACHINE, dnsPolicyConfigMatchPath) - if err != nil { - return fmt.Errorf("unable to remove existing key from registry, key: HKEY_LOCAL_MACHINE\\%s, error: %w", dnsPolicyConfigMatchPath, err) + // if the gpo key is present, we need to put our DNS settings there, otherwise our config might be ignored + // see https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-gpnrpt/8cc31cb9-20cb-4140-9e85-3e08703b4745 + policyPath := dnsPolicyConfigMatchPath + if r.gpo { + policyPath = gpoDnsPolicyConfigMatchPath + } + + if err := removeRegistryKeyFromDNSPolicyConfig(policyPath); err != nil { + return fmt.Errorf("remove existing dns policy: %w", err) + } + + regKey, _, err := registry.CreateKey(registry.LOCAL_MACHINE, policyPath, registry.SET_VALUE) + if err != nil { + return fmt.Errorf("create registry key HKEY_LOCAL_MACHINE\\%s: %w", policyPath, err) + } + defer closer(regKey) + + if err := regKey.SetDWordValue(dnsPolicyConfigVersionKey, dnsPolicyConfigVersionValue); err != nil { + return fmt.Errorf("set %s: %w", dnsPolicyConfigVersionKey, err) + } + + if err := regKey.SetStringsValue(dnsPolicyConfigNameKey, domains); err != nil { + return fmt.Errorf("set %s: %w", dnsPolicyConfigNameKey, err) + } + + if err := regKey.SetStringValue(dnsPolicyConfigGenericDNSServersKey, ip); err != nil { + return fmt.Errorf("set %s: %w", dnsPolicyConfigGenericDNSServersKey, err) + } + + if err := regKey.SetDWordValue(dnsPolicyConfigConfigOptionsKey, dnsPolicyConfigConfigOptionsValue); err != nil { + return fmt.Errorf("set %s: %w", dnsPolicyConfigConfigOptionsKey, err) + } + + if r.gpo { + if err := refreshGroupPolicy(); err != nil { + log.Warnf("failed to refresh group policy: %v", err) } } - regKey, _, err := registry.CreateKey(registry.LOCAL_MACHINE, dnsPolicyConfigMatchPath, registry.SET_VALUE) - if err != nil { - return fmt.Errorf("unable to create registry key, key: HKEY_LOCAL_MACHINE\\%s, error: %w", dnsPolicyConfigMatchPath, err) - } - - err = regKey.SetDWordValue(dnsPolicyConfigVersionKey, dnsPolicyConfigVersionValue) - if err != nil { - return fmt.Errorf("unable to set registry value for %s, error: %w", dnsPolicyConfigVersionKey, err) - } - - err = regKey.SetStringsValue(dnsPolicyConfigNameKey, domains) - if err != nil { - return fmt.Errorf("unable to set registry value for %s, error: %w", dnsPolicyConfigNameKey, err) - } - - err = regKey.SetStringValue(dnsPolicyConfigGenericDNSServersKey, ip) - if err != nil { - return fmt.Errorf("unable to set registry value for %s, error: %w", dnsPolicyConfigGenericDNSServersKey, err) - } - - err = regKey.SetDWordValue(dnsPolicyConfigConfigOptionsKey, dnsPolicyConfigConfigOptionsValue) - if err != nil { - return fmt.Errorf("unable to set registry value for %s, error: %w", dnsPolicyConfigConfigOptionsKey, err) - } - - log.Infof("added %d match domains to the state. Domain list: %s", len(domains), domains) - - return nil -} - -func (r *registryConfigurator) restoreHostDNS() error { - if err := removeRegistryKeyFromDNSPolicyConfig(dnsPolicyConfigMatchPath); err != nil { - log.Errorf("remove registry key from dns policy config: %s", err) - } - - if err := r.deleteInterfaceRegistryKeyProperty(interfaceConfigSearchListKey); err != nil { - return fmt.Errorf("remove interface registry key: %w", err) - } - + log.Infof("added %d match domains. Domain list: %s", len(domains), domains) return nil } func (r *registryConfigurator) updateSearchDomains(domains []string) error { - err := r.setInterfaceRegistryKeyStringValue(interfaceConfigSearchListKey, strings.Join(domains, ",")) - if err != nil { - return fmt.Errorf("adding search domain failed with error: %w", err) + if err := r.setInterfaceRegistryKeyStringValue(interfaceConfigSearchListKey, strings.Join(domains, ",")); err != nil { + return fmt.Errorf("update search domains: %w", err) } - - log.Infof("updated the search domains in the registry with %d domains. Domain list: %s", len(domains), domains) - + log.Infof("updated search domains: %s", domains) return nil } @@ -181,11 +187,9 @@ func (r *registryConfigurator) setInterfaceRegistryKeyStringValue(key, value str } defer closer(regKey) - err = regKey.SetStringValue(key, value) - if err != nil { - return fmt.Errorf("applying key %s with value \"%s\" for interface failed with error: %w", key, value, err) + if err := regKey.SetStringValue(key, value); err != nil { + return fmt.Errorf("set key %s=%s: %w", key, value, err) } - return nil } @@ -196,43 +200,91 @@ func (r *registryConfigurator) deleteInterfaceRegistryKeyProperty(propertyKey st } defer closer(regKey) - err = regKey.DeleteValue(propertyKey) - if err != nil { - return fmt.Errorf("deleting registry key %s for interface failed with error: %w", propertyKey, err) + if err := regKey.DeleteValue(propertyKey); err != nil { + return fmt.Errorf("delete registry key %s: %w", propertyKey, err) } - return nil } func (r *registryConfigurator) getInterfaceRegistryKey() (registry.Key, error) { - var regKey registry.Key - regKeyPath := interfaceConfigPath + "\\" + r.guid - regKey, err := registry.OpenKey(registry.LOCAL_MACHINE, regKeyPath, registry.SET_VALUE) if err != nil { - return regKey, fmt.Errorf("unable to open the interface registry key, key: HKEY_LOCAL_MACHINE\\%s, error: %w", regKeyPath, err) + return regKey, fmt.Errorf("open HKEY_LOCAL_MACHINE\\%s: %w", regKeyPath, err) } - return regKey, nil } -func (r *registryConfigurator) restoreUncleanShutdownDNS() error { - if err := r.restoreHostDNS(); err != nil { - return fmt.Errorf("restoring dns via registry: %w", err) +func (r *registryConfigurator) restoreHostDNS() error { + if err := r.removeDNSMatchPolicies(); err != nil { + log.Errorf("remove dns match policies: %s", err) } + + if err := r.deleteInterfaceRegistryKeyProperty(interfaceConfigSearchListKey); err != nil { + return fmt.Errorf("remove interface registry key: %w", err) + } + return nil } +func (r *registryConfigurator) removeDNSMatchPolicies() error { + var merr *multierror.Error + if err := removeRegistryKeyFromDNSPolicyConfig(dnsPolicyConfigMatchPath); err != nil { + merr = multierror.Append(merr, fmt.Errorf("remove local registry key: %w", err)) + } + + if err := removeRegistryKeyFromDNSPolicyConfig(gpoDnsPolicyConfigMatchPath); err != nil { + merr = multierror.Append(merr, fmt.Errorf("remove GPO registry key: %w", err)) + } + + if err := refreshGroupPolicy(); err != nil { + merr = multierror.Append(merr, fmt.Errorf("refresh group policy: %w", err)) + } + + return nberrors.FormatErrorOrNil(merr) +} + +func (r *registryConfigurator) restoreUncleanShutdownDNS() error { + return r.restoreHostDNS() +} + func removeRegistryKeyFromDNSPolicyConfig(regKeyPath string) error { k, err := registry.OpenKey(registry.LOCAL_MACHINE, regKeyPath, registry.QUERY_VALUE) - if err == nil { - defer closer(k) - err = registry.DeleteKey(registry.LOCAL_MACHINE, regKeyPath) - if err != nil { - return fmt.Errorf("unable to remove existing key from registry, key: HKEY_LOCAL_MACHINE\\%s, error: %w", regKeyPath, err) - } + if err != nil { + log.Debugf("failed to open HKEY_LOCAL_MACHINE\\%s: %v", regKeyPath, err) + return nil } + + closer(k) + if err := registry.DeleteKey(registry.LOCAL_MACHINE, regKeyPath); err != nil { + return fmt.Errorf("delete HKEY_LOCAL_MACHINE\\%s: %w", regKeyPath, err) + } + + return nil +} + +func refreshGroupPolicy() error { + // refreshPolicyExFn.Call() panics if the func is not found + defer func() { + if r := recover(); r != nil { + log.Errorf("Recovered from panic: %v", r) + } + }() + + ret, _, err := refreshPolicyExFn.Call( + // bMachine = TRUE (computer policy) + uintptr(1), + // dwOptions = RP_FORCE + uintptr(rpForce), + ) + + if ret == 0 { + if err != nil && !errors.Is(err, syscall.Errno(0)) { + return fmt.Errorf("RefreshPolicyEx failed: %w", err) + } + return fmt.Errorf("RefreshPolicyEx failed") + } + return nil } diff --git a/client/internal/dns/unclean_shutdown_windows.go b/client/internal/dns/unclean_shutdown_windows.go index 74e40cc11..ab0b2cc63 100644 --- a/client/internal/dns/unclean_shutdown_windows.go +++ b/client/internal/dns/unclean_shutdown_windows.go @@ -6,6 +6,7 @@ import ( type ShutdownState struct { Guid string + GPO bool } func (s *ShutdownState) Name() string { @@ -13,9 +14,9 @@ func (s *ShutdownState) Name() string { } func (s *ShutdownState) Cleanup() error { - manager, err := newHostManagerWithGuid(s.Guid) - if err != nil { - return fmt.Errorf("create host manager: %w", err) + manager := ®istryConfigurator{ + guid: s.Guid, + gpo: s.GPO, } if err := manager.restoreUncleanShutdownDNS(); err != nil { From 039a985f41f80eaadf4719651f5aa4da2786f2fe Mon Sep 17 00:00:00 2001 From: hakansa <43675540+hakansa@users.noreply.github.com> Date: Fri, 14 Feb 2025 13:13:40 +0300 Subject: [PATCH 60/92] [client] Normalize DNS record names to lowercase in local handler update (#3323) * [client] Normalize DNS record names to lowercase in lookup --- client/internal/dns/local.go | 2 ++ client/internal/dns/server.go | 1 + 2 files changed, 3 insertions(+) diff --git a/client/internal/dns/local.go b/client/internal/dns/local.go index 1fe88f750..80113885a 100644 --- a/client/internal/dns/local.go +++ b/client/internal/dns/local.go @@ -2,6 +2,7 @@ package dns import ( "fmt" + "strings" "sync" "github.com/miekg/dns" @@ -60,6 +61,7 @@ func (d *localResolver) ServeDNS(w dns.ResponseWriter, r *dns.Msg) { func (d *localResolver) lookupRecord(r *dns.Msg) dns.RR { question := r.Question[0] + question.Name = strings.ToLower(question.Name) record, found := d.records.Load(buildRecordKey(question.Name, question.Qclass, question.Qtype)) if !found { return nil diff --git a/client/internal/dns/server.go b/client/internal/dns/server.go index f714f9857..fb94e07ac 100644 --- a/client/internal/dns/server.go +++ b/client/internal/dns/server.go @@ -454,6 +454,7 @@ func (s *DefaultServer) buildLocalHandlerUpdate(customZones []nbdns.CustomZone) if record.Class != nbdns.DefaultClass { return nil, nil, fmt.Errorf("received an invalid class type: %s", record.Class) } + key := buildRecordKey(record.Name, class, uint16(record.Type)) localRecords[key] = record } From abe8da697c7737bfe7aca45fba10f621506fad1d Mon Sep 17 00:00:00 2001 From: Pascal Fischer <32096965+pascal-fischer@users.noreply.github.com> Date: Mon, 17 Feb 2025 17:07:30 +0100 Subject: [PATCH 61/92] [signal] add pprof and message size metrics (#3337) --- signal/cmd/run.go | 13 +++++++++++++ signal/metrics/app.go | 29 +++++++++++++++++++++++++++++ signal/server/signal.go | 2 ++ 3 files changed, 44 insertions(+) diff --git a/signal/cmd/run.go b/signal/cmd/run.go index 1bb2f1d0c..3a671a848 100644 --- a/signal/cmd/run.go +++ b/signal/cmd/run.go @@ -8,6 +8,8 @@ import ( "fmt" "net" "net/http" + // nolint:gosec + _ "net/http/pprof" "strings" "time" @@ -82,6 +84,8 @@ var ( RunE: func(cmd *cobra.Command, args []string) error { flag.Parse() + startPprof() + opts, certManager, err := getTLSConfigurations() if err != nil { return err @@ -170,6 +174,15 @@ var ( } ) +func startPprof() { + go func() { + log.Debugf("Starting pprof server on 127.0.0.1:6060") + if err := http.ListenAndServe("127.0.0.1:6060", nil); err != nil { + log.Fatalf("pprof server failed: %v", err) + } + }() +} + func getTLSConfigurations() ([]grpc.ServerOption, *autocert.Manager, error) { var ( err error diff --git a/signal/metrics/app.go b/signal/metrics/app.go index b3457cf96..e3b1c67cd 100644 --- a/signal/metrics/app.go +++ b/signal/metrics/app.go @@ -20,6 +20,8 @@ type AppMetrics struct { MessagesForwarded metric.Int64Counter MessageForwardFailures metric.Int64Counter MessageForwardLatency metric.Float64Histogram + + MessageSize metric.Int64Histogram } func NewAppMetrics(meter metric.Meter) (*AppMetrics, error) { @@ -97,6 +99,16 @@ func NewAppMetrics(meter metric.Meter) (*AppMetrics, error) { return nil, err } + messageSize, err := meter.Int64Histogram( + "message.size.bytes", + metric.WithUnit("bytes"), + metric.WithExplicitBucketBoundaries(getMessageSizeBucketBoundaries()...), + metric.WithDescription("Records the size of each message sent"), + ) + if err != nil { + return nil, err + } + return &AppMetrics{ Meter: meter, @@ -112,9 +124,26 @@ func NewAppMetrics(meter metric.Meter) (*AppMetrics, error) { MessagesForwarded: messagesForwarded, MessageForwardFailures: messageForwardFailures, MessageForwardLatency: messageForwardLatency, + + MessageSize: messageSize, }, nil } +func getMessageSizeBucketBoundaries() []float64 { + return []float64{ + 100, + 250, + 500, + 1000, + 5000, + 10000, + 50000, + 100000, + 500000, + 1000000, + } +} + func getStandardBucketBoundaries() []float64 { return []float64{ 0.1, diff --git a/signal/server/signal.go b/signal/server/signal.go index abc1c367b..05cc43276 100644 --- a/signal/server/signal.go +++ b/signal/server/signal.go @@ -13,6 +13,7 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/metadata" "google.golang.org/grpc/status" + gproto "google.golang.org/protobuf/proto" "github.com/netbirdio/netbird/signal/metrics" "github.com/netbirdio/netbird/signal/peer" @@ -175,4 +176,5 @@ func (s *Server) forwardMessageToPeer(ctx context.Context, msg *proto.EncryptedM // in milliseconds s.metrics.MessageForwardLatency.Record(ctx, float64(time.Since(start).Nanoseconds())/1e6, metric.WithAttributes(attribute.String(labelType, labelTypeStream))) s.metrics.MessagesForwarded.Add(ctx, 1) + s.metrics.MessageSize.Record(ctx, int64(gproto.Size(msg)), metric.WithAttributes(attribute.String(labelType, labelTypeMessage))) } From 4cdb2e533a2ef3d6dbefff390502d9cb45c07334 Mon Sep 17 00:00:00 2001 From: Bethuel Mmbaga Date: Mon, 17 Feb 2025 21:43:12 +0300 Subject: [PATCH 62/92] [management] Refactor users to use store methods (#2917) * Refactor setup key handling to use store methods Signed-off-by: bcmmbaga * add lock to get account groups Signed-off-by: bcmmbaga * add check for regular user Signed-off-by: bcmmbaga * get only required groups for auto-group validation Signed-off-by: bcmmbaga * add account lock and return auto groups map on validation Signed-off-by: bcmmbaga * refactor account peers update Signed-off-by: bcmmbaga * Refactor groups to use store methods Signed-off-by: bcmmbaga * refactor GetGroupByID and add NewGroupNotFoundError Signed-off-by: bcmmbaga * fix tests Signed-off-by: bcmmbaga * Add AddPeer and RemovePeer methods to Group struct Signed-off-by: bcmmbaga * Preserve store engine in SqlStore transactions Signed-off-by: bcmmbaga * Run groups ops in transaction Signed-off-by: bcmmbaga * fix missing group removed from setup key activity Signed-off-by: bcmmbaga * fix merge Signed-off-by: bcmmbaga * Refactor posture checks to remove get and save account Signed-off-by: bcmmbaga * fix refactor Signed-off-by: bcmmbaga * fix tests Signed-off-by: bcmmbaga * fix merge Signed-off-by: bcmmbaga * fix sonar Signed-off-by: bcmmbaga * Change setup key log level to debug for missing group Signed-off-by: bcmmbaga * Retrieve modified peers once for group events Signed-off-by: bcmmbaga * Refactor policy get and save account to use store methods Signed-off-by: bcmmbaga * Fix tests Signed-off-by: bcmmbaga * Add tests Signed-off-by: bcmmbaga * Add tests Signed-off-by: bcmmbaga * Retrieve policy groups and posture checks once for validation Signed-off-by: bcmmbaga * Fix typo Signed-off-by: bcmmbaga * Add policy tests Signed-off-by: bcmmbaga * Refactor anyGroupHasPeers to retrieve all groups once Signed-off-by: bcmmbaga * Refactor dns settings to use store methods Signed-off-by: bcmmbaga * Add tests Signed-off-by: bcmmbaga * Add account locking and merge group deletion methods Signed-off-by: bcmmbaga * Fix tests Signed-off-by: bcmmbaga * Refactor name server groups to use store methods Signed-off-by: bcmmbaga * Add tests Signed-off-by: bcmmbaga * Add peer store methods Signed-off-by: bcmmbaga * Refactor ephemeral peers Signed-off-by: bcmmbaga * Add lock for peer store methods Signed-off-by: bcmmbaga * Refactor peer handlers Signed-off-by: bcmmbaga * Refactor peer to use store methods Signed-off-by: bcmmbaga * Fix tests Signed-off-by: bcmmbaga * Fix typo Signed-off-by: bcmmbaga * Add locks and remove log Signed-off-by: bcmmbaga * run peer ops in transaction Signed-off-by: bcmmbaga * remove duplicate store method Signed-off-by: bcmmbaga * fix peer fields updated after save Signed-off-by: bcmmbaga * add tests Signed-off-by: bcmmbaga * Use update strength and simplify check Signed-off-by: bcmmbaga * prevent changing ruleID when not empty Signed-off-by: bcmmbaga * prevent duplicate rules during updates Signed-off-by: bcmmbaga * fix tests Signed-off-by: bcmmbaga * fix lint Signed-off-by: bcmmbaga * Refactor auth middleware Signed-off-by: bcmmbaga * Refactor account methods and mock Signed-off-by: bcmmbaga * Refactor user and PAT handling Signed-off-by: bcmmbaga * Remove db query context and fix get user by id Signed-off-by: bcmmbaga * Fix database transaction locking issue Signed-off-by: bcmmbaga * Fix tests Signed-off-by: bcmmbaga * Use UTC time in test Signed-off-by: bcmmbaga * Add account locks Signed-off-by: bcmmbaga * Fix prevent users from creating PATs for other users Signed-off-by: bcmmbaga * Add tests Signed-off-by: bcmmbaga * Add store locks and prevent fetching setup keys peers when retrieving user peers with empty userID Signed-off-by: bcmmbaga * Add missing tests Signed-off-by: bcmmbaga * Refactor test names and remove duplicate TestPostgresql_SavePeerStatus Signed-off-by: bcmmbaga * Add account locks and remove redundant ephemeral check Signed-off-by: bcmmbaga * Retrieve all groups for peers and restrict groups for regular users Signed-off-by: bcmmbaga * Fix merge Signed-off-by: bcmmbaga * Fix merge Signed-off-by: bcmmbaga * fix merge Signed-off-by: bcmmbaga * fix store tests Signed-off-by: bcmmbaga * use account object to get validated peers Signed-off-by: bcmmbaga * Fix merge Signed-off-by: bcmmbaga * Improve peer performance Signed-off-by: bcmmbaga * Get account direct from store without buffer Signed-off-by: bcmmbaga * Add get peer groups tests Signed-off-by: bcmmbaga * Adjust benchmarks Signed-off-by: bcmmbaga * Adjust benchmarks Signed-off-by: bcmmbaga * [management] Update benchmark workflow (#3181) * update local benchmark expectations * update cloud expectations * Add status error for generic result error Signed-off-by: bcmmbaga * Use integrated validator direct Signed-off-by: bcmmbaga * update expectations * update expectations * update expectations * Refactor peer scheduler to retry every 3 seconds on errors Signed-off-by: bcmmbaga * update expectations * fix validator * fix validator * fix validator * update timeouts * Refactor ToGroupsInfo to process slices of groups Signed-off-by: bcmmbaga * update expectations * update expectations * update expectations * Bump integrations version Signed-off-by: bcmmbaga * Refactor GetValidatedPeers Signed-off-by: bcmmbaga * Fix tests Signed-off-by: bcmmbaga * go mod tidy Signed-off-by: bcmmbaga * Use peers and groups map for peers validation Signed-off-by: bcmmbaga * remove mysql from api benchmark tests * Fix merge Signed-off-by: bcmmbaga * Fix blocked db calls on user auto groups update Signed-off-by: bcmmbaga * Fix tests Signed-off-by: bcmmbaga * update expectations Signed-off-by: bcmmbaga * update expectations Signed-off-by: bcmmbaga * Skip user check for system initiated peer deletion Signed-off-by: bcmmbaga * Remove context in db calls Signed-off-by: bcmmbaga * update expectations Signed-off-by: bcmmbaga * [management] Improve group peer/resource counting (#3192) * Fix sonar Signed-off-by: bcmmbaga * Adjust bench expectations Signed-off-by: bcmmbaga * Rename GetAccountInfoFromPAT to GetTokenInfo Signed-off-by: bcmmbaga * Fix tests Signed-off-by: bcmmbaga * Remove global account lock for ListUsers Signed-off-by: bcmmbaga * build userinfo after updating users in db Signed-off-by: bcmmbaga * [management] Optimize user bulk deletion (#3315) * refactor building user infos Signed-off-by: bcmmbaga * fix tests Signed-off-by: bcmmbaga * remove unused code Signed-off-by: bcmmbaga * Refactor GetUsersFromAccount to return a map of UserInfo instead of a slice Signed-off-by: bcmmbaga * Export BuildUserInfosForAccount to account manager Signed-off-by: bcmmbaga * Fetch account user info once for bulk users save Signed-off-by: bcmmbaga * Update user deletion expectations Signed-off-by: bcmmbaga * Set max open conns for activity store Signed-off-by: bcmmbaga * Update bench expectations Signed-off-by: bcmmbaga --------- Signed-off-by: bcmmbaga --------- Signed-off-by: bcmmbaga Co-authored-by: Pascal Fischer <32096965+pascal-fischer@users.noreply.github.com> Co-authored-by: Pascal Fischer Co-authored-by: Pedro Costa <550684+pnmcosta@users.noreply.github.com> --- management/client/rest/dns_test.go | 1 + management/server/account.go | 175 ++-- management/server/account_test.go | 7 +- management/server/activity/sqlite/sqlite.go | 2 + management/server/http/handler.go | 2 +- .../handlers/events/events_handler_test.go | 4 +- .../http/handlers/users/users_handler_test.go | 12 +- .../server/http/middleware/auth_middleware.go | 22 +- .../http/middleware/auth_middleware_test.go | 11 +- .../users_handler_benchmark_test.go | 50 +- management/server/mock_server/account_mock.go | 31 +- management/server/peer_test.go | 5 +- management/server/status/error.go | 17 +- management/server/store/sql_store.go | 184 +++- management/server/store/sql_store_test.go | 365 +++++++- management/server/store/store.go | 13 +- management/server/testdata/store.sql | 2 +- .../server/types/personal_access_token.go | 3 +- management/server/user.go | 877 ++++++++---------- management/server/user_test.go | 16 +- 20 files changed, 1080 insertions(+), 719 deletions(-) diff --git a/management/client/rest/dns_test.go b/management/client/rest/dns_test.go index 0d57d63d7..b2e0a0bee 100644 --- a/management/client/rest/dns_test.go +++ b/management/client/rest/dns_test.go @@ -260,6 +260,7 @@ func TestDNS_Integration(t *testing.T) { nsGroupReq := api.NameserverGroupRequest{ Description: "Test", Enabled: true, + Domains: []string{}, Groups: []string{"cs1tnh0hhcjnqoiuebeg"}, Name: "test", Nameservers: []api.Nameserver{ diff --git a/management/server/account.go b/management/server/account.go index 2c62a2453..a0c6fd0b0 100644 --- a/management/server/account.go +++ b/management/server/account.go @@ -67,7 +67,7 @@ type AccountManager interface { SaveSetupKey(ctx context.Context, accountID string, key *types.SetupKey, userID string) (*types.SetupKey, error) CreateUser(ctx context.Context, accountID, initiatorUserID string, key *types.UserInfo) (*types.UserInfo, error) DeleteUser(ctx context.Context, accountID, initiatorUserID string, targetUserID string) error - DeleteRegularUsers(ctx context.Context, accountID, initiatorUserID string, targetUserIDs []string) error + DeleteRegularUsers(ctx context.Context, accountID, initiatorUserID string, targetUserIDs []string, userInfos map[string]*types.UserInfo) error InviteUser(ctx context.Context, accountID string, initiatorUserID string, targetUserID string) error ListSetupKeys(ctx context.Context, accountID, userID string) ([]*types.SetupKey, error) SaveUser(ctx context.Context, accountID, initiatorUserID string, update *types.User) (*types.UserInfo, error) @@ -79,7 +79,7 @@ type AccountManager interface { GetAccountIDByUserID(ctx context.Context, userID, domain string) (string, error) GetAccountIDFromToken(ctx context.Context, claims jwtclaims.AuthorizationClaims) (string, string, error) CheckUserAccessByJWTGroups(ctx context.Context, claims jwtclaims.AuthorizationClaims) error - GetAccountFromPAT(ctx context.Context, pat string) (*types.Account, *types.User, *types.PersonalAccessToken, error) + GetPATInfo(ctx context.Context, token string) (*types.User, *types.PersonalAccessToken, string, string, error) DeleteAccount(ctx context.Context, accountID, userID string) error MarkPATUsed(ctx context.Context, tokenID string) error GetUserByID(ctx context.Context, id string) (*types.User, error) @@ -96,7 +96,7 @@ type AccountManager interface { DeletePAT(ctx context.Context, accountID string, initiatorUserID string, targetUserID string, tokenID string) error GetPAT(ctx context.Context, accountID string, initiatorUserID string, targetUserID string, tokenID string) (*types.PersonalAccessToken, error) GetAllPATs(ctx context.Context, accountID string, initiatorUserID string, targetUserID string) ([]*types.PersonalAccessToken, error) - GetUsersFromAccount(ctx context.Context, accountID, userID string) ([]*types.UserInfo, error) + GetUsersFromAccount(ctx context.Context, accountID, userID string) (map[string]*types.UserInfo, error) GetGroup(ctx context.Context, accountId, groupID, userID string) (*types.Group, error) GetAllGroups(ctx context.Context, accountID, userID string) ([]*types.Group, error) GetGroupByName(ctx context.Context, groupName, accountID string) (*types.Group, error) @@ -149,6 +149,7 @@ type AccountManager interface { GetAccountSettings(ctx context.Context, accountID string, userID string) (*types.Settings, error) DeleteSetupKey(ctx context.Context, accountID, userID, keyID string) error UpdateAccountPeers(ctx context.Context, accountID string) + BuildUserInfosForAccount(ctx context.Context, accountID, initiatorUserID string, accountUsers []*types.User) (map[string]*types.UserInfo, error) } type DefaultAccountManager struct { @@ -617,6 +618,12 @@ func (am *DefaultAccountManager) DeleteAccount(ctx context.Context, accountID, u if user.Role != types.UserRoleOwner { return status.Errorf(status.PermissionDenied, "user is not allowed to delete account. Only account owner can delete account") } + + userInfosMap, err := am.BuildUserInfosForAccount(ctx, accountID, userID, maps.Values(account.Users)) + if err != nil { + return status.Errorf(status.Internal, "failed to build user infos for account %s: %v", accountID, err) + } + for _, otherUser := range account.Users { if otherUser.IsServiceUser { continue @@ -626,13 +633,23 @@ func (am *DefaultAccountManager) DeleteAccount(ctx context.Context, accountID, u continue } - deleteUserErr := am.deleteRegularUser(ctx, account, userID, otherUser.Id) + userInfo, ok := userInfosMap[otherUser.Id] + if !ok { + return status.Errorf(status.NotFound, "user info not found for user %s", otherUser.Id) + } + + _, deleteUserErr := am.deleteRegularUser(ctx, accountID, userID, userInfo) if deleteUserErr != nil { return deleteUserErr } } - err = am.deleteRegularUser(ctx, account, userID, userID) + userInfo, ok := userInfosMap[userID] + if !ok { + return status.Errorf(status.NotFound, "user info not found for user %s", userID) + } + + _, err = am.deleteRegularUser(ctx, accountID, userID, userInfo) if err != nil { log.WithContext(ctx).Errorf("failed deleting user %s. error: %s", userID, err) return err @@ -689,20 +706,8 @@ func isNil(i idp.Manager) bool { // addAccountIDToIDPAppMeta update user's app metadata in idp manager func (am *DefaultAccountManager) addAccountIDToIDPAppMeta(ctx context.Context, userID string, accountID string) error { if !isNil(am.idpManager) { - accountUsers, err := am.Store.GetAccountUsers(ctx, store.LockingStrengthShare, accountID) - if err != nil { - return err - } - cachedAccount := &types.Account{ - Id: accountID, - Users: make(map[string]*types.User), - } - for _, user := range accountUsers { - cachedAccount.Users[user.Id] = user - } - // user can be nil if it wasn't found (e.g., just created) - user, err := am.lookupUserInCache(ctx, userID, cachedAccount) + user, err := am.lookupUserInCache(ctx, userID, accountID) if err != nil { return err } @@ -778,10 +783,15 @@ func (am *DefaultAccountManager) lookupUserInCacheByEmail(ctx context.Context, e } // lookupUserInCache looks up user in the IdP cache and returns it. If the user wasn't found, the function returns nil -func (am *DefaultAccountManager) lookupUserInCache(ctx context.Context, userID string, account *types.Account) (*idp.UserData, error) { - users := make(map[string]userLoggedInOnce, len(account.Users)) +func (am *DefaultAccountManager) lookupUserInCache(ctx context.Context, userID string, accountID string) (*idp.UserData, error) { + accountUsers, err := am.Store.GetAccountUsers(ctx, store.LockingStrengthShare, accountID) + if err != nil { + return nil, err + } + + users := make(map[string]userLoggedInOnce, len(accountUsers)) // ignore service users and users provisioned by integrations than are never logged in - for _, user := range account.Users { + for _, user := range accountUsers { if user.IsServiceUser { continue } @@ -790,8 +800,8 @@ func (am *DefaultAccountManager) lookupUserInCache(ctx context.Context, userID s } users[user.Id] = userLoggedInOnce(!user.GetLastLogin().IsZero()) } - log.WithContext(ctx).Debugf("looking up user %s of account %s in cache", userID, account.Id) - userData, err := am.lookupCache(ctx, users, account.Id) + log.WithContext(ctx).Debugf("looking up user %s of account %s in cache", userID, accountID) + userData, err := am.lookupCache(ctx, users, accountID) if err != nil { return nil, err } @@ -804,13 +814,13 @@ func (am *DefaultAccountManager) lookupUserInCache(ctx context.Context, userID s // add extra check on external cache manager. We may get to this point when the user is not yet findable in IDP, // or it didn't have its metadata updated with am.addAccountIDToIDPAppMeta - user, err := account.FindUser(userID) + user, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, userID) if err != nil { - log.WithContext(ctx).Errorf("failed finding user %s in account %s", userID, account.Id) + log.WithContext(ctx).Errorf("failed finding user %s in account %s", userID, accountID) return nil, err } - key := user.IntegrationReference.CacheKey(account.Id, userID) + key := user.IntegrationReference.CacheKey(accountID, userID) ud, err := am.externalCacheManager.Get(am.ctx, key) if err != nil { log.WithContext(ctx).Debugf("failed to get externalCache for key: %s, error: %s", key, err) @@ -1050,9 +1060,9 @@ func (am *DefaultAccountManager) addNewUserToDomainAccount(ctx context.Context, unlockAccount := am.Store.AcquireWriteLockByUID(ctx, domainAccountID) defer unlockAccount() - usersMap := make(map[string]*types.User) - usersMap[claims.UserId] = types.NewRegularUser(claims.UserId) - err := am.Store.SaveUsers(domainAccountID, usersMap) + newUser := types.NewRegularUser(claims.UserId) + newUser.AccountID = domainAccountID + err := am.Store.SaveUser(ctx, store.LockingStrengthUpdate, newUser) if err != nil { return "", err } @@ -1075,12 +1085,7 @@ func (am *DefaultAccountManager) redeemInvite(ctx context.Context, accountID str return nil } - account, err := am.Store.GetAccount(ctx, accountID) - if err != nil { - return err - } - - user, err := am.lookupUserInCache(ctx, userID, account) + user, err := am.lookupUserInCache(ctx, userID, accountID) if err != nil { return err } @@ -1090,17 +1095,17 @@ func (am *DefaultAccountManager) redeemInvite(ctx context.Context, accountID str } if user.AppMetadata.WTPendingInvite != nil && *user.AppMetadata.WTPendingInvite { - log.WithContext(ctx).Infof("redeeming invite for user %s account %s", userID, account.Id) + log.WithContext(ctx).Infof("redeeming invite for user %s account %s", userID, accountID) // User has already logged in, meaning that IdP should have set wt_pending_invite to false. // Our job is to just reload cache. go func() { - _, err = am.refreshCache(ctx, account.Id) + _, err = am.refreshCache(ctx, accountID) if err != nil { - log.WithContext(ctx).Warnf("failed reloading cache when redeeming user %s under account %s", userID, account.Id) + log.WithContext(ctx).Warnf("failed reloading cache when redeeming user %s under account %s", userID, accountID) return } - log.WithContext(ctx).Debugf("user %s of account %s redeemed invite", user.ID, account.Id) - am.StoreEvent(ctx, userID, userID, account.Id, activity.UserJoined, nil) + log.WithContext(ctx).Debugf("user %s of account %s redeemed invite", user.ID, accountID) + am.StoreEvent(ctx, userID, userID, accountID, activity.UserJoined, nil) }() } @@ -1109,33 +1114,7 @@ func (am *DefaultAccountManager) redeemInvite(ctx context.Context, accountID str // MarkPATUsed marks a personal access token as used func (am *DefaultAccountManager) MarkPATUsed(ctx context.Context, tokenID string) error { - - user, err := am.Store.GetUserByTokenID(ctx, tokenID) - if err != nil { - return err - } - - account, err := am.Store.GetAccountByUser(ctx, user.Id) - if err != nil { - return err - } - - unlock := am.Store.AcquireWriteLockByUID(ctx, account.Id) - defer unlock() - - account, err = am.Store.GetAccountByUser(ctx, user.Id) - if err != nil { - return err - } - - pat, ok := account.Users[user.Id].PATs[tokenID] - if !ok { - return fmt.Errorf("token not found") - } - - pat.LastUsed = util.ToPtr(time.Now().UTC()) - - return am.Store.SaveAccount(ctx, account) + return am.Store.MarkPATUsed(ctx, store.LockingStrengthUpdate, tokenID) } // GetAccount returns an account associated with this account ID. @@ -1143,52 +1122,64 @@ func (am *DefaultAccountManager) GetAccount(ctx context.Context, accountID strin return am.Store.GetAccount(ctx, accountID) } -// GetAccountFromPAT returns Account and User associated with a personal access token -func (am *DefaultAccountManager) GetAccountFromPAT(ctx context.Context, token string) (*types.Account, *types.User, *types.PersonalAccessToken, error) { +// GetPATInfo retrieves user, personal access token, domain, and category details from a personal access token. +func (am *DefaultAccountManager) GetPATInfo(ctx context.Context, token string) (user *types.User, pat *types.PersonalAccessToken, domain string, category string, err error) { + user, pat, err = am.extractPATFromToken(ctx, token) + if err != nil { + return nil, nil, "", "", err + } + + domain, category, err = am.Store.GetAccountDomainAndCategory(ctx, store.LockingStrengthShare, user.AccountID) + if err != nil { + return nil, nil, "", "", err + } + + return user, pat, domain, category, nil +} + +// extractPATFromToken validates the token structure and retrieves associated User and PAT. +func (am *DefaultAccountManager) extractPATFromToken(ctx context.Context, token string) (*types.User, *types.PersonalAccessToken, error) { if len(token) != types.PATLength { - return nil, nil, nil, fmt.Errorf("token has wrong length") + return nil, nil, fmt.Errorf("token has incorrect length") } prefix := token[:len(types.PATPrefix)] if prefix != types.PATPrefix { - return nil, nil, nil, fmt.Errorf("token has wrong prefix") + return nil, nil, fmt.Errorf("token has wrong prefix") } secret := token[len(types.PATPrefix) : len(types.PATPrefix)+types.PATSecretLength] encodedChecksum := token[len(types.PATPrefix)+types.PATSecretLength : len(types.PATPrefix)+types.PATSecretLength+types.PATChecksumLength] verificationChecksum, err := base62.Decode(encodedChecksum) if err != nil { - return nil, nil, nil, fmt.Errorf("token checksum decoding failed: %w", err) + return nil, nil, fmt.Errorf("token checksum decoding failed: %w", err) } secretChecksum := crc32.ChecksumIEEE([]byte(secret)) if secretChecksum != verificationChecksum { - return nil, nil, nil, fmt.Errorf("token checksum does not match") + return nil, nil, fmt.Errorf("token checksum does not match") } hashedToken := sha256.Sum256([]byte(token)) encodedHashedToken := b64.StdEncoding.EncodeToString(hashedToken[:]) - tokenID, err := am.Store.GetTokenIDByHashedToken(ctx, encodedHashedToken) + + var user *types.User + var pat *types.PersonalAccessToken + + err = am.Store.ExecuteInTransaction(ctx, func(transaction store.Store) error { + pat, err = transaction.GetPATByHashedToken(ctx, store.LockingStrengthShare, encodedHashedToken) + if err != nil { + return err + } + + user, err = transaction.GetUserByPATID(ctx, store.LockingStrengthShare, pat.ID) + return err + }) if err != nil { - return nil, nil, nil, err + return nil, nil, err } - user, err := am.Store.GetUserByTokenID(ctx, tokenID) - if err != nil { - return nil, nil, nil, err - } - - account, err := am.Store.GetAccountByUser(ctx, user.Id) - if err != nil { - return nil, nil, nil, err - } - - pat := user.PATs[tokenID] - if pat == nil { - return nil, nil, nil, fmt.Errorf("personal access token not found") - } - - return account, user, pat, nil + return user, pat, nil } // GetAccountByID returns an account associated with this account ID. @@ -1334,7 +1325,7 @@ func (am *DefaultAccountManager) syncJWTGroups(ctx context.Context, accountID st return fmt.Errorf("error getting user peers: %w", err) } - updatedGroups, err := am.updateUserPeersInGroups(groupsMap, peers, addNewGroups, removeOldGroups) + updatedGroups, err := updateUserPeersInGroups(groupsMap, peers, addNewGroups, removeOldGroups) if err != nil { return fmt.Errorf("error modifying user peers in groups: %w", err) } diff --git a/management/server/account_test.go b/management/server/account_test.go index 1fc1ceb92..0a7f9119b 100644 --- a/management/server/account_test.go +++ b/management/server/account_test.go @@ -732,6 +732,7 @@ func TestAccountManager_GetAccountFromPAT(t *testing.T) { PATs: map[string]*types.PersonalAccessToken{ "tokenId": { ID: "tokenId", + UserID: "someUser", HashedToken: encodedHashedToken, }, }, @@ -745,14 +746,14 @@ func TestAccountManager_GetAccountFromPAT(t *testing.T) { Store: store, } - account, user, pat, err := am.GetAccountFromPAT(context.Background(), token) + user, pat, _, _, err := am.GetPATInfo(context.Background(), token) if err != nil { t.Fatalf("Error when getting Account from PAT: %s", err) } - assert.Equal(t, "account_id", account.Id) + assert.Equal(t, "account_id", user.AccountID) assert.Equal(t, "someUser", user.Id) - assert.Equal(t, account.Users["someUser"].PATs["tokenId"], pat) + assert.Equal(t, account.Users["someUser"].PATs["tokenId"].ID, pat.ID) } func TestDefaultAccountManager_MarkPATUsed(t *testing.T) { diff --git a/management/server/activity/sqlite/sqlite.go b/management/server/activity/sqlite/sqlite.go index 823e0b4ac..ffb863de9 100644 --- a/management/server/activity/sqlite/sqlite.go +++ b/management/server/activity/sqlite/sqlite.go @@ -6,6 +6,7 @@ import ( "encoding/json" "fmt" "path/filepath" + "runtime" "time" _ "github.com/mattn/go-sqlite3" @@ -95,6 +96,7 @@ func NewSQLiteStore(ctx context.Context, dataDir string, encryptionKey string) ( if err != nil { return nil, err } + db.SetMaxOpenConns(runtime.NumCPU()) crypt, err := NewFieldEncrypt(encryptionKey) if err != nil { diff --git a/management/server/http/handler.go b/management/server/http/handler.go index cc2ad00b7..7ce09fffa 100644 --- a/management/server/http/handler.go +++ b/management/server/http/handler.go @@ -43,7 +43,7 @@ func NewAPIHandler(ctx context.Context, accountManager s.AccountManager, network ) authMiddleware := middleware.NewAuthMiddleware( - accountManager.GetAccountFromPAT, + accountManager.GetPATInfo, jwtValidator.ValidateAndParse, accountManager.MarkPATUsed, accountManager.CheckUserAccessByJWTGroups, diff --git a/management/server/http/handlers/events/events_handler_test.go b/management/server/http/handlers/events/events_handler_test.go index 17478aba3..fd603f289 100644 --- a/management/server/http/handlers/events/events_handler_test.go +++ b/management/server/http/handlers/events/events_handler_test.go @@ -32,8 +32,8 @@ func initEventsTestData(account string, events ...*activity.Event) *handler { GetAccountIDFromTokenFunc: func(_ context.Context, claims jwtclaims.AuthorizationClaims) (string, string, error) { return claims.AccountId, claims.UserId, nil }, - GetUsersFromAccountFunc: func(_ context.Context, accountID, userID string) ([]*types.UserInfo, error) { - return make([]*types.UserInfo, 0), nil + GetUsersFromAccountFunc: func(_ context.Context, accountID, userID string) (map[string]*types.UserInfo, error) { + return make(map[string]*types.UserInfo), nil }, }, claimsExtractor: jwtclaims.NewClaimsExtractor( diff --git a/management/server/http/handlers/users/users_handler_test.go b/management/server/http/handlers/users/users_handler_test.go index 90081830a..ff77cedff 100644 --- a/management/server/http/handlers/users/users_handler_test.go +++ b/management/server/http/handlers/users/users_handler_test.go @@ -52,7 +52,7 @@ var usersTestAccount = &types.Account{ Issued: types.UserIssuedAPI, }, nonDeletableServiceUserID: { - Id: serviceUserID, + Id: nonDeletableServiceUserID, Role: "admin", IsServiceUser: true, NonDeletable: true, @@ -70,10 +70,10 @@ func initUsersTestData() *handler { GetUserByIDFunc: func(ctx context.Context, id string) (*types.User, error) { return usersTestAccount.Users[id], nil }, - GetUsersFromAccountFunc: func(_ context.Context, accountID, userID string) ([]*types.UserInfo, error) { - users := make([]*types.UserInfo, 0) + GetUsersFromAccountFunc: func(_ context.Context, accountID, userID string) (map[string]*types.UserInfo, error) { + usersInfos := make(map[string]*types.UserInfo) for _, v := range usersTestAccount.Users { - users = append(users, &types.UserInfo{ + usersInfos[v.Id] = &types.UserInfo{ ID: v.Id, Role: string(v.Role), Name: "", @@ -81,9 +81,9 @@ func initUsersTestData() *handler { IsServiceUser: v.IsServiceUser, NonDeletable: v.NonDeletable, Issued: v.Issued, - }) + } } - return users, nil + return usersInfos, nil }, CreateUserFunc: func(_ context.Context, accountID, userID string, key *types.UserInfo) (*types.UserInfo, error) { if userID != existingUserID { diff --git a/management/server/http/middleware/auth_middleware.go b/management/server/http/middleware/auth_middleware.go index 182c30cf6..dcf73259a 100644 --- a/management/server/http/middleware/auth_middleware.go +++ b/management/server/http/middleware/auth_middleware.go @@ -19,8 +19,8 @@ import ( "github.com/netbirdio/netbird/management/server/types" ) -// GetAccountFromPATFunc function -type GetAccountFromPATFunc func(ctx context.Context, token string) (*types.Account, *types.User, *types.PersonalAccessToken, error) +// GetAccountInfoFromPATFunc function +type GetAccountInfoFromPATFunc func(ctx context.Context, token string) (user *types.User, pat *types.PersonalAccessToken, domain string, category string, err error) // ValidateAndParseTokenFunc function type ValidateAndParseTokenFunc func(ctx context.Context, token string) (*jwt.Token, error) @@ -33,7 +33,7 @@ type CheckUserAccessByJWTGroupsFunc func(ctx context.Context, claims jwtclaims.A // AuthMiddleware middleware to verify personal access tokens (PAT) and JWT tokens type AuthMiddleware struct { - getAccountFromPAT GetAccountFromPATFunc + getAccountInfoFromPAT GetAccountInfoFromPATFunc validateAndParseToken ValidateAndParseTokenFunc markPATUsed MarkPATUsedFunc checkUserAccessByJWTGroups CheckUserAccessByJWTGroupsFunc @@ -47,7 +47,7 @@ const ( ) // NewAuthMiddleware instance constructor -func NewAuthMiddleware(getAccountFromPAT GetAccountFromPATFunc, validateAndParseToken ValidateAndParseTokenFunc, +func NewAuthMiddleware(getAccountInfoFromPAT GetAccountInfoFromPATFunc, validateAndParseToken ValidateAndParseTokenFunc, markPATUsed MarkPATUsedFunc, checkUserAccessByJWTGroups CheckUserAccessByJWTGroupsFunc, claimsExtractor *jwtclaims.ClaimsExtractor, audience string, userIdClaim string) *AuthMiddleware { if userIdClaim == "" { @@ -55,7 +55,7 @@ func NewAuthMiddleware(getAccountFromPAT GetAccountFromPATFunc, validateAndParse } return &AuthMiddleware{ - getAccountFromPAT: getAccountFromPAT, + getAccountInfoFromPAT: getAccountInfoFromPAT, validateAndParseToken: validateAndParseToken, markPATUsed: markPATUsed, checkUserAccessByJWTGroups: checkUserAccessByJWTGroups, @@ -151,13 +151,11 @@ func (m *AuthMiddleware) verifyUserAccess(ctx context.Context, validatedToken *j // CheckPATFromRequest checks if the PAT is valid func (m *AuthMiddleware) checkPATFromRequest(w http.ResponseWriter, r *http.Request, auth []string) error { token, err := getTokenFromPATRequest(auth) - - // If an error occurs, call the error handler and return an error if err != nil { - return fmt.Errorf("Error extracting token: %w", err) + return fmt.Errorf("error extracting token: %w", err) } - account, user, pat, err := m.getAccountFromPAT(r.Context(), token) + user, pat, accDomain, accCategory, err := m.getAccountInfoFromPAT(r.Context(), token) if err != nil { return fmt.Errorf("invalid Token: %w", err) } @@ -172,9 +170,9 @@ func (m *AuthMiddleware) checkPATFromRequest(w http.ResponseWriter, r *http.Requ claimMaps := jwt.MapClaims{} claimMaps[m.userIDClaim] = user.Id - claimMaps[m.audience+jwtclaims.AccountIDSuffix] = account.Id - claimMaps[m.audience+jwtclaims.DomainIDSuffix] = account.Domain - claimMaps[m.audience+jwtclaims.DomainCategorySuffix] = account.DomainCategory + claimMaps[m.audience+jwtclaims.AccountIDSuffix] = user.AccountID + claimMaps[m.audience+jwtclaims.DomainIDSuffix] = accDomain + claimMaps[m.audience+jwtclaims.DomainCategorySuffix] = accCategory claimMaps[jwtclaims.IsToken] = true jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, claimMaps) newRequest := r.WithContext(context.WithValue(r.Context(), jwtclaims.TokenUserProperty, jwtToken)) //nolint diff --git a/management/server/http/middleware/auth_middleware_test.go b/management/server/http/middleware/auth_middleware_test.go index 41bdb7fc5..c1686ed44 100644 --- a/management/server/http/middleware/auth_middleware_test.go +++ b/management/server/http/middleware/auth_middleware_test.go @@ -34,7 +34,8 @@ var testAccount = &types.Account{ Domain: domain, Users: map[string]*types.User{ userID: { - Id: userID, + Id: userID, + AccountID: accountID, PATs: map[string]*types.PersonalAccessToken{ tokenID: { ID: tokenID, @@ -50,11 +51,11 @@ var testAccount = &types.Account{ }, } -func mockGetAccountFromPAT(_ context.Context, token string) (*types.Account, *types.User, *types.PersonalAccessToken, error) { +func mockGetAccountInfoFromPAT(_ context.Context, token string) (user *types.User, pat *types.PersonalAccessToken, domain string, category string, err error) { if token == PAT { - return testAccount, testAccount.Users[userID], testAccount.Users[userID].PATs[tokenID], nil + return testAccount.Users[userID], testAccount.Users[userID].PATs[tokenID], testAccount.Domain, testAccount.DomainCategory, nil } - return nil, nil, nil, fmt.Errorf("PAT invalid") + return nil, nil, "", "", fmt.Errorf("PAT invalid") } func mockValidateAndParseToken(_ context.Context, token string) (*jwt.Token, error) { @@ -166,7 +167,7 @@ func TestAuthMiddleware_Handler(t *testing.T) { ) authMiddleware := NewAuthMiddleware( - mockGetAccountFromPAT, + mockGetAccountInfoFromPAT, mockValidateAndParseToken, mockMarkPATUsed, mockCheckUserAccessByJWTGroups, diff --git a/management/server/http/testing/benchmarks/users_handler_benchmark_test.go b/management/server/http/testing/benchmarks/users_handler_benchmark_test.go index 549a51c0e..0baf76328 100644 --- a/management/server/http/testing/benchmarks/users_handler_benchmark_test.go +++ b/management/server/http/testing/benchmarks/users_handler_benchmark_test.go @@ -35,14 +35,14 @@ var benchCasesUsers = map[string]testing_tools.BenchmarkCase{ func BenchmarkUpdateUser(b *testing.B) { var expectedMetrics = map[string]testing_tools.PerformanceMetrics{ - "Users - XS": {MinMsPerOpLocal: 700, MaxMsPerOpLocal: 1000, MinMsPerOpCICD: 1300, MaxMsPerOpCICD: 8000}, - "Users - S": {MinMsPerOpLocal: 1, MaxMsPerOpLocal: 5, MinMsPerOpCICD: 4, MaxMsPerOpCICD: 50}, - "Users - M": {MinMsPerOpLocal: 20, MaxMsPerOpLocal: 40, MinMsPerOpCICD: 30, MaxMsPerOpCICD: 250}, - "Users - L": {MinMsPerOpLocal: 60, MaxMsPerOpLocal: 100, MinMsPerOpCICD: 90, MaxMsPerOpCICD: 700}, - "Peers - L": {MinMsPerOpLocal: 300, MaxMsPerOpLocal: 500, MinMsPerOpCICD: 550, MaxMsPerOpCICD: 2400}, - "Groups - L": {MinMsPerOpLocal: 400, MaxMsPerOpLocal: 600, MinMsPerOpCICD: 750, MaxMsPerOpCICD: 5000}, - "Setup Keys - L": {MinMsPerOpLocal: 50, MaxMsPerOpLocal: 200, MinMsPerOpCICD: 130, MaxMsPerOpCICD: 1000}, - "Users - XL": {MinMsPerOpLocal: 350, MaxMsPerOpLocal: 550, MinMsPerOpCICD: 650, MaxMsPerOpCICD: 3500}, + "Users - XS": {MinMsPerOpLocal: 100, MaxMsPerOpLocal: 160, MinMsPerOpCICD: 100, MaxMsPerOpCICD: 310}, + "Users - S": {MinMsPerOpLocal: 0.3, MaxMsPerOpLocal: 3, MinMsPerOpCICD: 1, MaxMsPerOpCICD: 15}, + "Users - M": {MinMsPerOpLocal: 1, MaxMsPerOpLocal: 10, MinMsPerOpCICD: 3, MaxMsPerOpCICD: 20}, + "Users - L": {MinMsPerOpLocal: 5, MaxMsPerOpLocal: 20, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 50}, + "Peers - L": {MinMsPerOpLocal: 80, MaxMsPerOpLocal: 150, MinMsPerOpCICD: 80, MaxMsPerOpCICD: 310}, + "Groups - L": {MinMsPerOpLocal: 10, MaxMsPerOpLocal: 50, MinMsPerOpCICD: 20, MaxMsPerOpCICD: 120}, + "Setup Keys - L": {MinMsPerOpLocal: 5, MaxMsPerOpLocal: 20, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 50}, + "Users - XL": {MinMsPerOpLocal: 30, MaxMsPerOpLocal: 100, MinMsPerOpCICD: 60, MaxMsPerOpCICD: 280}, } log.SetOutput(io.Discard) @@ -118,14 +118,14 @@ func BenchmarkGetOneUser(b *testing.B) { func BenchmarkGetAllUsers(b *testing.B) { var expectedMetrics = map[string]testing_tools.PerformanceMetrics{ - "Users - XS": {MinMsPerOpLocal: 50, MaxMsPerOpLocal: 90, MinMsPerOpCICD: 60, MaxMsPerOpCICD: 180}, - "Users - S": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 0, MaxMsPerOpCICD: 30}, - "Users - M": {MinMsPerOpLocal: 5, MaxMsPerOpLocal: 12, MinMsPerOpCICD: 0, MaxMsPerOpCICD: 30}, - "Users - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 0, MaxMsPerOpCICD: 30}, - "Peers - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 0, MaxMsPerOpCICD: 30}, - "Groups - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 0, MaxMsPerOpCICD: 30}, - "Setup Keys - L": {MinMsPerOpLocal: 40, MaxMsPerOpLocal: 140, MinMsPerOpCICD: 60, MaxMsPerOpCICD: 200}, - "Users - XL": {MinMsPerOpLocal: 15, MaxMsPerOpLocal: 40, MinMsPerOpCICD: 20, MaxMsPerOpCICD: 90}, + "Users - XS": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 0, MaxMsPerOpCICD: 10}, + "Users - S": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 0, MaxMsPerOpCICD: 10}, + "Users - M": {MinMsPerOpLocal: 3, MaxMsPerOpLocal: 10, MinMsPerOpCICD: 5, MaxMsPerOpCICD: 15}, + "Users - L": {MinMsPerOpLocal: 10, MaxMsPerOpLocal: 20, MinMsPerOpCICD: 20, MaxMsPerOpCICD: 50}, + "Peers - L": {MinMsPerOpLocal: 15, MaxMsPerOpLocal: 25, MinMsPerOpCICD: 20, MaxMsPerOpCICD: 55}, + "Groups - L": {MinMsPerOpLocal: 15, MaxMsPerOpLocal: 25, MinMsPerOpCICD: 25, MaxMsPerOpCICD: 55}, + "Setup Keys - L": {MinMsPerOpLocal: 15, MaxMsPerOpLocal: 25, MinMsPerOpCICD: 25, MaxMsPerOpCICD: 55}, + "Users - XL": {MinMsPerOpLocal: 80, MaxMsPerOpLocal: 120, MinMsPerOpCICD: 100, MaxMsPerOpCICD: 300}, } log.SetOutput(io.Discard) @@ -141,7 +141,7 @@ func BenchmarkGetAllUsers(b *testing.B) { b.ResetTimer() start := time.Now() for i := 0; i < b.N; i++ { - req := testing_tools.BuildRequest(b, nil, http.MethodGet, "/api/setup-keys", testing_tools.TestAdminId) + req := testing_tools.BuildRequest(b, nil, http.MethodGet, "/api/users", testing_tools.TestAdminId) apiHandler.ServeHTTP(recorder, req) } @@ -152,14 +152,14 @@ func BenchmarkGetAllUsers(b *testing.B) { func BenchmarkDeleteUsers(b *testing.B) { var expectedMetrics = map[string]testing_tools.PerformanceMetrics{ - "Users - XS": {MinMsPerOpLocal: 1000, MaxMsPerOpLocal: 1600, MinMsPerOpCICD: 1900, MaxMsPerOpCICD: 11000}, - "Users - S": {MinMsPerOpLocal: 15, MaxMsPerOpLocal: 40, MinMsPerOpCICD: 30, MaxMsPerOpCICD: 200}, - "Users - M": {MinMsPerOpLocal: 15, MaxMsPerOpLocal: 70, MinMsPerOpCICD: 15, MaxMsPerOpCICD: 230}, - "Users - L": {MinMsPerOpLocal: 15, MaxMsPerOpLocal: 45, MinMsPerOpCICD: 30, MaxMsPerOpCICD: 190}, - "Peers - L": {MinMsPerOpLocal: 400, MaxMsPerOpLocal: 600, MinMsPerOpCICD: 650, MaxMsPerOpCICD: 1800}, - "Groups - L": {MinMsPerOpLocal: 600, MaxMsPerOpLocal: 800, MinMsPerOpCICD: 1200, MaxMsPerOpCICD: 7500}, - "Setup Keys - L": {MinMsPerOpLocal: 20, MaxMsPerOpLocal: 200, MinMsPerOpCICD: 40, MaxMsPerOpCICD: 600}, - "Users - XL": {MinMsPerOpLocal: 50, MaxMsPerOpLocal: 150, MinMsPerOpCICD: 80, MaxMsPerOpCICD: 400}, + "Users - XS": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 5, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 15}, + "Users - S": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 5, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 15}, + "Users - M": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 5, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 15}, + "Users - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 5, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 15}, + "Peers - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 5, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 15}, + "Groups - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 5, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 15}, + "Setup Keys - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 5, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 15}, + "Users - XL": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 5, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 15}, } log.SetOutput(io.Discard) diff --git a/management/server/mock_server/account_mock.go b/management/server/mock_server/account_mock.go index c8e42d20a..b20eb87bb 100644 --- a/management/server/mock_server/account_mock.go +++ b/management/server/mock_server/account_mock.go @@ -53,8 +53,8 @@ type MockAccountManager struct { SavePolicyFunc func(ctx context.Context, accountID, userID string, policy *types.Policy) (*types.Policy, error) DeletePolicyFunc func(ctx context.Context, accountID, policyID, userID string) error ListPoliciesFunc func(ctx context.Context, accountID, userID string) ([]*types.Policy, error) - GetUsersFromAccountFunc func(ctx context.Context, accountID, userID string) ([]*types.UserInfo, error) - GetAccountFromPATFunc func(ctx context.Context, pat string) (*types.Account, *types.User, *types.PersonalAccessToken, error) + GetUsersFromAccountFunc func(ctx context.Context, accountID, userID string) (map[string]*types.UserInfo, error) + GetPATInfoFunc func(ctx context.Context, token string) (*types.User, *types.PersonalAccessToken, string, string, error) MarkPATUsedFunc func(ctx context.Context, pat string) error UpdatePeerMetaFunc func(ctx context.Context, peerID string, meta nbpeer.PeerSystemMeta) error UpdatePeerFunc func(ctx context.Context, accountID, userID string, peer *nbpeer.Peer) (*nbpeer.Peer, error) @@ -69,7 +69,7 @@ type MockAccountManager struct { SaveOrAddUserFunc func(ctx context.Context, accountID, userID string, user *types.User, addIfNotExists bool) (*types.UserInfo, error) SaveOrAddUsersFunc func(ctx context.Context, accountID, initiatorUserID string, update []*types.User, addIfNotExists bool) ([]*types.UserInfo, error) DeleteUserFunc func(ctx context.Context, accountID string, initiatorUserID string, targetUserID string) error - DeleteRegularUsersFunc func(ctx context.Context, accountID, initiatorUserID string, targetUserIDs []string) error + DeleteRegularUsersFunc func(ctx context.Context, accountID, initiatorUserID string, targetUserIDs []string, userInfos map[string]*types.UserInfo) error CreatePATFunc func(ctx context.Context, accountID string, initiatorUserID string, targetUserId string, tokenName string, expiresIn int) (*types.PersonalAccessTokenGenerated, error) DeletePATFunc func(ctx context.Context, accountID string, initiatorUserID string, targetUserId string, tokenID string) error GetPATFunc func(ctx context.Context, accountID string, initiatorUserID string, targetUserId string, tokenID string) (*types.PersonalAccessToken, error) @@ -110,6 +110,7 @@ type MockAccountManager struct { GetUserByIDFunc func(ctx context.Context, id string) (*types.User, error) GetAccountSettingsFunc func(ctx context.Context, accountID string, userID string) (*types.Settings, error) DeleteSetupKeyFunc func(ctx context.Context, accountID, userID, keyID string) error + BuildUserInfosForAccountFunc func(ctx context.Context, accountID, initiatorUserID string, accountUsers []*types.User) (map[string]*types.UserInfo, error) } func (am *MockAccountManager) UpdateAccountPeers(ctx context.Context, accountID string) { @@ -165,7 +166,7 @@ func (am *MockAccountManager) GetAllGroups(ctx context.Context, accountID, userI } // GetUsersFromAccount mock implementation of GetUsersFromAccount from server.AccountManager interface -func (am *MockAccountManager) GetUsersFromAccount(ctx context.Context, accountID string, userID string) ([]*types.UserInfo, error) { +func (am *MockAccountManager) GetUsersFromAccount(ctx context.Context, accountID string, userID string) (map[string]*types.UserInfo, error) { if am.GetUsersFromAccountFunc != nil { return am.GetUsersFromAccountFunc(ctx, accountID, userID) } @@ -238,12 +239,12 @@ func (am *MockAccountManager) MarkPeerConnected(ctx context.Context, peerKey str return status.Errorf(codes.Unimplemented, "method MarkPeerConnected is not implemented") } -// GetAccountFromPAT mock implementation of GetAccountFromPAT from server.AccountManager interface -func (am *MockAccountManager) GetAccountFromPAT(ctx context.Context, pat string) (*types.Account, *types.User, *types.PersonalAccessToken, error) { - if am.GetAccountFromPATFunc != nil { - return am.GetAccountFromPATFunc(ctx, pat) +// GetPATInfo mock implementation of GetPATInfo from server.AccountManager interface +func (am *MockAccountManager) GetPATInfo(ctx context.Context, pat string) (*types.User, *types.PersonalAccessToken, string, string, error) { + if am.GetPATInfoFunc != nil { + return am.GetPATInfoFunc(ctx, pat) } - return nil, nil, nil, status.Errorf(codes.Unimplemented, "method GetAccountFromPAT is not implemented") + return nil, nil, "", "", status.Errorf(codes.Unimplemented, "method GetPATInfo is not implemented") } // DeleteAccount mock implementation of DeleteAccount from server.AccountManager interface @@ -550,9 +551,9 @@ func (am *MockAccountManager) DeleteUser(ctx context.Context, accountID string, } // DeleteRegularUsers mocks DeleteRegularUsers of the AccountManager interface -func (am *MockAccountManager) DeleteRegularUsers(ctx context.Context, accountID string, initiatorUserID string, targetUserIDs []string) error { +func (am *MockAccountManager) DeleteRegularUsers(ctx context.Context, accountID, initiatorUserID string, targetUserIDs []string, userInfos map[string]*types.UserInfo) error { if am.DeleteRegularUsersFunc != nil { - return am.DeleteRegularUsersFunc(ctx, accountID, initiatorUserID, targetUserIDs) + return am.DeleteRegularUsersFunc(ctx, accountID, initiatorUserID, targetUserIDs, userInfos) } return status.Errorf(codes.Unimplemented, "method DeleteRegularUsers is not implemented") } @@ -849,3 +850,11 @@ func (am *MockAccountManager) GetPeerGroups(ctx context.Context, accountID, peer } return nil, status.Errorf(codes.Unimplemented, "method GetPeerGroups is not implemented") } + +// BuildUserInfosForAccount mocks BuildUserInfosForAccount of the AccountManager interface +func (am *MockAccountManager) BuildUserInfosForAccount(ctx context.Context, accountID, initiatorUserID string, accountUsers []*types.User) (map[string]*types.UserInfo, error) { + if am.BuildUserInfosForAccountFunc != nil { + return am.BuildUserInfosForAccountFunc(ctx, accountID, initiatorUserID, accountUsers) + } + return nil, status.Errorf(codes.Unimplemented, "method BuildUserInfosForAccount is not implemented") +} diff --git a/management/server/peer_test.go b/management/server/peer_test.go index a0417c996..6894d092d 100644 --- a/management/server/peer_test.go +++ b/management/server/peer_test.go @@ -13,6 +13,7 @@ import ( "testing" "time" + nbAccount "github.com/netbirdio/netbird/management/server/account" "github.com/rs/xid" log "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" @@ -28,7 +29,6 @@ import ( nbdns "github.com/netbirdio/netbird/dns" "github.com/netbirdio/netbird/management/domain" "github.com/netbirdio/netbird/management/proto" - nbAccount "github.com/netbirdio/netbird/management/server/account" "github.com/netbirdio/netbird/management/server/activity" nbpeer "github.com/netbirdio/netbird/management/server/peer" "github.com/netbirdio/netbird/management/server/posture" @@ -1554,7 +1554,8 @@ func TestPeerAccountPeersUpdate(t *testing.T) { // Adding peer to group linked with policy should update account peers and send peer update t.Run("adding peer to group linked with policy", func(t *testing.T) { _, err = manager.SavePolicy(context.Background(), account.Id, userID, &types.Policy{ - Enabled: true, + AccountID: account.Id, + Enabled: true, Rules: []*types.PolicyRule{ { Enabled: true, diff --git a/management/server/status/error.go b/management/server/status/error.go index 7e384922d..96b103183 100644 --- a/management/server/status/error.go +++ b/management/server/status/error.go @@ -93,7 +93,7 @@ func NewPeerNotPartOfAccountError() error { // NewUserNotFoundError creates a new Error with NotFound type for a missing user func NewUserNotFoundError(userKey string) error { - return Errorf(NotFound, "user not found: %s", userKey) + return Errorf(NotFound, "user: %s not found", userKey) } // NewPeerNotRegisteredError creates a new Error with NotFound type for a missing peer @@ -191,3 +191,18 @@ func NewResourceNotPartOfNetworkError(resourceID, networkID string) error { func NewRouterNotPartOfNetworkError(routerID, networkID string) error { return Errorf(BadRequest, "router %s is not part of the network %s", routerID, networkID) } + +// NewServiceUserRoleInvalidError creates a new Error with InvalidArgument type for creating a service user with owner role +func NewServiceUserRoleInvalidError() error { + return Errorf(InvalidArgument, "can't create a service user with owner role") +} + +// NewOwnerDeletePermissionError creates a new Error with PermissionDenied type for attempting +// to delete a user with the owner role. +func NewOwnerDeletePermissionError() error { + return Errorf(PermissionDenied, "can't delete a user with the owner role") +} + +func NewPATNotFoundError(patID string) error { + return Errorf(NotFound, "PAT: %s not found", patID) +} diff --git a/management/server/store/sql_store.go b/management/server/store/sql_store.go index 2179f0754..6a6753595 100644 --- a/management/server/store/sql_store.go +++ b/management/server/store/sql_store.go @@ -15,6 +15,7 @@ import ( "sync" "time" + "github.com/netbirdio/netbird/management/server/util" log "github.com/sirupsen/logrus" "gorm.io/driver/mysql" "gorm.io/driver/postgres" @@ -414,24 +415,16 @@ func (s *SqlStore) SavePeerLocation(ctx context.Context, lockStrength LockingStr } // SaveUsers saves the given list of users to the database. -// It updates existing users if a conflict occurs. -func (s *SqlStore) SaveUsers(accountID string, users map[string]*types.User) error { - usersToSave := make([]types.User, 0, len(users)) - for _, user := range users { - user.AccountID = accountID - for id, pat := range user.PATs { - pat.ID = id - user.PATsG = append(user.PATsG, *pat) - } - usersToSave = append(usersToSave, *user) - } - err := s.db.Session(&gorm.Session{FullSaveAssociations: true}). - Clauses(clause.OnConflict{UpdateAll: true}). - Create(&usersToSave).Error - if err != nil { - return status.Errorf(status.Internal, "failed to save users to store: %v", err) +func (s *SqlStore) SaveUsers(ctx context.Context, lockStrength LockingStrength, users []*types.User) error { + if len(users) == 0 { + return nil } + result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Save(&users) + if result.Error != nil { + log.WithContext(ctx).Errorf("failed to save users to store: %s", result.Error) + return status.Errorf(status.Internal, "failed to save users to store") + } return nil } @@ -439,7 +432,8 @@ func (s *SqlStore) SaveUsers(accountID string, users map[string]*types.User) err func (s *SqlStore) SaveUser(ctx context.Context, lockStrength LockingStrength, user *types.User) error { result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Save(user) if result.Error != nil { - return status.Errorf(status.Internal, "failed to save user to store: %v", result.Error) + log.WithContext(ctx).Errorf("failed to save user to store: %s", result.Error) + return status.Errorf(status.Internal, "failed to save user to store") } return nil } @@ -526,30 +520,17 @@ func (s *SqlStore) GetTokenIDByHashedToken(ctx context.Context, hashedToken stri return token.ID, nil } -func (s *SqlStore) GetUserByTokenID(ctx context.Context, tokenID string) (*types.User, error) { - var token types.PersonalAccessToken - result := s.db.First(&token, idQueryCondition, tokenID) +func (s *SqlStore) GetUserByPATID(ctx context.Context, lockStrength LockingStrength, patID string) (*types.User, error) { + var user types.User + result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}). + Joins("JOIN personal_access_tokens ON personal_access_tokens.user_id = users.id"). + Where("personal_access_tokens.id = ?", patID).First(&user) if result.Error != nil { if errors.Is(result.Error, gorm.ErrRecordNotFound) { - return nil, status.Errorf(status.NotFound, "account not found: index lookup failed") + return nil, status.NewPATNotFoundError(patID) } - log.WithContext(ctx).Errorf("error when getting token from the store: %s", result.Error) - return nil, status.NewGetAccountFromStoreError(result.Error) - } - - if token.UserID == "" { - return nil, status.Errorf(status.NotFound, "account not found: index lookup failed") - } - - var user types.User - result = s.db.Preload("PATsG").First(&user, idQueryCondition, token.UserID) - if result.Error != nil { - return nil, status.Errorf(status.NotFound, "account not found: index lookup failed") - } - - user.PATs = make(map[string]*types.PersonalAccessToken, len(user.PATsG)) - for _, pat := range user.PATsG { - user.PATs[pat.ID] = pat.Copy() + log.WithContext(ctx).Errorf("failed to get token user from the store: %s", result.Error) + return nil, status.NewGetUserFromStoreError() } return &user, nil @@ -557,8 +538,7 @@ func (s *SqlStore) GetUserByTokenID(ctx context.Context, tokenID string) (*types func (s *SqlStore) GetUserByUserID(ctx context.Context, lockStrength LockingStrength, userID string) (*types.User, error) { var user types.User - result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}). - Preload(clause.Associations).First(&user, idQueryCondition, userID) + result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).First(&user, idQueryCondition, userID) if result.Error != nil { if errors.Is(result.Error, gorm.ErrRecordNotFound) { return nil, status.NewUserNotFoundError(userID) @@ -569,6 +549,25 @@ func (s *SqlStore) GetUserByUserID(ctx context.Context, lockStrength LockingStre return &user, nil } +func (s *SqlStore) DeleteUser(ctx context.Context, lockStrength LockingStrength, accountID, userID string) error { + err := s.db.Transaction(func(tx *gorm.DB) error { + result := tx.Clauses(clause.Locking{Strength: string(lockStrength)}). + Delete(&types.PersonalAccessToken{}, "user_id = ?", userID) + if result.Error != nil { + return result.Error + } + + return tx.Clauses(clause.Locking{Strength: string(lockStrength)}). + Delete(&types.User{}, accountAndIDQueryCondition, accountID, userID).Error + }) + if err != nil { + log.WithContext(ctx).Errorf("failed to delete user from the store: %s", err) + return status.Errorf(status.Internal, "failed to delete user from store") + } + + return nil +} + func (s *SqlStore) GetAccountUsers(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*types.User, error) { var users []*types.User result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Find(&users, accountIDCondition, accountID) @@ -899,6 +898,20 @@ func (s *SqlStore) GetAccountSettings(ctx context.Context, lockStrength LockingS return accountSettings.Settings, nil } +func (s *SqlStore) GetAccountCreatedBy(ctx context.Context, lockStrength LockingStrength, accountID string) (string, error) { + var createdBy string + result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Model(&types.Account{}). + Select("created_by").First(&createdBy, idQueryCondition, accountID) + if result.Error != nil { + if errors.Is(result.Error, gorm.ErrRecordNotFound) { + return "", status.NewAccountNotFoundError(accountID) + } + return "", status.NewGetAccountFromStoreError(result.Error) + } + + return createdBy, nil +} + // SaveUserLastLogin stores the last login time for a user in DB. func (s *SqlStore) SaveUserLastLogin(ctx context.Context, accountID, userID string, lastLogin time.Time) error { var user types.User @@ -2053,3 +2066,94 @@ func (s *SqlStore) DeleteNetworkResource(ctx context.Context, lockStrength Locki return nil } + +// GetPATByHashedToken returns a PersonalAccessToken by its hashed token. +func (s *SqlStore) GetPATByHashedToken(ctx context.Context, lockStrength LockingStrength, hashedToken string) (*types.PersonalAccessToken, error) { + var pat types.PersonalAccessToken + result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).First(&pat, "hashed_token = ?", hashedToken) + if result.Error != nil { + if errors.Is(result.Error, gorm.ErrRecordNotFound) { + return nil, status.NewPATNotFoundError(hashedToken) + } + log.WithContext(ctx).Errorf("failed to get pat by hash from the store: %s", result.Error) + return nil, status.Errorf(status.Internal, "failed to get pat by hash from store") + } + + return &pat, nil +} + +// GetPATByID retrieves a personal access token by its ID and user ID. +func (s *SqlStore) GetPATByID(ctx context.Context, lockStrength LockingStrength, userID string, patID string) (*types.PersonalAccessToken, error) { + var pat types.PersonalAccessToken + result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}). + First(&pat, "id = ? AND user_id = ?", patID, userID) + if err := result.Error; err != nil { + if errors.Is(result.Error, gorm.ErrRecordNotFound) { + return nil, status.NewPATNotFoundError(patID) + } + log.WithContext(ctx).Errorf("failed to get pat from the store: %s", err) + return nil, status.Errorf(status.Internal, "failed to get pat from store") + } + + return &pat, nil +} + +// GetUserPATs retrieves personal access tokens for a user. +func (s *SqlStore) GetUserPATs(ctx context.Context, lockStrength LockingStrength, userID string) ([]*types.PersonalAccessToken, error) { + var pats []*types.PersonalAccessToken + result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Find(&pats, "user_id = ?", userID) + if err := result.Error; err != nil { + log.WithContext(ctx).Errorf("failed to get user pat's from the store: %s", err) + return nil, status.Errorf(status.Internal, "failed to get user pat's from store") + } + + return pats, nil +} + +// MarkPATUsed marks a personal access token as used. +func (s *SqlStore) MarkPATUsed(ctx context.Context, lockStrength LockingStrength, patID string) error { + patCopy := types.PersonalAccessToken{ + LastUsed: util.ToPtr(time.Now().UTC()), + } + + fieldsToUpdate := []string{"last_used"} + result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Select(fieldsToUpdate). + Where(idQueryCondition, patID).Updates(&patCopy) + if result.Error != nil { + log.WithContext(ctx).Errorf("failed to mark pat as used: %s", result.Error) + return status.Errorf(status.Internal, "failed to mark pat as used") + } + + if result.RowsAffected == 0 { + return status.NewPATNotFoundError(patID) + } + + return nil +} + +// SavePAT saves a personal access token to the database. +func (s *SqlStore) SavePAT(ctx context.Context, lockStrength LockingStrength, pat *types.PersonalAccessToken) error { + result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Save(pat) + if err := result.Error; err != nil { + log.WithContext(ctx).Errorf("failed to save pat to the store: %s", err) + return status.Errorf(status.Internal, "failed to save pat to store") + } + + return nil +} + +// DeletePAT deletes a personal access token from the database. +func (s *SqlStore) DeletePAT(ctx context.Context, lockStrength LockingStrength, userID, patID string) error { + result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}). + Delete(&types.PersonalAccessToken{}, "user_id = ? AND id = ?", userID, patID) + if err := result.Error; err != nil { + log.WithContext(ctx).Errorf("failed to delete pat from the store: %s", err) + return status.Errorf(status.Internal, "failed to delete pat from store") + } + + if result.RowsAffected == 0 { + return status.NewPATNotFoundError(patID) + } + + return nil +} diff --git a/management/server/store/sql_store_test.go b/management/server/store/sql_store_test.go index 9350da1c8..4dcdadf44 100644 --- a/management/server/store/sql_store_test.go +++ b/management/server/store/sql_store_test.go @@ -627,29 +627,6 @@ func TestSqlite_GetTokenIDByHashedToken(t *testing.T) { require.Equal(t, status.NotFound, parsedErr.Type(), "should return not found error") } -func TestSqlite_GetUserByTokenID(t *testing.T) { - if runtime.GOOS == "windows" { - t.Skip("The SQLite store is not properly supported by Windows yet") - } - - t.Setenv("NETBIRD_STORE_ENGINE", string(SqliteStoreEngine)) - store, cleanUp, err := NewTestStoreFromSQL(context.Background(), "../testdata/store.sql", t.TempDir()) - t.Cleanup(cleanUp) - assert.NoError(t, err) - - id := "9dj38s35-63fb-11ec-90d6-0242ac120003" - - user, err := store.GetUserByTokenID(context.Background(), id) - require.NoError(t, err) - require.Equal(t, id, user.PATs[id].ID) - - _, err = store.GetUserByTokenID(context.Background(), "non-existing-id") - require.Error(t, err) - parsedErr, ok := status.FromError(err) - require.True(t, ok) - require.Equal(t, status.NotFound, parsedErr.Type(), "should return not found error") -} - func TestMigrate(t *testing.T) { if (os.Getenv("CI") == "true" && runtime.GOOS == "darwin") || runtime.GOOS == "windows" { t.Skip("skip CI tests on darwin and windows") @@ -962,23 +939,6 @@ func TestPostgresql_GetTokenIDByHashedToken(t *testing.T) { require.Equal(t, id, token) } -func TestPostgresql_GetUserByTokenID(t *testing.T) { - if (os.Getenv("CI") == "true" && runtime.GOOS == "darwin") || runtime.GOOS == "windows" { - t.Skip("skip CI tests on darwin and windows") - } - - t.Setenv("NETBIRD_STORE_ENGINE", string(PostgresStoreEngine)) - store, cleanUp, err := NewTestStoreFromSQL(context.Background(), "../testdata/store.sql", t.TempDir()) - t.Cleanup(cleanUp) - assert.NoError(t, err) - - id := "9dj38s35-63fb-11ec-90d6-0242ac120003" - - user, err := store.GetUserByTokenID(context.Background(), id) - require.NoError(t, err) - require.Equal(t, id, user.PATs[id].ID) -} - func TestSqlite_GetTakenIPs(t *testing.T) { t.Setenv("NETBIRD_STORE_ENGINE", string(SqliteStoreEngine)) store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/extended-store.sql", t.TempDir()) @@ -1182,7 +1142,7 @@ func TestSqlite_CreateAndGetObjectInTransaction(t *testing.T) { assert.NoError(t, err) } -func TestSqlite_GetAccoundUsers(t *testing.T) { +func TestSqlStore_GetAccountUsers(t *testing.T) { store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/extended-store.sql", t.TempDir()) t.Cleanup(cleanup) if err != nil { @@ -2915,3 +2875,326 @@ func TestSqlStore_DatabaseBlocking(t *testing.T) { t.Logf("Test completed") } + +func TestSqlStore_GetAccountCreatedBy(t *testing.T) { + store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/store.sql", t.TempDir()) + t.Cleanup(cleanup) + require.NoError(t, err) + + tests := []struct { + name string + accountID string + expectError bool + createdBy string + }{ + { + name: "existing account ID", + accountID: "bf1c8084-ba50-4ce7-9439-34653001fc3b", + expectError: false, + createdBy: "edafee4e-63fb-11ec-90d6-0242ac120003", + }, + { + name: "non-existing account ID", + accountID: "nonexistent", + expectError: true, + }, + { + name: "empty account ID", + accountID: "", + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + createdBy, err := store.GetAccountCreatedBy(context.Background(), LockingStrengthShare, tt.accountID) + if tt.expectError { + require.Error(t, err) + sErr, ok := status.FromError(err) + require.True(t, ok) + require.Equal(t, sErr.Type(), status.NotFound) + require.Empty(t, createdBy) + } else { + require.NoError(t, err) + require.NotNil(t, createdBy) + require.Equal(t, tt.createdBy, createdBy) + } + }) + } + +} + +func TestSqlStore_GetUserByUserID(t *testing.T) { + store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/extended-store.sql", t.TempDir()) + t.Cleanup(cleanup) + require.NoError(t, err) + + tests := []struct { + name string + userID string + expectError bool + }{ + { + name: "retrieve existing user", + userID: "edafee4e-63fb-11ec-90d6-0242ac120003", + expectError: false, + }, + { + name: "retrieve non-existing user", + userID: "non-existing", + expectError: true, + }, + { + name: "retrieve with empty user ID", + userID: "", + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + user, err := store.GetUserByUserID(context.Background(), LockingStrengthShare, tt.userID) + if tt.expectError { + require.Error(t, err) + sErr, ok := status.FromError(err) + require.True(t, ok) + require.Equal(t, sErr.Type(), status.NotFound) + require.Nil(t, user) + } else { + require.NoError(t, err) + require.NotNil(t, user) + require.Equal(t, tt.userID, user.Id) + } + }) + } +} + +func TestSqlStore_GetUserByPATID(t *testing.T) { + store, cleanUp, err := NewTestStoreFromSQL(context.Background(), "../testdata/store.sql", t.TempDir()) + t.Cleanup(cleanUp) + assert.NoError(t, err) + + id := "9dj38s35-63fb-11ec-90d6-0242ac120003" + + user, err := store.GetUserByPATID(context.Background(), LockingStrengthShare, id) + require.NoError(t, err) + require.Equal(t, "f4f6d672-63fb-11ec-90d6-0242ac120003", user.Id) +} + +func TestSqlStore_SaveUser(t *testing.T) { + store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/extended-store.sql", t.TempDir()) + t.Cleanup(cleanup) + require.NoError(t, err) + + accountID := "bf1c8084-ba50-4ce7-9439-34653001fc3b" + + user := &types.User{ + Id: "user-id", + AccountID: accountID, + Role: types.UserRoleAdmin, + IsServiceUser: false, + AutoGroups: []string{"groupA", "groupB"}, + Blocked: false, + LastLogin: util.ToPtr(time.Now().UTC()), + CreatedAt: time.Now().UTC().Add(-time.Hour), + Issued: types.UserIssuedIntegration, + } + err = store.SaveUser(context.Background(), LockingStrengthUpdate, user) + require.NoError(t, err) + + saveUser, err := store.GetUserByUserID(context.Background(), LockingStrengthShare, user.Id) + require.NoError(t, err) + require.Equal(t, user.Id, saveUser.Id) + require.Equal(t, user.AccountID, saveUser.AccountID) + require.Equal(t, user.Role, saveUser.Role) + require.Equal(t, user.AutoGroups, saveUser.AutoGroups) + require.WithinDurationf(t, user.GetLastLogin(), saveUser.LastLogin.UTC(), time.Millisecond, "LastLogin should be equal") + require.WithinDurationf(t, user.CreatedAt, saveUser.CreatedAt.UTC(), time.Millisecond, "CreatedAt should be equal") + require.Equal(t, user.Issued, saveUser.Issued) + require.Equal(t, user.Blocked, saveUser.Blocked) + require.Equal(t, user.IsServiceUser, saveUser.IsServiceUser) +} + +func TestSqlStore_SaveUsers(t *testing.T) { + store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/extended-store.sql", t.TempDir()) + t.Cleanup(cleanup) + require.NoError(t, err) + + accountID := "bf1c8084-ba50-4ce7-9439-34653001fc3b" + + accountUsers, err := store.GetAccountUsers(context.Background(), LockingStrengthShare, accountID) + require.NoError(t, err) + require.Len(t, accountUsers, 2) + + users := []*types.User{ + { + Id: "user-1", + AccountID: accountID, + Issued: "api", + AutoGroups: []string{"groupA", "groupB"}, + }, + { + Id: "user-2", + AccountID: accountID, + Issued: "integration", + AutoGroups: []string{"groupA"}, + }, + } + err = store.SaveUsers(context.Background(), LockingStrengthUpdate, users) + require.NoError(t, err) + + accountUsers, err = store.GetAccountUsers(context.Background(), LockingStrengthShare, accountID) + require.NoError(t, err) + require.Len(t, accountUsers, 4) +} + +func TestSqlStore_DeleteUser(t *testing.T) { + store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/extended-store.sql", t.TempDir()) + t.Cleanup(cleanup) + require.NoError(t, err) + + accountID := "bf1c8084-ba50-4ce7-9439-34653001fc3b" + userID := "f4f6d672-63fb-11ec-90d6-0242ac120003" + + err = store.DeleteUser(context.Background(), LockingStrengthUpdate, accountID, userID) + require.NoError(t, err) + + user, err := store.GetUserByUserID(context.Background(), LockingStrengthShare, userID) + require.Error(t, err) + require.Nil(t, user) + + userPATs, err := store.GetUserPATs(context.Background(), LockingStrengthShare, userID) + require.NoError(t, err) + require.Len(t, userPATs, 0) +} + +func TestSqlStore_GetPATByID(t *testing.T) { + store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/extended-store.sql", t.TempDir()) + t.Cleanup(cleanup) + require.NoError(t, err) + + userID := "f4f6d672-63fb-11ec-90d6-0242ac120003" + + tests := []struct { + name string + patID string + expectError bool + }{ + { + name: "retrieve existing PAT", + patID: "9dj38s35-63fb-11ec-90d6-0242ac120003", + expectError: false, + }, + { + name: "retrieve non-existing PAT", + patID: "non-existing", + expectError: true, + }, + { + name: "retrieve with empty PAT ID", + patID: "", + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + pat, err := store.GetPATByID(context.Background(), LockingStrengthShare, userID, tt.patID) + if tt.expectError { + require.Error(t, err) + sErr, ok := status.FromError(err) + require.True(t, ok) + require.Equal(t, sErr.Type(), status.NotFound) + require.Nil(t, pat) + } else { + require.NoError(t, err) + require.NotNil(t, pat) + require.Equal(t, tt.patID, pat.ID) + } + }) + } +} + +func TestSqlStore_GetUserPATs(t *testing.T) { + store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/extended-store.sql", t.TempDir()) + t.Cleanup(cleanup) + require.NoError(t, err) + + userPATs, err := store.GetUserPATs(context.Background(), LockingStrengthShare, "f4f6d672-63fb-11ec-90d6-0242ac120003") + require.NoError(t, err) + require.Len(t, userPATs, 1) +} + +func TestSqlStore_GetPATByHashedToken(t *testing.T) { + store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/extended-store.sql", t.TempDir()) + t.Cleanup(cleanup) + require.NoError(t, err) + + pat, err := store.GetPATByHashedToken(context.Background(), LockingStrengthShare, "SoMeHaShEdToKeN") + require.NoError(t, err) + require.Equal(t, "9dj38s35-63fb-11ec-90d6-0242ac120003", pat.ID) +} + +func TestSqlStore_MarkPATUsed(t *testing.T) { + store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/extended-store.sql", t.TempDir()) + t.Cleanup(cleanup) + require.NoError(t, err) + + userID := "f4f6d672-63fb-11ec-90d6-0242ac120003" + patID := "9dj38s35-63fb-11ec-90d6-0242ac120003" + + err = store.MarkPATUsed(context.Background(), LockingStrengthUpdate, patID) + require.NoError(t, err) + + pat, err := store.GetPATByID(context.Background(), LockingStrengthShare, userID, patID) + require.NoError(t, err) + now := time.Now().UTC() + require.WithinRange(t, pat.LastUsed.UTC(), now.Add(-15*time.Second), now, "LastUsed should be within 1 second of now") +} + +func TestSqlStore_SavePAT(t *testing.T) { + store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/extended-store.sql", t.TempDir()) + t.Cleanup(cleanup) + require.NoError(t, err) + + userID := "edafee4e-63fb-11ec-90d6-0242ac120003" + + pat := &types.PersonalAccessToken{ + ID: "pat-id", + UserID: userID, + Name: "token", + HashedToken: "SoMeHaShEdToKeN", + ExpirationDate: util.ToPtr(time.Now().UTC().Add(12 * time.Hour)), + CreatedBy: userID, + CreatedAt: time.Now().UTC().Add(time.Hour), + LastUsed: util.ToPtr(time.Now().UTC().Add(-15 * time.Minute)), + } + err = store.SavePAT(context.Background(), LockingStrengthUpdate, pat) + require.NoError(t, err) + + savePAT, err := store.GetPATByID(context.Background(), LockingStrengthShare, userID, pat.ID) + require.NoError(t, err) + require.Equal(t, pat.ID, savePAT.ID) + require.Equal(t, pat.UserID, savePAT.UserID) + require.Equal(t, pat.HashedToken, savePAT.HashedToken) + require.Equal(t, pat.CreatedBy, savePAT.CreatedBy) + require.WithinDurationf(t, pat.GetExpirationDate(), savePAT.ExpirationDate.UTC(), time.Millisecond, "ExpirationDate should be equal") + require.WithinDurationf(t, pat.CreatedAt, savePAT.CreatedAt.UTC(), time.Millisecond, "CreatedAt should be equal") + require.WithinDurationf(t, pat.GetLastUsed(), savePAT.LastUsed.UTC(), time.Millisecond, "LastUsed should be equal") +} + +func TestSqlStore_DeletePAT(t *testing.T) { + store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/extended-store.sql", t.TempDir()) + t.Cleanup(cleanup) + require.NoError(t, err) + + userID := "f4f6d672-63fb-11ec-90d6-0242ac120003" + patID := "9dj38s35-63fb-11ec-90d6-0242ac120003" + + err = store.DeletePAT(context.Background(), LockingStrengthUpdate, userID, patID) + require.NoError(t, err) + + pat, err := store.GetPATByID(context.Background(), LockingStrengthShare, userID, patID) + require.Error(t, err) + require.Nil(t, pat) +} diff --git a/management/server/store/store.go b/management/server/store/store.go index 4b4dcfb4f..6d3a409e6 100644 --- a/management/server/store/store.go +++ b/management/server/store/store.go @@ -59,21 +59,30 @@ type Store interface { GetAccountIDByPrivateDomain(ctx context.Context, lockStrength LockingStrength, domain string) (string, error) GetAccountSettings(ctx context.Context, lockStrength LockingStrength, accountID string) (*types.Settings, error) GetAccountDNSSettings(ctx context.Context, lockStrength LockingStrength, accountID string) (*types.DNSSettings, error) + GetAccountCreatedBy(ctx context.Context, lockStrength LockingStrength, accountID string) (string, error) SaveAccount(ctx context.Context, account *types.Account) error DeleteAccount(ctx context.Context, account *types.Account) error UpdateAccountDomainAttributes(ctx context.Context, accountID string, domain string, category string, isPrimaryDomain bool) error SaveDNSSettings(ctx context.Context, lockStrength LockingStrength, accountID string, settings *types.DNSSettings) error - GetUserByTokenID(ctx context.Context, tokenID string) (*types.User, error) + GetUserByPATID(ctx context.Context, lockStrength LockingStrength, patID string) (*types.User, error) GetUserByUserID(ctx context.Context, lockStrength LockingStrength, userID string) (*types.User, error) GetAccountUsers(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*types.User, error) - SaveUsers(accountID string, users map[string]*types.User) error + SaveUsers(ctx context.Context, lockStrength LockingStrength, users []*types.User) error SaveUser(ctx context.Context, lockStrength LockingStrength, user *types.User) error SaveUserLastLogin(ctx context.Context, accountID, userID string, lastLogin time.Time) error + DeleteUser(ctx context.Context, lockStrength LockingStrength, accountID, userID string) error GetTokenIDByHashedToken(ctx context.Context, secret string) (string, error) DeleteHashedPAT2TokenIDIndex(hashedToken string) error DeleteTokenID2UserIDIndex(tokenID string) error + GetPATByID(ctx context.Context, lockStrength LockingStrength, userID, patID string) (*types.PersonalAccessToken, error) + GetUserPATs(ctx context.Context, lockStrength LockingStrength, userID string) ([]*types.PersonalAccessToken, error) + GetPATByHashedToken(ctx context.Context, lockStrength LockingStrength, hashedToken string) (*types.PersonalAccessToken, error) + MarkPATUsed(ctx context.Context, lockStrength LockingStrength, patID string) error + SavePAT(ctx context.Context, strength LockingStrength, pat *types.PersonalAccessToken) error + DeletePAT(ctx context.Context, strength LockingStrength, userID, patID string) error + GetAccountGroups(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*types.Group, error) GetResourceGroups(ctx context.Context, lockStrength LockingStrength, accountID, resourceID string) ([]*types.Group, error) GetGroupByID(ctx context.Context, lockStrength LockingStrength, accountID, groupID string) (*types.Group, error) diff --git a/management/server/testdata/store.sql b/management/server/testdata/store.sql index 1c0767bde..41b8fa2f7 100644 --- a/management/server/testdata/store.sql +++ b/management/server/testdata/store.sql @@ -37,7 +37,7 @@ CREATE INDEX `idx_network_resources_id` ON `network_resources`(`id`); CREATE INDEX `idx_networks_id` ON `networks`(`id`); CREATE INDEX `idx_networks_account_id` ON `networks`(`account_id`); -INSERT INTO accounts VALUES('bf1c8084-ba50-4ce7-9439-34653001fc3b','','2024-10-02 16:03:06.778746+02:00','test.com','private',1,'af1c8024-ha40-4ce2-9418-34653101fc3c','{"IP":"100.64.0.0","Mask":"//8AAA=="}','',0,'[]',0,86400000000000,0,0,0,'',NULL,NULL,NULL); +INSERT INTO accounts VALUES('bf1c8084-ba50-4ce7-9439-34653001fc3b','edafee4e-63fb-11ec-90d6-0242ac120003','2024-10-02 16:03:06.778746+02:00','test.com','private',1,'af1c8024-ha40-4ce2-9418-34653101fc3c','{"IP":"100.64.0.0","Mask":"//8AAA=="}','',0,'[]',0,86400000000000,0,0,0,'',NULL,NULL,NULL); INSERT INTO "groups" VALUES('cs1tnh0hhcjnqoiuebeg','bf1c8084-ba50-4ce7-9439-34653001fc3b','All','api','[]',0,''); INSERT INTO setup_keys VALUES('','bf1c8084-ba50-4ce7-9439-34653001fc3b','A2C8E62B-38F5-4553-B31E-DD66C696CEBB','Default key','reusable','2021-08-19 20:46:20.005936822+02:00','2321-09-18 20:46:20.005936822+02:00','2021-08-19 20:46:20.005936822+02:00',0,0,NULL,'["cs1tnh0hhcjnqoiuebeg"]',0,0); INSERT INTO users VALUES('a23efe53-63fb-11ec-90d6-0242ac120003','bf1c8084-ba50-4ce7-9439-34653001fc3b','owner',0,0,'','[]',0,NULL,'2024-10-02 16:03:06.779156+02:00','api',0,''); diff --git a/management/server/types/personal_access_token.go b/management/server/types/personal_access_token.go index ff157fcc6..0aa6b152b 100644 --- a/management/server/types/personal_access_token.go +++ b/management/server/types/personal_access_token.go @@ -75,7 +75,7 @@ type PersonalAccessTokenGenerated struct { // CreateNewPAT will generate a new PersonalAccessToken that can be assigned to a User. // Additionally, it will return the token in plain text once, to give to the user and only save a hashed version -func CreateNewPAT(name string, expirationInDays int, createdBy string) (*PersonalAccessTokenGenerated, error) { +func CreateNewPAT(name string, expirationInDays int, targetID, createdBy string) (*PersonalAccessTokenGenerated, error) { hashedToken, plainToken, err := generateNewToken() if err != nil { return nil, err @@ -84,6 +84,7 @@ func CreateNewPAT(name string, expirationInDays int, createdBy string) (*Persona return &PersonalAccessTokenGenerated{ PersonalAccessToken: PersonalAccessToken{ ID: xid.New().String(), + UserID: targetID, Name: name, HashedToken: hashedToken, ExpirationDate: util.ToPtr(currentTime.AddDate(0, 0, expirationInDays)), diff --git a/management/server/user.go b/management/server/user.go index 17770a423..6ba9b68d3 100644 --- a/management/server/user.go +++ b/management/server/user.go @@ -4,13 +4,10 @@ import ( "context" "errors" "fmt" - "slices" "strings" "time" "github.com/google/uuid" - log "github.com/sirupsen/logrus" - "github.com/netbirdio/netbird/management/server/activity" nbContext "github.com/netbirdio/netbird/management/server/context" "github.com/netbirdio/netbird/management/server/idp" @@ -20,6 +17,7 @@ import ( "github.com/netbirdio/netbird/management/server/store" "github.com/netbirdio/netbird/management/server/types" "github.com/netbirdio/netbird/management/server/util" + log "github.com/sirupsen/logrus" ) // createServiceUser creates a new service user under the given account. @@ -27,30 +25,29 @@ func (am *DefaultAccountManager) createServiceUser(ctx context.Context, accountI unlock := am.Store.AcquireWriteLockByUID(ctx, accountID) defer unlock() - account, err := am.Store.GetAccount(ctx, accountID) + initiatorUser, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, initiatorUserID) if err != nil { - return nil, status.Errorf(status.NotFound, "account %s doesn't exist", accountID) + return nil, err } - executingUser := account.Users[initiatorUserID] - if executingUser == nil { - return nil, status.Errorf(status.NotFound, "user not found") + if initiatorUser.AccountID != accountID { + return nil, status.NewUserNotPartOfAccountError() } - if !executingUser.HasAdminPower() { - return nil, status.Errorf(status.PermissionDenied, "only users with admin power can create service users") + + if !initiatorUser.HasAdminPower() { + return nil, status.NewAdminPermissionError() } if role == types.UserRoleOwner { - return nil, status.Errorf(status.InvalidArgument, "can't create a service user with owner role") + return nil, status.NewServiceUserRoleInvalidError() } newUserID := uuid.New().String() newUser := types.NewUser(newUserID, role, true, nonDeletable, serviceUserName, autoGroups, types.UserIssuedAPI) + newUser.AccountID = accountID log.WithContext(ctx).Debugf("New User: %v", newUser) - account.Users[newUserID] = newUser - err = am.Store.SaveAccount(ctx, account) - if err != nil { + if err = am.Store.SaveUser(ctx, store.LockingStrengthUpdate, newUser); err != nil { return nil, err } @@ -87,40 +84,67 @@ func (am *DefaultAccountManager) inviteNewUser(ctx context.Context, accountID, u return nil, status.Errorf(status.PreconditionFailed, "IdP manager must be enabled to send user invites") } - if invite == nil { - return nil, fmt.Errorf("provided user update is nil") + if err := validateUserInvite(invite); err != nil { + return nil, err } - invitedRole := types.StrRoleToUserRole(invite.Role) - - switch { - case invite.Name == "": - return nil, status.Errorf(status.InvalidArgument, "name can't be empty") - case invite.Email == "": - return nil, status.Errorf(status.InvalidArgument, "email can't be empty") - case invitedRole == types.UserRoleOwner: - return nil, status.Errorf(status.InvalidArgument, "can't invite a user with owner role") - default: - } - - account, err := am.Store.GetAccount(ctx, accountID) + initiatorUser, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, userID) if err != nil { - return nil, status.Errorf(status.NotFound, "account %s doesn't exist", accountID) + return nil, err } - initiatorUser, err := account.FindUser(userID) - if err != nil { - return nil, status.Errorf(status.NotFound, "initiator user with ID %s doesn't exist", userID) + if initiatorUser.AccountID != accountID { + return nil, status.NewUserNotPartOfAccountError() } inviterID := userID if initiatorUser.IsServiceUser { - inviterID = account.CreatedBy + createdBy, err := am.Store.GetAccountCreatedBy(ctx, store.LockingStrengthShare, accountID) + if err != nil { + return nil, err + } + inviterID = createdBy } + idpUser, err := am.createNewIdpUser(ctx, accountID, inviterID, invite) + if err != nil { + return nil, err + } + + newUser := &types.User{ + Id: idpUser.ID, + AccountID: accountID, + Role: types.StrRoleToUserRole(invite.Role), + AutoGroups: invite.AutoGroups, + Issued: invite.Issued, + IntegrationReference: invite.IntegrationReference, + CreatedAt: time.Now().UTC(), + } + + settings, err := am.Store.GetAccountSettings(ctx, store.LockingStrengthShare, accountID) + if err != nil { + return nil, err + } + + if err = am.Store.SaveUser(ctx, store.LockingStrengthUpdate, newUser); err != nil { + return nil, err + } + + _, err = am.refreshCache(ctx, accountID) + if err != nil { + return nil, err + } + + am.StoreEvent(ctx, userID, newUser.Id, accountID, activity.UserInvited, nil) + + return newUser.ToUserInfo(idpUser, settings) +} + +// createNewIdpUser validates the invite and creates a new user in the IdP +func (am *DefaultAccountManager) createNewIdpUser(ctx context.Context, accountID string, inviterID string, invite *types.UserInfo) (*idp.UserData, error) { // inviterUser is the one who is inviting the new user - inviterUser, err := am.lookupUserInCache(ctx, inviterID, account) - if err != nil || inviterUser == nil { + inviterUser, err := am.lookupUserInCache(ctx, inviterID, accountID) + if err != nil { return nil, status.Errorf(status.NotFound, "inviter user with ID %s doesn't exist in IdP", inviterID) } @@ -143,34 +167,7 @@ func (am *DefaultAccountManager) inviteNewUser(ctx context.Context, accountID, u return nil, status.Errorf(status.UserAlreadyExists, "can't invite a user with an existing NetBird account") } - idpUser, err := am.idpManager.CreateUser(ctx, invite.Email, invite.Name, accountID, inviterUser.Email) - if err != nil { - return nil, err - } - - newUser := &types.User{ - Id: idpUser.ID, - Role: invitedRole, - AutoGroups: invite.AutoGroups, - Issued: invite.Issued, - IntegrationReference: invite.IntegrationReference, - CreatedAt: time.Now().UTC(), - } - account.Users[idpUser.ID] = newUser - - err = am.Store.SaveAccount(ctx, account) - if err != nil { - return nil, err - } - - _, err = am.refreshCache(ctx, account.Id) - if err != nil { - return nil, err - } - - am.StoreEvent(ctx, userID, newUser.Id, accountID, activity.UserInvited, nil) - - return newUser.ToUserInfo(idpUser, account.Settings) + return am.idpManager.CreateUser(ctx, invite.Email, invite.Name, accountID, inviterUser.Email) } func (am *DefaultAccountManager) GetUserByID(ctx context.Context, id string) (*types.User, error) { @@ -210,60 +207,51 @@ func (am *DefaultAccountManager) GetUser(ctx context.Context, claims jwtclaims.A // ListUsers returns lists of all users under the account. // It doesn't populate user information such as email or name. func (am *DefaultAccountManager) ListUsers(ctx context.Context, accountID string) ([]*types.User, error) { - unlock := am.Store.AcquireWriteLockByUID(ctx, accountID) - defer unlock() - - account, err := am.Store.GetAccount(ctx, accountID) - if err != nil { - return nil, err - } - - users := make([]*types.User, 0, len(account.Users)) - for _, item := range account.Users { - users = append(users, item) - } - - return users, nil + return am.Store.GetAccountUsers(ctx, store.LockingStrengthShare, accountID) } -func (am *DefaultAccountManager) deleteServiceUser(ctx context.Context, account *types.Account, initiatorUserID string, targetUser *types.User) { +func (am *DefaultAccountManager) deleteServiceUser(ctx context.Context, accountID string, initiatorUserID string, targetUser *types.User) error { + if err := am.Store.DeleteUser(ctx, store.LockingStrengthUpdate, accountID, targetUser.Id); err != nil { + return err + } meta := map[string]any{"name": targetUser.ServiceUserName, "created_at": targetUser.CreatedAt} - am.StoreEvent(ctx, initiatorUserID, targetUser.Id, account.Id, activity.ServiceUserDeleted, meta) - delete(account.Users, targetUser.Id) + am.StoreEvent(ctx, initiatorUserID, targetUser.Id, accountID, activity.ServiceUserDeleted, meta) + return nil } // DeleteUser deletes a user from the given account. -func (am *DefaultAccountManager) DeleteUser(ctx context.Context, accountID, initiatorUserID string, targetUserID string) error { +func (am *DefaultAccountManager) DeleteUser(ctx context.Context, accountID, initiatorUserID, targetUserID string) error { if initiatorUserID == targetUserID { return status.Errorf(status.InvalidArgument, "self deletion is not allowed") } + unlock := am.Store.AcquireWriteLockByUID(ctx, accountID) defer unlock() - account, err := am.Store.GetAccount(ctx, accountID) + initiatorUser, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, initiatorUserID) if err != nil { return err } - executingUser := account.Users[initiatorUserID] - if executingUser == nil { - return status.Errorf(status.NotFound, "user not found") - } - if !executingUser.HasAdminPower() { - return status.Errorf(status.PermissionDenied, "only users with admin power can delete users") + if initiatorUser.AccountID != accountID { + return status.NewUserNotPartOfAccountError() } - targetUser := account.Users[targetUserID] - if targetUser == nil { - return status.Errorf(status.NotFound, "target user not found") + if !initiatorUser.HasAdminPower() { + return status.NewAdminPermissionError() + } + + targetUser, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, targetUserID) + if err != nil { + return err } if targetUser.Role == types.UserRoleOwner { - return status.Errorf(status.PermissionDenied, "unable to delete a user with owner role") + return status.NewOwnerDeletePermissionError() } // disable deleting integration user if the initiator is not admin service user - if targetUser.Issued == types.UserIssuedIntegration && !executingUser.IsServiceUser { + if targetUser.Issued == types.UserIssuedIntegration && !initiatorUser.IsServiceUser { return status.Errorf(status.PermissionDenied, "only integration service user can delete this user") } @@ -273,64 +261,26 @@ func (am *DefaultAccountManager) DeleteUser(ctx context.Context, accountID, init return status.Errorf(status.PermissionDenied, "service user is marked as non-deletable") } - am.deleteServiceUser(ctx, account, initiatorUserID, targetUser) - return am.Store.SaveAccount(ctx, account) + return am.deleteServiceUser(ctx, accountID, initiatorUserID, targetUser) } - return am.deleteRegularUser(ctx, account, initiatorUserID, targetUserID) -} - -func (am *DefaultAccountManager) deleteRegularUser(ctx context.Context, account *types.Account, initiatorUserID, targetUserID string) error { - meta, updateAccountPeers, err := am.prepareUserDeletion(ctx, account, initiatorUserID, targetUserID) + userInfo, err := am.getUserInfo(ctx, targetUser, accountID) if err != nil { return err } - delete(account.Users, targetUserID) - if updateAccountPeers { - account.Network.IncSerial() - } - - err = am.Store.SaveAccount(ctx, account) + updateAccountPeers, err := am.deleteRegularUser(ctx, accountID, initiatorUserID, userInfo) if err != nil { return err } - am.StoreEvent(ctx, initiatorUserID, targetUserID, account.Id, activity.UserDeleted, meta) if updateAccountPeers { - am.UpdateAccountPeers(ctx, account.Id) + am.UpdateAccountPeers(ctx, accountID) } return nil } -func (am *DefaultAccountManager) deleteUserPeers(ctx context.Context, initiatorUserID string, targetUserID string, account *types.Account) (bool, error) { - peers, err := account.FindUserPeers(targetUserID) - if err != nil { - return false, status.Errorf(status.Internal, "failed to find user peers") - } - - hadPeers := len(peers) > 0 - if !hadPeers { - return false, nil - } - - eventsToStore, err := deletePeers(ctx, am, am.Store, account.Id, initiatorUserID, peers) - if err != nil { - return false, err - } - - for _, storeEvent := range eventsToStore { - storeEvent() - } - - for _, peer := range peers { - account.DeletePeer(peer.ID) - } - - return hadPeers, nil -} - // InviteUser resend invitations to users who haven't activated their accounts prior to the expiration period. func (am *DefaultAccountManager) InviteUser(ctx context.Context, accountID string, initiatorUserID string, targetUserID string) error { unlock := am.Store.AcquireWriteLockByUID(ctx, accountID) @@ -340,13 +290,17 @@ func (am *DefaultAccountManager) InviteUser(ctx context.Context, accountID strin return status.Errorf(status.PreconditionFailed, "IdP manager must be enabled to send user invites") } - account, err := am.Store.GetAccount(ctx, accountID) + initiatorUser, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, initiatorUserID) if err != nil { - return status.Errorf(status.NotFound, "account %s doesn't exist", accountID) + return err + } + + if initiatorUser.AccountID != accountID { + return status.NewUserNotPartOfAccountError() } // check if the user is already registered with this ID - user, err := am.lookupUserInCache(ctx, targetUserID, account) + user, err := am.lookupUserInCache(ctx, targetUserID, accountID) if err != nil { return err } @@ -384,35 +338,31 @@ func (am *DefaultAccountManager) CreatePAT(ctx context.Context, accountID string return nil, status.Errorf(status.InvalidArgument, "expiration has to be between 1 and 365") } - account, err := am.Store.GetAccount(ctx, accountID) + initiatorUser, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, initiatorUserID) if err != nil { return nil, err } - targetUser, ok := account.Users[targetUserID] - if !ok { - return nil, status.Errorf(status.NotFound, "user not found") + if initiatorUser.AccountID != accountID { + return nil, status.NewUserNotPartOfAccountError() } - executingUser, ok := account.Users[initiatorUserID] - if !ok { - return nil, status.Errorf(status.NotFound, "user not found") + targetUser, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, targetUserID) + if err != nil { + return nil, err } - if !(initiatorUserID == targetUserID || (executingUser.HasAdminPower() && targetUser.IsServiceUser)) { - return nil, status.Errorf(status.PermissionDenied, "no permission to create PAT for this user") + if initiatorUserID != targetUserID && !(initiatorUser.HasAdminPower() && targetUser.IsServiceUser) { + return nil, status.NewAdminPermissionError() } - pat, err := types.CreateNewPAT(tokenName, expiresIn, executingUser.Id) + pat, err := types.CreateNewPAT(tokenName, expiresIn, targetUserID, initiatorUser.Id) if err != nil { return nil, status.Errorf(status.Internal, "failed to create PAT: %v", err) } - targetUser.PATs[pat.ID] = &pat.PersonalAccessToken - - err = am.Store.SaveAccount(ctx, account) - if err != nil { - return nil, status.Errorf(status.Internal, "failed to save account: %v", err) + if err = am.Store.SavePAT(ctx, store.LockingStrengthUpdate, &pat.PersonalAccessToken); err != nil { + return nil, err } meta := map[string]any{"name": pat.Name, "is_service_user": targetUser.IsServiceUser, "user_name": targetUser.ServiceUserName} @@ -426,48 +376,36 @@ func (am *DefaultAccountManager) DeletePAT(ctx context.Context, accountID string unlock := am.Store.AcquireWriteLockByUID(ctx, accountID) defer unlock() - account, err := am.Store.GetAccount(ctx, accountID) + initiatorUser, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, initiatorUserID) if err != nil { - return status.Errorf(status.NotFound, "account not found: %s", err) + return err } - targetUser, ok := account.Users[targetUserID] - if !ok { - return status.Errorf(status.NotFound, "user not found") + if initiatorUser.AccountID != accountID { + return status.NewUserNotPartOfAccountError() } - executingUser, ok := account.Users[initiatorUserID] - if !ok { - return status.Errorf(status.NotFound, "user not found") + if initiatorUserID != targetUserID && initiatorUser.IsRegularUser() { + return status.NewAdminPermissionError() } - if !(initiatorUserID == targetUserID || (executingUser.HasAdminPower() && targetUser.IsServiceUser)) { - return status.Errorf(status.PermissionDenied, "no permission to delete PAT for this user") - } - - pat := targetUser.PATs[tokenID] - if pat == nil { - return status.Errorf(status.NotFound, "PAT not found") - } - - err = am.Store.DeleteTokenID2UserIDIndex(pat.ID) + pat, err := am.Store.GetPATByID(ctx, store.LockingStrengthShare, targetUserID, tokenID) if err != nil { - return status.Errorf(status.Internal, "Failed to delete token id index: %s", err) + return err } - err = am.Store.DeleteHashedPAT2TokenIDIndex(pat.HashedToken) + + targetUser, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, targetUserID) if err != nil { - return status.Errorf(status.Internal, "Failed to delete hashed token index: %s", err) + return err + } + + if err = am.Store.DeletePAT(ctx, store.LockingStrengthUpdate, targetUserID, tokenID); err != nil { + return err } meta := map[string]any{"name": pat.Name, "is_service_user": targetUser.IsServiceUser, "user_name": targetUser.ServiceUserName} am.StoreEvent(ctx, initiatorUserID, targetUserID, accountID, activity.PersonalAccessTokenDeleted, meta) - delete(targetUser.PATs, tokenID) - - err = am.Store.SaveAccount(ctx, account) - if err != nil { - return status.Errorf(status.Internal, "Failed to save account: %s", err) - } return nil } @@ -478,22 +416,15 @@ func (am *DefaultAccountManager) GetPAT(ctx context.Context, accountID string, i return nil, err } - targetUser, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, targetUserID) - if err != nil { - return nil, err + if initiatorUser.AccountID != accountID { + return nil, status.NewUserNotPartOfAccountError() } - if (initiatorUserID != targetUserID && !initiatorUser.IsAdminOrServiceUser()) || initiatorUser.AccountID != accountID { - return nil, status.Errorf(status.PermissionDenied, "no permission to get PAT for this user") + if initiatorUserID != targetUserID && initiatorUser.IsRegularUser() { + return nil, status.NewAdminPermissionError() } - for _, pat := range targetUser.PATsG { - if pat.ID == tokenID { - return pat.Copy(), nil - } - } - - return nil, status.Errorf(status.NotFound, "PAT not found") + return am.Store.GetPATByID(ctx, store.LockingStrengthShare, targetUserID, tokenID) } // GetAllPATs returns all PATs for a user @@ -503,21 +434,15 @@ func (am *DefaultAccountManager) GetAllPATs(ctx context.Context, accountID strin return nil, err } - targetUser, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, targetUserID) - if err != nil { - return nil, err + if initiatorUser.AccountID != accountID { + return nil, status.NewUserNotPartOfAccountError() } - if (initiatorUserID != targetUserID && !initiatorUser.IsAdminOrServiceUser()) || initiatorUser.AccountID != accountID { - return nil, status.Errorf(status.PermissionDenied, "no permission to get PAT for this user") + if initiatorUserID != targetUserID && initiatorUser.IsRegularUser() { + return nil, status.NewAdminPermissionError() } - pats := make([]*types.PersonalAccessToken, 0, len(targetUser.PATsG)) - for _, pat := range targetUser.PATsG { - pats = append(pats, pat.Copy()) - } - - return pats, nil + return am.Store.GetUserPATs(ctx, store.LockingStrengthShare, targetUserID) } // SaveUser saves updates to the given user. If the user doesn't exist, it will throw status.NotFound error. @@ -528,10 +453,6 @@ func (am *DefaultAccountManager) SaveUser(ctx context.Context, accountID, initia // SaveOrAddUser updates the given user. If addIfNotExists is set to true it will add user when no exist // Only User.AutoGroups, User.Role, and User.Blocked fields are allowed to be updated for now. func (am *DefaultAccountManager) SaveOrAddUser(ctx context.Context, accountID, initiatorUserID string, update *types.User, addIfNotExists bool) (*types.UserInfo, error) { - if update == nil { - return nil, status.Errorf(status.InvalidArgument, "provided user update is nil") - } - unlock := am.Store.AcquireWriteLockByUID(ctx, accountID) defer unlock() @@ -555,125 +476,113 @@ func (am *DefaultAccountManager) SaveOrAddUsers(ctx context.Context, accountID, return nil, nil //nolint:nilnil } - account, err := am.Store.GetAccount(ctx, accountID) + initiatorUser, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, initiatorUserID) if err != nil { return nil, err } - initiatorUser, err := account.FindUser(initiatorUserID) - if err != nil { - return nil, err + if initiatorUser.AccountID != accountID { + return nil, status.NewUserNotPartOfAccountError() } if !initiatorUser.HasAdminPower() || initiatorUser.IsBlocked() { - return nil, status.Errorf(status.PermissionDenied, "only users with admin power are authorized to perform user update operations") + return nil, status.NewAdminPermissionError() } - updatedUsers := make([]*types.UserInfo, 0, len(updates)) - var ( - expiredPeers []*nbpeer.Peer - userIDs []string - eventsToStore []func() - ) + settings, err := am.Store.GetAccountSettings(ctx, store.LockingStrengthShare, accountID) + if err != nil { + return nil, err + } - for _, update := range updates { - if update == nil { - return nil, status.Errorf(status.InvalidArgument, "provided user update is nil") - } + var updateAccountPeers bool + var peersToExpire []*nbpeer.Peer + var addUserEvents []func() + var usersToSave = make([]*types.User, 0, len(updates)) - userIDs = append(userIDs, update.Id) + groups, err := am.Store.GetAccountGroups(ctx, store.LockingStrengthShare, accountID) + if err != nil { + return nil, fmt.Errorf("error getting account groups: %w", err) + } - oldUser := account.Users[update.Id] - if oldUser == nil { - if !addIfNotExists { - return nil, status.Errorf(status.NotFound, "user to update doesn't exist: %s", update.Id) + groupsMap := make(map[string]*types.Group, len(groups)) + for _, group := range groups { + groupsMap[group.ID] = group + } + + err = am.Store.ExecuteInTransaction(ctx, func(transaction store.Store) error { + for _, update := range updates { + if update == nil { + return status.Errorf(status.InvalidArgument, "provided user update is nil") } - // when addIfNotExists is set to true, the newUser will use all fields from the update input - oldUser = update - } - if err := validateUserUpdate(account, initiatorUser, oldUser, update); err != nil { - return nil, err - } - - // only auto groups, revoked status, and integration reference can be updated for now - newUser := oldUser.Copy() - newUser.Role = update.Role - newUser.Blocked = update.Blocked - newUser.AutoGroups = update.AutoGroups - // these two fields can't be set via API, only via direct call to the method - newUser.Issued = update.Issued - newUser.IntegrationReference = update.IntegrationReference - - transferredOwnerRole := handleOwnerRoleTransfer(account, initiatorUser, update) - account.Users[newUser.Id] = newUser - - if !oldUser.IsBlocked() && update.IsBlocked() { - // expire peers that belong to the user who's getting blocked - blockedPeers, err := account.FindUserPeers(update.Id) + userHadPeers, updatedUser, userPeersToExpire, userEvents, err := am.processUserUpdate( + ctx, transaction, groupsMap, initiatorUser, update, addIfNotExists, settings, + ) if err != nil { - return nil, err + return fmt.Errorf("failed to process user update: %w", err) + } + usersToSave = append(usersToSave, updatedUser) + addUserEvents = append(addUserEvents, userEvents...) + peersToExpire = append(peersToExpire, userPeersToExpire...) + + if userHadPeers { + updateAccountPeers = true } - expiredPeers = append(expiredPeers, blockedPeers...) } - - peerGroupsAdded := make(map[string][]string) - peerGroupsRemoved := make(map[string][]string) - if update.AutoGroups != nil && account.Settings.GroupsPropagationEnabled { - removedGroups := util.Difference(oldUser.AutoGroups, update.AutoGroups) - // need force update all auto groups in any case they will not be duplicated - peerGroupsAdded = account.UserGroupsAddToPeers(oldUser.Id, update.AutoGroups...) - peerGroupsRemoved = account.UserGroupsRemoveFromPeers(oldUser.Id, removedGroups...) - } - - userUpdateEvents := am.prepareUserUpdateEvents(ctx, initiatorUser.Id, oldUser, newUser, account, transferredOwnerRole) - eventsToStore = append(eventsToStore, userUpdateEvents...) - - userGroupsEvents := am.prepareUserGroupsEvents(ctx, initiatorUser.Id, oldUser, newUser, account, peerGroupsAdded, peerGroupsRemoved) - eventsToStore = append(eventsToStore, userGroupsEvents...) - - updatedUserInfo, err := getUserInfo(ctx, am, newUser, account) - if err != nil { - return nil, err - } - updatedUsers = append(updatedUsers, updatedUserInfo) + return transaction.SaveUsers(ctx, store.LockingStrengthUpdate, usersToSave) + }) + if err != nil { + return nil, err } - if len(expiredPeers) > 0 { - if err := am.expireAndUpdatePeers(ctx, account.Id, expiredPeers); err != nil { + var updatedUsersInfo = make([]*types.UserInfo, 0, len(updates)) + + userInfos, err := am.GetUsersFromAccount(ctx, accountID, initiatorUserID) + if err != nil { + return nil, err + } + + for _, updatedUser := range usersToSave { + updatedUserInfo, ok := userInfos[updatedUser.Id] + if !ok || updatedUserInfo == nil { + return nil, fmt.Errorf("failed to get user: %s updated user info", updatedUser.Id) + } + updatedUsersInfo = append(updatedUsersInfo, updatedUserInfo) + } + + for _, addUserEvent := range addUserEvents { + addUserEvent() + } + + if len(peersToExpire) > 0 { + if err := am.expireAndUpdatePeers(ctx, accountID, peersToExpire); err != nil { log.WithContext(ctx).Errorf("failed update expired peers: %s", err) return nil, err } } - account.Network.IncSerial() - if err = am.Store.SaveAccount(ctx, account); err != nil { - return nil, err + if settings.GroupsPropagationEnabled && updateAccountPeers { + if err = am.Store.IncrementNetworkSerial(ctx, store.LockingStrengthUpdate, accountID); err != nil { + return nil, fmt.Errorf("failed to increment network serial: %w", err) + } + am.UpdateAccountPeers(ctx, accountID) } - if account.Settings.GroupsPropagationEnabled && areUsersLinkedToPeers(account, userIDs) { - am.UpdateAccountPeers(ctx, account.Id) - } - - for _, storeEvent := range eventsToStore { - storeEvent() - } - - return updatedUsers, nil + return updatedUsersInfo, nil } // prepareUserUpdateEvents prepares a list user update events based on the changes between the old and new user data. -func (am *DefaultAccountManager) prepareUserUpdateEvents(ctx context.Context, initiatorUserID string, oldUser, newUser *types.User, account *types.Account, transferredOwnerRole bool) []func() { +func (am *DefaultAccountManager) prepareUserUpdateEvents(ctx context.Context, accountID string, initiatorUserID string, oldUser, newUser *types.User, transferredOwnerRole bool) []func() { var eventsToStore []func() if oldUser.IsBlocked() != newUser.IsBlocked() { if newUser.IsBlocked() { eventsToStore = append(eventsToStore, func() { - am.StoreEvent(ctx, initiatorUserID, oldUser.Id, account.Id, activity.UserBlocked, nil) + am.StoreEvent(ctx, initiatorUserID, oldUser.Id, accountID, activity.UserBlocked, nil) }) } else { eventsToStore = append(eventsToStore, func() { - am.StoreEvent(ctx, initiatorUserID, oldUser.Id, account.Id, activity.UserUnblocked, nil) + am.StoreEvent(ctx, initiatorUserID, oldUser.Id, accountID, activity.UserUnblocked, nil) }) } } @@ -681,115 +590,126 @@ func (am *DefaultAccountManager) prepareUserUpdateEvents(ctx context.Context, in switch { case transferredOwnerRole: eventsToStore = append(eventsToStore, func() { - am.StoreEvent(ctx, initiatorUserID, oldUser.Id, account.Id, activity.TransferredOwnerRole, nil) + am.StoreEvent(ctx, initiatorUserID, oldUser.Id, accountID, activity.TransferredOwnerRole, nil) }) case oldUser.Role != newUser.Role: eventsToStore = append(eventsToStore, func() { - am.StoreEvent(ctx, initiatorUserID, oldUser.Id, account.Id, activity.UserRoleUpdated, map[string]any{"role": newUser.Role}) + am.StoreEvent(ctx, initiatorUserID, oldUser.Id, accountID, activity.UserRoleUpdated, map[string]any{"role": newUser.Role}) }) } return eventsToStore } -func (am *DefaultAccountManager) prepareUserGroupsEvents(ctx context.Context, initiatorUserID string, oldUser, newUser *types.User, account *types.Account, peerGroupsAdded, peerGroupsRemoved map[string][]string) []func() { - var eventsToStore []func() - if newUser.AutoGroups != nil { - removedGroups := util.Difference(oldUser.AutoGroups, newUser.AutoGroups) - addedGroups := util.Difference(newUser.AutoGroups, oldUser.AutoGroups) +func (am *DefaultAccountManager) processUserUpdate(ctx context.Context, transaction store.Store, groupsMap map[string]*types.Group, + initiatorUser, update *types.User, addIfNotExists bool, settings *types.Settings) (bool, *types.User, []*nbpeer.Peer, []func(), error) { - removedEvents := am.handleGroupRemovedFromUser(ctx, initiatorUserID, oldUser, newUser, account, removedGroups, peerGroupsRemoved) - eventsToStore = append(eventsToStore, removedEvents...) - - addedEvents := am.handleGroupAddedToUser(ctx, initiatorUserID, oldUser, newUser, account, addedGroups, peerGroupsAdded) - eventsToStore = append(eventsToStore, addedEvents...) + if update == nil { + return false, nil, nil, nil, status.Errorf(status.InvalidArgument, "provided user update is nil") } - return eventsToStore + + oldUser, err := getUserOrCreateIfNotExists(ctx, transaction, update, addIfNotExists) + if err != nil { + return false, nil, nil, nil, err + } + + if err := validateUserUpdate(groupsMap, initiatorUser, oldUser, update); err != nil { + return false, nil, nil, nil, err + } + + // only auto groups, revoked status, and integration reference can be updated for now + updatedUser := oldUser.Copy() + updatedUser.AccountID = initiatorUser.AccountID + updatedUser.Role = update.Role + updatedUser.Blocked = update.Blocked + updatedUser.AutoGroups = update.AutoGroups + // these two fields can't be set via API, only via direct call to the method + updatedUser.Issued = update.Issued + updatedUser.IntegrationReference = update.IntegrationReference + + transferredOwnerRole, err := handleOwnerRoleTransfer(ctx, transaction, initiatorUser, update) + if err != nil { + return false, nil, nil, nil, err + } + + userPeers, err := transaction.GetUserPeers(ctx, store.LockingStrengthUpdate, updatedUser.AccountID, update.Id) + if err != nil { + return false, nil, nil, nil, err + } + + var peersToExpire []*nbpeer.Peer + + if !oldUser.IsBlocked() && update.IsBlocked() { + peersToExpire = userPeers + } + + if update.AutoGroups != nil && settings.GroupsPropagationEnabled { + removedGroups := util.Difference(oldUser.AutoGroups, update.AutoGroups) + updatedGroups, err := updateUserPeersInGroups(groupsMap, userPeers, update.AutoGroups, removedGroups) + if err != nil { + return false, nil, nil, nil, fmt.Errorf("error modifying user peers in groups: %w", err) + } + + if err = transaction.SaveGroups(ctx, store.LockingStrengthUpdate, updatedGroups); err != nil { + return false, nil, nil, nil, fmt.Errorf("error saving groups: %w", err) + } + } + + updateAccountPeers := len(userPeers) > 0 + userEventsToAdd := am.prepareUserUpdateEvents(ctx, updatedUser.AccountID, initiatorUser.Id, oldUser, updatedUser, transferredOwnerRole) + + return updateAccountPeers, updatedUser, peersToExpire, userEventsToAdd, nil } -func (am *DefaultAccountManager) handleGroupAddedToUser(ctx context.Context, initiatorUserID string, oldUser, newUser *types.User, account *types.Account, addedGroups []string, peerGroupsAdded map[string][]string) []func() { - var eventsToStore []func() - for _, g := range addedGroups { - group := account.GetGroup(g) - if group != nil { - eventsToStore = append(eventsToStore, func() { - am.StoreEvent(ctx, initiatorUserID, oldUser.Id, account.Id, activity.GroupAddedToUser, - map[string]any{"group": group.Name, "group_id": group.ID, "is_service_user": newUser.IsServiceUser, "user_name": newUser.ServiceUserName}) - }) +// getUserOrCreateIfNotExists retrieves the existing user or creates a new one if it doesn't exist. +func getUserOrCreateIfNotExists(ctx context.Context, transaction store.Store, update *types.User, addIfNotExists bool) (*types.User, error) { + existingUser, err := transaction.GetUserByUserID(ctx, store.LockingStrengthShare, update.Id) + if err != nil { + if sErr, ok := status.FromError(err); ok && sErr.Type() == status.NotFound { + if !addIfNotExists { + return nil, status.Errorf(status.NotFound, "user to update doesn't exist: %s", update.Id) + } + return update, nil // use all fields from update if addIfNotExists is true } + return nil, err } - for groupID, peerIDs := range peerGroupsAdded { - group := account.GetGroup(groupID) - for _, peerID := range peerIDs { - peer := account.GetPeer(peerID) - eventsToStore = append(eventsToStore, func() { - meta := map[string]any{ - "group": group.Name, "group_id": group.ID, - "peer_ip": peer.IP.String(), "peer_fqdn": peer.FQDN(am.GetDNSDomain()), - } - am.StoreEvent(ctx, activity.SystemInitiator, peer.ID, account.Id, activity.GroupAddedToPeer, meta) - }) - } - } - return eventsToStore + return existingUser, nil } -func (am *DefaultAccountManager) handleGroupRemovedFromUser(ctx context.Context, initiatorUserID string, oldUser, newUser *types.User, account *types.Account, removedGroups []string, peerGroupsRemoved map[string][]string) []func() { - var eventsToStore []func() - for _, g := range removedGroups { - group := account.GetGroup(g) - if group != nil { - eventsToStore = append(eventsToStore, func() { - am.StoreEvent(ctx, initiatorUserID, oldUser.Id, account.Id, activity.GroupRemovedFromUser, - map[string]any{"group": group.Name, "group_id": group.ID, "is_service_user": newUser.IsServiceUser, "user_name": newUser.ServiceUserName}) - }) - - } else { - log.WithContext(ctx).Errorf("group %s not found while saving user activity event of account %s", g, account.Id) - } - } - for groupID, peerIDs := range peerGroupsRemoved { - group := account.GetGroup(groupID) - for _, peerID := range peerIDs { - peer := account.GetPeer(peerID) - eventsToStore = append(eventsToStore, func() { - meta := map[string]any{ - "group": group.Name, "group_id": group.ID, - "peer_ip": peer.IP.String(), "peer_fqdn": peer.FQDN(am.GetDNSDomain()), - } - am.StoreEvent(ctx, activity.SystemInitiator, peer.ID, account.Id, activity.GroupRemovedFromPeer, meta) - }) - } - } - return eventsToStore -} - -func handleOwnerRoleTransfer(account *types.Account, initiatorUser, update *types.User) bool { +func handleOwnerRoleTransfer(ctx context.Context, transaction store.Store, initiatorUser, update *types.User) (bool, error) { if initiatorUser.Role == types.UserRoleOwner && initiatorUser.Id != update.Id && update.Role == types.UserRoleOwner { newInitiatorUser := initiatorUser.Copy() newInitiatorUser.Role = types.UserRoleAdmin - account.Users[initiatorUser.Id] = newInitiatorUser - return true + + if err := transaction.SaveUser(ctx, store.LockingStrengthUpdate, newInitiatorUser); err != nil { + return false, err + } + return true, nil } - return false + return false, nil } // getUserInfo retrieves the UserInfo for a given User and Account. // If the AccountManager has a non-nil idpManager and the User is not a service user, // it will attempt to look up the UserData from the cache. -func getUserInfo(ctx context.Context, am *DefaultAccountManager, user *types.User, account *types.Account) (*types.UserInfo, error) { +func (am *DefaultAccountManager) getUserInfo(ctx context.Context, user *types.User, accountID string) (*types.UserInfo, error) { + settings, err := am.Store.GetAccountSettings(ctx, store.LockingStrengthShare, accountID) + if err != nil { + return nil, err + } + if !isNil(am.idpManager) && !user.IsServiceUser { - userData, err := am.lookupUserInCache(ctx, user.Id, account) + userData, err := am.lookupUserInCache(ctx, user.Id, accountID) if err != nil { return nil, err } - return user.ToUserInfo(userData, account.Settings) + return user.ToUserInfo(userData, settings) } - return user.ToUserInfo(nil, account.Settings) + return user.ToUserInfo(nil, settings) } // validateUserUpdate validates the update operation for a user. -func validateUserUpdate(account *types.Account, initiatorUser, oldUser, update *types.User) error { +func validateUserUpdate(groupsMap map[string]*types.Group, initiatorUser, oldUser, update *types.User) error { if initiatorUser.HasAdminPower() && initiatorUser.Id == update.Id && oldUser.Blocked != update.Blocked { return status.Errorf(status.PermissionDenied, "admins can't block or unblock themselves") } @@ -810,12 +730,12 @@ func validateUserUpdate(account *types.Account, initiatorUser, oldUser, update * } for _, newGroupID := range update.AutoGroups { - group, ok := account.Groups[newGroupID] + group, ok := groupsMap[newGroupID] if !ok { return status.Errorf(status.InvalidArgument, "provided group ID %s in the user %s update doesn't exist", newGroupID, update.Id) } - if group.Name == "All" { + if group.IsGroupAll() { return status.Errorf(status.InvalidArgument, "can't add All group to the user") } } @@ -864,22 +784,38 @@ func (am *DefaultAccountManager) GetOrCreateAccountByUser(ctx context.Context, u // GetUsersFromAccount performs a batched request for users from IDP by account ID apply filter on what data to return // based on provided user role. -func (am *DefaultAccountManager) GetUsersFromAccount(ctx context.Context, accountID, userID string) ([]*types.UserInfo, error) { - account, err := am.Store.GetAccount(ctx, accountID) +func (am *DefaultAccountManager) GetUsersFromAccount(ctx context.Context, accountID, initiatorUserID string) (map[string]*types.UserInfo, error) { + accountUsers, err := am.Store.GetAccountUsers(ctx, store.LockingStrengthShare, accountID) if err != nil { return nil, err } - user, err := account.FindUser(userID) + initiatorUser, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, initiatorUserID) + if err != nil { + return nil, err + } + + if initiatorUser.AccountID != accountID { + return nil, status.NewUserNotPartOfAccountError() + } + + return am.BuildUserInfosForAccount(ctx, accountID, initiatorUserID, accountUsers) +} + +// BuildUserInfosForAccount builds user info for the given account. +func (am *DefaultAccountManager) BuildUserInfosForAccount(ctx context.Context, accountID, initiatorUserID string, accountUsers []*types.User) (map[string]*types.UserInfo, error) { + var queriedUsers []*idp.UserData + var err error + + initiatorUser, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, initiatorUserID) if err != nil { return nil, err } - queriedUsers := make([]*idp.UserData, 0) if !isNil(am.idpManager) { - users := make(map[string]userLoggedInOnce, len(account.Users)) + users := make(map[string]userLoggedInOnce, len(accountUsers)) usersFromIntegration := make([]*idp.UserData, 0) - for _, user := range account.Users { + for _, user := range accountUsers { if user.Issued == types.UserIssuedIntegration { key := user.IntegrationReference.CacheKey(accountID, user.Id) info, err := am.externalCacheManager.Get(am.ctx, key) @@ -904,33 +840,40 @@ func (am *DefaultAccountManager) GetUsersFromAccount(ctx context.Context, accoun queriedUsers = append(queriedUsers, usersFromIntegration...) } - userInfos := make([]*types.UserInfo, 0) + settings, err := am.Store.GetAccountSettings(ctx, store.LockingStrengthShare, accountID) + if err != nil { + return nil, err + } + + userInfosMap := make(map[string]*types.UserInfo) // in case of self-hosted, or IDP doesn't return anything, we will return the locally stored userInfo if len(queriedUsers) == 0 { - for _, accountUser := range account.Users { - if !(user.HasAdminPower() || user.IsServiceUser || user.Id == accountUser.Id) { + for _, accountUser := range accountUsers { + if initiatorUser.IsRegularUser() && initiatorUser.Id != accountUser.Id { // if user is not an admin then show only current user and do not show other users continue } - info, err := accountUser.ToUserInfo(nil, account.Settings) + + info, err := accountUser.ToUserInfo(nil, settings) if err != nil { return nil, err } - userInfos = append(userInfos, info) + userInfosMap[accountUser.Id] = info } - return userInfos, nil + + return userInfosMap, nil } - for _, localUser := range account.Users { - if !(user.HasAdminPower() || user.IsServiceUser) && user.Id != localUser.Id { + for _, localUser := range accountUsers { + if initiatorUser.IsRegularUser() && initiatorUser.Id != localUser.Id { // if user is not an admin then show only current user and do not show other users continue } var info *types.UserInfo if queriedUser, contains := findUserInIDPUserdata(localUser.Id, queriedUsers); contains { - info, err = localUser.ToUserInfo(queriedUser, account.Settings) + info, err = localUser.ToUserInfo(queriedUser, settings) if err != nil { return nil, err } @@ -943,7 +886,7 @@ func (am *DefaultAccountManager) GetUsersFromAccount(ctx context.Context, accoun dashboardViewPermissions := "full" if !localUser.HasAdminPower() { dashboardViewPermissions = "limited" - if account.Settings.RegularUsersViewBlocked { + if settings.RegularUsersViewBlocked { dashboardViewPermissions = "blocked" } } @@ -960,10 +903,10 @@ func (am *DefaultAccountManager) GetUsersFromAccount(ctx context.Context, accoun Permissions: types.UserPermissions{DashboardView: dashboardViewPermissions}, } } - userInfos = append(userInfos, info) + userInfosMap[info.ID] = info } - return userInfos, nil + return userInfosMap, nil } // expireAndUpdatePeers expires all peers of the given user and updates them in the account @@ -1017,55 +960,34 @@ func (am *DefaultAccountManager) deleteUserFromIDP(ctx context.Context, targetUs return nil } -func (am *DefaultAccountManager) getEmailAndNameOfTargetUser(ctx context.Context, accountId, initiatorId, targetId string) (string, string, error) { - userInfos, err := am.GetUsersFromAccount(ctx, accountId, initiatorId) - if err != nil { - return "", "", err - } - for _, ui := range userInfos { - if ui.ID == targetId { - return ui.Email, ui.Name, nil - } - } - - return "", "", fmt.Errorf("user info not found for user: %s", targetId) -} - // DeleteRegularUsers deletes regular users from an account. // Note: This function does not acquire the global lock. // It is the caller's responsibility to ensure proper locking is in place before invoking this method. // // If an error occurs while deleting the user, the function skips it and continues deleting other users. // Errors are collected and returned at the end. -func (am *DefaultAccountManager) DeleteRegularUsers(ctx context.Context, accountID, initiatorUserID string, targetUserIDs []string) error { - account, err := am.Store.GetAccount(ctx, accountID) +func (am *DefaultAccountManager) DeleteRegularUsers(ctx context.Context, accountID, initiatorUserID string, targetUserIDs []string, userInfos map[string]*types.UserInfo) error { + initiatorUser, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, initiatorUserID) if err != nil { return err } - executingUser := account.Users[initiatorUserID] - if executingUser == nil { - return status.Errorf(status.NotFound, "user not found") - } - if !executingUser.HasAdminPower() { - return status.Errorf(status.PermissionDenied, "only users with admin power can delete users") + if !initiatorUser.HasAdminPower() { + return status.NewAdminPermissionError() } - var ( - allErrors error - updateAccountPeers bool - ) + var allErrors error + var updateAccountPeers bool - deletedUsersMeta := make(map[string]map[string]any) for _, targetUserID := range targetUserIDs { if initiatorUserID == targetUserID { allErrors = errors.Join(allErrors, errors.New("self deletion is not allowed")) continue } - targetUser := account.Users[targetUserID] - if targetUser == nil { - allErrors = errors.Join(allErrors, fmt.Errorf("target user: %s not found", targetUserID)) + targetUser, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, targetUserID) + if err != nil { + allErrors = errors.Join(allErrors, err) continue } @@ -1075,88 +997,97 @@ func (am *DefaultAccountManager) DeleteRegularUsers(ctx context.Context, account } // disable deleting integration user if the initiator is not admin service user - if targetUser.Issued == types.UserIssuedIntegration && !executingUser.IsServiceUser { + if targetUser.Issued == types.UserIssuedIntegration && !initiatorUser.IsServiceUser { allErrors = errors.Join(allErrors, errors.New("only integration service user can delete this user")) continue } - meta, hadPeers, err := am.prepareUserDeletion(ctx, account, initiatorUserID, targetUserID) - if err != nil { - allErrors = errors.Join(allErrors, fmt.Errorf("failed to delete user %s: %s", targetUserID, err)) + userInfo, ok := userInfos[targetUserID] + if !ok || userInfo == nil { + allErrors = errors.Join(allErrors, fmt.Errorf("user info not found for user: %s", targetUserID)) continue } - if hadPeers { - updateAccountPeers = true + userHadPeers, err := am.deleteRegularUser(ctx, accountID, initiatorUserID, userInfo) + if err != nil { + allErrors = errors.Join(allErrors, err) + continue } - delete(account.Users, targetUserID) - deletedUsersMeta[targetUserID] = meta - } - - if updateAccountPeers { - account.Network.IncSerial() - } - err = am.Store.SaveAccount(ctx, account) - if err != nil { - return fmt.Errorf("failed to delete users: %w", err) + if userHadPeers { + updateAccountPeers = true + } } if updateAccountPeers { am.UpdateAccountPeers(ctx, accountID) } - for targetUserID, meta := range deletedUsersMeta { - am.StoreEvent(ctx, initiatorUserID, targetUserID, account.Id, activity.UserDeleted, meta) - } - return allErrors } -func (am *DefaultAccountManager) prepareUserDeletion(ctx context.Context, account *types.Account, initiatorUserID, targetUserID string) (map[string]any, bool, error) { - tuEmail, tuName, err := am.getEmailAndNameOfTargetUser(ctx, account.Id, initiatorUserID, targetUserID) - if err != nil { - log.WithContext(ctx).Errorf("failed to resolve email address: %s", err) - return nil, false, err - } - +// deleteRegularUser deletes a specified user and their related peers from the account. +func (am *DefaultAccountManager) deleteRegularUser(ctx context.Context, accountID, initiatorUserID string, targetUserInfo *types.UserInfo) (bool, error) { if !isNil(am.idpManager) { // Delete if the user already exists in the IdP. Necessary in cases where a user account // was created where a user account was provisioned but the user did not sign in - _, err = am.idpManager.GetUserDataByID(ctx, targetUserID, idp.AppMetadata{WTAccountID: account.Id}) + _, err := am.idpManager.GetUserDataByID(ctx, targetUserInfo.ID, idp.AppMetadata{WTAccountID: accountID}) if err == nil { - err = am.deleteUserFromIDP(ctx, targetUserID, account.Id) + err = am.deleteUserFromIDP(ctx, targetUserInfo.ID, accountID) if err != nil { - log.WithContext(ctx).Debugf("failed to delete user from IDP: %s", targetUserID) - return nil, false, err + log.WithContext(ctx).Debugf("failed to delete user from IDP: %s", targetUserInfo.ID) + return false, err } } else { - log.WithContext(ctx).Debugf("skipped deleting user %s from IDP, error: %v", targetUserID, err) + log.WithContext(ctx).Debugf("skipped deleting user %s from IDP, error: %v", targetUserInfo.ID, err) } } - hadPeers, err := am.deleteUserPeers(ctx, initiatorUserID, targetUserID, account) + var addPeerRemovedEvents []func() + var updateAccountPeers bool + var targetUser *types.User + var err error + + err = am.Store.ExecuteInTransaction(ctx, func(transaction store.Store) error { + targetUser, err = transaction.GetUserByUserID(ctx, store.LockingStrengthShare, targetUserInfo.ID) + if err != nil { + return fmt.Errorf("failed to get user to delete: %w", err) + } + + userPeers, err := transaction.GetUserPeers(ctx, store.LockingStrengthShare, accountID, targetUserInfo.ID) + if err != nil { + return fmt.Errorf("failed to get user peers: %w", err) + } + + if len(userPeers) > 0 { + updateAccountPeers = true + addPeerRemovedEvents, err = deletePeers(ctx, am, transaction, accountID, targetUserInfo.ID, userPeers) + if err != nil { + return fmt.Errorf("failed to delete user peers: %w", err) + } + } + + if err = transaction.DeleteUser(ctx, store.LockingStrengthUpdate, accountID, targetUserInfo.ID); err != nil { + return fmt.Errorf("failed to delete user: %s %w", targetUserInfo.ID, err) + } + + return nil + }) if err != nil { - return nil, false, err + return false, err } - u, err := account.FindUser(targetUserID) - if err != nil { - log.WithContext(ctx).Errorf("failed to find user %s for deletion, this should never happen: %s", targetUserID, err) + for _, addPeerRemovedEvent := range addPeerRemovedEvents { + addPeerRemovedEvent() } + meta := map[string]any{"name": targetUserInfo.Name, "email": targetUserInfo.Email, "created_at": targetUser.CreatedAt} + am.StoreEvent(ctx, initiatorUserID, targetUser.Id, accountID, activity.UserDeleted, meta) - var tuCreatedAt time.Time - if u != nil { - tuCreatedAt = u.CreatedAt - } - - return map[string]any{"name": tuName, "email": tuEmail, "created_at": tuCreatedAt}, hadPeers, nil + return updateAccountPeers, nil } // updateUserPeersInGroups updates the user's peers in the specified groups by adding or removing them. -func (am *DefaultAccountManager) updateUserPeersInGroups(accountGroups map[string]*types.Group, peers []*nbpeer.Peer, groupsToAdd, - groupsToRemove []string) (groupsToUpdate []*types.Group, err error) { - +func updateUserPeersInGroups(accountGroups map[string]*types.Group, peers []*nbpeer.Peer, groupsToAdd, groupsToRemove []string) (groupsToUpdate []*types.Group, err error) { if len(groupsToAdd) == 0 && len(groupsToRemove) == 0 { return } @@ -1230,12 +1161,22 @@ func findUserInIDPUserdata(userID string, userData []*idp.UserData) (*idp.UserDa return nil, false } -// areUsersLinkedToPeers checks if any of the given userIDs are linked to any of the peers in the account. -func areUsersLinkedToPeers(account *types.Account, userIDs []string) bool { - for _, peer := range account.Peers { - if slices.Contains(userIDs, peer.UserID) { - return true - } +func validateUserInvite(invite *types.UserInfo) error { + if invite == nil { + return fmt.Errorf("provided user update is nil") } - return false + + invitedRole := types.StrRoleToUserRole(invite.Role) + + switch { + case invite.Name == "": + return status.Errorf(status.InvalidArgument, "name can't be empty") + case invite.Email == "": + return status.Errorf(status.InvalidArgument, "email can't be empty") + case invitedRole == types.UserRoleOwner: + return status.Errorf(status.InvalidArgument, "can't invite a user with owner role") + default: + } + + return nil } diff --git a/management/server/user_test.go b/management/server/user_test.go index a028d164b..4a532c8a6 100644 --- a/management/server/user_test.go +++ b/management/server/user_test.go @@ -11,6 +11,7 @@ import ( cacheStore "github.com/eko/gocache/v3/store" "github.com/google/go-cmp/cmp" "github.com/netbirdio/netbird/management/server/util" + "golang.org/x/exp/maps" nbpeer "github.com/netbirdio/netbird/management/server/peer" "github.com/netbirdio/netbird/management/server/store" @@ -45,7 +46,7 @@ const ( ) func TestUser_CreatePAT_ForSameUser(t *testing.T) { - store, cleanup, err := store.NewTestStoreFromSQL(context.Background(), "", t.TempDir()) + s, cleanup, err := store.NewTestStoreFromSQL(context.Background(), "", t.TempDir()) if err != nil { t.Fatalf("Error when creating store: %s", err) } @@ -53,13 +54,13 @@ func TestUser_CreatePAT_ForSameUser(t *testing.T) { account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "") - err = store.SaveAccount(context.Background(), account) + err = s.SaveAccount(context.Background(), account) if err != nil { t.Fatalf("Error when saving account: %s", err) } am := DefaultAccountManager{ - Store: store, + Store: s, eventStore: &activity.InMemoryEventStore{}, } @@ -81,7 +82,7 @@ func TestUser_CreatePAT_ForSameUser(t *testing.T) { assert.Equal(t, pat.ID, tokenID) - user, err := am.Store.GetUserByTokenID(context.Background(), tokenID) + user, err := am.Store.GetUserByPATID(context.Background(), store.LockingStrengthShare, tokenID) if err != nil { t.Fatalf("Error when getting user by token ID: %s", err) } @@ -855,7 +856,7 @@ func TestUser_DeleteUser_RegularUsers(t *testing.T) { { name: "Delete non-existent user", userIDs: []string{"non-existent-user"}, - expectedReasons: []string{"target user: non-existent-user not found"}, + expectedReasons: []string{"user: non-existent-user not found"}, expectedNotDeleted: []string{}, }, { @@ -867,7 +868,10 @@ func TestUser_DeleteUser_RegularUsers(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - err = am.DeleteRegularUsers(context.Background(), mockAccountID, mockUserID, tc.userIDs) + userInfos, err := am.BuildUserInfosForAccount(context.Background(), mockAccountID, mockUserID, maps.Values(account.Users)) + assert.NoError(t, err) + + err = am.DeleteRegularUsers(context.Background(), mockAccountID, mockUserID, tc.userIDs, userInfos) if len(tc.expectedReasons) > 0 { assert.Error(t, err) var foundExpectedErrors int From 8fb5a9ce1145cd53303de5bf1e33041a5d7de0ba Mon Sep 17 00:00:00 2001 From: Bethuel Mmbaga Date: Tue, 18 Feb 2025 02:08:03 +0300 Subject: [PATCH 63/92] [management] add batching support for SaveUsers and SaveGroups (#3341) Signed-off-by: bcmmbaga --- management/server/store/sql_store.go | 4 +- management/server/store/sql_store_test.go | 74 +++++++++++++++++++++++ 2 files changed, 76 insertions(+), 2 deletions(-) diff --git a/management/server/store/sql_store.go b/management/server/store/sql_store.go index 6a6753595..5c4ddf666 100644 --- a/management/server/store/sql_store.go +++ b/management/server/store/sql_store.go @@ -420,7 +420,7 @@ func (s *SqlStore) SaveUsers(ctx context.Context, lockStrength LockingStrength, return nil } - result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Save(&users) + result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}, clause.OnConflict{UpdateAll: true}).Create(&users) if result.Error != nil { log.WithContext(ctx).Errorf("failed to save users to store: %s", result.Error) return status.Errorf(status.Internal, "failed to save users to store") @@ -444,7 +444,7 @@ func (s *SqlStore) SaveGroups(ctx context.Context, lockStrength LockingStrength, return nil } - result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Save(&groups) + result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}, clause.OnConflict{UpdateAll: true}).Create(&groups) if result.Error != nil { return status.Errorf(status.Internal, "failed to save groups to store: %v", result.Error) } diff --git a/management/server/store/sql_store_test.go b/management/server/store/sql_store_test.go index 4dcdadf44..6e04c7d9d 100644 --- a/management/server/store/sql_store_test.go +++ b/management/server/store/sql_store_test.go @@ -1331,6 +1331,14 @@ func TestSqlStore_SaveGroups(t *testing.T) { } err = store.SaveGroups(context.Background(), LockingStrengthUpdate, groups) require.NoError(t, err) + + groups[1].Peers = []string{} + err = store.SaveGroups(context.Background(), LockingStrengthUpdate, groups) + require.NoError(t, err) + + group, err := store.GetGroupByID(context.Background(), LockingStrengthShare, accountID, groups[1].ID) + require.NoError(t, err) + require.Equal(t, groups[1], group) } func TestSqlStore_DeleteGroup(t *testing.T) { @@ -3046,6 +3054,14 @@ func TestSqlStore_SaveUsers(t *testing.T) { accountUsers, err = store.GetAccountUsers(context.Background(), LockingStrengthShare, accountID) require.NoError(t, err) require.Len(t, accountUsers, 4) + + users[1].AutoGroups = []string{"groupA", "groupC"} + err = store.SaveUsers(context.Background(), LockingStrengthUpdate, users) + require.NoError(t, err) + + user, err := store.GetUserByUserID(context.Background(), LockingStrengthShare, users[1].Id) + require.NoError(t, err) + require.Equal(t, users[1].AutoGroups, user.AutoGroups) } func TestSqlStore_DeleteUser(t *testing.T) { @@ -3198,3 +3214,61 @@ func TestSqlStore_DeletePAT(t *testing.T) { require.Error(t, err) require.Nil(t, pat) } + +func TestSqlStore_SaveUsers_LargeBatch(t *testing.T) { + store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/extended-store.sql", t.TempDir()) + t.Cleanup(cleanup) + require.NoError(t, err) + + accountID := "bf1c8084-ba50-4ce7-9439-34653001fc3b" + + accountUsers, err := store.GetAccountUsers(context.Background(), LockingStrengthShare, accountID) + require.NoError(t, err) + require.Len(t, accountUsers, 2) + + usersToSave := make([]*types.User, 0) + + for i := 1; i <= 8000; i++ { + usersToSave = append(usersToSave, &types.User{ + Id: fmt.Sprintf("user-%d", i), + AccountID: accountID, + Role: types.UserRoleUser, + }) + } + + err = store.SaveUsers(context.Background(), LockingStrengthUpdate, usersToSave) + require.NoError(t, err) + + accountUsers, err = store.GetAccountUsers(context.Background(), LockingStrengthShare, accountID) + require.NoError(t, err) + require.Equal(t, 8002, len(accountUsers)) +} + +func TestSqlStore_SaveGroups_LargeBatch(t *testing.T) { + store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/extended-store.sql", t.TempDir()) + t.Cleanup(cleanup) + require.NoError(t, err) + + accountID := "bf1c8084-ba50-4ce7-9439-34653001fc3b" + + accountGroups, err := store.GetAccountGroups(context.Background(), LockingStrengthShare, accountID) + require.NoError(t, err) + require.Len(t, accountGroups, 3) + + groupsToSave := make([]*types.Group, 0) + + for i := 1; i <= 8000; i++ { + groupsToSave = append(groupsToSave, &types.Group{ + ID: fmt.Sprintf("%d", i), + AccountID: accountID, + Name: fmt.Sprintf("group-%d", i), + }) + } + + err = store.SaveGroups(context.Background(), LockingStrengthUpdate, groupsToSave) + require.NoError(t, err) + + accountGroups, err = store.GetAccountGroups(context.Background(), LockingStrengthShare, accountID) + require.NoError(t, err) + require.Equal(t, 8003, len(accountGroups)) +} From f67e56d3b9f018481470cc9ea42af86cfce81df4 Mon Sep 17 00:00:00 2001 From: Karsa Date: Tue, 18 Feb 2025 02:21:44 +0100 Subject: [PATCH 64/92] [client][ui] added accessible tray icons (#3335) Added accessible tray icons with: - dark mode support on Windows and Linux, kudos to @burgosz for the PoC - template icon support on MacOS Also added appropriate connecting status icons --- .goreleaser_ui.yaml | 4 +- client/ui/client_ui.go | 178 +++++++++++++++--- .../ui/netbird-systemtray-connected-dark.ico | Bin 0 -> 105144 bytes .../ui/netbird-systemtray-connected-dark.png | Bin 0 -> 5272 bytes .../ui/netbird-systemtray-connected-macos.png | Bin 0 -> 3858 bytes client/ui/netbird-systemtray-connected.ico | Bin 5139 -> 105151 bytes client/ui/netbird-systemtray-connected.png | Bin 9105 -> 5287 bytes .../ui/netbird-systemtray-connecting-dark.ico | Bin 0 -> 105128 bytes .../ui/netbird-systemtray-connecting-dark.png | Bin 0 -> 5434 bytes .../netbird-systemtray-connecting-macos.png | Bin 0 -> 3843 bytes client/ui/netbird-systemtray-connecting.ico | Bin 0 -> 105091 bytes client/ui/netbird-systemtray-connecting.png | Bin 0 -> 5412 bytes .../netbird-systemtray-disconnected-macos.png | Bin 0 -> 3491 bytes client/ui/netbird-systemtray-disconnected.ico | Bin 5167 -> 104575 bytes client/ui/netbird-systemtray-disconnected.png | Bin 9816 -> 4800 bytes client/ui/netbird-systemtray-error-dark.ico | Bin 0 -> 105062 bytes client/ui/netbird-systemtray-error-dark.png | Bin 0 -> 5279 bytes client/ui/netbird-systemtray-error-macos.png | Bin 0 -> 3837 bytes client/ui/netbird-systemtray-error.ico | Bin 0 -> 105013 bytes client/ui/netbird-systemtray-error.png | Bin 0 -> 5260 bytes client/ui/netbird-systemtray-update-cloud.ico | Bin 3647 -> 0 bytes client/ui/netbird-systemtray-update-cloud.png | Bin 5652 -> 0 bytes ...tbird-systemtray-update-connected-dark.ico | Bin 0 -> 104704 bytes ...tbird-systemtray-update-connected-dark.png | Bin 0 -> 4867 bytes ...bird-systemtray-update-connected-macos.png | Bin 0 -> 3570 bytes .../netbird-systemtray-update-connected.ico | Bin 7678 -> 104698 bytes .../netbird-systemtray-update-connected.png | Bin 11471 -> 4842 bytes ...rd-systemtray-update-disconnected-dark.ico | Bin 0 -> 105086 bytes ...rd-systemtray-update-disconnected-dark.png | Bin 0 -> 5275 bytes ...d-systemtray-update-disconnected-macos.png | Bin 0 -> 3816 bytes ...netbird-systemtray-update-disconnected.ico | Bin 7966 -> 105115 bytes ...netbird-systemtray-update-disconnected.png | Bin 12437 -> 5298 bytes client/ui/netbird.png | Bin 0 -> 4800 bytes 33 files changed, 149 insertions(+), 33 deletions(-) create mode 100644 client/ui/netbird-systemtray-connected-dark.ico create mode 100644 client/ui/netbird-systemtray-connected-dark.png create mode 100644 client/ui/netbird-systemtray-connected-macos.png create mode 100644 client/ui/netbird-systemtray-connecting-dark.ico create mode 100644 client/ui/netbird-systemtray-connecting-dark.png create mode 100644 client/ui/netbird-systemtray-connecting-macos.png create mode 100644 client/ui/netbird-systemtray-connecting.ico create mode 100644 client/ui/netbird-systemtray-connecting.png create mode 100644 client/ui/netbird-systemtray-disconnected-macos.png create mode 100644 client/ui/netbird-systemtray-error-dark.ico create mode 100644 client/ui/netbird-systemtray-error-dark.png create mode 100644 client/ui/netbird-systemtray-error-macos.png create mode 100644 client/ui/netbird-systemtray-error.ico create mode 100644 client/ui/netbird-systemtray-error.png delete mode 100644 client/ui/netbird-systemtray-update-cloud.ico delete mode 100644 client/ui/netbird-systemtray-update-cloud.png create mode 100644 client/ui/netbird-systemtray-update-connected-dark.ico create mode 100644 client/ui/netbird-systemtray-update-connected-dark.png create mode 100644 client/ui/netbird-systemtray-update-connected-macos.png create mode 100644 client/ui/netbird-systemtray-update-disconnected-dark.ico create mode 100644 client/ui/netbird-systemtray-update-disconnected-dark.png create mode 100644 client/ui/netbird-systemtray-update-disconnected-macos.png create mode 100644 client/ui/netbird.png diff --git a/.goreleaser_ui.yaml b/.goreleaser_ui.yaml index 06577f4e3..983aa0e78 100644 --- a/.goreleaser_ui.yaml +++ b/.goreleaser_ui.yaml @@ -53,7 +53,7 @@ nfpms: contents: - src: client/ui/netbird.desktop dst: /usr/share/applications/netbird.desktop - - src: client/ui/netbird-systemtray-connected.png + - src: client/ui/netbird.png dst: /usr/share/pixmaps/netbird.png dependencies: - netbird @@ -70,7 +70,7 @@ nfpms: contents: - src: client/ui/netbird.desktop dst: /usr/share/applications/netbird.desktop - - src: client/ui/netbird-systemtray-connected.png + - src: client/ui/netbird.png dst: /usr/share/pixmaps/netbird.png dependencies: - netbird diff --git a/client/ui/client_ui.go b/client/ui/client_ui.go index f22ee377b..1aa61a2b2 100644 --- a/client/ui/client_ui.go +++ b/client/ui/client_ui.go @@ -21,6 +21,7 @@ import ( "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" @@ -90,6 +91,14 @@ func main() { } client := newServiceClient(daemonAddr, a, showSettings, showRoutes) + settingsChangeChan := make(chan fyne.Settings) + a.Settings().AddChangeListener(settingsChangeChan) + go func() { + for range settingsChangeChan { + client.updateIcon() + } + }() + if showSettings || showRoutes { a.Run() } else { @@ -106,46 +115,108 @@ func main() { } } +//go:embed netbird.ico +var iconAboutICO []byte + +//go:embed netbird.png +var iconAboutPNG []byte + //go:embed netbird-systemtray-connected.ico var iconConnectedICO []byte //go:embed netbird-systemtray-connected.png var iconConnectedPNG []byte +//go:embed netbird-systemtray-connected-macos.png +var iconConnectedMacOS []byte + +//go:embed netbird-systemtray-connected-dark.ico +var iconConnectedDarkICO []byte + +//go:embed netbird-systemtray-connected-dark.png +var iconConnectedDarkPNG []byte + //go:embed netbird-systemtray-disconnected.ico var iconDisconnectedICO []byte //go:embed netbird-systemtray-disconnected.png var iconDisconnectedPNG []byte +//go:embed netbird-systemtray-disconnected-macos.png +var iconDisconnectedMacOS []byte + //go:embed netbird-systemtray-update-disconnected.ico var iconUpdateDisconnectedICO []byte //go:embed netbird-systemtray-update-disconnected.png var iconUpdateDisconnectedPNG []byte +//go:embed netbird-systemtray-update-disconnected-macos.png +var iconUpdateDisconnectedMacOS []byte + +//go:embed netbird-systemtray-update-disconnected-dark.ico +var iconUpdateDisconnectedDarkICO []byte + +//go:embed netbird-systemtray-update-disconnected-dark.png +var iconUpdateDisconnectedDarkPNG []byte + //go:embed netbird-systemtray-update-connected.ico var iconUpdateConnectedICO []byte //go:embed netbird-systemtray-update-connected.png var iconUpdateConnectedPNG []byte -//go:embed netbird-systemtray-update-cloud.ico -var iconUpdateCloudICO []byte +//go:embed netbird-systemtray-update-connected-macos.png +var iconUpdateConnectedMacOS []byte -//go:embed netbird-systemtray-update-cloud.png -var iconUpdateCloudPNG []byte +//go:embed netbird-systemtray-update-connected-dark.ico +var iconUpdateConnectedDarkICO []byte + +//go:embed netbird-systemtray-update-connected-dark.png +var iconUpdateConnectedDarkPNG []byte + +//go:embed netbird-systemtray-connecting.ico +var iconConnectingICO []byte + +//go:embed netbird-systemtray-connecting.png +var iconConnectingPNG []byte + +//go:embed netbird-systemtray-connecting-macos.png +var iconConnectingMacOS []byte + +//go:embed netbird-systemtray-connecting-dark.ico +var iconConnectingDarkICO []byte + +//go:embed netbird-systemtray-connecting-dark.png +var iconConnectingDarkPNG []byte + +//go:embed netbird-systemtray-error.ico +var iconErrorICO []byte + +//go:embed netbird-systemtray-error.png +var iconErrorPNG []byte + +//go:embed netbird-systemtray-error-macos.png +var iconErrorMacOS []byte + +//go:embed netbird-systemtray-error-dark.ico +var iconErrorDarkICO []byte + +//go:embed netbird-systemtray-error-dark.png +var iconErrorDarkPNG []byte type serviceClient struct { ctx context.Context addr string conn proto.DaemonServiceClient + icAbout []byte icConnected []byte icDisconnected []byte icUpdateConnected []byte icUpdateDisconnected []byte - icUpdateCloud []byte + icConnecting []byte + icError []byte // systray menu items mStatus *systray.MenuItem @@ -214,20 +285,7 @@ func newServiceClient(addr string, a fyne.App, showSettings bool, showRoutes boo update: version.NewUpdate(), } - if runtime.GOOS == "windows" { - s.icConnected = iconConnectedICO - s.icDisconnected = iconDisconnectedICO - s.icUpdateConnected = iconUpdateConnectedICO - s.icUpdateDisconnected = iconUpdateDisconnectedICO - s.icUpdateCloud = iconUpdateCloudICO - - } else { - s.icConnected = iconConnectedPNG - s.icDisconnected = iconDisconnectedPNG - s.icUpdateConnected = iconUpdateConnectedPNG - s.icUpdateDisconnected = iconUpdateDisconnectedPNG - s.icUpdateCloud = iconUpdateCloudPNG - } + s.setNewIcons() if showSettings { s.showSettingsUI() @@ -239,6 +297,63 @@ func newServiceClient(addr string, a fyne.App, showSettings bool, showRoutes boo return s } +func (s *serviceClient) setNewIcons() { + if runtime.GOOS == "windows" { + s.icAbout = iconAboutICO + if s.app.Settings().ThemeVariant() == theme.VariantDark { + s.icConnected = iconConnectedDarkICO + s.icDisconnected = iconDisconnectedICO + s.icUpdateConnected = iconUpdateConnectedDarkICO + s.icUpdateDisconnected = iconUpdateDisconnectedDarkICO + s.icConnecting = iconConnectingDarkICO + s.icError = iconErrorDarkICO + } else { + s.icConnected = iconConnectedICO + s.icDisconnected = iconDisconnectedICO + s.icUpdateConnected = iconUpdateConnectedICO + s.icUpdateDisconnected = iconUpdateDisconnectedICO + s.icConnecting = iconConnectingICO + s.icError = iconErrorICO + } + } else { + s.icAbout = iconAboutPNG + if s.app.Settings().ThemeVariant() == theme.VariantDark { + s.icConnected = iconConnectedDarkPNG + s.icDisconnected = iconDisconnectedPNG + s.icUpdateConnected = iconUpdateConnectedDarkPNG + s.icUpdateDisconnected = iconUpdateDisconnectedDarkPNG + s.icConnecting = iconConnectingDarkPNG + s.icError = iconErrorDarkPNG + } else { + s.icConnected = iconConnectedPNG + s.icDisconnected = iconDisconnectedPNG + s.icUpdateConnected = iconUpdateConnectedPNG + s.icUpdateDisconnected = iconUpdateDisconnectedPNG + s.icConnecting = iconConnectingPNG + s.icError = iconErrorPNG + } + } +} + +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") @@ -376,8 +491,10 @@ func (s *serviceClient) login() error { } 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 } @@ -407,6 +524,7 @@ func (s *serviceClient) menuUpClick() error { } func (s *serviceClient) menuDownClick() error { + systray.SetTemplateIcon(iconConnectingMacOS, s.icConnecting) conn, err := s.getSrvClient(defaultFailTimeout) if err != nil { log.Errorf("get client: %v", err) @@ -458,9 +576,9 @@ func (s *serviceClient) updateStatus() error { s.connected = true s.sendNotification = true if s.isUpdateIconActive { - systray.SetIcon(s.icUpdateConnected) + systray.SetTemplateIcon(iconUpdateConnectedMacOS, s.icUpdateConnected) } else { - systray.SetIcon(s.icConnected) + systray.SetTemplateIcon(iconConnectedMacOS, s.icConnected) } systray.SetTooltip("NetBird (Connected)") s.mStatus.SetTitle("Connected") @@ -482,11 +600,9 @@ func (s *serviceClient) updateStatus() error { s.isUpdateIconActive = s.update.SetDaemonVersion(status.DaemonVersion) if !s.isUpdateIconActive { if systrayIconState { - systray.SetIcon(s.icConnected) - s.mAbout.SetIcon(s.icConnected) + systray.SetTemplateIcon(iconConnectedMacOS, s.icConnected) } else { - systray.SetIcon(s.icDisconnected) - s.mAbout.SetIcon(s.icDisconnected) + systray.SetTemplateIcon(iconDisconnectedMacOS, s.icDisconnected) } } @@ -517,9 +633,9 @@ func (s *serviceClient) updateStatus() error { func (s *serviceClient) setDisconnectedStatus() { s.connected = false if s.isUpdateIconActive { - systray.SetIcon(s.icUpdateDisconnected) + systray.SetTemplateIcon(iconUpdateDisconnectedMacOS, s.icUpdateDisconnected) } else { - systray.SetIcon(s.icDisconnected) + systray.SetTemplateIcon(iconDisconnectedMacOS, s.icDisconnected) } systray.SetTooltip("NetBird (Disconnected)") s.mStatus.SetTitle("Disconnected") @@ -529,7 +645,7 @@ func (s *serviceClient) setDisconnectedStatus() { } func (s *serviceClient) onTrayReady() { - systray.SetIcon(s.icDisconnected) + systray.SetTemplateIcon(iconDisconnectedMacOS, s.icDisconnected) systray.SetTooltip("NetBird") // setup systray menu items @@ -554,7 +670,7 @@ func (s *serviceClient) onTrayReady() { systray.AddSeparator() s.mAbout = systray.AddMenuItem("About", "About") - s.mAbout.SetIcon(s.icDisconnected) + s.mAbout.SetIcon(s.icAbout) versionString := normalizedVersion(version.NetbirdVersion()) s.mVersionUI = s.mAbout.AddSubMenuItem(fmt.Sprintf("GUI: %s", versionString), fmt.Sprintf("GUI Version: %s", versionString)) s.mVersionUI.Disable() @@ -771,9 +887,9 @@ func (s *serviceClient) onUpdateAvailable() { s.isUpdateIconActive = true if s.connected { - systray.SetIcon(s.icUpdateConnected) + systray.SetTemplateIcon(iconUpdateConnectedMacOS, s.icUpdateConnected) } else { - systray.SetIcon(s.icUpdateDisconnected) + systray.SetTemplateIcon(iconUpdateDisconnectedMacOS, s.icUpdateDisconnected) } } diff --git a/client/ui/netbird-systemtray-connected-dark.ico b/client/ui/netbird-systemtray-connected-dark.ico new file mode 100644 index 0000000000000000000000000000000000000000..0db8a08624e6f0f0e679fff0e69d9d182f64e41b GIT binary patch literal 105144 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 literal 0 HcmV?d00001 diff --git a/client/ui/netbird-systemtray-connected-dark.png b/client/ui/netbird-systemtray-connected-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..f18a929a0c46fe6eb534932bb8af7dee80a15698 GIT binary patch literal 5272 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 b)IkE9NkTHqSjM4s{{jzZW@B1u>>2kzO3uwf literal 0 HcmV?d00001 diff --git a/client/ui/netbird-systemtray-connected-macos.png b/client/ui/netbird-systemtray-connected-macos.png new file mode 100644 index 0000000000000000000000000000000000000000..ead210250d7a4b25461aa0311fcb6c13cec75057 GIT binary patch literal 3858 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 literal 0 HcmV?d00001 diff --git a/client/ui/netbird-systemtray-connected.ico b/client/ui/netbird-systemtray-connected.ico index 80550aa37e20d9d134cda6c0cfdeb731f1765cfe..c16bec3f51e8e41d8263b2b4b5dc2095b51e1261 100644 GIT binary patch literal 105151 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

    6A!aevjJ&cU%ypida~ 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 literal 5139 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 diff --git a/client/ui/netbird-systemtray-connecting-dark.ico b/client/ui/netbird-systemtray-connecting-dark.ico new file mode 100644 index 0000000000000000000000000000000000000000..615d40f075b41f3b3c9241611cc4b249d0851899 GIT binary patch literal 105128 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 OF~=dJ2Ok^g7W{u+(t)V} literal 0 HcmV?d00001 diff --git a/client/ui/netbird-systemtray-connecting-dark.png b/client/ui/netbird-systemtray-connecting-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..a665eb61cc066b42388ce7acd0858d0321a7b708 GIT binary patch literal 5434 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 literal 0 HcmV?d00001 diff --git a/client/ui/netbird-systemtray-connecting-macos.png b/client/ui/netbird-systemtray-connecting-macos.png new file mode 100644 index 0000000000000000000000000000000000000000..0fe7fa0dbe50bcf8c08e45c7cf13b7e6e2348335 GIT binary patch literal 3843 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 OY{ZbUgZ~;x5BxvcE4?TH literal 0 HcmV?d00001 diff --git a/client/ui/netbird-systemtray-connecting.png b/client/ui/netbird-systemtray-connecting.png new file mode 100644 index 0000000000000000000000000000000000000000..4f607c997df29ed0104e0e824155f65a1098a3a5 GIT binary patch literal 5412 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 literal 0 HcmV?d00001 diff --git a/client/ui/netbird-systemtray-disconnected-macos.png b/client/ui/netbird-systemtray-disconnected-macos.png new file mode 100644 index 0000000000000000000000000000000000000000..36b9a488f16673b1fcb1c87037d3d1606dfb1e5b GIT binary patch literal 3491 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 VFbHp#EYMSdj14UGKVEZ=`9J&Jr*QxP literal 0 HcmV?d00001 diff --git a/client/ui/netbird-systemtray-disconnected.ico b/client/ui/netbird-systemtray-disconnected.ico index aa75268b0c7c110352eed1b76b21a97e8e24b909..dcb9f4bf83dcfcb18d4858d5cf553895b092eca0 100644 GIT binary patch literal 104575 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 Uw;PvXstfzveZt6u;jD=N1N46qGXMYp literal 5167 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 diff --git a/client/ui/netbird-systemtray-error-dark.ico b/client/ui/netbird-systemtray-error-dark.ico new file mode 100644 index 0000000000000000000000000000000000000000..083816188d695c0a083ba7dcd78c1f31e9f06080 GIT binary patch literal 105062 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 literal 0 HcmV?d00001 diff --git a/client/ui/netbird-systemtray-error-macos.png b/client/ui/netbird-systemtray-error-macos.png new file mode 100644 index 0000000000000000000000000000000000000000..9a9998bcfd1f2f4b686f2c08ac4be42198111d83 GIT binary patch literal 3837 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 Z(%>Cos#cWtK%X^)%YJvqTKk}k{{iS8*lPd) literal 0 HcmV?d00001 diff --git a/client/ui/netbird-systemtray-update-cloud.ico b/client/ui/netbird-systemtray-update-cloud.ico deleted file mode 100644 index b87c6f4b55620eea0b397d6243063e22315933f4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3647 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 UPWN{{D&hjh2Il(Jy7=h-0rzr)wg3PC diff --git a/client/ui/netbird-systemtray-update-connected-dark.ico b/client/ui/netbird-systemtray-update-connected-dark.ico new file mode 100644 index 0000000000000000000000000000000000000000..b11bb54927fe232adf7924216fbbce9569d5156d GIT binary patch literal 104704 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 literal 0 HcmV?d00001 diff --git a/client/ui/netbird-systemtray-update-connected-dark.png b/client/ui/netbird-systemtray-update-connected-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..52ae621ac0f35138d31b68201d70940a61d947e1 GIT binary patch literal 4867 zcma)Ac{J4F*MDXV#y(^#gtCk!W2v7l&DfJ@gsd59p{&^{vq+Yav{_0RN>Yh7OTze} zjY0{f&}7Xr#yXlA-tWBszvsN)bLM>SbMNQ5+jH+d-+8j|u8tC-J468h5>5y0jsgIK zNEkp0K`;O4hi9OdNYp`}7y!h!@qaLI=dL0j5_8ni20Z<&JOeEd!PYL;06fbT8eyy4AI9+CmHjg5#$5Z-T)>ub~?SKqf~F16MOn%?cV!cWf&0@Kt1u9O>sBs z7OX{()js(#i>N0I$bdYi_;lSx&`N}TjQ|c(@d*(5lCN>hN(tIm_DK*0 z#`bkVWE)=v!0c-oF`d}(hmLOK{40oD_6N>jk=nJ|qEk8rM}kEMX9yhdnGa@lJXv>1 z+N*#AeJ z%%pkAqqqL>!`qRuN;Ef^f_uYp_i9HlJrjJrIc>?U^4c*leHh*1$@-qqD1))n;9&K(+5m~RZX5)>^!c#Q=7@Bb(Y!O;j2{VzyD!h}D zITI;A90HY}uE+f*XussTY@f~^nBW!MaCv^81;*f`ga>)TwjUcBEbTBc>a(rpp%pZx za`w%8?swQllIeG+2`cuycNzV;>dJ}DHLbJKdodLwZMioepQ7XeMaicW!1YtBy^ozJ`I#qcYAhlrONwG2i?g(c%WHL9b}tXA9{k|NR?|FX@S!!JA_S?mtEVsY2#;^Md;FIz)-fD_bzM@&D5Sj%y( zGyYi_

  • ()8s$5@jC}jr3>=1=CoCwKd%e@(zi*>#ijP~;24kFVj4|@&*ON)Ps(y4 z3qJqAmNtHAwM#wGjPbwRh&#ktQAiz7e#I-OP{7u#Gb>(RW+)u)ODg%WIQ4O*{(j{J z-uyWE*ta1Gqp@yW#mj#ke!N)?#f7PL$M1vEvNeKZs4X9r`nid3A!Bt3);VZb8>DfrXah>Z-Jn-BH?Q;Z{6?70-cGgrIK{ zy(fQRq8JnOO48uyVcD$F3`yz;#ZB1QX1zZ=Zlry+g;V|2e=~mmeTYGCU+M6NdKW7x zvMpKrE5e|dvo=3>{M_vlCw8nU1Bda!?0j=~<^8iuFPw6>B9=lT>h@2>ST5sGC5QEv zv?o%p^=hiXt0RP?iypStc4lDfUpbo>|H6hc^Uj}UjecUb_hr-V{`6G!$prqq>GjVl zjeJt1dUzKFiZ)bG^e|Vg9k|zqf?vICCA$*e9Qo)&BYCDb^5}f9z%GQcSR@)vB#qVy zEhsej#(nTOi!_t?EE9#kRz~~u?zq!V_~)RB(}S8B#G6m7RM_26$GFNxYowJ0(xI^w zHwE{E>^?cL^R6v!i<0*hd5D#SR^%p+dgL_H=??3s_JN@pJ)WN#zC(H|J+%)?PF}H? zhsr;f=R1LTrKtJ%2fSeiFJ<`9PfXOefp^cYgFt1tnKM0Wfl{u-J*9n4n$VVb9J`w1 z%~bL;Qy42N?Ua1<3P6ZFiayWox_^fFF*FukPv(}Qgb%Q zz_9k!X^YQ4Qb6{YN7~zmzZdVCQ0j*59VEjg&W1blFkNwQZ-R zK5Gp`?v7u}4YXAH{vg8K0Oeg#vw15L%k_}F@$yCTnPsM1^C`=55OVa7Gx_{Q8q!tn z`H__PxzK0M8;frje)Jqe2Er;~bTh~JJmzqp0+$uiL`?H;9rFOu+`-*@A`0=*d#9zRMCbWt@&gI06JY8}o@ECc=n6d~y zAl?1)UmYP*fIQh3ehmc;ToXw(^^K)5t~B_4Ob$a$5%iv{%_I z-<|Xo=7|1+tem#d{feRv z-Ps_}C>>|S<~-Vn(KeL5Qeeu@V2}m=z9M(ug;iBf7sR~o1)~RX)Y0*+jP9)&JiK- z2ZfwJ-I0BDBU;F|4V|b0^zXkwgpDvG1=3T0FCPHjXmxxTXF@|XJ@tgxE--{fT(okg z<%DBx6-9;g3;8x%cA{^Pbl08S6-Cqa9)iFFCAuC{g_vMNS@EM$Fgn%!@~vM}4^8bS zoMwlK4L8v1h{vXMWFbWGFHC0g4;;QD`3!|4L zJ|-Ri+m)F|>LeI$7bV90&P$N1nnjcgs)EA|ANb#rp>V8;NqXwf?H6Nye=D-kG}K!p zmln{d3*o1%cv1n{u=L?7uh1=@XsfUksNS>nQjC;(y95?c4v|}*3D_%tL%U7;QljJk z^L+O8itq8Fr8)vpyHJX!=4AQ!))aPd4=Fd%!B7aM=69=9Sp{BXfiS>}QQJl3s1HHEj4g;uN=IyZq&5l-=)Jy zvXRSOUD@e6RRkPn*fv>ZGqff!g97eD7M=%F(g##e*@ABTx<^^iaAYp9gBEU69_nEn ziWCZnd6;`5hlyii2r|XEQZy;KTft5g7WjU}L!FSjan43&?2@1DC4fHRhJ0uh#>t6d zF|{{zDIpF1o`$m?W%#Qbk3v1TJz;D0B6haI__o`6Jm;Ij)I?USsxY2Qv=+v<*DHOO z;QiXbBvL7UVlS{kxtwlOFgP2?OeA zOZj5n5B*JTr-1{Tn>9-2?J_Dj7h&`21mb;dc$ciEY-`FpYUzxs={FHIaBe8c((mhN z@f0K12!ZFW6{9z=r`rG+-uBK_tJCP`C>(3p^aa~-2NH(B**7VDx2ST=rRz>Jj*w`n zTMx_G^M2mk-TAiAjn#p#72J?1e)BI^$7U>p zYIBKEUlR)nL}6h9`5yi}%ayDTH4-!GG>tFYB@$1iOf9QtS(+rBcWb!5hM%zVod z2DyEVTKa7cfdZO~b)_vO!KaB*8Mq}Y(*GKI#Zax1h9k)`SHn4Z8!4l!Cj4-GFq_w@ z>SSQJH*UBFF=_+3L^#=rJ*ti{MC`?%S*%B@Djd7J9jyqZ?QJ&6KdVj60tj^0m1^2+ zFUn?JX4LnbWxbFo1{P$&_@}3R?tGQ^$scvks~WbA_ME!DLphJ`(z7K7$q_Imf>E0MO%h?WFpI zLNs;sfKewl;%zigp^NKQ3@TxW`E~2YK~UVgvP(ZrLvgz=ej}_q6ui%q|11M3)2?GX zE?wHDf`tc)77Y{MDyKuIe}L6lldgazuGJUa#O8oP{7+`hgVur8^OF4FOKUoNP+;35 zXd<~>suV3_&o4K!x5ERwc{$ym#v&Bo6lsGJ-oPyBr;}v-S`TN`zFy zf8AGvY<`Cw99=WXpWWKL8OCJl-0s9ao!l1YPpx44+)}}ve@8%Cy!_?EqOosl7vl~? z%EZVIayOzK>UmBX$BwlAwr1rbr6@y?rdJ4j^OL3~JHHWOa&9Nqbu3!#EhE@14y;>h zFuoCAu>-h=y|q!HtQR2`WHlj@y6l!2tQgk~>6`(+=966ypd7sVDbf+Z)u&31jX1;y zY}qO$(KEKG%WpNvTfk(u%Fq^Mjk1IfUS$f{n4cmvwyMYh@MdH!yNvCqd>K%;8fXPT zCU?VF-kp^j8f8vqV0ES?aSS7ZL(m@L0A8ESC^Z<8qz{L;7G{&~cDjO2tY57P*?_nu zL_%*m>c_#+n;WU(3~bdID9H+9#^9pG0RNskl+&t<@`X ztJ0FfxFAb9ZH=JUxAq)pGbepQslsoFV2G1E8Tz|QPIuY_&TK!LI2k95>fYGA*(<{S zbdwPrkN`$hm-mTTz(`3R-0oq?z$xGS3iEKc_Yk1-1tWMQ9#k!{uVCG$5!~M=SSA<& zwZckHArAR|>MYBInGXU~mpz5^1&ovYx!p~YxC+?*UzMV<@TJ56ZuetJwW07<<6e>X zPy?IPSdatjc^`4wy)8IuY0@^ZpY43hv1T<=#)f7Elfy1+fj5?ukIy1u>u^2dq&@gK z`ktZ8-II!n(xx+l85!ih2u>cQE${_=4^gW{fNn$(>J{0mJ;n$ww*+CFyfjz(Q+jr( zATJ7kt*r}{NrT!G++EZ~?1CB&fZ#VsN+z{b9nf|9)KNYx69h2RHb(5yhlJ!~nWB_~ z*^rR7ZERrt7I<*h#OcQm0)7Mjn>O%|$%D;(1rD}r&sG6e?JVy=C#VYtyMI5@+Foa|z z9q1n8<4;I?0pu$4umr@?gjipmK`eoe4BN5Jo~>F3@P9nl+1SdZPX9kY?$+`VPWG;L JPi+EI{|6dE%h&(_ literal 0 HcmV?d00001 diff --git a/client/ui/netbird-systemtray-update-connected-macos.png b/client/ui/netbird-systemtray-update-connected-macos.png new file mode 100644 index 0000000000000000000000000000000000000000..8a6b2f2db85a6b6e312015ce634ec49096c45059 GIT binary patch literal 3570 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 MdJg!tzfZ*f0jyzlfdBvi literal 7678 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 My6&w?HM_9?0TFN?z5oCK diff --git a/client/ui/netbird-systemtray-update-connected.png b/client/ui/netbird-systemtray-update-connected.png index a0c4533406c399aacba6184ad7e74be9e20a0cf0..90bb0b7f1975c585b48a5088710678521a236ccd 100644 GIT binary patch literal 4842 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 WuRrb?z6NGGiIKzHh8`Q@9rAy92WzMR literal 0 HcmV?d00001 diff --git a/client/ui/netbird-systemtray-update-disconnected-dark.png b/client/ui/netbird-systemtray-update-disconnected-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..9e05351f1fe9e64bb78700907bb63875c0e8b5e1 GIT binary patch literal 5275 zcmbtYi8qvO*nfr@+vK%mE8EyZSt=yU%viD%#+J$&MUkblXUq`k#amjGB8DVUMhV$w zEXh`uP%<%O7iNrQhS|RH4}9l4=X=h1&i&ly{O;@bJJ)kx*L~gBzbI!rQIUfp004+O z*xR@P0EqVq0)zy3gU^lfVBR1cWqf1t)w%Z4px*+TKO&6ez*irMgR4s!9zS~;j z>#oIug5U!j`*m<>tQc$;oF-QN<=icxUF7tIbgMsOUv8@#_*NYbn7SCGG&(Uly2aqC z%Bc$Hq#3yX%6&Ha%Z5k)|9=c@#SyrB$xrdpT8~d7!wwAHQorid^75wkRnM09Ucxec zi^6;WI7ODp$oo-~ZHaY zJg~M@v>zdOuWlp8vzhr{V|yg*X`H<;8=rhM2}3)*0<>zkQ2$+v@*CJOuR=r!JJgq9 z{sRE#<~nNe{zfXjCn{u@E)L*4&oPutby-f<0QRhcD6O`{PHd!|h#WNCI(f~Pz4(!8 zazqX2^Sid7T+vC?t!A)W(}{*?%fHrRAgmFSzG1Gr=z2ZTQOvw89BwFgFE}|HI6UZu zevZyf`6qWg`SKVDp;qv>uaK~e-yd(cxwu<$UkyNnhh!uuEj3YF%^q!VvMdE{5#{Oi ztcnjTsVuU~a_xa{q0C$r0C71ZZbPt}-a?Q1HCj|GB=m*xmfN|E{uvN%{t^zK8PLFLXlD7dd4}Q+STyXeQ_FT<^B=}`D$6_=3eFu6I|`XH znak>XP0yi=Tu_wdHWwxN6zjYjSAUsZ`6?vwz<*zsyX5t+9;eKDgjO>;E`Hx`qK1?Q zEoAMa{n-5T?TY8zv&v^U`ev7l>}JKTqnFvU?cSM3rT<92hL7M!f3ACN1>s*O_alSm z^0ou_$qCYFc83@cs}|Xv!TWI_tSn<)_Ax;!4_0tPMJ3D=J4hbEkCOc0$Z?7!WC zLf2=gfCUQvik(TXBX`0t3{^4{EYB}K5;UATelFaar$>A<<8*4Q9@<>Nt*gI(8?GjF zr?K^S(#G)g{)&qczzJf-=?idD-AxstPMnNWtjIDEgX-*gp<6_-B)kbvT&G`eHc<}= zB1E`4Er4*Fv$-+tft26suyVzAitmazY7;63pLiWQO{s-rqWz_BF>LKd=*n@hk$%{t z&GglYHqJzn+Kd?!CTBi>I-D&yf^DtSKSTSdv17xYrAPkw%GIPQ38B96(-O7c7=mA9jh$ug>5Lj1%H3n3bC7-9l3pe-b$Wo-*Qns6M235 z(Z7)Jt+O6s*F(Lq?f&u4>)3iMbWQSpB+PlCHdQJ7I_yOAQ@HiC{$Zy*6@&z3ROCI< z^0##?%(3%+pdAbPF3Ym8OHo9gCnDW0MXYWkWjDs@ZrN?PvH*H96U z?+=`g9B)s7DTJR`7PqnGL^m?F2DoL(yJsM2lw%LIw>=w~H0wX6t=w;4q7g4-3EeJXX`8R2W7Bniedg0FwoA6W}%ejUq?>4<2REeXM|dIg*B zr@nK^sP`0Sq{7Z6x+=>jmFyP$xmD}VkhzNdxtzUIQSu#yk_H=(U=F@8%ZTWvq77!2qW!Fb~A7wu2;+;Cw4!q^Eo(fg&sKxI?>47GsOGnp&j1Tq;9W%@GW z%0a*F*5C=Poy{^bm5tGtxD~PdkwGQ_`K!ECa6IwoU~XHKOTl2w3Yg`|8vLHycw$uw zhZsvOZ0d;nPg(R1Y)XHo&Y!-Gl9QR$rh;DH_MdasMl7AS?cWtB!j7JxzF*-Y4==w#}K z#AMadpDD3-%`GJ_Ny+z$_>6|ebw4A48u0I%o{VEEcBKpZS|wZ7emGR({}IUi^s0VB z4~h}kY?T0~G{mDn>9L${-e_n=JlI}pC!25}SYaiLCoS&cXO_yTCf^*cd~wF1`$@^t z7qy}uRBmvfM1dPuFlLyylYB&t>WaU0{?b<@3fHHW2MX?7QaY6S#%jBV(tfp}*fu^> zdBs5~il8+E!})O(`E;X66b*>2U`m?HzB-Hf%1q~nA7q2jWS~ss{FFNhFG4u&N;qR6 zbV6>!(^d-1T%b+T^IBOfj&C;4)ctV%jh~je%taRyZ0owUrorPtKk@ZG^ow)&fG?~ zdrEd8bv>SzEn6&$#64HV)fH(^9EkD}i!yDpy7bXOi55&re`6KwlfAQ?ro-PIsVD%% zyq^Xc2cDtSqo>#3ta{CR(s+oh28E4Lh_6`Gb?AGXqUGHVN`22-P3^tMA>qWM`edg( zo#XOiyN6D#*MIvEr}3}us)Fo%aXzb{@4Rx@Oqo$()^yMse0sHMX)%JJ06PFvROL#RN1%L6z7MpMkK^U5^K_yei|F^uS3^==OfQ(~nCDXRITG?QAU#&g&e9-L>#E@O z^A2d75Ijr2x0fsVr}vk3+jE&YX4x`Sg3m@Lf!|SA)-hLJ+KA27NDNY-&smpB-Mz9b}x$oSyJ=`Is*FNgA)=QE`B`c7EZj(aGj+%UyV=ah0H#_w;m=GE2=iBsDLSoTgcW`_D1G1R;KEn%s zFxNRNyh1!OMgo}cOV)7kdm`o=u@fZ2I`kwjSeO^Md?w5h%`2sRvzjtQi=k#x;FvJ= z(M>lqTA1V9*sZRTPdgi39pzcXD%)e(;T10Ovxh;1-JtaWmCyGsuztR0tY$?J8!+$z zTcpbdxW7f_1Ik8r)Y8(jhYBN;m5DJyOJq!OGz?YdYKC?1%+C=l4nxc{_t2ly>RcD!njdkt=lqylbONoT=pqEZg8R%LtHOkK=FqD9Y@`ew!ft`g?j zoNfbB7zyOH{qRuP@#T|$i>0Br5-rO*&6+&5ms}lVK&1dwVd9%fDbCKrT6KZl(MNaE zaFay<=Sm{P?>4T=#uFlS(i?d+8u1^;-U=f1ByW=R@9-i*BnAWq_&v)+Y|-`pK0Aar z<$w9?K{^J{bfm76^e1_UG#&z*DFXrRxcRb)_g1{0+w+Xo?HS9A|9Gyr>Q0U2S(_i* zr?tr@(%!xR2zTeadwR*OMwjP2b61vm<_@0t-B0x3Hb_kG{gg63JTyRXu(i){;=6nL zr$yP0KVq{SbU$5e#JG8*b3lD!YMdYABn1Ow1>$ktj;HD~i5bKPRs?_l?maDcoJAUk znPQ$2=Zd3L$n`4SjX)?Mx&ynub+7u6*4{J{vMUcr&X^l!$1Y_XMHmfo;38Vr!Sz5fMn2V zAu&YQNp#~XW7x#*yg%87|ySiT`TN5~sKPD-IGO@?T zt}zdI8>7GrJCA<+K!fc;!PUREkJSa>glOn1m3mkJRWiWr`$gZak2ouiAc%1E;;VX-ZY+P)hj%mF?&I&a{60?((%d>Fdm7~+%% z0e@2bCq6>#0j0yrc$rTK2#KD?|E(nA6o+2^jYMZBDQ6p1Kyd;H#agx_e(ZzKCdIPqX}GL8oK&rs4r{> z7~DS%d&+>_B}>A!Fi)myVYJuXg4iY91hv*AOqP%CNrp+- z^ASkma_te!;U1vXi`(DN9H>u@H^Cqj-Hn9t1F*+b#fSCImg4+3R%V~eQHkID`5}Rq zzl7yq0&>uaj+TT35slYqc2U+%ynWpAU zb=7kF`(fR|MMR~t-~9fZ=GIJ*_vdNQ7hko%7|X6Z~$e& z&4FUFY}}=?4ap{#-|3;=^B67zgX!=Rd8tad%v6a5X4&%}kYy=$Z^=GDLYgBZ+;B(^ z&8sMQgqLP677sxHr44D%>BtAv%+wxU%~Zid%ka=5054tLt)|x7!))6gZ=l_!m-)Qw z2=`qf*v0YWV3xj!D%qs&quyXOkJ1UaTelk0PATDKh0OU;$hAE_VE}T!+BrYc7CFll z&0Xd@o0)vs^(+5Woxdh4uYzRiknaWmyX|1z;+qhZer54SA7$7aAjK09$j8J+&9};G z_f!Fe$-W|uJm$d6-0f52GB<%M?o@4rTR8lfpAAMB&|eJj{SAzlm68UXC&_y9z^H~>iA0e~C>Fn~={0RZ)V{~P50 d(wW0Lv-y*A*FK-s$qgR9OY2{tqh82QdHu literal 0 HcmV?d00001 diff --git a/client/ui/netbird-systemtray-update-disconnected-macos.png b/client/ui/netbird-systemtray-update-disconnected-macos.png new file mode 100644 index 0000000000000000000000000000000000000000..8b190034eac6588e58a36365880d83d9ea8d8db0 GIT binary patch literal 3816 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 literal 7966 zcmcI}hd)*SAOE@c;$HjOLR=zyRkF7vGCpQeE-HkO%`NvD8I`DPWrXaNJ+6C;RQAkX z$-MTuxVXRD_xCUSe)m4^x#w}t>+yKM*7G$107Uuz`+xuh022TJPRf0(naNE?IvzU8 zE#oZ%J@bEW|NUSzl)rmEh0XxL-*-z-+cN0G#XQ=p)x#YXLJ=Tak*X^&S zLT3(GG2k&2^q0Lb?fQ&Pxt?@mp200u-xIm!-#*yCz1_Ww-J3)R1K~^aRiWT>-=%x4 zAnpgR2u^4YDCxQY(9O5O635?TIjVwbR=4D|y|{ z`>Z~qZ^vlGiH6y_iVT=;LMp=%Ut1(*Kc_XUgb%oV_x*z+EWn?H&uvOX^=OJ1{4&Pc z>cnURbAUrZzHJ%?z8~wGmQxl{)|hEbxEtxK&Wu4>LaCdK+^~ z9Jl%VDv|b6&bJC>;0|J!xPLQ0@g}Zy&^MQunW&5H<1^w4?Lt0XD)~0TkDtlz%b)oR zJ?ED_zq$#ML$dR<{*4mgre#PvzW@dH*cG2Cz`ZxjX7QG*S-yar>Faogzy!FcNI&m> zVH`VRLK};9K1vNKPWQc)$K(_+hgQEMuIW1m>f4~L;TK)4FXHI#{ zL3vyNYGK_Eu8hk|%&YOFuK>U9#Od2UvQxJzY#&7Zu4aH|^W(kr-Q#D9& zCSd93-#cr0R3^LF*7TT_-5i&A^n-~Cb|_HXLa#&oO57><3k=ubK0OHrU~hhKG9 zACW{i={8Z3xP9{Q&J%8d3!DAv6cP1RfOh};TL}sBLNB$2^mY6IOyU=a)@zIsz#b~*FAj=U_~X|$woQty zN<5|)uhvUU(h@{;&+_W?L`n${>LqgMJbPTw%yIn~73z5^sHJBSeL^EM8JfiUJ8QcC(}lAEM&8gGw(D*P3tGH ze+mm&jXD=o40v?!(p){@e7Yid`%S*apRM2^rL;p*hs$5$Q-QypEq}VJB1_D9uChq` zjBJcUTep}63Me{vb&{&3H~&nrSg)0M9P>0R;xzL-eo94tNnI#iWq*BVwB(#`j?h_F zv}XE>Ivl^&)Kv@#L&k${zs?TG0o!?UWc!FeiUp#0|_TCitkz}2t>h^qVn7F~J z;g?E>TiFo3oBnM_6y^@}&;Og`r2ve=VNB8$WI*D5s2s&U!ZAU54IS`=>v{5idE`cM z{v&z;a5?`PFSqyc)vg72;eD#SQLhU0`0zyccXgfiIEp)FSW~G?p`Bv9pl%j2DTF({ zQd{?1LLr0_ckL{uYP?CBtZJPUZ9oILJt1*dx1PB(yw>k31nBpNjOR-!!G@jdyHhT5 zxP$puHFtO@g4{vgk0|#ljWFY*B5qDGu5^z%uO}aWY1{S0|EjWlDX_!m8RdxBL%*7Z zVa$ztj9p*VYrhh+kp1l0sJ>y->0>pAYMeskwJeRq%59aEm0Ek80^TlXOq@8|_O&lx zMCjL|CMgj@3W~RY$UgLK(!dMCO+sX%CnXOz_q-}f9d}L`R~kRubopMVqh~SVaZu~W z(Cjs*zA#_Y(h#Xi2;{@z@@p@Y?TAIf#&d;t4=!r&i(sw+25n;}q3L2KdjGM9cH}>k zi^&R<{d{JqEJ52L2$!LVa^te*E|Srh&A`L7Sou2QQF3I1ME)sKJkDM3k8j+Hk>aaj zin#;c9wUZ7B0{bq3x8)ATgsFZXJ0O)WB9=HRY7M_`6zUWZLYNID`+!H4H@IFZ@7@o zb-=RzQ?`1B_7h}KuDHBJTgw*qbSfTgVRb`WnQFMcwJB=-9VsICZA?@U>KkqbGoFN_ zanOf7y}RhMe!Ou2rB#}%d}3*})b*yg1njHL;=kI+m+a|$4|~EjbSR$gQcaQbGIhJ>1utOkb% z6OBEL7%r~y2F+GSbpJAesnLFWa7?r1{GdKcE`vF8T#hDO76f?1f;N8JD8B7l)y|^1 zW{gOW$+xq$Yq{H{A=(VrmSs?*&F(trtLO#8su8bWh%h{($IRxPt8)(2uxo~QKG!xO zakx{swJY5i?ddTlFD=ovuBMqfsc(>Iv{rhMt8EUTNTdFpH=PU6R%!q*IG2=Na0XP{ zYe`)|ca6x9KdeVaStP97gioXD;rdn^97j9=<^q`U?N4Y+ME}K-uDg>-p;Nzkp4_Vc z{ve{fhd?$7(EW2eru@)a8sp6r-|yNlYCeYofR?y1X>I0HT5Z?3y8PUoLRBJZi36QS z!y798C>)V+Vsi71ddhW%g`iMJn6In9as#!d4*bs~>*$p0>Cn}z_U$w^<}lf{>>Y@f zsIdNa+K2WJ>kPYk;8hU2w<*4HY8utxzhI5{@%vg>5Ng-f;z66%NW}B3P79z{sGE2+ z!m6bDe4!I}$Tzk{GVsg>0jZSKDBvv(LWT1jQLr;scNE=YZ0uz{azrR-_8I|Gy*XAl z;|`)!qGs*o*`LoM5yBJiQ?f!fzke;(h^aWqxYQDNb43KlRG=5P-1ZzYs&x$zkI3q_ z{&078mW(2QQ%74^6GItDBwW`5UfD75OK)b#J) zC%QkT&y~li!)~P#_U=Bq04!HdPD<%i1q3e+WC*jI*VGU$?xmATZytRV)MRZctY$^Z)=pFf}Ek zwDukUs2BNszI6P)xAo9@VJndMnj2$MudB8CkIe-0bCs1nm?3;9w5P0Rl@e4uPu+YJ zs1lh0Kxq*D$5?zZT083|+&abPIrc>H+I7Turr-4ymzh0cGupaTPn7E31#X5L6kxqQ zXR8>ogHiH8g~E9xqUQU=hmlpXW#&986zPB9F>B+Gf*Ats|lWZD#h0XbbQL*+Y!TB=k2qgNsexVq^B#gg6LAV0(IroE$&>jAqR{nUUN ziTZ|hN_8mnX?})X`fT-?KI8J&F4TyF!^4mzw#gD{-3x%$MieHq%kk{6$?FHj7ngoJ zlOX7&?AiDHA33Wls$EE}EX`i#0pgWbwboWYw+cwtD7Gb;VD}r!6w~gLWz|$ftUso# zuOk?qFlT4?vlYIu1yjPz<8(@xs>bcS+#*>8bo2`4^g{uMC@pDr_Pb4KI|o6OWMRWh z%xdr;7%y$MnwXlzGz!u(CVNr42?0QLF`VT^fZ)qn!kL0Gf+zmG^LIwKq>(GG!(aE) z0 zE7!y1x4pki?3V>3J|0>-1nN9Uk)zw5|eIUD3B;) zj5+eZ9I8qwT3ns>!Il*g>}F}QsCgyPX7`*>v^hkDH>{j9yBcHw+v}q{4`Tkgf?Q^3t>*TwDzp6tR^EWI@_Xd_ z(uXt6-dw5oL7%_i^F_RR`=Dv7-zAxed}GK%XxS13FjJ%o4{9$z#lqi**b~SYe;Kxma=< z^T%gKwcEtwO@8=)jKlv#0`=!~N+b*_PC!nx1b6YsDJA%IuRc*@5}FapO!x#flhX*9 zchTF|>tD`U_PIl`ojSn7!#ntcyd(NM2N!{`U@{;m6`Dn`&5I|*NCLHr4giB9| z1w|yk>?M^<9MLB-fnqlY%f9*Z0vMd2w<=*PgzS`lxu5e%?F_`MhJ;=M)$*dhjzck` z3))%VFSmC{nD}1=Fq(o6$;Z3osu^m%KgDH9XdSFM@@z8v!fK$~U%?wLP#E?*u8j1* zIEBx)p}uh%+GHx8Ga%rB4Xd2YwRoCW{olOj!R2@9jSdFNBGn*1AUK*tQy%;u1)Nuq zi!ec{KyrbLMr=_48NFop(%x8dHLm8S+$GkjN|o99&36xNxQSjnN`PU*T`Sk;{*1)U#i=8)AJ>e#CM1rfhnr5a zCN?*gUHrUKb}uVXbJ224|9J`|&RbjMAPUYls~hKT<&em|`g6Y`1GCVZDwP|=C3G}r z3w^jnF*Sy27Hnz$#7YpCwOM_kE#Z&`$X6Lc+neO~l&59{)5%C@@S<80hB2Ghy)-6s z9y+Q6P&_9SF^k)W6&$KL?IQU$>Su~IG6OyH%;r1SN#1B1aMW;n*<+?HZ9?OqL%s=M z!o2(tiC^25V9*WmH>~@+IYXLSb4R@+wGtK^?<5U>u-Y!N?r3Am@S%^093NT$D)A<; zLb(l_(yF3?lbX+*9z>j~+vX<8DdiZwsNYwTFnr2fL#F~_0=i1AId(~_8c80LRXw}X zz`g@GyMNoI^}%^lGEb;{eg_Cfi-F*iFP(|nPfAdQi|y5N2!;2E;9E8JFBO5_&$iBP zyMHu)l_neyQu4_ox4SR8(NgXBWf|rr1`rkfLZ0$3`qie6__cgkvWPPWC7{2tK{1d; zx|c_1egcw_@WW)mrREp*l77b=?(j@PTt<%K@e|)zZ#+4a2`HemdO@cOiwJ6p=NOb* z-*n$T<;*Uj)U!wHD{k^A^-Iju_KvBLY@UC~M%v>NQ*pP=nBVnu8_fV@&hXK(Ci7)> zWuVym@0joEJsD!?<_tF5G+!#G=wPGlhbPV|0?m2*g=1}~j*b{>ET6H5X}AIQrWAM7 zhPGDUhhPjZTI6G#Gt5tc6nL8nBSGm?GLyBUIb#t;4lsi@GpDn=F1hKXKURsu6d0dy zf%yQYILb^0zI}V1__l*acw!`dNJvP;d9=;8|B~1(#?sNb#D2 zP`cTt9`Wy6H#Djx@l~22+5G9?1Q&h9dL{u9n_NK2NniD+?GL&YH(E6HHZD-6Cct%0 z8!6Yv?$ki3RM}CmVMgqDNNu~ov_{xwAuYL1DNL-5u6h`P!{R|lSo-pPI;{7o2i_zE zoF9jC7%s2}<{rxrGvGlElmD9Vm;kGg`*v!@I}4%Ho7 z^*XY6|8~$^4C3kFnmY#!gaT=8LNT*;4vLbvN1{{`3ZXcoXTdc{C(s;F`N&7=ATA7N z7|s+nuQ*#1+S=3%g12|pZLTauxLE6Cn=feYVaUv4t9wp#3Ys%*Acp};lN3!Qg*<|FRt)?=Afkf#06Vx&L&$;b=Nm$l z=K6#c{O#Mf{E9|3xKc^`OV{@!aI>-L|btiy4%>QA;!A7DpZV zy^)ZFd~jjjd{y8nz`o-)(f8(ava@M19m@bnwIZwc-mS*q)7MOVw%&Ve?z1b>DelaP zwEj#Op~cQ;)H(TO98aDGDux+3RD3)D)S-ti(|in6-Cw0_DqIyj=F17l66$rqO^`=N z*}=KzBF6@S(Q{|WgZLv9%|jf&9^PI zfqWm$ynpHtSbf*BCYfKP)q8Xbaj^4bZtxwm`z0!-InuS}apdShgtlcG1y;$gwQ+BC zWo$-Fdxroi=>M4N{uPMu{+u3RtI+)ADVL*pNgR9|8>q`YUA&VaTV(MM8N%&1wX>qx z2IUeOpOzF;TAoex&i-oY$mlW(lm%MxduFyI3t{%Ueq>`-{{TqJ+|2!=W0m6;0B7|c z?j`I!0LdzAAofN%FOk2mT>(lprf)tH*7Ki?iG4GX$vUN*GG%}9r&^VRKa0(M8q696 z1Xg#A5YF{n0MP%GLBU+5#rcAg%iMkc2j--rQ z0Z0EWNy1*hz0J(pDQ}svJ@sJ2FH;Ayg(bjtNw*EW#(?UUsGyeL;O{*M{gJhn`t_!B zYJWE1-*n#0wLXiB;?{3;w0Jsr>zJKq=(bBSD!t>YCtSnE031H?f8D;$oz}o%3w`>B z>yR!Q{F96iuDhLj_j>%a-|t_%1R)??YL6o_xz2jh>V@!j)CNc!7&DGg#!=V z{k2Bm47UL#s?$evcNF3T3U`2i<9p!k+2e8^r~2%e1v`+h*)}}L881L%FJZILSDwy< zvCTZJZgYkywQ}J3)BA{2N>nZWOsJ{xF7s+og6y8v&`@Z=SdoNqSZ*8cbdl{1Q@h|? zt*^<=)2^a%pzsGJA*@?+T*-U(eE=+&twhDihs{cInG;du)ilcm$;l^&G}%MXB`5*C zzu%#V>2|l=>sN0&AOI66@X;wYnAYnM|Y5q#j0 zyFX12;zD-j6}SLLslCN|9c>m#!33np%4r%c%GEJsr}NSrM}$fDGB5e_4fPT$3sCmN z&~rz}n}^gX&%hhEv7H%$be8&bM8N@b&1J1nnSOC{_* zxYMomm0yJ(!W`j6Tjw;hwe|UHCWItC>fu-H3`XE)X3wLNRa``=7{t*_IqVqSSiSQ_ zfmCLJX=CQmgE7F-k0JpwME<+W#){IVp^WjfSazvIi)^f37@Aa(XX;7}9 z<|0i^gOy)wu}Kay%K|?Gi7pYEAK~^aOJ8{Zkz?{|y^pQwE17L7t zmVpEpY?zbp7kVv4SbSJNWhl6C$r&L}9~%$qE}6jA3;BWY5cJn7UNA72;Ir~#sGy)l zrNHMirAIjsCY}LWTvRIBV~nxF-IGx!uLRzQjrT9_k;&{!pYnroT!bKTzyG-}q-9=I zuIrUiLv`qn->AGq5>oBCZJOs(4MbhaS#x(%hU3~d8%awFZOA;Lc$|{D97U;kc!Vr| zpm&Ex)ip$x%nCWdEN}OouXZc!97g8bg7p%gJa#__kV~)w6z)(sQAavg{8{{vbfHH!cM diff --git a/client/ui/netbird-systemtray-update-disconnected.png b/client/ui/netbird-systemtray-update-disconnected.png index 3fbe88953aac6d630a053a45c5d8a51ea2a0e4e3..3adc3903436f78a5b7eab298c5e78d52a51d2fa7 100644 GIT binary patch literal 5298 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 Date: Tue, 18 Feb 2025 10:45:41 +0100 Subject: [PATCH 65/92] [misc] Run management benchmark jobs on file changes (#3343) They will always run on Main --- .github/workflows/golang-test-linux.yml | 12 ++++++++++++ management/README.md | 1 - 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/.github/workflows/golang-test-linux.yml b/.github/workflows/golang-test-linux.yml index a4a3da66c..b1ec0a896 100644 --- a/.github/workflows/golang-test-linux.yml +++ b/.github/workflows/golang-test-linux.yml @@ -13,10 +13,19 @@ concurrency: jobs: build-cache: runs-on: ubuntu-22.04 + outputs: + management: ${{ steps.filter.outputs.management }} steps: - name: Checkout code uses: actions/checkout@v4 + - uses: dorny/paths-filter@v3 + id: filter + with: + filters: | + management: + - 'management/**' + - name: Install Go uses: actions/setup-go@v5 with: @@ -198,6 +207,7 @@ jobs: benchmark: needs: [ build-cache ] + if: ${{ needs.build-cache.outputs.management == 'true' || github.event_name != 'pull_request' }} strategy: fail-fast: false matrix: @@ -258,6 +268,7 @@ jobs: api_benchmark: needs: [ build-cache ] + if: ${{ needs.build-cache.outputs.management == 'true' || github.event_name != 'pull_request' }} strategy: fail-fast: false matrix: @@ -318,6 +329,7 @@ jobs: api_integration_test: needs: [ build-cache ] + if: ${{ needs.build-cache.outputs.management == 'true' || github.event_name != 'pull_request' }} strategy: fail-fast: false matrix: diff --git a/management/README.md b/management/README.md index f0eb0cb70..1122a9e76 100644 --- a/management/README.md +++ b/management/README.md @@ -111,4 +111,3 @@ Generate gRpc code: #!/bin/bash protoc -I proto/ proto/management.proto --go_out=. --go-grpc_out=. ``` - From 50926bdbb4441f4b48d9ab341c41f203e082fc69 Mon Sep 17 00:00:00 2001 From: hakansa <43675540+hakansa@users.noreply.github.com> Date: Tue, 18 Feb 2025 13:17:34 +0300 Subject: [PATCH 66/92] [client] [ui] issue when changing setting in GUI while peer session is expired (#3334) * [client] [ui] fix issue when changing settings in GUI while peer session is expired --- client/ui/client_ui.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/client/ui/client_ui.go b/client/ui/client_ui.go index 1aa61a2b2..30fb8d764 100644 --- a/client/ui/client_ui.go +++ b/client/ui/client_ui.go @@ -732,7 +732,6 @@ func (s *serviceClient) onTrayReady() { } if err := s.updateConfig(); err != nil { log.Errorf("failed to update config: %v", err) - return } case <-s.mAutoConnect.ClickedCh: if s.mAutoConnect.Checked() { @@ -742,7 +741,6 @@ func (s *serviceClient) onTrayReady() { } if err := s.updateConfig(); err != nil { log.Errorf("failed to update config: %v", err) - return } case <-s.mEnableRosenpass.ClickedCh: if s.mEnableRosenpass.Checked() { @@ -752,7 +750,6 @@ func (s *serviceClient) onTrayReady() { } if err := s.updateConfig(); err != nil { log.Errorf("failed to update config: %v", err) - return } case <-s.mAdvancedSettings.ClickedCh: s.mAdvancedSettings.Disable() @@ -967,17 +964,20 @@ func (s *serviceClient) updateConfig() error { // 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(s.ctx, loginRequest) + _, err = client.Login(ctx, loginRequest) if err != nil { return err } - _, err = client.Up(s.ctx, &proto.UpRequest{}) + _, err = client.Up(ctx, &proto.UpRequest{}) if err != nil { return err } From c974c12d652f276492f0e66f49aff08239cc5b2f Mon Sep 17 00:00:00 2001 From: Pascal Fischer <32096965+pascal-fischer@users.noreply.github.com> Date: Tue, 18 Feb 2025 14:23:34 +0100 Subject: [PATCH 67/92] [signal] Fix registry not found (#3342) --- signal/server/signal.go | 1 + 1 file changed, 1 insertion(+) diff --git a/signal/server/signal.go b/signal/server/signal.go index 05cc43276..3cae7e860 100644 --- a/signal/server/signal.go +++ b/signal/server/signal.go @@ -160,6 +160,7 @@ func (s *Server) forwardMessageToPeer(ctx context.Context, msg *proto.EncryptedM s.metrics.MessageForwardFailures.Add(ctx, 1, metric.WithAttributes(attribute.String(labelType, labelTypeNotConnected))) log.Debugf("message from peer [%s] can't be forwarded to peer [%s] because destination peer is not connected", msg.Key, msg.RemoteKey) // todo respond to the sender? + return } s.metrics.GetRegistrationDelay.Record(ctx, float64(time.Since(getRegistrationStart).Nanoseconds())/1e6, metric.WithAttributes(attribute.String(labelType, labelTypeStream), attribute.String(labelRegistrationStatus, labelRegistrationFound))) From 2a864832c6820b2bf6c64d8b79fc83cc10950687 Mon Sep 17 00:00:00 2001 From: Pascal Fischer <32096965+pascal-fischer@users.noreply.github.com> Date: Tue, 18 Feb 2025 15:24:17 +0100 Subject: [PATCH 68/92] [management] remove gorm preparestmt from all DB connections (#3292) --- management/server/account_test.go | 16 ++++++++-------- management/server/geolocation/database.go | 1 - management/server/geolocation/store.go | 3 +-- management/server/migration/migration_test.go | 4 +--- management/server/store/sql_store.go | 13 ++++--------- management/server/store/store.go | 2 +- 6 files changed, 15 insertions(+), 24 deletions(-) diff --git a/management/server/account_test.go b/management/server/account_test.go index 0a7f9119b..eb36dbd84 100644 --- a/management/server/account_test.go +++ b/management/server/account_test.go @@ -3018,11 +3018,11 @@ func BenchmarkSyncAndMarkPeer(b *testing.B) { minMsPerOpCICD float64 maxMsPerOpCICD float64 }{ - {"Small", 50, 5, 1, 5, 3, 19}, - {"Medium", 500, 100, 7, 22, 10, 90}, - {"Large", 5000, 200, 65, 110, 60, 240}, + {"Small", 50, 5, 1, 5, 3, 24}, + {"Medium", 500, 100, 7, 22, 10, 135}, + {"Large", 5000, 200, 65, 110, 60, 320}, {"Small single", 50, 10, 1, 4, 3, 80}, - {"Medium single", 500, 10, 7, 13, 10, 37}, + {"Medium single", 500, 10, 7, 13, 10, 43}, {"Large 5", 5000, 15, 65, 80, 60, 220}, } @@ -3087,8 +3087,8 @@ func BenchmarkLoginPeer_ExistingPeer(b *testing.B) { maxMsPerOpCICD float64 }{ {"Small", 50, 5, 2, 10, 3, 35}, - {"Medium", 500, 100, 5, 40, 20, 110}, - {"Large", 5000, 200, 60, 100, 120, 260}, + {"Medium", 500, 100, 5, 40, 20, 140}, + {"Large", 5000, 200, 60, 100, 120, 320}, {"Small single", 50, 10, 2, 10, 5, 40}, {"Medium single", 500, 10, 5, 40, 10, 60}, {"Large 5", 5000, 15, 60, 100, 60, 180}, @@ -3163,9 +3163,9 @@ func BenchmarkLoginPeer_NewPeer(b *testing.B) { }{ {"Small", 50, 5, 7, 20, 10, 80}, {"Medium", 500, 100, 5, 40, 30, 140}, - {"Large", 5000, 200, 80, 120, 140, 300}, + {"Large", 5000, 200, 80, 120, 140, 390}, {"Small single", 50, 10, 7, 20, 10, 80}, - {"Medium single", 500, 10, 5, 40, 20, 60}, + {"Medium single", 500, 10, 5, 40, 20, 85}, {"Large 5", 5000, 15, 80, 120, 80, 200}, } diff --git a/management/server/geolocation/database.go b/management/server/geolocation/database.go index 21ae93b9d..97ab398fb 100644 --- a/management/server/geolocation/database.go +++ b/management/server/geolocation/database.go @@ -123,7 +123,6 @@ func importCsvToSqlite(dataDir string, csvFile string, geonamesdbFile string) er db, err := gorm.Open(sqlite.Open(path.Join(dataDir, geonamesdbFile)), &gorm.Config{ Logger: logger.Default.LogMode(logger.Silent), CreateBatchSize: 1000, - PrepareStmt: true, }) if err != nil { return err diff --git a/management/server/geolocation/store.go b/management/server/geolocation/store.go index 1f94bf47e..5af8276b5 100644 --- a/management/server/geolocation/store.go +++ b/management/server/geolocation/store.go @@ -132,8 +132,7 @@ func connectDB(ctx context.Context, filePath string) (*gorm.DB, error) { } db, err := gorm.Open(sqlite.Open(storeStr), &gorm.Config{ - Logger: logger.Default.LogMode(logger.Silent), - PrepareStmt: true, + Logger: logger.Default.LogMode(logger.Silent), }) if err != nil { return nil, err diff --git a/management/server/migration/migration_test.go b/management/server/migration/migration_test.go index a645ae325..e907d6853 100644 --- a/management/server/migration/migration_test.go +++ b/management/server/migration/migration_test.go @@ -21,9 +21,7 @@ import ( func setupDatabase(t *testing.T) *gorm.DB { t.Helper() - db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{ - PrepareStmt: true, - }) + db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{}) require.NoError(t, err, "Failed to open database") return db diff --git a/management/server/store/sql_store.go b/management/server/store/sql_store.go index 5c4ddf666..947694420 100644 --- a/management/server/store/sql_store.go +++ b/management/server/store/sql_store.go @@ -969,7 +969,7 @@ func NewSqliteStore(ctx context.Context, dataDir string, metrics telemetry.AppMe } file := filepath.Join(dataDir, storeStr) - db, err := gorm.Open(sqlite.Open(file), getGormConfig(SqliteStoreEngine)) + db, err := gorm.Open(sqlite.Open(file), getGormConfig()) if err != nil { return nil, err } @@ -979,7 +979,7 @@ func NewSqliteStore(ctx context.Context, dataDir string, metrics telemetry.AppMe // NewPostgresqlStore creates a new Postgres store. func NewPostgresqlStore(ctx context.Context, dsn string, metrics telemetry.AppMetrics) (*SqlStore, error) { - db, err := gorm.Open(postgres.Open(dsn), getGormConfig(PostgresStoreEngine)) + db, err := gorm.Open(postgres.Open(dsn), getGormConfig()) if err != nil { return nil, err } @@ -989,7 +989,7 @@ func NewPostgresqlStore(ctx context.Context, dsn string, metrics telemetry.AppMe // NewMysqlStore creates a new MySQL store. func NewMysqlStore(ctx context.Context, dsn string, metrics telemetry.AppMetrics) (*SqlStore, error) { - db, err := gorm.Open(mysql.Open(dsn+"?charset=utf8&parseTime=True&loc=Local"), getGormConfig(MysqlStoreEngine)) + db, err := gorm.Open(mysql.Open(dsn+"?charset=utf8&parseTime=True&loc=Local"), getGormConfig()) if err != nil { return nil, err } @@ -997,15 +997,10 @@ func NewMysqlStore(ctx context.Context, dsn string, metrics telemetry.AppMetrics return NewSqlStore(ctx, db, MysqlStoreEngine, metrics) } -func getGormConfig(engine Engine) *gorm.Config { - prepStmt := true - if engine == SqliteStoreEngine { - prepStmt = false - } +func getGormConfig() *gorm.Config { return &gorm.Config{ Logger: logger.Default.LogMode(logger.Silent), CreateBatchSize: 400, - PrepareStmt: prepStmt, } } diff --git a/management/server/store/store.go b/management/server/store/store.go index 6d3a409e6..29ed22fa5 100644 --- a/management/server/store/store.go +++ b/management/server/store/store.go @@ -328,7 +328,7 @@ func NewTestStoreFromSQL(ctx context.Context, filename string, dataDir string) ( } file := filepath.Join(dataDir, storeStr) - db, err := gorm.Open(sqlite.Open(file), getGormConfig(kind)) + db, err := gorm.Open(sqlite.Open(file), getGormConfig()) if err != nil { return nil, nil, err } From 27b3891b14354f9c4bce3e9050fc9c9e43fcd431 Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Wed, 19 Feb 2025 12:35:30 +0100 Subject: [PATCH 69/92] [client] Set up local dns policy additionally if a gpo policy is detected (#3336) --- client/internal/dns/host_windows.go | 30 ++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/client/internal/dns/host_windows.go b/client/internal/dns/host_windows.go index 0cd078472..58b0a14de 100644 --- a/client/internal/dns/host_windows.go +++ b/client/internal/dns/host_windows.go @@ -131,11 +131,30 @@ func (r *registryConfigurator) addDNSSetupForAll(ip string) error { func (r *registryConfigurator) addDNSMatchPolicy(domains []string, ip string) error { // if the gpo key is present, we need to put our DNS settings there, otherwise our config might be ignored // see https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-gpnrpt/8cc31cb9-20cb-4140-9e85-3e08703b4745 - policyPath := dnsPolicyConfigMatchPath if r.gpo { - policyPath = gpoDnsPolicyConfigMatchPath + if err := r.configureDNSPolicy(gpoDnsPolicyConfigMatchPath, domains, ip); err != nil { + return fmt.Errorf("configure GPO DNS policy: %w", err) + } + + if err := r.configureDNSPolicy(dnsPolicyConfigMatchPath, domains, ip); err != nil { + return fmt.Errorf("configure local DNS policy: %w", err) + } + + if err := refreshGroupPolicy(); err != nil { + log.Warnf("failed to refresh group policy: %v", err) + } + } else { + if err := r.configureDNSPolicy(dnsPolicyConfigMatchPath, domains, ip); err != nil { + return fmt.Errorf("configure local DNS policy: %w", err) + } } + log.Infof("added %d match domains. Domain list: %s", len(domains), domains) + return nil +} + +// configureDNSPolicy handles the actual configuration of a DNS policy at the specified path +func (r *registryConfigurator) configureDNSPolicy(policyPath string, domains []string, ip string) error { if err := removeRegistryKeyFromDNSPolicyConfig(policyPath); err != nil { return fmt.Errorf("remove existing dns policy: %w", err) } @@ -162,13 +181,6 @@ func (r *registryConfigurator) addDNSMatchPolicy(domains []string, ip string) er return fmt.Errorf("set %s: %w", dnsPolicyConfigConfigOptionsKey, err) } - if r.gpo { - if err := refreshGroupPolicy(); err != nil { - log.Warnf("failed to refresh group policy: %v", err) - } - } - - log.Infof("added %d match domains. Domain list: %s", len(domains), domains) return nil } From 7e6beee7f6bb18865c544c959f8dd5defe4b3762 Mon Sep 17 00:00:00 2001 From: Pascal Fischer <32096965+pascal-fischer@users.noreply.github.com> Date: Wed, 19 Feb 2025 19:13:45 +0100 Subject: [PATCH 70/92] [management] optimize test execution (#3204) --- .github/workflows/golang-test-linux.yml | 150 ++- .golangci.yaml | 2 +- management/client/client_test.go | 5 +- management/server/dns_test.go | 14 +- management/server/group_test.go | 6 +- management/server/management_suite_test.go | 13 - management/server/management_test.go | 1046 ++++++++++++-------- management/server/nameserver_test.go | 13 +- management/server/route_test.go | 5 +- management/server/store/sql_store_test.go | 244 +++-- management/server/store/store.go | 152 ++- management/server/testutil/store.go | 4 +- management/server/types/user.go | 2 +- relay/client/dialer/ws/ws.go | 2 +- relay/server/listener/ws/conn.go | 2 +- relay/server/listener/ws/listener.go | 2 +- 16 files changed, 1019 insertions(+), 643 deletions(-) delete mode 100644 management/server/management_suite_test.go diff --git a/.github/workflows/golang-test-linux.yml b/.github/workflows/golang-test-linux.yml index b1ec0a896..efe1a2654 100644 --- a/.github/workflows/golang-test-linux.yml +++ b/.github/workflows/golang-test-linux.yml @@ -1,4 +1,4 @@ -name: Test Code Linux +name: Linux on: push: @@ -12,6 +12,7 @@ concurrency: jobs: build-cache: + name: "Build Cache" runs-on: ubuntu-22.04 outputs: management: ${{ steps.filter.outputs.management }} @@ -47,7 +48,6 @@ jobs: key: ${{ runner.os }}-gotest-cache-${{ hashFiles('**/go.sum') }} restore-keys: | ${{ runner.os }}-gotest-cache-${{ hashFiles('**/go.sum') }} - - name: Install dependencies if: steps.cache.outputs.cache-hit != 'true' @@ -98,6 +98,7 @@ jobs: run: CGO_ENABLED=1 GOARCH=386 go build -o relay-386 . test: + name: "Client / Unit" needs: [build-cache] strategy: fail-fast: false @@ -143,9 +144,116 @@ jobs: run: git --no-pager diff --exit-code - name: Test - run: CGO_ENABLED=1 GOARCH=${{ matrix.arch }} CI=true go test -tags devcert -exec 'sudo' -timeout 10m -p 1 $(go list ./... | grep -v /management) + run: CGO_ENABLED=1 GOARCH=${{ matrix.arch }} CI=true go test -tags devcert -exec 'sudo' -timeout 10m -p 1 $(go list ./... | grep -v -e /management -e /signal -e /relay) + + test_relay: + name: "Relay / Unit" + needs: [build-cache] + strategy: + fail-fast: false + matrix: + arch: [ '386','amd64' ] + runs-on: ubuntu-22.04 + steps: + - name: Install Go + uses: actions/setup-go@v5 + with: + go-version: "1.23.x" + cache: false + + - name: Checkout code + uses: actions/checkout@v4 + + - name: Get Go environment + run: | + echo "cache=$(go env GOCACHE)" >> $GITHUB_ENV + echo "modcache=$(go env GOMODCACHE)" >> $GITHUB_ENV + + - name: Cache Go modules + uses: actions/cache/restore@v4 + with: + path: | + ${{ env.cache }} + ${{ env.modcache }} + key: ${{ runner.os }}-gotest-cache-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-gotest-cache- + + - name: Install dependencies + run: sudo apt update && sudo apt install -y -q libgtk-3-dev libayatana-appindicator3-dev libgl1-mesa-dev xorg-dev gcc-multilib libpcap-dev + + - name: Install 32-bit libpcap + if: matrix.arch == '386' + run: sudo dpkg --add-architecture i386 && sudo apt update && sudo apt-get install -y libpcap0.8-dev:i386 + + - name: Install modules + run: go mod tidy + + - name: check git status + run: git --no-pager diff --exit-code + + - name: Test + run: | + CGO_ENABLED=1 GOARCH=${{ matrix.arch }} \ + go test \ + -exec 'sudo' \ + -timeout 10m ./signal/... + + test_signal: + name: "Signal / Unit" + needs: [build-cache] + strategy: + fail-fast: false + matrix: + arch: [ '386','amd64' ] + runs-on: ubuntu-22.04 + steps: + - name: Install Go + uses: actions/setup-go@v5 + with: + go-version: "1.23.x" + cache: false + + - name: Checkout code + uses: actions/checkout@v4 + + - name: Get Go environment + run: | + echo "cache=$(go env GOCACHE)" >> $GITHUB_ENV + echo "modcache=$(go env GOMODCACHE)" >> $GITHUB_ENV + + - name: Cache Go modules + uses: actions/cache/restore@v4 + with: + path: | + ${{ env.cache }} + ${{ env.modcache }} + key: ${{ runner.os }}-gotest-cache-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-gotest-cache- + + - name: Install dependencies + run: sudo apt update && sudo apt install -y -q libgtk-3-dev libayatana-appindicator3-dev libgl1-mesa-dev xorg-dev gcc-multilib libpcap-dev + + - name: Install 32-bit libpcap + if: matrix.arch == '386' + run: sudo dpkg --add-architecture i386 && sudo apt update && sudo apt-get install -y libpcap0.8-dev:i386 + + - name: Install modules + run: go mod tidy + + - name: check git status + run: git --no-pager diff --exit-code + + - name: Test + run: | + CGO_ENABLED=1 GOARCH=${{ matrix.arch }} \ + go test \ + -exec 'sudo' \ + -timeout 10m ./signal/... test_management: + name: "Management / Unit" needs: [ build-cache ] strategy: fail-fast: false @@ -203,9 +311,15 @@ jobs: run: docker pull mlsmaycon/warmed-mysql:8 - name: Test - run: CGO_ENABLED=1 GOARCH=${{ matrix.arch }} NETBIRD_STORE_ENGINE=${{ matrix.store }} CI=true go test -tags=devcert -p 1 -exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE' -timeout 10m $(go list ./... | grep /management) + run: | + CGO_ENABLED=1 GOARCH=${{ matrix.arch }} \ + NETBIRD_STORE_ENGINE=${{ matrix.store }} \ + go test -tags=devcert \ + -exec "sudo --preserve-env=CI,NETBIRD_STORE_ENGINE" \ + -timeout 10m ./management/... benchmark: + name: "Management / Benchmark" needs: [ build-cache ] if: ${{ needs.build-cache.outputs.management == 'true' || github.event_name != 'pull_request' }} strategy: @@ -264,9 +378,15 @@ jobs: run: docker pull mlsmaycon/warmed-mysql:8 - name: Test - run: CGO_ENABLED=1 GOARCH=${{ matrix.arch }} NETBIRD_STORE_ENGINE=${{ matrix.store }} CI=true go test -tags devcert -run=^$ -bench=. -exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE' -timeout 20m ./... + run: | + CGO_ENABLED=1 GOARCH=${{ matrix.arch }} \ + NETBIRD_STORE_ENGINE=${{ matrix.store }} CI=true \ + go test -tags devcert -run=^$ -bench=. \ + -exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE' \ + -timeout 20m ./... api_benchmark: + name: "Management / Benchmark (API)" needs: [ build-cache ] if: ${{ needs.build-cache.outputs.management == 'true' || github.event_name != 'pull_request' }} strategy: @@ -323,11 +443,19 @@ jobs: - name: download mysql image if: matrix.store == 'mysql' run: docker pull mlsmaycon/warmed-mysql:8 - + - name: Test - run: CGO_ENABLED=1 GOARCH=${{ matrix.arch }} NETBIRD_STORE_ENGINE=${{ matrix.store }} CI=true go test -run=^$ -tags=benchmark -bench=. -exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE' -timeout 30m $(go list -tags=benchmark ./... | grep /management) + run: | + CGO_ENABLED=1 GOARCH=${{ matrix.arch }} \ + NETBIRD_STORE_ENGINE=${{ matrix.store }} CI=true \ + go test -tags=benchmark \ + -run=^$ \ + -bench=. \ + -exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE' \ + -timeout 20m ./management/... api_integration_test: + name: "Management / Integration" needs: [ build-cache ] if: ${{ needs.build-cache.outputs.management == 'true' || github.event_name != 'pull_request' }} strategy: @@ -375,9 +503,15 @@ jobs: run: git --no-pager diff --exit-code - name: Test - run: CGO_ENABLED=1 GOARCH=${{ matrix.arch }} NETBIRD_STORE_ENGINE=${{ matrix.store }} CI=true go test -tags=integration -p 1 -exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE' -timeout 30m $(go list -tags=integration ./... | grep /management) + run: | + CGO_ENABLED=1 GOARCH=${{ matrix.arch }} \ + NETBIRD_STORE_ENGINE=${{ matrix.store }} CI=true \ + go test -tags=integration \ + -exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE' \ + -timeout 10m ./management/... test_client_on_docker: + name: "Client (Docker) / Unit" needs: [ build-cache ] runs-on: ubuntu-20.04 steps: diff --git a/.golangci.yaml b/.golangci.yaml index 44b03d0e1..461677c2e 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -103,7 +103,7 @@ linters: - predeclared # predeclared finds code that shadows one of Go's predeclared identifiers - revive # Fast, configurable, extensible, flexible, and beautiful linter for Go. Drop-in replacement of golint. - sqlclosecheck # checks that sql.Rows and sql.Stmt are closed - - thelper # thelper detects Go test helpers without t.Helper() call and checks the consistency of test helpers. + # - thelper # thelper detects Go test helpers without t.Helper() call and checks the consistency of test helpers. - wastedassign # wastedassign finds wasted assignment statements issues: # Maximum count of issues with the same text. diff --git a/management/client/client_test.go b/management/client/client_test.go index 3e498a5ea..b4ee58298 100644 --- a/management/client/client_test.go +++ b/management/client/client_test.go @@ -258,8 +258,11 @@ func TestClient_Sync(t *testing.T) { ch := make(chan *mgmtProto.SyncResponse, 1) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + go func() { - err = client.Sync(context.Background(), info, func(msg *mgmtProto.SyncResponse) error { + err = client.Sync(ctx, info, func(msg *mgmtProto.SyncResponse) error { ch <- msg return nil }) diff --git a/management/server/dns_test.go b/management/server/dns_test.go index 6fb9f6a29..c40f62324 100644 --- a/management/server/dns_test.go +++ b/management/server/dns_test.go @@ -42,7 +42,7 @@ func TestGetDNSSettings(t *testing.T) { account, err := initTestDNSAccount(t, am) if err != nil { - t.Fatal("failed to init testing account") + t.Fatalf("failed to init testing account: %s", err) } dnsSettings, err := am.GetDNSSettings(context.Background(), account.Id, dnsAdminUserID) @@ -124,12 +124,12 @@ func TestSaveDNSSettings(t *testing.T) { t.Run(testCase.name, func(t *testing.T) { am, err := createDNSManager(t) if err != nil { - t.Error("failed to create account manager") + t.Fatalf("failed to create account manager") } account, err := initTestDNSAccount(t, am) if err != nil { - t.Error("failed to init testing account") + t.Fatalf("failed to init testing account: %v", err) } err = am.SaveDNSSettings(context.Background(), account.Id, testCase.userID, testCase.inputSettings) @@ -156,22 +156,22 @@ func TestGetNetworkMap_DNSConfigSync(t *testing.T) { am, err := createDNSManager(t) if err != nil { - t.Error("failed to create account manager") + t.Fatalf("failed to create account manager: %s", err) } account, err := initTestDNSAccount(t, am) if err != nil { - t.Error("failed to init testing account") + t.Fatalf("failed to init testing account: %s", err) } peer1, err := account.FindPeerByPubKey(dnsPeer1Key) if err != nil { - t.Error("failed to init testing account") + t.Fatalf("failed to init testing account: %s", err) } peer2, err := account.FindPeerByPubKey(dnsPeer2Key) if err != nil { - t.Error("failed to init testing account") + t.Fatalf("failed to init testing account: %s", err) } newAccountDNSConfig, err := am.GetNetworkMap(context.Background(), peer1.ID) diff --git a/management/server/group_test.go b/management/server/group_test.go index cc90f187b..b21b5e834 100644 --- a/management/server/group_test.go +++ b/management/server/group_test.go @@ -29,7 +29,7 @@ func TestDefaultAccountManager_CreateGroup(t *testing.T) { _, account, err := initTestGroupAccount(am) if err != nil { - t.Error("failed to init testing account") + t.Fatalf("failed to init testing account: %s", err) } for _, group := range account.Groups { group.Issued = types.GroupIssuedIntegration @@ -59,12 +59,12 @@ func TestDefaultAccountManager_CreateGroup(t *testing.T) { func TestDefaultAccountManager_DeleteGroup(t *testing.T) { am, err := createManager(t) if err != nil { - t.Error("failed to create account manager") + t.Fatalf("failed to create account manager: %s", err) } _, account, err := initTestGroupAccount(am) if err != nil { - t.Error("failed to init testing account") + t.Fatalf("failed to init testing account: %s", err) } testCases := []struct { diff --git a/management/server/management_suite_test.go b/management/server/management_suite_test.go deleted file mode 100644 index cc99624a0..000000000 --- a/management/server/management_suite_test.go +++ /dev/null @@ -1,13 +0,0 @@ -package server_test - -import ( - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" - - "testing" -) - -func TestManagement(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "Management Service Suite") -} diff --git a/management/server/management_test.go b/management/server/management_test.go index 43a6e40d5..1b91b3447 100644 --- a/management/server/management_test.go +++ b/management/server/management_test.go @@ -6,13 +6,13 @@ import ( "net" "os" "runtime" - sync2 "sync" + "sync" + "testing" "time" pb "github.com/golang/protobuf/proto" //nolint - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" log "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" "golang.zx2c4.com/wireguard/wgctrl/wgtypes" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" @@ -30,424 +30,77 @@ import ( const ( ValidSetupKey = "A2C8E62B-38F5-4553-B31E-DD66C696CEBB" - AccountKey = "bf1c8084-ba50-4ce7-9439-34653001fc3b" ) -var _ = Describe("Management service", func() { - var ( - addr string - s *grpc.Server - dataDir string - client mgmtProto.ManagementServiceClient - serverPubKey wgtypes.Key - conn *grpc.ClientConn - ) - - BeforeEach(func() { - level, _ := log.ParseLevel("Debug") - log.SetLevel(level) - var err error - dataDir, err = os.MkdirTemp("", "netbird_mgmt_test_tmp_*") - Expect(err).NotTo(HaveOccurred()) - - var listener net.Listener - - config := &server.Config{} - _, err = util.ReadJson("testdata/management.json", config) - Expect(err).NotTo(HaveOccurred()) - config.Datadir = dataDir - - s, listener = startServer(config, dataDir, "testdata/store.sql") - addr = listener.Addr().String() - client, conn = createRawClient(addr) - - // s public key - resp, err := client.GetServerKey(context.TODO(), &mgmtProto.Empty{}) - Expect(err).NotTo(HaveOccurred()) - serverPubKey, err = wgtypes.ParseKey(resp.Key) - Expect(err).NotTo(HaveOccurred()) - }) - - AfterEach(func() { - s.Stop() - err := conn.Close() - Expect(err).NotTo(HaveOccurred()) - os.RemoveAll(dataDir) - }) - - Context("when calling IsHealthy endpoint", func() { - Specify("a non-error result is returned", func() { - healthy, err := client.IsHealthy(context.TODO(), &mgmtProto.Empty{}) - - Expect(err).NotTo(HaveOccurred()) - Expect(healthy).ToNot(BeNil()) - }) - }) - - Context("when calling Sync endpoint", func() { - Context("when there is a new peer registered", func() { - Specify("a proper configuration is returned", func() { - key, _ := wgtypes.GenerateKey() - loginPeerWithValidSetupKey(serverPubKey, key, client) - - syncReq := &mgmtProto.SyncRequest{Meta: &mgmtProto.PeerSystemMeta{}} - encryptedBytes, err := encryption.EncryptMessage(serverPubKey, key, syncReq) - Expect(err).NotTo(HaveOccurred()) - - sync, err := client.Sync(context.TODO(), &mgmtProto.EncryptedMessage{ - WgPubKey: key.PublicKey().String(), - Body: encryptedBytes, - }) - Expect(err).NotTo(HaveOccurred()) - - encryptedResponse := &mgmtProto.EncryptedMessage{} - err = sync.RecvMsg(encryptedResponse) - Expect(err).NotTo(HaveOccurred()) - - resp := &mgmtProto.SyncResponse{} - err = encryption.DecryptMessage(serverPubKey, key, encryptedResponse.Body, resp) - Expect(err).NotTo(HaveOccurred()) - - expectedSignalConfig := &mgmtProto.HostConfig{ - Uri: "signal.netbird.io:10000", - Protocol: mgmtProto.HostConfig_HTTP, - } - expectedStunsConfig := &mgmtProto.HostConfig{ - Uri: "stun:stun.netbird.io:3468", - Protocol: mgmtProto.HostConfig_UDP, - } - expectedTRUNHost := &mgmtProto.HostConfig{ - Uri: "turn:stun.netbird.io:3468", - Protocol: mgmtProto.HostConfig_UDP, - } - - Expect(resp.NetbirdConfig.Signal).To(BeEquivalentTo(expectedSignalConfig)) - Expect(resp.NetbirdConfig.Stuns).To(ConsistOf(expectedStunsConfig)) - // TURN validation is special because credentials are dynamically generated - Expect(resp.NetbirdConfig.Turns).To(HaveLen(1)) - actualTURN := resp.NetbirdConfig.Turns[0] - Expect(len(actualTURN.User) > 0).To(BeTrue()) - Expect(actualTURN.HostConfig).To(BeEquivalentTo(expectedTRUNHost)) - Expect(len(resp.NetworkMap.OfflinePeers) == 0).To(BeTrue()) - }) - }) - - Context("when there are 3 peers registered under one account", func() { - Specify("a list containing other 2 peers is returned", func() { - key, _ := wgtypes.GenerateKey() - key1, _ := wgtypes.GenerateKey() - key2, _ := wgtypes.GenerateKey() - loginPeerWithValidSetupKey(serverPubKey, key, client) - loginPeerWithValidSetupKey(serverPubKey, key1, client) - loginPeerWithValidSetupKey(serverPubKey, key2, client) - - messageBytes, err := pb.Marshal(&mgmtProto.SyncRequest{Meta: &mgmtProto.PeerSystemMeta{}}) - Expect(err).NotTo(HaveOccurred()) - encryptedBytes, err := encryption.Encrypt(messageBytes, serverPubKey, key) - Expect(err).NotTo(HaveOccurred()) - - sync, err := client.Sync(context.TODO(), &mgmtProto.EncryptedMessage{ - WgPubKey: key.PublicKey().String(), - Body: encryptedBytes, - }) - Expect(err).NotTo(HaveOccurred()) - - encryptedResponse := &mgmtProto.EncryptedMessage{} - err = sync.RecvMsg(encryptedResponse) - Expect(err).NotTo(HaveOccurred()) - decryptedBytes, err := encryption.Decrypt(encryptedResponse.Body, serverPubKey, key) - Expect(err).NotTo(HaveOccurred()) - - resp := &mgmtProto.SyncResponse{} - err = pb.Unmarshal(decryptedBytes, resp) - Expect(err).NotTo(HaveOccurred()) - - Expect(resp.GetRemotePeers()).To(HaveLen(2)) - peers := []string{resp.GetRemotePeers()[0].WgPubKey, resp.GetRemotePeers()[1].WgPubKey} - Expect(peers).To(ContainElements(key1.PublicKey().String(), key2.PublicKey().String())) - }) - }) - - Context("when there is a new peer registered", func() { - Specify("an update is returned", func() { - // register only a single peer - key, _ := wgtypes.GenerateKey() - loginPeerWithValidSetupKey(serverPubKey, key, client) - - messageBytes, err := pb.Marshal(&mgmtProto.SyncRequest{Meta: &mgmtProto.PeerSystemMeta{}}) - Expect(err).NotTo(HaveOccurred()) - encryptedBytes, err := encryption.Encrypt(messageBytes, serverPubKey, key) - Expect(err).NotTo(HaveOccurred()) - - sync, err := client.Sync(context.TODO(), &mgmtProto.EncryptedMessage{ - WgPubKey: key.PublicKey().String(), - Body: encryptedBytes, - }) - Expect(err).NotTo(HaveOccurred()) - - // after the initial sync call we have 0 peer updates - encryptedResponse := &mgmtProto.EncryptedMessage{} - err = sync.RecvMsg(encryptedResponse) - Expect(err).NotTo(HaveOccurred()) - decryptedBytes, err := encryption.Decrypt(encryptedResponse.Body, serverPubKey, key) - Expect(err).NotTo(HaveOccurred()) - resp := &mgmtProto.SyncResponse{} - err = pb.Unmarshal(decryptedBytes, resp) - Expect(resp.GetRemotePeers()).To(HaveLen(0)) - - wg := sync2.WaitGroup{} - wg.Add(1) - - // continue listening on updates for a peer - go func() { - err = sync.RecvMsg(encryptedResponse) - - decryptedBytes, err = encryption.Decrypt(encryptedResponse.Body, serverPubKey, key) - Expect(err).NotTo(HaveOccurred()) - resp = &mgmtProto.SyncResponse{} - err = pb.Unmarshal(decryptedBytes, resp) - wg.Done() - }() - - // register a new peer - key1, _ := wgtypes.GenerateKey() - loginPeerWithValidSetupKey(serverPubKey, key1, client) - - wg.Wait() - - Expect(err).NotTo(HaveOccurred()) - Expect(resp.GetRemotePeers()).To(HaveLen(1)) - Expect(resp.GetRemotePeers()[0].WgPubKey).To(BeEquivalentTo(key1.PublicKey().String())) - }) - }) - }) - - Context("when calling GetServerKey endpoint", func() { - Specify("a public Wireguard key of the service is returned", func() { - resp, err := client.GetServerKey(context.TODO(), &mgmtProto.Empty{}) - - Expect(err).NotTo(HaveOccurred()) - Expect(resp).ToNot(BeNil()) - Expect(resp.Key).ToNot(BeNil()) - Expect(resp.ExpiresAt).ToNot(BeNil()) - - // check if the key is a valid Wireguard key - key, err := wgtypes.ParseKey(resp.Key) - Expect(err).NotTo(HaveOccurred()) - Expect(key).ToNot(BeNil()) - }) - }) - - Context("when calling Login endpoint", func() { - Context("with an invalid setup key", func() { - Specify("an error is returned", func() { - key, _ := wgtypes.GenerateKey() - message, err := encryption.EncryptMessage(serverPubKey, key, &mgmtProto.LoginRequest{SetupKey: "invalid setup key", - Meta: &mgmtProto.PeerSystemMeta{}}) - Expect(err).NotTo(HaveOccurred()) - - resp, err := client.Login(context.TODO(), &mgmtProto.EncryptedMessage{ - WgPubKey: key.PublicKey().String(), - Body: message, - }) - - Expect(err).To(HaveOccurred()) - Expect(resp).To(BeNil()) - }) - }) - - Context("with a valid setup key", func() { - It("a non error result is returned", func() { - key, _ := wgtypes.GenerateKey() - resp := loginPeerWithValidSetupKey(serverPubKey, key, client) - - Expect(resp).ToNot(BeNil()) - }) - }) - - Context("with a registered peer", func() { - It("a non error result is returned", func() { - key, _ := wgtypes.GenerateKey() - regResp := loginPeerWithValidSetupKey(serverPubKey, key, client) - Expect(regResp).NotTo(BeNil()) - - // just login without registration - message, err := encryption.EncryptMessage(serverPubKey, key, &mgmtProto.LoginRequest{Meta: &mgmtProto.PeerSystemMeta{}}) - Expect(err).NotTo(HaveOccurred()) - loginResp, err := client.Login(context.TODO(), &mgmtProto.EncryptedMessage{ - WgPubKey: key.PublicKey().String(), - Body: message, - }) - - Expect(err).NotTo(HaveOccurred()) - - decryptedResp := &mgmtProto.LoginResponse{} - err = encryption.DecryptMessage(serverPubKey, key, loginResp.Body, decryptedResp) - Expect(err).NotTo(HaveOccurred()) - - expectedSignalConfig := &mgmtProto.HostConfig{ - Uri: "signal.netbird.io:10000", - Protocol: mgmtProto.HostConfig_HTTP, - } - expectedStunsConfig := &mgmtProto.HostConfig{ - Uri: "stun:stun.netbird.io:3468", - Protocol: mgmtProto.HostConfig_UDP, - } - expectedTurnsConfig := &mgmtProto.ProtectedHostConfig{ - HostConfig: &mgmtProto.HostConfig{ - Uri: "turn:stun.netbird.io:3468", - Protocol: mgmtProto.HostConfig_UDP, - }, - User: "some_user", - Password: "some_password", - } - - Expect(decryptedResp.GetNetbirdConfig().Signal).To(BeEquivalentTo(expectedSignalConfig)) - Expect(decryptedResp.GetNetbirdConfig().Stuns).To(ConsistOf(expectedStunsConfig)) - Expect(decryptedResp.GetNetbirdConfig().Turns).To(ConsistOf(expectedTurnsConfig)) - }) - }) - }) - - Context("when there are 10 peers registered under one account", func() { - Context("when there are 10 more peers registered under the same account", func() { - Specify("all of the 10 peers will get updates of 10 newly registered peers", func() { - initialPeers := 10 - additionalPeers := 10 - - var peers []wgtypes.Key - for i := 0; i < initialPeers; i++ { - key, _ := wgtypes.GenerateKey() - loginPeerWithValidSetupKey(serverPubKey, key, client) - peers = append(peers, key) - } - - wg := sync2.WaitGroup{} - wg.Add(initialPeers + initialPeers*additionalPeers) - - var clients []mgmtProto.ManagementService_SyncClient - for _, peer := range peers { - messageBytes, err := pb.Marshal(&mgmtProto.SyncRequest{Meta: &mgmtProto.PeerSystemMeta{}}) - Expect(err).NotTo(HaveOccurred()) - encryptedBytes, err := encryption.Encrypt(messageBytes, serverPubKey, peer) - Expect(err).NotTo(HaveOccurred()) - - // open stream - sync, err := client.Sync(context.TODO(), &mgmtProto.EncryptedMessage{ - WgPubKey: peer.PublicKey().String(), - Body: encryptedBytes, - }) - Expect(err).NotTo(HaveOccurred()) - clients = append(clients, sync) - - // receive stream - peer := peer - go func() { - for { - encryptedResponse := &mgmtProto.EncryptedMessage{} - err = sync.RecvMsg(encryptedResponse) - if err != nil { - break - } - decryptedBytes, err := encryption.Decrypt(encryptedResponse.Body, serverPubKey, peer) - Expect(err).NotTo(HaveOccurred()) - - resp := &mgmtProto.SyncResponse{} - err = pb.Unmarshal(decryptedBytes, resp) - Expect(err).NotTo(HaveOccurred()) - if len(resp.GetRemotePeers()) > 0 { - // only consider peer updates - wg.Done() - } - } - }() - } - - time.Sleep(1 * time.Second) - for i := 0; i < additionalPeers; i++ { - key, _ := wgtypes.GenerateKey() - loginPeerWithValidSetupKey(serverPubKey, key, client) - r := rand.New(rand.NewSource(time.Now().UnixNano())) - n := r.Intn(200) - time.Sleep(time.Duration(n) * time.Millisecond) - } - - wg.Wait() - - for _, syncClient := range clients { - err := syncClient.CloseSend() - Expect(err).NotTo(HaveOccurred()) - } - }) - }) - }) - - Context("when there are peers registered under one account concurrently", func() { - Specify("then there are no duplicate IPs", func() { - initialPeers := 30 - - ipChannel := make(chan string, 20) - for i := 0; i < initialPeers; i++ { - go func() { - defer GinkgoRecover() - key, _ := wgtypes.GenerateKey() - loginPeerWithValidSetupKey(serverPubKey, key, client) - syncReq := &mgmtProto.SyncRequest{Meta: &mgmtProto.PeerSystemMeta{}} - encryptedBytes, err := encryption.EncryptMessage(serverPubKey, key, syncReq) - Expect(err).NotTo(HaveOccurred()) - - // open stream - sync, err := client.Sync(context.TODO(), &mgmtProto.EncryptedMessage{ - WgPubKey: key.PublicKey().String(), - Body: encryptedBytes, - }) - Expect(err).NotTo(HaveOccurred()) - encryptedResponse := &mgmtProto.EncryptedMessage{} - err = sync.RecvMsg(encryptedResponse) - Expect(err).NotTo(HaveOccurred()) - - resp := &mgmtProto.SyncResponse{} - err = encryption.DecryptMessage(serverPubKey, key, encryptedResponse.Body, resp) - Expect(err).NotTo(HaveOccurred()) - - ipChannel <- resp.GetPeerConfig().Address - }() - } - - ips := make(map[string]struct{}) - for ip := range ipChannel { - if _, ok := ips[ip]; ok { - Fail("found duplicate IP: " + ip) - } - ips[ip] = struct{}{} - if len(ips) == initialPeers { - break - } - } - close(ipChannel) - }) - }) - - Context("after login two peers", func() { - Specify("then they receive the same network", func() { - key, _ := wgtypes.GenerateKey() - firstLogin := loginPeerWithValidSetupKey(serverPubKey, key, client) - key, _ = wgtypes.GenerateKey() - secondLogin := loginPeerWithValidSetupKey(serverPubKey, key, client) - - _, firstLoginNetwork, err := net.ParseCIDR(firstLogin.GetPeerConfig().GetAddress()) - Expect(err).NotTo(HaveOccurred()) - _, secondLoginNetwork, err := net.ParseCIDR(secondLogin.GetPeerConfig().GetAddress()) - Expect(err).NotTo(HaveOccurred()) - - Expect(secondLoginNetwork.String()).To(BeEquivalentTo(firstLoginNetwork.String())) - }) - }) -}) - -func loginPeerWithValidSetupKey(serverPubKey wgtypes.Key, key wgtypes.Key, client mgmtProto.ManagementServiceClient) *mgmtProto.LoginResponse { - defer GinkgoRecover() - +type testSuite struct { + t *testing.T + addr string + grpcServer *grpc.Server + dataDir string + client mgmtProto.ManagementServiceClient + serverPubKey wgtypes.Key + conn *grpc.ClientConn +} + +func setupTest(t *testing.T) *testSuite { + t.Helper() + level, _ := log.ParseLevel("Debug") + log.SetLevel(level) + + ts := &testSuite{t: t} + + var err error + ts.dataDir, err = os.MkdirTemp("", "netbird_mgmt_test_tmp_*") + if err != nil { + t.Fatalf("failed to create temp directory: %v", err) + } + + config := &server.Config{} + _, err = util.ReadJson("testdata/management.json", config) + if err != nil { + t.Fatalf("failed to read management.json: %v", err) + } + config.Datadir = ts.dataDir + + var listener net.Listener + ts.grpcServer, listener = startServer(t, config, ts.dataDir, "testdata/store.sql") + ts.addr = listener.Addr().String() + + ts.client, ts.conn = createRawClient(t, ts.addr) + + resp, err := ts.client.GetServerKey(context.TODO(), &mgmtProto.Empty{}) + if err != nil { + t.Fatalf("failed to get server key: %v", err) + } + + serverKey, err := wgtypes.ParseKey(resp.Key) + if err != nil { + t.Fatalf("failed to parse server key: %v", err) + } + ts.serverPubKey = serverKey + + return ts +} + +func tearDownTest(t *testing.T, ts *testSuite) { + t.Helper() + ts.grpcServer.Stop() + if err := ts.conn.Close(); err != nil { + t.Fatalf("failed to close client connection: %v", err) + } + time.Sleep(100 * time.Millisecond) + if err := os.RemoveAll(ts.dataDir); err != nil { + t.Fatalf("failed to remove data directory %s: %v", ts.dataDir, err) + } +} + +func loginPeerWithValidSetupKey( + t *testing.T, + serverPubKey wgtypes.Key, + key wgtypes.Key, + client mgmtProto.ManagementServiceClient, +) *mgmtProto.LoginResponse { + t.Helper() meta := &mgmtProto.PeerSystemMeta{ Hostname: key.PublicKey().String(), GoOS: runtime.GOOS, @@ -457,23 +110,30 @@ func loginPeerWithValidSetupKey(serverPubKey wgtypes.Key, key wgtypes.Key, clien Kernel: "kernel", NetbirdVersion: "", } - message, err := encryption.EncryptMessage(serverPubKey, key, &mgmtProto.LoginRequest{SetupKey: ValidSetupKey, Meta: meta}) - Expect(err).NotTo(HaveOccurred()) + msgToEncrypt := &mgmtProto.LoginRequest{SetupKey: ValidSetupKey, Meta: meta} + message, err := encryption.EncryptMessage(serverPubKey, key, msgToEncrypt) + if err != nil { + t.Fatalf("failed to encrypt login request: %v", err) + } resp, err := client.Login(context.TODO(), &mgmtProto.EncryptedMessage{ WgPubKey: key.PublicKey().String(), Body: message, }) - - Expect(err).NotTo(HaveOccurred()) + if err != nil { + t.Fatalf("login request failed: %v", err) + } loginResp := &mgmtProto.LoginResponse{} err = encryption.DecryptMessage(serverPubKey, key, resp.Body, loginResp) - Expect(err).NotTo(HaveOccurred()) + if err != nil { + t.Fatalf("failed to decrypt login response: %v", err) + } return loginResp } -func createRawClient(addr string) (mgmtProto.ManagementServiceClient, *grpc.ClientConn) { +func createRawClient(t *testing.T, addr string) (mgmtProto.ManagementServiceClient, *grpc.ClientConn) { + t.Helper() ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() @@ -484,17 +144,27 @@ func createRawClient(addr string) (mgmtProto.ManagementServiceClient, *grpc.Clie Time: 10 * time.Second, Timeout: 2 * time.Second, })) - Expect(err).NotTo(HaveOccurred()) + if err != nil { + t.Fatalf("failed to dial gRPC server: %v", err) + } return mgmtProto.NewManagementServiceClient(conn), conn } -func startServer(config *server.Config, dataDir string, testFile string) (*grpc.Server, net.Listener) { +func startServer( + t *testing.T, + config *server.Config, + dataDir string, + testFile string, +) (*grpc.Server, net.Listener) { + t.Helper() lis, err := net.Listen("tcp", ":0") - Expect(err).NotTo(HaveOccurred()) + if err != nil { + t.Fatalf("failed to listen on a random port: %v", err) + } s := grpc.NewServer() - store, _, err := store.NewTestStoreFromSQL(context.Background(), testFile, dataDir) + str, _, err := store.NewTestStoreFromSQL(context.Background(), testFile, dataDir) if err != nil { log.Fatalf("failed creating a store: %s: %v", config.Datadir, err) } @@ -504,23 +174,529 @@ func startServer(config *server.Config, dataDir string, testFile string) (*grpc. metrics, err := telemetry.NewDefaultAppMetrics(context.Background()) if err != nil { - log.Fatalf("failed creating metrics: %v", err) + t.Fatalf("failed creating metrics: %v", err) } - accountManager, err := server.BuildManager(context.Background(), store, peersUpdateManager, nil, "", "netbird.selfhosted", eventStore, nil, false, server.MocIntegratedValidator{}, metrics) + accountManager, err := server.BuildManager( + context.Background(), + str, + peersUpdateManager, + nil, + "", + "netbird.selfhosted", + eventStore, + nil, + false, + server.MocIntegratedValidator{}, + metrics, + ) if err != nil { - log.Fatalf("failed creating a manager: %v", err) + t.Fatalf("failed creating an account manager: %v", err) } secretsManager := server.NewTimeBasedAuthSecretsManager(peersUpdateManager, config.TURNConfig, config.Relay) - mgmtServer, err := server.NewServer(context.Background(), config, accountManager, settings.NewManager(store), peersUpdateManager, secretsManager, nil, nil) - Expect(err).NotTo(HaveOccurred()) + mgmtServer, err := server.NewServer( + context.Background(), + config, + accountManager, + settings.NewManager(str), + peersUpdateManager, + secretsManager, + nil, + nil, + ) + if err != nil { + t.Fatalf("failed creating management server: %v", err) + } + mgmtProto.RegisterManagementServiceServer(s, mgmtServer) + go func() { if err := s.Serve(lis); err != nil { - Expect(err).NotTo(HaveOccurred()) + t.Errorf("failed to serve gRPC: %v", err) + return } }() return s, lis } + +func TestIsHealthy(t *testing.T) { + ts := setupTest(t) + defer tearDownTest(t, ts) + + healthy, err := ts.client.IsHealthy(context.TODO(), &mgmtProto.Empty{}) + if err != nil { + t.Fatalf("IsHealthy call returned an error: %v", err) + } + if healthy == nil { + t.Fatal("IsHealthy returned a nil response") + } +} + +func TestSyncNewPeerConfiguration(t *testing.T) { + ts := setupTest(t) + defer tearDownTest(t, ts) + + peerKey, _ := wgtypes.GenerateKey() + loginPeerWithValidSetupKey(t, ts.serverPubKey, peerKey, ts.client) + + syncReq := &mgmtProto.SyncRequest{Meta: &mgmtProto.PeerSystemMeta{}} + encryptedBytes, err := encryption.EncryptMessage(ts.serverPubKey, peerKey, syncReq) + if err != nil { + t.Fatalf("failed to encrypt sync request: %v", err) + } + + syncStream, err := ts.client.Sync(context.TODO(), &mgmtProto.EncryptedMessage{ + WgPubKey: peerKey.PublicKey().String(), + Body: encryptedBytes, + }) + if err != nil { + t.Fatalf("failed to call Sync: %v", err) + } + + encryptedResponse := &mgmtProto.EncryptedMessage{} + err = syncStream.RecvMsg(encryptedResponse) + if err != nil { + t.Fatalf("failed to receive sync response message: %v", err) + } + + resp := &mgmtProto.SyncResponse{} + err = encryption.DecryptMessage(ts.serverPubKey, peerKey, encryptedResponse.Body, resp) + if err != nil { + t.Fatalf("failed to decrypt sync response: %v", err) + } + + expectedSignalConfig := &mgmtProto.HostConfig{ + Uri: "signal.netbird.io:10000", + Protocol: mgmtProto.HostConfig_HTTP, + } + expectedStunsConfig := &mgmtProto.HostConfig{ + Uri: "stun:stun.netbird.io:3468", + Protocol: mgmtProto.HostConfig_UDP, + } + expectedTRUNHost := &mgmtProto.HostConfig{ + Uri: "turn:stun.netbird.io:3468", + Protocol: mgmtProto.HostConfig_UDP, + } + + assert.NotNil(t, resp.NetbirdConfig) + assert.Equal(t, resp.NetbirdConfig.Signal, expectedSignalConfig) + assert.Contains(t, resp.NetbirdConfig.Stuns, expectedStunsConfig) + assert.Equal(t, len(resp.NetbirdConfig.Turns), 1) + actualTURN := resp.NetbirdConfig.Turns[0] + assert.Greater(t, len(actualTURN.User), 0) + assert.Equal(t, actualTURN.HostConfig, expectedTRUNHost) + assert.Equal(t, len(resp.NetworkMap.OfflinePeers), 0) +} + +func TestSyncThreePeers(t *testing.T) { + ts := setupTest(t) + defer tearDownTest(t, ts) + + peerKey, _ := wgtypes.GenerateKey() + peerKey1, _ := wgtypes.GenerateKey() + peerKey2, _ := wgtypes.GenerateKey() + + loginPeerWithValidSetupKey(t, ts.serverPubKey, peerKey, ts.client) + loginPeerWithValidSetupKey(t, ts.serverPubKey, peerKey1, ts.client) + loginPeerWithValidSetupKey(t, ts.serverPubKey, peerKey2, ts.client) + + syncReq := &mgmtProto.SyncRequest{Meta: &mgmtProto.PeerSystemMeta{}} + syncBytes, err := pb.Marshal(syncReq) + if err != nil { + t.Fatalf("failed to marshal sync request: %v", err) + } + encryptedBytes, err := encryption.Encrypt(syncBytes, ts.serverPubKey, peerKey) + if err != nil { + t.Fatalf("failed to encrypt sync request: %v", err) + } + + syncStream, err := ts.client.Sync(context.TODO(), &mgmtProto.EncryptedMessage{ + WgPubKey: peerKey.PublicKey().String(), + Body: encryptedBytes, + }) + if err != nil { + t.Fatalf("failed to call Sync: %v", err) + } + + encryptedResponse := &mgmtProto.EncryptedMessage{} + err = syncStream.RecvMsg(encryptedResponse) + if err != nil { + t.Fatalf("failed to receive sync response: %v", err) + } + + decryptedBytes, err := encryption.Decrypt(encryptedResponse.Body, ts.serverPubKey, peerKey) + if err != nil { + t.Fatalf("failed to decrypt sync response: %v", err) + } + + resp := &mgmtProto.SyncResponse{} + err = pb.Unmarshal(decryptedBytes, resp) + if err != nil { + t.Fatalf("failed to unmarshal sync response: %v", err) + } + + if len(resp.GetRemotePeers()) != 2 { + t.Fatalf("expected 2 remote peers, got %d", len(resp.GetRemotePeers())) + } + + var found1, found2 bool + for _, rp := range resp.GetRemotePeers() { + if rp.WgPubKey == peerKey1.PublicKey().String() { + found1 = true + } else if rp.WgPubKey == peerKey2.PublicKey().String() { + found2 = true + } + } + if !found1 || !found2 { + t.Fatalf("did not find the expected peer keys %s, %s among %v", + peerKey1.PublicKey().String(), + peerKey2.PublicKey().String(), + resp.GetRemotePeers()) + } +} + +func TestSyncNewPeerUpdate(t *testing.T) { + ts := setupTest(t) + defer tearDownTest(t, ts) + + peerKey, _ := wgtypes.GenerateKey() + loginPeerWithValidSetupKey(t, ts.serverPubKey, peerKey, ts.client) + + syncReq := &mgmtProto.SyncRequest{Meta: &mgmtProto.PeerSystemMeta{}} + syncBytes, err := pb.Marshal(syncReq) + if err != nil { + t.Fatalf("failed to marshal sync request: %v", err) + } + + encryptedBytes, err := encryption.Encrypt(syncBytes, ts.serverPubKey, peerKey) + if err != nil { + t.Fatalf("failed to encrypt sync request: %v", err) + } + + syncStream, err := ts.client.Sync(context.TODO(), &mgmtProto.EncryptedMessage{ + WgPubKey: peerKey.PublicKey().String(), + Body: encryptedBytes, + }) + if err != nil { + t.Fatalf("failed to call Sync: %v", err) + } + + encryptedResponse := &mgmtProto.EncryptedMessage{} + err = syncStream.RecvMsg(encryptedResponse) + if err != nil { + t.Fatalf("failed to receive first sync response: %v", err) + } + + decryptedBytes, err := encryption.Decrypt(encryptedResponse.Body, ts.serverPubKey, peerKey) + if err != nil { + t.Fatalf("failed to decrypt first sync response: %v", err) + } + + resp := &mgmtProto.SyncResponse{} + if err := pb.Unmarshal(decryptedBytes, resp); err != nil { + t.Fatalf("failed to unmarshal first sync response: %v", err) + } + + if len(resp.GetRemotePeers()) != 0 { + t.Fatalf("expected 0 remote peers at first sync, got %d", len(resp.GetRemotePeers())) + } + + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + encryptedResponse := &mgmtProto.EncryptedMessage{} + err = syncStream.RecvMsg(encryptedResponse) + if err != nil { + t.Errorf("failed to receive second sync response: %v", err) + return + } + + decryptedBytes, err := encryption.Decrypt(encryptedResponse.Body, ts.serverPubKey, peerKey) + if err != nil { + t.Errorf("failed to decrypt second sync response: %v", err) + return + } + err = pb.Unmarshal(decryptedBytes, resp) + if err != nil { + t.Errorf("failed to unmarshal second sync response: %v", err) + return + } + }() + + newPeerKey, _ := wgtypes.GenerateKey() + loginPeerWithValidSetupKey(t, ts.serverPubKey, newPeerKey, ts.client) + + wg.Wait() + + if len(resp.GetRemotePeers()) != 1 { + t.Fatalf("expected exactly 1 remote peer update, got %d", len(resp.GetRemotePeers())) + } + if resp.GetRemotePeers()[0].WgPubKey != newPeerKey.PublicKey().String() { + t.Fatalf("expected new peer key %s, got %s", + newPeerKey.PublicKey().String(), + resp.GetRemotePeers()[0].WgPubKey) + } +} + +func TestGetServerKey(t *testing.T) { + ts := setupTest(t) + defer tearDownTest(t, ts) + + resp, err := ts.client.GetServerKey(context.TODO(), &mgmtProto.Empty{}) + if err != nil { + t.Fatalf("GetServerKey returned error: %v", err) + } + if resp == nil { + t.Fatal("GetServerKey returned nil response") + } + if resp.Key == "" { + t.Fatal("GetServerKey returned empty key") + } + if resp.ExpiresAt.AsTime().IsZero() { + t.Fatal("GetServerKey returned 0 for ExpiresAt") + } + + _, err = wgtypes.ParseKey(resp.Key) + if err != nil { + t.Fatalf("GetServerKey returned an invalid WG key: %v", err) + } +} + +func TestLoginInvalidSetupKey(t *testing.T) { + ts := setupTest(t) + defer tearDownTest(t, ts) + + peerKey, _ := wgtypes.GenerateKey() + request := &mgmtProto.LoginRequest{ + SetupKey: "invalid setup key", + Meta: &mgmtProto.PeerSystemMeta{}, + } + encryptedMsg, err := encryption.EncryptMessage(ts.serverPubKey, peerKey, request) + if err != nil { + t.Fatalf("failed to encrypt login request: %v", err) + } + + resp, err := ts.client.Login(context.TODO(), &mgmtProto.EncryptedMessage{ + WgPubKey: peerKey.PublicKey().String(), + Body: encryptedMsg, + }) + if err == nil { + t.Fatal("expected error for invalid setup key but got nil") + } + if resp != nil { + t.Fatalf("expected nil response for invalid setup key but got: %+v", resp) + } +} + +func TestLoginValidSetupKey(t *testing.T) { + ts := setupTest(t) + defer tearDownTest(t, ts) + + peerKey, _ := wgtypes.GenerateKey() + resp := loginPeerWithValidSetupKey(t, ts.serverPubKey, peerKey, ts.client) + if resp == nil { + t.Fatal("loginPeerWithValidSetupKey returned nil, expected a valid response") + } +} + +func TestLoginRegisteredPeer(t *testing.T) { + ts := setupTest(t) + defer tearDownTest(t, ts) + + peerKey, _ := wgtypes.GenerateKey() + regResp := loginPeerWithValidSetupKey(t, ts.serverPubKey, peerKey, ts.client) + if regResp == nil { + t.Fatal("registration with valid setup key failed") + } + + loginReq := &mgmtProto.LoginRequest{Meta: &mgmtProto.PeerSystemMeta{}} + encryptedLogin, err := encryption.EncryptMessage(ts.serverPubKey, peerKey, loginReq) + if err != nil { + t.Fatalf("failed to encrypt login request: %v", err) + } + loginRespEnc, err := ts.client.Login(context.TODO(), &mgmtProto.EncryptedMessage{ + WgPubKey: peerKey.PublicKey().String(), + Body: encryptedLogin, + }) + if err != nil { + t.Fatalf("login call returned an error: %v", err) + } + + loginResp := &mgmtProto.LoginResponse{} + err = encryption.DecryptMessage(ts.serverPubKey, peerKey, loginRespEnc.Body, loginResp) + if err != nil { + t.Fatalf("failed to decrypt login response: %v", err) + } + + expectedSignalConfig := &mgmtProto.HostConfig{ + Uri: "signal.netbird.io:10000", + Protocol: mgmtProto.HostConfig_HTTP, + } + expectedStunsConfig := &mgmtProto.HostConfig{ + Uri: "stun:stun.netbird.io:3468", + Protocol: mgmtProto.HostConfig_UDP, + } + expectedTurnsConfig := &mgmtProto.ProtectedHostConfig{ + HostConfig: &mgmtProto.HostConfig{ + Uri: "turn:stun.netbird.io:3468", + Protocol: mgmtProto.HostConfig_UDP, + }, + User: "some_user", + Password: "some_password", + } + + assert.NotNil(t, loginResp.GetNetbirdConfig()) + assert.Equal(t, loginResp.GetNetbirdConfig().Signal, expectedSignalConfig) + assert.Contains(t, loginResp.GetNetbirdConfig().Stuns, expectedStunsConfig) + assert.Contains(t, loginResp.GetNetbirdConfig().Turns, expectedTurnsConfig) +} + +func TestSync10PeersGetUpdates(t *testing.T) { + ts := setupTest(t) + defer tearDownTest(t, ts) + + initialPeers := 10 + additionalPeers := 10 + + var peers []wgtypes.Key + for i := 0; i < initialPeers; i++ { + key, _ := wgtypes.GenerateKey() + loginPeerWithValidSetupKey(t, ts.serverPubKey, key, ts.client) + peers = append(peers, key) + } + + var wg sync.WaitGroup + wg.Add(initialPeers + initialPeers*additionalPeers) + + var syncClients []mgmtProto.ManagementService_SyncClient + for _, pk := range peers { + syncReq := &mgmtProto.SyncRequest{Meta: &mgmtProto.PeerSystemMeta{}} + msgBytes, err := pb.Marshal(syncReq) + if err != nil { + t.Fatalf("failed to marshal SyncRequest: %v", err) + } + encBytes, err := encryption.Encrypt(msgBytes, ts.serverPubKey, pk) + if err != nil { + t.Fatalf("failed to encrypt SyncRequest: %v", err) + } + + s, err := ts.client.Sync(context.TODO(), &mgmtProto.EncryptedMessage{ + WgPubKey: pk.PublicKey().String(), + Body: encBytes, + }) + if err != nil { + t.Fatalf("failed to call Sync for peer: %v", err) + } + syncClients = append(syncClients, s) + + go func(pk wgtypes.Key, syncStream mgmtProto.ManagementService_SyncClient) { + for { + encMsg := &mgmtProto.EncryptedMessage{} + err := syncStream.RecvMsg(encMsg) + if err != nil { + return + } + decryptedBytes, decErr := encryption.Decrypt(encMsg.Body, ts.serverPubKey, pk) + if decErr != nil { + t.Errorf("failed to decrypt SyncResponse for peer %s: %v", pk.PublicKey().String(), decErr) + return + } + resp := &mgmtProto.SyncResponse{} + umErr := pb.Unmarshal(decryptedBytes, resp) + if umErr != nil { + t.Errorf("failed to unmarshal SyncResponse for peer %s: %v", pk.PublicKey().String(), umErr) + return + } + // We only count if there's a new peer update + if len(resp.GetRemotePeers()) > 0 { + wg.Done() + } + } + }(pk, s) + } + + time.Sleep(500 * time.Millisecond) + for i := 0; i < additionalPeers; i++ { + key, _ := wgtypes.GenerateKey() + loginPeerWithValidSetupKey(t, ts.serverPubKey, key, ts.client) + r := rand.New(rand.NewSource(time.Now().UnixNano())) + n := r.Intn(200) + time.Sleep(time.Duration(n) * time.Millisecond) + } + + wg.Wait() + + for _, sc := range syncClients { + err := sc.CloseSend() + if err != nil { + t.Fatalf("failed to close sync client: %v", err) + } + } +} + +func TestConcurrentPeersNoDuplicateIPs(t *testing.T) { + ts := setupTest(t) + defer tearDownTest(t, ts) + + initialPeers := 30 + ipChan := make(chan string, initialPeers) + + var wg sync.WaitGroup + wg.Add(initialPeers) + + for i := 0; i < initialPeers; i++ { + go func() { + defer wg.Done() + key, _ := wgtypes.GenerateKey() + loginPeerWithValidSetupKey(t, ts.serverPubKey, key, ts.client) + + syncReq := &mgmtProto.SyncRequest{Meta: &mgmtProto.PeerSystemMeta{}} + encryptedBytes, err := encryption.EncryptMessage(ts.serverPubKey, key, syncReq) + if err != nil { + t.Errorf("failed to encrypt sync request: %v", err) + return + } + + s, err := ts.client.Sync(context.TODO(), &mgmtProto.EncryptedMessage{ + WgPubKey: key.PublicKey().String(), + Body: encryptedBytes, + }) + if err != nil { + t.Errorf("failed to call Sync: %v", err) + return + } + + encResp := &mgmtProto.EncryptedMessage{} + if err = s.RecvMsg(encResp); err != nil { + t.Errorf("failed to receive sync response: %v", err) + return + } + + resp := &mgmtProto.SyncResponse{} + if err = encryption.DecryptMessage(ts.serverPubKey, key, encResp.Body, resp); err != nil { + t.Errorf("failed to decrypt sync response: %v", err) + return + } + ipChan <- resp.GetPeerConfig().Address + }() + } + + wg.Wait() + close(ipChan) + + ipMap := make(map[string]bool) + for ip := range ipChan { + if ipMap[ip] { + t.Fatalf("found duplicate IP: %s", ip) + } + ipMap[ip] = true + } + + // Ensure we collected all peers + if len(ipMap) != initialPeers { + t.Fatalf("expected %d unique IPs, got %d", initialPeers, len(ipMap)) + } +} diff --git a/management/server/nameserver_test.go b/management/server/nameserver_test.go index 0743db513..497d9af4f 100644 --- a/management/server/nameserver_test.go +++ b/management/server/nameserver_test.go @@ -379,12 +379,12 @@ func TestCreateNameServerGroup(t *testing.T) { t.Run(testCase.name, func(t *testing.T) { am, err := createNSManager(t) if err != nil { - t.Error("failed to create account manager") + t.Fatalf("failed to create account manager: %s", err) } account, err := initTestNSAccount(t, am) if err != nil { - t.Error("failed to init testing account") + t.Fatalf("failed to init testing account: %s", err) } outNSGroup, err := am.CreateNameServerGroup( @@ -607,12 +607,12 @@ func TestSaveNameServerGroup(t *testing.T) { t.Run(testCase.name, func(t *testing.T) { am, err := createNSManager(t) if err != nil { - t.Error("failed to create account manager") + t.Fatalf("failed to create account manager: %s", err) } account, err := initTestNSAccount(t, am) if err != nil { - t.Error("failed to init testing account") + t.Fatalf("failed to init testing account: %s", err) } account.NameServerGroups[testCase.existingNSGroup.ID] = testCase.existingNSGroup @@ -706,7 +706,7 @@ func TestDeleteNameServerGroup(t *testing.T) { account, err := initTestNSAccount(t, am) if err != nil { - t.Error("failed to init testing account") + t.Fatalf("failed to init testing account: %s", err) } account.NameServerGroups[testingNSGroup.ID] = testingNSGroup @@ -741,7 +741,7 @@ func TestGetNameServerGroup(t *testing.T) { account, err := initTestNSAccount(t, am) if err != nil { - t.Error("failed to init testing account") + t.Fatalf("failed to init testing account: %s", err) } foundGroup, err := am.GetNameServerGroup(context.Background(), account.Id, testUserID, existingNSGroupID) @@ -761,6 +761,7 @@ func TestGetNameServerGroup(t *testing.T) { func createNSManager(t *testing.T) (*DefaultAccountManager, error) { t.Helper() + store, err := createNSStore(t) if err != nil { return nil, err diff --git a/management/server/route_test.go b/management/server/route_test.go index 1c5c56f60..40e0f41b0 100644 --- a/management/server/route_test.go +++ b/management/server/route_test.go @@ -13,12 +13,11 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/netbirdio/netbird/management/domain" + "github.com/netbirdio/netbird/management/server/activity" resourceTypes "github.com/netbirdio/netbird/management/server/networks/resources/types" routerTypes "github.com/netbirdio/netbird/management/server/networks/routers/types" networkTypes "github.com/netbirdio/netbird/management/server/networks/types" - - "github.com/netbirdio/netbird/management/domain" - "github.com/netbirdio/netbird/management/server/activity" nbpeer "github.com/netbirdio/netbird/management/server/peer" "github.com/netbirdio/netbird/management/server/store" "github.com/netbirdio/netbird/management/server/telemetry" diff --git a/management/server/store/sql_store_test.go b/management/server/store/sql_store_test.go index 6e04c7d9d..dd240ce6c 100644 --- a/management/server/store/sql_store_test.go +++ b/management/server/store/sql_store_test.go @@ -37,40 +37,44 @@ import ( nbroute "github.com/netbirdio/netbird/route" ) -func TestSqlite_NewStore(t *testing.T) { +func runTestForAllEngines(t *testing.T, testDataFile string, f func(t *testing.T, store Store)) { + t.Helper() + for _, engine := range supportedEngines { + if os.Getenv("NETBIRD_STORE_ENGINE") != "" && os.Getenv("NETBIRD_STORE_ENGINE") != string(engine) { + continue + } + t.Setenv("NETBIRD_STORE_ENGINE", string(engine)) + store, cleanUp, err := NewTestStoreFromSQL(context.Background(), testDataFile, t.TempDir()) + t.Cleanup(cleanUp) + assert.NoError(t, err) + t.Run(string(engine), func(t *testing.T) { + f(t, store) + }) + os.Unsetenv("NETBIRD_STORE_ENGINE") + } +} + +func Test_NewStore(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("The SQLite store is not properly supported by Windows yet") } - t.Setenv("NETBIRD_STORE_ENGINE", string(SqliteStoreEngine)) - store, cleanUp, err := NewTestStoreFromSQL(context.Background(), "", t.TempDir()) - t.Cleanup(cleanUp) - assert.NoError(t, err) - - if len(store.GetAllAccounts(context.Background())) != 0 { - t.Errorf("expected to create a new empty Accounts map when creating a new FileStore") - } + runTestForAllEngines(t, "", func(t *testing.T, store Store) { + if store == nil { + t.Errorf("expected to create a new Store") + } + if len(store.GetAllAccounts(context.Background())) != 0 { + t.Errorf("expected to create a new empty Accounts map when creating a new FileStore") + } + }) } -func TestSqlite_SaveAccount_Large(t *testing.T) { +func Test_SaveAccount_Large(t *testing.T) { if (os.Getenv("CI") == "true" && runtime.GOOS == "darwin") || runtime.GOOS == "windows" { t.Skip("skip CI tests on darwin and windows") } - t.Run("SQLite", func(t *testing.T) { - t.Setenv("NETBIRD_STORE_ENGINE", string(SqliteStoreEngine)) - store, cleanUp, err := NewTestStoreFromSQL(context.Background(), "", t.TempDir()) - t.Cleanup(cleanUp) - assert.NoError(t, err) - runLargeTest(t, store) - }) - - // create store outside to have a better time counter for the test - t.Setenv("NETBIRD_STORE_ENGINE", string(SqliteStoreEngine)) - store, cleanUp, err := NewTestStoreFromSQL(context.Background(), "", t.TempDir()) - t.Cleanup(cleanUp) - assert.NoError(t, err) - t.Run("PostgreSQL", func(t *testing.T) { + runTestForAllEngines(t, "", func(t *testing.T, store Store) { runLargeTest(t, store) }) } @@ -215,77 +219,74 @@ func randomIPv4() net.IP { return net.IP(b) } -func TestSqlite_SaveAccount(t *testing.T) { +func Test_SaveAccount(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("The SQLite store is not properly supported by Windows yet") } - t.Setenv("NETBIRD_STORE_ENGINE", string(SqliteStoreEngine)) - store, cleanUp, err := NewTestStoreFromSQL(context.Background(), "", t.TempDir()) - t.Cleanup(cleanUp) - assert.NoError(t, err) + runTestForAllEngines(t, "", func(t *testing.T, store Store) { + account := newAccountWithId(context.Background(), "account_id", "testuser", "") + setupKey, _ := types.GenerateDefaultSetupKey() + account.SetupKeys[setupKey.Key] = setupKey + account.Peers["testpeer"] = &nbpeer.Peer{ + Key: "peerkey", + IP: net.IP{127, 0, 0, 1}, + Meta: nbpeer.PeerSystemMeta{}, + Name: "peer name", + Status: &nbpeer.PeerStatus{Connected: true, LastSeen: time.Now().UTC()}, + } - account := newAccountWithId(context.Background(), "account_id", "testuser", "") - setupKey, _ := types.GenerateDefaultSetupKey() - account.SetupKeys[setupKey.Key] = setupKey - account.Peers["testpeer"] = &nbpeer.Peer{ - Key: "peerkey", - IP: net.IP{127, 0, 0, 1}, - Meta: nbpeer.PeerSystemMeta{}, - Name: "peer name", - Status: &nbpeer.PeerStatus{Connected: true, LastSeen: time.Now().UTC()}, - } + err := store.SaveAccount(context.Background(), account) + require.NoError(t, err) - err = store.SaveAccount(context.Background(), account) - require.NoError(t, err) + account2 := newAccountWithId(context.Background(), "account_id2", "testuser2", "") + setupKey, _ = types.GenerateDefaultSetupKey() + account2.SetupKeys[setupKey.Key] = setupKey + account2.Peers["testpeer2"] = &nbpeer.Peer{ + Key: "peerkey2", + IP: net.IP{127, 0, 0, 2}, + Meta: nbpeer.PeerSystemMeta{}, + Name: "peer name 2", + Status: &nbpeer.PeerStatus{Connected: true, LastSeen: time.Now().UTC()}, + } - account2 := newAccountWithId(context.Background(), "account_id2", "testuser2", "") - setupKey, _ = types.GenerateDefaultSetupKey() - account2.SetupKeys[setupKey.Key] = setupKey - account2.Peers["testpeer2"] = &nbpeer.Peer{ - Key: "peerkey2", - IP: net.IP{127, 0, 0, 2}, - Meta: nbpeer.PeerSystemMeta{}, - Name: "peer name 2", - Status: &nbpeer.PeerStatus{Connected: true, LastSeen: time.Now().UTC()}, - } + err = store.SaveAccount(context.Background(), account2) + require.NoError(t, err) - err = store.SaveAccount(context.Background(), account2) - require.NoError(t, err) + if len(store.GetAllAccounts(context.Background())) != 2 { + t.Errorf("expecting 2 Accounts to be stored after SaveAccount()") + } - if len(store.GetAllAccounts(context.Background())) != 2 { - t.Errorf("expecting 2 Accounts to be stored after SaveAccount()") - } + a, err := store.GetAccount(context.Background(), account.Id) + if a == nil { + t.Errorf("expecting Account to be stored after SaveAccount(): %v", err) + } - a, err := store.GetAccount(context.Background(), account.Id) - if a == nil { - t.Errorf("expecting Account to be stored after SaveAccount(): %v", err) - } + if a != nil && len(a.Policies) != 1 { + t.Errorf("expecting Account to have one policy stored after SaveAccount(), got %d", len(a.Policies)) + } - if a != nil && len(a.Policies) != 1 { - t.Errorf("expecting Account to have one policy stored after SaveAccount(), got %d", len(a.Policies)) - } + if a != nil && len(a.Policies[0].Rules) != 1 { + t.Errorf("expecting Account to have one policy rule stored after SaveAccount(), got %d", len(a.Policies[0].Rules)) + return + } - if a != nil && len(a.Policies[0].Rules) != 1 { - t.Errorf("expecting Account to have one policy rule stored after SaveAccount(), got %d", len(a.Policies[0].Rules)) - return - } + if a, err := store.GetAccountByPeerPubKey(context.Background(), "peerkey"); a == nil { + t.Errorf("expecting PeerKeyID2AccountID index updated after SaveAccount(): %v", err) + } - if a, err := store.GetAccountByPeerPubKey(context.Background(), "peerkey"); a == nil { - t.Errorf("expecting PeerKeyID2AccountID index updated after SaveAccount(): %v", err) - } + if a, err := store.GetAccountByUser(context.Background(), "testuser"); a == nil { + t.Errorf("expecting UserID2AccountID index updated after SaveAccount(): %v", err) + } - if a, err := store.GetAccountByUser(context.Background(), "testuser"); a == nil { - t.Errorf("expecting UserID2AccountID index updated after SaveAccount(): %v", err) - } + if a, err := store.GetAccountByPeerID(context.Background(), "testpeer"); a == nil { + t.Errorf("expecting PeerID2AccountID index updated after SaveAccount(): %v", err) + } - if a, err := store.GetAccountByPeerID(context.Background(), "testpeer"); a == nil { - t.Errorf("expecting PeerID2AccountID index updated after SaveAccount(): %v", err) - } - - if a, err := store.GetAccountBySetupKey(context.Background(), setupKey.Key); a == nil { - t.Errorf("expecting SetupKeyID2AccountID index updated after SaveAccount(): %v", err) - } + if a, err := store.GetAccountBySetupKey(context.Background(), setupKey.Key); a == nil { + t.Errorf("expecting SetupKeyID2AccountID index updated after SaveAccount(): %v", err) + } + }) } func TestSqlite_DeleteAccount(t *testing.T) { @@ -402,27 +403,24 @@ func TestSqlite_DeleteAccount(t *testing.T) { } } -func TestSqlite_GetAccount(t *testing.T) { +func Test_GetAccount(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("The SQLite store is not properly supported by Windows yet") } - t.Setenv("NETBIRD_STORE_ENGINE", string(SqliteStoreEngine)) - store, cleanUp, err := NewTestStoreFromSQL(context.Background(), "../testdata/store.sql", t.TempDir()) - t.Cleanup(cleanUp) - assert.NoError(t, err) + runTestForAllEngines(t, "../testdata/store.sql", func(t *testing.T, store Store) { + id := "bf1c8084-ba50-4ce7-9439-34653001fc3b" - id := "bf1c8084-ba50-4ce7-9439-34653001fc3b" + account, err := store.GetAccount(context.Background(), id) + require.NoError(t, err) + require.Equal(t, id, account.Id, "account id should match") - account, err := store.GetAccount(context.Background(), id) - require.NoError(t, err) - require.Equal(t, id, account.Id, "account id should match") - - _, err = store.GetAccount(context.Background(), "non-existing-account") - assert.Error(t, err) - parsedErr, ok := status.FromError(err) - require.True(t, ok) - require.Equal(t, status.NotFound, parsedErr.Type(), "should return not found error") + _, err = store.GetAccount(context.Background(), "non-existing-account") + assert.Error(t, err) + parsedErr, ok := status.FromError(err) + require.True(t, ok) + require.Equal(t, status.NotFound, parsedErr.Type(), "should return not found error") + }) } func TestSqlStore_SavePeer(t *testing.T) { @@ -580,51 +578,45 @@ func TestSqlStore_SavePeerLocation(t *testing.T) { require.Equal(t, status.NotFound, parsedErr.Type(), "should return not found error") } -func TestSqlite_TestGetAccountByPrivateDomain(t *testing.T) { +func Test_TestGetAccountByPrivateDomain(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("The SQLite store is not properly supported by Windows yet") } - t.Setenv("NETBIRD_STORE_ENGINE", string(SqliteStoreEngine)) - store, cleanUp, err := NewTestStoreFromSQL(context.Background(), "../testdata/store.sql", t.TempDir()) - t.Cleanup(cleanUp) - assert.NoError(t, err) + runTestForAllEngines(t, "../testdata/store.sql", func(t *testing.T, store Store) { + existingDomain := "test.com" - existingDomain := "test.com" + account, err := store.GetAccountByPrivateDomain(context.Background(), existingDomain) + require.NoError(t, err, "should found account") + require.Equal(t, existingDomain, account.Domain, "domains should match") - account, err := store.GetAccountByPrivateDomain(context.Background(), existingDomain) - require.NoError(t, err, "should found account") - require.Equal(t, existingDomain, account.Domain, "domains should match") - - _, err = store.GetAccountByPrivateDomain(context.Background(), "missing-domain.com") - require.Error(t, err, "should return error on domain lookup") - parsedErr, ok := status.FromError(err) - require.True(t, ok) - require.Equal(t, status.NotFound, parsedErr.Type(), "should return not found error") + _, err = store.GetAccountByPrivateDomain(context.Background(), "missing-domain.com") + require.Error(t, err, "should return error on domain lookup") + parsedErr, ok := status.FromError(err) + require.True(t, ok) + require.Equal(t, status.NotFound, parsedErr.Type(), "should return not found error") + }) } -func TestSqlite_GetTokenIDByHashedToken(t *testing.T) { +func Test_GetTokenIDByHashedToken(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("The SQLite store is not properly supported by Windows yet") } - t.Setenv("NETBIRD_STORE_ENGINE", string(SqliteStoreEngine)) - store, cleanUp, err := NewTestStoreFromSQL(context.Background(), "../testdata/store.sql", t.TempDir()) - t.Cleanup(cleanUp) - assert.NoError(t, err) + runTestForAllEngines(t, "../testdata/store.sql", func(t *testing.T, store Store) { + hashed := "SoMeHaShEdToKeN" + id := "9dj38s35-63fb-11ec-90d6-0242ac120003" - hashed := "SoMeHaShEdToKeN" - id := "9dj38s35-63fb-11ec-90d6-0242ac120003" + token, err := store.GetTokenIDByHashedToken(context.Background(), hashed) + require.NoError(t, err) + require.Equal(t, id, token) - token, err := store.GetTokenIDByHashedToken(context.Background(), hashed) - require.NoError(t, err) - require.Equal(t, id, token) - - _, err = store.GetTokenIDByHashedToken(context.Background(), "non-existing-hash") - require.Error(t, err) - parsedErr, ok := status.FromError(err) - require.True(t, ok) - require.Equal(t, status.NotFound, parsedErr.Type(), "should return not found error") + _, err = store.GetTokenIDByHashedToken(context.Background(), "non-existing-hash") + require.Error(t, err) + parsedErr, ok := status.FromError(err) + require.True(t, ok) + require.Equal(t, status.NotFound, parsedErr.Type(), "should return not found error") + }) } func TestMigrate(t *testing.T) { diff --git a/management/server/store/store.go b/management/server/store/store.go index 29ed22fa5..e074c4c60 100644 --- a/management/server/store/store.go +++ b/management/server/store/store.go @@ -9,11 +9,16 @@ import ( "os" "path" "path/filepath" + "regexp" "runtime" + "slices" "strings" "time" + "github.com/google/uuid" log "github.com/sirupsen/logrus" + "gorm.io/driver/mysql" + "gorm.io/driver/postgres" "gorm.io/driver/sqlite" "gorm.io/gorm" @@ -193,6 +198,8 @@ const ( mysqlDsnEnv = "NETBIRD_STORE_ENGINE_MYSQL_DSN" ) +var supportedEngines = []Engine{SqliteStoreEngine, PostgresStoreEngine, MysqlStoreEngine} + func getStoreEngineFromEnv() Engine { // NETBIRD_STORE_ENGINE supposed to be used in tests. Otherwise, rely on the config file. kind, ok := os.LookupEnv("NETBIRD_STORE_ENGINE") @@ -201,7 +208,7 @@ func getStoreEngineFromEnv() Engine { } value := Engine(strings.ToLower(kind)) - if value == SqliteStoreEngine || value == PostgresStoreEngine || value == MysqlStoreEngine { + if slices.Contains(supportedEngines, value) { return value } @@ -349,51 +356,126 @@ func NewTestStoreFromSQL(ctx context.Context, filename string, dataDir string) ( } func getSqlStoreEngine(ctx context.Context, store *SqlStore, kind Engine) (Store, func(), error) { - if kind == PostgresStoreEngine { - cleanUp, err := testutil.CreatePostgresTestContainer() - if err != nil { - return nil, nil, err + var cleanup func() + var err error + switch kind { + case PostgresStoreEngine: + store, cleanup, err = newReusedPostgresStore(ctx, store, kind) + case MysqlStoreEngine: + store, cleanup, err = newReusedMysqlStore(ctx, store, kind) + default: + cleanup = func() { + // sqlite doesn't need to be cleaned up } - - dsn, ok := os.LookupEnv(postgresDsnEnv) - if !ok { - return nil, nil, fmt.Errorf("%s is not set", postgresDsnEnv) - } - - store, err = NewPostgresqlStoreFromSqlStore(ctx, store, dsn, nil) - if err != nil { - return nil, nil, err - } - - return store, cleanUp, nil } - - if kind == MysqlStoreEngine { - cleanUp, err := testutil.CreateMysqlTestContainer() - if err != nil { - return nil, nil, err - } - - dsn, ok := os.LookupEnv(mysqlDsnEnv) - if !ok { - return nil, nil, fmt.Errorf("%s is not set", mysqlDsnEnv) - } - - store, err = NewMysqlStoreFromSqlStore(ctx, store, dsn, nil) - if err != nil { - return nil, nil, err - } - - return store, cleanUp, nil + if err != nil { + return nil, cleanup, fmt.Errorf("failed to create test store: %v", err) } closeConnection := func() { + cleanup() store.Close(ctx) } return store, closeConnection, nil } +func newReusedPostgresStore(ctx context.Context, store *SqlStore, kind Engine) (*SqlStore, func(), error) { + if envDsn, ok := os.LookupEnv(postgresDsnEnv); !ok || envDsn == "" { + var err error + _, err = testutil.CreatePostgresTestContainer() + if err != nil { + return nil, nil, err + } + } + + dsn, ok := os.LookupEnv(postgresDsnEnv) + if !ok { + return nil, nil, fmt.Errorf("%s is not set", postgresDsnEnv) + } + + db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{}) + if err != nil { + return nil, nil, fmt.Errorf("failed to open postgres connection: %v", err) + } + + dsn, cleanup, err := createRandomDB(dsn, db, kind) + if err != nil { + return nil, cleanup, err + } + + store, err = NewPostgresqlStoreFromSqlStore(ctx, store, dsn, nil) + if err != nil { + return nil, cleanup, err + } + + return store, cleanup, nil +} + +func newReusedMysqlStore(ctx context.Context, store *SqlStore, kind Engine) (*SqlStore, func(), error) { + if envDsn, ok := os.LookupEnv(mysqlDsnEnv); !ok || envDsn == "" { + var err error + _, err = testutil.CreateMysqlTestContainer() + if err != nil { + return nil, nil, err + } + } + + dsn, ok := os.LookupEnv(mysqlDsnEnv) + if !ok { + return nil, nil, fmt.Errorf("%s is not set", mysqlDsnEnv) + } + + db, err := gorm.Open(mysql.Open(dsn+"?charset=utf8&parseTime=True&loc=Local"), &gorm.Config{}) + if err != nil { + return nil, nil, fmt.Errorf("failed to open mysql connection: %v", err) + } + + dsn, cleanup, err := createRandomDB(dsn, db, kind) + if err != nil { + return nil, cleanup, err + } + + store, err = NewMysqlStoreFromSqlStore(ctx, store, dsn, nil) + if err != nil { + return nil, nil, err + } + + return store, cleanup, nil +} + +func createRandomDB(dsn string, db *gorm.DB, engine Engine) (string, func(), error) { + dbName := fmt.Sprintf("test_db_%s", strings.ReplaceAll(uuid.New().String(), "-", "_")) + + if err := db.Exec(fmt.Sprintf("CREATE DATABASE %s", dbName)).Error; err != nil { + return "", nil, fmt.Errorf("failed to create database: %v", err) + } + + var err error + cleanup := func() { + switch engine { + case PostgresStoreEngine: + err = db.Exec(fmt.Sprintf("DROP DATABASE %s WITH (FORCE)", dbName)).Error + case MysqlStoreEngine: + // err = killMySQLConnections(dsn, dbName) + err = db.Exec(fmt.Sprintf("DROP DATABASE %s", dbName)).Error + } + if err != nil { + log.Errorf("failed to drop database %s: %v", dbName, err) + panic(err) + } + sqlDB, _ := db.DB() + _ = sqlDB.Close() + } + + return replaceDBName(dsn, dbName), cleanup, nil +} + +func replaceDBName(dsn, newDBName string) string { + re := regexp.MustCompile(`(?P
    [:/@])(?P[^/?]+)(?P\?|$)`)
    +	return re.ReplaceAllString(dsn, `${pre}`+newDBName+`${post}`)
    +}
    +
     func loadSQL(db *gorm.DB, filepath string) error {
     	sqlContent, err := os.ReadFile(filepath)
     	if err != nil {
    diff --git a/management/server/testutil/store.go b/management/server/testutil/store.go
    index 16438cab8..8672efa7f 100644
    --- a/management/server/testutil/store.go
    +++ b/management/server/testutil/store.go
    @@ -22,7 +22,7 @@ func CreateMysqlTestContainer() (func(), error) {
     	myContainer, err := mysql.RunContainer(ctx,
     		testcontainers.WithImage("mlsmaycon/warmed-mysql:8"),
     		mysql.WithDatabase("testing"),
    -		mysql.WithUsername("testing"),
    +		mysql.WithUsername("root"),
     		mysql.WithPassword("testing"),
     		testcontainers.WithWaitStrategy(
     			wait.ForLog("/usr/sbin/mysqld: ready for connections").
    @@ -34,6 +34,7 @@ func CreateMysqlTestContainer() (func(), error) {
     	}
     
     	cleanup := func() {
    +		os.Unsetenv("NETBIRD_STORE_ENGINE_MYSQL_DSN")
     		timeoutCtx, cancelFunc := context.WithTimeout(ctx, 1*time.Second)
     		defer cancelFunc()
     		if err = myContainer.Terminate(timeoutCtx); err != nil {
    @@ -68,6 +69,7 @@ func CreatePostgresTestContainer() (func(), error) {
     	}
     
     	cleanup := func() {
    +		os.Unsetenv("NETBIRD_STORE_ENGINE_POSTGRES_DSN")
     		timeoutCtx, cancelFunc := context.WithTimeout(ctx, 1*time.Second)
     		defer cancelFunc()
     		if err = pgContainer.Terminate(timeoutCtx); err != nil {
    diff --git a/management/server/types/user.go b/management/server/types/user.go
    index 348fbfb22..5f7a4f2cb 100644
    --- a/management/server/types/user.go
    +++ b/management/server/types/user.go
    @@ -80,7 +80,7 @@ type User struct {
     	// AutoGroups is a list of Group IDs to auto-assign to peers registered by this user
     	AutoGroups []string                        `gorm:"serializer:json"`
     	PATs       map[string]*PersonalAccessToken `gorm:"-"`
    -	PATsG      []PersonalAccessToken           `json:"-" gorm:"foreignKey:UserID;references:id"`
    +	PATsG      []PersonalAccessToken           `json:"-" gorm:"foreignKey:UserID;references:id;constraint:OnDelete:CASCADE;"`
     	// Blocked indicates whether the user is blocked. Blocked users can't use the system.
     	Blocked bool
     	// LastLogin is the last time the user logged in to IdP
    diff --git a/relay/client/dialer/ws/ws.go b/relay/client/dialer/ws/ws.go
    index b007e24bb..cb525865b 100644
    --- a/relay/client/dialer/ws/ws.go
    +++ b/relay/client/dialer/ws/ws.go
    @@ -11,8 +11,8 @@ import (
     	"net/url"
     	"strings"
     
    -	log "github.com/sirupsen/logrus"
     	"github.com/coder/websocket"
    +	log "github.com/sirupsen/logrus"
     
     	"github.com/netbirdio/netbird/relay/server/listener/ws"
     	"github.com/netbirdio/netbird/util/embeddedroots"
    diff --git a/relay/server/listener/ws/conn.go b/relay/server/listener/ws/conn.go
    index 3466b2abd..3ec08945b 100644
    --- a/relay/server/listener/ws/conn.go
    +++ b/relay/server/listener/ws/conn.go
    @@ -8,8 +8,8 @@ import (
     	"sync"
     	"time"
     
    -	log "github.com/sirupsen/logrus"
     	"github.com/coder/websocket"
    +	log "github.com/sirupsen/logrus"
     )
     
     const (
    diff --git a/relay/server/listener/ws/listener.go b/relay/server/listener/ws/listener.go
    index 4597669dc..3a95951ee 100644
    --- a/relay/server/listener/ws/listener.go
    +++ b/relay/server/listener/ws/listener.go
    @@ -8,8 +8,8 @@ import (
     	"net"
     	"net/http"
     
    -	log "github.com/sirupsen/logrus"
     	"github.com/coder/websocket"
    +	log "github.com/sirupsen/logrus"
     )
     
     // URLPath is the path for the websocket connection.
    
    From 33cf9535b328c06d27385e5476653a40aba0fd84 Mon Sep 17 00:00:00 2001
    From: Carlos Hernandez 
    Date: Thu, 20 Feb 2025 02:55:44 -0700
    Subject: [PATCH 71/92] [client] Use go build to embed less icons (#3351)
    
    ---
     client/ui/client_ui.go     | 122 +++++--------------------------------
     client/ui/icons.go         |  43 +++++++++++++
     client/ui/icons_windows.go |  41 +++++++++++++
     3 files changed, 99 insertions(+), 107 deletions(-)
     create mode 100644 client/ui/icons.go
     create mode 100644 client/ui/icons_windows.go
    
    diff --git a/client/ui/client_ui.go b/client/ui/client_ui.go
    index 30fb8d764..618160128 100644
    --- a/client/ui/client_ui.go
    +++ b/client/ui/client_ui.go
    @@ -83,7 +83,7 @@ func main() {
     	}
     
     	a := app.NewWithID("NetBird")
    -	a.SetIcon(fyne.NewStaticResource("netbird", iconDisconnectedPNG))
    +	a.SetIcon(fyne.NewStaticResource("netbird", iconDisconnected))
     
     	if errorMSG != "" {
     		showErrorMSG(errorMSG)
    @@ -115,96 +115,24 @@ func main() {
     	}
     }
     
    -//go:embed netbird.ico
    -var iconAboutICO []byte
    -
    -//go:embed netbird.png
    -var iconAboutPNG []byte
    -
    -//go:embed netbird-systemtray-connected.ico
    -var iconConnectedICO []byte
    -
    -//go:embed netbird-systemtray-connected.png
    -var iconConnectedPNG []byte
    -
     //go:embed netbird-systemtray-connected-macos.png
     var iconConnectedMacOS []byte
     
    -//go:embed netbird-systemtray-connected-dark.ico
    -var iconConnectedDarkICO []byte
    -
    -//go:embed netbird-systemtray-connected-dark.png
    -var iconConnectedDarkPNG []byte
    -
    -//go:embed netbird-systemtray-disconnected.ico
    -var iconDisconnectedICO []byte
    -
    -//go:embed netbird-systemtray-disconnected.png
    -var iconDisconnectedPNG []byte
    -
     //go:embed netbird-systemtray-disconnected-macos.png
     var iconDisconnectedMacOS []byte
     
    -//go:embed netbird-systemtray-update-disconnected.ico
    -var iconUpdateDisconnectedICO []byte
    -
    -//go:embed netbird-systemtray-update-disconnected.png
    -var iconUpdateDisconnectedPNG []byte
    -
     //go:embed netbird-systemtray-update-disconnected-macos.png
     var iconUpdateDisconnectedMacOS []byte
     
    -//go:embed netbird-systemtray-update-disconnected-dark.ico
    -var iconUpdateDisconnectedDarkICO []byte
    -
    -//go:embed netbird-systemtray-update-disconnected-dark.png
    -var iconUpdateDisconnectedDarkPNG []byte
    -
    -//go:embed netbird-systemtray-update-connected.ico
    -var iconUpdateConnectedICO []byte
    -
    -//go:embed netbird-systemtray-update-connected.png
    -var iconUpdateConnectedPNG []byte
    -
     //go:embed netbird-systemtray-update-connected-macos.png
     var iconUpdateConnectedMacOS []byte
     
    -//go:embed netbird-systemtray-update-connected-dark.ico
    -var iconUpdateConnectedDarkICO []byte
    -
    -//go:embed netbird-systemtray-update-connected-dark.png
    -var iconUpdateConnectedDarkPNG []byte
    -
    -//go:embed netbird-systemtray-connecting.ico
    -var iconConnectingICO []byte
    -
    -//go:embed netbird-systemtray-connecting.png
    -var iconConnectingPNG []byte
    -
     //go:embed netbird-systemtray-connecting-macos.png
     var iconConnectingMacOS []byte
     
    -//go:embed netbird-systemtray-connecting-dark.ico
    -var iconConnectingDarkICO []byte
    -
    -//go:embed netbird-systemtray-connecting-dark.png
    -var iconConnectingDarkPNG []byte
    -
    -//go:embed netbird-systemtray-error.ico
    -var iconErrorICO []byte
    -
    -//go:embed netbird-systemtray-error.png
    -var iconErrorPNG []byte
    -
     //go:embed netbird-systemtray-error-macos.png
     var iconErrorMacOS []byte
     
    -//go:embed netbird-systemtray-error-dark.ico
    -var iconErrorDarkICO []byte
    -
    -//go:embed netbird-systemtray-error-dark.png
    -var iconErrorDarkPNG []byte
    -
     type serviceClient struct {
     	ctx  context.Context
     	addr string
    @@ -298,40 +226,21 @@ func newServiceClient(addr string, a fyne.App, showSettings bool, showRoutes boo
     }
     
     func (s *serviceClient) setNewIcons() {
    -	if runtime.GOOS == "windows" {
    -		s.icAbout = iconAboutICO
    -		if s.app.Settings().ThemeVariant() == theme.VariantDark {
    -			s.icConnected = iconConnectedDarkICO
    -			s.icDisconnected = iconDisconnectedICO
    -			s.icUpdateConnected = iconUpdateConnectedDarkICO
    -			s.icUpdateDisconnected = iconUpdateDisconnectedDarkICO
    -			s.icConnecting = iconConnectingDarkICO
    -			s.icError = iconErrorDarkICO
    -		} else {
    -			s.icConnected = iconConnectedICO
    -			s.icDisconnected = iconDisconnectedICO
    -			s.icUpdateConnected = iconUpdateConnectedICO
    -			s.icUpdateDisconnected = iconUpdateDisconnectedICO
    -			s.icConnecting = iconConnectingICO
    -			s.icError = iconErrorICO
    -		}
    +	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.icAbout = iconAboutPNG
    -		if s.app.Settings().ThemeVariant() == theme.VariantDark {
    -			s.icConnected = iconConnectedDarkPNG
    -			s.icDisconnected = iconDisconnectedPNG
    -			s.icUpdateConnected = iconUpdateConnectedDarkPNG
    -			s.icUpdateDisconnected = iconUpdateDisconnectedDarkPNG
    -			s.icConnecting = iconConnectingDarkPNG
    -			s.icError = iconErrorDarkPNG
    -		} else {
    -			s.icConnected = iconConnectedPNG
    -			s.icDisconnected = iconDisconnectedPNG
    -			s.icUpdateConnected = iconUpdateConnectedPNG
    -			s.icUpdateDisconnected = iconUpdateDisconnectedPNG
    -			s.icConnecting = iconConnectingPNG
    -			s.icError = iconErrorPNG
    -		}
    +		s.icConnected = iconConnected
    +		s.icDisconnected = iconDisconnected
    +		s.icUpdateConnected = iconUpdateConnected
    +		s.icUpdateDisconnected = iconUpdateDisconnected
    +		s.icConnecting = iconConnecting
    +		s.icError = iconError
     	}
     }
     
    @@ -622,7 +531,6 @@ func (s *serviceClient) updateStatus() error {
     		Stop:                backoff.Stop,
     		Clock:               backoff.SystemClock,
     	})
    -
     	if err != nil {
     		return err
     	}
    diff --git a/client/ui/icons.go b/client/ui/icons.go
    new file mode 100644
    index 000000000..6f3a9dbc9
    --- /dev/null
    +++ b/client/ui/icons.go
    @@ -0,0 +1,43 @@
    +//go:build !(linux && 386) && !windows
    +
    +package main
    +
    +import (
    +	_ "embed"
    +)
    +
    +//go:embed netbird.png
    +var iconAbout []byte
    +
    +//go:embed netbird-systemtray-connected.png
    +var iconConnected []byte
    +
    +//go:embed netbird-systemtray-connected-dark.png
    +var iconConnectedDark []byte
    +
    +//go:embed netbird-systemtray-disconnected.png
    +var iconDisconnected []byte
    +
    +//go:embed netbird-systemtray-update-disconnected.png
    +var iconUpdateDisconnected []byte
    +
    +//go:embed netbird-systemtray-update-disconnected-dark.png
    +var iconUpdateDisconnectedDark []byte
    +
    +//go:embed netbird-systemtray-update-connected.png
    +var iconUpdateConnected []byte
    +
    +//go:embed netbird-systemtray-update-connected-dark.png
    +var iconUpdateConnectedDark []byte
    +
    +//go:embed netbird-systemtray-connecting.png
    +var iconConnecting []byte
    +
    +//go:embed netbird-systemtray-connecting-dark.png
    +var iconConnectingDark []byte
    +
    +//go:embed netbird-systemtray-error.png
    +var iconError []byte
    +
    +//go:embed netbird-systemtray-error-dark.png
    +var iconErrorDark []byte
    diff --git a/client/ui/icons_windows.go b/client/ui/icons_windows.go
    new file mode 100644
    index 000000000..a2a924763
    --- /dev/null
    +++ b/client/ui/icons_windows.go
    @@ -0,0 +1,41 @@
    +package main
    +
    +import (
    + _ "embed"
    +)
    +
    +//go:embed netbird.ico
    +var iconAbout []byte
    +
    +//go:embed netbird-systemtray-connected.ico
    +var iconConnected []byte
    +
    +//go:embed netbird-systemtray-connected-dark.ico
    +var iconConnectedDark []byte
    +
    +//go:embed netbird-systemtray-disconnected.ico
    +var iconDisconnected []byte
    +
    +//go:embed netbird-systemtray-update-disconnected.ico
    +var iconUpdateDisconnected []byte
    +
    +//go:embed netbird-systemtray-update-disconnected-dark.ico
    +var iconUpdateDisconnectedDark []byte
    +
    +//go:embed netbird-systemtray-update-connected.ico
    +var iconUpdateConnected []byte
    +
    +//go:embed netbird-systemtray-update-connected-dark.ico
    +var iconUpdateConnectedDark []byte
    +
    +//go:embed netbird-systemtray-connecting.ico
    +var iconConnecting []byte
    +
    +//go:embed netbird-systemtray-connecting-dark.ico
    +var iconConnectingDark []byte
    +
    +//go:embed netbird-systemtray-error.ico
    +var iconError []byte
    +
    +//go:embed netbird-systemtray-error-dark.ico
    +var iconErrorDark []byte
    
    From 87311074f1a67f3f54ed0fce25c51cfeda11f7e6 Mon Sep 17 00:00:00 2001
    From: =?UTF-8?q?C=C3=A9sar=20Gon=C3=A7alves?= 
    Date: Thu, 20 Feb 2025 09:56:22 +0000
    Subject: [PATCH 72/92] [misc] improvement(template): add traefik labels to
     relay (#3333)
    
    ---
     infrastructure_files/docker-compose.yml.tmpl.traefik | 4 ++++
     1 file changed, 4 insertions(+)
    
    diff --git a/infrastructure_files/docker-compose.yml.tmpl.traefik b/infrastructure_files/docker-compose.yml.tmpl.traefik
    index 71471c3ef..dcd3f955c 100644
    --- a/infrastructure_files/docker-compose.yml.tmpl.traefik
    +++ b/infrastructure_files/docker-compose.yml.tmpl.traefik
    @@ -67,6 +67,10 @@ services:
           options:
             max-size: "500m"
             max-file: "2"
    +    labels:
    +    - traefik.enable=true
    +    - traefik.http.routers.netbird-relay.rule=Host(`$NETBIRD_DOMAIN`) && PathPrefix(`/relay`)
    +    - traefik.http.services.netbird-relay.loadbalancer.server.port=$NETBIRD_RELAY_PORT
     
       # Management
       management:
    
    From 62a0c358f9ea9e1c69dd30a5328551f18e18f8a7 Mon Sep 17 00:00:00 2001
    From: Viktor Liu <17948409+lixmal@users.noreply.github.com>
    Date: Thu, 20 Feb 2025 11:00:02 +0100
    Subject: [PATCH 73/92] [client] Add UI client event notifications (#3207)
    
    ---
     client/cmd/status.go                   |   35 +-
     client/cmd/status_event.go             |   69 ++
     client/cmd/status_test.go              |   28 +-
     client/internal/config.go              |   14 +
     client/internal/dns/upstream.go        |    9 +
     client/internal/peer/status.go         |  125 ++
     client/internal/routemanager/client.go |   76 +-
     client/proto/daemon.pb.go              | 1496 ++++++++++++++++--------
     client/proto/daemon.proto              |   42 +
     client/proto/daemon_grpc.pb.go         |  102 +-
     client/server/event.go                 |   36 +
     client/server/server.go                |   29 +-
     client/ui/client_ui.go                 |   48 +-
     client/ui/event/event.go               |  151 +++
     14 files changed, 1685 insertions(+), 575 deletions(-)
     create mode 100644 client/cmd/status_event.go
     create mode 100644 client/server/event.go
     create mode 100644 client/ui/event/event.go
    
    diff --git a/client/cmd/status.go b/client/cmd/status.go
    index fa4bff77b..bf4588ce4 100644
    --- a/client/cmd/status.go
    +++ b/client/cmd/status.go
    @@ -39,7 +39,6 @@ type peerStateDetailOutput struct {
     	TransferSent           int64            `json:"transferSent" yaml:"transferSent"`
     	Latency                time.Duration    `json:"latency" yaml:"latency"`
     	RosenpassEnabled       bool             `json:"quantumResistance" yaml:"quantumResistance"`
    -	Routes                 []string         `json:"routes" yaml:"routes"`
     	Networks               []string         `json:"networks" yaml:"networks"`
     }
     
    @@ -98,9 +97,9 @@ type statusOutputOverview struct {
     	FQDN                string                     `json:"fqdn" yaml:"fqdn"`
     	RosenpassEnabled    bool                       `json:"quantumResistance" yaml:"quantumResistance"`
     	RosenpassPermissive bool                       `json:"quantumResistancePermissive" yaml:"quantumResistancePermissive"`
    -	Routes              []string                   `json:"routes" yaml:"routes"`
     	Networks            []string                   `json:"networks" yaml:"networks"`
     	NSServerGroups      []nsServerGroupStateOutput `json:"dnsServers" yaml:"dnsServers"`
    +	Events              []systemEventOutput        `json:"events" yaml:"events"`
     }
     
     var (
    @@ -284,9 +283,9 @@ func convertToStatusOutputOverview(resp *proto.StatusResponse) statusOutputOverv
     		FQDN:                pbFullStatus.GetLocalPeerState().GetFqdn(),
     		RosenpassEnabled:    pbFullStatus.GetLocalPeerState().GetRosenpassEnabled(),
     		RosenpassPermissive: pbFullStatus.GetLocalPeerState().GetRosenpassPermissive(),
    -		Routes:              pbFullStatus.GetLocalPeerState().GetNetworks(),
     		Networks:            pbFullStatus.GetLocalPeerState().GetNetworks(),
     		NSServerGroups:      mapNSGroups(pbFullStatus.GetDnsServers()),
    +		Events:              mapEvents(pbFullStatus.GetEvents()),
     	}
     
     	if anonymizeFlag {
    @@ -393,7 +392,6 @@ func mapPeers(peers []*proto.PeerState) peersStateOutput {
     			TransferSent:           transferSent,
     			Latency:                pbPeerState.GetLatency().AsDuration(),
     			RosenpassEnabled:       pbPeerState.GetRosenpassEnabled(),
    -			Routes:                 pbPeerState.GetNetworks(),
     			Networks:               pbPeerState.GetNetworks(),
     		}
     
    @@ -559,7 +557,6 @@ func parseGeneralSummary(overview statusOutputOverview, showURL bool, showRelays
     			"NetBird IP: %s\n"+
     			"Interface type: %s\n"+
     			"Quantum resistance: %s\n"+
    -			"Routes: %s\n"+
     			"Networks: %s\n"+
     			"Peers count: %s\n",
     		fmt.Sprintf("%s/%s%s", goos, goarch, goarm),
    @@ -574,7 +571,6 @@ func parseGeneralSummary(overview statusOutputOverview, showURL bool, showRelays
     		interfaceTypeString,
     		rosenpassEnabledStatus,
     		networks,
    -		networks,
     		peersCountString,
     	)
     	return summary
    @@ -582,13 +578,17 @@ func parseGeneralSummary(overview statusOutputOverview, showURL bool, showRelays
     
     func parseToFullDetailSummary(overview statusOutputOverview) string {
     	parsedPeersString := parsePeers(overview.Peers, overview.RosenpassEnabled, overview.RosenpassPermissive)
    +	parsedEventsString := parseEvents(overview.Events)
     	summary := parseGeneralSummary(overview, true, true, true)
     
     	return fmt.Sprintf(
     		"Peers detail:"+
    +			"%s\n"+
    +			"Events:"+
     			"%s\n"+
     			"%s",
     		parsedPeersString,
    +		parsedEventsString,
     		summary,
     	)
     }
    @@ -657,7 +657,6 @@ func parsePeers(peers peersStateOutput, rosenpassEnabled, rosenpassPermissive bo
     				"  Last WireGuard handshake: %s\n"+
     				"  Transfer status (received/sent) %s/%s\n"+
     				"  Quantum resistance: %s\n"+
    -				"  Routes: %s\n"+
     				"  Networks: %s\n"+
     				"  Latency: %s\n",
     			peerState.FQDN,
    @@ -676,7 +675,6 @@ func parsePeers(peers peersStateOutput, rosenpassEnabled, rosenpassPermissive bo
     			toIEC(peerState.TransferSent),
     			rosenpassEnabledStatus,
     			networks,
    -			networks,
     			peerState.Latency.String(),
     		)
     
    @@ -825,14 +823,6 @@ func anonymizePeerDetail(a *anonymize.Anonymizer, peer *peerStateDetailOutput) {
     	for i, route := range peer.Networks {
     		peer.Networks[i] = a.AnonymizeRoute(route)
     	}
    -
    -	for i, route := range peer.Routes {
    -		peer.Routes[i] = a.AnonymizeIPString(route)
    -	}
    -
    -	for i, route := range peer.Routes {
    -		peer.Routes[i] = a.AnonymizeRoute(route)
    -	}
     }
     
     func anonymizeOverview(a *anonymize.Anonymizer, overview *statusOutputOverview) {
    @@ -870,9 +860,14 @@ func anonymizeOverview(a *anonymize.Anonymizer, overview *statusOutputOverview)
     		overview.Networks[i] = a.AnonymizeRoute(route)
     	}
     
    -	for i, route := range overview.Routes {
    -		overview.Routes[i] = a.AnonymizeRoute(route)
    -	}
    -
     	overview.FQDN = a.AnonymizeDomain(overview.FQDN)
    +
    +	for i, event := range overview.Events {
    +		overview.Events[i].Message = a.AnonymizeString(event.Message)
    +		overview.Events[i].UserMessage = a.AnonymizeString(event.UserMessage)
    +
    +		for k, v := range event.Metadata {
    +			event.Metadata[k] = a.AnonymizeString(v)
    +		}
    +	}
     }
    diff --git a/client/cmd/status_event.go b/client/cmd/status_event.go
    new file mode 100644
    index 000000000..9331570e6
    --- /dev/null
    +++ b/client/cmd/status_event.go
    @@ -0,0 +1,69 @@
    +package cmd
    +
    +import (
    +	"fmt"
    +	"sort"
    +	"strings"
    +	"time"
    +
    +	"github.com/netbirdio/netbird/client/proto"
    +)
    +
    +type systemEventOutput struct {
    +	ID          string            `json:"id" yaml:"id"`
    +	Severity    string            `json:"severity" yaml:"severity"`
    +	Category    string            `json:"category" yaml:"category"`
    +	Message     string            `json:"message" yaml:"message"`
    +	UserMessage string            `json:"userMessage" yaml:"userMessage"`
    +	Timestamp   time.Time         `json:"timestamp" yaml:"timestamp"`
    +	Metadata    map[string]string `json:"metadata" yaml:"metadata"`
    +}
    +
    +func mapEvents(protoEvents []*proto.SystemEvent) []systemEventOutput {
    +	events := make([]systemEventOutput, len(protoEvents))
    +	for i, event := range protoEvents {
    +		events[i] = systemEventOutput{
    +			ID:          event.GetId(),
    +			Severity:    event.GetSeverity().String(),
    +			Category:    event.GetCategory().String(),
    +			Message:     event.GetMessage(),
    +			UserMessage: event.GetUserMessage(),
    +			Timestamp:   event.GetTimestamp().AsTime(),
    +			Metadata:    event.GetMetadata(),
    +		}
    +	}
    +	return events
    +}
    +
    +func parseEvents(events []systemEventOutput) string {
    +	if len(events) == 0 {
    +		return " No events recorded"
    +	}
    +
    +	var eventsString strings.Builder
    +	for _, event := range events {
    +		timeStr := timeAgo(event.Timestamp)
    +
    +		metadataStr := ""
    +		if len(event.Metadata) > 0 {
    +			pairs := make([]string, 0, len(event.Metadata))
    +			for k, v := range event.Metadata {
    +				pairs = append(pairs, fmt.Sprintf("%s: %s", k, v))
    +			}
    +			sort.Strings(pairs)
    +			metadataStr = fmt.Sprintf("\n    Metadata: %s", strings.Join(pairs, ", "))
    +		}
    +
    +		eventsString.WriteString(fmt.Sprintf("\n  [%s] %s (%s)"+
    +			"\n    Message: %s"+
    +			"\n    Time: %s%s",
    +			event.Severity,
    +			event.Category,
    +			event.ID,
    +			event.Message,
    +			timeStr,
    +			metadataStr,
    +		))
    +	}
    +	return eventsString.String()
    +}
    diff --git a/client/cmd/status_test.go b/client/cmd/status_test.go
    index 1f1e95726..1e240d192 100644
    --- a/client/cmd/status_test.go
    +++ b/client/cmd/status_test.go
    @@ -146,9 +146,6 @@ var overview = statusOutputOverview{
     				LastWireguardHandshake: time.Date(2001, 1, 1, 1, 1, 2, 0, time.UTC),
     				TransferReceived:       200,
     				TransferSent:           100,
    -				Routes: []string{
    -					"10.1.0.0/24",
    -				},
     				Networks: []string{
     					"10.1.0.0/24",
     				},
    @@ -176,6 +173,7 @@ var overview = statusOutputOverview{
     			},
     		},
     	},
    +	Events:        []systemEventOutput{},
     	CliVersion:    version.NetbirdVersion(),
     	DaemonVersion: "0.14.1",
     	ManagementState: managementStateOutput{
    @@ -230,9 +228,6 @@ var overview = statusOutputOverview{
     			Error:   "timeout",
     		},
     	},
    -	Routes: []string{
    -		"10.10.0.0/24",
    -	},
     	Networks: []string{
     		"10.10.0.0/24",
     	},
    @@ -299,9 +294,6 @@ func TestParsingToJSON(t *testing.T) {
                     "transferSent": 100,
     				"latency": 10000000,
                     "quantumResistance": false,
    -                "routes": [
    -                  "10.1.0.0/24"
    -                ],
                     "networks": [
                       "10.1.0.0/24"
                     ]
    @@ -327,7 +319,6 @@ func TestParsingToJSON(t *testing.T) {
                     "transferSent": 1000,
     				"latency": 10000000,
                     "quantumResistance": false,
    -                "routes": null,
                     "networks": null
                   }
                 ]
    @@ -366,9 +357,6 @@ func TestParsingToJSON(t *testing.T) {
               "fqdn": "some-localhost.awesome-domain.com",
               "quantumResistance": false,
               "quantumResistancePermissive": false,
    -          "routes": [
    -            "10.10.0.0/24"
    -          ],
               "networks": [
                 "10.10.0.0/24"
               ],
    @@ -393,7 +381,8 @@ func TestParsingToJSON(t *testing.T) {
                   "enabled": false,
                   "error": "timeout"
                 }
    -          ]
    +          ],
    +          "events": []
             }`
     	// @formatter:on
     
    @@ -429,8 +418,6 @@ func TestParsingToYAML(t *testing.T) {
               transferSent: 100
               latency: 10ms
               quantumResistance: false
    -          routes:
    -            - 10.1.0.0/24
               networks:
                 - 10.1.0.0/24
             - fqdn: peer-2.awesome-domain.com
    @@ -451,7 +438,6 @@ func TestParsingToYAML(t *testing.T) {
               transferSent: 1000
               latency: 10ms
               quantumResistance: false
    -          routes: []
               networks: []
     cliVersion: development
     daemonVersion: 0.14.1
    @@ -479,8 +465,6 @@ usesKernelInterface: true
     fqdn: some-localhost.awesome-domain.com
     quantumResistance: false
     quantumResistancePermissive: false
    -routes:
    -    - 10.10.0.0/24
     networks:
         - 10.10.0.0/24
     dnsServers:
    @@ -497,6 +481,7 @@ dnsServers:
             - example.net
           enabled: false
           error: timeout
    +events: []
     `
     
     	assert.Equal(t, expectedYAML, yaml)
    @@ -526,7 +511,6 @@ func TestParsingToDetail(t *testing.T) {
       Last WireGuard handshake: %s
       Transfer status (received/sent) 200 B/100 B
       Quantum resistance: false
    -  Routes: 10.1.0.0/24
       Networks: 10.1.0.0/24
       Latency: 10ms
     
    @@ -543,10 +527,10 @@ func TestParsingToDetail(t *testing.T) {
       Last WireGuard handshake: %s
       Transfer status (received/sent) 2.0 KiB/1000 B
       Quantum resistance: false
    -  Routes: -
       Networks: -
       Latency: 10ms
     
    +Events: No events recorded
     OS: %s/%s
     Daemon version: 0.14.1
     CLI version: %s
    @@ -562,7 +546,6 @@ FQDN: some-localhost.awesome-domain.com
     NetBird IP: 192.168.178.100/16
     Interface type: Kernel
     Quantum resistance: false
    -Routes: 10.10.0.0/24
     Networks: 10.10.0.0/24
     Peers count: 2/2 Connected
     `, lastConnectionUpdate1, lastHandshake1, lastConnectionUpdate2, lastHandshake2, runtime.GOOS, runtime.GOARCH, overview.CliVersion)
    @@ -584,7 +567,6 @@ FQDN: some-localhost.awesome-domain.com
     NetBird IP: 192.168.178.100/16
     Interface type: Kernel
     Quantum resistance: false
    -Routes: 10.10.0.0/24
     Networks: 10.10.0.0/24
     Peers count: 2/2 Connected
     `
    diff --git a/client/internal/config.go b/client/internal/config.go
    index 3196c4e04..5703539cc 100644
    --- a/client/internal/config.go
    +++ b/client/internal/config.go
    @@ -68,6 +68,8 @@ type ConfigInput struct {
     	DisableFirewall     *bool
     
     	BlockLANAccess *bool
    +
    +	DisableNotifications *bool
     }
     
     // Config Configuration type
    @@ -93,6 +95,8 @@ type Config struct {
     
     	BlockLANAccess bool
     
    +	DisableNotifications bool
    +
     	// SSHKey is a private SSH key in a PEM format
     	SSHKey string
     
    @@ -469,6 +473,16 @@ func (config *Config) apply(input ConfigInput) (updated bool, err error) {
     		updated = true
     	}
     
    +	if input.DisableNotifications != nil && *input.DisableNotifications != config.DisableNotifications {
    +		if *input.DisableNotifications {
    +			log.Infof("disabling notifications")
    +		} else {
    +			log.Infof("enabling notifications")
    +		}
    +		config.DisableNotifications = *input.DisableNotifications
    +		updated = true
    +	}
    +
     	if input.ClientCertKeyPath != "" {
     		config.ClientCertKeyPath = input.ClientCertKeyPath
     		updated = true
    diff --git a/client/internal/dns/upstream.go b/client/internal/dns/upstream.go
    index 4c69a173d..d269107e3 100644
    --- a/client/internal/dns/upstream.go
    +++ b/client/internal/dns/upstream.go
    @@ -19,6 +19,7 @@ import (
     	log "github.com/sirupsen/logrus"
     
     	"github.com/netbirdio/netbird/client/internal/peer"
    +	"github.com/netbirdio/netbird/client/proto"
     )
     
     const (
    @@ -230,6 +231,14 @@ func (u *upstreamResolverBase) probeAvailability() {
     	// didn't find a working upstream server, let's disable and try later
     	if !success {
     		u.disable(errors.ErrorOrNil())
    +
    +		u.statusRecorder.PublishEvent(
    +			proto.SystemEvent_WARNING,
    +			proto.SystemEvent_DNS,
    +			"All upstream servers failed",
    +			"Unable to reach one or more DNS servers. This might affect your ability to connect to some services.",
    +			map[string]string{"upstreams": strings.Join(u.upstreamServers, ", ")},
    +		)
     	}
     }
     
    diff --git a/client/internal/peer/status.go b/client/internal/peer/status.go
    index 311ddbd7f..e9976270c 100644
    --- a/client/internal/peer/status.go
    +++ b/client/internal/peer/status.go
    @@ -7,21 +7,31 @@ import (
     	"sync"
     	"time"
     
    +	"github.com/google/uuid"
    +	log "github.com/sirupsen/logrus"
     	"golang.org/x/exp/maps"
     	"google.golang.org/grpc/codes"
     	gstatus "google.golang.org/grpc/status"
    +	"google.golang.org/protobuf/types/known/timestamppb"
     
     	"github.com/netbirdio/netbird/client/iface/configurer"
     	"github.com/netbirdio/netbird/client/internal/relay"
    +	"github.com/netbirdio/netbird/client/proto"
     	"github.com/netbirdio/netbird/management/domain"
     	relayClient "github.com/netbirdio/netbird/relay/client"
     )
     
    +const eventQueueSize = 10
    +
     type ResolvedDomainInfo struct {
     	Prefixes     []netip.Prefix
     	ParentDomain domain.Domain
     }
     
    +type EventListener interface {
    +	OnEvent(event *proto.SystemEvent)
    +}
    +
     // State contains the latest state of a peer
     type State struct {
     	Mux                        *sync.RWMutex
    @@ -157,6 +167,10 @@ type Status struct {
     	peerListChangedForNotification bool
     
     	relayMgr *relayClient.Manager
    +
    +	eventMux     sync.RWMutex
    +	eventStreams map[string]chan *proto.SystemEvent
    +	eventQueue   *EventQueue
     }
     
     // NewRecorder returns a new Status instance
    @@ -164,6 +178,8 @@ func NewRecorder(mgmAddress string) *Status {
     	return &Status{
     		peers:                 make(map[string]State),
     		changeNotify:          make(map[string]chan struct{}),
    +		eventStreams:          make(map[string]chan *proto.SystemEvent),
    +		eventQueue:            NewEventQueue(eventQueueSize),
     		offlinePeers:          make([]State, 0),
     		notifier:              newNotifier(),
     		mgmAddress:            mgmAddress,
    @@ -806,3 +822,112 @@ func (d *Status) notifyAddressChanged() {
     func (d *Status) numOfPeers() int {
     	return len(d.peers) + len(d.offlinePeers)
     }
    +
    +// PublishEvent adds an event to the queue and distributes it to all subscribers
    +func (d *Status) PublishEvent(
    +	severity proto.SystemEvent_Severity,
    +	category proto.SystemEvent_Category,
    +	msg string,
    +	userMsg string,
    +	metadata map[string]string,
    +) {
    +	event := &proto.SystemEvent{
    +		Id:          uuid.New().String(),
    +		Severity:    severity,
    +		Category:    category,
    +		Message:     msg,
    +		UserMessage: userMsg,
    +		Metadata:    metadata,
    +		Timestamp:   timestamppb.Now(),
    +	}
    +
    +	d.eventMux.Lock()
    +	defer d.eventMux.Unlock()
    +
    +	d.eventQueue.Add(event)
    +
    +	for _, stream := range d.eventStreams {
    +		select {
    +		case stream <- event:
    +		default:
    +			log.Debugf("event stream buffer full, skipping event: %v", event)
    +		}
    +	}
    +
    +	log.Debugf("event published: %v", event)
    +}
    +
    +// SubscribeToEvents returns a new event subscription
    +func (d *Status) SubscribeToEvents() *EventSubscription {
    +	d.eventMux.Lock()
    +	defer d.eventMux.Unlock()
    +
    +	id := uuid.New().String()
    +	stream := make(chan *proto.SystemEvent, 10)
    +	d.eventStreams[id] = stream
    +
    +	return &EventSubscription{
    +		id:     id,
    +		events: stream,
    +	}
    +}
    +
    +// UnsubscribeFromEvents removes an event subscription
    +func (d *Status) UnsubscribeFromEvents(sub *EventSubscription) {
    +	if sub == nil {
    +		return
    +	}
    +
    +	d.eventMux.Lock()
    +	defer d.eventMux.Unlock()
    +
    +	if stream, exists := d.eventStreams[sub.id]; exists {
    +		close(stream)
    +		delete(d.eventStreams, sub.id)
    +	}
    +}
    +
    +// GetEventHistory returns all events in the queue
    +func (d *Status) GetEventHistory() []*proto.SystemEvent {
    +	return d.eventQueue.GetAll()
    +}
    +
    +type EventQueue struct {
    +	maxSize int
    +	events  []*proto.SystemEvent
    +	mutex   sync.RWMutex
    +}
    +
    +func NewEventQueue(size int) *EventQueue {
    +	return &EventQueue{
    +		maxSize: size,
    +		events:  make([]*proto.SystemEvent, 0, size),
    +	}
    +}
    +
    +func (q *EventQueue) Add(event *proto.SystemEvent) {
    +	q.mutex.Lock()
    +	defer q.mutex.Unlock()
    +
    +	q.events = append(q.events, event)
    +
    +	if len(q.events) > q.maxSize {
    +		q.events = q.events[len(q.events)-q.maxSize:]
    +	}
    +}
    +
    +func (q *EventQueue) GetAll() []*proto.SystemEvent {
    +	q.mutex.RLock()
    +	defer q.mutex.RUnlock()
    +
    +	return slices.Clone(q.events)
    +}
    +
    +type EventSubscription struct {
    +	id     string
    +	events chan *proto.SystemEvent
    +}
    +
    +func (s *EventSubscription) Events() <-chan *proto.SystemEvent {
    +	return s.events
    +}
    diff --git a/client/internal/routemanager/client.go b/client/internal/routemanager/client.go
    index faf0fadaa..3238dd831 100644
    --- a/client/internal/routemanager/client.go
    +++ b/client/internal/routemanager/client.go
    @@ -19,6 +19,7 @@ import (
     	"github.com/netbirdio/netbird/client/internal/routemanager/dynamic"
     	"github.com/netbirdio/netbird/client/internal/routemanager/refcounter"
     	"github.com/netbirdio/netbird/client/internal/routemanager/static"
    +	"github.com/netbirdio/netbird/client/proto"
     	"github.com/netbirdio/netbird/route"
     )
     
    @@ -28,6 +29,15 @@ const (
     	handlerTypeStatic
     )
     
    +type reason int
    +
    +const (
    +	reasonUnknown reason = iota
    +	reasonRouteUpdate
    +	reasonPeerUpdate
    +	reasonShutdown
    +)
    +
     type routerPeerStatus struct {
     	connected bool
     	relayed   bool
    @@ -255,7 +265,7 @@ func (c *clientNetwork) removeRouteFromWireGuardPeer() error {
     	return nil
     }
     
    -func (c *clientNetwork) removeRouteFromPeerAndSystem() error {
    +func (c *clientNetwork) removeRouteFromPeerAndSystem(rsn reason) error {
     	if c.currentChosen == nil {
     		return nil
     	}
    @@ -269,17 +279,19 @@ func (c *clientNetwork) removeRouteFromPeerAndSystem() error {
     		merr = multierror.Append(merr, fmt.Errorf("remove route: %w", err))
     	}
     
    +	c.disconnectEvent(rsn)
    +
     	return nberrors.FormatErrorOrNil(merr)
     }
     
    -func (c *clientNetwork) recalculateRouteAndUpdatePeerAndSystem() error {
    +func (c *clientNetwork) recalculateRouteAndUpdatePeerAndSystem(rsn reason) error {
     	routerPeerStatuses := c.getRouterPeerStatuses()
     
     	newChosenID := c.getBestRouteFromStatuses(routerPeerStatuses)
     
     	// If no route is chosen, remove the route from the peer and system
     	if newChosenID == "" {
    -		if err := c.removeRouteFromPeerAndSystem(); err != nil {
    +		if err := c.removeRouteFromPeerAndSystem(rsn); err != nil {
     			return fmt.Errorf("remove route for peer %s: %w", c.currentChosen.Peer, err)
     		}
     
    @@ -319,6 +331,58 @@ func (c *clientNetwork) recalculateRouteAndUpdatePeerAndSystem() error {
     	return nil
     }
     
    +func (c *clientNetwork) disconnectEvent(rsn reason) {
    +	var defaultRoute bool
    +	for _, r := range c.routes {
    +		if r.Network.Bits() == 0 {
    +			defaultRoute = true
    +			break
    +		}
    +	}
    +
    +	if !defaultRoute {
    +		return
    +	}
    +
    +	var severity proto.SystemEvent_Severity
    +	var message string
    +	var userMessage string
    +	meta := make(map[string]string)
    +
    +	switch rsn {
    +	case reasonShutdown:
    +		severity = proto.SystemEvent_INFO
    +		message = "Default route removed"
    +		userMessage = "Exit node disconnected."
    +		meta["network"] = c.handler.String()
    +	case reasonRouteUpdate:
    +		severity = proto.SystemEvent_INFO
    +		message = "Default route updated due to configuration change"
    +		meta["network"] = c.handler.String()
    +	case reasonPeerUpdate:
    +		severity = proto.SystemEvent_WARNING
    +		message = "Default route disconnected due to peer unreachability"
    +		userMessage = "Exit node connection lost. Your internet access might be affected."
    +		if c.currentChosen != nil {
    +			meta["peer"] = c.currentChosen.Peer
    +			meta["network"] = c.handler.String()
    +		}
    +	default:
    +		severity = proto.SystemEvent_ERROR
    +		message = "Default route disconnected for unknown reason"
    +		userMessage = "Exit node disconnected for unknown reasons."
    +		meta["network"] = c.handler.String()
    +	}
    +
    +	c.statusRecorder.PublishEvent(
    +		severity,
    +		proto.SystemEvent_NETWORK,
    +		message,
    +		userMessage,
    +		meta,
    +	)
    +}
    +
     func (c *clientNetwork) sendUpdateToClientNetworkWatcher(update routesUpdate) {
     	go func() {
     		c.routeUpdate <- update
    @@ -361,12 +425,12 @@ func (c *clientNetwork) peersStateAndUpdateWatcher() {
     		select {
     		case <-c.ctx.Done():
     			log.Debugf("Stopping watcher for network [%v]", c.handler)
    -			if err := c.removeRouteFromPeerAndSystem(); err != nil {
    +			if err := c.removeRouteFromPeerAndSystem(reasonShutdown); err != nil {
     				log.Errorf("Failed to remove routes for [%v]: %v", c.handler, err)
     			}
     			return
     		case <-c.peerStateUpdate:
    -			err := c.recalculateRouteAndUpdatePeerAndSystem()
    +			err := c.recalculateRouteAndUpdatePeerAndSystem(reasonPeerUpdate)
     			if err != nil {
     				log.Errorf("Failed to recalculate routes for network [%v]: %v", c.handler, err)
     			}
    @@ -385,7 +449,7 @@ func (c *clientNetwork) peersStateAndUpdateWatcher() {
     
     			if isTrueRouteUpdate {
     				log.Debug("Client network update contains different routes, recalculating routes")
    -				err := c.recalculateRouteAndUpdatePeerAndSystem()
    +				err := c.recalculateRouteAndUpdatePeerAndSystem(reasonRouteUpdate)
     				if err != nil {
     					log.Errorf("Failed to recalculate routes for network [%v]: %v", c.handler, err)
     				}
    diff --git a/client/proto/daemon.pb.go b/client/proto/daemon.pb.go
    index c9651efed..b40f6beea 100644
    --- a/client/proto/daemon.pb.go
    +++ b/client/proto/daemon.pb.go
    @@ -87,6 +87,110 @@ func (LogLevel) EnumDescriptor() ([]byte, []int) {
     	return file_daemon_proto_rawDescGZIP(), []int{0}
     }
     
    +type SystemEvent_Severity int32
    +
    +const (
    +	SystemEvent_INFO     SystemEvent_Severity = 0
    +	SystemEvent_WARNING  SystemEvent_Severity = 1
    +	SystemEvent_ERROR    SystemEvent_Severity = 2
    +	SystemEvent_CRITICAL SystemEvent_Severity = 3
    +)
    +
    +// Enum value maps for SystemEvent_Severity.
    +var (
    +	SystemEvent_Severity_name = map[int32]string{
    +		0: "INFO",
    +		1: "WARNING",
    +		2: "ERROR",
    +		3: "CRITICAL",
    +	}
    +	SystemEvent_Severity_value = map[string]int32{
    +		"INFO":     0,
    +		"WARNING":  1,
    +		"ERROR":    2,
    +		"CRITICAL": 3,
    +	}
    +)
    +
    +func (x SystemEvent_Severity) Enum() *SystemEvent_Severity {
    +	p := new(SystemEvent_Severity)
    +	*p = x
    +	return p
    +}
    +
    +func (x SystemEvent_Severity) String() string {
    +	return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
    +}
    +
    +func (SystemEvent_Severity) Descriptor() protoreflect.EnumDescriptor {
    +	return file_daemon_proto_enumTypes[1].Descriptor()
    +}
    +
    +func (SystemEvent_Severity) Type() protoreflect.EnumType {
    +	return &file_daemon_proto_enumTypes[1]
    +}
    +
    +func (x SystemEvent_Severity) Number() protoreflect.EnumNumber {
    +	return protoreflect.EnumNumber(x)
    +}
    +
    +// Deprecated: Use SystemEvent_Severity.Descriptor instead.
    +func (SystemEvent_Severity) EnumDescriptor() ([]byte, []int) {
    +	return file_daemon_proto_rawDescGZIP(), []int{45, 0}
    +}
    +
    +type SystemEvent_Category int32
    +
    +const (
    +	SystemEvent_NETWORK        SystemEvent_Category = 0
    +	SystemEvent_DNS            SystemEvent_Category = 1
    +	SystemEvent_AUTHENTICATION SystemEvent_Category = 2
    +	SystemEvent_CONNECTIVITY   SystemEvent_Category = 3
    +)
    +
    +// Enum value maps for SystemEvent_Category.
    +var (
    +	SystemEvent_Category_name = map[int32]string{
    +		0: "NETWORK",
    +		1: "DNS",
    +		2: "AUTHENTICATION",
    +		3: "CONNECTIVITY",
    +	}
    +	SystemEvent_Category_value = map[string]int32{
    +		"NETWORK":        0,
    +		"DNS":            1,
    +		"AUTHENTICATION": 2,
    +		"CONNECTIVITY":   3,
    +	}
    +)
    +
    +func (x SystemEvent_Category) Enum() *SystemEvent_Category {
    +	p := new(SystemEvent_Category)
    +	*p = x
    +	return p
    +}
    +
    +func (x SystemEvent_Category) String() string {
    +	return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
    +}
    +
    +func (SystemEvent_Category) Descriptor() protoreflect.EnumDescriptor {
    +	return file_daemon_proto_enumTypes[2].Descriptor()
    +}
    +
    +func (SystemEvent_Category) Type() protoreflect.EnumType {
    +	return &file_daemon_proto_enumTypes[2]
    +}
    +
    +func (x SystemEvent_Category) Number() protoreflect.EnumNumber {
    +	return protoreflect.EnumNumber(x)
    +}
    +
    +// Deprecated: Use SystemEvent_Category.Descriptor instead.
    +func (SystemEvent_Category) EnumDescriptor() ([]byte, []int) {
    +	return file_daemon_proto_rawDescGZIP(), []int{45, 1}
    +}
    +
     type LoginRequest struct {
     	state         protoimpl.MessageState
     	sizeCache     protoimpl.SizeCache
    @@ -127,6 +231,7 @@ type LoginRequest struct {
     	DisableDns           *bool                `protobuf:"varint,22,opt,name=disable_dns,json=disableDns,proto3,oneof" json:"disable_dns,omitempty"`
     	DisableFirewall      *bool                `protobuf:"varint,23,opt,name=disable_firewall,json=disableFirewall,proto3,oneof" json:"disable_firewall,omitempty"`
     	BlockLanAccess       *bool                `protobuf:"varint,24,opt,name=block_lan_access,json=blockLanAccess,proto3,oneof" json:"block_lan_access,omitempty"`
    +	DisableNotifications *bool                `protobuf:"varint,25,opt,name=disable_notifications,json=disableNotifications,proto3,oneof" json:"disable_notifications,omitempty"`
     }
     
     func (x *LoginRequest) Reset() {
    @@ -330,6 +435,13 @@ func (x *LoginRequest) GetBlockLanAccess() bool {
     	return false
     }
     
    +func (x *LoginRequest) GetDisableNotifications() bool {
    +	if x != nil && x.DisableNotifications != nil {
    +		return *x.DisableNotifications
    +	}
    +	return false
    +}
    +
     type LoginResponse struct {
     	state         protoimpl.MessageState
     	sizeCache     protoimpl.SizeCache
    @@ -810,13 +922,14 @@ type GetConfigResponse struct {
     	// 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"`
    -	InterfaceName       string `protobuf:"bytes,6,opt,name=interfaceName,proto3" json:"interfaceName,omitempty"`
    -	WireguardPort       int64  `protobuf:"varint,7,opt,name=wireguardPort,proto3" json:"wireguardPort,omitempty"`
    -	DisableAutoConnect  bool   `protobuf:"varint,9,opt,name=disableAutoConnect,proto3" json:"disableAutoConnect,omitempty"`
    -	ServerSSHAllowed    bool   `protobuf:"varint,10,opt,name=serverSSHAllowed,proto3" json:"serverSSHAllowed,omitempty"`
    -	RosenpassEnabled    bool   `protobuf:"varint,11,opt,name=rosenpassEnabled,proto3" json:"rosenpassEnabled,omitempty"`
    -	RosenpassPermissive bool   `protobuf:"varint,12,opt,name=rosenpassPermissive,proto3" json:"rosenpassPermissive,omitempty"`
    +	AdminURL             string `protobuf:"bytes,5,opt,name=adminURL,proto3" json:"adminURL,omitempty"`
    +	InterfaceName        string `protobuf:"bytes,6,opt,name=interfaceName,proto3" json:"interfaceName,omitempty"`
    +	WireguardPort        int64  `protobuf:"varint,7,opt,name=wireguardPort,proto3" json:"wireguardPort,omitempty"`
    +	DisableAutoConnect   bool   `protobuf:"varint,9,opt,name=disableAutoConnect,proto3" json:"disableAutoConnect,omitempty"`
    +	ServerSSHAllowed     bool   `protobuf:"varint,10,opt,name=serverSSHAllowed,proto3" json:"serverSSHAllowed,omitempty"`
    +	RosenpassEnabled     bool   `protobuf:"varint,11,opt,name=rosenpassEnabled,proto3" json:"rosenpassEnabled,omitempty"`
    +	RosenpassPermissive  bool   `protobuf:"varint,12,opt,name=rosenpassPermissive,proto3" json:"rosenpassPermissive,omitempty"`
    +	DisableNotifications bool   `protobuf:"varint,13,opt,name=disable_notifications,json=disableNotifications,proto3" json:"disable_notifications,omitempty"`
     }
     
     func (x *GetConfigResponse) Reset() {
    @@ -928,6 +1041,13 @@ func (x *GetConfigResponse) GetRosenpassPermissive() bool {
     	return false
     }
     
    +func (x *GetConfigResponse) GetDisableNotifications() bool {
    +	if x != nil {
    +		return x.DisableNotifications
    +	}
    +	return false
    +}
    +
     // PeerState contains the latest state of a peer
     type PeerState struct {
     	state         protoimpl.MessageState
    @@ -1475,6 +1595,7 @@ type FullStatus struct {
     	Peers           []*PeerState     `protobuf:"bytes,4,rep,name=peers,proto3" json:"peers,omitempty"`
     	Relays          []*RelayState    `protobuf:"bytes,5,rep,name=relays,proto3" json:"relays,omitempty"`
     	DnsServers      []*NSGroupState  `protobuf:"bytes,6,rep,name=dns_servers,json=dnsServers,proto3" json:"dns_servers,omitempty"`
    +	Events          []*SystemEvent   `protobuf:"bytes,7,rep,name=events,proto3" json:"events,omitempty"`
     }
     
     func (x *FullStatus) Reset() {
    @@ -1551,6 +1672,13 @@ func (x *FullStatus) GetDnsServers() []*NSGroupState {
     	return nil
     }
     
    +func (x *FullStatus) GetEvents() []*SystemEvent {
    +	if x != nil {
    +		return x.Events
    +	}
    +	return nil
    +}
    +
     type ListNetworksRequest struct {
     	state         protoimpl.MessageState
     	sizeCache     protoimpl.SizeCache
    @@ -2895,6 +3023,224 @@ func (x *TracePacketResponse) GetFinalDisposition() bool {
     	return false
     }
     
    +type SubscribeRequest struct {
    +	state         protoimpl.MessageState
    +	sizeCache     protoimpl.SizeCache
    +	unknownFields protoimpl.UnknownFields
    +}
    +
    +func (x *SubscribeRequest) Reset() {
    +	*x = SubscribeRequest{}
    +	if protoimpl.UnsafeEnabled {
    +		mi := &file_daemon_proto_msgTypes[44]
    +		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
    +		ms.StoreMessageInfo(mi)
    +	}
    +}
    +
    +func (x *SubscribeRequest) String() string {
    +	return protoimpl.X.MessageStringOf(x)
    +}
    +
    +func (*SubscribeRequest) ProtoMessage() {}
    +
    +func (x *SubscribeRequest) ProtoReflect() protoreflect.Message {
    +	mi := &file_daemon_proto_msgTypes[44]
    +	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 SubscribeRequest.ProtoReflect.Descriptor instead.
    +func (*SubscribeRequest) Descriptor() ([]byte, []int) {
    +	return file_daemon_proto_rawDescGZIP(), []int{44}
    +}
    +
    +type SystemEvent struct {
    +	state         protoimpl.MessageState
    +	sizeCache     protoimpl.SizeCache
    +	unknownFields protoimpl.UnknownFields
    +
    +	Id          string                 `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
    +	Severity    SystemEvent_Severity   `protobuf:"varint,2,opt,name=severity,proto3,enum=daemon.SystemEvent_Severity" json:"severity,omitempty"`
    +	Category    SystemEvent_Category   `protobuf:"varint,3,opt,name=category,proto3,enum=daemon.SystemEvent_Category" json:"category,omitempty"`
    +	Message     string                 `protobuf:"bytes,4,opt,name=message,proto3" json:"message,omitempty"`
    +	UserMessage string                 `protobuf:"bytes,5,opt,name=userMessage,proto3" json:"userMessage,omitempty"`
    +	Timestamp   *timestamppb.Timestamp `protobuf:"bytes,6,opt,name=timestamp,proto3" json:"timestamp,omitempty"`
    +	Metadata    map[string]string      `protobuf:"bytes,7,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
    +}
    +
    +func (x *SystemEvent) Reset() {
    +	*x = SystemEvent{}
    +	if protoimpl.UnsafeEnabled {
    +		mi := &file_daemon_proto_msgTypes[45]
    +		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
    +		ms.StoreMessageInfo(mi)
    +	}
    +}
    +
    +func (x *SystemEvent) String() string {
    +	return protoimpl.X.MessageStringOf(x)
    +}
    +
    +func (*SystemEvent) ProtoMessage() {}
    +
    +func (x *SystemEvent) ProtoReflect() protoreflect.Message {
    +	mi := &file_daemon_proto_msgTypes[45]
    +	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 SystemEvent.ProtoReflect.Descriptor instead.
    +func (*SystemEvent) Descriptor() ([]byte, []int) {
    +	return file_daemon_proto_rawDescGZIP(), []int{45}
    +}
    +
    +func (x *SystemEvent) GetId() string {
    +	if x != nil {
    +		return x.Id
    +	}
    +	return ""
    +}
    +
    +func (x *SystemEvent) GetSeverity() SystemEvent_Severity {
    +	if x != nil {
    +		return x.Severity
    +	}
    +	return SystemEvent_INFO
    +}
    +
    +func (x *SystemEvent) GetCategory() SystemEvent_Category {
    +	if x != nil {
    +		return x.Category
    +	}
    +	return SystemEvent_NETWORK
    +}
    +
    +func (x *SystemEvent) GetMessage() string {
    +	if x != nil {
    +		return x.Message
    +	}
    +	return ""
    +}
    +
    +func (x *SystemEvent) GetUserMessage() string {
    +	if x != nil {
    +		return x.UserMessage
    +	}
    +	return ""
    +}
    +
    +func (x *SystemEvent) GetTimestamp() *timestamppb.Timestamp {
    +	if x != nil {
    +		return x.Timestamp
    +	}
    +	return nil
    +}
    +
    +func (x *SystemEvent) GetMetadata() map[string]string {
    +	if x != nil {
    +		return x.Metadata
    +	}
    +	return nil
    +}
    +
    +type GetEventsRequest struct {
    +	state         protoimpl.MessageState
    +	sizeCache     protoimpl.SizeCache
    +	unknownFields protoimpl.UnknownFields
    +}
    +
    +func (x *GetEventsRequest) Reset() {
    +	*x = GetEventsRequest{}
    +	if protoimpl.UnsafeEnabled {
    +		mi := &file_daemon_proto_msgTypes[46]
    +		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
    +		ms.StoreMessageInfo(mi)
    +	}
    +}
    +
    +func (x *GetEventsRequest) String() string {
    +	return protoimpl.X.MessageStringOf(x)
    +}
    +
    +func (*GetEventsRequest) ProtoMessage() {}
    +
    +func (x *GetEventsRequest) ProtoReflect() protoreflect.Message {
    +	mi := &file_daemon_proto_msgTypes[46]
    +	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 GetEventsRequest.ProtoReflect.Descriptor instead.
    +func (*GetEventsRequest) Descriptor() ([]byte, []int) {
    +	return file_daemon_proto_rawDescGZIP(), []int{46}
    +}
    +
    +type GetEventsResponse struct {
    +	state         protoimpl.MessageState
    +	sizeCache     protoimpl.SizeCache
    +	unknownFields protoimpl.UnknownFields
    +
    +	Events []*SystemEvent `protobuf:"bytes,1,rep,name=events,proto3" json:"events,omitempty"`
    +}
    +
    +func (x *GetEventsResponse) Reset() {
    +	*x = GetEventsResponse{}
    +	if protoimpl.UnsafeEnabled {
    +		mi := &file_daemon_proto_msgTypes[47]
    +		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
    +		ms.StoreMessageInfo(mi)
    +	}
    +}
    +
    +func (x *GetEventsResponse) String() string {
    +	return protoimpl.X.MessageStringOf(x)
    +}
    +
    +func (*GetEventsResponse) ProtoMessage() {}
    +
    +func (x *GetEventsResponse) ProtoReflect() protoreflect.Message {
    +	mi := &file_daemon_proto_msgTypes[47]
    +	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 GetEventsResponse.ProtoReflect.Descriptor instead.
    +func (*GetEventsResponse) Descriptor() ([]byte, []int) {
    +	return file_daemon_proto_rawDescGZIP(), []int{47}
    +}
    +
    +func (x *GetEventsResponse) GetEvents() []*SystemEvent {
    +	if x != nil {
    +		return x.Events
    +	}
    +	return nil
    +}
    +
     var File_daemon_proto protoreflect.FileDescriptor
     
     var file_daemon_proto_rawDesc = []byte{
    @@ -2905,7 +3251,7 @@ var file_daemon_proto_rawDesc = []byte{
     	0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74,
     	0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1e, 0x67, 0x6f, 0x6f, 0x67, 0x6c,
     	0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x64, 0x75, 0x72, 0x61, 0x74,
    -	0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x95, 0x0b, 0x0a, 0x0c, 0x4c, 0x6f,
    +	0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xe9, 0x0b, 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, 0x26, 0x0a, 0x0c, 0x70, 0x72, 0x65, 0x53, 0x68, 0x61,
    @@ -2976,409 +3322,468 @@ var file_daemon_proto_rawDesc = []byte{
     	0x61, 0x6c, 0x6c, 0x88, 0x01, 0x01, 0x12, 0x2d, 0x0a, 0x10, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x5f,
     	0x6c, 0x61, 0x6e, 0x5f, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x18, 0x18, 0x20, 0x01, 0x28, 0x08,
     	0x48, 0x0d, 0x52, 0x0e, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x4c, 0x61, 0x6e, 0x41, 0x63, 0x63, 0x65,
    -	0x73, 0x73, 0x88, 0x01, 0x01, 0x42, 0x13, 0x0a, 0x11, 0x5f, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70,
    -	0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x42, 0x10, 0x0a, 0x0e, 0x5f, 0x69,
    -	0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x42, 0x10, 0x0a, 0x0e,
    -	0x5f, 0x77, 0x69, 0x72, 0x65, 0x67, 0x75, 0x61, 0x72, 0x64, 0x50, 0x6f, 0x72, 0x74, 0x42, 0x17,
    -	0x0a, 0x15, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x50, 0x72, 0x65, 0x53, 0x68,
    -	0x61, 0x72, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x42, 0x15, 0x0a, 0x13, 0x5f, 0x64, 0x69, 0x73, 0x61,
    -	0x62, 0x6c, 0x65, 0x41, 0x75, 0x74, 0x6f, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x42, 0x13,
    -	0x0a, 0x11, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x53, 0x53, 0x48, 0x41, 0x6c, 0x6c, 0x6f,
    -	0x77, 0x65, 0x64, 0x42, 0x16, 0x0a, 0x14, 0x5f, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73,
    -	0x73, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x76, 0x65, 0x42, 0x11, 0x0a, 0x0f, 0x5f,
    -	0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x42, 0x13,
    -	0x0a, 0x11, 0x5f, 0x64, 0x6e, 0x73, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x49, 0x6e, 0x74, 0x65, 0x72,
    -	0x76, 0x61, 0x6c, 0x42, 0x18, 0x0a, 0x16, 0x5f, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f,
    -	0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x42, 0x18, 0x0a,
    -	0x16, 0x5f, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72,
    -	0x5f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x42, 0x0e, 0x0a, 0x0c, 0x5f, 0x64, 0x69, 0x73, 0x61,
    -	0x62, 0x6c, 0x65, 0x5f, 0x64, 0x6e, 0x73, 0x42, 0x13, 0x0a, 0x11, 0x5f, 0x64, 0x69, 0x73, 0x61,
    -	0x62, 0x6c, 0x65, 0x5f, 0x66, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x42, 0x13, 0x0a, 0x11,
    -	0x5f, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x5f, 0x6c, 0x61, 0x6e, 0x5f, 0x61, 0x63, 0x63, 0x65, 0x73,
    -	0x73, 0x22, 0xb5, 0x01, 0x0a, 0x0d, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f,
    -	0x6e, 0x73, 0x65, 0x12, 0x24, 0x0a, 0x0d, 0x6e, 0x65, 0x65, 0x64, 0x73, 0x53, 0x53, 0x4f, 0x4c,
    -	0x6f, 0x67, 0x69, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x6e, 0x65, 0x65, 0x64,
    -	0x73, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65,
    -	0x72, 0x43, 0x6f, 0x64, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65,
    -	0x72, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x28, 0x0a, 0x0f, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x63,
    -	0x61, 0x74, 0x69, 0x6f, 0x6e, 0x55, 0x52, 0x49, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f,
    -	0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x55, 0x52, 0x49, 0x12,
    -	0x38, 0x0a, 0x17, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x55,
    -	0x52, 0x49, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09,
    -	0x52, 0x17, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x55, 0x52,
    -	0x49, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x22, 0x4d, 0x0a, 0x13, 0x57, 0x61, 0x69,
    -	0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
    -	0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01,
    -	0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x1a, 0x0a, 0x08,
    -	0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08,
    -	0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x16, 0x0a, 0x14, 0x57, 0x61, 0x69, 0x74,
    -	0x53, 0x53, 0x4f, 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, 0x3d, 0x0a, 0x0d, 0x53,
    -	0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2c, 0x0a, 0x11,
    -	0x67, 0x65, 0x74, 0x46, 0x75, 0x6c, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x75,
    -	0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x11, 0x67, 0x65, 0x74, 0x46, 0x75, 0x6c, 0x6c,
    -	0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x82, 0x01, 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, 0x12, 0x32, 0x0a, 0x0a, 0x66, 0x75, 0x6c, 0x6c, 0x53, 0x74, 0x61,
    -	0x74, 0x75, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x64, 0x61, 0x65, 0x6d,
    -	0x6f, 0x6e, 0x2e, 0x46, 0x75, 0x6c, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x0a, 0x66,
    -	0x75, 0x6c, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x24, 0x0a, 0x0d, 0x64, 0x61, 0x65,
    -	0x6d, 0x6f, 0x6e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09,
    -	0x52, 0x0d, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 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, 0xb9, 0x03, 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, 0x12, 0x24, 0x0a, 0x0d, 0x69, 0x6e, 0x74, 0x65,
    -	0x72, 0x66, 0x61, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52,
    -	0x0d, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x24,
    -	0x0a, 0x0d, 0x77, 0x69, 0x72, 0x65, 0x67, 0x75, 0x61, 0x72, 0x64, 0x50, 0x6f, 0x72, 0x74, 0x18,
    -	0x07, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, 0x77, 0x69, 0x72, 0x65, 0x67, 0x75, 0x61, 0x72, 0x64,
    -	0x50, 0x6f, 0x72, 0x74, 0x12, 0x2e, 0x0a, 0x12, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x41,
    -	0x75, 0x74, 0x6f, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08,
    -	0x52, 0x12, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x41, 0x75, 0x74, 0x6f, 0x43, 0x6f, 0x6e,
    -	0x6e, 0x65, 0x63, 0x74, 0x12, 0x2a, 0x0a, 0x10, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x53, 0x53,
    -	0x48, 0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10,
    -	0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x53, 0x53, 0x48, 0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64,
    -	0x12, 0x2a, 0x0a, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61,
    -	0x62, 0x6c, 0x65, 0x64, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x72, 0x6f, 0x73, 0x65,
    -	0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x30, 0x0a, 0x13,
    -	0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73,
    -	0x69, 0x76, 0x65, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x08, 0x52, 0x13, 0x72, 0x6f, 0x73, 0x65, 0x6e,
    -	0x70, 0x61, 0x73, 0x73, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x76, 0x65, 0x22, 0xde,
    -	0x05, 0x0a, 0x09, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x0e, 0x0a, 0x02,
    -	0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x50, 0x12, 0x16, 0x0a, 0x06,
    -	0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x75,
    -	0x62, 0x4b, 0x65, 0x79, 0x12, 0x1e, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x6e, 0x53, 0x74, 0x61, 0x74,
    -	0x75, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x6e, 0x53, 0x74,
    -	0x61, 0x74, 0x75, 0x73, 0x12, 0x46, 0x0a, 0x10, 0x63, 0x6f, 0x6e, 0x6e, 0x53, 0x74, 0x61, 0x74,
    -	0x75, 0x73, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a,
    -	0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66,
    -	0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x10, 0x63, 0x6f, 0x6e, 0x6e,
    -	0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x18, 0x0a, 0x07,
    -	0x72, 0x65, 0x6c, 0x61, 0x79, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x72,
    -	0x65, 0x6c, 0x61, 0x79, 0x65, 0x64, 0x12, 0x34, 0x0a, 0x15, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x49,
    +	0x73, 0x73, 0x88, 0x01, 0x01, 0x12, 0x38, 0x0a, 0x15, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65,
    +	0x5f, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x19,
    +	0x20, 0x01, 0x28, 0x08, 0x48, 0x0e, 0x52, 0x14, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x4e,
    +	0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x88, 0x01, 0x01, 0x42,
    +	0x13, 0x0a, 0x11, 0x5f, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61,
    +	0x62, 0x6c, 0x65, 0x64, 0x42, 0x10, 0x0a, 0x0e, 0x5f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61,
    +	0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x42, 0x10, 0x0a, 0x0e, 0x5f, 0x77, 0x69, 0x72, 0x65, 0x67,
    +	0x75, 0x61, 0x72, 0x64, 0x50, 0x6f, 0x72, 0x74, 0x42, 0x17, 0x0a, 0x15, 0x5f, 0x6f, 0x70, 0x74,
    +	0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x50, 0x72, 0x65, 0x53, 0x68, 0x61, 0x72, 0x65, 0x64, 0x4b, 0x65,
    +	0x79, 0x42, 0x15, 0x0a, 0x13, 0x5f, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x41, 0x75, 0x74,
    +	0x6f, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x42, 0x13, 0x0a, 0x11, 0x5f, 0x73, 0x65, 0x72,
    +	0x76, 0x65, 0x72, 0x53, 0x53, 0x48, 0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x42, 0x16, 0x0a,
    +	0x14, 0x5f, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, 0x65, 0x72, 0x6d, 0x69,
    +	0x73, 0x73, 0x69, 0x76, 0x65, 0x42, 0x11, 0x0a, 0x0f, 0x5f, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72,
    +	0x6b, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x42, 0x13, 0x0a, 0x11, 0x5f, 0x64, 0x6e, 0x73,
    +	0x52, 0x6f, 0x75, 0x74, 0x65, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x42, 0x18, 0x0a,
    +	0x16, 0x5f, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74,
    +	0x5f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x42, 0x18, 0x0a, 0x16, 0x5f, 0x64, 0x69, 0x73, 0x61,
    +	0x62, 0x6c, 0x65, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x5f, 0x72, 0x6f, 0x75, 0x74, 0x65,
    +	0x73, 0x42, 0x0e, 0x0a, 0x0c, 0x5f, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x64, 0x6e,
    +	0x73, 0x42, 0x13, 0x0a, 0x11, 0x5f, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x66, 0x69,
    +	0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x42, 0x13, 0x0a, 0x11, 0x5f, 0x62, 0x6c, 0x6f, 0x63, 0x6b,
    +	0x5f, 0x6c, 0x61, 0x6e, 0x5f, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x42, 0x18, 0x0a, 0x16, 0x5f,
    +	0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61,
    +	0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0xb5, 0x01, 0x0a, 0x0d, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52,
    +	0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x24, 0x0a, 0x0d, 0x6e, 0x65, 0x65, 0x64, 0x73,
    +	0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d,
    +	0x6e, 0x65, 0x65, 0x64, 0x73, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x1a, 0x0a,
    +	0x08, 0x75, 0x73, 0x65, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52,
    +	0x08, 0x75, 0x73, 0x65, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x28, 0x0a, 0x0f, 0x76, 0x65, 0x72,
    +	0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x55, 0x52, 0x49, 0x18, 0x03, 0x20, 0x01,
    +	0x28, 0x09, 0x52, 0x0f, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e,
    +	0x55, 0x52, 0x49, 0x12, 0x38, 0x0a, 0x17, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74,
    +	0x69, 0x6f, 0x6e, 0x55, 0x52, 0x49, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x18, 0x04,
    +	0x20, 0x01, 0x28, 0x09, 0x52, 0x17, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69,
    +	0x6f, 0x6e, 0x55, 0x52, 0x49, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x22, 0x4d, 0x0a,
    +	0x13, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71,
    +	0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x43, 0x6f, 0x64, 0x65,
    +	0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x43, 0x6f, 0x64, 0x65,
    +	0x12, 0x1a, 0x0a, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01,
    +	0x28, 0x09, 0x52, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x16, 0x0a, 0x14,
    +	0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 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,
    +	0x3d, 0x0a, 0x0d, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
    +	0x12, 0x2c, 0x0a, 0x11, 0x67, 0x65, 0x74, 0x46, 0x75, 0x6c, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53,
    +	0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x11, 0x67, 0x65, 0x74,
    +	0x46, 0x75, 0x6c, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x82,
    +	0x01, 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, 0x12, 0x32, 0x0a, 0x0a, 0x66, 0x75, 0x6c,
    +	0x6c, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e,
    +	0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x46, 0x75, 0x6c, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x75,
    +	0x73, 0x52, 0x0a, 0x66, 0x75, 0x6c, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x24, 0x0a,
    +	0x0d, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03,
    +	0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x56, 0x65, 0x72, 0x73,
    +	0x69, 0x6f, 0x6e, 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, 0xee, 0x03, 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, 0x12, 0x24, 0x0a, 0x0d,
    +	0x69, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x06, 0x20,
    +	0x01, 0x28, 0x09, 0x52, 0x0d, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x4e, 0x61,
    +	0x6d, 0x65, 0x12, 0x24, 0x0a, 0x0d, 0x77, 0x69, 0x72, 0x65, 0x67, 0x75, 0x61, 0x72, 0x64, 0x50,
    +	0x6f, 0x72, 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, 0x77, 0x69, 0x72, 0x65, 0x67,
    +	0x75, 0x61, 0x72, 0x64, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x2e, 0x0a, 0x12, 0x64, 0x69, 0x73, 0x61,
    +	0x62, 0x6c, 0x65, 0x41, 0x75, 0x74, 0x6f, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x18, 0x09,
    +	0x20, 0x01, 0x28, 0x08, 0x52, 0x12, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x41, 0x75, 0x74,
    +	0x6f, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x12, 0x2a, 0x0a, 0x10, 0x73, 0x65, 0x72, 0x76,
    +	0x65, 0x72, 0x53, 0x53, 0x48, 0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x18, 0x0a, 0x20, 0x01,
    +	0x28, 0x08, 0x52, 0x10, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x53, 0x53, 0x48, 0x41, 0x6c, 0x6c,
    +	0x6f, 0x77, 0x65, 0x64, 0x12, 0x2a, 0x0a, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73,
    +	0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10,
    +	0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64,
    +	0x12, 0x30, 0x0a, 0x13, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, 0x65, 0x72,
    +	0x6d, 0x69, 0x73, 0x73, 0x69, 0x76, 0x65, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x08, 0x52, 0x13, 0x72,
    +	0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69,
    +	0x76, 0x65, 0x12, 0x33, 0x0a, 0x15, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x6e, 0x6f,
    +	0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x0d, 0x20, 0x01, 0x28,
    +	0x08, 0x52, 0x14, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69,
    +	0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0xde, 0x05, 0x0a, 0x09, 0x50, 0x65, 0x65, 0x72,
    +	0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28,
    +	0x09, 0x52, 0x02, 0x49, 0x50, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18,
    +	0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x12, 0x1e, 0x0a,
    +	0x0a, 0x63, 0x6f, 0x6e, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28,
    +	0x09, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x46, 0x0a,
    +	0x10, 0x63, 0x6f, 0x6e, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x55, 0x70, 0x64, 0x61, 0x74,
    +	0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65,
    +	0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74,
    +	0x61, 0x6d, 0x70, 0x52, 0x10, 0x63, 0x6f, 0x6e, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x55,
    +	0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x65, 0x64,
    +	0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x65, 0x64, 0x12,
    +	0x34, 0x0a, 0x15, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69,
    +	0x64, 0x61, 0x74, 0x65, 0x54, 0x79, 0x70, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x15,
    +	0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74,
    +	0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x36, 0x0a, 0x16, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x49,
     	0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x54, 0x79, 0x70, 0x65, 0x18,
    -	0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x15, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x49, 0x63, 0x65, 0x43,
    -	0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x36, 0x0a, 0x16,
    -	0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61,
    -	0x74, 0x65, 0x54, 0x79, 0x70, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x16, 0x72, 0x65,
    -	0x6d, 0x6f, 0x74, 0x65, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65,
    -	0x54, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x18, 0x09, 0x20, 0x01,
    -	0x28, 0x09, 0x52, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x12, 0x3c, 0x0a, 0x19, 0x6c, 0x6f, 0x63, 0x61,
    -	0x6c, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x64,
    -	0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x19, 0x6c, 0x6f, 0x63,
    -	0x61, 0x6c, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e,
    -	0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x3e, 0x0a, 0x1a, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65,
    -	0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x64, 0x70,
    -	0x6f, 0x69, 0x6e, 0x74, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x1a, 0x72, 0x65, 0x6d, 0x6f,
    -	0x74, 0x65, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e,
    -	0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x52, 0x0a, 0x16, 0x6c, 0x61, 0x73, 0x74, 0x57, 0x69,
    -	0x72, 0x65, 0x67, 0x75, 0x61, 0x72, 0x64, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65,
    -	0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e,
    -	0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61,
    -	0x6d, 0x70, 0x52, 0x16, 0x6c, 0x61, 0x73, 0x74, 0x57, 0x69, 0x72, 0x65, 0x67, 0x75, 0x61, 0x72,
    -	0x64, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x62, 0x79,
    -	0x74, 0x65, 0x73, 0x52, 0x78, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x62, 0x79, 0x74,
    -	0x65, 0x73, 0x52, 0x78, 0x12, 0x18, 0x0a, 0x07, 0x62, 0x79, 0x74, 0x65, 0x73, 0x54, 0x78, 0x18,
    -	0x0e, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x62, 0x79, 0x74, 0x65, 0x73, 0x54, 0x78, 0x12, 0x2a,
    -	0x0a, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c,
    -	0x65, 0x64, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70,
    -	0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x6e, 0x65,
    -	0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x18, 0x10, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x6e, 0x65,
    -	0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x12, 0x33, 0x0a, 0x07, 0x6c, 0x61, 0x74, 0x65, 0x6e, 0x63,
    -	0x79, 0x18, 0x11, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65,
    -	0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69,
    -	0x6f, 0x6e, 0x52, 0x07, 0x6c, 0x61, 0x74, 0x65, 0x6e, 0x63, 0x79, 0x12, 0x22, 0x0a, 0x0c, 0x72,
    -	0x65, 0x6c, 0x61, 0x79, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x12, 0x20, 0x01, 0x28,
    -	0x09, 0x52, 0x0c, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x22,
    -	0xf0, 0x01, 0x0a, 0x0e, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61,
    -	0x74, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02,
    -	0x49, 0x50, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01,
    -	0x28, 0x09, 0x52, 0x06, 0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x12, 0x28, 0x0a, 0x0f, 0x6b, 0x65,
    -	0x72, 0x6e, 0x65, 0x6c, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x18, 0x03, 0x20,
    -	0x01, 0x28, 0x08, 0x52, 0x0f, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x49, 0x6e, 0x74, 0x65, 0x72,
    -	0x66, 0x61, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x18, 0x04, 0x20, 0x01,
    -	0x28, 0x09, 0x52, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x12, 0x2a, 0x0a, 0x10, 0x72, 0x6f, 0x73, 0x65,
    -	0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01,
    -	0x28, 0x08, 0x52, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61,
    -	0x62, 0x6c, 0x65, 0x64, 0x12, 0x30, 0x0a, 0x13, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73,
    -	0x73, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x76, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28,
    -	0x08, 0x52, 0x13, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, 0x65, 0x72, 0x6d,
    -	0x69, 0x73, 0x73, 0x69, 0x76, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72,
    -	0x6b, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72,
    -	0x6b, 0x73, 0x22, 0x53, 0x0a, 0x0b, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74,
    -	0x65, 0x12, 0x10, 0x0a, 0x03, 0x55, 0x52, 0x4c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03,
    -	0x55, 0x52, 0x4c, 0x12, 0x1c, 0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65, 0x64,
    -	0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65,
    -	0x64, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09,
    -	0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, 0x57, 0x0a, 0x0f, 0x4d, 0x61, 0x6e, 0x61, 0x67,
    -	0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x55, 0x52,
    +	0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x16, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x49, 0x63, 0x65,
    +	0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a,
    +	0x04, 0x66, 0x71, 0x64, 0x6e, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x71, 0x64,
    +	0x6e, 0x12, 0x3c, 0x0a, 0x19, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e,
    +	0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x0a,
    +	0x20, 0x01, 0x28, 0x09, 0x52, 0x19, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x49, 0x63, 0x65, 0x43, 0x61,
    +	0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12,
    +	0x3e, 0x0a, 0x1a, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64,
    +	0x69, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x0b, 0x20,
    +	0x01, 0x28, 0x09, 0x52, 0x1a, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x49, 0x63, 0x65, 0x43, 0x61,
    +	0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12,
    +	0x52, 0x0a, 0x16, 0x6c, 0x61, 0x73, 0x74, 0x57, 0x69, 0x72, 0x65, 0x67, 0x75, 0x61, 0x72, 0x64,
    +	0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32,
    +	0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75,
    +	0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x16, 0x6c, 0x61, 0x73,
    +	0x74, 0x57, 0x69, 0x72, 0x65, 0x67, 0x75, 0x61, 0x72, 0x64, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68,
    +	0x61, 0x6b, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x62, 0x79, 0x74, 0x65, 0x73, 0x52, 0x78, 0x18, 0x0d,
    +	0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x62, 0x79, 0x74, 0x65, 0x73, 0x52, 0x78, 0x12, 0x18, 0x0a,
    +	0x07, 0x62, 0x79, 0x74, 0x65, 0x73, 0x54, 0x78, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07,
    +	0x62, 0x79, 0x74, 0x65, 0x73, 0x54, 0x78, 0x12, 0x2a, 0x0a, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e,
    +	0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x0f, 0x20, 0x01, 0x28,
    +	0x08, 0x52, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62,
    +	0x6c, 0x65, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x18,
    +	0x10, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x12,
    +	0x33, 0x0a, 0x07, 0x6c, 0x61, 0x74, 0x65, 0x6e, 0x63, 0x79, 0x18, 0x11, 0x20, 0x01, 0x28, 0x0b,
    +	0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62,
    +	0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x07, 0x6c, 0x61, 0x74,
    +	0x65, 0x6e, 0x63, 0x79, 0x12, 0x22, 0x0a, 0x0c, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x41, 0x64, 0x64,
    +	0x72, 0x65, 0x73, 0x73, 0x18, 0x12, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x72, 0x65, 0x6c, 0x61,
    +	0x79, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x22, 0xf0, 0x01, 0x0a, 0x0e, 0x4c, 0x6f, 0x63,
    +	0x61, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x49,
    +	0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x50, 0x12, 0x16, 0x0a, 0x06, 0x70,
    +	0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x75, 0x62,
    +	0x4b, 0x65, 0x79, 0x12, 0x28, 0x0a, 0x0f, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x49, 0x6e, 0x74,
    +	0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, 0x6b, 0x65,
    +	0x72, 0x6e, 0x65, 0x6c, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x12, 0x12, 0x0a,
    +	0x04, 0x66, 0x71, 0x64, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x71, 0x64,
    +	0x6e, 0x12, 0x2a, 0x0a, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e,
    +	0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x72, 0x6f, 0x73,
    +	0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x30, 0x0a,
    +	0x13, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73,
    +	0x73, 0x69, 0x76, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x13, 0x72, 0x6f, 0x73, 0x65,
    +	0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x76, 0x65, 0x12,
    +	0x1a, 0x0a, 0x08, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28,
    +	0x09, 0x52, 0x08, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x22, 0x53, 0x0a, 0x0b, 0x53,
    +	0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x55, 0x52,
     	0x4c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x55, 0x52, 0x4c, 0x12, 0x1c, 0x0a, 0x09,
     	0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52,
     	0x09, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72,
     	0x72, 0x6f, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72,
    -	0x22, 0x52, 0x0a, 0x0a, 0x52, 0x65, 0x6c, 0x61, 0x79, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x10,
    -	0x0a, 0x03, 0x55, 0x52, 0x49, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x55, 0x52, 0x49,
    -	0x12, 0x1c, 0x0a, 0x09, 0x61, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x02, 0x20,
    -	0x01, 0x28, 0x08, 0x52, 0x09, 0x61, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x14,
    -	0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65,
    -	0x72, 0x72, 0x6f, 0x72, 0x22, 0x72, 0x0a, 0x0c, 0x4e, 0x53, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x53,
    -	0x74, 0x61, 0x74, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x18,
    -	0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x12, 0x18,
    -	0x0a, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52,
    -	0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62,
    -	0x6c, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c,
    -	0x65, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28,
    -	0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, 0xd2, 0x02, 0x0a, 0x0a, 0x46, 0x75, 0x6c,
    -	0x6c, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x41, 0x0a, 0x0f, 0x6d, 0x61, 0x6e, 0x61, 0x67,
    -	0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b,
    -	0x32, 0x17, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65,
    -	0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x0f, 0x6d, 0x61, 0x6e, 0x61, 0x67,
    -	0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x35, 0x0a, 0x0b, 0x73, 0x69,
    -	0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32,
    -	0x13, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53,
    -	0x74, 0x61, 0x74, 0x65, 0x52, 0x0b, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74,
    -	0x65, 0x12, 0x3e, 0x0a, 0x0e, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74,
    -	0x61, 0x74, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x64, 0x61, 0x65, 0x6d,
    -	0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74,
    -	0x65, 0x52, 0x0e, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74,
    -	0x65, 0x12, 0x27, 0x0a, 0x05, 0x70, 0x65, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b,
    -	0x32, 0x11, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74,
    -	0x61, 0x74, 0x65, 0x52, 0x05, 0x70, 0x65, 0x65, 0x72, 0x73, 0x12, 0x2a, 0x0a, 0x06, 0x72, 0x65,
    -	0x6c, 0x61, 0x79, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x64, 0x61, 0x65,
    -	0x6d, 0x6f, 0x6e, 0x2e, 0x52, 0x65, 0x6c, 0x61, 0x79, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x06,
    -	0x72, 0x65, 0x6c, 0x61, 0x79, 0x73, 0x12, 0x35, 0x0a, 0x0b, 0x64, 0x6e, 0x73, 0x5f, 0x73, 0x65,
    -	0x72, 0x76, 0x65, 0x72, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x64, 0x61,
    -	0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4e, 0x53, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x53, 0x74, 0x61, 0x74,
    -	0x65, 0x52, 0x0a, 0x64, 0x6e, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x22, 0x15, 0x0a,
    -	0x13, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x71,
    -	0x75, 0x65, 0x73, 0x74, 0x22, 0x3f, 0x0a, 0x14, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x65, 0x74, 0x77,
    -	0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x27, 0x0a, 0x06,
    -	0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x64,
    -	0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x52, 0x06, 0x72,
    -	0x6f, 0x75, 0x74, 0x65, 0x73, 0x22, 0x61, 0x0a, 0x15, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e,
    -	0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1e,
    -	0x0a, 0x0a, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x49, 0x44, 0x73, 0x18, 0x01, 0x20, 0x03,
    -	0x28, 0x09, 0x52, 0x0a, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x49, 0x44, 0x73, 0x12, 0x16,
    -	0x0a, 0x06, 0x61, 0x70, 0x70, 0x65, 0x6e, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06,
    -	0x61, 0x70, 0x70, 0x65, 0x6e, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x61, 0x6c, 0x6c, 0x18, 0x03, 0x20,
    -	0x01, 0x28, 0x08, 0x52, 0x03, 0x61, 0x6c, 0x6c, 0x22, 0x18, 0x0a, 0x16, 0x53, 0x65, 0x6c, 0x65,
    -	0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
    -	0x73, 0x65, 0x22, 0x1a, 0x0a, 0x06, 0x49, 0x50, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03,
    -	0x69, 0x70, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x03, 0x69, 0x70, 0x73, 0x22, 0xf9,
    -	0x01, 0x0a, 0x07, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x44,
    -	0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x44, 0x12, 0x14, 0x0a, 0x05, 0x72, 0x61,
    -	0x6e, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65,
    -	0x12, 0x1a, 0x0a, 0x08, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01,
    -	0x28, 0x08, 0x52, 0x08, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x65, 0x64, 0x12, 0x18, 0x0a, 0x07,
    -	0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x64,
    -	0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x42, 0x0a, 0x0b, 0x72, 0x65, 0x73, 0x6f, 0x6c, 0x76,
    -	0x65, 0x64, 0x49, 0x50, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x64, 0x61,
    -	0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x2e, 0x52, 0x65, 0x73,
    -	0x6f, 0x6c, 0x76, 0x65, 0x64, 0x49, 0x50, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0b, 0x72,
    -	0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x64, 0x49, 0x50, 0x73, 0x1a, 0x4e, 0x0a, 0x10, 0x52, 0x65,
    -	0x73, 0x6f, 0x6c, 0x76, 0x65, 0x64, 0x49, 0x50, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10,
    -	0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79,
    -	0x12, 0x24, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32,
    -	0x0e, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x49, 0x50, 0x4c, 0x69, 0x73, 0x74, 0x52,
    -	0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x6a, 0x0a, 0x12, 0x44, 0x65,
    -	0x62, 0x75, 0x67, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
    -	0x12, 0x1c, 0x0a, 0x09, 0x61, 0x6e, 0x6f, 0x6e, 0x79, 0x6d, 0x69, 0x7a, 0x65, 0x18, 0x01, 0x20,
    -	0x01, 0x28, 0x08, 0x52, 0x09, 0x61, 0x6e, 0x6f, 0x6e, 0x79, 0x6d, 0x69, 0x7a, 0x65, 0x12, 0x16,
    -	0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06,
    -	0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x1e, 0x0a, 0x0a, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d,
    -	0x49, 0x6e, 0x66, 0x6f, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x73, 0x79, 0x73, 0x74,
    -	0x65, 0x6d, 0x49, 0x6e, 0x66, 0x6f, 0x22, 0x29, 0x0a, 0x13, 0x44, 0x65, 0x62, 0x75, 0x67, 0x42,
    -	0x75, 0x6e, 0x64, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x12, 0x0a,
    -	0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74,
    -	0x68, 0x22, 0x14, 0x0a, 0x12, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c,
    -	0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x3d, 0x0a, 0x13, 0x47, 0x65, 0x74, 0x4c, 0x6f,
    -	0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x26,
    -	0x0a, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x10, 0x2e,
    -	0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52,
    -	0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x22, 0x3c, 0x0a, 0x12, 0x53, 0x65, 0x74, 0x4c, 0x6f, 0x67,
    -	0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x05,
    -	0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x10, 0x2e, 0x64, 0x61,
    -	0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x05, 0x6c,
    -	0x65, 0x76, 0x65, 0x6c, 0x22, 0x15, 0x0a, 0x13, 0x53, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65,
    -	0x76, 0x65, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1b, 0x0a, 0x05, 0x53,
    -	0x74, 0x61, 0x74, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01,
    -	0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x13, 0x0a, 0x11, 0x4c, 0x69, 0x73, 0x74,
    -	0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x3b, 0x0a,
    -	0x12, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f,
    -	0x6e, 0x73, 0x65, 0x12, 0x25, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20,
    -	0x03, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x74, 0x61,
    -	0x74, 0x65, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x65, 0x73, 0x22, 0x44, 0x0a, 0x11, 0x43, 0x6c,
    -	0x65, 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12,
    -	0x1d, 0x0a, 0x0a, 0x73, 0x74, 0x61, 0x74, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20,
    -	0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x74, 0x61, 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x10,
    -	0x0a, 0x03, 0x61, 0x6c, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x61, 0x6c, 0x6c,
    -	0x22, 0x3b, 0x0a, 0x12, 0x43, 0x6c, 0x65, 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65,
    -	0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x63, 0x6c, 0x65, 0x61, 0x6e, 0x65,
    -	0x64, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0d,
    -	0x63, 0x6c, 0x65, 0x61, 0x6e, 0x65, 0x64, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x22, 0x45, 0x0a,
    -	0x12, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75,
    -	0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x74, 0x61, 0x74, 0x65, 0x5f, 0x6e, 0x61, 0x6d,
    -	0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x74, 0x61, 0x74, 0x65, 0x4e, 0x61,
    -	0x6d, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x61, 0x6c, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52,
    -	0x03, 0x61, 0x6c, 0x6c, 0x22, 0x3c, 0x0a, 0x13, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x74,
    -	0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x64,
    -	0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20,
    -	0x01, 0x28, 0x05, 0x52, 0x0d, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x53, 0x74, 0x61, 0x74,
    -	0x65, 0x73, 0x22, 0x3b, 0x0a, 0x1f, 0x53, 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b,
    -	0x4d, 0x61, 0x70, 0x50, 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x65,
    -	0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64,
    -	0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x22,
    -	0x22, 0x0a, 0x20, 0x53, 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70,
    -	0x50, 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f,
    -	0x6e, 0x73, 0x65, 0x22, 0x76, 0x0a, 0x08, 0x54, 0x43, 0x50, 0x46, 0x6c, 0x61, 0x67, 0x73, 0x12,
    -	0x10, 0x0a, 0x03, 0x73, 0x79, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x73, 0x79,
    -	0x6e, 0x12, 0x10, 0x0a, 0x03, 0x61, 0x63, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03,
    -	0x61, 0x63, 0x6b, 0x12, 0x10, 0x0a, 0x03, 0x66, 0x69, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08,
    -	0x52, 0x03, 0x66, 0x69, 0x6e, 0x12, 0x10, 0x0a, 0x03, 0x72, 0x73, 0x74, 0x18, 0x04, 0x20, 0x01,
    -	0x28, 0x08, 0x52, 0x03, 0x72, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x70, 0x73, 0x68, 0x18, 0x05,
    -	0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x70, 0x73, 0x68, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x67,
    -	0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x75, 0x72, 0x67, 0x22, 0x80, 0x03, 0x0a, 0x12,
    -	0x54, 0x72, 0x61, 0x63, 0x65, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65,
    -	0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x69, 0x70, 0x18,
    -	0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x70, 0x12,
    -	0x25, 0x0a, 0x0e, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69,
    -	0x70, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61,
    -	0x74, 0x69, 0x6f, 0x6e, 0x49, 0x70, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63,
    -	0x6f, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63,
    -	0x6f, 0x6c, 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x70, 0x6f, 0x72,
    -	0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0a, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x50,
    -	0x6f, 0x72, 0x74, 0x12, 0x29, 0x0a, 0x10, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69,
    -	0x6f, 0x6e, 0x5f, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0f, 0x64,
    -	0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x1c,
    -	0x0a, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28,
    -	0x09, 0x52, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x32, 0x0a, 0x09,
    -	0x74, 0x63, 0x70, 0x5f, 0x66, 0x6c, 0x61, 0x67, 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32,
    -	0x10, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x54, 0x43, 0x50, 0x46, 0x6c, 0x61, 0x67,
    -	0x73, 0x48, 0x00, 0x52, 0x08, 0x74, 0x63, 0x70, 0x46, 0x6c, 0x61, 0x67, 0x73, 0x88, 0x01, 0x01,
    -	0x12, 0x20, 0x0a, 0x09, 0x69, 0x63, 0x6d, 0x70, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x08, 0x20,
    -	0x01, 0x28, 0x0d, 0x48, 0x01, 0x52, 0x08, 0x69, 0x63, 0x6d, 0x70, 0x54, 0x79, 0x70, 0x65, 0x88,
    -	0x01, 0x01, 0x12, 0x20, 0x0a, 0x09, 0x69, 0x63, 0x6d, 0x70, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18,
    -	0x09, 0x20, 0x01, 0x28, 0x0d, 0x48, 0x02, 0x52, 0x08, 0x69, 0x63, 0x6d, 0x70, 0x43, 0x6f, 0x64,
    -	0x65, 0x88, 0x01, 0x01, 0x42, 0x0c, 0x0a, 0x0a, 0x5f, 0x74, 0x63, 0x70, 0x5f, 0x66, 0x6c, 0x61,
    -	0x67, 0x73, 0x42, 0x0c, 0x0a, 0x0a, 0x5f, 0x69, 0x63, 0x6d, 0x70, 0x5f, 0x74, 0x79, 0x70, 0x65,
    -	0x42, 0x0c, 0x0a, 0x0a, 0x5f, 0x69, 0x63, 0x6d, 0x70, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x22, 0x9f,
    -	0x01, 0x0a, 0x0a, 0x54, 0x72, 0x61, 0x63, 0x65, 0x53, 0x74, 0x61, 0x67, 0x65, 0x12, 0x12, 0x0a,
    -	0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d,
    -	0x65, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01,
    -	0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x61,
    -	0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x61, 0x6c,
    -	0x6c, 0x6f, 0x77, 0x65, 0x64, 0x12, 0x32, 0x0a, 0x12, 0x66, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64,
    -	0x69, 0x6e, 0x67, 0x5f, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28,
    -	0x09, 0x48, 0x00, 0x52, 0x11, 0x66, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x44,
    -	0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x88, 0x01, 0x01, 0x42, 0x15, 0x0a, 0x13, 0x5f, 0x66, 0x6f,
    -	0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x5f, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73,
    -	0x22, 0x6e, 0x0a, 0x13, 0x54, 0x72, 0x61, 0x63, 0x65, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x52,
    -	0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2a, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x67, 0x65,
    -	0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e,
    -	0x2e, 0x54, 0x72, 0x61, 0x63, 0x65, 0x53, 0x74, 0x61, 0x67, 0x65, 0x52, 0x06, 0x73, 0x74, 0x61,
    -	0x67, 0x65, 0x73, 0x12, 0x2b, 0x0a, 0x11, 0x66, 0x69, 0x6e, 0x61, 0x6c, 0x5f, 0x64, 0x69, 0x73,
    -	0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10,
    -	0x66, 0x69, 0x6e, 0x61, 0x6c, 0x44, 0x69, 0x73, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e,
    -	0x2a, 0x62, 0x0a, 0x08, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x0b, 0x0a, 0x07,
    -	0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x50, 0x41, 0x4e,
    -	0x49, 0x43, 0x10, 0x01, 0x12, 0x09, 0x0a, 0x05, 0x46, 0x41, 0x54, 0x41, 0x4c, 0x10, 0x02, 0x12,
    -	0x09, 0x0a, 0x05, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x03, 0x12, 0x08, 0x0a, 0x04, 0x57, 0x41,
    -	0x52, 0x4e, 0x10, 0x04, 0x12, 0x08, 0x0a, 0x04, 0x49, 0x4e, 0x46, 0x4f, 0x10, 0x05, 0x12, 0x09,
    -	0x0a, 0x05, 0x44, 0x45, 0x42, 0x55, 0x47, 0x10, 0x06, 0x12, 0x09, 0x0a, 0x05, 0x54, 0x52, 0x41,
    -	0x43, 0x45, 0x10, 0x07, 0x32, 0xdd, 0x09, 0x0a, 0x0d, 0x44, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x53,
    -	0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x36, 0x0a, 0x05, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12,
    -	0x14, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65,
    -	0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x15, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c,
    -	0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x4b,
    -	0x0a, 0x0c, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x1b,
    -	0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c,
    -	0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x64, 0x61,
    -	0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69,
    -	0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x2d, 0x0a, 0x02, 0x55,
    -	0x70, 0x12, 0x11, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x55, 0x70, 0x52, 0x65, 0x71,
    -	0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x55, 0x70,
    -	0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x39, 0x0a, 0x06, 0x53, 0x74,
    -	0x61, 0x74, 0x75, 0x73, 0x12, 0x15, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x74,
    -	0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x64, 0x61,
    -	0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f,
    -	0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x33, 0x0a, 0x04, 0x44, 0x6f, 0x77, 0x6e, 0x12, 0x13, 0x2e,
    -	0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65,
    -	0x73, 0x74, 0x1a, 0x14, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x6f, 0x77, 0x6e,
    -	0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x42, 0x0a, 0x09, 0x47, 0x65,
    -	0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x18, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e,
    -	0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
    -	0x74, 0x1a, 0x19, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f,
    -	0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x4b,
    -	0x0a, 0x0c, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x12, 0x1b,
    -	0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x65, 0x74, 0x77,
    -	0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x64, 0x61,
    -	0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b,
    -	0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x51, 0x0a, 0x0e, 0x53,
    -	0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x12, 0x1d, 0x2e,
    -	0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74,
    -	0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x64,
    -	0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77,
    -	0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x53,
    -	0x0a, 0x10, 0x44, 0x65, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72,
    -	0x6b, 0x73, 0x12, 0x1d, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x6c, 0x65,
    -	0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
    -	0x74, 0x1a, 0x1e, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x6c, 0x65, 0x63,
    -	0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
    -	0x65, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x0b, 0x44, 0x65, 0x62, 0x75, 0x67, 0x42, 0x75, 0x6e, 0x64,
    -	0x6c, 0x65, 0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x62, 0x75,
    -	0x67, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b,
    -	0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x62, 0x75, 0x67, 0x42, 0x75, 0x6e,
    -	0x64, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a,
    -	0x0b, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x1a, 0x2e, 0x64,
    -	0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65,
    -	0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f,
    -	0x6e, 0x2e, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x73,
    -	0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x0b, 0x53, 0x65, 0x74, 0x4c, 0x6f,
    -	0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e,
    +	0x22, 0x57, 0x0a, 0x0f, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74,
    +	0x61, 0x74, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x55, 0x52, 0x4c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
    +	0x52, 0x03, 0x55, 0x52, 0x4c, 0x12, 0x1c, 0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74,
    +	0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63,
    +	0x74, 0x65, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x03, 0x20, 0x01,
    +	0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, 0x52, 0x0a, 0x0a, 0x52, 0x65, 0x6c,
    +	0x61, 0x79, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x55, 0x52, 0x49, 0x18, 0x01,
    +	0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x55, 0x52, 0x49, 0x12, 0x1c, 0x0a, 0x09, 0x61, 0x76, 0x61,
    +	0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x61, 0x76,
    +	0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72,
    +	0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, 0x72, 0x0a,
    +	0x0c, 0x4e, 0x53, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x18, 0x0a,
    +	0x07, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07,
    +	0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69,
    +	0x6e, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e,
    +	0x73, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01,
    +	0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x65,
    +	0x72, 0x72, 0x6f, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f,
    +	0x72, 0x22, 0xff, 0x02, 0x0a, 0x0a, 0x46, 0x75, 0x6c, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73,
    +	0x12, 0x41, 0x0a, 0x0f, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74,
    +	0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x64, 0x61, 0x65, 0x6d,
    +	0x6f, 0x6e, 0x2e, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61,
    +	0x74, 0x65, 0x52, 0x0f, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74,
    +	0x61, 0x74, 0x65, 0x12, 0x35, 0x0a, 0x0b, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61,
    +	0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f,
    +	0x6e, 0x2e, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x0b, 0x73,
    +	0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x3e, 0x0a, 0x0e, 0x6c, 0x6f,
    +	0x63, 0x61, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x18, 0x03, 0x20, 0x01,
    +	0x28, 0x0b, 0x32, 0x16, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x63, 0x61,
    +	0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x0e, 0x6c, 0x6f, 0x63, 0x61,
    +	0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x27, 0x0a, 0x05, 0x70, 0x65,
    +	0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x64, 0x61, 0x65, 0x6d,
    +	0x6f, 0x6e, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x05, 0x70, 0x65,
    +	0x65, 0x72, 0x73, 0x12, 0x2a, 0x0a, 0x06, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x73, 0x18, 0x05, 0x20,
    +	0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x52, 0x65, 0x6c,
    +	0x61, 0x79, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x06, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x73, 0x12,
    +	0x35, 0x0a, 0x0b, 0x64, 0x6e, 0x73, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x18, 0x06,
    +	0x20, 0x03, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4e, 0x53,
    +	0x47, 0x72, 0x6f, 0x75, 0x70, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x0a, 0x64, 0x6e, 0x73, 0x53,
    +	0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x12, 0x2b, 0x0a, 0x06, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73,
    +	0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e,
    +	0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x06, 0x65, 0x76, 0x65,
    +	0x6e, 0x74, 0x73, 0x22, 0x15, 0x0a, 0x13, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f,
    +	0x72, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x3f, 0x0a, 0x14, 0x4c, 0x69,
    +	0x73, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
    +	0x73, 0x65, 0x12, 0x27, 0x0a, 0x06, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03,
    +	0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4e, 0x65, 0x74, 0x77,
    +	0x6f, 0x72, 0x6b, 0x52, 0x06, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x22, 0x61, 0x0a, 0x15, 0x53,
    +	0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x71,
    +	0x75, 0x65, 0x73, 0x74, 0x12, 0x1e, 0x0a, 0x0a, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x49,
    +	0x44, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72,
    +	0x6b, 0x49, 0x44, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x61, 0x70, 0x70, 0x65, 0x6e, 0x64, 0x18, 0x02,
    +	0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x61, 0x70, 0x70, 0x65, 0x6e, 0x64, 0x12, 0x10, 0x0a, 0x03,
    +	0x61, 0x6c, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x61, 0x6c, 0x6c, 0x22, 0x18,
    +	0x0a, 0x16, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73,
    +	0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1a, 0x0a, 0x06, 0x49, 0x50, 0x4c, 0x69,
    +	0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x69, 0x70, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52,
    +	0x03, 0x69, 0x70, 0x73, 0x22, 0xf9, 0x01, 0x0a, 0x07, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b,
    +	0x12, 0x0e, 0x0a, 0x02, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x44,
    +	0x12, 0x14, 0x0a, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52,
    +	0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74,
    +	0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74,
    +	0x65, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x04, 0x20,
    +	0x03, 0x28, 0x09, 0x52, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x42, 0x0a, 0x0b,
    +	0x72, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x64, 0x49, 0x50, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28,
    +	0x0b, 0x32, 0x20, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f,
    +	0x72, 0x6b, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x64, 0x49, 0x50, 0x73, 0x45, 0x6e,
    +	0x74, 0x72, 0x79, 0x52, 0x0b, 0x72, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x64, 0x49, 0x50, 0x73,
    +	0x1a, 0x4e, 0x0a, 0x10, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x64, 0x49, 0x50, 0x73, 0x45,
    +	0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28,
    +	0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x24, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18,
    +	0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x49,
    +	0x50, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01,
    +	0x22, 0x6a, 0x0a, 0x12, 0x44, 0x65, 0x62, 0x75, 0x67, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x52,
    +	0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x61, 0x6e, 0x6f, 0x6e, 0x79, 0x6d,
    +	0x69, 0x7a, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x61, 0x6e, 0x6f, 0x6e, 0x79,
    +	0x6d, 0x69, 0x7a, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x02,
    +	0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x1e, 0x0a, 0x0a,
    +	0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x49, 0x6e, 0x66, 0x6f, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08,
    +	0x52, 0x0a, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x49, 0x6e, 0x66, 0x6f, 0x22, 0x29, 0x0a, 0x13,
    +	0x44, 0x65, 0x62, 0x75, 0x67, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f,
    +	0x6e, 0x73, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28,
    +	0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x22, 0x14, 0x0a, 0x12, 0x47, 0x65, 0x74, 0x4c, 0x6f,
    +	0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x3d, 0x0a,
    +	0x13, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x73, 0x70,
    +	0x6f, 0x6e, 0x73, 0x65, 0x12, 0x26, 0x0a, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x01, 0x20,
    +	0x01, 0x28, 0x0e, 0x32, 0x10, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x67,
    +	0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x22, 0x3c, 0x0a, 0x12,
     	0x53, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65,
    -	0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x74, 0x4c,
    +	0x73, 0x74, 0x12, 0x26, 0x0a, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28,
    +	0x0e, 0x32, 0x10, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x67, 0x4c, 0x65,
    +	0x76, 0x65, 0x6c, 0x52, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x22, 0x15, 0x0a, 0x13, 0x53, 0x65,
    +	0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
    +	0x65, 0x22, 0x1b, 0x0a, 0x05, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61,
    +	0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x13,
    +	0x0a, 0x11, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75,
    +	0x65, 0x73, 0x74, 0x22, 0x3b, 0x0a, 0x12, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65,
    +	0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x25, 0x0a, 0x06, 0x73, 0x74, 0x61,
    +	0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x64, 0x61, 0x65, 0x6d,
    +	0x6f, 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x65, 0x73,
    +	0x22, 0x44, 0x0a, 0x11, 0x43, 0x6c, 0x65, 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65,
    +	0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x74, 0x61, 0x74, 0x65, 0x5f, 0x6e,
    +	0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x74, 0x61, 0x74, 0x65,
    +	0x4e, 0x61, 0x6d, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x61, 0x6c, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28,
    +	0x08, 0x52, 0x03, 0x61, 0x6c, 0x6c, 0x22, 0x3b, 0x0a, 0x12, 0x43, 0x6c, 0x65, 0x61, 0x6e, 0x53,
    +	0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x25, 0x0a, 0x0e,
    +	0x63, 0x6c, 0x65, 0x61, 0x6e, 0x65, 0x64, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x73, 0x18, 0x01,
    +	0x20, 0x01, 0x28, 0x05, 0x52, 0x0d, 0x63, 0x6c, 0x65, 0x61, 0x6e, 0x65, 0x64, 0x53, 0x74, 0x61,
    +	0x74, 0x65, 0x73, 0x22, 0x45, 0x0a, 0x12, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x74, 0x61,
    +	0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x74, 0x61,
    +	0x74, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73,
    +	0x74, 0x61, 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x61, 0x6c, 0x6c, 0x18,
    +	0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x61, 0x6c, 0x6c, 0x22, 0x3c, 0x0a, 0x13, 0x44, 0x65,
    +	0x6c, 0x65, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
    +	0x65, 0x12, 0x25, 0x0a, 0x0e, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x5f, 0x73, 0x74, 0x61,
    +	0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0d, 0x64, 0x65, 0x6c, 0x65, 0x74,
    +	0x65, 0x64, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x22, 0x3b, 0x0a, 0x1f, 0x53, 0x65, 0x74, 0x4e,
    +	0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x50, 0x65, 0x72, 0x73, 0x69, 0x73, 0x74,
    +	0x65, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x65,
    +	0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e,
    +	0x61, 0x62, 0x6c, 0x65, 0x64, 0x22, 0x22, 0x0a, 0x20, 0x53, 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77,
    +	0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x50, 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63,
    +	0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x76, 0x0a, 0x08, 0x54, 0x43, 0x50,
    +	0x46, 0x6c, 0x61, 0x67, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x73, 0x79, 0x6e, 0x18, 0x01, 0x20, 0x01,
    +	0x28, 0x08, 0x52, 0x03, 0x73, 0x79, 0x6e, 0x12, 0x10, 0x0a, 0x03, 0x61, 0x63, 0x6b, 0x18, 0x02,
    +	0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x61, 0x63, 0x6b, 0x12, 0x10, 0x0a, 0x03, 0x66, 0x69, 0x6e,
    +	0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x66, 0x69, 0x6e, 0x12, 0x10, 0x0a, 0x03, 0x72,
    +	0x73, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x72, 0x73, 0x74, 0x12, 0x10, 0x0a,
    +	0x03, 0x70, 0x73, 0x68, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x70, 0x73, 0x68, 0x12,
    +	0x10, 0x0a, 0x03, 0x75, 0x72, 0x67, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x75, 0x72,
    +	0x67, 0x22, 0x80, 0x03, 0x0a, 0x12, 0x54, 0x72, 0x61, 0x63, 0x65, 0x50, 0x61, 0x63, 0x6b, 0x65,
    +	0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x73, 0x6f, 0x75, 0x72,
    +	0x63, 0x65, 0x5f, 0x69, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x73, 0x6f, 0x75,
    +	0x72, 0x63, 0x65, 0x49, 0x70, 0x12, 0x25, 0x0a, 0x0e, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61,
    +	0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x70, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x64,
    +	0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x70, 0x12, 0x1a, 0x0a, 0x08,
    +	0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08,
    +	0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x6f, 0x75, 0x72,
    +	0x63, 0x65, 0x5f, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0a, 0x73,
    +	0x6f, 0x75, 0x72, 0x63, 0x65, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x29, 0x0a, 0x10, 0x64, 0x65, 0x73,
    +	0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x05, 0x20,
    +	0x01, 0x28, 0x0d, 0x52, 0x0f, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e,
    +	0x50, 0x6f, 0x72, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f,
    +	0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69,
    +	0x6f, 0x6e, 0x12, 0x32, 0x0a, 0x09, 0x74, 0x63, 0x70, 0x5f, 0x66, 0x6c, 0x61, 0x67, 0x73, 0x18,
    +	0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x54,
    +	0x43, 0x50, 0x46, 0x6c, 0x61, 0x67, 0x73, 0x48, 0x00, 0x52, 0x08, 0x74, 0x63, 0x70, 0x46, 0x6c,
    +	0x61, 0x67, 0x73, 0x88, 0x01, 0x01, 0x12, 0x20, 0x0a, 0x09, 0x69, 0x63, 0x6d, 0x70, 0x5f, 0x74,
    +	0x79, 0x70, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0d, 0x48, 0x01, 0x52, 0x08, 0x69, 0x63, 0x6d,
    +	0x70, 0x54, 0x79, 0x70, 0x65, 0x88, 0x01, 0x01, 0x12, 0x20, 0x0a, 0x09, 0x69, 0x63, 0x6d, 0x70,
    +	0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0d, 0x48, 0x02, 0x52, 0x08, 0x69,
    +	0x63, 0x6d, 0x70, 0x43, 0x6f, 0x64, 0x65, 0x88, 0x01, 0x01, 0x42, 0x0c, 0x0a, 0x0a, 0x5f, 0x74,
    +	0x63, 0x70, 0x5f, 0x66, 0x6c, 0x61, 0x67, 0x73, 0x42, 0x0c, 0x0a, 0x0a, 0x5f, 0x69, 0x63, 0x6d,
    +	0x70, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x42, 0x0c, 0x0a, 0x0a, 0x5f, 0x69, 0x63, 0x6d, 0x70, 0x5f,
    +	0x63, 0x6f, 0x64, 0x65, 0x22, 0x9f, 0x01, 0x0a, 0x0a, 0x54, 0x72, 0x61, 0x63, 0x65, 0x53, 0x74,
    +	0x61, 0x67, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28,
    +	0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61,
    +	0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67,
    +	0x65, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01,
    +	0x28, 0x08, 0x52, 0x07, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x12, 0x32, 0x0a, 0x12, 0x66,
    +	0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x5f, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c,
    +	0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x11, 0x66, 0x6f, 0x72, 0x77, 0x61,
    +	0x72, 0x64, 0x69, 0x6e, 0x67, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x88, 0x01, 0x01, 0x42,
    +	0x15, 0x0a, 0x13, 0x5f, 0x66, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x5f, 0x64,
    +	0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x22, 0x6e, 0x0a, 0x13, 0x54, 0x72, 0x61, 0x63, 0x65, 0x50,
    +	0x61, 0x63, 0x6b, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2a, 0x0a,
    +	0x06, 0x73, 0x74, 0x61, 0x67, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e,
    +	0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x65, 0x53, 0x74, 0x61, 0x67,
    +	0x65, 0x52, 0x06, 0x73, 0x74, 0x61, 0x67, 0x65, 0x73, 0x12, 0x2b, 0x0a, 0x11, 0x66, 0x69, 0x6e,
    +	0x61, 0x6c, 0x5f, 0x64, 0x69, 0x73, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02,
    +	0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x66, 0x69, 0x6e, 0x61, 0x6c, 0x44, 0x69, 0x73, 0x70, 0x6f,
    +	0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x12, 0x0a, 0x10, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72,
    +	0x69, 0x62, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x87, 0x04, 0x0a, 0x0b, 0x53,
    +	0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64,
    +	0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x38, 0x0a, 0x08, 0x73, 0x65,
    +	0x76, 0x65, 0x72, 0x69, 0x74, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1c, 0x2e, 0x64,
    +	0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65, 0x6e,
    +	0x74, 0x2e, 0x53, 0x65, 0x76, 0x65, 0x72, 0x69, 0x74, 0x79, 0x52, 0x08, 0x73, 0x65, 0x76, 0x65,
    +	0x72, 0x69, 0x74, 0x79, 0x12, 0x38, 0x0a, 0x08, 0x63, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79,
    +	0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1c, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e,
    +	0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x61, 0x74, 0x65,
    +	0x67, 0x6f, 0x72, 0x79, 0x52, 0x08, 0x63, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79, 0x12, 0x18,
    +	0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52,
    +	0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x75, 0x73, 0x65, 0x72,
    +	0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x75,
    +	0x73, 0x65, 0x72, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x38, 0x0a, 0x09, 0x74, 0x69,
    +	0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e,
    +	0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e,
    +	0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73,
    +	0x74, 0x61, 0x6d, 0x70, 0x12, 0x3d, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61,
    +	0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e,
    +	0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x2e, 0x4d, 0x65, 0x74, 0x61,
    +	0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64,
    +	0x61, 0x74, 0x61, 0x1a, 0x3b, 0x0a, 0x0d, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x45,
    +	0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28,
    +	0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18,
    +	0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01,
    +	0x22, 0x3a, 0x0a, 0x08, 0x53, 0x65, 0x76, 0x65, 0x72, 0x69, 0x74, 0x79, 0x12, 0x08, 0x0a, 0x04,
    +	0x49, 0x4e, 0x46, 0x4f, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x57, 0x41, 0x52, 0x4e, 0x49, 0x4e,
    +	0x47, 0x10, 0x01, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x02, 0x12, 0x0c,
    +	0x0a, 0x08, 0x43, 0x52, 0x49, 0x54, 0x49, 0x43, 0x41, 0x4c, 0x10, 0x03, 0x22, 0x46, 0x0a, 0x08,
    +	0x43, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79, 0x12, 0x0b, 0x0a, 0x07, 0x4e, 0x45, 0x54, 0x57,
    +	0x4f, 0x52, 0x4b, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x44, 0x4e, 0x53, 0x10, 0x01, 0x12, 0x12,
    +	0x0a, 0x0e, 0x41, 0x55, 0x54, 0x48, 0x45, 0x4e, 0x54, 0x49, 0x43, 0x41, 0x54, 0x49, 0x4f, 0x4e,
    +	0x10, 0x02, 0x12, 0x10, 0x0a, 0x0c, 0x43, 0x4f, 0x4e, 0x4e, 0x45, 0x43, 0x54, 0x49, 0x56, 0x49,
    +	0x54, 0x59, 0x10, 0x03, 0x22, 0x12, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74,
    +	0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x40, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x45,
    +	0x76, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2b, 0x0a,
    +	0x06, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e,
    +	0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65,
    +	0x6e, 0x74, 0x52, 0x06, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x2a, 0x62, 0x0a, 0x08, 0x4c, 0x6f,
    +	0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57,
    +	0x4e, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x50, 0x41, 0x4e, 0x49, 0x43, 0x10, 0x01, 0x12, 0x09,
    +	0x0a, 0x05, 0x46, 0x41, 0x54, 0x41, 0x4c, 0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x52, 0x52,
    +	0x4f, 0x52, 0x10, 0x03, 0x12, 0x08, 0x0a, 0x04, 0x57, 0x41, 0x52, 0x4e, 0x10, 0x04, 0x12, 0x08,
    +	0x0a, 0x04, 0x49, 0x4e, 0x46, 0x4f, 0x10, 0x05, 0x12, 0x09, 0x0a, 0x05, 0x44, 0x45, 0x42, 0x55,
    +	0x47, 0x10, 0x06, 0x12, 0x09, 0x0a, 0x05, 0x54, 0x52, 0x41, 0x43, 0x45, 0x10, 0x07, 0x32, 0xe7,
    +	0x0a, 0x0a, 0x0d, 0x44, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65,
    +	0x12, 0x36, 0x0a, 0x05, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x14, 0x2e, 0x64, 0x61, 0x65, 0x6d,
    +	0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a,
    +	0x15, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65,
    +	0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x4b, 0x0a, 0x0c, 0x57, 0x61, 0x69, 0x74,
    +	0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f,
    +	0x6e, 0x2e, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65,
    +	0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x57,
    +	0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f,
    +	0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x2d, 0x0a, 0x02, 0x55, 0x70, 0x12, 0x11, 0x2e, 0x64, 0x61,
    +	0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x55, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12,
    +	0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x55, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
    +	0x73, 0x65, 0x22, 0x00, 0x12, 0x39, 0x0a, 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x15,
    +	0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65,
    +	0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53,
    +	0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12,
    +	0x33, 0x0a, 0x04, 0x44, 0x6f, 0x77, 0x6e, 0x12, 0x13, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e,
    +	0x2e, 0x44, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x64,
    +	0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
    +	0x73, 0x65, 0x22, 0x00, 0x12, 0x42, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69,
    +	0x67, 0x12, 0x18, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f,
    +	0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x64, 0x61,
    +	0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65,
    +	0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x4b, 0x0a, 0x0c, 0x4c, 0x69, 0x73, 0x74,
    +	0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x12, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f,
    +	0x6e, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65,
    +	0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c,
    +	0x69, 0x73, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f,
    +	0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x51, 0x0a, 0x0e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e,
    +	0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x12, 0x1d, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e,
    +	0x2e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52,
    +	0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e,
    +	0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65,
    +	0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x53, 0x0a, 0x10, 0x44, 0x65, 0x73, 0x65,
    +	0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x12, 0x1d, 0x2e, 0x64,
    +	0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77,
    +	0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x64, 0x61,
    +	0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f,
    +	0x72, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a,
    +	0x0b, 0x44, 0x65, 0x62, 0x75, 0x67, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x12, 0x1a, 0x2e, 0x64,
    +	0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x62, 0x75, 0x67, 0x42, 0x75, 0x6e, 0x64, 0x6c,
    +	0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f,
    +	0x6e, 0x2e, 0x44, 0x65, 0x62, 0x75, 0x67, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x52, 0x65, 0x73,
    +	0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x0b, 0x47, 0x65, 0x74, 0x4c, 0x6f,
    +	0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e,
    +	0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65,
    +	0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x4c,
     	0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22,
    -	0x00, 0x12, 0x45, 0x0a, 0x0a, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x12,
    -	0x19, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, 0x61,
    -	0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x64, 0x61, 0x65,
    -	0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x52, 0x65,
    -	0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x45, 0x0a, 0x0a, 0x43, 0x6c, 0x65, 0x61,
    -	0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x19, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e,
    -	0x43, 0x6c, 0x65, 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
    -	0x74, 0x1a, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x43, 0x6c, 0x65, 0x61, 0x6e,
    -	0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12,
    -	0x48, 0x0a, 0x0b, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x1a,
    -	0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x74,
    -	0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x65,
    -	0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52,
    -	0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x6f, 0x0a, 0x18, 0x53, 0x65, 0x74,
    -	0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x50, 0x65, 0x72, 0x73, 0x69, 0x73,
    -	0x74, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x27, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53,
    -	0x65, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x50, 0x65, 0x72, 0x73,
    -	0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x28,
    -	0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f,
    -	0x72, 0x6b, 0x4d, 0x61, 0x70, 0x50, 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65,
    -	0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x0b, 0x54, 0x72,
    -	0x61, 0x63, 0x65, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d,
    -	0x6f, 0x6e, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x65, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x52, 0x65,
    -	0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x54,
    -	0x72, 0x61, 0x63, 0x65, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 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,
    +	0x00, 0x12, 0x48, 0x0a, 0x0b, 0x53, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c,
    +	0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x74, 0x4c, 0x6f, 0x67,
    +	0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64,
    +	0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65,
    +	0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x45, 0x0a, 0x0a, 0x4c,
    +	0x69, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x12, 0x19, 0x2e, 0x64, 0x61, 0x65, 0x6d,
    +	0x6f, 0x6e, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x52, 0x65, 0x71,
    +	0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x69,
    +	0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
    +	0x22, 0x00, 0x12, 0x45, 0x0a, 0x0a, 0x43, 0x6c, 0x65, 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65,
    +	0x12, 0x19, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x43, 0x6c, 0x65, 0x61, 0x6e, 0x53,
    +	0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x64, 0x61,
    +	0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x43, 0x6c, 0x65, 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52,
    +	0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x0b, 0x44, 0x65, 0x6c,
    +	0x65, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f,
    +	0x6e, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71,
    +	0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65,
    +	0x6c, 0x65, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
    +	0x65, 0x22, 0x00, 0x12, 0x6f, 0x0a, 0x18, 0x53, 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72,
    +	0x6b, 0x4d, 0x61, 0x70, 0x50, 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x12,
    +	0x27, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77,
    +	0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x50, 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63,
    +	0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x28, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f,
    +	0x6e, 0x2e, 0x53, 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x50,
    +	0x65, 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
    +	0x73, 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x0b, 0x54, 0x72, 0x61, 0x63, 0x65, 0x50, 0x61, 0x63,
    +	0x6b, 0x65, 0x74, 0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x54, 0x72, 0x61,
    +	0x63, 0x65, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a,
    +	0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x65, 0x50, 0x61,
    +	0x63, 0x6b, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x44,
    +	0x0a, 0x0f, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x45, 0x76, 0x65, 0x6e, 0x74,
    +	0x73, 0x12, 0x18, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x75, 0x62, 0x73, 0x63,
    +	0x72, 0x69, 0x62, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x13, 0x2e, 0x64, 0x61,
    +	0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74,
    +	0x22, 0x00, 0x30, 0x01, 0x12, 0x42, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74,
    +	0x73, 0x12, 0x18, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x45, 0x76,
    +	0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x64, 0x61,
    +	0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 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 (
    @@ -3393,117 +3798,134 @@ func file_daemon_proto_rawDescGZIP() []byte {
     	return file_daemon_proto_rawDescData
     }
     
    -var file_daemon_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
    -var file_daemon_proto_msgTypes = make([]protoimpl.MessageInfo, 45)
    +var file_daemon_proto_enumTypes = make([]protoimpl.EnumInfo, 3)
    +var file_daemon_proto_msgTypes = make([]protoimpl.MessageInfo, 50)
     var file_daemon_proto_goTypes = []interface{}{
     	(LogLevel)(0),                            // 0: daemon.LogLevel
    -	(*LoginRequest)(nil),                     // 1: daemon.LoginRequest
    -	(*LoginResponse)(nil),                    // 2: daemon.LoginResponse
    -	(*WaitSSOLoginRequest)(nil),              // 3: daemon.WaitSSOLoginRequest
    -	(*WaitSSOLoginResponse)(nil),             // 4: daemon.WaitSSOLoginResponse
    -	(*UpRequest)(nil),                        // 5: daemon.UpRequest
    -	(*UpResponse)(nil),                       // 6: daemon.UpResponse
    -	(*StatusRequest)(nil),                    // 7: daemon.StatusRequest
    -	(*StatusResponse)(nil),                   // 8: daemon.StatusResponse
    -	(*DownRequest)(nil),                      // 9: daemon.DownRequest
    -	(*DownResponse)(nil),                     // 10: daemon.DownResponse
    -	(*GetConfigRequest)(nil),                 // 11: daemon.GetConfigRequest
    -	(*GetConfigResponse)(nil),                // 12: daemon.GetConfigResponse
    -	(*PeerState)(nil),                        // 13: daemon.PeerState
    -	(*LocalPeerState)(nil),                   // 14: daemon.LocalPeerState
    -	(*SignalState)(nil),                      // 15: daemon.SignalState
    -	(*ManagementState)(nil),                  // 16: daemon.ManagementState
    -	(*RelayState)(nil),                       // 17: daemon.RelayState
    -	(*NSGroupState)(nil),                     // 18: daemon.NSGroupState
    -	(*FullStatus)(nil),                       // 19: daemon.FullStatus
    -	(*ListNetworksRequest)(nil),              // 20: daemon.ListNetworksRequest
    -	(*ListNetworksResponse)(nil),             // 21: daemon.ListNetworksResponse
    -	(*SelectNetworksRequest)(nil),            // 22: daemon.SelectNetworksRequest
    -	(*SelectNetworksResponse)(nil),           // 23: daemon.SelectNetworksResponse
    -	(*IPList)(nil),                           // 24: daemon.IPList
    -	(*Network)(nil),                          // 25: daemon.Network
    -	(*DebugBundleRequest)(nil),               // 26: daemon.DebugBundleRequest
    -	(*DebugBundleResponse)(nil),              // 27: daemon.DebugBundleResponse
    -	(*GetLogLevelRequest)(nil),               // 28: daemon.GetLogLevelRequest
    -	(*GetLogLevelResponse)(nil),              // 29: daemon.GetLogLevelResponse
    -	(*SetLogLevelRequest)(nil),               // 30: daemon.SetLogLevelRequest
    -	(*SetLogLevelResponse)(nil),              // 31: daemon.SetLogLevelResponse
    -	(*State)(nil),                            // 32: daemon.State
    -	(*ListStatesRequest)(nil),                // 33: daemon.ListStatesRequest
    -	(*ListStatesResponse)(nil),               // 34: daemon.ListStatesResponse
    -	(*CleanStateRequest)(nil),                // 35: daemon.CleanStateRequest
    -	(*CleanStateResponse)(nil),               // 36: daemon.CleanStateResponse
    -	(*DeleteStateRequest)(nil),               // 37: daemon.DeleteStateRequest
    -	(*DeleteStateResponse)(nil),              // 38: daemon.DeleteStateResponse
    -	(*SetNetworkMapPersistenceRequest)(nil),  // 39: daemon.SetNetworkMapPersistenceRequest
    -	(*SetNetworkMapPersistenceResponse)(nil), // 40: daemon.SetNetworkMapPersistenceResponse
    -	(*TCPFlags)(nil),                         // 41: daemon.TCPFlags
    -	(*TracePacketRequest)(nil),               // 42: daemon.TracePacketRequest
    -	(*TraceStage)(nil),                       // 43: daemon.TraceStage
    -	(*TracePacketResponse)(nil),              // 44: daemon.TracePacketResponse
    -	nil,                                      // 45: daemon.Network.ResolvedIPsEntry
    -	(*durationpb.Duration)(nil),              // 46: google.protobuf.Duration
    -	(*timestamppb.Timestamp)(nil),            // 47: google.protobuf.Timestamp
    +	(SystemEvent_Severity)(0),                // 1: daemon.SystemEvent.Severity
    +	(SystemEvent_Category)(0),                // 2: daemon.SystemEvent.Category
    +	(*LoginRequest)(nil),                     // 3: daemon.LoginRequest
    +	(*LoginResponse)(nil),                    // 4: daemon.LoginResponse
    +	(*WaitSSOLoginRequest)(nil),              // 5: daemon.WaitSSOLoginRequest
    +	(*WaitSSOLoginResponse)(nil),             // 6: daemon.WaitSSOLoginResponse
    +	(*UpRequest)(nil),                        // 7: daemon.UpRequest
    +	(*UpResponse)(nil),                       // 8: daemon.UpResponse
    +	(*StatusRequest)(nil),                    // 9: daemon.StatusRequest
    +	(*StatusResponse)(nil),                   // 10: daemon.StatusResponse
    +	(*DownRequest)(nil),                      // 11: daemon.DownRequest
    +	(*DownResponse)(nil),                     // 12: daemon.DownResponse
    +	(*GetConfigRequest)(nil),                 // 13: daemon.GetConfigRequest
    +	(*GetConfigResponse)(nil),                // 14: daemon.GetConfigResponse
    +	(*PeerState)(nil),                        // 15: daemon.PeerState
    +	(*LocalPeerState)(nil),                   // 16: daemon.LocalPeerState
    +	(*SignalState)(nil),                      // 17: daemon.SignalState
    +	(*ManagementState)(nil),                  // 18: daemon.ManagementState
    +	(*RelayState)(nil),                       // 19: daemon.RelayState
    +	(*NSGroupState)(nil),                     // 20: daemon.NSGroupState
    +	(*FullStatus)(nil),                       // 21: daemon.FullStatus
    +	(*ListNetworksRequest)(nil),              // 22: daemon.ListNetworksRequest
    +	(*ListNetworksResponse)(nil),             // 23: daemon.ListNetworksResponse
    +	(*SelectNetworksRequest)(nil),            // 24: daemon.SelectNetworksRequest
    +	(*SelectNetworksResponse)(nil),           // 25: daemon.SelectNetworksResponse
    +	(*IPList)(nil),                           // 26: daemon.IPList
    +	(*Network)(nil),                          // 27: daemon.Network
    +	(*DebugBundleRequest)(nil),               // 28: daemon.DebugBundleRequest
    +	(*DebugBundleResponse)(nil),              // 29: daemon.DebugBundleResponse
    +	(*GetLogLevelRequest)(nil),               // 30: daemon.GetLogLevelRequest
    +	(*GetLogLevelResponse)(nil),              // 31: daemon.GetLogLevelResponse
    +	(*SetLogLevelRequest)(nil),               // 32: daemon.SetLogLevelRequest
    +	(*SetLogLevelResponse)(nil),              // 33: daemon.SetLogLevelResponse
    +	(*State)(nil),                            // 34: daemon.State
    +	(*ListStatesRequest)(nil),                // 35: daemon.ListStatesRequest
    +	(*ListStatesResponse)(nil),               // 36: daemon.ListStatesResponse
    +	(*CleanStateRequest)(nil),                // 37: daemon.CleanStateRequest
    +	(*CleanStateResponse)(nil),               // 38: daemon.CleanStateResponse
    +	(*DeleteStateRequest)(nil),               // 39: daemon.DeleteStateRequest
    +	(*DeleteStateResponse)(nil),              // 40: daemon.DeleteStateResponse
    +	(*SetNetworkMapPersistenceRequest)(nil),  // 41: daemon.SetNetworkMapPersistenceRequest
    +	(*SetNetworkMapPersistenceResponse)(nil), // 42: daemon.SetNetworkMapPersistenceResponse
    +	(*TCPFlags)(nil),                         // 43: daemon.TCPFlags
    +	(*TracePacketRequest)(nil),               // 44: daemon.TracePacketRequest
    +	(*TraceStage)(nil),                       // 45: daemon.TraceStage
    +	(*TracePacketResponse)(nil),              // 46: daemon.TracePacketResponse
    +	(*SubscribeRequest)(nil),                 // 47: daemon.SubscribeRequest
    +	(*SystemEvent)(nil),                      // 48: daemon.SystemEvent
    +	(*GetEventsRequest)(nil),                 // 49: daemon.GetEventsRequest
    +	(*GetEventsResponse)(nil),                // 50: daemon.GetEventsResponse
    +	nil,                                      // 51: daemon.Network.ResolvedIPsEntry
    +	nil,                                      // 52: daemon.SystemEvent.MetadataEntry
    +	(*durationpb.Duration)(nil),              // 53: google.protobuf.Duration
    +	(*timestamppb.Timestamp)(nil),            // 54: google.protobuf.Timestamp
     }
     var file_daemon_proto_depIdxs = []int32{
    -	46, // 0: daemon.LoginRequest.dnsRouteInterval:type_name -> google.protobuf.Duration
    -	19, // 1: daemon.StatusResponse.fullStatus:type_name -> daemon.FullStatus
    -	47, // 2: daemon.PeerState.connStatusUpdate:type_name -> google.protobuf.Timestamp
    -	47, // 3: daemon.PeerState.lastWireguardHandshake:type_name -> google.protobuf.Timestamp
    -	46, // 4: daemon.PeerState.latency:type_name -> google.protobuf.Duration
    -	16, // 5: daemon.FullStatus.managementState:type_name -> daemon.ManagementState
    -	15, // 6: daemon.FullStatus.signalState:type_name -> daemon.SignalState
    -	14, // 7: daemon.FullStatus.localPeerState:type_name -> daemon.LocalPeerState
    -	13, // 8: daemon.FullStatus.peers:type_name -> daemon.PeerState
    -	17, // 9: daemon.FullStatus.relays:type_name -> daemon.RelayState
    -	18, // 10: daemon.FullStatus.dns_servers:type_name -> daemon.NSGroupState
    -	25, // 11: daemon.ListNetworksResponse.routes:type_name -> daemon.Network
    -	45, // 12: daemon.Network.resolvedIPs:type_name -> daemon.Network.ResolvedIPsEntry
    -	0,  // 13: daemon.GetLogLevelResponse.level:type_name -> daemon.LogLevel
    -	0,  // 14: daemon.SetLogLevelRequest.level:type_name -> daemon.LogLevel
    -	32, // 15: daemon.ListStatesResponse.states:type_name -> daemon.State
    -	41, // 16: daemon.TracePacketRequest.tcp_flags:type_name -> daemon.TCPFlags
    -	43, // 17: daemon.TracePacketResponse.stages:type_name -> daemon.TraceStage
    -	24, // 18: daemon.Network.ResolvedIPsEntry.value:type_name -> daemon.IPList
    -	1,  // 19: daemon.DaemonService.Login:input_type -> daemon.LoginRequest
    -	3,  // 20: daemon.DaemonService.WaitSSOLogin:input_type -> daemon.WaitSSOLoginRequest
    -	5,  // 21: daemon.DaemonService.Up:input_type -> daemon.UpRequest
    -	7,  // 22: daemon.DaemonService.Status:input_type -> daemon.StatusRequest
    -	9,  // 23: daemon.DaemonService.Down:input_type -> daemon.DownRequest
    -	11, // 24: daemon.DaemonService.GetConfig:input_type -> daemon.GetConfigRequest
    -	20, // 25: daemon.DaemonService.ListNetworks:input_type -> daemon.ListNetworksRequest
    -	22, // 26: daemon.DaemonService.SelectNetworks:input_type -> daemon.SelectNetworksRequest
    -	22, // 27: daemon.DaemonService.DeselectNetworks:input_type -> daemon.SelectNetworksRequest
    -	26, // 28: daemon.DaemonService.DebugBundle:input_type -> daemon.DebugBundleRequest
    -	28, // 29: daemon.DaemonService.GetLogLevel:input_type -> daemon.GetLogLevelRequest
    -	30, // 30: daemon.DaemonService.SetLogLevel:input_type -> daemon.SetLogLevelRequest
    -	33, // 31: daemon.DaemonService.ListStates:input_type -> daemon.ListStatesRequest
    -	35, // 32: daemon.DaemonService.CleanState:input_type -> daemon.CleanStateRequest
    -	37, // 33: daemon.DaemonService.DeleteState:input_type -> daemon.DeleteStateRequest
    -	39, // 34: daemon.DaemonService.SetNetworkMapPersistence:input_type -> daemon.SetNetworkMapPersistenceRequest
    -	42, // 35: daemon.DaemonService.TracePacket:input_type -> daemon.TracePacketRequest
    -	2,  // 36: daemon.DaemonService.Login:output_type -> daemon.LoginResponse
    -	4,  // 37: daemon.DaemonService.WaitSSOLogin:output_type -> daemon.WaitSSOLoginResponse
    -	6,  // 38: daemon.DaemonService.Up:output_type -> daemon.UpResponse
    -	8,  // 39: daemon.DaemonService.Status:output_type -> daemon.StatusResponse
    -	10, // 40: daemon.DaemonService.Down:output_type -> daemon.DownResponse
    -	12, // 41: daemon.DaemonService.GetConfig:output_type -> daemon.GetConfigResponse
    -	21, // 42: daemon.DaemonService.ListNetworks:output_type -> daemon.ListNetworksResponse
    -	23, // 43: daemon.DaemonService.SelectNetworks:output_type -> daemon.SelectNetworksResponse
    -	23, // 44: daemon.DaemonService.DeselectNetworks:output_type -> daemon.SelectNetworksResponse
    -	27, // 45: daemon.DaemonService.DebugBundle:output_type -> daemon.DebugBundleResponse
    -	29, // 46: daemon.DaemonService.GetLogLevel:output_type -> daemon.GetLogLevelResponse
    -	31, // 47: daemon.DaemonService.SetLogLevel:output_type -> daemon.SetLogLevelResponse
    -	34, // 48: daemon.DaemonService.ListStates:output_type -> daemon.ListStatesResponse
    -	36, // 49: daemon.DaemonService.CleanState:output_type -> daemon.CleanStateResponse
    -	38, // 50: daemon.DaemonService.DeleteState:output_type -> daemon.DeleteStateResponse
    -	40, // 51: daemon.DaemonService.SetNetworkMapPersistence:output_type -> daemon.SetNetworkMapPersistenceResponse
    -	44, // 52: daemon.DaemonService.TracePacket:output_type -> daemon.TracePacketResponse
    -	36, // [36:53] is the sub-list for method output_type
    -	19, // [19:36] is the sub-list for method input_type
    -	19, // [19:19] is the sub-list for extension type_name
    -	19, // [19:19] is the sub-list for extension extendee
    -	0,  // [0:19] is the sub-list for field type_name
    +	53, // 0: daemon.LoginRequest.dnsRouteInterval:type_name -> google.protobuf.Duration
    +	21, // 1: daemon.StatusResponse.fullStatus:type_name -> daemon.FullStatus
    +	54, // 2: daemon.PeerState.connStatusUpdate:type_name -> google.protobuf.Timestamp
    +	54, // 3: daemon.PeerState.lastWireguardHandshake:type_name -> google.protobuf.Timestamp
    +	53, // 4: daemon.PeerState.latency:type_name -> google.protobuf.Duration
    +	18, // 5: daemon.FullStatus.managementState:type_name -> daemon.ManagementState
    +	17, // 6: daemon.FullStatus.signalState:type_name -> daemon.SignalState
    +	16, // 7: daemon.FullStatus.localPeerState:type_name -> daemon.LocalPeerState
    +	15, // 8: daemon.FullStatus.peers:type_name -> daemon.PeerState
    +	19, // 9: daemon.FullStatus.relays:type_name -> daemon.RelayState
    +	20, // 10: daemon.FullStatus.dns_servers:type_name -> daemon.NSGroupState
    +	48, // 11: daemon.FullStatus.events:type_name -> daemon.SystemEvent
    +	27, // 12: daemon.ListNetworksResponse.routes:type_name -> daemon.Network
    +	51, // 13: daemon.Network.resolvedIPs:type_name -> daemon.Network.ResolvedIPsEntry
    +	0,  // 14: daemon.GetLogLevelResponse.level:type_name -> daemon.LogLevel
    +	0,  // 15: daemon.SetLogLevelRequest.level:type_name -> daemon.LogLevel
    +	34, // 16: daemon.ListStatesResponse.states:type_name -> daemon.State
    +	43, // 17: daemon.TracePacketRequest.tcp_flags:type_name -> daemon.TCPFlags
    +	45, // 18: daemon.TracePacketResponse.stages:type_name -> daemon.TraceStage
    +	1,  // 19: daemon.SystemEvent.severity:type_name -> daemon.SystemEvent.Severity
    +	2,  // 20: daemon.SystemEvent.category:type_name -> daemon.SystemEvent.Category
    +	54, // 21: daemon.SystemEvent.timestamp:type_name -> google.protobuf.Timestamp
    +	52, // 22: daemon.SystemEvent.metadata:type_name -> daemon.SystemEvent.MetadataEntry
    +	48, // 23: daemon.GetEventsResponse.events:type_name -> daemon.SystemEvent
    +	26, // 24: daemon.Network.ResolvedIPsEntry.value:type_name -> daemon.IPList
    +	3,  // 25: daemon.DaemonService.Login:input_type -> daemon.LoginRequest
    +	5,  // 26: daemon.DaemonService.WaitSSOLogin:input_type -> daemon.WaitSSOLoginRequest
    +	7,  // 27: daemon.DaemonService.Up:input_type -> daemon.UpRequest
    +	9,  // 28: daemon.DaemonService.Status:input_type -> daemon.StatusRequest
    +	11, // 29: daemon.DaemonService.Down:input_type -> daemon.DownRequest
    +	13, // 30: daemon.DaemonService.GetConfig:input_type -> daemon.GetConfigRequest
    +	22, // 31: daemon.DaemonService.ListNetworks:input_type -> daemon.ListNetworksRequest
    +	24, // 32: daemon.DaemonService.SelectNetworks:input_type -> daemon.SelectNetworksRequest
    +	24, // 33: daemon.DaemonService.DeselectNetworks:input_type -> daemon.SelectNetworksRequest
    +	28, // 34: daemon.DaemonService.DebugBundle:input_type -> daemon.DebugBundleRequest
    +	30, // 35: daemon.DaemonService.GetLogLevel:input_type -> daemon.GetLogLevelRequest
    +	32, // 36: daemon.DaemonService.SetLogLevel:input_type -> daemon.SetLogLevelRequest
    +	35, // 37: daemon.DaemonService.ListStates:input_type -> daemon.ListStatesRequest
    +	37, // 38: daemon.DaemonService.CleanState:input_type -> daemon.CleanStateRequest
    +	39, // 39: daemon.DaemonService.DeleteState:input_type -> daemon.DeleteStateRequest
    +	41, // 40: daemon.DaemonService.SetNetworkMapPersistence:input_type -> daemon.SetNetworkMapPersistenceRequest
    +	44, // 41: daemon.DaemonService.TracePacket:input_type -> daemon.TracePacketRequest
    +	47, // 42: daemon.DaemonService.SubscribeEvents:input_type -> daemon.SubscribeRequest
    +	49, // 43: daemon.DaemonService.GetEvents:input_type -> daemon.GetEventsRequest
    +	4,  // 44: daemon.DaemonService.Login:output_type -> daemon.LoginResponse
    +	6,  // 45: daemon.DaemonService.WaitSSOLogin:output_type -> daemon.WaitSSOLoginResponse
    +	8,  // 46: daemon.DaemonService.Up:output_type -> daemon.UpResponse
    +	10, // 47: daemon.DaemonService.Status:output_type -> daemon.StatusResponse
    +	12, // 48: daemon.DaemonService.Down:output_type -> daemon.DownResponse
    +	14, // 49: daemon.DaemonService.GetConfig:output_type -> daemon.GetConfigResponse
    +	23, // 50: daemon.DaemonService.ListNetworks:output_type -> daemon.ListNetworksResponse
    +	25, // 51: daemon.DaemonService.SelectNetworks:output_type -> daemon.SelectNetworksResponse
    +	25, // 52: daemon.DaemonService.DeselectNetworks:output_type -> daemon.SelectNetworksResponse
    +	29, // 53: daemon.DaemonService.DebugBundle:output_type -> daemon.DebugBundleResponse
    +	31, // 54: daemon.DaemonService.GetLogLevel:output_type -> daemon.GetLogLevelResponse
    +	33, // 55: daemon.DaemonService.SetLogLevel:output_type -> daemon.SetLogLevelResponse
    +	36, // 56: daemon.DaemonService.ListStates:output_type -> daemon.ListStatesResponse
    +	38, // 57: daemon.DaemonService.CleanState:output_type -> daemon.CleanStateResponse
    +	40, // 58: daemon.DaemonService.DeleteState:output_type -> daemon.DeleteStateResponse
    +	42, // 59: daemon.DaemonService.SetNetworkMapPersistence:output_type -> daemon.SetNetworkMapPersistenceResponse
    +	46, // 60: daemon.DaemonService.TracePacket:output_type -> daemon.TracePacketResponse
    +	48, // 61: daemon.DaemonService.SubscribeEvents:output_type -> daemon.SystemEvent
    +	50, // 62: daemon.DaemonService.GetEvents:output_type -> daemon.GetEventsResponse
    +	44, // [44:63] is the sub-list for method output_type
    +	25, // [25:44] is the sub-list for method input_type
    +	25, // [25:25] is the sub-list for extension type_name
    +	25, // [25:25] is the sub-list for extension extendee
    +	0,  // [0:25] is the sub-list for field type_name
     }
     
     func init() { file_daemon_proto_init() }
    @@ -4040,6 +4462,54 @@ func file_daemon_proto_init() {
     				return nil
     			}
     		}
    +		file_daemon_proto_msgTypes[44].Exporter = func(v interface{}, i int) interface{} {
    +			switch v := v.(*SubscribeRequest); i {
    +			case 0:
    +				return &v.state
    +			case 1:
    +				return &v.sizeCache
    +			case 2:
    +				return &v.unknownFields
    +			default:
    +				return nil
    +			}
    +		}
    +		file_daemon_proto_msgTypes[45].Exporter = func(v interface{}, i int) interface{} {
    +			switch v := v.(*SystemEvent); i {
    +			case 0:
    +				return &v.state
    +			case 1:
    +				return &v.sizeCache
    +			case 2:
    +				return &v.unknownFields
    +			default:
    +				return nil
    +			}
    +		}
    +		file_daemon_proto_msgTypes[46].Exporter = func(v interface{}, i int) interface{} {
    +			switch v := v.(*GetEventsRequest); i {
    +			case 0:
    +				return &v.state
    +			case 1:
    +				return &v.sizeCache
    +			case 2:
    +				return &v.unknownFields
    +			default:
    +				return nil
    +			}
    +		}
    +		file_daemon_proto_msgTypes[47].Exporter = func(v interface{}, i int) interface{} {
    +			switch v := v.(*GetEventsResponse); i {
    +			case 0:
    +				return &v.state
    +			case 1:
    +				return &v.sizeCache
    +			case 2:
    +				return &v.unknownFields
    +			default:
    +				return nil
    +			}
    +		}
     	}
     	file_daemon_proto_msgTypes[0].OneofWrappers = []interface{}{}
     	file_daemon_proto_msgTypes[41].OneofWrappers = []interface{}{}
    @@ -4049,8 +4519,8 @@ func file_daemon_proto_init() {
     		File: protoimpl.DescBuilder{
     			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
     			RawDescriptor: file_daemon_proto_rawDesc,
    -			NumEnums:      1,
    -			NumMessages:   45,
    +			NumEnums:      3,
    +			NumMessages:   50,
     			NumExtensions: 0,
     			NumServices:   1,
     		},
    diff --git a/client/proto/daemon.proto b/client/proto/daemon.proto
    index 412449076..92a289c41 100644
    --- a/client/proto/daemon.proto
    +++ b/client/proto/daemon.proto
    @@ -59,6 +59,10 @@ service DaemonService {
       rpc SetNetworkMapPersistence(SetNetworkMapPersistenceRequest) returns (SetNetworkMapPersistenceResponse) {}
     
       rpc TracePacket(TracePacketRequest) returns (TracePacketResponse) {}
    +
    +  rpc SubscribeEvents(SubscribeRequest) returns (stream SystemEvent) {}
    +
    +  rpc GetEvents(GetEventsRequest) returns (GetEventsResponse) {}
     }
     
     
    @@ -116,6 +120,8 @@ message LoginRequest {
       optional bool disable_firewall = 23;
     
       optional bool block_lan_access = 24;
    +
    +  optional bool disable_notifications = 25;
     }
     
     message LoginResponse {
    @@ -181,6 +187,8 @@ message GetConfigResponse {
       bool rosenpassEnabled = 11;
     
       bool rosenpassPermissive = 12;
    +
    +  bool disable_notifications = 13;
     }
     
     // PeerState contains the latest state of a peer
    @@ -251,6 +259,8 @@ message FullStatus {
       repeated PeerState peers = 4;
       repeated RelayState relays = 5;
       repeated NSGroupState dns_servers = 6;
    +
    +  repeated SystemEvent events = 7;
     }
     
     message ListNetworksRequest {
    @@ -391,3 +401,35 @@ message TracePacketResponse {
       repeated TraceStage stages = 1;
       bool final_disposition = 2;
     }
    +
    +message SubscribeRequest{}
    +
    +message SystemEvent {
    +  enum Severity {
    +    INFO = 0;
    +    WARNING = 1;
    +    ERROR = 2;
    +    CRITICAL = 3;
    +  }
    +
    +  enum Category {
    +    NETWORK = 0;
    +    DNS = 1;
    +    AUTHENTICATION = 2;
    +    CONNECTIVITY = 3;
    +  }
    +
    +  string id = 1;
    +  Severity severity = 2;
    +  Category category = 3;
    +  string message = 4;
    +  string userMessage = 5;
    +  google.protobuf.Timestamp timestamp = 6;
    +  map metadata = 7;
    +}
    +
    +message GetEventsRequest {}
    +
    +message GetEventsResponse {
    +  repeated SystemEvent events = 1;
    +}
    diff --git a/client/proto/daemon_grpc.pb.go b/client/proto/daemon_grpc.pb.go
    index 9dcb543a8..0cb2a7c59 100644
    --- a/client/proto/daemon_grpc.pb.go
    +++ b/client/proto/daemon_grpc.pb.go
    @@ -52,6 +52,8 @@ type DaemonServiceClient interface {
     	// SetNetworkMapPersistence enables or disables network map persistence
     	SetNetworkMapPersistence(ctx context.Context, in *SetNetworkMapPersistenceRequest, opts ...grpc.CallOption) (*SetNetworkMapPersistenceResponse, error)
     	TracePacket(ctx context.Context, in *TracePacketRequest, opts ...grpc.CallOption) (*TracePacketResponse, error)
    +	SubscribeEvents(ctx context.Context, in *SubscribeRequest, opts ...grpc.CallOption) (DaemonService_SubscribeEventsClient, error)
    +	GetEvents(ctx context.Context, in *GetEventsRequest, opts ...grpc.CallOption) (*GetEventsResponse, error)
     }
     
     type daemonServiceClient struct {
    @@ -215,6 +217,47 @@ func (c *daemonServiceClient) TracePacket(ctx context.Context, in *TracePacketRe
     	return out, nil
     }
     
    +func (c *daemonServiceClient) SubscribeEvents(ctx context.Context, in *SubscribeRequest, opts ...grpc.CallOption) (DaemonService_SubscribeEventsClient, error) {
    +	stream, err := c.cc.NewStream(ctx, &DaemonService_ServiceDesc.Streams[0], "/daemon.DaemonService/SubscribeEvents", opts...)
    +	if err != nil {
    +		return nil, err
    +	}
    +	x := &daemonServiceSubscribeEventsClient{stream}
    +	if err := x.ClientStream.SendMsg(in); err != nil {
    +		return nil, err
    +	}
    +	if err := x.ClientStream.CloseSend(); err != nil {
    +		return nil, err
    +	}
    +	return x, nil
    +}
    +
    +type DaemonService_SubscribeEventsClient interface {
    +	Recv() (*SystemEvent, error)
    +	grpc.ClientStream
    +}
    +
    +type daemonServiceSubscribeEventsClient struct {
    +	grpc.ClientStream
    +}
    +
    +func (x *daemonServiceSubscribeEventsClient) Recv() (*SystemEvent, error) {
    +	m := new(SystemEvent)
    +	if err := x.ClientStream.RecvMsg(m); err != nil {
    +		return nil, err
    +	}
    +	return m, nil
    +}
    +
    +func (c *daemonServiceClient) GetEvents(ctx context.Context, in *GetEventsRequest, opts ...grpc.CallOption) (*GetEventsResponse, error) {
    +	out := new(GetEventsResponse)
    +	err := c.cc.Invoke(ctx, "/daemon.DaemonService/GetEvents", 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
    @@ -253,6 +296,8 @@ type DaemonServiceServer interface {
     	// SetNetworkMapPersistence enables or disables network map persistence
     	SetNetworkMapPersistence(context.Context, *SetNetworkMapPersistenceRequest) (*SetNetworkMapPersistenceResponse, error)
     	TracePacket(context.Context, *TracePacketRequest) (*TracePacketResponse, error)
    +	SubscribeEvents(*SubscribeRequest, DaemonService_SubscribeEventsServer) error
    +	GetEvents(context.Context, *GetEventsRequest) (*GetEventsResponse, error)
     	mustEmbedUnimplementedDaemonServiceServer()
     }
     
    @@ -311,6 +356,12 @@ func (UnimplementedDaemonServiceServer) SetNetworkMapPersistence(context.Context
     func (UnimplementedDaemonServiceServer) TracePacket(context.Context, *TracePacketRequest) (*TracePacketResponse, error) {
     	return nil, status.Errorf(codes.Unimplemented, "method TracePacket not implemented")
     }
    +func (UnimplementedDaemonServiceServer) SubscribeEvents(*SubscribeRequest, DaemonService_SubscribeEventsServer) error {
    +	return status.Errorf(codes.Unimplemented, "method SubscribeEvents not implemented")
    +}
    +func (UnimplementedDaemonServiceServer) GetEvents(context.Context, *GetEventsRequest) (*GetEventsResponse, error) {
    +	return nil, status.Errorf(codes.Unimplemented, "method GetEvents not implemented")
    +}
     func (UnimplementedDaemonServiceServer) mustEmbedUnimplementedDaemonServiceServer() {}
     
     // UnsafeDaemonServiceServer may be embedded to opt out of forward compatibility for this service.
    @@ -630,6 +681,45 @@ func _DaemonService_TracePacket_Handler(srv interface{}, ctx context.Context, de
     	return interceptor(ctx, in, info, handler)
     }
     
    +func _DaemonService_SubscribeEvents_Handler(srv interface{}, stream grpc.ServerStream) error {
    +	m := new(SubscribeRequest)
    +	if err := stream.RecvMsg(m); err != nil {
    +		return err
    +	}
    +	return srv.(DaemonServiceServer).SubscribeEvents(m, &daemonServiceSubscribeEventsServer{stream})
    +}
    +
    +type DaemonService_SubscribeEventsServer interface {
    +	Send(*SystemEvent) error
    +	grpc.ServerStream
    +}
    +
    +type daemonServiceSubscribeEventsServer struct {
    +	grpc.ServerStream
    +}
    +
    +func (x *daemonServiceSubscribeEventsServer) Send(m *SystemEvent) error {
    +	return x.ServerStream.SendMsg(m)
    +}
    +
    +func _DaemonService_GetEvents_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
    +	in := new(GetEventsRequest)
    +	if err := dec(in); err != nil {
    +		return nil, err
    +	}
    +	if interceptor == nil {
    +		return srv.(DaemonServiceServer).GetEvents(ctx, in)
    +	}
    +	info := &grpc.UnaryServerInfo{
    +		Server:     srv,
    +		FullMethod: "/daemon.DaemonService/GetEvents",
    +	}
    +	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
    +		return srv.(DaemonServiceServer).GetEvents(ctx, req.(*GetEventsRequest))
    +	}
    +	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)
    @@ -705,7 +795,17 @@ var DaemonService_ServiceDesc = grpc.ServiceDesc{
     			MethodName: "TracePacket",
     			Handler:    _DaemonService_TracePacket_Handler,
     		},
    +		{
    +			MethodName: "GetEvents",
    +			Handler:    _DaemonService_GetEvents_Handler,
    +		},
    +	},
    +	Streams: []grpc.StreamDesc{
    +		{
    +			StreamName:    "SubscribeEvents",
    +			Handler:       _DaemonService_SubscribeEvents_Handler,
    +			ServerStreams: true,
    +		},
     	},
    -	Streams:  []grpc.StreamDesc{},
     	Metadata: "daemon.proto",
     }
    diff --git a/client/server/event.go b/client/server/event.go
    new file mode 100644
    index 000000000..9a4e0fbf5
    --- /dev/null
    +++ b/client/server/event.go
    @@ -0,0 +1,36 @@
    +package server
    +
    +import (
    +	"context"
    +
    +	log "github.com/sirupsen/logrus"
    +
    +	"github.com/netbirdio/netbird/client/proto"
    +)
    +
    +func (s *Server) SubscribeEvents(req *proto.SubscribeRequest, stream proto.DaemonService_SubscribeEventsServer) error {
    +	subscription := s.statusRecorder.SubscribeToEvents()
    +	defer func() {
    +		s.statusRecorder.UnsubscribeFromEvents(subscription)
    +		log.Debug("client unsubscribed from events")
    +	}()
    +
    +	log.Debug("client subscribed to events")
    +
    +	for {
    +		select {
    +		case event := <-subscription.Events():
    +			if err := stream.Send(event); err != nil {
    +				log.Warnf("error sending event to %v: %v", req, err)
    +				return err
    +			}
    +		case <-stream.Context().Done():
    +			return nil
    +		}
    +	}
    +}
    +
    +func (s *Server) GetEvents(context.Context, *proto.GetEventsRequest) (*proto.GetEventsResponse, error) {
    +	events := s.statusRecorder.GetEventHistory()
    +	return &proto.GetEventsResponse{Events: events}, nil
    +}
    diff --git a/client/server/server.go b/client/server/server.go
    index 42420d1c1..9250b3e8b 100644
    --- a/client/server/server.go
    +++ b/client/server/server.go
    @@ -404,6 +404,11 @@ func (s *Server) Login(callerCtx context.Context, msg *proto.LoginRequest) (*pro
     		s.latestConfigInput.BlockLANAccess = msg.BlockLanAccess
     	}
     
    +	if msg.DisableNotifications != nil {
    +		inputConfig.DisableNotifications = msg.DisableNotifications
    +		s.latestConfigInput.DisableNotifications = msg.DisableNotifications
    +	}
    +
     	s.mutex.Unlock()
     
     	if msg.OptionalPreSharedKey != nil {
    @@ -687,6 +692,7 @@ func (s *Server) Status(
     
     		fullStatus := s.statusRecorder.GetFullStatus()
     		pbFullStatus := toProtoFullStatus(fullStatus)
    +		pbFullStatus.Events = s.statusRecorder.GetEventHistory()
     		statusResponse.FullStatus = pbFullStatus
     	}
     
    @@ -736,17 +742,18 @@ func (s *Server) GetConfig(_ context.Context, _ *proto.GetConfigRequest) (*proto
     	}
     
     	return &proto.GetConfigResponse{
    -		ManagementUrl:       managementURL,
    -		ConfigFile:          s.latestConfigInput.ConfigPath,
    -		LogFile:             s.logFile,
    -		PreSharedKey:        preSharedKey,
    -		AdminURL:            adminURL,
    -		InterfaceName:       s.config.WgIface,
    -		WireguardPort:       int64(s.config.WgPort),
    -		DisableAutoConnect:  s.config.DisableAutoConnect,
    -		ServerSSHAllowed:    *s.config.ServerSSHAllowed,
    -		RosenpassEnabled:    s.config.RosenpassEnabled,
    -		RosenpassPermissive: s.config.RosenpassPermissive,
    +		ManagementUrl:        managementURL,
    +		ConfigFile:           s.latestConfigInput.ConfigPath,
    +		LogFile:              s.logFile,
    +		PreSharedKey:         preSharedKey,
    +		AdminURL:             adminURL,
    +		InterfaceName:        s.config.WgIface,
    +		WireguardPort:        int64(s.config.WgPort),
    +		DisableAutoConnect:   s.config.DisableAutoConnect,
    +		ServerSSHAllowed:     *s.config.ServerSSHAllowed,
    +		RosenpassEnabled:     s.config.RosenpassEnabled,
    +		RosenpassPermissive:  s.config.RosenpassPermissive,
    +		DisableNotifications: s.config.DisableNotifications,
     	}, nil
     }
     func (s *Server) onSessionExpire() {
    diff --git a/client/ui/client_ui.go b/client/ui/client_ui.go
    index 618160128..9ed40b0be 100644
    --- a/client/ui/client_ui.go
    +++ b/client/ui/client_ui.go
    @@ -34,6 +34,7 @@ import (
     	"github.com/netbirdio/netbird/client/internal"
     	"github.com/netbirdio/netbird/client/proto"
     	"github.com/netbirdio/netbird/client/system"
    +	"github.com/netbirdio/netbird/client/ui/event"
     	"github.com/netbirdio/netbird/util"
     	"github.com/netbirdio/netbird/version"
     )
    @@ -161,6 +162,7 @@ type serviceClient struct {
     	mAllowSSH         *systray.MenuItem
     	mAutoConnect      *systray.MenuItem
     	mEnableRosenpass  *systray.MenuItem
    +	mNotifications    *systray.MenuItem
     	mAdvancedSettings *systray.MenuItem
     
     	// application with main windows.
    @@ -196,6 +198,8 @@ type serviceClient struct {
     	isUpdateIconActive   bool
     	showRoutes           bool
     	wRoutes              fyne.Window
    +
    +	eventManager *event.Manager
     }
     
     // newServiceClient instance constructor
    @@ -429,6 +433,7 @@ func (s *serviceClient) menuUpClick() error {
     		log.Errorf("up service: %v", err)
     		return err
     	}
    +
     	return nil
     }
     
    @@ -570,6 +575,7 @@ func (s *serviceClient) onTrayReady() {
     	s.mAllowSSH = s.mSettings.AddSubMenuItemCheckbox("Allow SSH", "Allow SSH connections", false)
     	s.mAutoConnect = s.mSettings.AddSubMenuItemCheckbox("Connect on Startup", "Connect automatically when the service starts", false)
     	s.mEnableRosenpass = s.mSettings.AddSubMenuItemCheckbox("Enable Quantum-Resistance", "Enable post-quantum security via Rosenpass", false)
    +	s.mNotifications = s.mSettings.AddSubMenuItemCheckbox("Notifications", "Enable notifications", true)
     	s.mAdvancedSettings = s.mSettings.AddSubMenuItem("Advanced Settings", "Advanced settings of the application")
     	s.loadSettings()
     
    @@ -606,6 +612,10 @@ func (s *serviceClient) onTrayReady() {
     		}
     	}()
     
    +	s.eventManager = event.NewManager(s.app, s.addr)
    +	s.eventManager.SetNotificationsEnabled(s.mNotifications.Checked())
    +	go s.eventManager.Start(s.ctx)
    +
     	go func() {
     		var err error
     		for {
    @@ -680,7 +690,20 @@ func (s *serviceClient) onTrayReady() {
     					defer s.mRoutes.Enable()
     					s.runSelfCommand("networks", "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)
    +				}
     			}
    +
     			if err != nil {
     				log.Errorf("process connection: %v", err)
     			}
    @@ -780,8 +803,20 @@ func (s *serviceClient) getSrvConfig() {
     		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() {
    @@ -846,6 +881,15 @@ func (s *serviceClient) loadSettings() {
     	} 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
    @@ -854,12 +898,14 @@ func (s *serviceClient) updateConfig() error {
     	disableAutoStart := !s.mAutoConnect.Checked()
     	sshAllowed := s.mAllowSSH.Checked()
     	rosenpassEnabled := s.mEnableRosenpass.Checked()
    +	notificationsDisabled := !s.mNotifications.Checked()
     
     	loginRequest := proto.LoginRequest{
     		IsLinuxDesktopClient: runtime.GOOS == "linux",
     		ServerSSHAllowed:     &sshAllowed,
     		RosenpassEnabled:     &rosenpassEnabled,
     		DisableAutoConnect:   &disableAutoStart,
    +		DisableNotifications: ¬ificationsDisabled,
     	}
     
     	if err := s.restartClient(&loginRequest); err != nil {
    diff --git a/client/ui/event/event.go b/client/ui/event/event.go
    new file mode 100644
    index 000000000..7925ee4d3
    --- /dev/null
    +++ b/client/ui/event/event.go
    @@ -0,0 +1,151 @@
    +package event
    +
    +import (
    +	"context"
    +	"fmt"
    +	"strings"
    +	"sync"
    +	"time"
    +
    +	"fyne.io/fyne/v2"
    +	"github.com/cenkalti/backoff/v4"
    +	log "github.com/sirupsen/logrus"
    +	"google.golang.org/grpc"
    +	"google.golang.org/grpc/credentials/insecure"
    +
    +	"github.com/netbirdio/netbird/client/proto"
    +	"github.com/netbirdio/netbird/client/system"
    +)
    +
    +type Manager struct {
    +	app  fyne.App
    +	addr string
    +
    +	mu      sync.Mutex
    +	ctx     context.Context
    +	cancel  context.CancelFunc
    +	enabled bool
    +}
    +
    +func NewManager(app fyne.App, addr string) *Manager {
    +	return &Manager{
    +		app:  app,
    +		addr: addr,
    +	}
    +}
    +
    +func (e *Manager) Start(ctx context.Context) {
    +	e.mu.Lock()
    +	e.ctx, e.cancel = context.WithCancel(ctx)
    +	e.mu.Unlock()
    +
    +	expBackOff := backoff.WithContext(&backoff.ExponentialBackOff{
    +		InitialInterval:     time.Second,
    +		RandomizationFactor: backoff.DefaultRandomizationFactor,
    +		Multiplier:          backoff.DefaultMultiplier,
    +		MaxInterval:         10 * time.Second,
    +		MaxElapsedTime:      0,
    +		Stop:                backoff.Stop,
    +		Clock:               backoff.SystemClock,
    +	}, ctx)
    +
    +	if err := backoff.Retry(e.streamEvents, expBackOff); err != nil {
    +		log.Errorf("event stream ended: %v", err)
    +	}
    +}
    +
    +func (e *Manager) streamEvents() error {
    +	e.mu.Lock()
    +	ctx := e.ctx
    +	e.mu.Unlock()
    +
    +	client, err := getClient(e.addr)
    +	if err != nil {
    +		return fmt.Errorf("create client: %w", err)
    +	}
    +
    +	stream, err := client.SubscribeEvents(ctx, &proto.SubscribeRequest{})
    +	if err != nil {
    +		return fmt.Errorf("failed to subscribe to events: %w", err)
    +	}
    +
    +	log.Info("subscribed to daemon events")
    +	defer func() {
    +		log.Info("unsubscribed from daemon events")
    +	}()
    +
    +	for {
    +		event, err := stream.Recv()
    +		if err != nil {
    +			return fmt.Errorf("error receiving event: %w", err)
    +		}
    +		e.handleEvent(event)
    +	}
    +}
    +
    +func (e *Manager) Stop() {
    +	e.mu.Lock()
    +	defer e.mu.Unlock()
    +	if e.cancel != nil {
    +		e.cancel()
    +	}
    +}
    +
    +func (e *Manager) SetNotificationsEnabled(enabled bool) {
    +	e.mu.Lock()
    +	defer e.mu.Unlock()
    +	e.enabled = enabled
    +}
    +
    +func (e *Manager) handleEvent(event *proto.SystemEvent) {
    +	e.mu.Lock()
    +	enabled := e.enabled
    +	e.mu.Unlock()
    +
    +	if !enabled {
    +		return
    +	}
    +
    +	title := e.getEventTitle(event)
    +	e.app.SendNotification(fyne.NewNotification(title, event.UserMessage))
    +}
    +
    +func (e *Manager) getEventTitle(event *proto.SystemEvent) string {
    +	var prefix string
    +	switch event.Severity {
    +	case proto.SystemEvent_ERROR, proto.SystemEvent_CRITICAL:
    +		prefix = "Error"
    +	case proto.SystemEvent_WARNING:
    +		prefix = "Warning"
    +	default:
    +		prefix = "Info"
    +	}
    +
    +	var category string
    +	switch event.Category {
    +	case proto.SystemEvent_DNS:
    +		category = "DNS"
    +	case proto.SystemEvent_NETWORK:
    +		category = "Network"
    +	case proto.SystemEvent_AUTHENTICATION:
    +		category = "Authentication"
    +	case proto.SystemEvent_CONNECTIVITY:
    +		category = "Connectivity"
    +	default:
    +		category = "System"
    +	}
    +
    +	return fmt.Sprintf("%s: %s", prefix, category)
    +}
    +
    +func getClient(addr string) (proto.DaemonServiceClient, error) {
    +	conn, err := grpc.NewClient(
    +		strings.TrimPrefix(addr, "tcp://"),
    +		grpc.WithTransportCredentials(insecure.NewCredentials()),
    +		grpc.WithUserAgent(system.GetDesktopUIUserAgent()),
    +	)
    +	if err != nil {
    +		return nil, err
    +	}
    +	return proto.NewDaemonServiceClient(conn), nil
    +}
    
    From 39986b0e9757997032e4dc9460ce0e63b04032c8 Mon Sep 17 00:00:00 2001
    From: hakansa <43675540+hakansa@users.noreply.github.com>
    Date: Thu, 20 Feb 2025 13:43:20 +0300
    Subject: [PATCH 74/92] [client, management] Support DNS Labels for Peer
     Addressing (#3252)
    
    * [client] Support Extra DNS Labels for Peer Addressing
    
    * [management] Support Extra DNS Labels for Peer Addressing
    
    ---------
    
    Co-authored-by: Viktor Liu <17948409+lixmal@users.noreply.github.com>
    ---
     client/cmd/login.go                           |   6 +
     client/cmd/up.go                              |  46 +-
     client/internal/config.go                     |  14 +
     client/internal/connect.go                    |   2 +-
     client/internal/dns/local.go                  |  70 +-
     client/internal/dns/local_test.go             |   2 +-
     client/internal/dns/server.go                 |  39 +-
     client/internal/dns/server_test.go            |   2 +-
     client/internal/login.go                      |   2 +-
     client/proto/daemon.pb.go                     | 928 +++++++++---------
     client/proto/daemon.proto                     |   8 +
     client/server/server.go                       |  10 +
     management/client/client.go                   |   3 +-
     management/client/client_test.go              |   2 +-
     management/client/grpc.go                     |   5 +-
     management/client/mock.go                     |   7 +-
     management/domain/validate.go                 |  65 ++
     management/domain/validate_test.go            | 206 ++++
     management/proto/management.pb.go             | 792 +++++++--------
     management/proto/management.proto             |   3 +-
     management/server/account.go                  |   2 +-
     management/server/account_test.go             |   6 +-
     management/server/grpcserver.go               |   1 +
     management/server/http/api/openapi.yml        |  16 +
     management/server/http/api/types.gen.go       |  18 +
     .../http/handlers/peers/peers_handler.go      |  10 +
     .../http/handlers/routes/routes_handler.go    |  39 +-
     .../handlers/routes/routes_handler_test.go    |  90 --
     .../handlers/setup_keys/setupkeys_handler.go  |  37 +-
     .../setup_keys/setupkeys_handler_test.go      |   5 +-
     management/server/management_proto_test.go    |   2 +-
     management/server/mock_server/account_mock.go |   5 +-
     management/server/peer.go                     |  26 +
     management/server/peer/peer.go                |   7 +
     management/server/peer_test.go                |   6 +-
     management/server/setupkey.go                 |   4 +-
     management/server/setupkey_test.go            |  24 +-
     management/server/types/account.go            |  17 +-
     management/server/types/setupkey.go           |  65 +-
     39 files changed, 1504 insertions(+), 1088 deletions(-)
     create mode 100644 management/domain/validate.go
     create mode 100644 management/domain/validate_test.go
    
    diff --git a/client/cmd/login.go b/client/cmd/login.go
    index c7dd0fda1..b91cedede 100644
    --- a/client/cmd/login.go
    +++ b/client/cmd/login.go
    @@ -85,11 +85,17 @@ var loginCmd = &cobra.Command{
     
     		client := proto.NewDaemonServiceClient(conn)
     
    +		var dnsLabelsReq []string
    +		if dnsLabelsValidated != nil {
    +			dnsLabelsReq = dnsLabelsValidated.ToSafeStringList()
    +		}
    +
     		loginRequest := proto.LoginRequest{
     			SetupKey:             providedSetupKey,
     			ManagementUrl:        managementURL,
     			IsLinuxDesktopClient: isLinuxRunningDesktop(),
     			Hostname:             hostName,
    +			DnsLabels:            dnsLabelsReq,
     		}
     
     		if rootCmd.PersistentFlags().Changed(preSharedKeyFlag) {
    diff --git a/client/cmd/up.go b/client/cmd/up.go
    index f7c2bbfe4..926317b8e 100644
    --- a/client/cmd/up.go
    +++ b/client/cmd/up.go
    @@ -20,6 +20,7 @@ import (
     	"github.com/netbirdio/netbird/client/internal/peer"
     	"github.com/netbirdio/netbird/client/proto"
     	"github.com/netbirdio/netbird/client/system"
    +	"github.com/netbirdio/netbird/management/domain"
     	"github.com/netbirdio/netbird/util"
     )
     
    @@ -29,9 +30,16 @@ const (
     	interfaceInputType
     )
     
    +const (
    +	dnsLabelsFlag = "extra-dns-labels"
    +)
    +
     var (
    -	foregroundMode bool
    -	upCmd          = &cobra.Command{
    +	foregroundMode     bool
    +	dnsLabels          []string
    +	dnsLabelsValidated domain.List
    +
    +	upCmd = &cobra.Command{
     		Use:   "up",
     		Short: "install, login and start Netbird client",
     		RunE:  upFunc,
    @@ -49,6 +57,14 @@ func init() {
     	upCmd.PersistentFlags().StringSliceVar(&extraIFaceBlackList, extraIFaceBlackListFlag, nil, "Extra list of default interfaces to ignore for listening")
     	upCmd.PersistentFlags().DurationVar(&dnsRouteInterval, dnsRouteIntervalFlag, time.Minute, "DNS route update interval")
     	upCmd.PersistentFlags().BoolVar(&blockLANAccess, blockLANAccessFlag, false, "Block access to local networks (LAN) when using this peer as a router or exit node")
    +
    +	upCmd.PersistentFlags().StringSliceVar(&dnsLabels, dnsLabelsFlag, nil,
    +		`Sets DNS labels`+
    +			`You can specify a comma-separated list of up to 32 labels. `+
    +			`An empty string "" clears the previous configuration. `+
    +			`E.g. --extra-dns-labels vpc1 or --extra-dns-labels vpc1,mgmt1 `+
    +			`or --extra-dns-labels ""`,
    +	)
     }
     
     func upFunc(cmd *cobra.Command, args []string) error {
    @@ -67,6 +83,11 @@ func upFunc(cmd *cobra.Command, args []string) error {
     		return err
     	}
     
    +	dnsLabelsValidated, err = validateDnsLabels(dnsLabels)
    +	if err != nil {
    +		return err
    +	}
    +
     	ctx := internal.CtxInitState(cmd.Context())
     
     	if hostName != "" {
    @@ -98,6 +119,7 @@ func runInForegroundMode(ctx context.Context, cmd *cobra.Command) error {
     		NATExternalIPs:      natExternalIPs,
     		CustomDNSAddress:    customDNSAddressConverted,
     		ExtraIFaceBlackList: extraIFaceBlackList,
    +		DNSLabels:           dnsLabelsValidated,
     	}
     
     	if cmd.Flag(enableRosenpassFlag).Changed {
    @@ -240,6 +262,8 @@ func runInDaemonMode(ctx context.Context, cmd *cobra.Command) error {
     		IsLinuxDesktopClient: isLinuxRunningDesktop(),
     		Hostname:             hostName,
     		ExtraIFaceBlacklist:  extraIFaceBlackList,
    +		DnsLabels:            dnsLabels,
    +		CleanDNSLabels:       dnsLabels != nil && len(dnsLabels) == 0,
     	}
     
     	if rootCmd.PersistentFlags().Changed(preSharedKeyFlag) {
    @@ -430,6 +454,24 @@ func parseCustomDNSAddress(modified bool) ([]byte, error) {
     	return parsed, nil
     }
     
    +func validateDnsLabels(labels []string) (domain.List, error) {
    +	var (
    +		domains domain.List
    +		err     error
    +	)
    +
    +	if len(labels) == 0 {
    +		return domains, nil
    +	}
    +
    +	domains, err = domain.ValidateDomains(labels)
    +	if err != nil {
    +		return nil, fmt.Errorf("failed to validate dns labels: %v", err)
    +	}
    +
    +	return domains, nil
    +}
    +
     func isValidAddrPort(input string) bool {
     	if input == "" {
     		return true
    diff --git a/client/internal/config.go b/client/internal/config.go
    index 5703539cc..b269a3854 100644
    --- a/client/internal/config.go
    +++ b/client/internal/config.go
    @@ -8,6 +8,7 @@ import (
     	"os"
     	"reflect"
     	"runtime"
    +	"slices"
     	"strings"
     	"time"
     
    @@ -20,6 +21,7 @@ import (
     	"github.com/netbirdio/netbird/client/internal/routemanager/dynamic"
     	"github.com/netbirdio/netbird/client/ssh"
     	mgm "github.com/netbirdio/netbird/management/client"
    +	"github.com/netbirdio/netbird/management/domain"
     	"github.com/netbirdio/netbird/util"
     )
     
    @@ -70,6 +72,8 @@ type ConfigInput struct {
     	BlockLANAccess *bool
     
     	DisableNotifications *bool
    +
    +	DNSLabels domain.List
     }
     
     // Config Configuration type
    @@ -97,6 +101,8 @@ type Config struct {
     
     	DisableNotifications bool
     
    +	DNSLabels domain.List
    +
     	// SSHKey is a private SSH key in a PEM format
     	SSHKey string
     
    @@ -503,6 +509,14 @@ func (config *Config) apply(input ConfigInput) (updated bool, err error) {
     		}
     	}
     
    +	if input.DNSLabels != nil && !slices.Equal(config.DNSLabels, input.DNSLabels) {
    +		log.Infof("updating DNS labels [ %s ] (old value: [ %s ])",
    +			input.DNSLabels.SafeString(),
    +			config.DNSLabels.SafeString())
    +		config.DNSLabels = input.DNSLabels
    +		updated = true
    +	}
    +
     	return updated, nil
     }
     
    diff --git a/client/internal/connect.go b/client/internal/connect.go
    index a0d585ffe..26ae3b687 100644
    --- a/client/internal/connect.go
    +++ b/client/internal/connect.go
    @@ -478,7 +478,7 @@ func loginToManagement(ctx context.Context, client mgm.Client, pubSSHKey []byte,
     		config.DisableDNS,
     		config.DisableFirewall,
     	)
    -	loginResp, err := client.Login(*serverPublicKey, sysInfo, pubSSHKey)
    +	loginResp, err := client.Login(*serverPublicKey, sysInfo, pubSSHKey, config.DNSLabels)
     	if err != nil {
     		return nil, err
     	}
    diff --git a/client/internal/dns/local.go b/client/internal/dns/local.go
    index 80113885a..3a25a23b6 100644
    --- a/client/internal/dns/local.go
    +++ b/client/internal/dns/local.go
    @@ -15,7 +15,7 @@ type registrationMap map[string]struct{}
     
     type localResolver struct {
     	registeredMap registrationMap
    -	records       sync.Map
    +	records       sync.Map // key: string (domain_class_type), value: []dns.RR
     }
     
     func (d *localResolver) MatchSubdomains() bool {
    @@ -44,11 +44,12 @@ func (d *localResolver) ServeDNS(w dns.ResponseWriter, r *dns.Msg) {
     	replyMessage := &dns.Msg{}
     	replyMessage.SetReply(r)
     	replyMessage.RecursionAvailable = true
    -	replyMessage.Rcode = dns.RcodeSuccess
     
    -	response := d.lookupRecord(r)
    -	if response != nil {
    -		replyMessage.Answer = append(replyMessage.Answer, response)
    +	// lookup all records matching the question
    +	records := d.lookupRecords(r)
    +	if len(records) > 0 {
    +		replyMessage.Rcode = dns.RcodeSuccess
    +		replyMessage.Answer = append(replyMessage.Answer, records...)
     	} else {
     		replyMessage.Rcode = dns.RcodeNameError
     	}
    @@ -59,38 +60,65 @@ func (d *localResolver) ServeDNS(w dns.ResponseWriter, r *dns.Msg) {
     	}
     }
     
    -func (d *localResolver) lookupRecord(r *dns.Msg) dns.RR {
    +// lookupRecords fetches *all* DNS records matching the first question in r.
    +func (d *localResolver) lookupRecords(r *dns.Msg) []dns.RR {
    +	if len(r.Question) == 0 {
    +		return nil
    +	}
     	question := r.Question[0]
     	question.Name = strings.ToLower(question.Name)
    -	record, found := d.records.Load(buildRecordKey(question.Name, question.Qclass, question.Qtype))
    +	key := buildRecordKey(question.Name, question.Qclass, question.Qtype)
    +
    +	value, found := d.records.Load(key)
     	if !found {
     		return nil
     	}
     
    -	return record.(dns.RR)
    -}
    -
    -func (d *localResolver) registerRecord(record nbdns.SimpleRecord) error {
    -	fullRecord, err := dns.NewRR(record.String())
    -	if err != nil {
    -		return fmt.Errorf("register record: %w", err)
    +	records, ok := value.([]dns.RR)
    +	if !ok {
    +		log.Errorf("failed to cast records to []dns.RR, records: %v", value)
    +		return nil
     	}
     
    -	fullRecord.Header().Rdlength = record.Len()
    +	// if there's more than one record, rotate them (round-robin)
    +	if len(records) > 1 {
    +		first := records[0]
    +		records = append(records[1:], first)
    +		d.records.Store(key, records)
    +	}
     
    -	header := fullRecord.Header()
    -	d.records.Store(buildRecordKey(header.Name, header.Class, header.Rrtype), fullRecord)
    -
    -	return nil
    +	return records
     }
     
    +// registerRecord stores a new record by appending it to any existing list
    +func (d *localResolver) registerRecord(record nbdns.SimpleRecord) (string, error) {
    +	rr, err := dns.NewRR(record.String())
    +	if err != nil {
    +		return "", fmt.Errorf("register record: %w", err)
    +	}
    +
    +	rr.Header().Rdlength = record.Len()
    +	header := rr.Header()
    +	key := buildRecordKey(header.Name, header.Class, header.Rrtype)
    +
    +	// load any existing slice of records, then append
    +	existing, _ := d.records.LoadOrStore(key, []dns.RR{})
    +	records := existing.([]dns.RR)
    +	records = append(records, rr)
    +
    +	// store updated slice
    +	d.records.Store(key, records)
    +	return key, nil
    +}
    +
    +// deleteRecord removes *all* records under the recordKey.
     func (d *localResolver) deleteRecord(recordKey string) {
     	d.records.Delete(dns.Fqdn(recordKey))
     }
     
    +// buildRecordKey consistently generates a key: name_class_type
     func buildRecordKey(name string, class, qType uint16) string {
    -	key := fmt.Sprintf("%s_%d_%d", name, class, qType)
    -	return key
    +	return fmt.Sprintf("%s_%d_%d", dns.Fqdn(name), class, qType)
     }
     
     func (d *localResolver) probeAvailability() {}
    diff --git a/client/internal/dns/local_test.go b/client/internal/dns/local_test.go
    index b62cd66a9..0a42b321a 100644
    --- a/client/internal/dns/local_test.go
    +++ b/client/internal/dns/local_test.go
    @@ -55,7 +55,7 @@ func TestLocalResolver_ServeDNS(t *testing.T) {
     			resolver := &localResolver{
     				registeredMap: make(registrationMap),
     			}
    -			_ = resolver.registerRecord(testCase.inputRecord)
    +			_, _ = resolver.registerRecord(testCase.inputRecord)
     			var responseMSG *dns.Msg
     			responseWriter := &mockResponseWriter{
     				WriteMsgFunc: func(m *dns.Msg) error {
    diff --git a/client/internal/dns/server.go b/client/internal/dns/server.go
    index fb94e07ac..d4d68370d 100644
    --- a/client/internal/dns/server.go
    +++ b/client/internal/dns/server.go
    @@ -393,10 +393,11 @@ func (s *DefaultServer) applyConfiguration(update nbdns.Config) error {
     		s.service.Stop()
     	}
     
    -	localMuxUpdates, localRecords, err := s.buildLocalHandlerUpdate(update.CustomZones)
    +	localMuxUpdates, localRecordsByDomain, err := s.buildLocalHandlerUpdate(update.CustomZones)
     	if err != nil {
     		return fmt.Errorf("not applying dns update, error: %v", err)
     	}
    +
     	upstreamMuxUpdates, err := s.buildUpstreamHandlerUpdate(update.NameServerGroups)
     	if err != nil {
     		return fmt.Errorf("not applying dns update, error: %v", err)
    @@ -404,7 +405,10 @@ func (s *DefaultServer) applyConfiguration(update nbdns.Config) error {
     	muxUpdates := append(localMuxUpdates, upstreamMuxUpdates...) //nolint:gocritic
     
     	s.updateMux(muxUpdates)
    -	s.updateLocalResolver(localRecords)
    +
    +	// register local records
    +	s.updateLocalResolver(localRecordsByDomain)
    +
     	s.currentConfig = dnsConfigToHostDNSConfig(update, s.service.RuntimeIP(), s.service.RuntimePort())
     
     	hostUpdate := s.currentConfig
    @@ -434,9 +438,12 @@ func (s *DefaultServer) applyConfiguration(update nbdns.Config) error {
     	return nil
     }
     
    -func (s *DefaultServer) buildLocalHandlerUpdate(customZones []nbdns.CustomZone) ([]handlerWrapper, map[string]nbdns.SimpleRecord, error) {
    +func (s *DefaultServer) buildLocalHandlerUpdate(
    +	customZones []nbdns.CustomZone,
    +) ([]handlerWrapper, map[string][]nbdns.SimpleRecord, error) {
    +
     	var muxUpdates []handlerWrapper
    -	localRecords := make(map[string]nbdns.SimpleRecord, 0)
    +	localRecords := make(map[string][]nbdns.SimpleRecord)
     
     	for _, customZone := range customZones {
     		if len(customZone.Records) == 0 {
    @@ -449,6 +456,7 @@ func (s *DefaultServer) buildLocalHandlerUpdate(customZones []nbdns.CustomZone)
     			priority: PriorityMatchDomain,
     		})
     
    +		// group all records under this domain
     		for _, record := range customZone.Records {
     			var class uint16 = dns.ClassINET
     			if record.Class != nbdns.DefaultClass {
    @@ -456,9 +464,11 @@ func (s *DefaultServer) buildLocalHandlerUpdate(customZones []nbdns.CustomZone)
     			}
     
     			key := buildRecordKey(record.Name, class, uint16(record.Type))
    -			localRecords[key] = record
    +
    +			localRecords[key] = append(localRecords[key], record)
     		}
     	}
    +
     	return muxUpdates, localRecords, nil
     }
     
    @@ -594,7 +604,8 @@ func (s *DefaultServer) updateMux(muxUpdates []handlerWrapper) {
     	s.dnsMuxMap = muxUpdateMap
     }
     
    -func (s *DefaultServer) updateLocalResolver(update map[string]nbdns.SimpleRecord) {
    +func (s *DefaultServer) updateLocalResolver(update map[string][]nbdns.SimpleRecord) {
    +	// remove old records that are no longer present
     	for key := range s.localResolver.registeredMap {
     		_, found := update[key]
     		if !found {
    @@ -603,12 +614,18 @@ func (s *DefaultServer) updateLocalResolver(update map[string]nbdns.SimpleRecord
     	}
     
     	updatedMap := make(registrationMap)
    -	for key, record := range update {
    -		err := s.localResolver.registerRecord(record)
    -		if err != nil {
    -			log.Warnf("got an error while registering the record (%s), error: %v", record.String(), err)
    +	for _, recs := range update {
    +		for _, rec := range recs {
    +			// convert the record to a dns.RR and register
    +			key, err := s.localResolver.registerRecord(rec)
    +			if err != nil {
    +				log.Warnf("got an error while registering the record (%s), error: %v",
    +					rec.String(), err)
    +				continue
    +			}
    +
    +			updatedMap[key] = struct{}{}
     		}
    -		updatedMap[key] = struct{}{}
     	}
     
     	s.localResolver.registeredMap = updatedMap
    diff --git a/client/internal/dns/server_test.go b/client/internal/dns/server_test.go
    index db49f96a2..e9ddd5f59 100644
    --- a/client/internal/dns/server_test.go
    +++ b/client/internal/dns/server_test.go
    @@ -573,7 +573,7 @@ func TestDNSServerStartStop(t *testing.T) {
     			}
     			time.Sleep(100 * time.Millisecond)
     			defer dnsServer.Stop()
    -			err = dnsServer.localResolver.registerRecord(zoneRecords[0])
    +			_, err = dnsServer.localResolver.registerRecord(zoneRecords[0])
     			if err != nil {
     				t.Error(err)
     			}
    diff --git a/client/internal/login.go b/client/internal/login.go
    index b4ab1e363..092f2309c 100644
    --- a/client/internal/login.go
    +++ b/client/internal/login.go
    @@ -117,7 +117,7 @@ func doMgmLogin(ctx context.Context, mgmClient *mgm.GrpcClient, pubSSHKey []byte
     		config.DisableDNS,
     		config.DisableFirewall,
     	)
    -	_, err = mgmClient.Login(*serverKey, sysInfo, pubSSHKey)
    +	_, err = mgmClient.Login(*serverKey, sysInfo, pubSSHKey, config.DNSLabels)
     	return serverKey, err
     }
     
    diff --git a/client/proto/daemon.pb.go b/client/proto/daemon.pb.go
    index b40f6beea..3aa57da8f 100644
    --- a/client/proto/daemon.pb.go
    +++ b/client/proto/daemon.pb.go
    @@ -232,6 +232,11 @@ type LoginRequest struct {
     	DisableFirewall      *bool                `protobuf:"varint,23,opt,name=disable_firewall,json=disableFirewall,proto3,oneof" json:"disable_firewall,omitempty"`
     	BlockLanAccess       *bool                `protobuf:"varint,24,opt,name=block_lan_access,json=blockLanAccess,proto3,oneof" json:"block_lan_access,omitempty"`
     	DisableNotifications *bool                `protobuf:"varint,25,opt,name=disable_notifications,json=disableNotifications,proto3,oneof" json:"disable_notifications,omitempty"`
    +	DnsLabels            []string             `protobuf:"bytes,26,rep,name=dns_labels,json=dnsLabels,proto3" json:"dns_labels,omitempty"`
    +	// cleanDNSLabels clean map list of DNS labels.
    +	// This is needed because the generated code
    +	// omits initialized empty slices due to omitempty tags
    +	CleanDNSLabels bool `protobuf:"varint,27,opt,name=cleanDNSLabels,proto3" json:"cleanDNSLabels,omitempty"`
     }
     
     func (x *LoginRequest) Reset() {
    @@ -442,6 +447,20 @@ func (x *LoginRequest) GetDisableNotifications() bool {
     	return false
     }
     
    +func (x *LoginRequest) GetDnsLabels() []string {
    +	if x != nil {
    +		return x.DnsLabels
    +	}
    +	return nil
    +}
    +
    +func (x *LoginRequest) GetCleanDNSLabels() bool {
    +	if x != nil {
    +		return x.CleanDNSLabels
    +	}
    +	return false
    +}
    +
     type LoginResponse struct {
     	state         protoimpl.MessageState
     	sizeCache     protoimpl.SizeCache
    @@ -3251,7 +3270,7 @@ var file_daemon_proto_rawDesc = []byte{
     	0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74,
     	0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1e, 0x67, 0x6f, 0x6f, 0x67, 0x6c,
     	0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x64, 0x75, 0x72, 0x61, 0x74,
    -	0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xe9, 0x0b, 0x0a, 0x0c, 0x4c, 0x6f,
    +	0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xb0, 0x0c, 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, 0x26, 0x0a, 0x0c, 0x70, 0x72, 0x65, 0x53, 0x68, 0x61,
    @@ -3325,465 +3344,470 @@ var file_daemon_proto_rawDesc = []byte{
     	0x73, 0x73, 0x88, 0x01, 0x01, 0x12, 0x38, 0x0a, 0x15, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65,
     	0x5f, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x19,
     	0x20, 0x01, 0x28, 0x08, 0x48, 0x0e, 0x52, 0x14, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x4e,
    -	0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x88, 0x01, 0x01, 0x42,
    -	0x13, 0x0a, 0x11, 0x5f, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61,
    -	0x62, 0x6c, 0x65, 0x64, 0x42, 0x10, 0x0a, 0x0e, 0x5f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61,
    -	0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x42, 0x10, 0x0a, 0x0e, 0x5f, 0x77, 0x69, 0x72, 0x65, 0x67,
    -	0x75, 0x61, 0x72, 0x64, 0x50, 0x6f, 0x72, 0x74, 0x42, 0x17, 0x0a, 0x15, 0x5f, 0x6f, 0x70, 0x74,
    -	0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x50, 0x72, 0x65, 0x53, 0x68, 0x61, 0x72, 0x65, 0x64, 0x4b, 0x65,
    -	0x79, 0x42, 0x15, 0x0a, 0x13, 0x5f, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x41, 0x75, 0x74,
    -	0x6f, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x42, 0x13, 0x0a, 0x11, 0x5f, 0x73, 0x65, 0x72,
    -	0x76, 0x65, 0x72, 0x53, 0x53, 0x48, 0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x42, 0x16, 0x0a,
    -	0x14, 0x5f, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, 0x65, 0x72, 0x6d, 0x69,
    -	0x73, 0x73, 0x69, 0x76, 0x65, 0x42, 0x11, 0x0a, 0x0f, 0x5f, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72,
    -	0x6b, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x42, 0x13, 0x0a, 0x11, 0x5f, 0x64, 0x6e, 0x73,
    -	0x52, 0x6f, 0x75, 0x74, 0x65, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x42, 0x18, 0x0a,
    -	0x16, 0x5f, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74,
    -	0x5f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x42, 0x18, 0x0a, 0x16, 0x5f, 0x64, 0x69, 0x73, 0x61,
    -	0x62, 0x6c, 0x65, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x5f, 0x72, 0x6f, 0x75, 0x74, 0x65,
    -	0x73, 0x42, 0x0e, 0x0a, 0x0c, 0x5f, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x64, 0x6e,
    -	0x73, 0x42, 0x13, 0x0a, 0x11, 0x5f, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x66, 0x69,
    -	0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x42, 0x13, 0x0a, 0x11, 0x5f, 0x62, 0x6c, 0x6f, 0x63, 0x6b,
    -	0x5f, 0x6c, 0x61, 0x6e, 0x5f, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x42, 0x18, 0x0a, 0x16, 0x5f,
    -	0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61,
    -	0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0xb5, 0x01, 0x0a, 0x0d, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52,
    -	0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x24, 0x0a, 0x0d, 0x6e, 0x65, 0x65, 0x64, 0x73,
    -	0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d,
    -	0x6e, 0x65, 0x65, 0x64, 0x73, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x1a, 0x0a,
    -	0x08, 0x75, 0x73, 0x65, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52,
    -	0x08, 0x75, 0x73, 0x65, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x28, 0x0a, 0x0f, 0x76, 0x65, 0x72,
    -	0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x55, 0x52, 0x49, 0x18, 0x03, 0x20, 0x01,
    -	0x28, 0x09, 0x52, 0x0f, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e,
    -	0x55, 0x52, 0x49, 0x12, 0x38, 0x0a, 0x17, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74,
    -	0x69, 0x6f, 0x6e, 0x55, 0x52, 0x49, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x18, 0x04,
    -	0x20, 0x01, 0x28, 0x09, 0x52, 0x17, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69,
    -	0x6f, 0x6e, 0x55, 0x52, 0x49, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x22, 0x4d, 0x0a,
    -	0x13, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71,
    -	0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x43, 0x6f, 0x64, 0x65,
    -	0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x43, 0x6f, 0x64, 0x65,
    -	0x12, 0x1a, 0x0a, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01,
    -	0x28, 0x09, 0x52, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x16, 0x0a, 0x14,
    -	0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 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,
    -	0x3d, 0x0a, 0x0d, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
    -	0x12, 0x2c, 0x0a, 0x11, 0x67, 0x65, 0x74, 0x46, 0x75, 0x6c, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53,
    -	0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x11, 0x67, 0x65, 0x74,
    -	0x46, 0x75, 0x6c, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x82,
    -	0x01, 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, 0x12, 0x32, 0x0a, 0x0a, 0x66, 0x75, 0x6c,
    -	0x6c, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e,
    -	0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x46, 0x75, 0x6c, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x75,
    -	0x73, 0x52, 0x0a, 0x66, 0x75, 0x6c, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x24, 0x0a,
    -	0x0d, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03,
    -	0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x56, 0x65, 0x72, 0x73,
    -	0x69, 0x6f, 0x6e, 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, 0xee, 0x03, 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, 0x12, 0x24, 0x0a, 0x0d,
    -	0x69, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x06, 0x20,
    -	0x01, 0x28, 0x09, 0x52, 0x0d, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x4e, 0x61,
    -	0x6d, 0x65, 0x12, 0x24, 0x0a, 0x0d, 0x77, 0x69, 0x72, 0x65, 0x67, 0x75, 0x61, 0x72, 0x64, 0x50,
    -	0x6f, 0x72, 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, 0x77, 0x69, 0x72, 0x65, 0x67,
    -	0x75, 0x61, 0x72, 0x64, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x2e, 0x0a, 0x12, 0x64, 0x69, 0x73, 0x61,
    -	0x62, 0x6c, 0x65, 0x41, 0x75, 0x74, 0x6f, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x18, 0x09,
    -	0x20, 0x01, 0x28, 0x08, 0x52, 0x12, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x41, 0x75, 0x74,
    -	0x6f, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x12, 0x2a, 0x0a, 0x10, 0x73, 0x65, 0x72, 0x76,
    -	0x65, 0x72, 0x53, 0x53, 0x48, 0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x18, 0x0a, 0x20, 0x01,
    -	0x28, 0x08, 0x52, 0x10, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x53, 0x53, 0x48, 0x41, 0x6c, 0x6c,
    -	0x6f, 0x77, 0x65, 0x64, 0x12, 0x2a, 0x0a, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73,
    -	0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10,
    -	0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64,
    -	0x12, 0x30, 0x0a, 0x13, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, 0x65, 0x72,
    -	0x6d, 0x69, 0x73, 0x73, 0x69, 0x76, 0x65, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x08, 0x52, 0x13, 0x72,
    -	0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69,
    -	0x76, 0x65, 0x12, 0x33, 0x0a, 0x15, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x6e, 0x6f,
    -	0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x0d, 0x20, 0x01, 0x28,
    -	0x08, 0x52, 0x14, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69,
    -	0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0xde, 0x05, 0x0a, 0x09, 0x50, 0x65, 0x65, 0x72,
    -	0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28,
    -	0x09, 0x52, 0x02, 0x49, 0x50, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18,
    -	0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x12, 0x1e, 0x0a,
    -	0x0a, 0x63, 0x6f, 0x6e, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28,
    -	0x09, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x46, 0x0a,
    -	0x10, 0x63, 0x6f, 0x6e, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x55, 0x70, 0x64, 0x61, 0x74,
    -	0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65,
    -	0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74,
    -	0x61, 0x6d, 0x70, 0x52, 0x10, 0x63, 0x6f, 0x6e, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x55,
    -	0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x65, 0x64,
    -	0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x65, 0x64, 0x12,
    -	0x34, 0x0a, 0x15, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69,
    -	0x64, 0x61, 0x74, 0x65, 0x54, 0x79, 0x70, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x15,
    -	0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74,
    -	0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x36, 0x0a, 0x16, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x49,
    +	0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x88, 0x01, 0x01, 0x12,
    +	0x1d, 0x0a, 0x0a, 0x64, 0x6e, 0x73, 0x5f, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x18, 0x1a, 0x20,
    +	0x03, 0x28, 0x09, 0x52, 0x09, 0x64, 0x6e, 0x73, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x12, 0x26,
    +	0x0a, 0x0e, 0x63, 0x6c, 0x65, 0x61, 0x6e, 0x44, 0x4e, 0x53, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73,
    +	0x18, 0x1b, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0e, 0x63, 0x6c, 0x65, 0x61, 0x6e, 0x44, 0x4e, 0x53,
    +	0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x42, 0x13, 0x0a, 0x11, 0x5f, 0x72, 0x6f, 0x73, 0x65, 0x6e,
    +	0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x42, 0x10, 0x0a, 0x0e, 0x5f,
    +	0x69, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x42, 0x10, 0x0a,
    +	0x0e, 0x5f, 0x77, 0x69, 0x72, 0x65, 0x67, 0x75, 0x61, 0x72, 0x64, 0x50, 0x6f, 0x72, 0x74, 0x42,
    +	0x17, 0x0a, 0x15, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x50, 0x72, 0x65, 0x53,
    +	0x68, 0x61, 0x72, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x42, 0x15, 0x0a, 0x13, 0x5f, 0x64, 0x69, 0x73,
    +	0x61, 0x62, 0x6c, 0x65, 0x41, 0x75, 0x74, 0x6f, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x42,
    +	0x13, 0x0a, 0x11, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x53, 0x53, 0x48, 0x41, 0x6c, 0x6c,
    +	0x6f, 0x77, 0x65, 0x64, 0x42, 0x16, 0x0a, 0x14, 0x5f, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61,
    +	0x73, 0x73, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x76, 0x65, 0x42, 0x11, 0x0a, 0x0f,
    +	0x5f, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x42,
    +	0x13, 0x0a, 0x11, 0x5f, 0x64, 0x6e, 0x73, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x49, 0x6e, 0x74, 0x65,
    +	0x72, 0x76, 0x61, 0x6c, 0x42, 0x18, 0x0a, 0x16, 0x5f, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65,
    +	0x5f, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x42, 0x18,
    +	0x0a, 0x16, 0x5f, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65,
    +	0x72, 0x5f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x42, 0x0e, 0x0a, 0x0c, 0x5f, 0x64, 0x69, 0x73,
    +	0x61, 0x62, 0x6c, 0x65, 0x5f, 0x64, 0x6e, 0x73, 0x42, 0x13, 0x0a, 0x11, 0x5f, 0x64, 0x69, 0x73,
    +	0x61, 0x62, 0x6c, 0x65, 0x5f, 0x66, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x42, 0x13, 0x0a,
    +	0x11, 0x5f, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x5f, 0x6c, 0x61, 0x6e, 0x5f, 0x61, 0x63, 0x63, 0x65,
    +	0x73, 0x73, 0x42, 0x18, 0x0a, 0x16, 0x5f, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x6e,
    +	0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0xb5, 0x01, 0x0a,
    +	0x0d, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x24,
    +	0x0a, 0x0d, 0x6e, 0x65, 0x65, 0x64, 0x73, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x18,
    +	0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x6e, 0x65, 0x65, 0x64, 0x73, 0x53, 0x53, 0x4f, 0x4c,
    +	0x6f, 0x67, 0x69, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x43, 0x6f, 0x64, 0x65,
    +	0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x43, 0x6f, 0x64, 0x65,
    +	0x12, 0x28, 0x0a, 0x0f, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e,
    +	0x55, 0x52, 0x49, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x76, 0x65, 0x72, 0x69, 0x66,
    +	0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x55, 0x52, 0x49, 0x12, 0x38, 0x0a, 0x17, 0x76, 0x65,
    +	0x72, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x55, 0x52, 0x49, 0x43, 0x6f, 0x6d,
    +	0x70, 0x6c, 0x65, 0x74, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x17, 0x76, 0x65, 0x72,
    +	0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x55, 0x52, 0x49, 0x43, 0x6f, 0x6d, 0x70,
    +	0x6c, 0x65, 0x74, 0x65, 0x22, 0x4d, 0x0a, 0x13, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c,
    +	0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x75,
    +	0x73, 0x65, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75,
    +	0x73, 0x65, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e,
    +	0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e,
    +	0x61, 0x6d, 0x65, 0x22, 0x16, 0x0a, 0x14, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 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, 0x3d, 0x0a, 0x0d, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73,
    +	0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2c, 0x0a, 0x11, 0x67, 0x65, 0x74, 0x46, 0x75,
    +	0x6c, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01,
    +	0x28, 0x08, 0x52, 0x11, 0x67, 0x65, 0x74, 0x46, 0x75, 0x6c, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53,
    +	0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x82, 0x01, 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,
    +	0x12, 0x32, 0x0a, 0x0a, 0x66, 0x75, 0x6c, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x02,
    +	0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x46, 0x75,
    +	0x6c, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x0a, 0x66, 0x75, 0x6c, 0x6c, 0x53, 0x74,
    +	0x61, 0x74, 0x75, 0x73, 0x12, 0x24, 0x0a, 0x0d, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x56, 0x65,
    +	0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x64, 0x61, 0x65,
    +	0x6d, 0x6f, 0x6e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 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, 0xee, 0x03,
    +	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, 0x12, 0x24, 0x0a, 0x0d, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65,
    +	0x4e, 0x61, 0x6d, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x69, 0x6e, 0x74, 0x65,
    +	0x72, 0x66, 0x61, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x24, 0x0a, 0x0d, 0x77, 0x69, 0x72,
    +	0x65, 0x67, 0x75, 0x61, 0x72, 0x64, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, 0x03,
    +	0x52, 0x0d, 0x77, 0x69, 0x72, 0x65, 0x67, 0x75, 0x61, 0x72, 0x64, 0x50, 0x6f, 0x72, 0x74, 0x12,
    +	0x2e, 0x0a, 0x12, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x41, 0x75, 0x74, 0x6f, 0x43, 0x6f,
    +	0x6e, 0x6e, 0x65, 0x63, 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x12, 0x64, 0x69, 0x73,
    +	0x61, 0x62, 0x6c, 0x65, 0x41, 0x75, 0x74, 0x6f, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x12,
    +	0x2a, 0x0a, 0x10, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x53, 0x53, 0x48, 0x41, 0x6c, 0x6c, 0x6f,
    +	0x77, 0x65, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x73, 0x65, 0x72, 0x76, 0x65,
    +	0x72, 0x53, 0x53, 0x48, 0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x12, 0x2a, 0x0a, 0x10, 0x72,
    +	0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18,
    +	0x0b, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73,
    +	0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x30, 0x0a, 0x13, 0x72, 0x6f, 0x73, 0x65, 0x6e,
    +	0x70, 0x61, 0x73, 0x73, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x76, 0x65, 0x18, 0x0c,
    +	0x20, 0x01, 0x28, 0x08, 0x52, 0x13, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50,
    +	0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x76, 0x65, 0x12, 0x33, 0x0a, 0x15, 0x64, 0x69, 0x73,
    +	0x61, 0x62, 0x6c, 0x65, 0x5f, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f,
    +	0x6e, 0x73, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x08, 0x52, 0x14, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c,
    +	0x65, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0xde,
    +	0x05, 0x0a, 0x09, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x0e, 0x0a, 0x02,
    +	0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x50, 0x12, 0x16, 0x0a, 0x06,
    +	0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x75,
    +	0x62, 0x4b, 0x65, 0x79, 0x12, 0x1e, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x6e, 0x53, 0x74, 0x61, 0x74,
    +	0x75, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x6e, 0x53, 0x74,
    +	0x61, 0x74, 0x75, 0x73, 0x12, 0x46, 0x0a, 0x10, 0x63, 0x6f, 0x6e, 0x6e, 0x53, 0x74, 0x61, 0x74,
    +	0x75, 0x73, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a,
    +	0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66,
    +	0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x10, 0x63, 0x6f, 0x6e, 0x6e,
    +	0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x18, 0x0a, 0x07,
    +	0x72, 0x65, 0x6c, 0x61, 0x79, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x72,
    +	0x65, 0x6c, 0x61, 0x79, 0x65, 0x64, 0x12, 0x34, 0x0a, 0x15, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x49,
     	0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x54, 0x79, 0x70, 0x65, 0x18,
    -	0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x16, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x49, 0x63, 0x65,
    -	0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a,
    -	0x04, 0x66, 0x71, 0x64, 0x6e, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x71, 0x64,
    -	0x6e, 0x12, 0x3c, 0x0a, 0x19, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e,
    -	0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x0a,
    -	0x20, 0x01, 0x28, 0x09, 0x52, 0x19, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x49, 0x63, 0x65, 0x43, 0x61,
    -	0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12,
    -	0x3e, 0x0a, 0x1a, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64,
    -	0x69, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x0b, 0x20,
    -	0x01, 0x28, 0x09, 0x52, 0x1a, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x49, 0x63, 0x65, 0x43, 0x61,
    -	0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12,
    -	0x52, 0x0a, 0x16, 0x6c, 0x61, 0x73, 0x74, 0x57, 0x69, 0x72, 0x65, 0x67, 0x75, 0x61, 0x72, 0x64,
    -	0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32,
    -	0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75,
    -	0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x16, 0x6c, 0x61, 0x73,
    -	0x74, 0x57, 0x69, 0x72, 0x65, 0x67, 0x75, 0x61, 0x72, 0x64, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68,
    -	0x61, 0x6b, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x62, 0x79, 0x74, 0x65, 0x73, 0x52, 0x78, 0x18, 0x0d,
    -	0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x62, 0x79, 0x74, 0x65, 0x73, 0x52, 0x78, 0x12, 0x18, 0x0a,
    -	0x07, 0x62, 0x79, 0x74, 0x65, 0x73, 0x54, 0x78, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07,
    -	0x62, 0x79, 0x74, 0x65, 0x73, 0x54, 0x78, 0x12, 0x2a, 0x0a, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e,
    -	0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x0f, 0x20, 0x01, 0x28,
    -	0x08, 0x52, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62,
    -	0x6c, 0x65, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x18,
    -	0x10, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x12,
    -	0x33, 0x0a, 0x07, 0x6c, 0x61, 0x74, 0x65, 0x6e, 0x63, 0x79, 0x18, 0x11, 0x20, 0x01, 0x28, 0x0b,
    -	0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62,
    -	0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x07, 0x6c, 0x61, 0x74,
    -	0x65, 0x6e, 0x63, 0x79, 0x12, 0x22, 0x0a, 0x0c, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x41, 0x64, 0x64,
    -	0x72, 0x65, 0x73, 0x73, 0x18, 0x12, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x72, 0x65, 0x6c, 0x61,
    -	0x79, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x22, 0xf0, 0x01, 0x0a, 0x0e, 0x4c, 0x6f, 0x63,
    -	0x61, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x49,
    -	0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x50, 0x12, 0x16, 0x0a, 0x06, 0x70,
    -	0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x75, 0x62,
    -	0x4b, 0x65, 0x79, 0x12, 0x28, 0x0a, 0x0f, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x49, 0x6e, 0x74,
    -	0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, 0x6b, 0x65,
    -	0x72, 0x6e, 0x65, 0x6c, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x12, 0x12, 0x0a,
    -	0x04, 0x66, 0x71, 0x64, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x71, 0x64,
    -	0x6e, 0x12, 0x2a, 0x0a, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e,
    -	0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x72, 0x6f, 0x73,
    -	0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x30, 0x0a,
    -	0x13, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73,
    -	0x73, 0x69, 0x76, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x13, 0x72, 0x6f, 0x73, 0x65,
    -	0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x76, 0x65, 0x12,
    -	0x1a, 0x0a, 0x08, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28,
    -	0x09, 0x52, 0x08, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x22, 0x53, 0x0a, 0x0b, 0x53,
    -	0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x55, 0x52,
    +	0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x15, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x49, 0x63, 0x65, 0x43,
    +	0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x36, 0x0a, 0x16,
    +	0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61,
    +	0x74, 0x65, 0x54, 0x79, 0x70, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x16, 0x72, 0x65,
    +	0x6d, 0x6f, 0x74, 0x65, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65,
    +	0x54, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x18, 0x09, 0x20, 0x01,
    +	0x28, 0x09, 0x52, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x12, 0x3c, 0x0a, 0x19, 0x6c, 0x6f, 0x63, 0x61,
    +	0x6c, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x64,
    +	0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x19, 0x6c, 0x6f, 0x63,
    +	0x61, 0x6c, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e,
    +	0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x3e, 0x0a, 0x1a, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65,
    +	0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x64, 0x70,
    +	0x6f, 0x69, 0x6e, 0x74, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x1a, 0x72, 0x65, 0x6d, 0x6f,
    +	0x74, 0x65, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e,
    +	0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x52, 0x0a, 0x16, 0x6c, 0x61, 0x73, 0x74, 0x57, 0x69,
    +	0x72, 0x65, 0x67, 0x75, 0x61, 0x72, 0x64, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65,
    +	0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e,
    +	0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61,
    +	0x6d, 0x70, 0x52, 0x16, 0x6c, 0x61, 0x73, 0x74, 0x57, 0x69, 0x72, 0x65, 0x67, 0x75, 0x61, 0x72,
    +	0x64, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x62, 0x79,
    +	0x74, 0x65, 0x73, 0x52, 0x78, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x62, 0x79, 0x74,
    +	0x65, 0x73, 0x52, 0x78, 0x12, 0x18, 0x0a, 0x07, 0x62, 0x79, 0x74, 0x65, 0x73, 0x54, 0x78, 0x18,
    +	0x0e, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x62, 0x79, 0x74, 0x65, 0x73, 0x54, 0x78, 0x12, 0x2a,
    +	0x0a, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c,
    +	0x65, 0x64, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70,
    +	0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x6e, 0x65,
    +	0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x18, 0x10, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x6e, 0x65,
    +	0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x12, 0x33, 0x0a, 0x07, 0x6c, 0x61, 0x74, 0x65, 0x6e, 0x63,
    +	0x79, 0x18, 0x11, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65,
    +	0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69,
    +	0x6f, 0x6e, 0x52, 0x07, 0x6c, 0x61, 0x74, 0x65, 0x6e, 0x63, 0x79, 0x12, 0x22, 0x0a, 0x0c, 0x72,
    +	0x65, 0x6c, 0x61, 0x79, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x12, 0x20, 0x01, 0x28,
    +	0x09, 0x52, 0x0c, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x22,
    +	0xf0, 0x01, 0x0a, 0x0e, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61,
    +	0x74, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02,
    +	0x49, 0x50, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01,
    +	0x28, 0x09, 0x52, 0x06, 0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x12, 0x28, 0x0a, 0x0f, 0x6b, 0x65,
    +	0x72, 0x6e, 0x65, 0x6c, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x18, 0x03, 0x20,
    +	0x01, 0x28, 0x08, 0x52, 0x0f, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x49, 0x6e, 0x74, 0x65, 0x72,
    +	0x66, 0x61, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x18, 0x04, 0x20, 0x01,
    +	0x28, 0x09, 0x52, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x12, 0x2a, 0x0a, 0x10, 0x72, 0x6f, 0x73, 0x65,
    +	0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01,
    +	0x28, 0x08, 0x52, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61,
    +	0x62, 0x6c, 0x65, 0x64, 0x12, 0x30, 0x0a, 0x13, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73,
    +	0x73, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x76, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28,
    +	0x08, 0x52, 0x13, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, 0x65, 0x72, 0x6d,
    +	0x69, 0x73, 0x73, 0x69, 0x76, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72,
    +	0x6b, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72,
    +	0x6b, 0x73, 0x22, 0x53, 0x0a, 0x0b, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74,
    +	0x65, 0x12, 0x10, 0x0a, 0x03, 0x55, 0x52, 0x4c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03,
    +	0x55, 0x52, 0x4c, 0x12, 0x1c, 0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65, 0x64,
    +	0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65,
    +	0x64, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09,
    +	0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, 0x57, 0x0a, 0x0f, 0x4d, 0x61, 0x6e, 0x61, 0x67,
    +	0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x55, 0x52,
     	0x4c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x55, 0x52, 0x4c, 0x12, 0x1c, 0x0a, 0x09,
     	0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52,
     	0x09, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72,
     	0x72, 0x6f, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72,
    -	0x22, 0x57, 0x0a, 0x0f, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74,
    -	0x61, 0x74, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x55, 0x52, 0x4c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
    -	0x52, 0x03, 0x55, 0x52, 0x4c, 0x12, 0x1c, 0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74,
    -	0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63,
    -	0x74, 0x65, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x03, 0x20, 0x01,
    -	0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, 0x52, 0x0a, 0x0a, 0x52, 0x65, 0x6c,
    -	0x61, 0x79, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x55, 0x52, 0x49, 0x18, 0x01,
    -	0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x55, 0x52, 0x49, 0x12, 0x1c, 0x0a, 0x09, 0x61, 0x76, 0x61,
    -	0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x61, 0x76,
    -	0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72,
    -	0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, 0x72, 0x0a,
    -	0x0c, 0x4e, 0x53, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x18, 0x0a,
    -	0x07, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07,
    -	0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69,
    -	0x6e, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e,
    -	0x73, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01,
    -	0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x65,
    -	0x72, 0x72, 0x6f, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f,
    -	0x72, 0x22, 0xff, 0x02, 0x0a, 0x0a, 0x46, 0x75, 0x6c, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73,
    -	0x12, 0x41, 0x0a, 0x0f, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74,
    -	0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x64, 0x61, 0x65, 0x6d,
    -	0x6f, 0x6e, 0x2e, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61,
    -	0x74, 0x65, 0x52, 0x0f, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74,
    -	0x61, 0x74, 0x65, 0x12, 0x35, 0x0a, 0x0b, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61,
    -	0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f,
    -	0x6e, 0x2e, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x0b, 0x73,
    -	0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x3e, 0x0a, 0x0e, 0x6c, 0x6f,
    -	0x63, 0x61, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x18, 0x03, 0x20, 0x01,
    -	0x28, 0x0b, 0x32, 0x16, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x63, 0x61,
    -	0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x0e, 0x6c, 0x6f, 0x63, 0x61,
    -	0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x27, 0x0a, 0x05, 0x70, 0x65,
    -	0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x64, 0x61, 0x65, 0x6d,
    -	0x6f, 0x6e, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x05, 0x70, 0x65,
    -	0x65, 0x72, 0x73, 0x12, 0x2a, 0x0a, 0x06, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x73, 0x18, 0x05, 0x20,
    -	0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x52, 0x65, 0x6c,
    -	0x61, 0x79, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x06, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x73, 0x12,
    -	0x35, 0x0a, 0x0b, 0x64, 0x6e, 0x73, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x18, 0x06,
    -	0x20, 0x03, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4e, 0x53,
    -	0x47, 0x72, 0x6f, 0x75, 0x70, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x0a, 0x64, 0x6e, 0x73, 0x53,
    -	0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x12, 0x2b, 0x0a, 0x06, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73,
    -	0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e,
    -	0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x06, 0x65, 0x76, 0x65,
    -	0x6e, 0x74, 0x73, 0x22, 0x15, 0x0a, 0x13, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f,
    -	0x72, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x3f, 0x0a, 0x14, 0x4c, 0x69,
    -	0x73, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
    -	0x73, 0x65, 0x12, 0x27, 0x0a, 0x06, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03,
    -	0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4e, 0x65, 0x74, 0x77,
    -	0x6f, 0x72, 0x6b, 0x52, 0x06, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x22, 0x61, 0x0a, 0x15, 0x53,
    -	0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x71,
    -	0x75, 0x65, 0x73, 0x74, 0x12, 0x1e, 0x0a, 0x0a, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x49,
    -	0x44, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72,
    -	0x6b, 0x49, 0x44, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x61, 0x70, 0x70, 0x65, 0x6e, 0x64, 0x18, 0x02,
    -	0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x61, 0x70, 0x70, 0x65, 0x6e, 0x64, 0x12, 0x10, 0x0a, 0x03,
    -	0x61, 0x6c, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x61, 0x6c, 0x6c, 0x22, 0x18,
    -	0x0a, 0x16, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73,
    -	0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1a, 0x0a, 0x06, 0x49, 0x50, 0x4c, 0x69,
    -	0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x69, 0x70, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52,
    -	0x03, 0x69, 0x70, 0x73, 0x22, 0xf9, 0x01, 0x0a, 0x07, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b,
    -	0x12, 0x0e, 0x0a, 0x02, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x44,
    -	0x12, 0x14, 0x0a, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52,
    -	0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74,
    -	0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74,
    -	0x65, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x04, 0x20,
    -	0x03, 0x28, 0x09, 0x52, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x42, 0x0a, 0x0b,
    -	0x72, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x64, 0x49, 0x50, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28,
    -	0x0b, 0x32, 0x20, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f,
    -	0x72, 0x6b, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x64, 0x49, 0x50, 0x73, 0x45, 0x6e,
    -	0x74, 0x72, 0x79, 0x52, 0x0b, 0x72, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x64, 0x49, 0x50, 0x73,
    -	0x1a, 0x4e, 0x0a, 0x10, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x64, 0x49, 0x50, 0x73, 0x45,
    -	0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28,
    -	0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x24, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18,
    -	0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x49,
    -	0x50, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01,
    -	0x22, 0x6a, 0x0a, 0x12, 0x44, 0x65, 0x62, 0x75, 0x67, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x52,
    -	0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x61, 0x6e, 0x6f, 0x6e, 0x79, 0x6d,
    -	0x69, 0x7a, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x61, 0x6e, 0x6f, 0x6e, 0x79,
    -	0x6d, 0x69, 0x7a, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x02,
    -	0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x1e, 0x0a, 0x0a,
    -	0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x49, 0x6e, 0x66, 0x6f, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08,
    -	0x52, 0x0a, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x49, 0x6e, 0x66, 0x6f, 0x22, 0x29, 0x0a, 0x13,
    -	0x44, 0x65, 0x62, 0x75, 0x67, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f,
    -	0x6e, 0x73, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28,
    -	0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x22, 0x14, 0x0a, 0x12, 0x47, 0x65, 0x74, 0x4c, 0x6f,
    -	0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x3d, 0x0a,
    -	0x13, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x73, 0x70,
    -	0x6f, 0x6e, 0x73, 0x65, 0x12, 0x26, 0x0a, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x01, 0x20,
    -	0x01, 0x28, 0x0e, 0x32, 0x10, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x67,
    -	0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x22, 0x3c, 0x0a, 0x12,
    -	0x53, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65,
    -	0x73, 0x74, 0x12, 0x26, 0x0a, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28,
    -	0x0e, 0x32, 0x10, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x67, 0x4c, 0x65,
    -	0x76, 0x65, 0x6c, 0x52, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x22, 0x15, 0x0a, 0x13, 0x53, 0x65,
    -	0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
    -	0x65, 0x22, 0x1b, 0x0a, 0x05, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61,
    -	0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x13,
    -	0x0a, 0x11, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75,
    -	0x65, 0x73, 0x74, 0x22, 0x3b, 0x0a, 0x12, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65,
    -	0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x25, 0x0a, 0x06, 0x73, 0x74, 0x61,
    -	0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x64, 0x61, 0x65, 0x6d,
    -	0x6f, 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x65, 0x73,
    -	0x22, 0x44, 0x0a, 0x11, 0x43, 0x6c, 0x65, 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65,
    -	0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x74, 0x61, 0x74, 0x65, 0x5f, 0x6e,
    -	0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x74, 0x61, 0x74, 0x65,
    -	0x4e, 0x61, 0x6d, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x61, 0x6c, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28,
    -	0x08, 0x52, 0x03, 0x61, 0x6c, 0x6c, 0x22, 0x3b, 0x0a, 0x12, 0x43, 0x6c, 0x65, 0x61, 0x6e, 0x53,
    -	0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x25, 0x0a, 0x0e,
    -	0x63, 0x6c, 0x65, 0x61, 0x6e, 0x65, 0x64, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x73, 0x18, 0x01,
    -	0x20, 0x01, 0x28, 0x05, 0x52, 0x0d, 0x63, 0x6c, 0x65, 0x61, 0x6e, 0x65, 0x64, 0x53, 0x74, 0x61,
    -	0x74, 0x65, 0x73, 0x22, 0x45, 0x0a, 0x12, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x74, 0x61,
    -	0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x74, 0x61,
    -	0x74, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73,
    -	0x74, 0x61, 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x61, 0x6c, 0x6c, 0x18,
    -	0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x61, 0x6c, 0x6c, 0x22, 0x3c, 0x0a, 0x13, 0x44, 0x65,
    -	0x6c, 0x65, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
    -	0x65, 0x12, 0x25, 0x0a, 0x0e, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x5f, 0x73, 0x74, 0x61,
    -	0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0d, 0x64, 0x65, 0x6c, 0x65, 0x74,
    -	0x65, 0x64, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x22, 0x3b, 0x0a, 0x1f, 0x53, 0x65, 0x74, 0x4e,
    -	0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x50, 0x65, 0x72, 0x73, 0x69, 0x73, 0x74,
    -	0x65, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x65,
    -	0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e,
    -	0x61, 0x62, 0x6c, 0x65, 0x64, 0x22, 0x22, 0x0a, 0x20, 0x53, 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77,
    -	0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x50, 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63,
    -	0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x76, 0x0a, 0x08, 0x54, 0x43, 0x50,
    -	0x46, 0x6c, 0x61, 0x67, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x73, 0x79, 0x6e, 0x18, 0x01, 0x20, 0x01,
    -	0x28, 0x08, 0x52, 0x03, 0x73, 0x79, 0x6e, 0x12, 0x10, 0x0a, 0x03, 0x61, 0x63, 0x6b, 0x18, 0x02,
    -	0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x61, 0x63, 0x6b, 0x12, 0x10, 0x0a, 0x03, 0x66, 0x69, 0x6e,
    -	0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x66, 0x69, 0x6e, 0x12, 0x10, 0x0a, 0x03, 0x72,
    -	0x73, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x72, 0x73, 0x74, 0x12, 0x10, 0x0a,
    -	0x03, 0x70, 0x73, 0x68, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x70, 0x73, 0x68, 0x12,
    -	0x10, 0x0a, 0x03, 0x75, 0x72, 0x67, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x75, 0x72,
    -	0x67, 0x22, 0x80, 0x03, 0x0a, 0x12, 0x54, 0x72, 0x61, 0x63, 0x65, 0x50, 0x61, 0x63, 0x6b, 0x65,
    -	0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x73, 0x6f, 0x75, 0x72,
    -	0x63, 0x65, 0x5f, 0x69, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x73, 0x6f, 0x75,
    -	0x72, 0x63, 0x65, 0x49, 0x70, 0x12, 0x25, 0x0a, 0x0e, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61,
    -	0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x70, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x64,
    -	0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x70, 0x12, 0x1a, 0x0a, 0x08,
    -	0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08,
    -	0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x6f, 0x75, 0x72,
    -	0x63, 0x65, 0x5f, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0a, 0x73,
    -	0x6f, 0x75, 0x72, 0x63, 0x65, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x29, 0x0a, 0x10, 0x64, 0x65, 0x73,
    -	0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x05, 0x20,
    -	0x01, 0x28, 0x0d, 0x52, 0x0f, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e,
    -	0x50, 0x6f, 0x72, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f,
    -	0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69,
    -	0x6f, 0x6e, 0x12, 0x32, 0x0a, 0x09, 0x74, 0x63, 0x70, 0x5f, 0x66, 0x6c, 0x61, 0x67, 0x73, 0x18,
    -	0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x54,
    -	0x43, 0x50, 0x46, 0x6c, 0x61, 0x67, 0x73, 0x48, 0x00, 0x52, 0x08, 0x74, 0x63, 0x70, 0x46, 0x6c,
    -	0x61, 0x67, 0x73, 0x88, 0x01, 0x01, 0x12, 0x20, 0x0a, 0x09, 0x69, 0x63, 0x6d, 0x70, 0x5f, 0x74,
    -	0x79, 0x70, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0d, 0x48, 0x01, 0x52, 0x08, 0x69, 0x63, 0x6d,
    -	0x70, 0x54, 0x79, 0x70, 0x65, 0x88, 0x01, 0x01, 0x12, 0x20, 0x0a, 0x09, 0x69, 0x63, 0x6d, 0x70,
    -	0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0d, 0x48, 0x02, 0x52, 0x08, 0x69,
    -	0x63, 0x6d, 0x70, 0x43, 0x6f, 0x64, 0x65, 0x88, 0x01, 0x01, 0x42, 0x0c, 0x0a, 0x0a, 0x5f, 0x74,
    -	0x63, 0x70, 0x5f, 0x66, 0x6c, 0x61, 0x67, 0x73, 0x42, 0x0c, 0x0a, 0x0a, 0x5f, 0x69, 0x63, 0x6d,
    -	0x70, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x42, 0x0c, 0x0a, 0x0a, 0x5f, 0x69, 0x63, 0x6d, 0x70, 0x5f,
    -	0x63, 0x6f, 0x64, 0x65, 0x22, 0x9f, 0x01, 0x0a, 0x0a, 0x54, 0x72, 0x61, 0x63, 0x65, 0x53, 0x74,
    -	0x61, 0x67, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28,
    -	0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61,
    -	0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67,
    -	0x65, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01,
    -	0x28, 0x08, 0x52, 0x07, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x12, 0x32, 0x0a, 0x12, 0x66,
    -	0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x5f, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c,
    -	0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x11, 0x66, 0x6f, 0x72, 0x77, 0x61,
    -	0x72, 0x64, 0x69, 0x6e, 0x67, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x88, 0x01, 0x01, 0x42,
    -	0x15, 0x0a, 0x13, 0x5f, 0x66, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x5f, 0x64,
    -	0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x22, 0x6e, 0x0a, 0x13, 0x54, 0x72, 0x61, 0x63, 0x65, 0x50,
    -	0x61, 0x63, 0x6b, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2a, 0x0a,
    -	0x06, 0x73, 0x74, 0x61, 0x67, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e,
    -	0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x65, 0x53, 0x74, 0x61, 0x67,
    -	0x65, 0x52, 0x06, 0x73, 0x74, 0x61, 0x67, 0x65, 0x73, 0x12, 0x2b, 0x0a, 0x11, 0x66, 0x69, 0x6e,
    -	0x61, 0x6c, 0x5f, 0x64, 0x69, 0x73, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02,
    -	0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x66, 0x69, 0x6e, 0x61, 0x6c, 0x44, 0x69, 0x73, 0x70, 0x6f,
    -	0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x12, 0x0a, 0x10, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72,
    -	0x69, 0x62, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x87, 0x04, 0x0a, 0x0b, 0x53,
    -	0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64,
    -	0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x38, 0x0a, 0x08, 0x73, 0x65,
    -	0x76, 0x65, 0x72, 0x69, 0x74, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1c, 0x2e, 0x64,
    -	0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65, 0x6e,
    -	0x74, 0x2e, 0x53, 0x65, 0x76, 0x65, 0x72, 0x69, 0x74, 0x79, 0x52, 0x08, 0x73, 0x65, 0x76, 0x65,
    -	0x72, 0x69, 0x74, 0x79, 0x12, 0x38, 0x0a, 0x08, 0x63, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79,
    -	0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1c, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e,
    -	0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x61, 0x74, 0x65,
    -	0x67, 0x6f, 0x72, 0x79, 0x52, 0x08, 0x63, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79, 0x12, 0x18,
    -	0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52,
    -	0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x75, 0x73, 0x65, 0x72,
    -	0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x75,
    -	0x73, 0x65, 0x72, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x38, 0x0a, 0x09, 0x74, 0x69,
    -	0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e,
    -	0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e,
    -	0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73,
    -	0x74, 0x61, 0x6d, 0x70, 0x12, 0x3d, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61,
    -	0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e,
    -	0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x2e, 0x4d, 0x65, 0x74, 0x61,
    -	0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64,
    -	0x61, 0x74, 0x61, 0x1a, 0x3b, 0x0a, 0x0d, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x45,
    -	0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28,
    -	0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18,
    -	0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01,
    -	0x22, 0x3a, 0x0a, 0x08, 0x53, 0x65, 0x76, 0x65, 0x72, 0x69, 0x74, 0x79, 0x12, 0x08, 0x0a, 0x04,
    -	0x49, 0x4e, 0x46, 0x4f, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x57, 0x41, 0x52, 0x4e, 0x49, 0x4e,
    -	0x47, 0x10, 0x01, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x02, 0x12, 0x0c,
    -	0x0a, 0x08, 0x43, 0x52, 0x49, 0x54, 0x49, 0x43, 0x41, 0x4c, 0x10, 0x03, 0x22, 0x46, 0x0a, 0x08,
    -	0x43, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79, 0x12, 0x0b, 0x0a, 0x07, 0x4e, 0x45, 0x54, 0x57,
    -	0x4f, 0x52, 0x4b, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x44, 0x4e, 0x53, 0x10, 0x01, 0x12, 0x12,
    -	0x0a, 0x0e, 0x41, 0x55, 0x54, 0x48, 0x45, 0x4e, 0x54, 0x49, 0x43, 0x41, 0x54, 0x49, 0x4f, 0x4e,
    -	0x10, 0x02, 0x12, 0x10, 0x0a, 0x0c, 0x43, 0x4f, 0x4e, 0x4e, 0x45, 0x43, 0x54, 0x49, 0x56, 0x49,
    -	0x54, 0x59, 0x10, 0x03, 0x22, 0x12, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74,
    -	0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x40, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x45,
    -	0x76, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2b, 0x0a,
    -	0x06, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e,
    +	0x22, 0x52, 0x0a, 0x0a, 0x52, 0x65, 0x6c, 0x61, 0x79, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x10,
    +	0x0a, 0x03, 0x55, 0x52, 0x49, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x55, 0x52, 0x49,
    +	0x12, 0x1c, 0x0a, 0x09, 0x61, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x02, 0x20,
    +	0x01, 0x28, 0x08, 0x52, 0x09, 0x61, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x14,
    +	0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65,
    +	0x72, 0x72, 0x6f, 0x72, 0x22, 0x72, 0x0a, 0x0c, 0x4e, 0x53, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x53,
    +	0x74, 0x61, 0x74, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x18,
    +	0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x12, 0x18,
    +	0x0a, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52,
    +	0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62,
    +	0x6c, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c,
    +	0x65, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28,
    +	0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, 0xff, 0x02, 0x0a, 0x0a, 0x46, 0x75, 0x6c,
    +	0x6c, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x41, 0x0a, 0x0f, 0x6d, 0x61, 0x6e, 0x61, 0x67,
    +	0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b,
    +	0x32, 0x17, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65,
    +	0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x0f, 0x6d, 0x61, 0x6e, 0x61, 0x67,
    +	0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x35, 0x0a, 0x0b, 0x73, 0x69,
    +	0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32,
    +	0x13, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53,
    +	0x74, 0x61, 0x74, 0x65, 0x52, 0x0b, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74,
    +	0x65, 0x12, 0x3e, 0x0a, 0x0e, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74,
    +	0x61, 0x74, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x64, 0x61, 0x65, 0x6d,
    +	0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74,
    +	0x65, 0x52, 0x0e, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74,
    +	0x65, 0x12, 0x27, 0x0a, 0x05, 0x70, 0x65, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b,
    +	0x32, 0x11, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74,
    +	0x61, 0x74, 0x65, 0x52, 0x05, 0x70, 0x65, 0x65, 0x72, 0x73, 0x12, 0x2a, 0x0a, 0x06, 0x72, 0x65,
    +	0x6c, 0x61, 0x79, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x64, 0x61, 0x65,
    +	0x6d, 0x6f, 0x6e, 0x2e, 0x52, 0x65, 0x6c, 0x61, 0x79, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x06,
    +	0x72, 0x65, 0x6c, 0x61, 0x79, 0x73, 0x12, 0x35, 0x0a, 0x0b, 0x64, 0x6e, 0x73, 0x5f, 0x73, 0x65,
    +	0x72, 0x76, 0x65, 0x72, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x64, 0x61,
    +	0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4e, 0x53, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x53, 0x74, 0x61, 0x74,
    +	0x65, 0x52, 0x0a, 0x64, 0x6e, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x12, 0x2b, 0x0a,
    +	0x06, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e,
     	0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65,
    -	0x6e, 0x74, 0x52, 0x06, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x2a, 0x62, 0x0a, 0x08, 0x4c, 0x6f,
    -	0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57,
    -	0x4e, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x50, 0x41, 0x4e, 0x49, 0x43, 0x10, 0x01, 0x12, 0x09,
    -	0x0a, 0x05, 0x46, 0x41, 0x54, 0x41, 0x4c, 0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x52, 0x52,
    -	0x4f, 0x52, 0x10, 0x03, 0x12, 0x08, 0x0a, 0x04, 0x57, 0x41, 0x52, 0x4e, 0x10, 0x04, 0x12, 0x08,
    -	0x0a, 0x04, 0x49, 0x4e, 0x46, 0x4f, 0x10, 0x05, 0x12, 0x09, 0x0a, 0x05, 0x44, 0x45, 0x42, 0x55,
    -	0x47, 0x10, 0x06, 0x12, 0x09, 0x0a, 0x05, 0x54, 0x52, 0x41, 0x43, 0x45, 0x10, 0x07, 0x32, 0xe7,
    -	0x0a, 0x0a, 0x0d, 0x44, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65,
    -	0x12, 0x36, 0x0a, 0x05, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x14, 0x2e, 0x64, 0x61, 0x65, 0x6d,
    -	0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a,
    -	0x15, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65,
    -	0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x4b, 0x0a, 0x0c, 0x57, 0x61, 0x69, 0x74,
    -	0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f,
    -	0x6e, 0x2e, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65,
    -	0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x57,
    -	0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f,
    -	0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x2d, 0x0a, 0x02, 0x55, 0x70, 0x12, 0x11, 0x2e, 0x64, 0x61,
    -	0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x55, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12,
    -	0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x55, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
    -	0x73, 0x65, 0x22, 0x00, 0x12, 0x39, 0x0a, 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x15,
    -	0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65,
    -	0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53,
    -	0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12,
    -	0x33, 0x0a, 0x04, 0x44, 0x6f, 0x77, 0x6e, 0x12, 0x13, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e,
    -	0x2e, 0x44, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x64,
    -	0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
    -	0x73, 0x65, 0x22, 0x00, 0x12, 0x42, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69,
    -	0x67, 0x12, 0x18, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f,
    -	0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x64, 0x61,
    -	0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65,
    -	0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x4b, 0x0a, 0x0c, 0x4c, 0x69, 0x73, 0x74,
    -	0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x12, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f,
    -	0x6e, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65,
    -	0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c,
    -	0x69, 0x73, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f,
    -	0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x51, 0x0a, 0x0e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e,
    -	0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x12, 0x1d, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e,
    -	0x2e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52,
    -	0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e,
    -	0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65,
    -	0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x53, 0x0a, 0x10, 0x44, 0x65, 0x73, 0x65,
    -	0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x12, 0x1d, 0x2e, 0x64,
    -	0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77,
    -	0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x64, 0x61,
    -	0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f,
    -	0x72, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a,
    -	0x0b, 0x44, 0x65, 0x62, 0x75, 0x67, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x12, 0x1a, 0x2e, 0x64,
    -	0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x62, 0x75, 0x67, 0x42, 0x75, 0x6e, 0x64, 0x6c,
    -	0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f,
    -	0x6e, 0x2e, 0x44, 0x65, 0x62, 0x75, 0x67, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x52, 0x65, 0x73,
    -	0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x0b, 0x47, 0x65, 0x74, 0x4c, 0x6f,
    -	0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e,
    -	0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65,
    -	0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x4c,
    -	0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22,
    -	0x00, 0x12, 0x48, 0x0a, 0x0b, 0x53, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c,
    -	0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x74, 0x4c, 0x6f, 0x67,
    -	0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64,
    -	0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65,
    -	0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x45, 0x0a, 0x0a, 0x4c,
    -	0x69, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x12, 0x19, 0x2e, 0x64, 0x61, 0x65, 0x6d,
    -	0x6f, 0x6e, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x52, 0x65, 0x71,
    -	0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x69,
    +	0x6e, 0x74, 0x52, 0x06, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x22, 0x15, 0x0a, 0x13, 0x4c, 0x69,
    +	0x73, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
    +	0x74, 0x22, 0x3f, 0x0a, 0x14, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b,
    +	0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x27, 0x0a, 0x06, 0x72, 0x6f, 0x75,
    +	0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x64, 0x61, 0x65, 0x6d,
    +	0x6f, 0x6e, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x52, 0x06, 0x72, 0x6f, 0x75, 0x74,
    +	0x65, 0x73, 0x22, 0x61, 0x0a, 0x15, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77,
    +	0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1e, 0x0a, 0x0a, 0x6e,
    +	0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x49, 0x44, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52,
    +	0x0a, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x49, 0x44, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x61,
    +	0x70, 0x70, 0x65, 0x6e, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x61, 0x70, 0x70,
    +	0x65, 0x6e, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x61, 0x6c, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08,
    +	0x52, 0x03, 0x61, 0x6c, 0x6c, 0x22, 0x18, 0x0a, 0x16, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e,
    +	0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22,
    +	0x1a, 0x0a, 0x06, 0x49, 0x50, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x69, 0x70, 0x73,
    +	0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x03, 0x69, 0x70, 0x73, 0x22, 0xf9, 0x01, 0x0a, 0x07,
    +	0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x44, 0x18, 0x01, 0x20,
    +	0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x44, 0x12, 0x14, 0x0a, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65,
    +	0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x1a, 0x0a,
    +	0x08, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52,
    +	0x08, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x65, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x64, 0x6f, 0x6d,
    +	0x61, 0x69, 0x6e, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x64, 0x6f, 0x6d, 0x61,
    +	0x69, 0x6e, 0x73, 0x12, 0x42, 0x0a, 0x0b, 0x72, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x64, 0x49,
    +	0x50, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f,
    +	0x6e, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76,
    +	0x65, 0x64, 0x49, 0x50, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0b, 0x72, 0x65, 0x73, 0x6f,
    +	0x6c, 0x76, 0x65, 0x64, 0x49, 0x50, 0x73, 0x1a, 0x4e, 0x0a, 0x10, 0x52, 0x65, 0x73, 0x6f, 0x6c,
    +	0x76, 0x65, 0x64, 0x49, 0x50, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b,
    +	0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x24, 0x0a,
    +	0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x64,
    +	0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x49, 0x50, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x05, 0x76, 0x61,
    +	0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x6a, 0x0a, 0x12, 0x44, 0x65, 0x62, 0x75, 0x67,
    +	0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a,
    +	0x09, 0x61, 0x6e, 0x6f, 0x6e, 0x79, 0x6d, 0x69, 0x7a, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08,
    +	0x52, 0x09, 0x61, 0x6e, 0x6f, 0x6e, 0x79, 0x6d, 0x69, 0x7a, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73,
    +	0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61,
    +	0x74, 0x75, 0x73, 0x12, 0x1e, 0x0a, 0x0a, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x49, 0x6e, 0x66,
    +	0x6f, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x49,
    +	0x6e, 0x66, 0x6f, 0x22, 0x29, 0x0a, 0x13, 0x44, 0x65, 0x62, 0x75, 0x67, 0x42, 0x75, 0x6e, 0x64,
    +	0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61,
    +	0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x22, 0x14,
    +	0x0a, 0x12, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x71,
    +	0x75, 0x65, 0x73, 0x74, 0x22, 0x3d, 0x0a, 0x13, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65,
    +	0x76, 0x65, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x26, 0x0a, 0x05, 0x6c,
    +	0x65, 0x76, 0x65, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x10, 0x2e, 0x64, 0x61, 0x65,
    +	0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x05, 0x6c, 0x65,
    +	0x76, 0x65, 0x6c, 0x22, 0x3c, 0x0a, 0x12, 0x53, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76,
    +	0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x05, 0x6c, 0x65, 0x76,
    +	0x65, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x10, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f,
    +	0x6e, 0x2e, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x05, 0x6c, 0x65, 0x76, 0x65,
    +	0x6c, 0x22, 0x15, 0x0a, 0x13, 0x53, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c,
    +	0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1b, 0x0a, 0x05, 0x53, 0x74, 0x61, 0x74,
    +	0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
    +	0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x13, 0x0a, 0x11, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, 0x61,
    +	0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x3b, 0x0a, 0x12, 0x4c, 0x69,
     	0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
    -	0x22, 0x00, 0x12, 0x45, 0x0a, 0x0a, 0x43, 0x6c, 0x65, 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65,
    -	0x12, 0x19, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x43, 0x6c, 0x65, 0x61, 0x6e, 0x53,
    -	0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x64, 0x61,
    -	0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x43, 0x6c, 0x65, 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52,
    -	0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x0b, 0x44, 0x65, 0x6c,
    -	0x65, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f,
    -	0x6e, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71,
    -	0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65,
    -	0x6c, 0x65, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
    -	0x65, 0x22, 0x00, 0x12, 0x6f, 0x0a, 0x18, 0x53, 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72,
    -	0x6b, 0x4d, 0x61, 0x70, 0x50, 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x12,
    -	0x27, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77,
    +	0x12, 0x25, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b,
    +	0x32, 0x0d, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52,
    +	0x06, 0x73, 0x74, 0x61, 0x74, 0x65, 0x73, 0x22, 0x44, 0x0a, 0x11, 0x43, 0x6c, 0x65, 0x61, 0x6e,
    +	0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a,
    +	0x73, 0x74, 0x61, 0x74, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
    +	0x52, 0x09, 0x73, 0x74, 0x61, 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x61,
    +	0x6c, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x61, 0x6c, 0x6c, 0x22, 0x3b, 0x0a,
    +	0x12, 0x43, 0x6c, 0x65, 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f,
    +	0x6e, 0x73, 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x63, 0x6c, 0x65, 0x61, 0x6e, 0x65, 0x64, 0x5f, 0x73,
    +	0x74, 0x61, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0d, 0x63, 0x6c, 0x65,
    +	0x61, 0x6e, 0x65, 0x64, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x22, 0x45, 0x0a, 0x12, 0x44, 0x65,
    +	0x6c, 0x65, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
    +	0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x74, 0x61, 0x74, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01,
    +	0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x74, 0x61, 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12,
    +	0x10, 0x0a, 0x03, 0x61, 0x6c, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x61, 0x6c,
    +	0x6c, 0x22, 0x3c, 0x0a, 0x13, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65,
    +	0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x64, 0x65, 0x6c, 0x65,
    +	0x74, 0x65, 0x64, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05,
    +	0x52, 0x0d, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x22,
    +	0x3b, 0x0a, 0x1f, 0x53, 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70,
    +	0x50, 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65,
    +	0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20,
    +	0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x22, 0x22, 0x0a, 0x20,
    +	0x53, 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x50, 0x65, 0x72,
    +	0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
    +	0x22, 0x76, 0x0a, 0x08, 0x54, 0x43, 0x50, 0x46, 0x6c, 0x61, 0x67, 0x73, 0x12, 0x10, 0x0a, 0x03,
    +	0x73, 0x79, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x73, 0x79, 0x6e, 0x12, 0x10,
    +	0x0a, 0x03, 0x61, 0x63, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x61, 0x63, 0x6b,
    +	0x12, 0x10, 0x0a, 0x03, 0x66, 0x69, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x66,
    +	0x69, 0x6e, 0x12, 0x10, 0x0a, 0x03, 0x72, 0x73, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52,
    +	0x03, 0x72, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x70, 0x73, 0x68, 0x18, 0x05, 0x20, 0x01, 0x28,
    +	0x08, 0x52, 0x03, 0x70, 0x73, 0x68, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x67, 0x18, 0x06, 0x20,
    +	0x01, 0x28, 0x08, 0x52, 0x03, 0x75, 0x72, 0x67, 0x22, 0x80, 0x03, 0x0a, 0x12, 0x54, 0x72, 0x61,
    +	0x63, 0x65, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12,
    +	0x1b, 0x0a, 0x09, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x69, 0x70, 0x18, 0x01, 0x20, 0x01,
    +	0x28, 0x09, 0x52, 0x08, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x70, 0x12, 0x25, 0x0a, 0x0e,
    +	0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x70, 0x18, 0x02,
    +	0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f,
    +	0x6e, 0x49, 0x70, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18,
    +	0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12,
    +	0x1f, 0x0a, 0x0b, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x04,
    +	0x20, 0x01, 0x28, 0x0d, 0x52, 0x0a, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x50, 0x6f, 0x72, 0x74,
    +	0x12, 0x29, 0x0a, 0x10, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f,
    +	0x70, 0x6f, 0x72, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0f, 0x64, 0x65, 0x73, 0x74,
    +	0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x64,
    +	0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09,
    +	0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x32, 0x0a, 0x09, 0x74, 0x63, 0x70,
    +	0x5f, 0x66, 0x6c, 0x61, 0x67, 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x64,
    +	0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x54, 0x43, 0x50, 0x46, 0x6c, 0x61, 0x67, 0x73, 0x48, 0x00,
    +	0x52, 0x08, 0x74, 0x63, 0x70, 0x46, 0x6c, 0x61, 0x67, 0x73, 0x88, 0x01, 0x01, 0x12, 0x20, 0x0a,
    +	0x09, 0x69, 0x63, 0x6d, 0x70, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0d,
    +	0x48, 0x01, 0x52, 0x08, 0x69, 0x63, 0x6d, 0x70, 0x54, 0x79, 0x70, 0x65, 0x88, 0x01, 0x01, 0x12,
    +	0x20, 0x0a, 0x09, 0x69, 0x63, 0x6d, 0x70, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x09, 0x20, 0x01,
    +	0x28, 0x0d, 0x48, 0x02, 0x52, 0x08, 0x69, 0x63, 0x6d, 0x70, 0x43, 0x6f, 0x64, 0x65, 0x88, 0x01,
    +	0x01, 0x42, 0x0c, 0x0a, 0x0a, 0x5f, 0x74, 0x63, 0x70, 0x5f, 0x66, 0x6c, 0x61, 0x67, 0x73, 0x42,
    +	0x0c, 0x0a, 0x0a, 0x5f, 0x69, 0x63, 0x6d, 0x70, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x42, 0x0c, 0x0a,
    +	0x0a, 0x5f, 0x69, 0x63, 0x6d, 0x70, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x22, 0x9f, 0x01, 0x0a, 0x0a,
    +	0x54, 0x72, 0x61, 0x63, 0x65, 0x53, 0x74, 0x61, 0x67, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61,
    +	0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x18,
    +	0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52,
    +	0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x6c, 0x6c, 0x6f,
    +	0x77, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x61, 0x6c, 0x6c, 0x6f, 0x77,
    +	0x65, 0x64, 0x12, 0x32, 0x0a, 0x12, 0x66, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67,
    +	0x5f, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00,
    +	0x52, 0x11, 0x66, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x44, 0x65, 0x74, 0x61,
    +	0x69, 0x6c, 0x73, 0x88, 0x01, 0x01, 0x42, 0x15, 0x0a, 0x13, 0x5f, 0x66, 0x6f, 0x72, 0x77, 0x61,
    +	0x72, 0x64, 0x69, 0x6e, 0x67, 0x5f, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x22, 0x6e, 0x0a,
    +	0x13, 0x54, 0x72, 0x61, 0x63, 0x65, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70,
    +	0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2a, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x67, 0x65, 0x73, 0x18, 0x01,
    +	0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x54, 0x72,
    +	0x61, 0x63, 0x65, 0x53, 0x74, 0x61, 0x67, 0x65, 0x52, 0x06, 0x73, 0x74, 0x61, 0x67, 0x65, 0x73,
    +	0x12, 0x2b, 0x0a, 0x11, 0x66, 0x69, 0x6e, 0x61, 0x6c, 0x5f, 0x64, 0x69, 0x73, 0x70, 0x6f, 0x73,
    +	0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x66, 0x69, 0x6e,
    +	0x61, 0x6c, 0x44, 0x69, 0x73, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x12, 0x0a,
    +	0x10, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
    +	0x74, 0x22, 0x87, 0x04, 0x0a, 0x0b, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65, 0x6e,
    +	0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69,
    +	0x64, 0x12, 0x38, 0x0a, 0x08, 0x73, 0x65, 0x76, 0x65, 0x72, 0x69, 0x74, 0x79, 0x18, 0x02, 0x20,
    +	0x01, 0x28, 0x0e, 0x32, 0x1c, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x79, 0x73,
    +	0x74, 0x65, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x65, 0x76, 0x65, 0x72, 0x69, 0x74,
    +	0x79, 0x52, 0x08, 0x73, 0x65, 0x76, 0x65, 0x72, 0x69, 0x74, 0x79, 0x12, 0x38, 0x0a, 0x08, 0x63,
    +	0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1c, 0x2e,
    +	0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65,
    +	0x6e, 0x74, 0x2e, 0x43, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79, 0x52, 0x08, 0x63, 0x61, 0x74,
    +	0x65, 0x67, 0x6f, 0x72, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65,
    +	0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12,
    +	0x20, 0x0a, 0x0b, 0x75, 0x73, 0x65, 0x72, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x05,
    +	0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x75, 0x73, 0x65, 0x72, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67,
    +	0x65, 0x12, 0x38, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x06,
    +	0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72,
    +	0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70,
    +	0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x3d, 0x0a, 0x08, 0x6d,
    +	0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x21, 0x2e,
    +	0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65,
    +	0x6e, 0x74, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79,
    +	0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x1a, 0x3b, 0x0a, 0x0d, 0x4d, 0x65,
    +	0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b,
    +	0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a,
    +	0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61,
    +	0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x3a, 0x0a, 0x08, 0x53, 0x65, 0x76, 0x65, 0x72,
    +	0x69, 0x74, 0x79, 0x12, 0x08, 0x0a, 0x04, 0x49, 0x4e, 0x46, 0x4f, 0x10, 0x00, 0x12, 0x0b, 0x0a,
    +	0x07, 0x57, 0x41, 0x52, 0x4e, 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x52,
    +	0x52, 0x4f, 0x52, 0x10, 0x02, 0x12, 0x0c, 0x0a, 0x08, 0x43, 0x52, 0x49, 0x54, 0x49, 0x43, 0x41,
    +	0x4c, 0x10, 0x03, 0x22, 0x46, 0x0a, 0x08, 0x43, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79, 0x12,
    +	0x0b, 0x0a, 0x07, 0x4e, 0x45, 0x54, 0x57, 0x4f, 0x52, 0x4b, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03,
    +	0x44, 0x4e, 0x53, 0x10, 0x01, 0x12, 0x12, 0x0a, 0x0e, 0x41, 0x55, 0x54, 0x48, 0x45, 0x4e, 0x54,
    +	0x49, 0x43, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x10, 0x02, 0x12, 0x10, 0x0a, 0x0c, 0x43, 0x4f, 0x4e,
    +	0x4e, 0x45, 0x43, 0x54, 0x49, 0x56, 0x49, 0x54, 0x59, 0x10, 0x03, 0x22, 0x12, 0x0a, 0x10, 0x47,
    +	0x65, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22,
    +	0x40, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70,
    +	0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2b, 0x0a, 0x06, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x01,
    +	0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x79,
    +	0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x06, 0x65, 0x76, 0x65, 0x6e, 0x74,
    +	0x73, 0x2a, 0x62, 0x0a, 0x08, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x0b, 0x0a,
    +	0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x50, 0x41,
    +	0x4e, 0x49, 0x43, 0x10, 0x01, 0x12, 0x09, 0x0a, 0x05, 0x46, 0x41, 0x54, 0x41, 0x4c, 0x10, 0x02,
    +	0x12, 0x09, 0x0a, 0x05, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x03, 0x12, 0x08, 0x0a, 0x04, 0x57,
    +	0x41, 0x52, 0x4e, 0x10, 0x04, 0x12, 0x08, 0x0a, 0x04, 0x49, 0x4e, 0x46, 0x4f, 0x10, 0x05, 0x12,
    +	0x09, 0x0a, 0x05, 0x44, 0x45, 0x42, 0x55, 0x47, 0x10, 0x06, 0x12, 0x09, 0x0a, 0x05, 0x54, 0x52,
    +	0x41, 0x43, 0x45, 0x10, 0x07, 0x32, 0xe7, 0x0a, 0x0a, 0x0d, 0x44, 0x61, 0x65, 0x6d, 0x6f, 0x6e,
    +	0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x36, 0x0a, 0x05, 0x4c, 0x6f, 0x67, 0x69, 0x6e,
    +	0x12, 0x14, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52,
    +	0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x15, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e,
    +	0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12,
    +	0x4b, 0x0a, 0x0c, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12,
    +	0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f,
    +	0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x64,
    +	0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67,
    +	0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x2d, 0x0a, 0x02,
    +	0x55, 0x70, 0x12, 0x11, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x55, 0x70, 0x52, 0x65,
    +	0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x55,
    +	0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x39, 0x0a, 0x06, 0x53,
    +	0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x15, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53,
    +	0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x64,
    +	0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70,
    +	0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x33, 0x0a, 0x04, 0x44, 0x6f, 0x77, 0x6e, 0x12, 0x13,
    +	0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x71, 0x75,
    +	0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x6f, 0x77,
    +	0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x42, 0x0a, 0x09, 0x47,
    +	0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x18, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f,
    +	0x6e, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65,
    +	0x73, 0x74, 0x1a, 0x19, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x43,
    +	0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12,
    +	0x4b, 0x0a, 0x0c, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x12,
    +	0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x65, 0x74,
    +	0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x64,
    +	0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72,
    +	0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x51, 0x0a, 0x0e,
    +	0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x12, 0x1d,
    +	0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65,
    +	0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e,
    +	0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74,
    +	0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12,
    +	0x53, 0x0a, 0x10, 0x44, 0x65, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f,
    +	0x72, 0x6b, 0x73, 0x12, 0x1d, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x6c,
    +	0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65,
    +	0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x6c, 0x65,
    +	0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
    +	0x73, 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x0b, 0x44, 0x65, 0x62, 0x75, 0x67, 0x42, 0x75, 0x6e,
    +	0x64, 0x6c, 0x65, 0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x62,
    +	0x75, 0x67, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a,
    +	0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x62, 0x75, 0x67, 0x42, 0x75,
    +	0x6e, 0x64, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x48,
    +	0x0a, 0x0b, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x1a, 0x2e,
    +	0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76,
    +	0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d,
    +	0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65,
    +	0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x0b, 0x53, 0x65, 0x74, 0x4c,
    +	0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e,
    +	0x2e, 0x53, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75,
    +	0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x74,
    +	0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
    +	0x22, 0x00, 0x12, 0x45, 0x0a, 0x0a, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73,
    +	0x12, 0x19, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74,
    +	0x61, 0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x64, 0x61,
    +	0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x52,
    +	0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x45, 0x0a, 0x0a, 0x43, 0x6c, 0x65,
    +	0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x19, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e,
    +	0x2e, 0x43, 0x6c, 0x65, 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65,
    +	0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x43, 0x6c, 0x65, 0x61,
    +	0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00,
    +	0x12, 0x48, 0x0a, 0x0b, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12,
    +	0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53,
    +	0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61,
    +	0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65,
    +	0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x6f, 0x0a, 0x18, 0x53, 0x65,
    +	0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x50, 0x65, 0x72, 0x73, 0x69,
    +	0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x27, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e,
    +	0x53, 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x50, 0x65, 0x72,
    +	0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a,
    +	0x28, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77,
     	0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x50, 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63,
    -	0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x28, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f,
    -	0x6e, 0x2e, 0x53, 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x50,
    -	0x65, 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
    -	0x73, 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x0b, 0x54, 0x72, 0x61, 0x63, 0x65, 0x50, 0x61, 0x63,
    -	0x6b, 0x65, 0x74, 0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x54, 0x72, 0x61,
    -	0x63, 0x65, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a,
    -	0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x65, 0x50, 0x61,
    -	0x63, 0x6b, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x44,
    -	0x0a, 0x0f, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x45, 0x76, 0x65, 0x6e, 0x74,
    -	0x73, 0x12, 0x18, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x75, 0x62, 0x73, 0x63,
    -	0x72, 0x69, 0x62, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x13, 0x2e, 0x64, 0x61,
    -	0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74,
    -	0x22, 0x00, 0x30, 0x01, 0x12, 0x42, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74,
    -	0x73, 0x12, 0x18, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x45, 0x76,
    -	0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x64, 0x61,
    -	0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 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,
    +	0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x0b, 0x54,
    +	0x72, 0x61, 0x63, 0x65, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65,
    +	0x6d, 0x6f, 0x6e, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x65, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x52,
    +	0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e,
    +	0x54, 0x72, 0x61, 0x63, 0x65, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f,
    +	0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x44, 0x0a, 0x0f, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69,
    +	0x62, 0x65, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x18, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f,
    +	0x6e, 0x2e, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65,
    +	0x73, 0x74, 0x1a, 0x13, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x79, 0x73, 0x74,
    +	0x65, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x22, 0x00, 0x30, 0x01, 0x12, 0x42, 0x0a, 0x09, 0x47,
    +	0x65, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x18, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f,
    +	0x6e, 0x2e, 0x47, 0x65, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65,
    +	0x73, 0x74, 0x1a, 0x19, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x45,
    +	0x76, 0x65, 0x6e, 0x74, 0x73, 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 (
    diff --git a/client/proto/daemon.proto b/client/proto/daemon.proto
    index 92a289c41..012b8b4db 100644
    --- a/client/proto/daemon.proto
    +++ b/client/proto/daemon.proto
    @@ -122,6 +122,14 @@ message LoginRequest {
       optional bool block_lan_access = 24;
     
       optional bool disable_notifications = 25;
    +
    +  repeated string dns_labels = 26;
    +
    +  // cleanDNSLabels clean map list of DNS labels.
    +  // This is needed because the generated code
    +  // omits initialized empty slices due to omitempty tags
    +  bool cleanDNSLabels = 27;
    +
     }
     
     message LoginResponse {
    diff --git a/client/server/server.go b/client/server/server.go
    index 9250b3e8b..e4e2c8f6f 100644
    --- a/client/server/server.go
    +++ b/client/server/server.go
    @@ -22,6 +22,7 @@ import (
     
     	"github.com/netbirdio/netbird/client/internal/auth"
     	"github.com/netbirdio/netbird/client/system"
    +	"github.com/netbirdio/netbird/management/domain"
     
     	"github.com/netbirdio/netbird/client/internal"
     	"github.com/netbirdio/netbird/client/internal/peer"
    @@ -404,6 +405,15 @@ func (s *Server) Login(callerCtx context.Context, msg *proto.LoginRequest) (*pro
     		s.latestConfigInput.BlockLANAccess = msg.BlockLanAccess
     	}
     
    +	if msg.CleanDNSLabels {
    +		inputConfig.DNSLabels = domain.List{}
    +		s.latestConfigInput.DNSLabels = nil
    +	} else if msg.DnsLabels != nil {
    +		dnsLabels := domain.FromPunycodeList(msg.DnsLabels)
    +		inputConfig.DNSLabels = dnsLabels
    +		s.latestConfigInput.DNSLabels = dnsLabels
    +	}
    +
     	if msg.DisableNotifications != nil {
     		inputConfig.DisableNotifications = msg.DisableNotifications
     		s.latestConfigInput.DisableNotifications = msg.DisableNotifications
    diff --git a/management/client/client.go b/management/client/client.go
    index e79884292..e9eeaccc1 100644
    --- a/management/client/client.go
    +++ b/management/client/client.go
    @@ -7,6 +7,7 @@ import (
     	"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
     
     	"github.com/netbirdio/netbird/client/system"
    +	"github.com/netbirdio/netbird/management/domain"
     	"github.com/netbirdio/netbird/management/proto"
     )
     
    @@ -15,7 +16,7 @@ type Client interface {
     	Sync(ctx context.Context, sysInfo *system.Info, msgHandler func(msg *proto.SyncResponse) error) error
     	GetServerPublicKey() (*wgtypes.Key, error)
     	Register(serverKey wgtypes.Key, setupKey string, jwtToken string, sysInfo *system.Info, sshKey []byte) (*proto.LoginResponse, error)
    -	Login(serverKey wgtypes.Key, sysInfo *system.Info, sshKey []byte) (*proto.LoginResponse, error)
    +	Login(serverKey wgtypes.Key, sysInfo *system.Info, sshKey []byte, dnsLabels domain.List) (*proto.LoginResponse, error)
     	GetDeviceAuthorizationFlow(serverKey wgtypes.Key) (*proto.DeviceAuthorizationFlow, error)
     	GetPKCEAuthorizationFlow(serverKey wgtypes.Key) (*proto.PKCEAuthorizationFlow, error)
     	GetNetworkMap(sysInfo *system.Info) (*proto.NetworkMap, error)
    diff --git a/management/client/client_test.go b/management/client/client_test.go
    index b4ee58298..6ef5df163 100644
    --- a/management/client/client_test.go
    +++ b/management/client/client_test.go
    @@ -177,7 +177,7 @@ func TestClient_LoginUnregistered_ShouldThrow_401(t *testing.T) {
     		t.Fatal(err)
     	}
     	sysInfo := system.GetInfo(context.TODO())
    -	_, err = client.Login(*key, sysInfo, nil)
    +	_, err = client.Login(*key, sysInfo, nil, nil)
     	if err == nil {
     		t.Error("expecting err on unregistered login, got nil")
     	}
    diff --git a/management/client/grpc.go b/management/client/grpc.go
    index 53f66da18..d02509c27 100644
    --- a/management/client/grpc.go
    +++ b/management/client/grpc.go
    @@ -19,6 +19,7 @@ import (
     
     	"github.com/netbirdio/netbird/client/system"
     	"github.com/netbirdio/netbird/encryption"
    +	"github.com/netbirdio/netbird/management/domain"
     	"github.com/netbirdio/netbird/management/proto"
     	nbgrpc "github.com/netbirdio/netbird/util/grpc"
     )
    @@ -373,12 +374,12 @@ func (c *GrpcClient) Register(serverKey wgtypes.Key, setupKey string, jwtToken s
     }
     
     // Login attempts login to Management Server. Takes care of encrypting and decrypting messages.
    -func (c *GrpcClient) Login(serverKey wgtypes.Key, sysInfo *system.Info, pubSSHKey []byte) (*proto.LoginResponse, error) {
    +func (c *GrpcClient) Login(serverKey wgtypes.Key, sysInfo *system.Info, pubSSHKey []byte, dnsLabels domain.List) (*proto.LoginResponse, error) {
     	keys := &proto.PeerKeys{
     		SshPubKey: pubSSHKey,
     		WgPubKey:  []byte(c.key.PublicKey().String()),
     	}
    -	return c.login(serverKey, &proto.LoginRequest{Meta: infoToMetaData(sysInfo), PeerKeys: keys})
    +	return c.login(serverKey, &proto.LoginRequest{Meta: infoToMetaData(sysInfo), PeerKeys: keys, DnsLabels: dnsLabels.ToPunycodeList()})
     }
     
     // GetDeviceAuthorizationFlow returns a device authorization flow information.
    diff --git a/management/client/mock.go b/management/client/mock.go
    index 73a7ac38f..11564093a 100644
    --- a/management/client/mock.go
    +++ b/management/client/mock.go
    @@ -6,6 +6,7 @@ import (
     	"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
     
     	"github.com/netbirdio/netbird/client/system"
    +	"github.com/netbirdio/netbird/management/domain"
     	"github.com/netbirdio/netbird/management/proto"
     )
     
    @@ -14,7 +15,7 @@ type MockClient struct {
     	SyncFunc                       func(ctx context.Context, sysInfo *system.Info, msgHandler func(msg *proto.SyncResponse) error) error
     	GetServerPublicKeyFunc         func() (*wgtypes.Key, error)
     	RegisterFunc                   func(serverKey wgtypes.Key, setupKey string, jwtToken string, info *system.Info, sshKey []byte) (*proto.LoginResponse, error)
    -	LoginFunc                      func(serverKey wgtypes.Key, info *system.Info, sshKey []byte) (*proto.LoginResponse, error)
    +	LoginFunc                      func(serverKey wgtypes.Key, info *system.Info, sshKey []byte, dnsLabels domain.List) (*proto.LoginResponse, error)
     	GetDeviceAuthorizationFlowFunc func(serverKey wgtypes.Key) (*proto.DeviceAuthorizationFlow, error)
     	GetPKCEAuthorizationFlowFunc   func(serverKey wgtypes.Key) (*proto.PKCEAuthorizationFlow, error)
     	SyncMetaFunc                   func(sysInfo *system.Info) error
    @@ -52,11 +53,11 @@ func (m *MockClient) Register(serverKey wgtypes.Key, setupKey string, jwtToken s
     	return m.RegisterFunc(serverKey, setupKey, jwtToken, info, sshKey)
     }
     
    -func (m *MockClient) Login(serverKey wgtypes.Key, info *system.Info, sshKey []byte) (*proto.LoginResponse, error) {
    +func (m *MockClient) Login(serverKey wgtypes.Key, info *system.Info, sshKey []byte, dnsLabels domain.List) (*proto.LoginResponse, error) {
     	if m.LoginFunc == nil {
     		return nil, nil
     	}
    -	return m.LoginFunc(serverKey, info, sshKey)
    +	return m.LoginFunc(serverKey, info, sshKey, dnsLabels)
     }
     
     func (m *MockClient) GetDeviceAuthorizationFlow(serverKey wgtypes.Key) (*proto.DeviceAuthorizationFlow, error) {
    diff --git a/management/domain/validate.go b/management/domain/validate.go
    new file mode 100644
    index 000000000..bcbf26e05
    --- /dev/null
    +++ b/management/domain/validate.go
    @@ -0,0 +1,65 @@
    +package domain
    +
    +import (
    +	"fmt"
    +	"regexp"
    +	"strings"
    +)
    +
    +const maxDomains = 32
    +
    +// ValidateDomains checks if each domain in the list is valid and returns a punycode-encoded DomainList.
    +func ValidateDomains(domains []string) (List, error) {
    +	if len(domains) == 0 {
    +		return nil, fmt.Errorf("domains list is empty")
    +	}
    +	if len(domains) > maxDomains {
    +		return nil, fmt.Errorf("domains list exceeds maximum allowed domains: %d", maxDomains)
    +	}
    +
    +	domainRegex := regexp.MustCompile(`^(?:\*\.)?(?:(?:xn--)?[a-zA-Z0-9_](?:[a-zA-Z0-9-_]{0,61}[a-zA-Z0-9])?\.)*(?:xn--)?[a-zA-Z0-9](?:[a-zA-Z0-9-_]{0,61}[a-zA-Z0-9])?$`)
    +
    +	var domainList List
    +
    +	for _, d := range domains {
    +		d := strings.ToLower(d)
    +
    +		// handles length and idna conversion
    +		punycode, err := FromString(d)
    +		if err != nil {
    +			return domainList, fmt.Errorf("convert domain to punycode: %s: %w", d, err)
    +		}
    +
    +		if !domainRegex.MatchString(string(punycode)) {
    +			return domainList, fmt.Errorf("invalid domain format: %s", d)
    +		}
    +
    +		domainList = append(domainList, punycode)
    +	}
    +	return domainList, nil
    +}
    +
    +// ValidateDomainsStrSlice checks if each domain in the list is valid
    +func ValidateDomainsStrSlice(domains []string) ([]string, error) {
    +	if len(domains) == 0 {
    +		return nil, nil
    +	}
    +	if len(domains) > maxDomains {
    +		return nil, fmt.Errorf("domains list exceeds maximum allowed domains: %d", maxDomains)
    +	}
    +
    +	domainRegex := regexp.MustCompile(`^(?:\*\.)?(?:(?:xn--)?[a-zA-Z0-9_](?:[a-zA-Z0-9-_]{0,61}[a-zA-Z0-9])?\.)*(?:xn--)?[a-zA-Z0-9](?:[a-zA-Z0-9-_]{0,61}[a-zA-Z0-9])?$`)
    +
    +	var domainList []string
    +
    +	for _, d := range domains {
    +		d := strings.ToLower(d)
    +
    +		if !domainRegex.MatchString(d) {
    +			return domainList, fmt.Errorf("invalid domain format: %s", d)
    +		}
    +
    +		domainList = append(domainList, d)
    +	}
    +	return domainList, nil
    +}
    diff --git a/management/domain/validate_test.go b/management/domain/validate_test.go
    new file mode 100644
    index 000000000..c9c042d9d
    --- /dev/null
    +++ b/management/domain/validate_test.go
    @@ -0,0 +1,206 @@
    +package domain
    +
    +import (
    +	"fmt"
    +	"testing"
    +
    +	"github.com/stretchr/testify/assert"
    +)
    +
    +func TestValidateDomains(t *testing.T) {
    +	tests := []struct {
    +		name     string
    +		domains  []string
    +		expected List
    +		wantErr  bool
    +	}{
    +		{
    +			name:     "Empty list",
    +			domains:  nil,
    +			expected: nil,
    +			wantErr:  true,
    +		},
    +		{
    +			name:     "Valid ASCII domain",
    +			domains:  []string{"sub.ex-ample.com"},
    +			expected: List{"sub.ex-ample.com"},
    +			wantErr:  false,
    +		},
    +		{
    +			name:     "Valid Unicode domain",
    +			domains:  []string{"münchen.de"},
    +			expected: List{"xn--mnchen-3ya.de"},
    +			wantErr:  false,
    +		},
    +		{
    +			name:     "Valid Unicode, all labels",
    +			domains:  []string{"中国.中国.中国"},
    +			expected: List{"xn--fiqs8s.xn--fiqs8s.xn--fiqs8s"},
    +			wantErr:  false,
    +		},
    +		{
    +			name:     "With underscores",
    +			domains:  []string{"_jabber._tcp.gmail.com"},
    +			expected: List{"_jabber._tcp.gmail.com"},
    +			wantErr:  false,
    +		},
    +		{
    +			name:     "Invalid domain format",
    +			domains:  []string{"-example.com"},
    +			expected: nil,
    +			wantErr:  true,
    +		},
    +		{
    +			name:     "Invalid domain format 2",
    +			domains:  []string{"example.com-"},
    +			expected: nil,
    +			wantErr:  true,
    +		},
    +		{
    +			name:     "Multiple domains valid and invalid",
    +			domains:  []string{"google.com", "invalid,nbdomain.com", "münchen.de"},
    +			expected: List{"google.com"},
    +			wantErr:  true,
    +		},
    +		{
    +			name:     "Valid wildcard domain",
    +			domains:  []string{"*.example.com"},
    +			expected: List{"*.example.com"},
    +			wantErr:  false,
    +		},
    +		{
    +			name:     "Wildcard with dot domain",
    +			domains:  []string{".*.example.com"},
    +			expected: nil,
    +			wantErr:  true,
    +		},
    +		{
    +			name:     "Wildcard with dot domain",
    +			domains:  []string{".*.example.com"},
    +			expected: nil,
    +			wantErr:  true,
    +		},
    +		{
    +			name:     "Invalid wildcard domain",
    +			domains:  []string{"a.*.example.com"},
    +			expected: nil,
    +			wantErr:  true,
    +		},
    +	}
    +
    +	for _, tt := range tests {
    +		t.Run(tt.name, func(t *testing.T) {
    +			got, err := ValidateDomains(tt.domains)
    +			assert.Equal(t, tt.wantErr, err != nil)
    +			assert.Equal(t, got, tt.expected)
    +		})
    +	}
    +}
    +
    +// TestValidateDomainsStrSlice tests the ValidateDomainsStrSlice function.
    +func TestValidateDomainsStrSlice(t *testing.T) {
    +	// Generate a slice of valid domains up to maxDomains
    +	validDomains := make([]string, maxDomains)
    +	for i := 0; i < maxDomains; i++ {
    +		validDomains[i] = fmt.Sprintf("example%d.com", i)
    +	}
    +
    +	tests := []struct {
    +		name     string
    +		domains  []string
    +		expected []string
    +		wantErr  bool
    +	}{
    +		{
    +			name:     "Empty list",
    +			domains:  nil,
    +			expected: nil,
    +			wantErr:  false,
    +		},
    +		{
    +			name:     "Single valid ASCII domain",
    +			domains:  []string{"sub.ex-ample.com"},
    +			expected: []string{"sub.ex-ample.com"},
    +			wantErr:  false,
    +		},
    +		{
    +			name:     "Underscores in labels",
    +			domains:  []string{"_jabber._tcp.gmail.com"},
    +			expected: []string{"_jabber._tcp.gmail.com"},
    +			wantErr:  false,
    +		},
    +		{
    +			// Unlike ValidateDomains (which converts to punycode),
    +			// ValidateDomainsStrSlice will fail on non-ASCII domain chars.
    +			name:     "Unicode domain fails (no punycode conversion)",
    +			domains:  []string{"münchen.de"},
    +			expected: nil,
    +			wantErr:  true,
    +		},
    +		{
    +			name:     "Invalid domain format - leading dash",
    +			domains:  []string{"-example.com"},
    +			expected: nil,
    +			wantErr:  true,
    +		},
    +		{
    +			name:     "Invalid domain format - trailing dash",
    +			domains:  []string{"example-.com"},
    +			expected: nil,
    +			wantErr:  true,
    +		},
    +		{
    +			// The function stops on the first invalid domain and returns an error,
    +			// so only the first domain is definitely valid, but the second is invalid.
    +			name:     "Multiple domains with a valid one, then invalid",
    +			domains:  []string{"google.com", "invalid_domain.com-"},
    +			expected: []string{"google.com"},
    +			wantErr:  true,
    +		},
    +		{
    +			name:     "Valid wildcard domain",
    +			domains:  []string{"*.example.com"},
    +			expected: []string{"*.example.com"},
    +			wantErr:  false,
    +		},
    +		{
    +			name:     "Wildcard with leading dot - invalid",
    +			domains:  []string{".*.example.com"},
    +			expected: nil,
    +			wantErr:  true,
    +		},
    +		{
    +			name:     "Invalid wildcard with multiple asterisks",
    +			domains:  []string{"a.*.example.com"},
    +			expected: nil,
    +			wantErr:  true,
    +		},
    +		{
    +			name:     "Exactly maxDomains items (valid)",
    +			domains:  validDomains,
    +			expected: validDomains,
    +			wantErr:  false,
    +		},
    +		{
    +			name:     "Exceeds maxDomains items",
    +			domains:  append(validDomains, "extra.com"),
    +			expected: nil,
    +			wantErr:  true,
    +		},
    +	}
    +
    +	for _, tt := range tests {
    +		t.Run(tt.name, func(t *testing.T) {
    +			got, err := ValidateDomainsStrSlice(tt.domains)
    +			// Check if we got an error where expected
    +			if tt.wantErr {
    +				assert.Error(t, err)
    +			} else {
    +				assert.NoError(t, err)
    +			}
    +
    +			// Compare the returned domains to what we expect
    +			assert.Equal(t, tt.expected, got)
    +		})
    +	}
    +}
    diff --git a/management/proto/management.pb.go b/management/proto/management.pb.go
    index a654a6365..2cd00783e 100644
    --- a/management/proto/management.pb.go
    +++ b/management/proto/management.pb.go
    @@ -537,7 +537,8 @@ type LoginRequest struct {
     	// SSO token (can be empty)
     	JwtToken string `protobuf:"bytes,3,opt,name=jwtToken,proto3" json:"jwtToken,omitempty"`
     	// Can be absent for now.
    -	PeerKeys *PeerKeys `protobuf:"bytes,4,opt,name=peerKeys,proto3" json:"peerKeys,omitempty"`
    +	PeerKeys  *PeerKeys `protobuf:"bytes,4,opt,name=peerKeys,proto3" json:"peerKeys,omitempty"`
    +	DnsLabels []string  `protobuf:"bytes,5,rep,name=dnsLabels,proto3" json:"dnsLabels,omitempty"`
     }
     
     func (x *LoginRequest) Reset() {
    @@ -600,6 +601,13 @@ func (x *LoginRequest) GetPeerKeys() *PeerKeys {
     	return nil
     }
     
    +func (x *LoginRequest) GetDnsLabels() []string {
    +	if x != nil {
    +		return x.DnsLabels
    +	}
    +	return nil
    +}
    +
     // PeerKeys is additional peer info like SSH pub key and WireGuard public key.
     // This message is sent on Login or register requests, or when a key rotation has to happen.
     type PeerKeys struct {
    @@ -3093,7 +3101,7 @@ var file_management_proto_rawDesc = []byte{
     	0x6e, 0x63, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2e, 0x0a,
     	0x04, 0x6d, 0x65, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61,
     	0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x53, 0x79, 0x73,
    -	0x74, 0x65, 0x6d, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x22, 0xa8, 0x01,
    +	0x74, 0x65, 0x6d, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x22, 0xc6, 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, 0x2e, 0x0a, 0x04, 0x6d, 0x65,
    @@ -3104,402 +3112,404 @@ var file_management_proto_rawDesc = []byte{
     	0x74, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x30, 0x0a, 0x08, 0x70, 0x65, 0x65, 0x72, 0x4b, 0x65,
     	0x79, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67,
     	0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x73, 0x52, 0x08,
    -	0x70, 0x65, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x73, 0x22, 0x44, 0x0a, 0x08, 0x50, 0x65, 0x65, 0x72,
    -	0x4b, 0x65, 0x79, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x73, 0x68, 0x50, 0x75, 0x62, 0x4b, 0x65,
    -	0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x73, 0x73, 0x68, 0x50, 0x75, 0x62, 0x4b,
    -	0x65, 0x79, 0x12, 0x1a, 0x0a, 0x08, 0x77, 0x67, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, 0x02,
    -	0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x77, 0x67, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x22, 0x3f,
    -	0x0a, 0x0b, 0x45, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x14, 0x0a,
    -	0x05, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x63, 0x6c,
    -	0x6f, 0x75, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x18,
    -	0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x22,
    -	0x5c, 0x0a, 0x04, 0x46, 0x69, 0x6c, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18,
    -	0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x12, 0x14, 0x0a, 0x05, 0x65,
    -	0x78, 0x69, 0x73, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x05, 0x65, 0x78, 0x69, 0x73,
    -	0x74, 0x12, 0x2a, 0x0a, 0x10, 0x70, 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x49, 0x73, 0x52, 0x75,
    -	0x6e, 0x6e, 0x69, 0x6e, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x70, 0x72, 0x6f,
    -	0x63, 0x65, 0x73, 0x73, 0x49, 0x73, 0x52, 0x75, 0x6e, 0x6e, 0x69, 0x6e, 0x67, 0x22, 0xbf, 0x02,
    -	0x0a, 0x05, 0x46, 0x6c, 0x61, 0x67, 0x73, 0x12, 0x2a, 0x0a, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e,
    -	0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28,
    -	0x08, 0x52, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62,
    -	0x6c, 0x65, 0x64, 0x12, 0x30, 0x0a, 0x13, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73,
    -	0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x76, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08,
    -	0x52, 0x13, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, 0x65, 0x72, 0x6d, 0x69,
    -	0x73, 0x73, 0x69, 0x76, 0x65, 0x12, 0x2a, 0x0a, 0x10, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x53,
    -	0x53, 0x48, 0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52,
    -	0x10, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x53, 0x53, 0x48, 0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x65,
    -	0x64, 0x12, 0x30, 0x0a, 0x13, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x43, 0x6c, 0x69, 0x65,
    -	0x6e, 0x74, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x13,
    -	0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, 0x6f, 0x75,
    -	0x74, 0x65, 0x73, 0x12, 0x30, 0x0a, 0x13, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x53, 0x65,
    -	0x72, 0x76, 0x65, 0x72, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08,
    -	0x52, 0x13, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52,
    -	0x6f, 0x75, 0x74, 0x65, 0x73, 0x12, 0x1e, 0x0a, 0x0a, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65,
    -	0x44, 0x4e, 0x53, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x64, 0x69, 0x73, 0x61, 0x62,
    -	0x6c, 0x65, 0x44, 0x4e, 0x53, 0x12, 0x28, 0x0a, 0x0f, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65,
    -	0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x18, 0x07, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f,
    -	0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x22,
    -	0xf2, 0x04, 0x0a, 0x0e, 0x50, 0x65, 0x65, 0x72, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x4d, 0x65,
    -	0x74, 0x61, 0x12, 0x1a, 0x0a, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01,
    -	0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12,
    -	0x0a, 0x04, 0x67, 0x6f, 0x4f, 0x53, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x67, 0x6f,
    -	0x4f, 0x53, 0x12, 0x16, 0x0a, 0x06, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x18, 0x03, 0x20, 0x01,
    -	0x28, 0x09, 0x52, 0x06, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f,
    -	0x72, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x63, 0x6f, 0x72, 0x65, 0x12, 0x1a,
    -	0x0a, 0x08, 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09,
    -	0x52, 0x08, 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x12, 0x0e, 0x0a, 0x02, 0x4f, 0x53,
    -	0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x4f, 0x53, 0x12, 0x26, 0x0a, 0x0e, 0x6e, 0x65,
    -	0x74, 0x62, 0x69, 0x72, 0x64, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x07, 0x20, 0x01,
    -	0x28, 0x09, 0x52, 0x0e, 0x6e, 0x65, 0x74, 0x62, 0x69, 0x72, 0x64, 0x56, 0x65, 0x72, 0x73, 0x69,
    -	0x6f, 0x6e, 0x12, 0x1c, 0x0a, 0x09, 0x75, 0x69, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18,
    -	0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x75, 0x69, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e,
    -	0x12, 0x24, 0x0a, 0x0d, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f,
    -	0x6e, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x56,
    -	0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1c, 0x0a, 0x09, 0x4f, 0x53, 0x56, 0x65, 0x72, 0x73,
    -	0x69, 0x6f, 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x4f, 0x53, 0x56, 0x65, 0x72,
    -	0x73, 0x69, 0x6f, 0x6e, 0x12, 0x46, 0x0a, 0x10, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x41,
    -	0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x18, 0x0b, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a,
    -	0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x4e, 0x65, 0x74, 0x77,
    -	0x6f, 0x72, 0x6b, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x52, 0x10, 0x6e, 0x65, 0x74, 0x77,
    -	0x6f, 0x72, 0x6b, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x12, 0x28, 0x0a, 0x0f,
    -	0x73, 0x79, 0x73, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x18,
    -	0x0c, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x73, 0x79, 0x73, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c,
    -	0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x12, 0x26, 0x0a, 0x0e, 0x73, 0x79, 0x73, 0x50, 0x72, 0x6f,
    -	0x64, 0x75, 0x63, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e,
    -	0x73, 0x79, 0x73, 0x50, 0x72, 0x6f, 0x64, 0x75, 0x63, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x28,
    -	0x0a, 0x0f, 0x73, 0x79, 0x73, 0x4d, 0x61, 0x6e, 0x75, 0x66, 0x61, 0x63, 0x74, 0x75, 0x72, 0x65,
    -	0x72, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x73, 0x79, 0x73, 0x4d, 0x61, 0x6e, 0x75,
    -	0x66, 0x61, 0x63, 0x74, 0x75, 0x72, 0x65, 0x72, 0x12, 0x39, 0x0a, 0x0b, 0x65, 0x6e, 0x76, 0x69,
    -	0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e,
    -	0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x76, 0x69, 0x72,
    -	0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x0b, 0x65, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d,
    -	0x65, 0x6e, 0x74, 0x12, 0x26, 0x0a, 0x05, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x18, 0x10, 0x20, 0x03,
    -	0x28, 0x0b, 0x32, 0x10, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e,
    -	0x46, 0x69, 0x6c, 0x65, 0x52, 0x05, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x12, 0x27, 0x0a, 0x05, 0x66,
    -	0x6c, 0x61, 0x67, 0x73, 0x18, 0x11, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x6d, 0x61, 0x6e,
    -	0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x46, 0x6c, 0x61, 0x67, 0x73, 0x52, 0x05, 0x66,
    -	0x6c, 0x61, 0x67, 0x73, 0x22, 0xb4, 0x01, 0x0a, 0x0d, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65,
    -	0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3f, 0x0a, 0x0d, 0x6e, 0x65, 0x74, 0x62, 0x69, 0x72,
    -	0x64, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e,
    -	0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x4e, 0x65, 0x74, 0x62, 0x69,
    -	0x72, 0x64, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0d, 0x6e, 0x65, 0x74, 0x62, 0x69, 0x72,
    -	0x64, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x36, 0x0a, 0x0a, 0x70, 0x65, 0x65, 0x72, 0x43,
    -	0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61,
    -	0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e,
    -	0x66, 0x69, 0x67, 0x52, 0x0a, 0x70, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12,
    -	0x2a, 0x0a, 0x06, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32,
    -	0x12, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x68, 0x65,
    -	0x63, 0x6b, 0x73, 0x52, 0x06, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x22, 0x79, 0x0a, 0x11, 0x53,
    -	0x65, 0x72, 0x76, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
    -	0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b,
    -	0x65, 0x79, 0x12, 0x38, 0x0a, 0x09, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x41, 0x74, 0x18,
    -	0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70,
    -	0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d,
    -	0x70, 0x52, 0x09, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x41, 0x74, 0x12, 0x18, 0x0a, 0x07,
    -	0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x07, 0x76,
    -	0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x07, 0x0a, 0x05, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22,
    -	0xd3, 0x01, 0x0a, 0x0d, 0x4e, 0x65, 0x74, 0x62, 0x69, 0x72, 0x64, 0x43, 0x6f, 0x6e, 0x66, 0x69,
    -	0x67, 0x12, 0x2c, 0x0a, 0x05, 0x73, 0x74, 0x75, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b,
    -	0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x48, 0x6f,
    -	0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x05, 0x73, 0x74, 0x75, 0x6e, 0x73, 0x12,
    -	0x35, 0x0a, 0x05, 0x74, 0x75, 0x72, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f,
    -	0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x72, 0x6f, 0x74,
    -	0x65, 0x63, 0x74, 0x65, 0x64, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52,
    -	0x05, 0x74, 0x75, 0x72, 0x6e, 0x73, 0x12, 0x2e, 0x0a, 0x06, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c,
    -	0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d,
    -	0x65, 0x6e, 0x74, 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x06,
    -	0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x12, 0x2d, 0x0a, 0x05, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x18,
    -	0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65,
    -	0x6e, 0x74, 0x2e, 0x52, 0x65, 0x6c, 0x61, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x05,
    -	0x72, 0x65, 0x6c, 0x61, 0x79, 0x22, 0x98, 0x01, 0x0a, 0x0a, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f,
    -	0x6e, 0x66, 0x69, 0x67, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x69, 0x18, 0x01, 0x20, 0x01, 0x28,
    -	0x09, 0x52, 0x03, 0x75, 0x72, 0x69, 0x12, 0x3b, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63,
    -	0x6f, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1f, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67,
    +	0x70, 0x65, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x64, 0x6e, 0x73, 0x4c,
    +	0x61, 0x62, 0x65, 0x6c, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x09, 0x64, 0x6e, 0x73,
    +	0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x22, 0x44, 0x0a, 0x08, 0x50, 0x65, 0x65, 0x72, 0x4b, 0x65,
    +	0x79, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x73, 0x68, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18,
    +	0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x73, 0x73, 0x68, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79,
    +	0x12, 0x1a, 0x0a, 0x08, 0x77, 0x67, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01,
    +	0x28, 0x0c, 0x52, 0x08, 0x77, 0x67, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x22, 0x3f, 0x0a, 0x0b,
    +	0x45, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x63,
    +	0x6c, 0x6f, 0x75, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x63, 0x6c, 0x6f, 0x75,
    +	0x64, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x18, 0x02, 0x20,
    +	0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x22, 0x5c, 0x0a,
    +	0x04, 0x46, 0x69, 0x6c, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20,
    +	0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x78, 0x69,
    +	0x73, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x05, 0x65, 0x78, 0x69, 0x73, 0x74, 0x12,
    +	0x2a, 0x0a, 0x10, 0x70, 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x49, 0x73, 0x52, 0x75, 0x6e, 0x6e,
    +	0x69, 0x6e, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x70, 0x72, 0x6f, 0x63, 0x65,
    +	0x73, 0x73, 0x49, 0x73, 0x52, 0x75, 0x6e, 0x6e, 0x69, 0x6e, 0x67, 0x22, 0xbf, 0x02, 0x0a, 0x05,
    +	0x46, 0x6c, 0x61, 0x67, 0x73, 0x12, 0x2a, 0x0a, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61,
    +	0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52,
    +	0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65,
    +	0x64, 0x12, 0x30, 0x0a, 0x13, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, 0x65,
    +	0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x76, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x13,
    +	0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73,
    +	0x69, 0x76, 0x65, 0x12, 0x2a, 0x0a, 0x10, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x53, 0x53, 0x48,
    +	0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x73,
    +	0x65, 0x72, 0x76, 0x65, 0x72, 0x53, 0x53, 0x48, 0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x12,
    +	0x30, 0x0a, 0x13, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74,
    +	0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x13, 0x64, 0x69,
    +	0x73, 0x61, 0x62, 0x6c, 0x65, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, 0x6f, 0x75, 0x74, 0x65,
    +	0x73, 0x12, 0x30, 0x0a, 0x13, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x53, 0x65, 0x72, 0x76,
    +	0x65, 0x72, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x13,
    +	0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x6f, 0x75,
    +	0x74, 0x65, 0x73, 0x12, 0x1e, 0x0a, 0x0a, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x44, 0x4e,
    +	0x53, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65,
    +	0x44, 0x4e, 0x53, 0x12, 0x28, 0x0a, 0x0f, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x46, 0x69,
    +	0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x18, 0x07, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, 0x64, 0x69,
    +	0x73, 0x61, 0x62, 0x6c, 0x65, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x22, 0xf2, 0x04,
    +	0x0a, 0x0e, 0x50, 0x65, 0x65, 0x72, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x4d, 0x65, 0x74, 0x61,
    +	0x12, 0x1a, 0x0a, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01,
    +	0x28, 0x09, 0x52, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04,
    +	0x67, 0x6f, 0x4f, 0x53, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x67, 0x6f, 0x4f, 0x53,
    +	0x12, 0x16, 0x0a, 0x06, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09,
    +	0x52, 0x06, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, 0x72, 0x65,
    +	0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x63, 0x6f, 0x72, 0x65, 0x12, 0x1a, 0x0a, 0x08,
    +	0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08,
    +	0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x12, 0x0e, 0x0a, 0x02, 0x4f, 0x53, 0x18, 0x06,
    +	0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x4f, 0x53, 0x12, 0x26, 0x0a, 0x0e, 0x6e, 0x65, 0x74, 0x62,
    +	0x69, 0x72, 0x64, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09,
    +	0x52, 0x0e, 0x6e, 0x65, 0x74, 0x62, 0x69, 0x72, 0x64, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e,
    +	0x12, 0x1c, 0x0a, 0x09, 0x75, 0x69, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x08, 0x20,
    +	0x01, 0x28, 0x09, 0x52, 0x09, 0x75, 0x69, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x24,
    +	0x0a, 0x0d, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18,
    +	0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x56, 0x65, 0x72,
    +	0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1c, 0x0a, 0x09, 0x4f, 0x53, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f,
    +	0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x4f, 0x53, 0x56, 0x65, 0x72, 0x73, 0x69,
    +	0x6f, 0x6e, 0x12, 0x46, 0x0a, 0x10, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x41, 0x64, 0x64,
    +	0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x18, 0x0b, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d,
    +	0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72,
    +	0x6b, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x52, 0x10, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72,
    +	0x6b, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x12, 0x28, 0x0a, 0x0f, 0x73, 0x79,
    +	0x73, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x18, 0x0c, 0x20,
    +	0x01, 0x28, 0x09, 0x52, 0x0f, 0x73, 0x79, 0x73, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x4e, 0x75,
    +	0x6d, 0x62, 0x65, 0x72, 0x12, 0x26, 0x0a, 0x0e, 0x73, 0x79, 0x73, 0x50, 0x72, 0x6f, 0x64, 0x75,
    +	0x63, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x73, 0x79,
    +	0x73, 0x50, 0x72, 0x6f, 0x64, 0x75, 0x63, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x28, 0x0a, 0x0f,
    +	0x73, 0x79, 0x73, 0x4d, 0x61, 0x6e, 0x75, 0x66, 0x61, 0x63, 0x74, 0x75, 0x72, 0x65, 0x72, 0x18,
    +	0x0e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x73, 0x79, 0x73, 0x4d, 0x61, 0x6e, 0x75, 0x66, 0x61,
    +	0x63, 0x74, 0x75, 0x72, 0x65, 0x72, 0x12, 0x39, 0x0a, 0x0b, 0x65, 0x6e, 0x76, 0x69, 0x72, 0x6f,
    +	0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x6d, 0x61,
    +	0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e,
    +	0x6d, 0x65, 0x6e, 0x74, 0x52, 0x0b, 0x65, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e,
    +	0x74, 0x12, 0x26, 0x0a, 0x05, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x18, 0x10, 0x20, 0x03, 0x28, 0x0b,
    +	0x32, 0x10, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x46, 0x69,
    +	0x6c, 0x65, 0x52, 0x05, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x12, 0x27, 0x0a, 0x05, 0x66, 0x6c, 0x61,
    +	0x67, 0x73, 0x18, 0x11, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67,
    +	0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x46, 0x6c, 0x61, 0x67, 0x73, 0x52, 0x05, 0x66, 0x6c, 0x61,
    +	0x67, 0x73, 0x22, 0xb4, 0x01, 0x0a, 0x0d, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70,
    +	0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3f, 0x0a, 0x0d, 0x6e, 0x65, 0x74, 0x62, 0x69, 0x72, 0x64, 0x43,
    +	0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x6d, 0x61,
    +	0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x4e, 0x65, 0x74, 0x62, 0x69, 0x72, 0x64,
    +	0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0d, 0x6e, 0x65, 0x74, 0x62, 0x69, 0x72, 0x64, 0x43,
    +	0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x36, 0x0a, 0x0a, 0x70, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e,
    +	0x66, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61,
    +	0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69,
    +	0x67, 0x52, 0x0a, 0x70, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x2a, 0x0a,
    +	0x06, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e,
    +	0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x68, 0x65, 0x63, 0x6b,
    +	0x73, 0x52, 0x06, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x22, 0x79, 0x0a, 0x11, 0x53, 0x65, 0x72,
    +	0x76, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x10,
    +	0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79,
    +	0x12, 0x38, 0x0a, 0x09, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x41, 0x74, 0x18, 0x02, 0x20,
    +	0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f,
    +	0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52,
    +	0x09, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x41, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65,
    +	0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x07, 0x76, 0x65, 0x72,
    +	0x73, 0x69, 0x6f, 0x6e, 0x22, 0x07, 0x0a, 0x05, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0xd3, 0x01,
    +	0x0a, 0x0d, 0x4e, 0x65, 0x74, 0x62, 0x69, 0x72, 0x64, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12,
    +	0x2c, 0x0a, 0x05, 0x73, 0x74, 0x75, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16,
    +	0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x48, 0x6f, 0x73, 0x74,
    +	0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x05, 0x73, 0x74, 0x75, 0x6e, 0x73, 0x12, 0x35, 0x0a,
    +	0x05, 0x74, 0x75, 0x72, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x6d,
    +	0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x72, 0x6f, 0x74, 0x65, 0x63,
    +	0x74, 0x65, 0x64, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x05, 0x74,
    +	0x75, 0x72, 0x6e, 0x73, 0x12, 0x2e, 0x0a, 0x06, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x18, 0x03,
    +	0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e,
    +	0x74, 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x06, 0x73, 0x69,
    +	0x67, 0x6e, 0x61, 0x6c, 0x12, 0x2d, 0x0a, 0x05, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x18, 0x04, 0x20,
    +	0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74,
    +	0x2e, 0x52, 0x65, 0x6c, 0x61, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x05, 0x72, 0x65,
    +	0x6c, 0x61, 0x79, 0x22, 0x98, 0x01, 0x0a, 0x0a, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66,
    +	0x69, 0x67, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x69, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
    +	0x03, 0x75, 0x72, 0x69, 0x12, 0x3b, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c,
    +	0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1f, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d,
    +	0x65, 0x6e, 0x74, 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x50,
    +	0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f,
    +	0x6c, 0x22, 0x3b, 0x0a, 0x08, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x07, 0x0a,
    +	0x03, 0x55, 0x44, 0x50, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x54, 0x43, 0x50, 0x10, 0x01, 0x12,
    +	0x08, 0x0a, 0x04, 0x48, 0x54, 0x54, 0x50, 0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, 0x48, 0x54, 0x54,
    +	0x50, 0x53, 0x10, 0x03, 0x12, 0x08, 0x0a, 0x04, 0x44, 0x54, 0x4c, 0x53, 0x10, 0x04, 0x22, 0x6d,
    +	0x0a, 0x0b, 0x52, 0x65, 0x6c, 0x61, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, 0x0a,
    +	0x04, 0x75, 0x72, 0x6c, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x04, 0x75, 0x72, 0x6c,
    +	0x73, 0x12, 0x22, 0x0a, 0x0c, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61,
    +	0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x50, 0x61,
    +	0x79, 0x6c, 0x6f, 0x61, 0x64, 0x12, 0x26, 0x0a, 0x0e, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x53, 0x69,
    +	0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x74,
    +	0x6f, 0x6b, 0x65, 0x6e, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x22, 0x7d, 0x0a,
    +	0x13, 0x50, 0x72, 0x6f, 0x74, 0x65, 0x63, 0x74, 0x65, 0x64, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f,
    +	0x6e, 0x66, 0x69, 0x67, 0x12, 0x36, 0x0a, 0x0a, 0x68, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66,
    +	0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67,
     	0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67,
    -	0x2e, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f,
    -	0x63, 0x6f, 0x6c, 0x22, 0x3b, 0x0a, 0x08, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12,
    -	0x07, 0x0a, 0x03, 0x55, 0x44, 0x50, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x54, 0x43, 0x50, 0x10,
    -	0x01, 0x12, 0x08, 0x0a, 0x04, 0x48, 0x54, 0x54, 0x50, 0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, 0x48,
    -	0x54, 0x54, 0x50, 0x53, 0x10, 0x03, 0x12, 0x08, 0x0a, 0x04, 0x44, 0x54, 0x4c, 0x53, 0x10, 0x04,
    -	0x22, 0x6d, 0x0a, 0x0b, 0x52, 0x65, 0x6c, 0x61, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12,
    -	0x12, 0x0a, 0x04, 0x75, 0x72, 0x6c, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x04, 0x75,
    -	0x72, 0x6c, 0x73, 0x12, 0x22, 0x0a, 0x0c, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x50, 0x61, 0x79, 0x6c,
    -	0x6f, 0x61, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x74, 0x6f, 0x6b, 0x65, 0x6e,
    -	0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x12, 0x26, 0x0a, 0x0e, 0x74, 0x6f, 0x6b, 0x65, 0x6e,
    -	0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52,
    -	0x0e, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x22,
    -	0x7d, 0x0a, 0x13, 0x50, 0x72, 0x6f, 0x74, 0x65, 0x63, 0x74, 0x65, 0x64, 0x48, 0x6f, 0x73, 0x74,
    -	0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x36, 0x0a, 0x0a, 0x68, 0x6f, 0x73, 0x74, 0x43, 0x6f,
    -	0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e,
    -	0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66,
    -	0x69, 0x67, 0x52, 0x0a, 0x68, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12,
    -	0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x75, 0x73,
    -	0x65, 0x72, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x03,
    -	0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, 0xcb,
    -	0x01, 0x0a, 0x0a, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x18, 0x0a,
    -	0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07,
    -	0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x64, 0x6e, 0x73, 0x18, 0x02,
    -	0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x64, 0x6e, 0x73, 0x12, 0x33, 0x0a, 0x09, 0x73, 0x73, 0x68,
    -	0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6d,
    -	0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x53, 0x48, 0x43, 0x6f, 0x6e,
    -	0x66, 0x69, 0x67, 0x52, 0x09, 0x73, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12,
    -	0x0a, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x71,
    -	0x64, 0x6e, 0x12, 0x48, 0x0a, 0x1f, 0x52, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, 0x50, 0x65, 0x65,
    -	0x72, 0x44, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e,
    -	0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x1f, 0x52, 0x6f, 0x75,
    -	0x74, 0x69, 0x6e, 0x67, 0x50, 0x65, 0x65, 0x72, 0x44, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x6f, 0x6c,
    -	0x75, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x22, 0xf3, 0x04, 0x0a,
    -	0x0a, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x12, 0x16, 0x0a, 0x06, 0x53,
    -	0x65, 0x72, 0x69, 0x61, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x53, 0x65, 0x72,
    -	0x69, 0x61, 0x6c, 0x12, 0x36, 0x0a, 0x0a, 0x70, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69,
    -	0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65,
    -	0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52,
    -	0x0a, 0x70, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x3e, 0x0a, 0x0b, 0x72,
    -	0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b,
    -	0x32, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x65,
    -	0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0b,
    -	0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x12, 0x2e, 0x0a, 0x12, 0x72,
    -	0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74,
    -	0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x12, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50,
    -	0x65, 0x65, 0x72, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x29, 0x0a, 0x06, 0x52,
    -	0x6f, 0x75, 0x74, 0x65, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x6d, 0x61,
    -	0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x52, 0x06,
    -	0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x12, 0x33, 0x0a, 0x09, 0x44, 0x4e, 0x53, 0x43, 0x6f, 0x6e,
    -	0x66, 0x69, 0x67, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6d, 0x61, 0x6e, 0x61,
    -	0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x44, 0x4e, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67,
    -	0x52, 0x09, 0x44, 0x4e, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x40, 0x0a, 0x0c, 0x6f,
    -	0x66, 0x66, 0x6c, 0x69, 0x6e, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28,
    -	0x0b, 0x32, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52,
    -	0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52,
    -	0x0c, 0x6f, 0x66, 0x66, 0x6c, 0x69, 0x6e, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x12, 0x3e, 0x0a,
    -	0x0d, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x18, 0x08,
    -	0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e,
    -	0x74, 0x2e, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x52, 0x0d,
    -	0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x12, 0x32, 0x0a,
    -	0x14, 0x66, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x49, 0x73,
    -	0x45, 0x6d, 0x70, 0x74, 0x79, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x14, 0x66, 0x69, 0x72,
    -	0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74,
    -	0x79, 0x12, 0x4f, 0x0a, 0x13, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x46, 0x69, 0x72, 0x65, 0x77,
    -	0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d,
    -	0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x6f, 0x75, 0x74,
    -	0x65, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x52, 0x13, 0x72,
    -	0x6f, 0x75, 0x74, 0x65, 0x73, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c,
    -	0x65, 0x73, 0x12, 0x3e, 0x0a, 0x1a, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x46, 0x69, 0x72, 0x65,
    +	0x52, 0x0a, 0x68, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, 0x0a, 0x04,
    +	0x75, 0x73, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72,
    +	0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x03, 0x20, 0x01,
    +	0x28, 0x09, 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, 0xcb, 0x01, 0x0a,
    +	0x0a, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x18, 0x0a, 0x07, 0x61,
    +	0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x64,
    +	0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x64, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x01,
    +	0x28, 0x09, 0x52, 0x03, 0x64, 0x6e, 0x73, 0x12, 0x33, 0x0a, 0x09, 0x73, 0x73, 0x68, 0x43, 0x6f,
    +	0x6e, 0x66, 0x69, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6d, 0x61, 0x6e,
    +	0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x53, 0x48, 0x43, 0x6f, 0x6e, 0x66, 0x69,
    +	0x67, 0x52, 0x09, 0x73, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, 0x0a, 0x04,
    +	0x66, 0x71, 0x64, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x71, 0x64, 0x6e,
    +	0x12, 0x48, 0x0a, 0x1f, 0x52, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, 0x50, 0x65, 0x65, 0x72, 0x44,
    +	0x6e, 0x73, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x61, 0x62,
    +	0x6c, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x1f, 0x52, 0x6f, 0x75, 0x74, 0x69,
    +	0x6e, 0x67, 0x50, 0x65, 0x65, 0x72, 0x44, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x75, 0x74,
    +	0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x22, 0xf3, 0x04, 0x0a, 0x0a, 0x4e,
    +	0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x12, 0x16, 0x0a, 0x06, 0x53, 0x65, 0x72,
    +	0x69, 0x61, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x53, 0x65, 0x72, 0x69, 0x61,
    +	0x6c, 0x12, 0x36, 0x0a, 0x0a, 0x70, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18,
    +	0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65,
    +	0x6e, 0x74, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0a, 0x70,
    +	0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x3e, 0x0a, 0x0b, 0x72, 0x65, 0x6d,
    +	0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c,
    +	0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x65, 0x6d, 0x6f,
    +	0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0b, 0x72, 0x65,
    +	0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x12, 0x2e, 0x0a, 0x12, 0x72, 0x65, 0x6d,
    +	0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x18,
    +	0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x12, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65,
    +	0x72, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x29, 0x0a, 0x06, 0x52, 0x6f, 0x75,
    +	0x74, 0x65, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61,
    +	0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x52, 0x06, 0x52, 0x6f,
    +	0x75, 0x74, 0x65, 0x73, 0x12, 0x33, 0x0a, 0x09, 0x44, 0x4e, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69,
    +	0x67, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65,
    +	0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x44, 0x4e, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x09,
    +	0x44, 0x4e, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x40, 0x0a, 0x0c, 0x6f, 0x66, 0x66,
    +	0x6c, 0x69, 0x6e, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32,
    +	0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x65, 0x6d,
    +	0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0c, 0x6f,
    +	0x66, 0x66, 0x6c, 0x69, 0x6e, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x12, 0x3e, 0x0a, 0x0d, 0x46,
    +	0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x18, 0x08, 0x20, 0x03,
    +	0x28, 0x0b, 0x32, 0x18, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e,
    +	0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x52, 0x0d, 0x46, 0x69,
    +	0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x12, 0x32, 0x0a, 0x14, 0x66,
    +	0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x49, 0x73, 0x45, 0x6d,
    +	0x70, 0x74, 0x79, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x14, 0x66, 0x69, 0x72, 0x65, 0x77,
    +	0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12,
    +	0x4f, 0x0a, 0x13, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c,
    +	0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x6d,
    +	0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x46,
    +	0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x52, 0x13, 0x72, 0x6f, 0x75,
    +	0x74, 0x65, 0x73, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73,
    +	0x12, 0x3e, 0x0a, 0x1a, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61,
    +	0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x18, 0x0b,
    +	0x20, 0x01, 0x28, 0x08, 0x52, 0x1a, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x46, 0x69, 0x72, 0x65,
     	0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79,
    -	0x18, 0x0b, 0x20, 0x01, 0x28, 0x08, 0x52, 0x1a, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x46, 0x69,
    -	0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70,
    -	0x74, 0x79, 0x22, 0x97, 0x01, 0x0a, 0x10, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65,
    -	0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1a, 0x0a, 0x08, 0x77, 0x67, 0x50, 0x75, 0x62,
    -	0x4b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x77, 0x67, 0x50, 0x75, 0x62,
    -	0x4b, 0x65, 0x79, 0x12, 0x1e, 0x0a, 0x0a, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x49, 0x70,
    -	0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64,
    -	0x49, 0x70, 0x73, 0x12, 0x33, 0x0a, 0x09, 0x73, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67,
    -	0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d,
    -	0x65, 0x6e, 0x74, 0x2e, 0x53, 0x53, 0x48, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x09, 0x73,
    -	0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x71, 0x64, 0x6e,
    -	0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x22, 0x49, 0x0a, 0x09,
    -	0x53, 0x53, 0x48, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1e, 0x0a, 0x0a, 0x73, 0x73, 0x68,
    -	0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x73,
    -	0x73, 0x68, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x73, 0x68,
    -	0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x73, 0x73,
    -	0x68, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x22, 0x20, 0x0a, 0x1e, 0x44, 0x65, 0x76, 0x69, 0x63,
    -	0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c,
    -	0x6f, 0x77, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xbf, 0x01, 0x0a, 0x17, 0x44, 0x65,
    -	0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f,
    -	0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x12, 0x48, 0x0a, 0x08, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65,
    -	0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x2c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65,
    -	0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f,
    -	0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x2e, 0x70, 0x72, 0x6f,
    -	0x76, 0x69, 0x64, 0x65, 0x72, 0x52, 0x08, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12,
    -	0x42, 0x0a, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69,
    -	0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65,
    -	0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e,
    -	0x66, 0x69, 0x67, 0x52, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e,
    -	0x66, 0x69, 0x67, 0x22, 0x16, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12,
    -	0x0a, 0x0a, 0x06, 0x48, 0x4f, 0x53, 0x54, 0x45, 0x44, 0x10, 0x00, 0x22, 0x1e, 0x0a, 0x1c, 0x50,
    -	0x4b, 0x43, 0x45, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e,
    -	0x46, 0x6c, 0x6f, 0x77, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x5b, 0x0a, 0x15, 0x50,
    -	0x4b, 0x43, 0x45, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e,
    -	0x46, 0x6c, 0x6f, 0x77, 0x12, 0x42, 0x0a, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72,
    -	0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d,
    -	0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64,
    -	0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64,
    -	0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0xea, 0x02, 0x0a, 0x0e, 0x50, 0x72, 0x6f,
    -	0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1a, 0x0a, 0x08, 0x43,
    -	0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x43,
    -	0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x44, 0x12, 0x22, 0x0a, 0x0c, 0x43, 0x6c, 0x69, 0x65, 0x6e,
    -	0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x43,
    -	0x6c, 0x69, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x44,
    -	0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x44, 0x6f, 0x6d,
    -	0x61, 0x69, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x41, 0x75, 0x64, 0x69, 0x65, 0x6e, 0x63, 0x65, 0x18,
    -	0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x41, 0x75, 0x64, 0x69, 0x65, 0x6e, 0x63, 0x65, 0x12,
    -	0x2e, 0x0a, 0x12, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x45, 0x6e, 0x64,
    -	0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x44, 0x65, 0x76,
    -	0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12,
    -	0x24, 0x0a, 0x0d, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74,
    -	0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x45, 0x6e, 0x64,
    -	0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x18, 0x07,
    -	0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x55,
    -	0x73, 0x65, 0x49, 0x44, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52,
    -	0x0a, 0x55, 0x73, 0x65, 0x49, 0x44, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x34, 0x0a, 0x15, 0x41,
    -	0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x64, 0x70,
    -	0x6f, 0x69, 0x6e, 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x15, 0x41, 0x75, 0x74, 0x68,
    -	0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e,
    -	0x74, 0x12, 0x22, 0x0a, 0x0c, 0x52, 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x55, 0x52, 0x4c,
    -	0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0c, 0x52, 0x65, 0x64, 0x69, 0x72, 0x65, 0x63,
    -	0x74, 0x55, 0x52, 0x4c, 0x73, 0x22, 0xed, 0x01, 0x0a, 0x05, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x12,
    -	0x0e, 0x0a, 0x02, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x44, 0x12,
    -	0x18, 0x0a, 0x07, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09,
    -	0x52, 0x07, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x12, 0x20, 0x0a, 0x0b, 0x4e, 0x65, 0x74,
    -	0x77, 0x6f, 0x72, 0x6b, 0x54, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b,
    -	0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x50,
    -	0x65, 0x65, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x50, 0x65, 0x65, 0x72, 0x12,
    -	0x16, 0x0a, 0x06, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52,
    -	0x06, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x12, 0x1e, 0x0a, 0x0a, 0x4d, 0x61, 0x73, 0x71, 0x75,
    -	0x65, 0x72, 0x61, 0x64, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x4d, 0x61, 0x73,
    -	0x71, 0x75, 0x65, 0x72, 0x61, 0x64, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x4e, 0x65, 0x74, 0x49, 0x44,
    -	0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x4e, 0x65, 0x74, 0x49, 0x44, 0x12, 0x18, 0x0a,
    -	0x07, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07,
    -	0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x6b, 0x65, 0x65, 0x70, 0x52,
    -	0x6f, 0x75, 0x74, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x6b, 0x65, 0x65, 0x70,
    -	0x52, 0x6f, 0x75, 0x74, 0x65, 0x22, 0xb4, 0x01, 0x0a, 0x09, 0x44, 0x4e, 0x53, 0x43, 0x6f, 0x6e,
    -	0x66, 0x69, 0x67, 0x12, 0x24, 0x0a, 0x0d, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x45, 0x6e,
    -	0x61, 0x62, 0x6c, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x53, 0x65, 0x72, 0x76,
    -	0x69, 0x63, 0x65, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x47, 0x0a, 0x10, 0x4e, 0x61, 0x6d,
    -	0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x18, 0x02, 0x20,
    -	0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74,
    -	0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70,
    -	0x52, 0x10, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75,
    -	0x70, 0x73, 0x12, 0x38, 0x0a, 0x0b, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65,
    -	0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65,
    -	0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, 0x52,
    -	0x0b, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, 0x73, 0x22, 0x58, 0x0a, 0x0a,
    -	0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x44, 0x6f,
    -	0x6d, 0x61, 0x69, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x44, 0x6f, 0x6d, 0x61,
    -	0x69, 0x6e, 0x12, 0x32, 0x0a, 0x07, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x18, 0x02, 0x20,
    -	0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74,
    -	0x2e, 0x53, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x52, 0x07, 0x52,
    -	0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x22, 0x74, 0x0a, 0x0c, 0x53, 0x69, 0x6d, 0x70, 0x6c, 0x65,
    -	0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01,
    -	0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x54, 0x79,
    -	0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12, 0x14,
    -	0x0a, 0x05, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x43,
    -	0x6c, 0x61, 0x73, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x54, 0x54, 0x4c, 0x18, 0x04, 0x20, 0x01, 0x28,
    -	0x03, 0x52, 0x03, 0x54, 0x54, 0x4c, 0x12, 0x14, 0x0a, 0x05, 0x52, 0x44, 0x61, 0x74, 0x61, 0x18,
    -	0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x52, 0x44, 0x61, 0x74, 0x61, 0x22, 0xb3, 0x01, 0x0a,
    -	0x0f, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70,
    -	0x12, 0x38, 0x0a, 0x0b, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x18,
    -	0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65,
    -	0x6e, 0x74, 0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x0b, 0x4e,
    -	0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x50, 0x72,
    -	0x69, 0x6d, 0x61, 0x72, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x50, 0x72, 0x69,
    -	0x6d, 0x61, 0x72, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18,
    -	0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x32,
    -	0x0a, 0x14, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x45,
    -	0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x14, 0x53, 0x65,
    -	0x61, 0x72, 0x63, 0x68, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c,
    -	0x65, 0x64, 0x22, 0x48, 0x0a, 0x0a, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72,
    -	0x12, 0x0e, 0x0a, 0x02, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x50,
    -	0x12, 0x16, 0x0a, 0x06, 0x4e, 0x53, 0x54, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03,
    -	0x52, 0x06, 0x4e, 0x53, 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x6f, 0x72, 0x74,
    -	0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x22, 0x8b, 0x02, 0x0a,
    -	0x0c, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x16, 0x0a,
    -	0x06, 0x50, 0x65, 0x65, 0x72, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x50,
    -	0x65, 0x65, 0x72, 0x49, 0x50, 0x12, 0x37, 0x0a, 0x09, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69,
    -	0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x19, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67,
    -	0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74,
    -	0x69, 0x6f, 0x6e, 0x52, 0x09, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x2e,
    -	0x0a, 0x06, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x16,
    +	0x22, 0x97, 0x01, 0x0a, 0x10, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x43,
    +	0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1a, 0x0a, 0x08, 0x77, 0x67, 0x50, 0x75, 0x62, 0x4b, 0x65,
    +	0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x77, 0x67, 0x50, 0x75, 0x62, 0x4b, 0x65,
    +	0x79, 0x12, 0x1e, 0x0a, 0x0a, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x49, 0x70, 0x73, 0x18,
    +	0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x49, 0x70,
    +	0x73, 0x12, 0x33, 0x0a, 0x09, 0x73, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x03,
    +	0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e,
    +	0x74, 0x2e, 0x53, 0x53, 0x48, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x09, 0x73, 0x73, 0x68,
    +	0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x18, 0x04,
    +	0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x22, 0x49, 0x0a, 0x09, 0x53, 0x53,
    +	0x48, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1e, 0x0a, 0x0a, 0x73, 0x73, 0x68, 0x45, 0x6e,
    +	0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x73, 0x73, 0x68,
    +	0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x73, 0x68, 0x50, 0x75,
    +	0x62, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x73, 0x73, 0x68, 0x50,
    +	0x75, 0x62, 0x4b, 0x65, 0x79, 0x22, 0x20, 0x0a, 0x1e, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41,
    +	0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77,
    +	0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xbf, 0x01, 0x0a, 0x17, 0x44, 0x65, 0x76, 0x69,
    +	0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46,
    +	0x6c, 0x6f, 0x77, 0x12, 0x48, 0x0a, 0x08, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x18,
    +	0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x2c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65,
    +	0x6e, 0x74, 0x2e, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69,
    +	0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69,
    +	0x64, 0x65, 0x72, 0x52, 0x08, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x42, 0x0a,
    +	0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18,
    +	0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65,
    +	0x6e, 0x74, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69,
    +	0x67, 0x52, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69,
    +	0x67, 0x22, 0x16, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x0a, 0x0a,
    +	0x06, 0x48, 0x4f, 0x53, 0x54, 0x45, 0x44, 0x10, 0x00, 0x22, 0x1e, 0x0a, 0x1c, 0x50, 0x4b, 0x43,
    +	0x45, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c,
    +	0x6f, 0x77, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x5b, 0x0a, 0x15, 0x50, 0x4b, 0x43,
    +	0x45, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c,
    +	0x6f, 0x77, 0x12, 0x42, 0x0a, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f,
    +	0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e,
    +	0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72,
    +	0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72,
    +	0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0xea, 0x02, 0x0a, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69,
    +	0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1a, 0x0a, 0x08, 0x43, 0x6c, 0x69,
    +	0x65, 0x6e, 0x74, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x43, 0x6c, 0x69,
    +	0x65, 0x6e, 0x74, 0x49, 0x44, 0x12, 0x22, 0x0a, 0x0c, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x53,
    +	0x65, 0x63, 0x72, 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x43, 0x6c, 0x69,
    +	0x65, 0x6e, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x44, 0x6f, 0x6d,
    +	0x61, 0x69, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69,
    +	0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x41, 0x75, 0x64, 0x69, 0x65, 0x6e, 0x63, 0x65, 0x18, 0x04, 0x20,
    +	0x01, 0x28, 0x09, 0x52, 0x08, 0x41, 0x75, 0x64, 0x69, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x2e, 0x0a,
    +	0x12, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x45, 0x6e, 0x64, 0x70, 0x6f,
    +	0x69, 0x6e, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x44, 0x65, 0x76, 0x69, 0x63,
    +	0x65, 0x41, 0x75, 0x74, 0x68, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x24, 0x0a,
    +	0x0d, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x06,
    +	0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f,
    +	0x69, 0x6e, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x18, 0x07, 0x20, 0x01,
    +	0x28, 0x09, 0x52, 0x05, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x55, 0x73, 0x65,
    +	0x49, 0x44, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x55,
    +	0x73, 0x65, 0x49, 0x44, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x34, 0x0a, 0x15, 0x41, 0x75, 0x74,
    +	0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69,
    +	0x6e, 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x15, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72,
    +	0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12,
    +	0x22, 0x0a, 0x0c, 0x52, 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x55, 0x52, 0x4c, 0x73, 0x18,
    +	0x0a, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0c, 0x52, 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x55,
    +	0x52, 0x4c, 0x73, 0x22, 0xed, 0x01, 0x0a, 0x05, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x12, 0x0e, 0x0a,
    +	0x02, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x44, 0x12, 0x18, 0x0a,
    +	0x07, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07,
    +	0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x12, 0x20, 0x0a, 0x0b, 0x4e, 0x65, 0x74, 0x77, 0x6f,
    +	0x72, 0x6b, 0x54, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x4e, 0x65,
    +	0x74, 0x77, 0x6f, 0x72, 0x6b, 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x65, 0x65,
    +	0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x50, 0x65, 0x65, 0x72, 0x12, 0x16, 0x0a,
    +	0x06, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x4d,
    +	0x65, 0x74, 0x72, 0x69, 0x63, 0x12, 0x1e, 0x0a, 0x0a, 0x4d, 0x61, 0x73, 0x71, 0x75, 0x65, 0x72,
    +	0x61, 0x64, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x4d, 0x61, 0x73, 0x71, 0x75,
    +	0x65, 0x72, 0x61, 0x64, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x4e, 0x65, 0x74, 0x49, 0x44, 0x18, 0x07,
    +	0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x4e, 0x65, 0x74, 0x49, 0x44, 0x12, 0x18, 0x0a, 0x07, 0x44,
    +	0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x44, 0x6f,
    +	0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x6b, 0x65, 0x65, 0x70, 0x52, 0x6f, 0x75,
    +	0x74, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x6b, 0x65, 0x65, 0x70, 0x52, 0x6f,
    +	0x75, 0x74, 0x65, 0x22, 0xb4, 0x01, 0x0a, 0x09, 0x44, 0x4e, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69,
    +	0x67, 0x12, 0x24, 0x0a, 0x0d, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x45, 0x6e, 0x61, 0x62,
    +	0x6c, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63,
    +	0x65, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x47, 0x0a, 0x10, 0x4e, 0x61, 0x6d, 0x65, 0x53,
    +	0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28,
    +	0x0b, 0x32, 0x1b, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x4e,
    +	0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x10,
    +	0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73,
    +	0x12, 0x38, 0x0a, 0x0b, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, 0x73, 0x18,
    +	0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65,
    +	0x6e, 0x74, 0x2e, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, 0x52, 0x0b, 0x43,
    +	0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, 0x73, 0x22, 0x58, 0x0a, 0x0a, 0x43, 0x75,
    +	0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x44, 0x6f, 0x6d, 0x61,
    +	0x69, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e,
    +	0x12, 0x32, 0x0a, 0x07, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28,
    +	0x0b, 0x32, 0x18, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53,
    +	0x69, 0x6d, 0x70, 0x6c, 0x65, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x52, 0x07, 0x52, 0x65, 0x63,
    +	0x6f, 0x72, 0x64, 0x73, 0x22, 0x74, 0x0a, 0x0c, 0x53, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x52, 0x65,
    +	0x63, 0x6f, 0x72, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01,
    +	0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65,
    +	0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x05,
    +	0x43, 0x6c, 0x61, 0x73, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x43, 0x6c, 0x61,
    +	0x73, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x54, 0x54, 0x4c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52,
    +	0x03, 0x54, 0x54, 0x4c, 0x12, 0x14, 0x0a, 0x05, 0x52, 0x44, 0x61, 0x74, 0x61, 0x18, 0x05, 0x20,
    +	0x01, 0x28, 0x09, 0x52, 0x05, 0x52, 0x44, 0x61, 0x74, 0x61, 0x22, 0xb3, 0x01, 0x0a, 0x0f, 0x4e,
    +	0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x38,
    +	0x0a, 0x0b, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20,
    +	0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74,
    +	0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x0b, 0x4e, 0x61, 0x6d,
    +	0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x50, 0x72, 0x69, 0x6d,
    +	0x61, 0x72, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x50, 0x72, 0x69, 0x6d, 0x61,
    +	0x72, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x03, 0x20,
    +	0x03, 0x28, 0x09, 0x52, 0x07, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x32, 0x0a, 0x14,
    +	0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x45, 0x6e, 0x61,
    +	0x62, 0x6c, 0x65, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x14, 0x53, 0x65, 0x61, 0x72,
    +	0x63, 0x68, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64,
    +	0x22, 0x48, 0x0a, 0x0a, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, 0x0e,
    +	0x0a, 0x02, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x50, 0x12, 0x16,
    +	0x0a, 0x06, 0x4e, 0x53, 0x54, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06,
    +	0x4e, 0x53, 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x03,
    +	0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x22, 0x8b, 0x02, 0x0a, 0x0c, 0x46,
    +	0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x50,
    +	0x65, 0x65, 0x72, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x50, 0x65, 0x65,
    +	0x72, 0x49, 0x50, 0x12, 0x37, 0x0a, 0x09, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e,
    +	0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x19, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d,
    +	0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f,
    +	0x6e, 0x52, 0x09, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x2e, 0x0a, 0x06,
    +	0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x16, 0x2e, 0x6d,
    +	0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x41, 0x63,
    +	0x74, 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x34, 0x0a, 0x08,
    +	0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x18,
     	0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65,
    -	0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x34,
    -	0x0a, 0x08, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e,
    -	0x32, 0x18, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75,
    -	0x6c, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x52, 0x08, 0x50, 0x72, 0x6f, 0x74,
    -	0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x05, 0x20, 0x01,
    -	0x28, 0x09, 0x52, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x30, 0x0a, 0x08, 0x50, 0x6f, 0x72, 0x74,
    -	0x49, 0x6e, 0x66, 0x6f, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x6d, 0x61, 0x6e,
    -	0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f,
    -	0x52, 0x08, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x22, 0x38, 0x0a, 0x0e, 0x4e, 0x65,
    -	0x74, 0x77, 0x6f, 0x72, 0x6b, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x14, 0x0a, 0x05,
    -	0x6e, 0x65, 0x74, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6e, 0x65, 0x74,
    -	0x49, 0x50, 0x12, 0x10, 0x0a, 0x03, 0x6d, 0x61, 0x63, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52,
    -	0x03, 0x6d, 0x61, 0x63, 0x22, 0x1e, 0x0a, 0x06, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x12, 0x14,
    -	0x0a, 0x05, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x46,
    -	0x69, 0x6c, 0x65, 0x73, 0x22, 0x96, 0x01, 0x0a, 0x08, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66,
    -	0x6f, 0x12, 0x14, 0x0a, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x48,
    -	0x00, 0x52, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x12, 0x32, 0x0a, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65,
    -	0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d,
    -	0x65, 0x6e, 0x74, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x2e, 0x52, 0x61, 0x6e,
    -	0x67, 0x65, 0x48, 0x00, 0x52, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x1a, 0x2f, 0x0a, 0x05, 0x52,
    -	0x61, 0x6e, 0x67, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x18, 0x01, 0x20,
    -	0x01, 0x28, 0x0d, 0x52, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x65, 0x6e,
    -	0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x03, 0x65, 0x6e, 0x64, 0x42, 0x0f, 0x0a, 0x0d,
    -	0x70, 0x6f, 0x72, 0x74, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0xd1, 0x02,
    -	0x0a, 0x11, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52,
    -	0x75, 0x6c, 0x65, 0x12, 0x22, 0x0a, 0x0c, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x61, 0x6e,
    -	0x67, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0c, 0x73, 0x6f, 0x75, 0x72, 0x63,
    -	0x65, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x12, 0x2e, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f,
    -	0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65,
    -	0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52,
    -	0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x74, 0x69,
    -	0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65,
    -	0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x34, 0x0a, 0x08, 0x70, 0x72, 0x6f,
    -	0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x18, 0x2e, 0x6d, 0x61,
    -	0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x50, 0x72, 0x6f,
    -	0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12,
    -	0x30, 0x0a, 0x08, 0x70, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x18, 0x05, 0x20, 0x01, 0x28,
    -	0x0b, 0x32, 0x14, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50,
    -	0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x08, 0x70, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66,
    -	0x6f, 0x12, 0x1c, 0x0a, 0x09, 0x69, 0x73, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x18, 0x06,
    -	0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x69, 0x73, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x12,
    -	0x18, 0x0a, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x09,
    -	0x52, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x26, 0x0a, 0x0e, 0x63, 0x75, 0x73,
    -	0x74, 0x6f, 0x6d, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x08, 0x20, 0x01, 0x28,
    -	0x0d, 0x52, 0x0e, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f,
    -	0x6c, 0x2a, 0x4c, 0x0a, 0x0c, 0x52, 0x75, 0x6c, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f,
    -	0x6c, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x07,
    -	0x0a, 0x03, 0x41, 0x4c, 0x4c, 0x10, 0x01, 0x12, 0x07, 0x0a, 0x03, 0x54, 0x43, 0x50, 0x10, 0x02,
    -	0x12, 0x07, 0x0a, 0x03, 0x55, 0x44, 0x50, 0x10, 0x03, 0x12, 0x08, 0x0a, 0x04, 0x49, 0x43, 0x4d,
    -	0x50, 0x10, 0x04, 0x12, 0x0a, 0x0a, 0x06, 0x43, 0x55, 0x53, 0x54, 0x4f, 0x4d, 0x10, 0x05, 0x2a,
    -	0x20, 0x0a, 0x0d, 0x52, 0x75, 0x6c, 0x65, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e,
    -	0x12, 0x06, 0x0a, 0x02, 0x49, 0x4e, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x4f, 0x55, 0x54, 0x10,
    -	0x01, 0x2a, 0x22, 0x0a, 0x0a, 0x52, 0x75, 0x6c, 0x65, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12,
    -	0x0a, 0x0a, 0x06, 0x41, 0x43, 0x43, 0x45, 0x50, 0x54, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x44,
    -	0x52, 0x4f, 0x50, 0x10, 0x01, 0x32, 0x90, 0x04, 0x0a, 0x11, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65,
    -	0x6d, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x45, 0x0a, 0x05, 0x4c,
    -	0x6f, 0x67, 0x69, 0x6e, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e,
    -	0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61,
    -	0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e,
    +	0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x52, 0x08, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63,
    +	0x6f, 0x6c, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09,
    +	0x52, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x30, 0x0a, 0x08, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e,
    +	0x66, 0x6f, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67,
    +	0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x08,
    +	0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x22, 0x38, 0x0a, 0x0e, 0x4e, 0x65, 0x74, 0x77,
    +	0x6f, 0x72, 0x6b, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x6e, 0x65,
    +	0x74, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6e, 0x65, 0x74, 0x49, 0x50,
    +	0x12, 0x10, 0x0a, 0x03, 0x6d, 0x61, 0x63, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6d,
    +	0x61, 0x63, 0x22, 0x1e, 0x0a, 0x06, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x12, 0x14, 0x0a, 0x05,
    +	0x46, 0x69, 0x6c, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x46, 0x69, 0x6c,
    +	0x65, 0x73, 0x22, 0x96, 0x01, 0x0a, 0x08, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x12,
    +	0x14, 0x0a, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x48, 0x00, 0x52,
    +	0x04, 0x70, 0x6f, 0x72, 0x74, 0x12, 0x32, 0x0a, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x18, 0x02,
    +	0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e,
    +	0x74, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x2e, 0x52, 0x61, 0x6e, 0x67, 0x65,
    +	0x48, 0x00, 0x52, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x1a, 0x2f, 0x0a, 0x05, 0x52, 0x61, 0x6e,
    +	0x67, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28,
    +	0x0d, 0x52, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x65, 0x6e, 0x64, 0x18,
    +	0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x03, 0x65, 0x6e, 0x64, 0x42, 0x0f, 0x0a, 0x0d, 0x70, 0x6f,
    +	0x72, 0x74, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0xd1, 0x02, 0x0a, 0x11,
    +	0x52, 0x6f, 0x75, 0x74, 0x65, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c,
    +	0x65, 0x12, 0x22, 0x0a, 0x0c, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x61, 0x6e, 0x67, 0x65,
    +	0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0c, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52,
    +	0x61, 0x6e, 0x67, 0x65, 0x73, 0x12, 0x2e, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18,
    +	0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65,
    +	0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x61,
    +	0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61,
    +	0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x74,
    +	0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x34, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f,
    +	0x63, 0x6f, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x18, 0x2e, 0x6d, 0x61, 0x6e, 0x61,
    +	0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f,
    +	0x63, 0x6f, 0x6c, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x30, 0x0a,
    +	0x08, 0x70, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32,
    +	0x14, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x6f, 0x72,
    +	0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x08, 0x70, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x12,
    +	0x1c, 0x0a, 0x09, 0x69, 0x73, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x18, 0x06, 0x20, 0x01,
    +	0x28, 0x08, 0x52, 0x09, 0x69, 0x73, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x12, 0x18, 0x0a,
    +	0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07,
    +	0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x26, 0x0a, 0x0e, 0x63, 0x75, 0x73, 0x74, 0x6f,
    +	0x6d, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0d, 0x52,
    +	0x0e, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x2a,
    +	0x4c, 0x0a, 0x0c, 0x52, 0x75, 0x6c, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12,
    +	0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03,
    +	0x41, 0x4c, 0x4c, 0x10, 0x01, 0x12, 0x07, 0x0a, 0x03, 0x54, 0x43, 0x50, 0x10, 0x02, 0x12, 0x07,
    +	0x0a, 0x03, 0x55, 0x44, 0x50, 0x10, 0x03, 0x12, 0x08, 0x0a, 0x04, 0x49, 0x43, 0x4d, 0x50, 0x10,
    +	0x04, 0x12, 0x0a, 0x0a, 0x06, 0x43, 0x55, 0x53, 0x54, 0x4f, 0x4d, 0x10, 0x05, 0x2a, 0x20, 0x0a,
    +	0x0d, 0x52, 0x75, 0x6c, 0x65, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x06,
    +	0x0a, 0x02, 0x49, 0x4e, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x4f, 0x55, 0x54, 0x10, 0x01, 0x2a,
    +	0x22, 0x0a, 0x0a, 0x52, 0x75, 0x6c, 0x65, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0a, 0x0a,
    +	0x06, 0x41, 0x43, 0x43, 0x45, 0x50, 0x54, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x44, 0x52, 0x4f,
    +	0x50, 0x10, 0x01, 0x32, 0x90, 0x04, 0x0a, 0x11, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65,
    +	0x6e, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x45, 0x0a, 0x05, 0x4c, 0x6f, 0x67,
    +	0x69, 0x6e, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e,
     	0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65,
    -	0x22, 0x00, 0x12, 0x46, 0x0a, 0x04, 0x53, 0x79, 0x6e, 0x63, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e,
    -	0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65,
    -	0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67,
    +	0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e,
    +	0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00,
    +	0x12, 0x46, 0x0a, 0x04, 0x53, 0x79, 0x6e, 0x63, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67,
     	0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d,
    -	0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x30, 0x01, 0x12, 0x42, 0x0a, 0x0c, 0x47, 0x65,
    -	0x74, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x12, 0x11, 0x2e, 0x6d, 0x61, 0x6e,
    -	0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1d, 0x2e,
    -	0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65,
    -	0x72, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x33,
    -	0x0a, 0x09, 0x69, 0x73, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x79, 0x12, 0x11, 0x2e, 0x6d, 0x61,
    -	0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x11,
    -	0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74,
    -	0x79, 0x22, 0x00, 0x12, 0x5a, 0x0a, 0x1a, 0x47, 0x65, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65,
    -	0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f,
    -	0x77, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45,
    -	0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a,
    +	0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d,
    +	0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73,
    +	0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x30, 0x01, 0x12, 0x42, 0x0a, 0x0c, 0x47, 0x65, 0x74, 0x53,
    +	0x65, 0x72, 0x76, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x12, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67,
    +	0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1d, 0x2e, 0x6d, 0x61,
    +	0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4b,
    +	0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x33, 0x0a, 0x09,
    +	0x69, 0x73, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x79, 0x12, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61,
    +	0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x11, 0x2e, 0x6d,
    +	0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22,
    +	0x00, 0x12, 0x5a, 0x0a, 0x1a, 0x47, 0x65, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75,
    +	0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x12,
     	0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63,
    -	0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12,
    -	0x58, 0x0a, 0x18, 0x47, 0x65, 0x74, 0x50, 0x4b, 0x43, 0x45, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72,
    -	0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x12, 0x1c, 0x2e, 0x6d, 0x61,
    -	0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74,
    -	0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61,
    +	0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e,
    +	0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79,
    +	0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12, 0x58, 0x0a,
    +	0x18, 0x47, 0x65, 0x74, 0x50, 0x4b, 0x43, 0x45, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a,
    +	0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61,
     	0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64,
    -	0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12, 0x3d, 0x0a, 0x08, 0x53, 0x79, 0x6e,
    -	0x63, 0x4d, 0x65, 0x74, 0x61, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65,
    -	0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73,
    -	0x61, 0x67, 0x65, 0x1a, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74,
    -	0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x42, 0x08, 0x5a, 0x06, 0x2f, 0x70, 0x72, 0x6f,
    -	0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
    +	0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65,
    +	0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65,
    +	0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12, 0x3d, 0x0a, 0x08, 0x53, 0x79, 0x6e, 0x63, 0x4d,
    +	0x65, 0x74, 0x61, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74,
    +	0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67,
    +	0x65, 0x1a, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45,
    +	0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x42, 0x08, 0x5a, 0x06, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f,
    +	0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
     }
     
     var (
    diff --git a/management/proto/management.proto b/management/proto/management.proto
    index b75d3f956..cd207136f 100644
    --- a/management/proto/management.proto
    +++ b/management/proto/management.proto
    @@ -97,7 +97,8 @@ message LoginRequest {
       string jwtToken = 3;
       // Can be absent for now.
       PeerKeys peerKeys = 4;
    -
    +  
    +  repeated string dnsLabels = 5;
     }
     
     // PeerKeys is additional peer info like SSH pub key and WireGuard public key.
    diff --git a/management/server/account.go b/management/server/account.go
    index a0c6fd0b0..661569418 100644
    --- a/management/server/account.go
    +++ b/management/server/account.go
    @@ -63,7 +63,7 @@ type AccountManager interface {
     	GetOrCreateAccountByUser(ctx context.Context, userId, domain string) (*types.Account, error)
     	GetAccount(ctx context.Context, accountID string) (*types.Account, error)
     	CreateSetupKey(ctx context.Context, accountID string, keyName string, keyType types.SetupKeyType, expiresIn time.Duration,
    -		autoGroups []string, usageLimit int, userID string, ephemeral bool) (*types.SetupKey, error)
    +		autoGroups []string, usageLimit int, userID string, ephemeral bool, allowExtraDNSLabels bool) (*types.SetupKey, error)
     	SaveSetupKey(ctx context.Context, accountID string, key *types.SetupKey, userID string) (*types.SetupKey, error)
     	CreateUser(ctx context.Context, accountID, initiatorUserID string, key *types.UserInfo) (*types.UserInfo, error)
     	DeleteUser(ctx context.Context, accountID, initiatorUserID string, targetUserID string) error
    diff --git a/management/server/account_test.go b/management/server/account_test.go
    index eb36dbd84..7d59544e0 100644
    --- a/management/server/account_test.go
    +++ b/management/server/account_test.go
    @@ -1080,7 +1080,7 @@ func TestAccountManager_AddPeer(t *testing.T) {
     
     	serial := account.Network.CurrentSerial() // should be 0
     
    -	setupKey, err := manager.CreateSetupKey(context.Background(), account.Id, "test-key", types.SetupKeyReusable, time.Hour, nil, 999, userID, false)
    +	setupKey, err := manager.CreateSetupKey(context.Background(), account.Id, "test-key", types.SetupKeyReusable, time.Hour, nil, 999, userID, false, false)
     	if err != nil {
     		t.Fatal("error creating setup key")
     		return
    @@ -1456,7 +1456,7 @@ func TestAccountManager_DeletePeer(t *testing.T) {
     		t.Fatal(err)
     	}
     
    -	setupKey, err := manager.CreateSetupKey(context.Background(), account.Id, "test-key", types.SetupKeyReusable, time.Hour, nil, 999, userID, false)
    +	setupKey, err := manager.CreateSetupKey(context.Background(), account.Id, "test-key", types.SetupKeyReusable, time.Hour, nil, 999, userID, false, false)
     	if err != nil {
     		t.Fatal("error creating setup key")
     		return
    @@ -2948,7 +2948,7 @@ func setupNetworkMapTest(t *testing.T) (*DefaultAccountManager, *types.Account,
     		t.Fatal(err)
     	}
     
    -	setupKey, err := manager.CreateSetupKey(context.Background(), account.Id, "test-key", types.SetupKeyReusable, time.Hour, nil, 999, userID, false)
    +	setupKey, err := manager.CreateSetupKey(context.Background(), account.Id, "test-key", types.SetupKeyReusable, time.Hour, nil, 999, userID, false, false)
     	if err != nil {
     		t.Fatal("error creating setup key")
     	}
    diff --git a/management/server/grpcserver.go b/management/server/grpcserver.go
    index e8e0c422e..8f5fae3e4 100644
    --- a/management/server/grpcserver.go
    +++ b/management/server/grpcserver.go
    @@ -481,6 +481,7 @@ func (s *GRPCServer) Login(ctx context.Context, req *proto.EncryptedMessage) (*p
     		UserID:          userID,
     		SetupKey:        loginReq.GetSetupKey(),
     		ConnectionIP:    realIP,
    +		ExtraDNSLabels:  loginReq.GetDnsLabels(),
     	})
     	if err != nil {
     		log.WithContext(ctx).Warnf("failed logging in peer %s: %s", peerKey, err)
    diff --git a/management/server/http/api/openapi.yml b/management/server/http/api/openapi.yml
    index f53092415..83f45ef91 100644
    --- a/management/server/http/api/openapi.yml
    +++ b/management/server/http/api/openapi.yml
    @@ -361,6 +361,12 @@ components:
                   description: System serial number
                   type: string
                   example: "C02XJ0J0JGH7"
    +            extra_dns_labels:
    +              description: Extra DNS labels added to the peer
    +              type: array
    +              items:
    +                type: string
    +                example: "stage-host-1"
               required:
                 - city_name
                 - connected
    @@ -384,6 +390,7 @@ components:
                 - ui_version
                 - approval_required
                 - serial_number
    +            - extra_dns_labels
         AccessiblePeer:
           allOf:
             - $ref: '#/components/schemas/PeerMinimum'
    @@ -503,6 +510,10 @@ components:
               description: Indicate that the peer will be ephemeral or not
               type: boolean
               example: true
    +        allow_extra_dns_labels:
    +          description: Allow extra DNS labels to be added to the peer
    +          type: boolean
    +          example: true
           required:
             - id
             - key
    @@ -518,6 +529,7 @@ components:
             - updated_at
             - usage_limit
             - ephemeral
    +        - allow_extra_dns_labels
         SetupKeyClear:
           allOf:
             - $ref: '#/components/schemas/SetupKeyBase'
    @@ -587,6 +599,10 @@ components:
               description: Indicate that the peer will be ephemeral or not
               type: boolean
               example: true
    +        allow_extra_dns_labels:
    +          description: Allow extra DNS labels to be added to the peer
    +          type: boolean
    +          example: true
           required:
             - name
             - type
    diff --git a/management/server/http/api/types.gen.go b/management/server/http/api/types.gen.go
    index 943d1b327..eb57d5d66 100644
    --- a/management/server/http/api/types.gen.go
    +++ b/management/server/http/api/types.gen.go
    @@ -297,6 +297,9 @@ type CountryCode = string
     
     // CreateSetupKeyRequest defines model for CreateSetupKeyRequest.
     type CreateSetupKeyRequest struct {
    +	// AllowExtraDnsLabels Allow extra DNS labels to be added to the peer
    +	AllowExtraDnsLabels *bool `json:"allow_extra_dns_labels,omitempty"`
    +
     	// AutoGroups List of group IDs to auto-assign to peers registered with this key
     	AutoGroups []string `json:"auto_groups"`
     
    @@ -689,6 +692,9 @@ type Peer struct {
     	// DnsLabel Peer's DNS label is the parsed peer name for domain resolution. It is used to form an FQDN by appending the account's domain to the peer label. e.g. peer-dns-label.netbird.cloud
     	DnsLabel string `json:"dns_label"`
     
    +	// ExtraDnsLabels Extra DNS labels added to the peer
    +	ExtraDnsLabels []string `json:"extra_dns_labels"`
    +
     	// GeonameId Unique identifier from the GeoNames database for a specific geographical location.
     	GeonameId int `json:"geoname_id"`
     
    @@ -767,6 +773,9 @@ type PeerBatch struct {
     	// DnsLabel Peer's DNS label is the parsed peer name for domain resolution. It is used to form an FQDN by appending the account's domain to the peer label. e.g. peer-dns-label.netbird.cloud
     	DnsLabel string `json:"dns_label"`
     
    +	// ExtraDnsLabels Extra DNS labels added to the peer
    +	ExtraDnsLabels []string `json:"extra_dns_labels"`
    +
     	// GeonameId Unique identifier from the GeoNames database for a specific geographical location.
     	GeonameId int `json:"geoname_id"`
     
    @@ -1230,6 +1239,9 @@ type RulePortRange struct {
     
     // SetupKey defines model for SetupKey.
     type SetupKey struct {
    +	// AllowExtraDnsLabels Allow extra DNS labels to be added to the peer
    +	AllowExtraDnsLabels bool `json:"allow_extra_dns_labels"`
    +
     	// AutoGroups List of group IDs to auto-assign to peers registered with this key
     	AutoGroups []string `json:"auto_groups"`
     
    @@ -1275,6 +1287,9 @@ type SetupKey struct {
     
     // SetupKeyBase defines model for SetupKeyBase.
     type SetupKeyBase struct {
    +	// AllowExtraDnsLabels Allow extra DNS labels to be added to the peer
    +	AllowExtraDnsLabels bool `json:"allow_extra_dns_labels"`
    +
     	// AutoGroups List of group IDs to auto-assign to peers registered with this key
     	AutoGroups []string `json:"auto_groups"`
     
    @@ -1317,6 +1332,9 @@ type SetupKeyBase struct {
     
     // SetupKeyClear defines model for SetupKeyClear.
     type SetupKeyClear struct {
    +	// AllowExtraDnsLabels Allow extra DNS labels to be added to the peer
    +	AllowExtraDnsLabels bool `json:"allow_extra_dns_labels"`
    +
     	// AutoGroups List of group IDs to auto-assign to peers registered with this key
     	AutoGroups []string `json:"auto_groups"`
     
    diff --git a/management/server/http/handlers/peers/peers_handler.go b/management/server/http/handlers/peers/peers_handler.go
    index cdd8026f2..26153d0a1 100644
    --- a/management/server/http/handlers/peers/peers_handler.go
    +++ b/management/server/http/handlers/peers/peers_handler.go
    @@ -338,6 +338,7 @@ func toSinglePeerResponse(peer *nbpeer.Peer, groupsInfo []api.GroupMinimum, dnsD
     		UserId:                      peer.UserID,
     		UiVersion:                   peer.Meta.UIVersion,
     		DnsLabel:                    fqdn(peer, dnsDomain),
    +		ExtraDnsLabels:              fqdnList(peer.ExtraDNSLabels, dnsDomain),
     		LoginExpirationEnabled:      peer.LoginExpirationEnabled,
     		LastLogin:                   peer.GetLastLogin(),
     		LoginExpired:                peer.Status.LoginExpired,
    @@ -372,6 +373,7 @@ func toPeerListItemResponse(peer *nbpeer.Peer, groupsInfo []api.GroupMinimum, dn
     		UserId:                 peer.UserID,
     		UiVersion:              peer.Meta.UIVersion,
     		DnsLabel:               fqdn(peer, dnsDomain),
    +		ExtraDnsLabels:         fqdnList(peer.ExtraDNSLabels, dnsDomain),
     		LoginExpirationEnabled: peer.LoginExpirationEnabled,
     		LastLogin:              peer.GetLastLogin(),
     		LoginExpired:           peer.Status.LoginExpired,
    @@ -392,3 +394,11 @@ func fqdn(peer *nbpeer.Peer, dnsDomain string) string {
     		return fqdn
     	}
     }
    +func fqdnList(extraLabels []string, dnsDomain string) []string {
    +	fqdnList := make([]string, 0, len(extraLabels))
    +	for _, label := range extraLabels {
    +		fqdn := fmt.Sprintf("%s.%s", label, dnsDomain)
    +		fqdnList = append(fqdnList, fqdn)
    +	}
    +	return fqdnList
    +}
    diff --git a/management/server/http/handlers/routes/routes_handler.go b/management/server/http/handlers/routes/routes_handler.go
    index a29ba4562..6b6c37910 100644
    --- a/management/server/http/handlers/routes/routes_handler.go
    +++ b/management/server/http/handlers/routes/routes_handler.go
    @@ -2,11 +2,8 @@ package routes
     
     import (
     	"encoding/json"
    -	"fmt"
     	"net/http"
     	"net/netip"
    -	"regexp"
    -	"strings"
     	"unicode/utf8"
     
     	"github.com/gorilla/mux"
    @@ -21,7 +18,6 @@ import (
     	"github.com/netbirdio/netbird/route"
     )
     
    -const maxDomains = 32
     const failedToConvertRoute = "failed to convert route to response: %v"
     
     // handler is the routes handler of the account
    @@ -102,7 +98,7 @@ func (h *handler) createRoute(w http.ResponseWriter, r *http.Request) {
     	var networkType route.NetworkType
     	var newPrefix netip.Prefix
     	if req.Domains != nil {
    -		d, err := validateDomains(*req.Domains)
    +		d, err := domain.ValidateDomains(*req.Domains)
     		if err != nil {
     			util.WriteError(r.Context(), status.Errorf(status.InvalidArgument, "invalid domains: %v", err), w)
     			return
    @@ -225,7 +221,7 @@ func (h *handler) updateRoute(w http.ResponseWriter, r *http.Request) {
     	}
     
     	if req.Domains != nil {
    -		d, err := validateDomains(*req.Domains)
    +		d, err := domain.ValidateDomains(*req.Domains)
     		if err != nil {
     			util.WriteError(r.Context(), status.Errorf(status.InvalidArgument, "invalid domains: %v", err), w)
     			return
    @@ -350,34 +346,3 @@ func toRouteResponse(serverRoute *route.Route) (*api.Route, error) {
     	}
     	return route, nil
     }
    -
    -// validateDomains checks if each domain in the list is valid and returns a punycode-encoded DomainList.
    -func validateDomains(domains []string) (domain.List, error) {
    -	if len(domains) == 0 {
    -		return nil, fmt.Errorf("domains list is empty")
    -	}
    -	if len(domains) > maxDomains {
    -		return nil, fmt.Errorf("domains list exceeds maximum allowed domains: %d", maxDomains)
    -	}
    -
    -	domainRegex := regexp.MustCompile(`^(?:\*\.)?(?:(?:xn--)?[a-zA-Z0-9_](?:[a-zA-Z0-9-_]{0,61}[a-zA-Z0-9])?\.)*(?:xn--)?[a-zA-Z0-9](?:[a-zA-Z0-9-_]{0,61}[a-zA-Z0-9])?$`)
    -
    -	var domainList domain.List
    -
    -	for _, d := range domains {
    -		d := strings.ToLower(d)
    -
    -		// handles length and idna conversion
    -		punycode, err := domain.FromString(d)
    -		if err != nil {
    -			return domainList, fmt.Errorf("failed to convert domain to punycode: %s: %v", d, err)
    -		}
    -
    -		if !domainRegex.MatchString(string(punycode)) {
    -			return domainList, fmt.Errorf("invalid domain format: %s", d)
    -		}
    -
    -		domainList = append(domainList, punycode)
    -	}
    -	return domainList, nil
    -}
    diff --git a/management/server/http/handlers/routes/routes_handler_test.go b/management/server/http/handlers/routes/routes_handler_test.go
    index 4064ec361..f3bd79ee4 100644
    --- a/management/server/http/handlers/routes/routes_handler_test.go
    +++ b/management/server/http/handlers/routes/routes_handler_test.go
    @@ -561,96 +561,6 @@ func TestRoutesHandlers(t *testing.T) {
     	}
     }
     
    -func TestValidateDomains(t *testing.T) {
    -	tests := []struct {
    -		name     string
    -		domains  []string
    -		expected domain.List
    -		wantErr  bool
    -	}{
    -		{
    -			name:     "Empty list",
    -			domains:  nil,
    -			expected: nil,
    -			wantErr:  true,
    -		},
    -		{
    -			name:     "Valid ASCII domain",
    -			domains:  []string{"sub.ex-ample.com"},
    -			expected: domain.List{"sub.ex-ample.com"},
    -			wantErr:  false,
    -		},
    -		{
    -			name:     "Valid Unicode domain",
    -			domains:  []string{"münchen.de"},
    -			expected: domain.List{"xn--mnchen-3ya.de"},
    -			wantErr:  false,
    -		},
    -		{
    -			name:     "Valid Unicode, all labels",
    -			domains:  []string{"中国.中国.中国"},
    -			expected: domain.List{"xn--fiqs8s.xn--fiqs8s.xn--fiqs8s"},
    -			wantErr:  false,
    -		},
    -		{
    -			name:     "With underscores",
    -			domains:  []string{"_jabber._tcp.gmail.com"},
    -			expected: domain.List{"_jabber._tcp.gmail.com"},
    -			wantErr:  false,
    -		},
    -		{
    -			name:     "Invalid domain format",
    -			domains:  []string{"-example.com"},
    -			expected: nil,
    -			wantErr:  true,
    -		},
    -		{
    -			name:     "Invalid domain format 2",
    -			domains:  []string{"example.com-"},
    -			expected: nil,
    -			wantErr:  true,
    -		},
    -		{
    -			name:     "Multiple domains valid and invalid",
    -			domains:  []string{"google.com", "invalid,nbdomain.com", "münchen.de"},
    -			expected: domain.List{"google.com"},
    -			wantErr:  true,
    -		},
    -		{
    -			name:     "Valid wildcard domain",
    -			domains:  []string{"*.example.com"},
    -			expected: domain.List{"*.example.com"},
    -			wantErr:  false,
    -		},
    -		{
    -			name:     "Wildcard with dot domain",
    -			domains:  []string{".*.example.com"},
    -			expected: nil,
    -			wantErr:  true,
    -		},
    -		{
    -			name:     "Wildcard with dot domain",
    -			domains:  []string{".*.example.com"},
    -			expected: nil,
    -			wantErr:  true,
    -		},
    -		{
    -			name:     "Invalid wildcard domain",
    -			domains:  []string{"a.*.example.com"},
    -			expected: nil,
    -			wantErr:  true,
    -		},
    -	}
    -
    -	for _, tt := range tests {
    -		t.Run(tt.name, func(t *testing.T) {
    -			got, err := validateDomains(tt.domains)
    -			assert.Equal(t, tt.wantErr, err != nil)
    -			assert.Equal(t, got, tt.expected)
    -		})
    -	}
    -}
    -
     func toApiRoute(t *testing.T, r *route.Route) *api.Route {
     	t.Helper()
     
    diff --git a/management/server/http/handlers/setup_keys/setupkeys_handler.go b/management/server/http/handlers/setup_keys/setupkeys_handler.go
    index 67e296901..3bd3ef589 100644
    --- a/management/server/http/handlers/setup_keys/setupkeys_handler.go
    +++ b/management/server/http/handlers/setup_keys/setupkeys_handler.go
    @@ -3,6 +3,7 @@ package setup_keys
     import (
     	"context"
     	"encoding/json"
    +
     	"net/http"
     	"time"
     
    @@ -86,8 +87,13 @@ func (h *handler) createSetupKey(w http.ResponseWriter, r *http.Request) {
     		ephemeral = *req.Ephemeral
     	}
     
    +	var allowExtraDNSLabels bool
    +	if req.AllowExtraDnsLabels != nil {
    +		allowExtraDNSLabels = *req.AllowExtraDnsLabels
    +	}
    +
     	setupKey, err := h.accountManager.CreateSetupKey(r.Context(), accountID, req.Name, types.SetupKeyType(req.Type), expiresIn,
    -		req.AutoGroups, req.UsageLimit, userID, ephemeral)
    +		req.AutoGroups, req.UsageLimit, userID, ephemeral, allowExtraDNSLabels)
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
    @@ -237,19 +243,20 @@ func ToResponseBody(key *types.SetupKey) *api.SetupKey {
     	}
     
     	return &api.SetupKey{
    -		Id:         key.Id,
    -		Key:        key.KeySecret,
    -		Name:       key.Name,
    -		Expires:    key.GetExpiresAt(),
    -		Type:       string(key.Type),
    -		Valid:      key.IsValid(),
    -		Revoked:    key.Revoked,
    -		UsedTimes:  key.UsedTimes,
    -		LastUsed:   key.GetLastUsed(),
    -		State:      state,
    -		AutoGroups: key.AutoGroups,
    -		UpdatedAt:  key.UpdatedAt,
    -		UsageLimit: key.UsageLimit,
    -		Ephemeral:  key.Ephemeral,
    +		Id:                  key.Id,
    +		Key:                 key.KeySecret,
    +		Name:                key.Name,
    +		Expires:             key.GetExpiresAt(),
    +		Type:                string(key.Type),
    +		Valid:               key.IsValid(),
    +		Revoked:             key.Revoked,
    +		UsedTimes:           key.UsedTimes,
    +		LastUsed:            key.GetLastUsed(),
    +		State:               state,
    +		AutoGroups:          key.AutoGroups,
    +		UpdatedAt:           key.UpdatedAt,
    +		UsageLimit:          key.UsageLimit,
    +		Ephemeral:           key.Ephemeral,
    +		AllowExtraDnsLabels: key.AllowExtraDNSLabels,
     	}
     }
    diff --git a/management/server/http/handlers/setup_keys/setupkeys_handler_test.go b/management/server/http/handlers/setup_keys/setupkeys_handler_test.go
    index f56227c10..4912f9639 100644
    --- a/management/server/http/handlers/setup_keys/setupkeys_handler_test.go
    +++ b/management/server/http/handlers/setup_keys/setupkeys_handler_test.go
    @@ -37,11 +37,12 @@ func initSetupKeysTestMetaData(defaultKey *types.SetupKey, newKey *types.SetupKe
     				return claims.AccountId, claims.UserId, nil
     			},
     			CreateSetupKeyFunc: func(_ context.Context, _ string, keyName string, typ types.SetupKeyType, _ time.Duration, _ []string,
    -				_ int, _ string, ephemeral bool,
    +				_ int, _ string, ephemeral bool, allowExtraDNSLabels bool,
     			) (*types.SetupKey, error) {
     				if keyName == newKey.Name || typ != newKey.Type {
     					nk := newKey.Copy()
     					nk.Ephemeral = ephemeral
    +					nk.AllowExtraDNSLabels = allowExtraDNSLabels
     					return nk, nil
     				}
     				return nil, fmt.Errorf("failed creating setup key")
    @@ -94,7 +95,7 @@ func TestSetupKeysHandlers(t *testing.T) {
     	adminUser := types.NewAdminUser("test_user")
     
     	newSetupKey, plainKey := types.GenerateSetupKey(newSetupKeyName, types.SetupKeyReusable, 0, []string{"group-1"},
    -		types.SetupKeyUnlimitedUsage, true)
    +		types.SetupKeyUnlimitedUsage, true, false)
     	newSetupKey.Key = plainKey
     	updatedDefaultSetupKey := defaultSetupKey.Copy()
     	updatedDefaultSetupKey.AutoGroups = []string{"group-1"}
    diff --git a/management/server/management_proto_test.go b/management/server/management_proto_test.go
    index bcdf75b8c..9c2ce5ad2 100644
    --- a/management/server/management_proto_test.go
    +++ b/management/server/management_proto_test.go
    @@ -714,7 +714,7 @@ func Test_LoginPerformance(t *testing.T) {
     						return
     					}
     
    -					setupKey, err := am.CreateSetupKey(context.Background(), account.Id, fmt.Sprintf("key-%d", j), types.SetupKeyReusable, time.Hour, nil, 0, fmt.Sprintf("user-%d", j), false)
    +					setupKey, err := am.CreateSetupKey(context.Background(), account.Id, fmt.Sprintf("key-%d", j), types.SetupKeyReusable, time.Hour, nil, 0, fmt.Sprintf("user-%d", j), false, false)
     					if err != nil {
     						t.Logf("error creating setup key: %v", err)
     						return
    diff --git a/management/server/mock_server/account_mock.go b/management/server/mock_server/account_mock.go
    index b20eb87bb..b2a90f156 100644
    --- a/management/server/mock_server/account_mock.go
    +++ b/management/server/mock_server/account_mock.go
    @@ -25,7 +25,7 @@ type MockAccountManager struct {
     	GetOrCreateAccountByUserFunc func(ctx context.Context, userId, domain string) (*types.Account, error)
     	GetAccountFunc               func(ctx context.Context, accountID string) (*types.Account, error)
     	CreateSetupKeyFunc           func(ctx context.Context, accountId string, keyName string, keyType types.SetupKeyType,
    -		expiresIn time.Duration, autoGroups []string, usageLimit int, userID string, ephemeral bool) (*types.SetupKey, error)
    +		expiresIn time.Duration, autoGroups []string, usageLimit int, userID string, ephemeral bool, allowExtraDNSLabels bool) (*types.SetupKey, error)
     	GetSetupKeyFunc                     func(ctx context.Context, accountID, userID, keyID string) (*types.SetupKey, error)
     	AccountExistsFunc                   func(ctx context.Context, accountID string) (bool, error)
     	GetAccountIDByUserIdFunc            func(ctx context.Context, userId, domain string) (string, error)
    @@ -205,9 +205,10 @@ func (am *MockAccountManager) CreateSetupKey(
     	usageLimit int,
     	userID string,
     	ephemeral bool,
    +	allowExtraDNSLabels bool,
     ) (*types.SetupKey, error) {
     	if am.CreateSetupKeyFunc != nil {
    -		return am.CreateSetupKeyFunc(ctx, accountID, keyName, keyType, expiresIn, autoGroups, usageLimit, userID, ephemeral)
    +		return am.CreateSetupKeyFunc(ctx, accountID, keyName, keyType, expiresIn, autoGroups, usageLimit, userID, ephemeral, allowExtraDNSLabels)
     	}
     	return nil, status.Errorf(codes.Unimplemented, "method CreateSetupKey is not implemented")
     }
    diff --git a/management/server/peer.go b/management/server/peer.go
    index efd9c64e3..c9b0fcfee 100644
    --- a/management/server/peer.go
    +++ b/management/server/peer.go
    @@ -15,6 +15,7 @@ import (
     	log "github.com/sirupsen/logrus"
     	"golang.org/x/exp/maps"
     
    +	"github.com/netbirdio/netbird/management/domain"
     	"github.com/netbirdio/netbird/management/server/geolocation"
     
     	"github.com/netbirdio/netbird/management/server/idp"
    @@ -53,6 +54,9 @@ type PeerLogin struct {
     	SetupKey string
     	// ConnectionIP is the real IP of the peer
     	ConnectionIP net.IP
    +
    +	// ExtraDNSLabels is a list of extra DNS labels that the peer wants to use
    +	ExtraDNSLabels []string
     }
     
     // GetPeers returns a list of peers under the given account filtering out peers that do not belong to a user if
    @@ -502,6 +506,7 @@ func (am *DefaultAccountManager) AddPeer(ctx context.Context, setupKey, userID s
     		var setupKeyName string
     		var ephemeral bool
     		var groupsToAdd []string
    +		var allowExtraDNSLabels bool
     		if addedByUser {
     			user, err := transaction.GetUserByUserID(ctx, store.LockingStrengthUpdate, userID)
     			if err != nil {
    @@ -527,6 +532,11 @@ func (am *DefaultAccountManager) AddPeer(ctx context.Context, setupKey, userID s
     			ephemeral = sk.Ephemeral
     			setupKeyID = sk.Id
     			setupKeyName = sk.Name
    +			allowExtraDNSLabels = sk.AllowExtraDNSLabels
    +
    +			if !sk.AllowExtraDNSLabels && len(peer.ExtraDNSLabels) > 0 {
    +				return status.Errorf(status.PreconditionFailed, "couldn't add peer: setup key doesn't allow extra DNS labels")
    +			}
     		}
     
     		if (strings.ToLower(peer.Meta.Hostname) == "iphone" || strings.ToLower(peer.Meta.Hostname) == "ipad") && userID != "" {
    @@ -567,6 +577,8 @@ func (am *DefaultAccountManager) AddPeer(ctx context.Context, setupKey, userID s
     			Ephemeral:                   ephemeral,
     			Location:                    peer.Location,
     			InactivityExpirationEnabled: addedByUser,
    +			ExtraDNSLabels:              peer.ExtraDNSLabels,
    +			AllowExtraDNSLabels:         allowExtraDNSLabels,
     		}
     		opEvent.TargetID = newPeer.ID
     		opEvent.Meta = newPeer.EventMeta(am.GetDNSDomain())
    @@ -860,6 +872,20 @@ func (am *DefaultAccountManager) LoginPeer(ctx context.Context, login PeerLogin)
     			shouldStorePeer = true
     		}
     
    +		if !peer.AllowExtraDNSLabels && len(login.ExtraDNSLabels) > 0 {
    +			return status.Errorf(status.PreconditionFailed, "couldn't login peer: setup key doesn't allow extra DNS labels")
    +		}
    +
    +		extraLabels, err := domain.ValidateDomainsStrSlice(login.ExtraDNSLabels)
    +		if err != nil {
    +			return status.Errorf(status.InvalidArgument, "invalid extra DNS labels: %v", err)
    +		}
    +
    +		if !slices.Equal(peer.ExtraDNSLabels, extraLabels) {
    +			peer.ExtraDNSLabels = extraLabels
    +			shouldStorePeer = true
    +		}
    +
     		if shouldStorePeer {
     			if err = transaction.SavePeer(ctx, store.LockingStrengthUpdate, accountID, peer); err != nil {
     				return err
    diff --git a/management/server/peer/peer.go b/management/server/peer/peer.go
    index 199c7c89d..afda55d17 100644
    --- a/management/server/peer/peer.go
    +++ b/management/server/peer/peer.go
    @@ -49,6 +49,11 @@ type Peer struct {
     	Ephemeral bool `gorm:"index"`
     	// Geo location based on connection IP
     	Location Location `gorm:"embedded;embeddedPrefix:location_"`
    +
    +	// ExtraDNSLabels is a list of additional DNS labels that can be used to resolve the peer
    +	ExtraDNSLabels []string `gorm:"serializer:json"`
    +	// AllowExtraDNSLabels indicates whether the peer allows extra DNS labels to be used for resolving the peer
    +	AllowExtraDNSLabels bool
     }
     
     type PeerStatus struct { //nolint:revive
    @@ -202,6 +207,8 @@ func (p *Peer) Copy() *Peer {
     		Ephemeral:                   p.Ephemeral,
     		Location:                    p.Location,
     		InactivityExpirationEnabled: p.InactivityExpirationEnabled,
    +		ExtraDNSLabels:              slices.Clone(p.ExtraDNSLabels),
    +		AllowExtraDNSLabels:         p.AllowExtraDNSLabels,
     	}
     }
     
    diff --git a/management/server/peer_test.go b/management/server/peer_test.go
    index 6894d092d..9deb8e456 100644
    --- a/management/server/peer_test.go
    +++ b/management/server/peer_test.go
    @@ -168,7 +168,7 @@ func TestAccountManager_GetNetworkMap(t *testing.T) {
     		t.Fatal(err)
     	}
     
    -	setupKey, err := manager.CreateSetupKey(context.Background(), account.Id, "test-key", types.SetupKeyReusable, time.Hour, nil, 999, userId, false)
    +	setupKey, err := manager.CreateSetupKey(context.Background(), account.Id, "test-key", types.SetupKeyReusable, time.Hour, nil, 999, userId, false, false)
     	if err != nil {
     		t.Fatal("error creating setup key")
     		return
    @@ -417,7 +417,7 @@ func TestAccountManager_GetPeerNetwork(t *testing.T) {
     		t.Fatal(err)
     	}
     
    -	setupKey, err := manager.CreateSetupKey(context.Background(), account.Id, "test-key", types.SetupKeyReusable, time.Hour, nil, 999, userId, false)
    +	setupKey, err := manager.CreateSetupKey(context.Background(), account.Id, "test-key", types.SetupKeyReusable, time.Hour, nil, 999, userId, false, false)
     	if err != nil {
     		t.Fatal("error creating setup key")
     		return
    @@ -489,7 +489,7 @@ func TestDefaultAccountManager_GetPeer(t *testing.T) {
     	}
     
     	// two peers one added by a regular user and one with a setup key
    -	setupKey, err := manager.CreateSetupKey(context.Background(), account.Id, "test-key", types.SetupKeyReusable, time.Hour, nil, 999, adminUser, false)
    +	setupKey, err := manager.CreateSetupKey(context.Background(), account.Id, "test-key", types.SetupKeyReusable, time.Hour, nil, 999, adminUser, false, false)
     	if err != nil {
     		t.Fatal("error creating setup key")
     		return
    diff --git a/management/server/setupkey.go b/management/server/setupkey.go
    index f2f1aad45..b0bdad4e5 100644
    --- a/management/server/setupkey.go
    +++ b/management/server/setupkey.go
    @@ -52,7 +52,7 @@ type SetupKeyUpdateOperation struct {
     // CreateSetupKey generates a new setup key with a given name, type, list of groups IDs to auto-assign to peers registered with this key,
     // and adds it to the specified account. A list of autoGroups IDs can be empty.
     func (am *DefaultAccountManager) CreateSetupKey(ctx context.Context, accountID string, keyName string, keyType types.SetupKeyType,
    -	expiresIn time.Duration, autoGroups []string, usageLimit int, userID string, ephemeral bool) (*types.SetupKey, error) {
    +	expiresIn time.Duration, autoGroups []string, usageLimit int, userID string, ephemeral bool, allowExtraDNSLabels bool) (*types.SetupKey, error) {
     	unlock := am.Store.AcquireWriteLockByUID(ctx, accountID)
     	defer unlock()
     
    @@ -78,7 +78,7 @@ func (am *DefaultAccountManager) CreateSetupKey(ctx context.Context, accountID s
     			return status.Errorf(status.InvalidArgument, "invalid auto groups: %v", err)
     		}
     
    -		setupKey, plainKey = types.GenerateSetupKey(keyName, keyType, expiresIn, autoGroups, usageLimit, ephemeral)
    +		setupKey, plainKey = types.GenerateSetupKey(keyName, keyType, expiresIn, autoGroups, usageLimit, ephemeral, allowExtraDNSLabels)
     		setupKey.AccountID = accountID
     
     		events := am.prepareSetupKeyEvents(ctx, transaction, accountID, userID, autoGroups, nil, setupKey)
    diff --git a/management/server/setupkey_test.go b/management/server/setupkey_test.go
    index e225ec54b..6e1e1cf7d 100644
    --- a/management/server/setupkey_test.go
    +++ b/management/server/setupkey_test.go
    @@ -50,7 +50,7 @@ func TestDefaultAccountManager_SaveSetupKey(t *testing.T) {
     	keyName := "my-test-key"
     
     	key, err := manager.CreateSetupKey(context.Background(), account.Id, keyName, types.SetupKeyReusable, expiresIn, []string{},
    -		types.SetupKeyUnlimitedUsage, userID, false)
    +		types.SetupKeyUnlimitedUsage, userID, false, false)
     	if err != nil {
     		t.Fatal(err)
     	}
    @@ -168,7 +168,7 @@ func TestDefaultAccountManager_CreateSetupKey(t *testing.T) {
     	for _, tCase := range []testCase{testCase1, testCase2, testCase3} {
     		t.Run(tCase.name, func(t *testing.T) {
     			key, err := manager.CreateSetupKey(context.Background(), account.Id, tCase.expectedKeyName, types.SetupKeyReusable, expiresIn,
    -				tCase.expectedGroups, types.SetupKeyUnlimitedUsage, userID, false)
    +				tCase.expectedGroups, types.SetupKeyUnlimitedUsage, userID, false, false)
     
     			if tCase.expectedFailure {
     				if err == nil {
    @@ -210,7 +210,7 @@ func TestGetSetupKeys(t *testing.T) {
     		t.Fatal(err)
     	}
     
    -	plainKey, err := manager.CreateSetupKey(context.Background(), account.Id, "key1", types.SetupKeyReusable, time.Hour, nil, types.SetupKeyUnlimitedUsage, userID, false)
    +	plainKey, err := manager.CreateSetupKey(context.Background(), account.Id, "key1", types.SetupKeyReusable, time.Hour, nil, types.SetupKeyUnlimitedUsage, userID, false, false)
     	if err != nil {
     		t.Fatal(err)
     	}
    @@ -275,7 +275,7 @@ func TestGenerateSetupKey(t *testing.T) {
     	expectedUpdatedAt := time.Now().UTC()
     	var expectedAutoGroups []string
     
    -	key, plain := types.GenerateSetupKey(expectedName, types.SetupKeyOneOff, time.Hour, []string{}, types.SetupKeyUnlimitedUsage, false)
    +	key, plain := types.GenerateSetupKey(expectedName, types.SetupKeyOneOff, time.Hour, []string{}, types.SetupKeyUnlimitedUsage, false, false)
     
     	assertKey(t, key, expectedName, expectedRevoke, expectedType, expectedUsedTimes, expectedCreatedAt,
     		expectedExpiresAt, strconv.Itoa(int(types.Hash(plain))), expectedUpdatedAt, expectedAutoGroups, true)
    @@ -283,33 +283,33 @@ func TestGenerateSetupKey(t *testing.T) {
     }
     
     func TestSetupKey_IsValid(t *testing.T) {
    -	validKey, _ := types.GenerateSetupKey("valid key", types.SetupKeyOneOff, time.Hour, []string{}, types.SetupKeyUnlimitedUsage, false)
    +	validKey, _ := types.GenerateSetupKey("valid key", types.SetupKeyOneOff, time.Hour, []string{}, types.SetupKeyUnlimitedUsage, false, false)
     	if !validKey.IsValid() {
     		t.Errorf("expected key to be valid, got invalid %v", validKey)
     	}
     
     	// expired
    -	expiredKey, _ := types.GenerateSetupKey("invalid key", types.SetupKeyOneOff, -time.Hour, []string{}, types.SetupKeyUnlimitedUsage, false)
    +	expiredKey, _ := types.GenerateSetupKey("invalid key", types.SetupKeyOneOff, -time.Hour, []string{}, types.SetupKeyUnlimitedUsage, false, false)
     	if expiredKey.IsValid() {
     		t.Errorf("expected key to be invalid due to expiration, got valid %v", expiredKey)
     	}
     
     	// revoked
    -	revokedKey, _ := types.GenerateSetupKey("invalid key", types.SetupKeyOneOff, time.Hour, []string{}, types.SetupKeyUnlimitedUsage, false)
    +	revokedKey, _ := types.GenerateSetupKey("invalid key", types.SetupKeyOneOff, time.Hour, []string{}, types.SetupKeyUnlimitedUsage, false, false)
     	revokedKey.Revoked = true
     	if revokedKey.IsValid() {
     		t.Errorf("expected revoked key to be invalid, got valid %v", revokedKey)
     	}
     
     	// overused
    -	overUsedKey, _ := types.GenerateSetupKey("invalid key", types.SetupKeyOneOff, time.Hour, []string{}, types.SetupKeyUnlimitedUsage, false)
    +	overUsedKey, _ := types.GenerateSetupKey("invalid key", types.SetupKeyOneOff, time.Hour, []string{}, types.SetupKeyUnlimitedUsage, false, false)
     	overUsedKey.UsedTimes = 1
     	if overUsedKey.IsValid() {
     		t.Errorf("expected overused key to be invalid, got valid %v", overUsedKey)
     	}
     
     	// overused
    -	reusableKey, _ := types.GenerateSetupKey("valid key", types.SetupKeyReusable, time.Hour, []string{}, types.SetupKeyUnlimitedUsage, false)
    +	reusableKey, _ := types.GenerateSetupKey("valid key", types.SetupKeyReusable, time.Hour, []string{}, types.SetupKeyUnlimitedUsage, false, false)
     	reusableKey.UsedTimes = 99
     	if !reusableKey.IsValid() {
     		t.Errorf("expected reusable key to be valid when used many times, got valid %v", reusableKey)
    @@ -388,7 +388,7 @@ func isValidBase64SHA256(encodedKey string) bool {
     
     func TestSetupKey_Copy(t *testing.T) {
     
    -	key, _ := types.GenerateSetupKey("key name", types.SetupKeyOneOff, time.Hour, []string{}, types.SetupKeyUnlimitedUsage, false)
    +	key, _ := types.GenerateSetupKey("key name", types.SetupKeyOneOff, time.Hour, []string{}, types.SetupKeyUnlimitedUsage, false, false)
     	keyCopy := key.Copy()
     
     	assertKey(t, keyCopy, key.Name, key.Revoked, string(key.Type), key.UsedTimes, key.CreatedAt, key.GetExpiresAt(), key.Id,
    @@ -436,7 +436,7 @@ func TestSetupKeyAccountPeersUpdate(t *testing.T) {
     			close(done)
     		}()
     
    -		setupKey, err = manager.CreateSetupKey(context.Background(), account.Id, "key1", types.SetupKeyReusable, time.Hour, nil, 999, userID, false)
    +		setupKey, err = manager.CreateSetupKey(context.Background(), account.Id, "key1", types.SetupKeyReusable, time.Hour, nil, 999, userID, false, false)
     		assert.NoError(t, err)
     
     		select {
    @@ -477,7 +477,7 @@ func TestDefaultAccountManager_CreateSetupKey_ShouldNotAllowToUpdateRevokedKey(t
     		t.Fatal(err)
     	}
     
    -	key, err := manager.CreateSetupKey(context.Background(), account.Id, "testName", types.SetupKeyReusable, time.Hour, nil, types.SetupKeyUnlimitedUsage, userID, false)
    +	key, err := manager.CreateSetupKey(context.Background(), account.Id, "testName", types.SetupKeyReusable, time.Hour, nil, types.SetupKeyUnlimitedUsage, userID, false, false)
     	assert.NoError(t, err)
     
     	// revoke the key
    diff --git a/management/server/types/account.go b/management/server/types/account.go
    index 0df15816f..4c68b9523 100644
    --- a/management/server/types/account.go
    +++ b/management/server/types/account.go
    @@ -459,8 +459,23 @@ func (a *Account) GetPeersCustomZone(ctx context.Context, dnsDomain string) nbdn
     			TTL:   defaultTTL,
     			RData: peer.IP.String(),
     		})
    -
     		sb.Reset()
    +
    +		for _, extraLabel := range peer.ExtraDNSLabels {
    +			sb.Grow(len(extraLabel) + len(domainSuffix))
    +			sb.WriteString(extraLabel)
    +			sb.WriteString(domainSuffix)
    +
    +			customZone.Records = append(customZone.Records, nbdns.SimpleRecord{
    +				Name:  sb.String(),
    +				Type:  int(dns.TypeA),
    +				Class: nbdns.DefaultClass,
    +				TTL:   defaultTTL,
    +				RData: peer.IP.String(),
    +			})
    +			sb.Reset()
    +		}
    +
     	}
     
     	go func() {
    diff --git a/management/server/types/setupkey.go b/management/server/types/setupkey.go
    index 2cd835289..ab8e46bea 100644
    --- a/management/server/types/setupkey.go
    +++ b/management/server/types/setupkey.go
    @@ -10,6 +10,7 @@ import (
     	"unicode/utf8"
     
     	"github.com/google/uuid"
    +
     	"github.com/netbirdio/netbird/management/server/util"
     )
     
    @@ -54,6 +55,8 @@ type SetupKey struct {
     	UsageLimit int
     	// Ephemeral indicate if the peers will be ephemeral or not
     	Ephemeral bool
    +	// AllowExtraDNSLabels indicates if the key allows extra DNS labels
    +	AllowExtraDNSLabels bool
     }
     
     // Copy copies SetupKey to a new object
    @@ -64,21 +67,22 @@ func (key *SetupKey) Copy() *SetupKey {
     		key.UpdatedAt = key.CreatedAt
     	}
     	return &SetupKey{
    -		Id:         key.Id,
    -		AccountID:  key.AccountID,
    -		Key:        key.Key,
    -		KeySecret:  key.KeySecret,
    -		Name:       key.Name,
    -		Type:       key.Type,
    -		CreatedAt:  key.CreatedAt,
    -		ExpiresAt:  key.ExpiresAt,
    -		UpdatedAt:  key.UpdatedAt,
    -		Revoked:    key.Revoked,
    -		UsedTimes:  key.UsedTimes,
    -		LastUsed:   key.LastUsed,
    -		AutoGroups: autoGroups,
    -		UsageLimit: key.UsageLimit,
    -		Ephemeral:  key.Ephemeral,
    +		Id:                  key.Id,
    +		AccountID:           key.AccountID,
    +		Key:                 key.Key,
    +		KeySecret:           key.KeySecret,
    +		Name:                key.Name,
    +		Type:                key.Type,
    +		CreatedAt:           key.CreatedAt,
    +		ExpiresAt:           key.ExpiresAt,
    +		UpdatedAt:           key.UpdatedAt,
    +		Revoked:             key.Revoked,
    +		UsedTimes:           key.UsedTimes,
    +		LastUsed:            key.LastUsed,
    +		AutoGroups:          autoGroups,
    +		UsageLimit:          key.UsageLimit,
    +		Ephemeral:           key.Ephemeral,
    +		AllowExtraDNSLabels: key.AllowExtraDNSLabels,
     	}
     }
     
    @@ -150,7 +154,7 @@ func (key *SetupKey) IsOverUsed() bool {
     
     // GenerateSetupKey generates a new setup key
     func GenerateSetupKey(name string, t SetupKeyType, validFor time.Duration, autoGroups []string,
    -	usageLimit int, ephemeral bool) (*SetupKey, string) {
    +	usageLimit int, ephemeral bool, allowExtraDNSLabels bool) (*SetupKey, string) {
     	key := strings.ToUpper(uuid.New().String())
     	limit := usageLimit
     	if t == SetupKeyOneOff {
    @@ -166,26 +170,27 @@ func GenerateSetupKey(name string, t SetupKeyType, validFor time.Duration, autoG
     	encodedHashedKey := b64.StdEncoding.EncodeToString(hashedKey[:])
     
     	return &SetupKey{
    -		Id:         strconv.Itoa(int(Hash(key))),
    -		Key:        encodedHashedKey,
    -		KeySecret:  HiddenKey(key, 4),
    -		Name:       name,
    -		Type:       t,
    -		CreatedAt:  time.Now().UTC(),
    -		ExpiresAt:  expiresAt,
    -		UpdatedAt:  time.Now().UTC(),
    -		Revoked:    false,
    -		UsedTimes:  0,
    -		AutoGroups: autoGroups,
    -		UsageLimit: limit,
    -		Ephemeral:  ephemeral,
    +		Id:                  strconv.Itoa(int(Hash(key))),
    +		Key:                 encodedHashedKey,
    +		KeySecret:           HiddenKey(key, 4),
    +		Name:                name,
    +		Type:                t,
    +		CreatedAt:           time.Now().UTC(),
    +		ExpiresAt:           expiresAt,
    +		UpdatedAt:           time.Now().UTC(),
    +		Revoked:             false,
    +		UsedTimes:           0,
    +		AutoGroups:          autoGroups,
    +		UsageLimit:          limit,
    +		Ephemeral:           ephemeral,
    +		AllowExtraDNSLabels: allowExtraDNSLabels,
     	}, key
     }
     
     // GenerateDefaultSetupKey generates a default reusable setup key with an unlimited usage and 30 days expiration
     func GenerateDefaultSetupKey() (*SetupKey, string) {
     	return GenerateSetupKey(DefaultSetupKeyName, SetupKeyReusable, DefaultSetupKeyDuration, []string{},
    -		SetupKeyUnlimitedUsage, false)
    +		SetupKeyUnlimitedUsage, false, false)
     }
     
     func Hash(s string) uint32 {
    
    From 631ef4ed28a5b1fcd4dfe53d645c169deb2882b0 Mon Sep 17 00:00:00 2001
    From: Viktor Liu <17948409+lixmal@users.noreply.github.com>
    Date: Thu, 20 Feb 2025 13:22:03 +0100
    Subject: [PATCH 75/92] [client] Add embeddable library (#3239)
    
    ---
     client/embed/doc.go                        | 167 ++++++++++++
     client/embed/embed.go                      | 296 +++++++++++++++++++++
     client/firewall/uspfilter/uspfilter.go     |  27 +-
     client/iface/device.go                     |   3 +
     client/iface/device/device_android.go      |   5 +
     client/iface/device/device_darwin.go       |   5 +
     client/iface/device/device_ios.go          |   5 +
     client/iface/device/device_kernel_unix.go  |   5 +
     client/iface/device/device_netstack.go     |  24 +-
     client/iface/device/device_usp_unix.go     |   5 +
     client/iface/device/device_windows.go      |   5 +
     client/iface/device_android.go             |   3 +
     client/iface/iface.go                      |   9 +
     client/iface/iface_moc.go                  |   6 +
     client/iface/iwginterface.go               |   2 +
     client/iface/iwginterface_windows.go       |   2 +
     client/iface/netstack/env.go               |   4 +-
     client/iface/netstack/tun.go               |  42 ++-
     client/internal/dns/service_memory.go      |  24 +-
     client/internal/dns/service_memory_test.go |   4 +-
     client/internal/engine.go                  |  36 ++-
     util/net/net.go                            |  20 ++
     22 files changed, 648 insertions(+), 51 deletions(-)
     create mode 100644 client/embed/doc.go
     create mode 100644 client/embed/embed.go
    
    diff --git a/client/embed/doc.go b/client/embed/doc.go
    new file mode 100644
    index 000000000..069d53ebf
    --- /dev/null
    +++ b/client/embed/doc.go
    @@ -0,0 +1,167 @@
    +// Package embed provides a way to embed the NetBird client directly
    +// into Go programs without requiring a separate NetBird client installation.
    +package embed
    +
    +// Basic Usage:
    +//
    +//	client, err := embed.New(embed.Options{
    +//	    DeviceName:    "my-service",
    +//	    SetupKey:      os.Getenv("NB_SETUP_KEY"),
    +//	    ManagementURL: os.Getenv("NB_MANAGEMENT_URL"),
    +//	})
    +//	if err != nil {
    +//	    log.Fatal(err)
    +//	}
    +//
    +//	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    +//	defer cancel()
    +//	if err := client.Start(ctx); err != nil {
    +//	    log.Fatal(err)
    +//	}
    +//
    +// Complete HTTP Server Example:
    +//
    +//	package main
    +//
    +//	import (
    +//	    "context"
    +//	    "fmt"
    +//	    "log"
    +//	    "net/http"
    +//	    "os"
    +//	    "os/signal"
    +//	    "syscall"
    +//	    "time"
    +//
    +//	    netbird "github.com/netbirdio/netbird/client/embed"
    +//	)
    +//
    +//	func main() {
    +//	    // Create client with setup key and device name
    +//	    client, err := netbird.New(netbird.Options{
    +//	        DeviceName:    "http-server",
    +//	        SetupKey:      os.Getenv("NB_SETUP_KEY"),
    +//	        ManagementURL: os.Getenv("NB_MANAGEMENT_URL"),
    +//	        LogOutput:     io.Discard,
    +//	    })
    +//	    if err != nil {
    +//	        log.Fatal(err)
    +//	    }
    +//
    +//	    // Start with timeout
    +//	    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    +//	    defer cancel()
    +//	    if err := client.Start(ctx); err != nil {
    +//	        log.Fatal(err)
    +//	    }
    +//
    +//	    // Create HTTP server
    +//	    mux := http.NewServeMux()
    +//	    mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
    +//	        fmt.Printf("Request from %s: %s %s\n", r.RemoteAddr, r.Method, r.URL.Path)
    +//	        fmt.Fprintf(w, "Hello from netbird!")
    +//	    })
    +//
    +//	    // Listen on netbird network
    +//	    l, err := client.ListenTCP(":8080")
    +//	    if err != nil {
    +//	        log.Fatal(err)
    +//	    }
    +//
    +//	    server := &http.Server{Handler: mux}
    +//	    go func() {
    +//	        if err := server.Serve(l); !errors.Is(err, http.ErrServerClosed) {
    +//	            log.Printf("HTTP server error: %v", err)
    +//	        }
    +//	    }()
    +//
    +//	    log.Printf("HTTP server listening on netbird network port 8080")
    +//
    +//	    // Handle shutdown
    +//	    stop := make(chan os.Signal, 1)
    +//	    signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM)
    +//	    <-stop
    +//
    +//	    shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    +//	    defer cancel()
    +//
    +//	    if err := server.Shutdown(shutdownCtx); err != nil {
    +//	        log.Printf("HTTP shutdown error: %v", err)
    +//	    }
    +//	    if err := client.Stop(shutdownCtx); err != nil {
    +//	        log.Printf("Netbird shutdown error: %v", err)
    +//	    }
    +//	}
    +//
    +// Complete HTTP Client Example:
    +//
    +//	package main
    +//
    +//	import (
    +//	    "context"
    +//	    "fmt"
    +//	    "io"
    +//	    "log"
    +//	    "os"
    +//	    "time"
    +//
    +//	    netbird "github.com/netbirdio/netbird/client/embed"
    +//	)
    +//
    +//	func main() {
    +//	    // Create client with setup key and device name
    +//	    client, err := netbird.New(netbird.Options{
    +//	        DeviceName:    "http-client",
    +//	        SetupKey:      os.Getenv("NB_SETUP_KEY"),
    +//	        ManagementURL: os.Getenv("NB_MANAGEMENT_URL"),
    +//	        LogOutput:     io.Discard,
    +//	    })
    +//	    if err != nil {
    +//	        log.Fatal(err)
    +//	    }
    +//
    +//	    // Start with timeout
    +//	    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    +//	    defer cancel()
    +//
    +//	    if err := client.Start(ctx); err != nil {
    +//	        log.Fatal(err)
    +//	    }
    +//
    +//	    // Create HTTP client that uses netbird network
    +//	    httpClient := client.NewHTTPClient()
    +//	    httpClient.Timeout = 10 * time.Second
    +//
    +//	    // Make request to server in netbird network
    +//	    target := os.Getenv("NB_TARGET")
    +//	    resp, err := httpClient.Get(target)
    +//	    if err != nil {
    +//	        log.Fatal(err)
    +//	    }
    +//	    defer resp.Body.Close()
    +//
    +//	    // Read and print response
    +//	    body, err := io.ReadAll(resp.Body)
    +//	    if err != nil {
    +//	        log.Fatal(err)
    +//	    }
    +//
    +//	    fmt.Printf("Response from server: %s\n", string(body))
    +//
    +//	    // Clean shutdown
    +//	    shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    +//	    defer cancel()
    +//
    +//	    if err := client.Stop(shutdownCtx); err != nil {
    +//	        log.Printf("Netbird shutdown error: %v", err)
    +//	    }
    +//	}
    +//
    +// The package provides several methods for network operations:
    +//   - Dial: Creates outbound connections
    +//   - ListenTCP: Creates TCP listeners
    +//   - ListenUDP: Creates UDP listeners
    +//
    +// By default, the embed package uses userspace networking mode, which doesn't
    +// require root/admin privileges. For production deployments, consider setting
    +// appropriate config and state paths for persistence.
    diff --git a/client/embed/embed.go b/client/embed/embed.go
    new file mode 100644
    index 000000000..9ded618c5
    --- /dev/null
    +++ b/client/embed/embed.go
    @@ -0,0 +1,296 @@
    +package embed
    +
    +import (
    +	"context"
    +	"errors"
    +	"fmt"
    +	"io"
    +	"net"
    +	"net/http"
    +	"net/netip"
    +	"os"
    +	"sync"
    +
    +	"github.com/sirupsen/logrus"
    +	wgnetstack "golang.zx2c4.com/wireguard/tun/netstack"
    +
    +	"github.com/netbirdio/netbird/client/iface/netstack"
    +	"github.com/netbirdio/netbird/client/internal"
    +	"github.com/netbirdio/netbird/client/internal/peer"
    +	"github.com/netbirdio/netbird/client/system"
    +)
    +
    +var ErrClientAlreadyStarted = errors.New("client already started")
    +var ErrClientNotStarted = errors.New("client not started")
    +
    +// Client manages a netbird embedded client instance
    +type Client struct {
    +	deviceName string
    +	config     *internal.Config
    +	mu         sync.Mutex
    +	cancel     context.CancelFunc
    +	setupKey   string
    +	connect    *internal.ConnectClient
    +}
    +
    +// Options configures a new Client
    +type Options struct {
    +	// DeviceName is this peer's name in the network
    +	DeviceName string
    +	// SetupKey is used for authentication
    +	SetupKey string
    +	// ManagementURL overrides the default management server URL
    +	ManagementURL string
    +	// PreSharedKey is the pre-shared key for the WireGuard interface
    +	PreSharedKey string
    +	// LogOutput is the output destination for logs (defaults to os.Stderr if nil)
    +	LogOutput io.Writer
    +	// LogLevel sets the logging level (defaults to info if empty)
    +	LogLevel string
    +	// NoUserspace disables the userspace networking mode. Needs admin/root privileges
    +	NoUserspace bool
    +	// ConfigPath is the path to the netbird config file. If empty, the config will be stored in memory and not persisted.
    +	ConfigPath string
    +	// StatePath is the path to the netbird state file
    +	StatePath string
    +	// DisableClientRoutes disables the client routes
    +	DisableClientRoutes bool
    +}
    +
    +// New creates a new netbird embedded client
    +func New(opts Options) (*Client, error) {
    +	if opts.LogOutput != nil {
    +		logrus.SetOutput(opts.LogOutput)
    +	}
    +
    +	if opts.LogLevel != "" {
    +		level, err := logrus.ParseLevel(opts.LogLevel)
    +		if err != nil {
    +			return nil, fmt.Errorf("parse log level: %w", err)
    +		}
    +		logrus.SetLevel(level)
    +	}
    +
    +	if !opts.NoUserspace {
    +		if err := os.Setenv(netstack.EnvUseNetstackMode, "true"); err != nil {
    +			return nil, fmt.Errorf("setenv: %w", err)
    +		}
    +		if err := os.Setenv(netstack.EnvSkipProxy, "true"); err != nil {
    +			return nil, fmt.Errorf("setenv: %w", err)
    +		}
    +	}
    +
    +	if opts.StatePath != "" {
    +		// TODO: Disable state if path not provided
    +		if err := os.Setenv("NB_DNS_STATE_FILE", opts.StatePath); err != nil {
    +			return nil, fmt.Errorf("setenv: %w", err)
    +		}
    +	}
    +
    +	t := true
    +	var config *internal.Config
    +	var err error
    +	input := internal.ConfigInput{
    +		ConfigPath:          opts.ConfigPath,
    +		ManagementURL:       opts.ManagementURL,
    +		PreSharedKey:        &opts.PreSharedKey,
    +		DisableServerRoutes: &t,
    +		DisableClientRoutes: &opts.DisableClientRoutes,
    +	}
    +	if opts.ConfigPath != "" {
    +		config, err = internal.UpdateOrCreateConfig(input)
    +	} else {
    +		config, err = internal.CreateInMemoryConfig(input)
    +	}
    +	if err != nil {
    +		return nil, fmt.Errorf("create config: %w", err)
    +	}
    +
    +	return &Client{
    +		deviceName: opts.DeviceName,
    +		setupKey:   opts.SetupKey,
    +		config:     config,
    +	}, nil
    +}
    +
    +// Start begins client operation and blocks until the engine has been started successfully or a startup error occurs.
    +// Pass a context with a deadline to limit the time spent waiting for the engine to start.
    +func (c *Client) Start(startCtx context.Context) error {
    +	c.mu.Lock()
    +	defer c.mu.Unlock()
    +	if c.cancel != nil {
    +		return ErrClientAlreadyStarted
    +	}
    +
    +	ctx := internal.CtxInitState(context.Background())
    +	// nolint:staticcheck
    +	ctx = context.WithValue(ctx, system.DeviceNameCtxKey, c.deviceName)
    +	if err := internal.Login(ctx, c.config, c.setupKey, ""); err != nil {
    +		return fmt.Errorf("login: %w", err)
    +	}
    +
    +	recorder := peer.NewRecorder(c.config.ManagementURL.String())
    +	client := internal.NewConnectClient(ctx, c.config, recorder)
    +
    +	// either startup error (permanent backoff err) or nil err (successful engine up)
    +	// TODO: make after-startup backoff err available
    +	run := make(chan error, 1)
    +	go func() {
    +		if err := client.Run(run); err != nil {
    +			run <- err
    +		}
    +	}()
    +
    +	select {
    +	case <-startCtx.Done():
    +		if stopErr := client.Stop(); stopErr != nil {
    +			return fmt.Errorf("stop error after context done. Stop error: %w. Context done: %w", stopErr, startCtx.Err())
    +		}
    +		return startCtx.Err()
    +	case err := <-run:
    +		if err != nil {
    +			if stopErr := client.Stop(); stopErr != nil {
    +				return fmt.Errorf("stop error after failed to startup. Stop error: %w. Start error: %w", stopErr, err)
    +			}
    +			return fmt.Errorf("startup: %w", err)
    +		}
    +	}
    +
    +	c.connect = client
    +
    +	return nil
    +}
    +
    +// Stop gracefully stops the client.
    +// Pass a context with a deadline to limit the time spent waiting for the engine to stop.
    +func (c *Client) Stop(ctx context.Context) error {
    +	c.mu.Lock()
    +	defer c.mu.Unlock()
    +
    +	if c.connect == nil {
    +		return ErrClientNotStarted
    +	}
    +
    +	done := make(chan error, 1)
    +	go func() {
    +		done <- c.connect.Stop()
    +	}()
    +
    +	select {
    +	case <-ctx.Done():
    +		c.cancel = nil
    +		return ctx.Err()
    +	case err := <-done:
    +		c.cancel = nil
    +		if err != nil {
    +			return fmt.Errorf("stop: %w", err)
    +		}
    +		return nil
    +	}
    +}
    +
    +// Dial dials a network address in the netbird network.
    +// Not applicable if the userspace networking mode is disabled.
    +func (c *Client) Dial(ctx context.Context, network, address string) (net.Conn, error) {
    +	c.mu.Lock()
    +	connect := c.connect
    +	if connect == nil {
    +		c.mu.Unlock()
    +		return nil, ErrClientNotStarted
    +	}
    +	c.mu.Unlock()
    +
    +	engine := connect.Engine()
    +	if engine == nil {
    +		return nil, errors.New("engine not started")
    +	}
    +
    +	nsnet, err := engine.GetNet()
    +	if err != nil {
    +		return nil, fmt.Errorf("get net: %w", err)
    +	}
    +
    +	return nsnet.DialContext(ctx, network, address)
    +}
    +
    +// ListenTCP listens on the given address in the netbird network
    +// Not applicable if the userspace networking mode is disabled.
    +func (c *Client) ListenTCP(address string) (net.Listener, error) {
    +	nsnet, addr, err := c.getNet()
    +	if err != nil {
    +		return nil, err
    +	}
    +
    +	_, port, err := net.SplitHostPort(address)
    +	if err != nil {
    +		return nil, fmt.Errorf("split host port: %w", err)
    +	}
    +	listenAddr := fmt.Sprintf("%s:%s", addr, port)
    +
    +	tcpAddr, err := net.ResolveTCPAddr("tcp", listenAddr)
    +	if err != nil {
    +		return nil, fmt.Errorf("resolve: %w", err)
    +	}
    +	return nsnet.ListenTCP(tcpAddr)
    +}
    +
    +// ListenUDP listens on the given address in the netbird network
    +// Not applicable if the userspace networking mode is disabled.
    +func (c *Client) ListenUDP(address string) (net.PacketConn, error) {
    +	nsnet, addr, err := c.getNet()
    +	if err != nil {
    +		return nil, err
    +	}
    +
    +	_, port, err := net.SplitHostPort(address)
    +	if err != nil {
    +		return nil, fmt.Errorf("split host port: %w", err)
    +	}
    +	listenAddr := fmt.Sprintf("%s:%s", addr, port)
    +
    +	udpAddr, err := net.ResolveUDPAddr("udp", listenAddr)
    +	if err != nil {
    +		return nil, fmt.Errorf("resolve: %w", err)
    +	}
    +
    +	return nsnet.ListenUDP(udpAddr)
    +}
    +
    +// NewHTTPClient returns a configured http.Client that uses the netbird network for requests.
    +// Not applicable if the userspace networking mode is disabled.
    +func (c *Client) NewHTTPClient() *http.Client {
    +	transport := &http.Transport{
    +		DialContext: c.Dial,
    +	}
    +
    +	return &http.Client{
    +		Transport: transport,
    +	}
    +}
    +
    +func (c *Client) getNet() (*wgnetstack.Net, netip.Addr, error) {
    +	c.mu.Lock()
    +	connect := c.connect
    +	if connect == nil {
    +		c.mu.Unlock()
    +		return nil, netip.Addr{}, errors.New("client not started")
    +	}
    +	c.mu.Unlock()
    +
    +	engine := connect.Engine()
    +	if engine == nil {
    +		return nil, netip.Addr{}, errors.New("engine not started")
    +	}
    +
    +	addr, err := engine.Address()
    +	if err != nil {
    +		return nil, netip.Addr{}, fmt.Errorf("engine address: %w", err)
    +	}
    +
    +	nsnet, err := engine.GetNet()
    +	if err != nil {
    +		return nil, netip.Addr{}, fmt.Errorf("get net: %w", err)
    +	}
    +
    +	return nsnet, addr, nil
    +}
    diff --git a/client/firewall/uspfilter/uspfilter.go b/client/firewall/uspfilter/uspfilter.go
    index 5bb225ccd..50f48a5c4 100644
    --- a/client/firewall/uspfilter/uspfilter.go
    +++ b/client/firewall/uspfilter/uspfilter.go
    @@ -173,8 +173,7 @@ func create(iface common.IFaceMapper, nativeFirewall firewall.Manager, disableSe
     		stateful:            !disableConntrack,
     		logger:              nblog.NewFromLogrus(log.StandardLogger()),
     		netstack:            netstack.IsEnabled(),
    -		// default true for non-netstack, for netstack only if explicitly enabled
    -		localForwarding: !netstack.IsEnabled() || enableLocalForwarding,
    +		localForwarding:     enableLocalForwarding,
     	}
     
     	if err := m.localipmanager.UpdateLocalIPs(iface); err != nil {
    @@ -647,11 +646,6 @@ func (m *Manager) dropFilter(packetData []byte) bool {
     // handleLocalTraffic handles local traffic.
     // If it returns true, the packet should be dropped.
     func (m *Manager) handleLocalTraffic(d *decoder, srcIP, dstIP net.IP, packetData []byte) bool {
    -	if !m.localForwarding {
    -		m.logger.Trace("Dropping local packet (local forwarding disabled): src=%s dst=%s", srcIP, dstIP)
    -		return true
    -	}
    -
     	if m.peerACLsBlock(srcIP, packetData, m.incomingRules, d) {
     		m.logger.Trace("Dropping local packet (ACL denied): src=%s dst=%s",
     			srcIP, dstIP)
    @@ -660,22 +654,29 @@ func (m *Manager) handleLocalTraffic(d *decoder, srcIP, dstIP net.IP, packetData
     
     	// if running in netstack mode we need to pass this to the forwarder
     	if m.netstack {
    -		m.handleNetstackLocalTraffic(packetData)
    -
    -		// don't process this packet further
    -		return true
    +		return m.handleNetstackLocalTraffic(packetData)
     	}
     
     	return false
     }
    -func (m *Manager) handleNetstackLocalTraffic(packetData []byte) {
    +
    +func (m *Manager) handleNetstackLocalTraffic(packetData []byte) bool {
    +	if !m.localForwarding {
    +		// pass to virtual tcp/ip stack to be picked up by listeners
    +		return false
    +	}
    +
     	if m.forwarder == nil {
    -		return
    +		m.logger.Trace("Dropping local packet (forwarder not initialized)")
    +		return true
     	}
     
     	if err := m.forwarder.InjectIncomingPacket(packetData); err != nil {
     		m.logger.Error("Failed to inject local packet: %v", err)
     	}
    +
    +	// don't process this packet further
    +	return true
     }
     
     // handleRoutedTraffic handles routed traffic.
    diff --git a/client/iface/device.go b/client/iface/device.go
    index 2a170adfb..86e9dab4b 100644
    --- a/client/iface/device.go
    +++ b/client/iface/device.go
    @@ -3,6 +3,8 @@
     package iface
     
     import (
    +	"golang.zx2c4.com/wireguard/tun/netstack"
    +
     	wgdevice "golang.zx2c4.com/wireguard/device"
     
     	"github.com/netbirdio/netbird/client/iface/bind"
    @@ -18,4 +20,5 @@ type WGTunDevice interface {
     	Close() error
     	FilteredDevice() *device.FilteredDevice
     	Device() *wgdevice.Device
    +	GetNet() *netstack.Net
     }
    diff --git a/client/iface/device/device_android.go b/client/iface/device/device_android.go
    index 772722b83..55081e181 100644
    --- a/client/iface/device/device_android.go
    +++ b/client/iface/device/device_android.go
    @@ -9,6 +9,7 @@ import (
     	"golang.org/x/sys/unix"
     	"golang.zx2c4.com/wireguard/device"
     	"golang.zx2c4.com/wireguard/tun"
    +	"golang.zx2c4.com/wireguard/tun/netstack"
     
     	"github.com/netbirdio/netbird/client/iface/bind"
     	"github.com/netbirdio/netbird/client/iface/configurer"
    @@ -130,6 +131,10 @@ func (t *WGTunDevice) FilteredDevice() *FilteredDevice {
     	return t.filteredDevice
     }
     
    +func (t *WGTunDevice) GetNet() *netstack.Net {
    +	return nil
    +}
    +
     func routesToString(routes []string) string {
     	return strings.Join(routes, ";")
     }
    diff --git a/client/iface/device/device_darwin.go b/client/iface/device/device_darwin.go
    index fe7ed1752..1a5635ff2 100644
    --- a/client/iface/device/device_darwin.go
    +++ b/client/iface/device/device_darwin.go
    @@ -9,6 +9,7 @@ import (
     	log "github.com/sirupsen/logrus"
     	"golang.zx2c4.com/wireguard/device"
     	"golang.zx2c4.com/wireguard/tun"
    +	"golang.zx2c4.com/wireguard/tun/netstack"
     
     	"github.com/netbirdio/netbird/client/iface/bind"
     	"github.com/netbirdio/netbird/client/iface/configurer"
    @@ -143,3 +144,7 @@ func (t *TunDevice) assignAddr() error {
     	}
     	return nil
     }
    +
    +func (t *TunDevice) GetNet() *netstack.Net {
    +	return nil
    +}
    diff --git a/client/iface/device/device_ios.go b/client/iface/device/device_ios.go
    index cdabd2c85..b106d475c 100644
    --- a/client/iface/device/device_ios.go
    +++ b/client/iface/device/device_ios.go
    @@ -10,6 +10,7 @@ import (
     	"golang.org/x/sys/unix"
     	"golang.zx2c4.com/wireguard/device"
     	"golang.zx2c4.com/wireguard/tun"
    +	"golang.zx2c4.com/wireguard/tun/netstack"
     
     	"github.com/netbirdio/netbird/client/iface/bind"
     	"github.com/netbirdio/netbird/client/iface/configurer"
    @@ -131,3 +132,7 @@ func (t *TunDevice) UpdateAddr(addr WGAddress) error {
     func (t *TunDevice) FilteredDevice() *FilteredDevice {
     	return t.filteredDevice
     }
    +
    +func (t *TunDevice) GetNet() *netstack.Net {
    +	return nil
    +}
    diff --git a/client/iface/device/device_kernel_unix.go b/client/iface/device/device_kernel_unix.go
    index 3314b576b..fe1d1147f 100644
    --- a/client/iface/device/device_kernel_unix.go
    +++ b/client/iface/device/device_kernel_unix.go
    @@ -10,6 +10,7 @@ import (
     	"github.com/pion/transport/v3"
     	log "github.com/sirupsen/logrus"
     	"golang.zx2c4.com/wireguard/device"
    +	"golang.zx2c4.com/wireguard/tun/netstack"
     
     	"github.com/netbirdio/netbird/client/iface/bind"
     	"github.com/netbirdio/netbird/client/iface/configurer"
    @@ -165,3 +166,7 @@ func (t *TunKernelDevice) FilteredDevice() *FilteredDevice {
     func (t *TunKernelDevice) assignAddr() error {
     	return t.link.assignAddr(t.address)
     }
    +
    +func (t *TunKernelDevice) GetNet() *netstack.Net {
    +	return nil
    +}
    diff --git a/client/iface/device/device_netstack.go b/client/iface/device/device_netstack.go
    index c7d297187..0cb02fd19 100644
    --- a/client/iface/device/device_netstack.go
    +++ b/client/iface/device/device_netstack.go
    @@ -8,10 +8,12 @@ import (
     
     	log "github.com/sirupsen/logrus"
     	"golang.zx2c4.com/wireguard/device"
    +	"golang.zx2c4.com/wireguard/tun/netstack"
     
     	"github.com/netbirdio/netbird/client/iface/bind"
     	"github.com/netbirdio/netbird/client/iface/configurer"
    -	"github.com/netbirdio/netbird/client/iface/netstack"
    +	nbnetstack "github.com/netbirdio/netbird/client/iface/netstack"
    +	nbnet "github.com/netbirdio/netbird/util/net"
     )
     
     type TunNetstackDevice struct {
    @@ -25,9 +27,11 @@ type TunNetstackDevice struct {
     
     	device         *device.Device
     	filteredDevice *FilteredDevice
    -	nsTun          *netstack.NetStackTun
    +	nsTun          *nbnetstack.NetStackTun
     	udpMux         *bind.UniversalUDPMuxDefault
     	configurer     WGConfigurer
    +
    +	net *netstack.Net
     }
     
     func NewNetstackDevice(name string, address WGAddress, wgPort int, key string, mtu int, iceBind *bind.ICEBind, listenAddress string) *TunNetstackDevice {
    @@ -43,13 +47,19 @@ func NewNetstackDevice(name string, address WGAddress, wgPort int, key string, m
     }
     
     func (t *TunNetstackDevice) Create() (WGConfigurer, error) {
    -	log.Info("create netstack tun interface")
    -	t.nsTun = netstack.NewNetStackTun(t.listenAddress, t.address.IP.String(), t.mtu)
    -	tunIface, err := t.nsTun.Create()
    +	log.Info("create nbnetstack tun interface")
    +
    +	// TODO: get from service listener runtime IP
    +	dnsAddr := nbnet.GetLastIPFromNetwork(t.address.Network, 1)
    +	log.Debugf("netstack using address: %s", t.address.IP)
    +	t.nsTun = nbnetstack.NewNetStackTun(t.listenAddress, t.address.IP, dnsAddr, t.mtu)
    +	log.Debugf("netstack using dns address: %s", dnsAddr)
    +	tunIface, net, err := t.nsTun.Create()
     	if err != nil {
     		return nil, fmt.Errorf("error creating tun device: %s", err)
     	}
     	t.filteredDevice = newDeviceFilter(tunIface)
    +	t.net = net
     
     	t.device = device.NewDevice(
     		t.filteredDevice,
    @@ -122,3 +132,7 @@ func (t *TunNetstackDevice) FilteredDevice() *FilteredDevice {
     func (t *TunNetstackDevice) Device() *device.Device {
     	return t.device
     }
    +
    +func (t *TunNetstackDevice) GetNet() *netstack.Net {
    +	return t.net
    +}
    diff --git a/client/iface/device/device_usp_unix.go b/client/iface/device/device_usp_unix.go
    index 4ac87aecb..07570617a 100644
    --- a/client/iface/device/device_usp_unix.go
    +++ b/client/iface/device/device_usp_unix.go
    @@ -8,6 +8,7 @@ import (
     	log "github.com/sirupsen/logrus"
     	"golang.zx2c4.com/wireguard/device"
     	"golang.zx2c4.com/wireguard/tun"
    +	"golang.zx2c4.com/wireguard/tun/netstack"
     
     	"github.com/netbirdio/netbird/client/iface/bind"
     	"github.com/netbirdio/netbird/client/iface/configurer"
    @@ -135,3 +136,7 @@ func (t *USPDevice) assignAddr() error {
     
     	return link.assignAddr(t.address)
     }
    +
    +func (t *USPDevice) GetNet() *netstack.Net {
    +	return nil
    +}
    diff --git a/client/iface/device/device_windows.go b/client/iface/device/device_windows.go
    index e603d7696..0fd1b3326 100644
    --- a/client/iface/device/device_windows.go
    +++ b/client/iface/device/device_windows.go
    @@ -8,6 +8,7 @@ import (
     	"golang.org/x/sys/windows"
     	"golang.zx2c4.com/wireguard/device"
     	"golang.zx2c4.com/wireguard/tun"
    +	"golang.zx2c4.com/wireguard/tun/netstack"
     	"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
     
     	"github.com/netbirdio/netbird/client/iface/bind"
    @@ -174,3 +175,7 @@ func (t *TunDevice) assignAddr() error {
     	log.Debugf("adding address %s to interface: %s", t.address.IP, t.name)
     	return luid.SetIPAddresses([]netip.Prefix{netip.MustParsePrefix(t.address.String())})
     }
    +
    +func (t *TunDevice) GetNet() *netstack.Net {
    +	return nil
    +}
    diff --git a/client/iface/device_android.go b/client/iface/device_android.go
    index 028f6fa7d..5cbeb70f8 100644
    --- a/client/iface/device_android.go
    +++ b/client/iface/device_android.go
    @@ -3,6 +3,8 @@ package iface
     import (
     	wgdevice "golang.zx2c4.com/wireguard/device"
     
    +	"golang.zx2c4.com/wireguard/tun/netstack"
    +
     	"github.com/netbirdio/netbird/client/iface/bind"
     	"github.com/netbirdio/netbird/client/iface/device"
     )
    @@ -16,4 +18,5 @@ type WGTunDevice interface {
     	Close() error
     	FilteredDevice() *device.FilteredDevice
     	Device() *wgdevice.Device
    +	GetNet() *netstack.Net
     }
    diff --git a/client/iface/iface.go b/client/iface/iface.go
    index 64219975f..8056dd9a6 100644
    --- a/client/iface/iface.go
    +++ b/client/iface/iface.go
    @@ -9,6 +9,7 @@ import (
     	"github.com/hashicorp/go-multierror"
     	"github.com/pion/transport/v3"
     	log "github.com/sirupsen/logrus"
    +	"golang.zx2c4.com/wireguard/tun/netstack"
     	"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
     
     	wgdevice "golang.zx2c4.com/wireguard/device"
    @@ -241,3 +242,11 @@ func (w *WGIface) waitUntilRemoved() error {
     		}
     	}
     }
    +
    +// GetNet returns the netstack.Net for the netstack device
    +func (w *WGIface) GetNet() *netstack.Net {
    +	w.mu.Lock()
    +	defer w.mu.Unlock()
    +
    +	return w.tun.GetNet()
    +}
    diff --git a/client/iface/iface_moc.go b/client/iface/iface_moc.go
    index 5f57bc821..f92a8cfc8 100644
    --- a/client/iface/iface_moc.go
    +++ b/client/iface/iface_moc.go
    @@ -5,6 +5,7 @@ import (
     	"time"
     
     	wgdevice "golang.zx2c4.com/wireguard/device"
    +	"golang.zx2c4.com/wireguard/tun/netstack"
     	"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
     
     	"github.com/netbirdio/netbird/client/iface/bind"
    @@ -34,6 +35,7 @@ type MockWGIface struct {
     	GetStatsFunc               func(peerKey string) (configurer.WGStats, error)
     	GetInterfaceGUIDStringFunc func() (string, error)
     	GetProxyFunc               func() wgproxy.Proxy
    +	GetNetFunc                 func() *netstack.Net
     }
     
     func (m *MockWGIface) GetInterfaceGUIDString() (string, error) {
    @@ -115,3 +117,7 @@ func (m *MockWGIface) GetStats(peerKey string) (configurer.WGStats, error) {
     func (m *MockWGIface) GetProxy() wgproxy.Proxy {
     	return m.GetProxyFunc()
     }
    +
    +func (m *MockWGIface) GetNet() *netstack.Net {
    +	return m.GetNetFunc()
    +}
    diff --git a/client/iface/iwginterface.go b/client/iface/iwginterface.go
    index 472ab45f9..2b919ac9e 100644
    --- a/client/iface/iwginterface.go
    +++ b/client/iface/iwginterface.go
    @@ -7,6 +7,7 @@ import (
     	"time"
     
     	wgdevice "golang.zx2c4.com/wireguard/device"
    +	"golang.zx2c4.com/wireguard/tun/netstack"
     	"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
     
     	"github.com/netbirdio/netbird/client/iface/bind"
    @@ -35,4 +36,5 @@ type IWGIface interface {
     	GetDevice() *device.FilteredDevice
     	GetWGDevice() *wgdevice.Device
     	GetStats(peerKey string) (configurer.WGStats, error)
    +	GetNet() *netstack.Net
     }
    diff --git a/client/iface/iwginterface_windows.go b/client/iface/iwginterface_windows.go
    index c9183cafd..cac096b54 100644
    --- a/client/iface/iwginterface_windows.go
    +++ b/client/iface/iwginterface_windows.go
    @@ -5,6 +5,7 @@ import (
     	"time"
     
     	wgdevice "golang.zx2c4.com/wireguard/device"
    +	"golang.zx2c4.com/wireguard/tun/netstack"
     	"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
     
     	"github.com/netbirdio/netbird/client/iface/bind"
    @@ -34,4 +35,5 @@ type IWGIface interface {
     	GetWGDevice() *wgdevice.Device
     	GetStats(peerKey string) (configurer.WGStats, error)
     	GetInterfaceGUIDString() (string, error)
    +	GetNet() *netstack.Net
     }
    diff --git a/client/iface/netstack/env.go b/client/iface/netstack/env.go
    index 09889a57e..cdbf975b1 100644
    --- a/client/iface/netstack/env.go
    +++ b/client/iface/netstack/env.go
    @@ -8,9 +8,11 @@ import (
     	log "github.com/sirupsen/logrus"
     )
     
    +const EnvUseNetstackMode = "NB_USE_NETSTACK_MODE"
    +
     // IsEnabled todo: move these function to cmd layer
     func IsEnabled() bool {
    -	return os.Getenv("NB_USE_NETSTACK_MODE") == "true"
    +	return os.Getenv(EnvUseNetstackMode) == "true"
     }
     
     func ListenAddr() string {
    diff --git a/client/iface/netstack/tun.go b/client/iface/netstack/tun.go
    index c180e4ef5..01f19875e 100644
    --- a/client/iface/netstack/tun.go
    +++ b/client/iface/netstack/tun.go
    @@ -1,15 +1,22 @@
     package netstack
     
     import (
    +	"fmt"
    +	"net"
     	"net/netip"
    +	"os"
    +	"strconv"
     
     	log "github.com/sirupsen/logrus"
     	"golang.zx2c4.com/wireguard/tun"
     	"golang.zx2c4.com/wireguard/tun/netstack"
     )
     
    +const EnvSkipProxy = "NB_NETSTACK_SKIP_PROXY"
    +
     type NetStackTun struct { //nolint:revive
    -	address       string
    +	address       net.IP
    +	dnsAddress    net.IP
     	mtu           int
     	listenAddress string
     
    @@ -17,29 +24,48 @@ type NetStackTun struct { //nolint:revive
     	tundev tun.Device
     }
     
    -func NewNetStackTun(listenAddress string, address string, mtu int) *NetStackTun {
    +func NewNetStackTun(listenAddress string, address net.IP, dnsAddress net.IP, mtu int) *NetStackTun {
     	return &NetStackTun{
     		address:       address,
    +		dnsAddress:    dnsAddress,
     		mtu:           mtu,
     		listenAddress: listenAddress,
     	}
     }
     
    -func (t *NetStackTun) Create() (tun.Device, error) {
    +func (t *NetStackTun) Create() (tun.Device, *netstack.Net, error) {
    +	addr, ok := netip.AddrFromSlice(t.address)
    +	if !ok {
    +		return nil, nil, fmt.Errorf("convert address to netip.Addr: %v", t.address)
    +	}
    +
    +	dnsAddr, ok := netip.AddrFromSlice(t.dnsAddress)
    +	if !ok {
    +		return nil, nil, fmt.Errorf("convert dns address to netip.Addr: %v", t.dnsAddress)
    +	}
    +
     	nsTunDev, tunNet, err := netstack.CreateNetTUN(
    -		[]netip.Addr{netip.MustParseAddr(t.address)},
    -		[]netip.Addr{},
    +		[]netip.Addr{addr.Unmap()},
    +		[]netip.Addr{dnsAddr.Unmap()},
     		t.mtu)
     	if err != nil {
    -		return nil, err
    +		return nil, nil, err
     	}
     	t.tundev = nsTunDev
     
    +	skipProxy, err := strconv.ParseBool(os.Getenv(EnvSkipProxy))
    +	if err != nil {
    +		log.Errorf("failed to parse NB_ETSTACK_SKIP_PROXY: %s", err)
    +	}
    +	if skipProxy {
    +		return nsTunDev, tunNet, nil
    +	}
    +
     	dialer := NewNSDialer(tunNet)
     	t.proxy, err = NewSocks5(dialer)
     	if err != nil {
     		_ = t.tundev.Close()
    -		return nil, err
    +		return nil, nil, err
     	}
     
     	go func() {
    @@ -49,7 +75,7 @@ func (t *NetStackTun) Create() (tun.Device, error) {
     		}
     	}()
     
    -	return nsTunDev, nil
    +	return nsTunDev, tunNet, nil
     }
     
     func (t *NetStackTun) Close() error {
    diff --git a/client/internal/dns/service_memory.go b/client/internal/dns/service_memory.go
    index 729b90cc0..250f3ab2e 100644
    --- a/client/internal/dns/service_memory.go
    +++ b/client/internal/dns/service_memory.go
    @@ -2,7 +2,6 @@ package dns
     
     import (
     	"fmt"
    -	"math/big"
     	"net"
     	"sync"
     
    @@ -10,6 +9,8 @@ import (
     	"github.com/google/gopacket/layers"
     	"github.com/miekg/dns"
     	log "github.com/sirupsen/logrus"
    +
    +	nbnet "github.com/netbirdio/netbird/util/net"
     )
     
     type ServiceViaMemory struct {
    @@ -27,7 +28,7 @@ func NewServiceViaMemory(wgIface WGIface) *ServiceViaMemory {
     		wgInterface: wgIface,
     		dnsMux:      dns.NewServeMux(),
     
    -		runtimeIP:   getLastIPFromNetwork(wgIface.Address().Network, 1),
    +		runtimeIP:   nbnet.GetLastIPFromNetwork(wgIface.Address().Network, 1).String(),
     		runtimePort: defaultPort,
     	}
     	return s
    @@ -118,22 +119,3 @@ func (s *ServiceViaMemory) filterDNSTraffic() (string, error) {
     
     	return filter.AddUDPPacketHook(false, net.ParseIP(s.runtimeIP), uint16(s.runtimePort), hook), nil
     }
    -
    -func getLastIPFromNetwork(network *net.IPNet, fromEnd int) string {
    -	// Calculate the last IP in the CIDR range
    -	var endIP net.IP
    -	for i := 0; i < len(network.IP); i++ {
    -		endIP = append(endIP, network.IP[i]|^network.Mask[i])
    -	}
    -
    -	// convert to big.Int
    -	endInt := big.NewInt(0)
    -	endInt.SetBytes(endIP)
    -
    -	// subtract fromEnd from the last ip
    -	fromEndBig := big.NewInt(int64(fromEnd))
    -	resultInt := big.NewInt(0)
    -	resultInt.Sub(endInt, fromEndBig)
    -
    -	return net.IP(resultInt.Bytes()).String()
    -}
    diff --git a/client/internal/dns/service_memory_test.go b/client/internal/dns/service_memory_test.go
    index bea4f4ce8..244adfaef 100644
    --- a/client/internal/dns/service_memory_test.go
    +++ b/client/internal/dns/service_memory_test.go
    @@ -3,6 +3,8 @@ package dns
     import (
     	"net"
     	"testing"
    +
    +	nbnet "github.com/netbirdio/netbird/util/net"
     )
     
     func TestGetLastIPFromNetwork(t *testing.T) {
    @@ -23,7 +25,7 @@ func TestGetLastIPFromNetwork(t *testing.T) {
     			return
     		}
     
    -		lastIP := getLastIPFromNetwork(ipnet, 1)
    +		lastIP := nbnet.GetLastIPFromNetwork(ipnet, 1).String()
     		if lastIP != tt.ip {
     			t.Errorf("wrong IP address, expected %s: got %s", tt.ip, lastIP)
     		}
    diff --git a/client/internal/engine.go b/client/internal/engine.go
    index 14e0d348f..d590c0db6 100644
    --- a/client/internal/engine.go
    +++ b/client/internal/engine.go
    @@ -19,6 +19,7 @@ import (
     	"github.com/pion/ice/v3"
     	"github.com/pion/stun/v2"
     	log "github.com/sirupsen/logrus"
    +	"golang.zx2c4.com/wireguard/tun/netstack"
     	"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
     	"google.golang.org/protobuf/proto"
     
    @@ -28,7 +29,7 @@ import (
     	"github.com/netbirdio/netbird/client/iface"
     	"github.com/netbirdio/netbird/client/iface/bind"
     	"github.com/netbirdio/netbird/client/iface/device"
    -	"github.com/netbirdio/netbird/client/iface/netstack"
    +	nbnetstack "github.com/netbirdio/netbird/client/iface/netstack"
     	"github.com/netbirdio/netbird/client/internal/acl"
     	"github.com/netbirdio/netbird/client/internal/dns"
     	"github.com/netbirdio/netbird/client/internal/dnsfwd"
    @@ -724,7 +725,7 @@ func (e *Engine) updateSSH(sshConf *mgmProto.SSHConfig) error {
     			// start SSH server if it wasn't running
     			if isNil(e.sshServer) {
     				listenAddr := fmt.Sprintf("%s:%d", e.wgInterface.Address().IP.String(), nbssh.DefaultSSHPort)
    -				if netstack.IsEnabled() {
    +				if nbnetstack.IsEnabled() {
     					listenAddr = fmt.Sprintf("127.0.0.1:%d", nbssh.DefaultSSHPort)
     				}
     				// nil sshServer means it has not yet been started
    @@ -1716,6 +1717,37 @@ func (e *Engine) updateDNSForwarder(enabled bool, domains []string) {
     	}
     }
     
    +func (e *Engine) GetNet() (*netstack.Net, error) {
    +	e.syncMsgMux.Lock()
    +	intf := e.wgInterface
    +	e.syncMsgMux.Unlock()
    +	if intf == nil {
    +		return nil, errors.New("wireguard interface not initialized")
    +	}
    +
    +	nsnet := intf.GetNet()
    +	if nsnet == nil {
    +		return nil, errors.New("failed to get netstack")
    +	}
    +	return nsnet, nil
    +}
    +
    +func (e *Engine) Address() (netip.Addr, error) {
    +	e.syncMsgMux.Lock()
    +	intf := e.wgInterface
    +	e.syncMsgMux.Unlock()
    +	if intf == nil {
    +		return netip.Addr{}, errors.New("wireguard interface not initialized")
    +	}
    +
    +	addr := e.wgInterface.Address()
    +	ip, ok := netip.AddrFromSlice(addr.IP)
    +	if !ok {
    +		return netip.Addr{}, errors.New("failed to convert address to netip.Addr")
    +	}
    +	return ip.Unmap(), nil
    +}
    +
     // isChecksEqual checks if two slices of checks are equal.
     func isChecksEqual(checks []*mgmProto.Checks, oChecks []*mgmProto.Checks) bool {
     	for _, check := range checks {
    diff --git a/util/net/net.go b/util/net/net.go
    index 403aa87e7..7b43b952f 100644
    --- a/util/net/net.go
    +++ b/util/net/net.go
    @@ -1,6 +1,7 @@
     package net
     
     import (
    +	"math/big"
     	"net"
     
     	"github.com/google/uuid"
    @@ -26,3 +27,22 @@ type RemoveHookFunc func(connID ConnectionID) error
     func GenerateConnID() ConnectionID {
     	return ConnectionID(uuid.NewString())
     }
    +
    +func GetLastIPFromNetwork(network *net.IPNet, fromEnd int) net.IP {
    +	// Calculate the last IP in the CIDR range
    +	var endIP net.IP
    +	for i := 0; i < len(network.IP); i++ {
    +		endIP = append(endIP, network.IP[i]|^network.Mask[i])
    +	}
    +
    +	// convert to big.Int
    +	endInt := big.NewInt(0)
    +	endInt.SetBytes(endIP)
    +
    +	// subtract fromEnd from the last ip
    +	fromEndBig := big.NewInt(int64(fromEnd))
    +	resultInt := big.NewInt(0)
    +	resultInt.Sub(endInt, fromEndBig)
    +
    +	return resultInt.Bytes()
    +}
    
    From d7d5b1b1d608ac7a072bc7106f8b6dc27c80d9e6 Mon Sep 17 00:00:00 2001
    From: Viktor Liu <17948409+lixmal@users.noreply.github.com>
    Date: Thu, 20 Feb 2025 15:01:53 +0100
    Subject: [PATCH 76/92] Skip CLI session expired notifcation if notifications
     are disabled (#3266)
    
    ---
     client/server/server.go | 3 ++-
     1 file changed, 2 insertions(+), 1 deletion(-)
    
    diff --git a/client/server/server.go b/client/server/server.go
    index e4e2c8f6f..2efbb94ff 100644
    --- a/client/server/server.go
    +++ b/client/server/server.go
    @@ -766,10 +766,11 @@ func (s *Server) GetConfig(_ context.Context, _ *proto.GetConfigRequest) (*proto
     		DisableNotifications: s.config.DisableNotifications,
     	}, nil
     }
    +
     func (s *Server) onSessionExpire() {
     	if runtime.GOOS != "windows" {
     		isUIActive := internal.CheckUIApp()
    -		if !isUIActive {
    +		if !isUIActive && !s.config.DisableNotifications {
     			if err := sendTerminalNotification(); err != nil {
     				log.Errorf("send session expire terminal notification: %v", err)
     			}
    
    From 77e40f41f24ce0b2c50adfc04f543f71262c4ff9 Mon Sep 17 00:00:00 2001
    From: Pedro Maia Costa <550684+pnmcosta@users.noreply.github.com>
    Date: Thu, 20 Feb 2025 20:24:40 +0000
    Subject: [PATCH 77/92] [management] refactor auth (#3296)
    
    ---
     client/cmd/testutil_test.go                   |   2 +-
     client/internal/engine_test.go                |   2 +-
     client/server/server_test.go                  |   2 +-
     go.mod                                        |   2 +-
     go.sum                                        |   4 +-
     management/client/client_test.go              |   2 +-
     management/cmd/management.go                  |  28 +-
     management/server/account.go                  | 314 ++++----------
     management/server/account_test.go             | 231 +++-------
     .../{jwtclaims => auth/jwt}/extractor.go      | 132 +++---
     management/server/auth/jwt/validator.go       | 302 +++++++++++++
     management/server/auth/manager.go             | 170 ++++++++
     management/server/auth/manager_mock.go        |  54 +++
     management/server/auth/manager_test.go        | 407 ++++++++++++++++++
     management/server/auth/test_data/jwks.json    |  11 +
     management/server/auth/test_data/sample_key   |  27 ++
     .../server/auth/test_data/sample_key.pub      |   9 +
     management/server/config.go                   |   7 -
     management/server/context/auth.go             |  60 +++
     management/server/grpcserver.go               |  56 +--
     management/server/http/handler.go             |  63 ++-
     .../handlers/accounts/accounts_handler.go     |  35 +-
     .../accounts/accounts_handler_test.go         |  23 +-
     .../http/handlers/dns/dns_settings_handler.go |  36 +-
     .../handlers/dns/dns_settings_handler_test.go |  20 +-
     .../http/handlers/dns/nameservers_handler.go  |  48 +--
     .../handlers/dns/nameservers_handler_test.go  |  20 +-
     .../http/handlers/events/events_handler.go    |  25 +-
     .../handlers/events/events_handler_test.go    |  20 +-
     .../http/handlers/groups/groups_handler.go    |  43 +-
     .../handlers/groups/groups_handler_test.go    |  30 +-
     .../server/http/handlers/networks/handler.go  |  53 +--
     .../handlers/networks/resources_handler.go    |  51 +--
     .../http/handlers/networks/routers_handler.go |  45 +-
     .../http/handlers/peers/peers_handler.go      |  31 +-
     .../http/handlers/peers/peers_handler_test.go |  38 +-
     .../policies/geolocation_handler_test.go      |  24 +-
     .../handlers/policies/geolocations_handler.go |  19 +-
     .../handlers/policies/policies_handler.go     |  42 +-
     .../policies/policies_handler_test.go         |  24 +-
     .../policies/posture_checks_handler.go        |  38 +-
     .../policies/posture_checks_handler_test.go   |  24 +-
     .../http/handlers/routes/routes_handler.go    |  39 +-
     .../handlers/routes/routes_handler_test.go    |  48 +--
     .../handlers/setup_keys/setupkeys_handler.go  |  36 +-
     .../setup_keys/setupkeys_handler_test.go      |  25 +-
     .../server/http/handlers/users/pat_handler.go |  32 +-
     .../http/handlers/users/pat_handler_test.go   |  23 +-
     .../http/handlers/users/users_handler.go      |  46 +-
     .../http/handlers/users/users_handler_test.go |  41 +-
     .../server/http/middleware/access_control.go  |  28 +-
     .../server/http/middleware/auth_middleware.go | 161 +++----
     .../http/middleware/auth_middleware_test.go   | 176 ++++++--
     .../peers_handler_benchmark_test.go           |  20 +-
     .../users_handler_benchmark_test.go           |  44 +-
     .../http/testing/testing_tools/tools.go       |  41 +-
     management/server/jwtclaims/claims.go         |  19 -
     management/server/jwtclaims/extractor_test.go | 227 ----------
     management/server/jwtclaims/jwtValidator.go   | 349 ---------------
     management/server/management_proto_test.go    |   2 +-
     management/server/management_test.go          |   1 +
     management/server/mock_server/account_mock.go |  55 +--
     management/server/user.go                     |  25 +-
     management/server/user_test.go                |  10 +-
     64 files changed, 2085 insertions(+), 1937 deletions(-)
     rename management/server/{jwtclaims => auth/jwt}/extractor.go (51%)
     create mode 100644 management/server/auth/jwt/validator.go
     create mode 100644 management/server/auth/manager.go
     create mode 100644 management/server/auth/manager_mock.go
     create mode 100644 management/server/auth/manager_test.go
     create mode 100644 management/server/auth/test_data/jwks.json
     create mode 100644 management/server/auth/test_data/sample_key
     create mode 100644 management/server/auth/test_data/sample_key.pub
     create mode 100644 management/server/context/auth.go
     delete mode 100644 management/server/jwtclaims/claims.go
     delete mode 100644 management/server/jwtclaims/extractor_test.go
     delete mode 100644 management/server/jwtclaims/jwtValidator.go
    
    diff --git a/client/cmd/testutil_test.go b/client/cmd/testutil_test.go
    index e3e644357..e0d784048 100644
    --- a/client/cmd/testutil_test.go
    +++ b/client/cmd/testutil_test.go
    @@ -95,7 +95,7 @@ func startManagement(t *testing.T, config *mgmt.Config, testFile string) (*grpc.
     	}
     
     	secretsManager := mgmt.NewTimeBasedAuthSecretsManager(peersUpdateManager, config.TURNConfig, config.Relay)
    -	mgmtServer, err := mgmt.NewServer(context.Background(), config, accountManager, settings.NewManager(store), peersUpdateManager, secretsManager, nil, nil)
    +	mgmtServer, err := mgmt.NewServer(context.Background(), config, accountManager, settings.NewManager(store), peersUpdateManager, secretsManager, nil, nil, nil)
     	if err != nil {
     		t.Fatal(err)
     	}
    diff --git a/client/internal/engine_test.go b/client/internal/engine_test.go
    index ca49eca09..e32e262b9 100644
    --- a/client/internal/engine_test.go
    +++ b/client/internal/engine_test.go
    @@ -1226,7 +1226,7 @@ func startManagement(t *testing.T, dataDir, testFile string) (*grpc.Server, stri
     	}
     
     	secretsManager := server.NewTimeBasedAuthSecretsManager(peersUpdateManager, config.TURNConfig, config.Relay)
    -	mgmtServer, err := server.NewServer(context.Background(), config, accountManager, settings.NewManager(store), peersUpdateManager, secretsManager, nil, nil)
    +	mgmtServer, err := server.NewServer(context.Background(), config, accountManager, settings.NewManager(store), peersUpdateManager, secretsManager, nil, nil, nil)
     	if err != nil {
     		return nil, "", err
     	}
    diff --git a/client/server/server_test.go b/client/server/server_test.go
    index 128de8e02..d6b651a79 100644
    --- a/client/server/server_test.go
    +++ b/client/server/server_test.go
    @@ -134,7 +134,7 @@ func startManagement(t *testing.T, signalAddr string, counter *int) (*grpc.Serve
     	}
     
     	secretsManager := server.NewTimeBasedAuthSecretsManager(peersUpdateManager, config.TURNConfig, config.Relay)
    -	mgmtServer, err := server.NewServer(context.Background(), config, accountManager, settings.NewManager(store), peersUpdateManager, secretsManager, nil, nil)
    +	mgmtServer, err := server.NewServer(context.Background(), config, accountManager, settings.NewManager(store), peersUpdateManager, secretsManager, nil, nil, nil)
     	if err != nil {
     		return nil, "", err
     	}
    diff --git a/go.mod b/go.mod
    index 3e1208e5a..25e5dd1d2 100644
    --- a/go.mod
    +++ b/go.mod
    @@ -60,7 +60,7 @@ require (
     	github.com/miekg/dns v1.1.59
     	github.com/mitchellh/hashstructure/v2 v2.0.2
     	github.com/nadoo/ipset v0.5.0
    -	github.com/netbirdio/management-integrations/integrations v0.0.0-20250115083837-a09722b8d2a6
    +	github.com/netbirdio/management-integrations/integrations v0.0.0-20250220173202-e599d83524fc
     	github.com/netbirdio/signal-dispatcher/dispatcher v0.0.0-20241010133937-e0df50df217d
     	github.com/okta/okta-sdk-golang/v2 v2.18.0
     	github.com/oschwald/maxminddb-golang v1.12.0
    diff --git a/go.sum b/go.sum
    index 54b77dbee..4057517d3 100644
    --- a/go.sum
    +++ b/go.sum
    @@ -529,8 +529,8 @@ github.com/netbirdio/go-netroute v0.0.0-20240611143515-f59b0e1d3944 h1:TDtJKmM6S
     github.com/netbirdio/go-netroute v0.0.0-20240611143515-f59b0e1d3944/go.mod h1:sHA6TRxjQ6RLbnI+3R4DZo2Eseg/iKiPRfNmcuNySVQ=
     github.com/netbirdio/ice/v3 v3.0.0-20240315174635-e72a50fcb64e h1:PURA50S8u4mF6RrkYYCAvvPCixhqqEiEy3Ej6avh04c=
     github.com/netbirdio/ice/v3 v3.0.0-20240315174635-e72a50fcb64e/go.mod h1:YMLU7qbKfVjmEv7EoZPIVEI+kNYxWCdPK3VS0BU+U4Q=
    -github.com/netbirdio/management-integrations/integrations v0.0.0-20250115083837-a09722b8d2a6 h1:I/ODkZ8rSDOzlJbhEjD2luSI71zl+s5JgNvFHY0+mBU=
    -github.com/netbirdio/management-integrations/integrations v0.0.0-20250115083837-a09722b8d2a6/go.mod h1:izUUs1NT7ja+PwSX3kJ7ox8Kkn478tboBJSjL4kU6J0=
    +github.com/netbirdio/management-integrations/integrations v0.0.0-20250220173202-e599d83524fc h1:18xvjOy2tZVIK7rihNpf9DF/3mAiljYKWaQlWa9vJgI=
    +github.com/netbirdio/management-integrations/integrations v0.0.0-20250220173202-e599d83524fc/go.mod h1:izUUs1NT7ja+PwSX3kJ7ox8Kkn478tboBJSjL4kU6J0=
     github.com/netbirdio/service v0.0.0-20240911161631-f62744f42502 h1:3tHlFmhTdX9axERMVN63dqyFqnvuD+EMJHzM7mNGON8=
     github.com/netbirdio/service v0.0.0-20240911161631-f62744f42502/go.mod h1:CIMRFEJVL+0DS1a3Nx06NaMn4Dz63Ng6O7dl0qH0zVM=
     github.com/netbirdio/signal-dispatcher/dispatcher v0.0.0-20241010133937-e0df50df217d h1:bRq5TKgC7Iq20pDiuC54yXaWnAVeS5PdGpSokFTlR28=
    diff --git a/management/client/client_test.go b/management/client/client_test.go
    index 6ef5df163..2bf802821 100644
    --- a/management/client/client_test.go
    +++ b/management/client/client_test.go
    @@ -78,7 +78,7 @@ func startManagement(t *testing.T) (*grpc.Server, net.Listener) {
     	}
     
     	secretsManager := mgmt.NewTimeBasedAuthSecretsManager(peersUpdateManager, config.TURNConfig, config.Relay)
    -	mgmtServer, err := mgmt.NewServer(context.Background(), config, accountManager, settings.NewManager(store), peersUpdateManager, secretsManager, nil, nil)
    +	mgmtServer, err := mgmt.NewServer(context.Background(), config, accountManager, settings.NewManager(store), peersUpdateManager, secretsManager, nil, nil, nil)
     	if err != nil {
     		t.Fatal(err)
     	}
    diff --git a/management/cmd/management.go b/management/cmd/management.go
    index 1c8fca8dc..9712f04aa 100644
    --- a/management/cmd/management.go
    +++ b/management/cmd/management.go
    @@ -39,13 +39,12 @@ import (
     	"github.com/netbirdio/netbird/formatter"
     	mgmtProto "github.com/netbirdio/netbird/management/proto"
     	"github.com/netbirdio/netbird/management/server"
    +	"github.com/netbirdio/netbird/management/server/auth"
     	nbContext "github.com/netbirdio/netbird/management/server/context"
     	"github.com/netbirdio/netbird/management/server/geolocation"
     	"github.com/netbirdio/netbird/management/server/groups"
     	nbhttp "github.com/netbirdio/netbird/management/server/http"
    -	"github.com/netbirdio/netbird/management/server/http/configs"
     	"github.com/netbirdio/netbird/management/server/idp"
    -	"github.com/netbirdio/netbird/management/server/jwtclaims"
     	"github.com/netbirdio/netbird/management/server/metrics"
     	"github.com/netbirdio/netbird/management/server/networks"
     	"github.com/netbirdio/netbird/management/server/networks/resources"
    @@ -255,24 +254,13 @@ var (
     				tlsEnabled = true
     			}
     
    -			jwtValidator, err := jwtclaims.NewJWTValidator(
    -				ctx,
    +			authManager := auth.NewManager(store,
     				config.HttpConfig.AuthIssuer,
    -				config.GetAuthAudiences(),
    +				config.HttpConfig.AuthAudience,
     				config.HttpConfig.AuthKeysLocation,
    -				config.HttpConfig.IdpSignKeyRefreshEnabled,
    -			)
    -			if err != nil {
    -				return fmt.Errorf("failed creating JWT validator: %v", err)
    -			}
    -
    -			httpAPIAuthCfg := configs.AuthCfg{
    -				Issuer:       config.HttpConfig.AuthIssuer,
    -				Audience:     config.HttpConfig.AuthAudience,
    -				UserIDClaim:  config.HttpConfig.AuthUserIDClaim,
    -				KeysLocation: config.HttpConfig.AuthKeysLocation,
    -			}
    -
    +				config.HttpConfig.AuthUserIDClaim,
    +				config.GetAuthAudiences(),
    +				config.HttpConfig.IdpSignKeyRefreshEnabled)
     			userManager := users.NewManager(store)
     			settingsManager := settings.NewManager(store)
     			permissionsManager := permissions.NewManager(userManager, settingsManager)
    @@ -281,7 +269,7 @@ var (
     			routersManager := routers.NewManager(store, permissionsManager, accountManager)
     			networksManager := networks.NewManager(store, permissionsManager, resourcesManager, routersManager, accountManager)
     
    -			httpAPIHandler, err := nbhttp.NewAPIHandler(ctx, accountManager, networksManager, resourcesManager, routersManager, groupsManager, geo, jwtValidator, appMetrics, httpAPIAuthCfg, integratedPeerValidator)
    +			httpAPIHandler, err := nbhttp.NewAPIHandler(ctx, accountManager, networksManager, resourcesManager, routersManager, groupsManager, geo, authManager, appMetrics, config, integratedPeerValidator)
     			if err != nil {
     				return fmt.Errorf("failed creating HTTP API handler: %v", err)
     			}
    @@ -290,7 +278,7 @@ var (
     			ephemeralManager.LoadInitialPeers(ctx)
     
     			gRPCAPIHandler := grpc.NewServer(gRPCOpts...)
    -			srv, err := server.NewServer(ctx, config, accountManager, settingsManager, peersUpdateManager, secretsManager, appMetrics, ephemeralManager)
    +			srv, err := server.NewServer(ctx, config, accountManager, settingsManager, peersUpdateManager, secretsManager, appMetrics, ephemeralManager, authManager)
     			if err != nil {
     				return fmt.Errorf("failed creating gRPC API handler: %v", err)
     			}
    diff --git a/management/server/account.go b/management/server/account.go
    index 661569418..76c984286 100644
    --- a/management/server/account.go
    +++ b/management/server/account.go
    @@ -2,11 +2,8 @@ package server
     
     import (
     	"context"
    -	"crypto/sha256"
    -	b64 "encoding/base64"
     	"errors"
     	"fmt"
    -	"hash/crc32"
     	"math/rand"
     	"net"
     	"net/netip"
    @@ -24,14 +21,13 @@ import (
     	log "github.com/sirupsen/logrus"
     	"golang.org/x/exp/maps"
     
    -	"github.com/netbirdio/netbird/base62"
     	nbdns "github.com/netbirdio/netbird/dns"
     	"github.com/netbirdio/netbird/management/domain"
     	"github.com/netbirdio/netbird/management/server/activity"
    +	nbcontext "github.com/netbirdio/netbird/management/server/context"
     	"github.com/netbirdio/netbird/management/server/geolocation"
     	"github.com/netbirdio/netbird/management/server/idp"
     	"github.com/netbirdio/netbird/management/server/integrated_validator"
    -	"github.com/netbirdio/netbird/management/server/jwtclaims"
     	nbpeer "github.com/netbirdio/netbird/management/server/peer"
     	"github.com/netbirdio/netbird/management/server/posture"
     	"github.com/netbirdio/netbird/management/server/status"
    @@ -77,13 +73,10 @@ type AccountManager interface {
     	GetAccountByID(ctx context.Context, accountID string, userID string) (*types.Account, error)
     	AccountExists(ctx context.Context, accountID string) (bool, error)
     	GetAccountIDByUserID(ctx context.Context, userID, domain string) (string, error)
    -	GetAccountIDFromToken(ctx context.Context, claims jwtclaims.AuthorizationClaims) (string, string, error)
    -	CheckUserAccessByJWTGroups(ctx context.Context, claims jwtclaims.AuthorizationClaims) error
    -	GetPATInfo(ctx context.Context, token string) (*types.User, *types.PersonalAccessToken, string, string, error)
    +	GetAccountIDFromUserAuth(ctx context.Context, userAuth nbcontext.UserAuth) (string, string, error)
     	DeleteAccount(ctx context.Context, accountID, userID string) error
    -	MarkPATUsed(ctx context.Context, tokenID string) error
     	GetUserByID(ctx context.Context, id string) (*types.User, error)
    -	GetUser(ctx context.Context, claims jwtclaims.AuthorizationClaims) (*types.User, error)
    +	GetUserFromUserAuth(ctx context.Context, userAuth nbcontext.UserAuth) (*types.User, error)
     	ListUsers(ctx context.Context, accountID string) ([]*types.User, error)
     	GetPeers(ctx context.Context, accountID, userID string) ([]*nbpeer.Peer, error)
     	MarkPeerConnected(ctx context.Context, peerKey string, connected bool, realIP net.IP, accountID string) error
    @@ -150,6 +143,7 @@ type AccountManager interface {
     	DeleteSetupKey(ctx context.Context, accountID, userID, keyID string) error
     	UpdateAccountPeers(ctx context.Context, accountID string)
     	BuildUserInfosForAccount(ctx context.Context, accountID, initiatorUserID string, accountUsers []*types.User) (map[string]*types.UserInfo, error)
    +	SyncUserJWTGroups(ctx context.Context, userAuth nbcontext.UserAuth) error
     }
     
     type DefaultAccountManager struct {
    @@ -954,11 +948,11 @@ func (am *DefaultAccountManager) removeUserFromCache(ctx context.Context, accoun
     }
     
     // updateAccountDomainAttributesIfNotUpToDate updates the account domain attributes if they are not up to date and then, saves the account changes
    -func (am *DefaultAccountManager) updateAccountDomainAttributesIfNotUpToDate(ctx context.Context, accountID string, claims jwtclaims.AuthorizationClaims,
    +func (am *DefaultAccountManager) updateAccountDomainAttributesIfNotUpToDate(ctx context.Context, accountID string, userAuth nbcontext.UserAuth,
     	primaryDomain bool,
     ) error {
    -	if claims.Domain == "" {
    -		log.WithContext(ctx).Errorf("claims don't contain a valid domain, skipping domain attributes update. Received claims: %v", claims)
    +	if userAuth.Domain == "" {
    +		log.WithContext(ctx).Errorf("claims don't contain a valid domain, skipping domain attributes update. Received claims: %v", userAuth)
     		return nil
     	}
     
    @@ -971,11 +965,11 @@ func (am *DefaultAccountManager) updateAccountDomainAttributesIfNotUpToDate(ctx
     		return err
     	}
     
    -	if domainIsUpToDate(accountDomain, domainCategory, claims) {
    +	if domainIsUpToDate(accountDomain, domainCategory, userAuth) {
     		return nil
     	}
     
    -	user, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, claims.UserId)
    +	user, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, userAuth.UserId)
     	if err != nil {
     		log.WithContext(ctx).Errorf("error getting user: %v", err)
     		return err
    @@ -984,13 +978,13 @@ func (am *DefaultAccountManager) updateAccountDomainAttributesIfNotUpToDate(ctx
     	newDomain := accountDomain
     	newCategoty := domainCategory
     
    -	lowerDomain := strings.ToLower(claims.Domain)
    +	lowerDomain := strings.ToLower(userAuth.Domain)
     	if accountDomain != lowerDomain && user.HasAdminPower() {
     		newDomain = lowerDomain
     	}
     
     	if accountDomain == lowerDomain {
    -		newCategoty = claims.DomainCategory
    +		newCategoty = userAuth.DomainCategory
     	}
     
     	return am.Store.UpdateAccountDomainAttributes(ctx, accountID, newDomain, newCategoty, primaryDomain)
    @@ -1006,16 +1000,16 @@ func (am *DefaultAccountManager) handleExistingUserAccount(
     	ctx context.Context,
     	userAccountID string,
     	domainAccountID string,
    -	claims jwtclaims.AuthorizationClaims,
    +	userAuth nbcontext.UserAuth,
     ) error {
     	primaryDomain := domainAccountID == "" || userAccountID == domainAccountID
    -	err := am.updateAccountDomainAttributesIfNotUpToDate(ctx, userAccountID, claims, primaryDomain)
    +	err := am.updateAccountDomainAttributesIfNotUpToDate(ctx, userAccountID, userAuth, primaryDomain)
     	if err != nil {
     		return err
     	}
     
     	// we should register the account ID to this user's metadata in our IDP manager
    -	err = am.addAccountIDToIDPAppMeta(ctx, claims.UserId, userAccountID)
    +	err = am.addAccountIDToIDPAppMeta(ctx, userAuth.UserId, userAccountID)
     	if err != nil {
     		return err
     	}
    @@ -1025,20 +1019,20 @@ func (am *DefaultAccountManager) handleExistingUserAccount(
     
     // addNewPrivateAccount validates if there is an existing primary account for the domain, if so it adds the new user to that account,
     // otherwise it will create a new account and make it primary account for the domain.
    -func (am *DefaultAccountManager) addNewPrivateAccount(ctx context.Context, domainAccountID string, claims jwtclaims.AuthorizationClaims) (string, error) {
    -	if claims.UserId == "" {
    +func (am *DefaultAccountManager) addNewPrivateAccount(ctx context.Context, domainAccountID string, userAuth nbcontext.UserAuth) (string, error) {
    +	if userAuth.UserId == "" {
     		return "", fmt.Errorf("user ID is empty")
     	}
     
    -	lowerDomain := strings.ToLower(claims.Domain)
    +	lowerDomain := strings.ToLower(userAuth.Domain)
     
    -	newAccount, err := am.newAccount(ctx, claims.UserId, lowerDomain)
    +	newAccount, err := am.newAccount(ctx, userAuth.UserId, lowerDomain)
     	if err != nil {
     		return "", err
     	}
     
     	newAccount.Domain = lowerDomain
    -	newAccount.DomainCategory = claims.DomainCategory
    +	newAccount.DomainCategory = userAuth.DomainCategory
     	newAccount.IsDomainPrimaryAccount = true
     
     	err = am.Store.SaveAccount(ctx, newAccount)
    @@ -1046,33 +1040,33 @@ func (am *DefaultAccountManager) addNewPrivateAccount(ctx context.Context, domai
     		return "", err
     	}
     
    -	err = am.addAccountIDToIDPAppMeta(ctx, claims.UserId, newAccount.Id)
    +	err = am.addAccountIDToIDPAppMeta(ctx, userAuth.UserId, newAccount.Id)
     	if err != nil {
     		return "", err
     	}
     
    -	am.StoreEvent(ctx, claims.UserId, claims.UserId, newAccount.Id, activity.UserJoined, nil)
    +	am.StoreEvent(ctx, userAuth.UserId, userAuth.UserId, newAccount.Id, activity.UserJoined, nil)
     
     	return newAccount.Id, nil
     }
     
    -func (am *DefaultAccountManager) addNewUserToDomainAccount(ctx context.Context, domainAccountID string, claims jwtclaims.AuthorizationClaims) (string, error) {
    +func (am *DefaultAccountManager) addNewUserToDomainAccount(ctx context.Context, domainAccountID string, userAuth nbcontext.UserAuth) (string, error) {
     	unlockAccount := am.Store.AcquireWriteLockByUID(ctx, domainAccountID)
     	defer unlockAccount()
     
    -	newUser := types.NewRegularUser(claims.UserId)
    +	newUser := types.NewRegularUser(userAuth.UserId)
     	newUser.AccountID = domainAccountID
     	err := am.Store.SaveUser(ctx, store.LockingStrengthUpdate, newUser)
     	if err != nil {
     		return "", err
     	}
     
    -	err = am.addAccountIDToIDPAppMeta(ctx, claims.UserId, domainAccountID)
    +	err = am.addAccountIDToIDPAppMeta(ctx, userAuth.UserId, domainAccountID)
     	if err != nil {
     		return "", err
     	}
     
    -	am.StoreEvent(ctx, claims.UserId, claims.UserId, domainAccountID, activity.UserJoined, nil)
    +	am.StoreEvent(ctx, userAuth.UserId, userAuth.UserId, domainAccountID, activity.UserJoined, nil)
     
     	return domainAccountID, nil
     }
    @@ -1112,76 +1106,11 @@ func (am *DefaultAccountManager) redeemInvite(ctx context.Context, accountID str
     	return nil
     }
     
    -// MarkPATUsed marks a personal access token as used
    -func (am *DefaultAccountManager) MarkPATUsed(ctx context.Context, tokenID string) error {
    -	return am.Store.MarkPATUsed(ctx, store.LockingStrengthUpdate, tokenID)
    -}
    -
     // GetAccount returns an account associated with this account ID.
     func (am *DefaultAccountManager) GetAccount(ctx context.Context, accountID string) (*types.Account, error) {
     	return am.Store.GetAccount(ctx, accountID)
     }
     
    -// GetPATInfo retrieves user, personal access token, domain, and category details from a personal access token.
    -func (am *DefaultAccountManager) GetPATInfo(ctx context.Context, token string) (user *types.User, pat *types.PersonalAccessToken, domain string, category string, err error) {
    -	user, pat, err = am.extractPATFromToken(ctx, token)
    -	if err != nil {
    -		return nil, nil, "", "", err
    -	}
    -
    -	domain, category, err = am.Store.GetAccountDomainAndCategory(ctx, store.LockingStrengthShare, user.AccountID)
    -	if err != nil {
    -		return nil, nil, "", "", err
    -	}
    -
    -	return user, pat, domain, category, nil
    -}
    -
    -// extractPATFromToken validates the token structure and retrieves associated User and PAT.
    -func (am *DefaultAccountManager) extractPATFromToken(ctx context.Context, token string) (*types.User, *types.PersonalAccessToken, error) {
    -	if len(token) != types.PATLength {
    -		return nil, nil, fmt.Errorf("token has incorrect length")
    -	}
    -
    -	prefix := token[:len(types.PATPrefix)]
    -	if prefix != types.PATPrefix {
    -		return nil, nil, fmt.Errorf("token has wrong prefix")
    -	}
    -	secret := token[len(types.PATPrefix) : len(types.PATPrefix)+types.PATSecretLength]
    -	encodedChecksum := token[len(types.PATPrefix)+types.PATSecretLength : len(types.PATPrefix)+types.PATSecretLength+types.PATChecksumLength]
    -
    -	verificationChecksum, err := base62.Decode(encodedChecksum)
    -	if err != nil {
    -		return nil, nil, fmt.Errorf("token checksum decoding failed: %w", err)
    -	}
    -
    -	secretChecksum := crc32.ChecksumIEEE([]byte(secret))
    -	if secretChecksum != verificationChecksum {
    -		return nil, nil, fmt.Errorf("token checksum does not match")
    -	}
    -
    -	hashedToken := sha256.Sum256([]byte(token))
    -	encodedHashedToken := b64.StdEncoding.EncodeToString(hashedToken[:])
    -
    -	var user *types.User
    -	var pat *types.PersonalAccessToken
    -
    -	err = am.Store.ExecuteInTransaction(ctx, func(transaction store.Store) error {
    -		pat, err = transaction.GetPATByHashedToken(ctx, store.LockingStrengthShare, encodedHashedToken)
    -		if err != nil {
    -			return err
    -		}
    -
    -		user, err = transaction.GetUserByPATID(ctx, store.LockingStrengthShare, pat.ID)
    -		return err
    -	})
    -	if err != nil {
    -		return nil, nil, err
    -	}
    -
    -	return user, pat, nil
    -}
    -
     // GetAccountByID returns an account associated with this account ID.
     func (am *DefaultAccountManager) GetAccountByID(ctx context.Context, accountID string, userID string) (*types.Account, error) {
     	user, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, userID)
    @@ -1196,58 +1125,56 @@ func (am *DefaultAccountManager) GetAccountByID(ctx context.Context, accountID s
     	return am.Store.GetAccount(ctx, accountID)
     }
     
    -// GetAccountIDFromToken returns an account ID associated with this token.
    -func (am *DefaultAccountManager) GetAccountIDFromToken(ctx context.Context, claims jwtclaims.AuthorizationClaims) (string, string, error) {
    -	if claims.UserId == "" {
    +func (am *DefaultAccountManager) GetAccountIDFromUserAuth(ctx context.Context, userAuth nbcontext.UserAuth) (string, string, error) {
    +	if userAuth.UserId == "" {
     		return "", "", errors.New(emptyUserID)
     	}
     	if am.singleAccountMode && am.singleAccountModeDomain != "" {
     		// This section is mostly related to self-hosted installations.
     		// We override incoming domain claims to group users under a single account.
    -		claims.Domain = am.singleAccountModeDomain
    -		claims.DomainCategory = types.PrivateCategory
    +		userAuth.Domain = am.singleAccountModeDomain
    +		userAuth.DomainCategory = types.PrivateCategory
     		log.WithContext(ctx).Debugf("overriding JWT Domain and DomainCategory claims since single account mode is enabled")
     	}
     
    -	accountID, err := am.getAccountIDWithAuthorizationClaims(ctx, claims)
    +	accountID, err := am.getAccountIDWithAuthorizationClaims(ctx, userAuth)
     	if err != nil {
     		return "", "", err
     	}
     
    -	user, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, claims.UserId)
    +	user, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, userAuth.UserId)
     	if err != nil {
     		// this is not really possible because we got an account by user ID
    -		return "", "", status.Errorf(status.NotFound, "user %s not found", claims.UserId)
    +		return "", "", status.Errorf(status.NotFound, "user %s not found", userAuth.UserId)
    +	}
    +
    +	if userAuth.IsChild {
    +		return accountID, user.Id, nil
     	}
     
     	if user.AccountID != accountID {
    -		return "", "", status.Errorf(status.PermissionDenied, "user %s is not part of the account %s", claims.UserId, accountID)
    +		return "", "", status.Errorf(status.PermissionDenied, "user %s is not part of the account %s", userAuth.UserId, accountID)
     	}
     
    -	if !user.IsServiceUser && claims.Invited {
    +	if !user.IsServiceUser && userAuth.Invited {
     		err = am.redeemInvite(ctx, accountID, user.Id)
     		if err != nil {
     			return "", "", err
     		}
     	}
     
    -	if err = am.syncJWTGroups(ctx, accountID, claims); err != nil {
    -		return "", "", err
    -	}
    -
     	return accountID, user.Id, nil
     }
     
     // syncJWTGroups processes the JWT groups for a user, updates the account based on the groups,
     // and propagates changes to peers if group propagation is enabled.
    -func (am *DefaultAccountManager) syncJWTGroups(ctx context.Context, accountID string, claims jwtclaims.AuthorizationClaims) error {
    -	if claim, exists := claims.Raw[jwtclaims.IsToken]; exists {
    -		if isToken, ok := claim.(bool); ok && isToken {
    -			return nil
    -		}
    +// requires userAuth to have been ValidateAndParseToken and EnsureUserAccessByJWTGroups by the AuthManager
    +func (am *DefaultAccountManager) SyncUserJWTGroups(ctx context.Context, userAuth nbcontext.UserAuth) error {
    +	if userAuth.IsChild || userAuth.IsPAT {
    +		return nil
     	}
     
    -	settings, err := am.Store.GetAccountSettings(ctx, store.LockingStrengthShare, accountID)
    +	settings, err := am.Store.GetAccountSettings(ctx, store.LockingStrengthShare, userAuth.AccountId)
     	if err != nil {
     		return err
     	}
    @@ -1261,9 +1188,7 @@ func (am *DefaultAccountManager) syncJWTGroups(ctx context.Context, accountID st
     		return nil
     	}
     
    -	jwtGroupsNames := extractJWTGroups(ctx, settings.JWTGroupsClaimName, claims)
    -
    -	unlockAccount := am.Store.AcquireWriteLockByUID(ctx, accountID)
    +	unlockAccount := am.Store.AcquireWriteLockByUID(ctx, userAuth.AccountId)
     	defer func() {
     		if unlockAccount != nil {
     			unlockAccount()
    @@ -1275,17 +1200,17 @@ func (am *DefaultAccountManager) syncJWTGroups(ctx context.Context, accountID st
     	var hasChanges bool
     	var user *types.User
     	err = am.Store.ExecuteInTransaction(ctx, func(transaction store.Store) error {
    -		user, err = transaction.GetUserByUserID(ctx, store.LockingStrengthShare, claims.UserId)
    +		user, err = transaction.GetUserByUserID(ctx, store.LockingStrengthShare, userAuth.UserId)
     		if err != nil {
     			return fmt.Errorf("error getting user: %w", err)
     		}
     
    -		groups, err := transaction.GetAccountGroups(ctx, store.LockingStrengthShare, accountID)
    +		groups, err := transaction.GetAccountGroups(ctx, store.LockingStrengthShare, userAuth.AccountId)
     		if err != nil {
     			return fmt.Errorf("error getting account groups: %w", err)
     		}
     
    -		changed, updatedAutoGroups, newGroupsToCreate, err := am.getJWTGroupsChanges(user, groups, jwtGroupsNames)
    +		changed, updatedAutoGroups, newGroupsToCreate, err := am.getJWTGroupsChanges(user, groups, userAuth.Groups)
     		if err != nil {
     			return fmt.Errorf("error getting JWT groups changes: %w", err)
     		}
    @@ -1310,7 +1235,7 @@ func (am *DefaultAccountManager) syncJWTGroups(ctx context.Context, accountID st
     
     		// Propagate changes to peers if group propagation is enabled
     		if settings.GroupsPropagationEnabled {
    -			groups, err = transaction.GetAccountGroups(ctx, store.LockingStrengthShare, accountID)
    +			groups, err = transaction.GetAccountGroups(ctx, store.LockingStrengthShare, userAuth.AccountId)
     			if err != nil {
     				return fmt.Errorf("error getting account groups: %w", err)
     			}
    @@ -1320,7 +1245,7 @@ func (am *DefaultAccountManager) syncJWTGroups(ctx context.Context, accountID st
     				groupsMap[group.ID] = group
     			}
     
    -			peers, err := transaction.GetUserPeers(ctx, store.LockingStrengthShare, accountID, claims.UserId)
    +			peers, err := transaction.GetUserPeers(ctx, store.LockingStrengthShare, userAuth.AccountId, userAuth.UserId)
     			if err != nil {
     				return fmt.Errorf("error getting user peers: %w", err)
     			}
    @@ -1334,7 +1259,7 @@ func (am *DefaultAccountManager) syncJWTGroups(ctx context.Context, accountID st
     				return fmt.Errorf("error saving groups: %w", err)
     			}
     
    -			if err = transaction.IncrementNetworkSerial(ctx, store.LockingStrengthUpdate, accountID); err != nil {
    +			if err = transaction.IncrementNetworkSerial(ctx, store.LockingStrengthUpdate, userAuth.AccountId); err != nil {
     				return fmt.Errorf("error incrementing network serial: %w", err)
     			}
     		}
    @@ -1352,45 +1277,45 @@ func (am *DefaultAccountManager) syncJWTGroups(ctx context.Context, accountID st
     	}
     
     	for _, g := range addNewGroups {
    -		group, err := am.Store.GetGroupByID(ctx, store.LockingStrengthShare, accountID, g)
    +		group, err := am.Store.GetGroupByID(ctx, store.LockingStrengthShare, userAuth.AccountId, g)
     		if err != nil {
    -			log.WithContext(ctx).Debugf("group %s not found while saving user activity event of account %s", g, accountID)
    +			log.WithContext(ctx).Debugf("group %s not found while saving user activity event of account %s", g, userAuth.AccountId)
     		} else {
     			meta := map[string]any{
     				"group": group.Name, "group_id": group.ID,
     				"is_service_user": user.IsServiceUser, "user_name": user.ServiceUserName,
     			}
    -			am.StoreEvent(ctx, user.Id, user.Id, accountID, activity.GroupAddedToUser, meta)
    +			am.StoreEvent(ctx, user.Id, user.Id, userAuth.AccountId, activity.GroupAddedToUser, meta)
     		}
     	}
     
     	for _, g := range removeOldGroups {
    -		group, err := am.Store.GetGroupByID(ctx, store.LockingStrengthShare, accountID, g)
    +		group, err := am.Store.GetGroupByID(ctx, store.LockingStrengthShare, userAuth.AccountId, g)
     		if err != nil {
    -			log.WithContext(ctx).Debugf("group %s not found while saving user activity event of account %s", g, accountID)
    +			log.WithContext(ctx).Debugf("group %s not found while saving user activity event of account %s", g, userAuth.AccountId)
     		} else {
     			meta := map[string]any{
     				"group": group.Name, "group_id": group.ID,
     				"is_service_user": user.IsServiceUser, "user_name": user.ServiceUserName,
     			}
    -			am.StoreEvent(ctx, user.Id, user.Id, accountID, activity.GroupRemovedFromUser, meta)
    +			am.StoreEvent(ctx, user.Id, user.Id, userAuth.AccountId, activity.GroupRemovedFromUser, meta)
     		}
     	}
     
     	if settings.GroupsPropagationEnabled {
    -		removedGroupAffectsPeers, err := areGroupChangesAffectPeers(ctx, am.Store, accountID, removeOldGroups)
    +		removedGroupAffectsPeers, err := areGroupChangesAffectPeers(ctx, am.Store, userAuth.AccountId, removeOldGroups)
     		if err != nil {
     			return err
     		}
     
    -		newGroupsAffectsPeers, err := areGroupChangesAffectPeers(ctx, am.Store, accountID, addNewGroups)
    +		newGroupsAffectsPeers, err := areGroupChangesAffectPeers(ctx, am.Store, userAuth.AccountId, addNewGroups)
     		if err != nil {
     			return err
     		}
     
     		if removedGroupAffectsPeers || newGroupsAffectsPeers {
    -			log.WithContext(ctx).Tracef("user %s: JWT group membership changed, updating account peers", claims.UserId)
    -			am.UpdateAccountPeers(ctx, accountID)
    +			log.WithContext(ctx).Tracef("user %s: JWT group membership changed, updating account peers", userAuth.UserId)
    +			am.UpdateAccountPeers(ctx, userAuth.AccountId)
     		}
     	}
     
    @@ -1415,24 +1340,34 @@ func (am *DefaultAccountManager) syncJWTGroups(ctx context.Context, accountID st
     // Existing user + Existing account + Existing Indexed Domain -> Nothing changes
     //
     // Existing user + Existing account + Existing domain reclassified Domain as private -> Nothing changes (index domain)
    -func (am *DefaultAccountManager) getAccountIDWithAuthorizationClaims(ctx context.Context, claims jwtclaims.AuthorizationClaims) (string, error) {
    +//
    +// UserAuth IsChild -> checks that account exists
    +func (am *DefaultAccountManager) getAccountIDWithAuthorizationClaims(ctx context.Context, userAuth nbcontext.UserAuth) (string, error) {
     	log.WithContext(ctx).Tracef("getting account with authorization claims. User ID: \"%s\", Account ID: \"%s\", Domain: \"%s\", Domain Category: \"%s\"",
    -		claims.UserId, claims.AccountId, claims.Domain, claims.DomainCategory)
    +		userAuth.UserId, userAuth.AccountId, userAuth.Domain, userAuth.DomainCategory)
     
    -	if claims.UserId == "" {
    +	if userAuth.UserId == "" {
     		return "", errors.New(emptyUserID)
     	}
     
    -	if claims.DomainCategory != types.PrivateCategory || !isDomainValid(claims.Domain) {
    -		return am.GetAccountIDByUserID(ctx, claims.UserId, claims.Domain)
    +	if userAuth.IsChild {
    +		exists, err := am.Store.AccountExists(ctx, store.LockingStrengthShare, userAuth.AccountId)
    +		if err != nil || !exists {
    +			return "", err
    +		}
    +		return userAuth.AccountId, nil
     	}
     
    -	if claims.AccountId != "" {
    -		return am.handlePrivateAccountWithIDFromClaim(ctx, claims)
    +	if userAuth.DomainCategory != types.PrivateCategory || !isDomainValid(userAuth.Domain) {
    +		return am.GetAccountIDByUserID(ctx, userAuth.UserId, userAuth.Domain)
    +	}
    +
    +	if userAuth.AccountId != "" {
    +		return am.handlePrivateAccountWithIDFromClaim(ctx, userAuth)
     	}
     
     	// We checked if the domain has a primary account already
    -	domainAccountID, cancel, err := am.getPrivateDomainWithGlobalLock(ctx, claims.Domain)
    +	domainAccountID, cancel, err := am.getPrivateDomainWithGlobalLock(ctx, userAuth.Domain)
     	if cancel != nil {
     		defer cancel()
     	}
    @@ -1440,14 +1375,14 @@ func (am *DefaultAccountManager) getAccountIDWithAuthorizationClaims(ctx context
     		return "", err
     	}
     
    -	userAccountID, err := am.Store.GetAccountIDByUserID(ctx, store.LockingStrengthShare, claims.UserId)
    +	userAccountID, err := am.Store.GetAccountIDByUserID(ctx, store.LockingStrengthShare, userAuth.UserId)
     	if handleNotFound(err) != nil {
     		log.WithContext(ctx).Errorf("error getting account ID by user ID: %v", err)
     		return "", err
     	}
     
     	if userAccountID != "" {
    -		if err = am.handleExistingUserAccount(ctx, userAccountID, domainAccountID, claims); err != nil {
    +		if err = am.handleExistingUserAccount(ctx, userAccountID, domainAccountID, userAuth); err != nil {
     			return "", err
     		}
     
    @@ -1455,10 +1390,10 @@ func (am *DefaultAccountManager) getAccountIDWithAuthorizationClaims(ctx context
     	}
     
     	if domainAccountID != "" {
    -		return am.addNewUserToDomainAccount(ctx, domainAccountID, claims)
    +		return am.addNewUserToDomainAccount(ctx, domainAccountID, userAuth)
     	}
     
    -	return am.addNewPrivateAccount(ctx, domainAccountID, claims)
    +	return am.addNewPrivateAccount(ctx, domainAccountID, userAuth)
     }
     func (am *DefaultAccountManager) getPrivateDomainWithGlobalLock(ctx context.Context, domain string) (string, context.CancelFunc, error) {
     	domainAccountID, err := am.Store.GetAccountIDByPrivateDomain(ctx, store.LockingStrengthShare, domain)
    @@ -1486,40 +1421,40 @@ func (am *DefaultAccountManager) getPrivateDomainWithGlobalLock(ctx context.Cont
     	return domainAccountID, cancel, nil
     }
     
    -func (am *DefaultAccountManager) handlePrivateAccountWithIDFromClaim(ctx context.Context, claims jwtclaims.AuthorizationClaims) (string, error) {
    -	userAccountID, err := am.Store.GetAccountIDByUserID(ctx, store.LockingStrengthShare, claims.UserId)
    +func (am *DefaultAccountManager) handlePrivateAccountWithIDFromClaim(ctx context.Context, userAuth nbcontext.UserAuth) (string, error) {
    +	userAccountID, err := am.Store.GetAccountIDByUserID(ctx, store.LockingStrengthShare, userAuth.UserId)
     	if err != nil {
     		log.WithContext(ctx).Errorf("error getting account ID by user ID: %v", err)
     		return "", err
     	}
     
    -	if userAccountID != claims.AccountId {
    -		return "", fmt.Errorf("user %s is not part of the account id %s", claims.UserId, claims.AccountId)
    +	if userAccountID != userAuth.AccountId {
    +		return "", fmt.Errorf("user %s is not part of the account id %s", userAuth.UserId, userAuth.AccountId)
     	}
     
    -	accountDomain, domainCategory, err := am.Store.GetAccountDomainAndCategory(ctx, store.LockingStrengthShare, claims.AccountId)
    +	accountDomain, domainCategory, err := am.Store.GetAccountDomainAndCategory(ctx, store.LockingStrengthShare, userAuth.AccountId)
     	if handleNotFound(err) != nil {
     		log.WithContext(ctx).Errorf("error getting account domain and category: %v", err)
     		return "", err
     	}
     
    -	if domainIsUpToDate(accountDomain, domainCategory, claims) {
    -		return claims.AccountId, nil
    +	if domainIsUpToDate(accountDomain, domainCategory, userAuth) {
    +		return userAuth.AccountId, nil
     	}
     
     	// We checked if the domain has a primary account already
    -	domainAccountID, err := am.Store.GetAccountIDByPrivateDomain(ctx, store.LockingStrengthShare, claims.Domain)
    +	domainAccountID, err := am.Store.GetAccountIDByPrivateDomain(ctx, store.LockingStrengthShare, userAuth.Domain)
     	if handleNotFound(err) != nil {
     		log.WithContext(ctx).Errorf(errorGettingDomainAccIDFmt, err)
     		return "", err
     	}
     
    -	err = am.handleExistingUserAccount(ctx, claims.AccountId, domainAccountID, claims)
    +	err = am.handleExistingUserAccount(ctx, userAuth.AccountId, domainAccountID, userAuth)
     	if err != nil {
     		return "", err
     	}
     
    -	return claims.AccountId, nil
    +	return userAuth.AccountId, nil
     }
     
     func handleNotFound(err error) error {
    @@ -1534,8 +1469,8 @@ func handleNotFound(err error) error {
     	return nil
     }
     
    -func domainIsUpToDate(domain string, domainCategory string, claims jwtclaims.AuthorizationClaims) bool {
    -	return domainCategory == types.PrivateCategory || claims.DomainCategory != types.PrivateCategory || domain != claims.Domain
    +func domainIsUpToDate(domain string, domainCategory string, userAuth nbcontext.UserAuth) bool {
    +	return domainCategory == types.PrivateCategory || userAuth.DomainCategory != types.PrivateCategory || domain != userAuth.Domain
     }
     
     func (am *DefaultAccountManager) SyncAndMarkPeer(ctx context.Context, accountID string, peerPubKey string, meta nbpeer.PeerSystemMeta, realIP net.IP) (*nbpeer.Peer, *types.NetworkMap, []*posture.Checks, error) {
    @@ -1617,34 +1552,6 @@ func (am *DefaultAccountManager) GetDNSDomain() string {
     	return am.dnsDomain
     }
     
    -// CheckUserAccessByJWTGroups checks if the user has access, particularly in cases where the admin enabled JWT
    -// group propagation and set the list of groups with access permissions.
    -func (am *DefaultAccountManager) CheckUserAccessByJWTGroups(ctx context.Context, claims jwtclaims.AuthorizationClaims) error {
    -	accountID, _, err := am.GetAccountIDFromToken(ctx, claims)
    -	if err != nil {
    -		return err
    -	}
    -
    -	settings, err := am.Store.GetAccountSettings(ctx, store.LockingStrengthShare, accountID)
    -	if err != nil {
    -		return err
    -	}
    -
    -	// Ensures JWT group synchronization to the management is enabled before,
    -	// filtering access based on the allowed groups.
    -	if settings != nil && settings.JWTGroupsEnabled {
    -		if allowedGroups := settings.JWTAllowGroups; len(allowedGroups) > 0 {
    -			userJWTGroups := extractJWTGroups(ctx, settings.JWTGroupsClaimName, claims)
    -
    -			if !userHasAllowedGroup(allowedGroups, userJWTGroups) {
    -				return fmt.Errorf("user does not belong to any of the allowed JWT groups")
    -			}
    -		}
    -	}
    -
    -	return nil
    -}
    -
     func (am *DefaultAccountManager) onPeersInvalidated(ctx context.Context, accountID string) {
     	log.WithContext(ctx).Debugf("validated peers has been invalidated for account %s", accountID)
     	am.UpdateAccountPeers(ctx, accountID)
    @@ -1802,39 +1709,6 @@ func newAccountWithId(ctx context.Context, accountID, userID, domain string) *ty
     	return acc
     }
     
    -// extractJWTGroups extracts the group names from a JWT token's claims.
    -func extractJWTGroups(ctx context.Context, claimName string, claims jwtclaims.AuthorizationClaims) []string {
    -	userJWTGroups := make([]string, 0)
    -
    -	if claim, ok := claims.Raw[claimName]; ok {
    -		if claimGroups, ok := claim.([]interface{}); ok {
    -			for _, g := range claimGroups {
    -				if group, ok := g.(string); ok {
    -					userJWTGroups = append(userJWTGroups, group)
    -				} else {
    -					log.WithContext(ctx).Debugf("JWT claim %q contains a non-string group (type: %T): %v", claimName, g, g)
    -				}
    -			}
    -		}
    -	} else {
    -		log.WithContext(ctx).Debugf("JWT claim %q is not a string array", claimName)
    -	}
    -
    -	return userJWTGroups
    -}
    -
    -// userHasAllowedGroup checks if a user belongs to any of the allowed groups.
    -func userHasAllowedGroup(allowedGroups []string, userGroups []string) bool {
    -	for _, userGroup := range userGroups {
    -		for _, allowedGroup := range allowedGroups {
    -			if userGroup == allowedGroup {
    -				return true
    -			}
    -		}
    -	}
    -	return false
    -}
    -
     // separateGroups separates user's auto groups into non-JWT and JWT groups.
     // Returns the list of standard auto groups and a map of JWT auto groups,
     // where the keys are the group names and the values are the group IDs.
    diff --git a/management/server/account_test.go b/management/server/account_test.go
    index 7d59544e0..f203e2066 100644
    --- a/management/server/account_test.go
    +++ b/management/server/account_test.go
    @@ -2,8 +2,6 @@ package server
     
     import (
     	"context"
    -	"crypto/sha256"
    -	b64 "encoding/base64"
     	"encoding/json"
     	"fmt"
     	"io"
    @@ -15,8 +13,6 @@ import (
     	"testing"
     	"time"
     
    -	"github.com/golang-jwt/jwt"
    -
     	"github.com/netbirdio/netbird/management/server/util"
     
     	resourceTypes "github.com/netbirdio/netbird/management/server/networks/resources/types"
    @@ -30,7 +26,7 @@ import (
     
     	nbdns "github.com/netbirdio/netbird/dns"
     	"github.com/netbirdio/netbird/management/server/activity"
    -	"github.com/netbirdio/netbird/management/server/jwtclaims"
    +	nbcontext "github.com/netbirdio/netbird/management/server/context"
     	nbpeer "github.com/netbirdio/netbird/management/server/peer"
     	"github.com/netbirdio/netbird/management/server/posture"
     	"github.com/netbirdio/netbird/management/server/store"
    @@ -437,7 +433,7 @@ func TestAccountManager_GetOrCreateAccountByUser(t *testing.T) {
     }
     
     func TestDefaultAccountManager_GetAccountIDFromToken(t *testing.T) {
    -	type initUserParams jwtclaims.AuthorizationClaims
    +	type initUserParams nbcontext.UserAuth
     
     	var (
     		publicDomain  = "public.com"
    @@ -460,7 +456,7 @@ func TestDefaultAccountManager_GetAccountIDFromToken(t *testing.T) {
     
     	testCases := []struct {
     		name                        string
    -		inputClaims                 jwtclaims.AuthorizationClaims
    +		inputClaims                 nbcontext.UserAuth
     		inputInitUserParams         initUserParams
     		inputUpdateAttrs            bool
     		inputUpdateClaimAccount     bool
    @@ -475,7 +471,7 @@ func TestDefaultAccountManager_GetAccountIDFromToken(t *testing.T) {
     	}{
     		{
     			name: "New User With Public Domain",
    -			inputClaims: jwtclaims.AuthorizationClaims{
    +			inputClaims: nbcontext.UserAuth{
     				Domain:         publicDomain,
     				UserId:         "pub-domain-user",
     				DomainCategory: types.PublicCategory,
    @@ -492,7 +488,7 @@ func TestDefaultAccountManager_GetAccountIDFromToken(t *testing.T) {
     		},
     		{
     			name: "New User With Unknown Domain",
    -			inputClaims: jwtclaims.AuthorizationClaims{
    +			inputClaims: nbcontext.UserAuth{
     				Domain:         unknownDomain,
     				UserId:         "unknown-domain-user",
     				DomainCategory: types.UnknownCategory,
    @@ -509,7 +505,7 @@ func TestDefaultAccountManager_GetAccountIDFromToken(t *testing.T) {
     		},
     		{
     			name: "New User With Private Domain",
    -			inputClaims: jwtclaims.AuthorizationClaims{
    +			inputClaims: nbcontext.UserAuth{
     				Domain:         privateDomain,
     				UserId:         "pvt-domain-user",
     				DomainCategory: types.PrivateCategory,
    @@ -526,7 +522,7 @@ func TestDefaultAccountManager_GetAccountIDFromToken(t *testing.T) {
     		},
     		{
     			name: "New Regular User With Existing Private Domain",
    -			inputClaims: jwtclaims.AuthorizationClaims{
    +			inputClaims: nbcontext.UserAuth{
     				Domain:         privateDomain,
     				UserId:         "new-pvt-domain-user",
     				DomainCategory: types.PrivateCategory,
    @@ -544,7 +540,7 @@ func TestDefaultAccountManager_GetAccountIDFromToken(t *testing.T) {
     		},
     		{
     			name: "Existing User With Existing Reclassified Private Domain",
    -			inputClaims: jwtclaims.AuthorizationClaims{
    +			inputClaims: nbcontext.UserAuth{
     				Domain:         defaultInitAccount.Domain,
     				UserId:         defaultInitAccount.UserId,
     				DomainCategory: types.PrivateCategory,
    @@ -561,7 +557,7 @@ func TestDefaultAccountManager_GetAccountIDFromToken(t *testing.T) {
     		},
     		{
     			name: "Existing Account Id With Existing Reclassified Private Domain",
    -			inputClaims: jwtclaims.AuthorizationClaims{
    +			inputClaims: nbcontext.UserAuth{
     				Domain:         defaultInitAccount.Domain,
     				UserId:         defaultInitAccount.UserId,
     				DomainCategory: types.PrivateCategory,
    @@ -579,7 +575,7 @@ func TestDefaultAccountManager_GetAccountIDFromToken(t *testing.T) {
     		},
     		{
     			name: "User With Private Category And Empty Domain",
    -			inputClaims: jwtclaims.AuthorizationClaims{
    +			inputClaims: nbcontext.UserAuth{
     				Domain:         "",
     				UserId:         "pvt-domain-user",
     				DomainCategory: types.PrivateCategory,
    @@ -608,7 +604,7 @@ func TestDefaultAccountManager_GetAccountIDFromToken(t *testing.T) {
     			require.NoError(t, err, "get init account failed")
     
     			if testCase.inputUpdateAttrs {
    -				err = manager.updateAccountDomainAttributesIfNotUpToDate(context.Background(), initAccount.Id, jwtclaims.AuthorizationClaims{UserId: testCase.inputInitUserParams.UserId, Domain: testCase.inputInitUserParams.Domain, DomainCategory: testCase.inputInitUserParams.DomainCategory}, true)
    +				err = manager.updateAccountDomainAttributesIfNotUpToDate(context.Background(), initAccount.Id, nbcontext.UserAuth{UserId: testCase.inputInitUserParams.UserId, Domain: testCase.inputInitUserParams.Domain, DomainCategory: testCase.inputInitUserParams.DomainCategory}, true)
     				require.NoError(t, err, "update init user failed")
     			}
     
    @@ -616,7 +612,7 @@ func TestDefaultAccountManager_GetAccountIDFromToken(t *testing.T) {
     				testCase.inputClaims.AccountId = initAccount.Id
     			}
     
    -			accountID, _, err = manager.GetAccountIDFromToken(context.Background(), testCase.inputClaims)
    +			accountID, _, err = manager.GetAccountIDFromUserAuth(context.Background(), testCase.inputClaims)
     			require.NoError(t, err, "support function failed")
     
     			account, err := manager.Store.GetAccount(context.Background(), accountID)
    @@ -635,14 +631,12 @@ func TestDefaultAccountManager_GetAccountIDFromToken(t *testing.T) {
     	}
     }
     
    -func TestDefaultAccountManager_GetGroupsFromTheToken(t *testing.T) {
    +func TestDefaultAccountManager_SyncUserJWTGroups(t *testing.T) {
     	userId := "user-id"
     	domain := "test.domain"
    -
     	_ = newAccountWithId(context.Background(), "", userId, domain)
     	manager, err := createManager(t)
     	require.NoError(t, err, "unable to create account manager")
    -
     	accountID, err := manager.GetAccountIDByUserID(context.Background(), userId, domain)
     	require.NoError(t, err, "create init user failed")
     	// as initAccount was created without account id we have to take the id after account initialization
    @@ -650,65 +644,50 @@ func TestDefaultAccountManager_GetGroupsFromTheToken(t *testing.T) {
     	// it is important to set the id as it help to avoid creating additional account with empty Id and re-pointing indices to it
     	initAccount, err := manager.Store.GetAccount(context.Background(), accountID)
     	require.NoError(t, err, "get init account failed")
    -
    -	claims := jwtclaims.AuthorizationClaims{
    +	claims := nbcontext.UserAuth{
     		AccountId:      accountID, // is empty as it is based on accountID right after initialization of initAccount
     		Domain:         domain,
     		UserId:         userId,
     		DomainCategory: "test-category",
    -		Raw:            jwt.MapClaims{"idp-groups": []interface{}{"group1", "group2"}},
    +		Groups:         []string{"group1", "group2"},
     	}
    -
     	t.Run("JWT groups disabled", func(t *testing.T) {
    -		accountID, _, err := manager.GetAccountIDFromToken(context.Background(), claims)
    -		require.NoError(t, err, "get account by token failed")
    -
    +		err := manager.SyncUserJWTGroups(context.Background(), claims)
    +		require.NoError(t, err, "synt user jwt groups failed")
     		account, err := manager.Store.GetAccount(context.Background(), accountID)
     		require.NoError(t, err, "get account failed")
    -
     		require.Len(t, account.Groups, 1, "only ALL group should exists")
     	})
    -
     	t.Run("JWT groups enabled without claim name", func(t *testing.T) {
     		initAccount.Settings.JWTGroupsEnabled = true
     		err := manager.Store.SaveAccount(context.Background(), initAccount)
     		require.NoError(t, err, "save account failed")
     		require.Len(t, manager.Store.GetAllAccounts(context.Background()), 1, "only one account should exist")
    -
    -		accountID, _, err := manager.GetAccountIDFromToken(context.Background(), claims)
    -		require.NoError(t, err, "get account by token failed")
    -
    +		err = manager.SyncUserJWTGroups(context.Background(), claims)
    +		require.NoError(t, err, "synt user jwt groups failed")
     		account, err := manager.Store.GetAccount(context.Background(), accountID)
     		require.NoError(t, err, "get account failed")
    -
     		require.Len(t, account.Groups, 1, "if group claim is not set no group added from JWT")
     	})
    -
     	t.Run("JWT groups enabled", func(t *testing.T) {
     		initAccount.Settings.JWTGroupsEnabled = true
     		initAccount.Settings.JWTGroupsClaimName = "idp-groups"
     		err := manager.Store.SaveAccount(context.Background(), initAccount)
     		require.NoError(t, err, "save account failed")
     		require.Len(t, manager.Store.GetAllAccounts(context.Background()), 1, "only one account should exist")
    -
    -		accountID, _, err := manager.GetAccountIDFromToken(context.Background(), claims)
    -		require.NoError(t, err, "get account by token failed")
    -
    +		err = manager.SyncUserJWTGroups(context.Background(), claims)
    +		require.NoError(t, err, "synt user jwt groups failed")
     		account, err := manager.Store.GetAccount(context.Background(), accountID)
     		require.NoError(t, err, "get account failed")
    -
     		require.Len(t, account.Groups, 3, "groups should be added to the account")
    -
     		groupsByNames := map[string]*types.Group{}
     		for _, g := range account.Groups {
     			groupsByNames[g.Name] = g
     		}
    -
     		g1, ok := groupsByNames["group1"]
     		require.True(t, ok, "group1 should be added to the account")
     		require.Equal(t, g1.Name, "group1", "group1 name should match")
     		require.Equal(t, g1.Issued, types.GroupIssuedJWT, "group1 issued should match")
    -
     		g2, ok := groupsByNames["group2"]
     		require.True(t, ok, "group2 should be added to the account")
     		require.Equal(t, g2.Name, "group2", "group2 name should match")
    @@ -716,88 +695,6 @@ func TestDefaultAccountManager_GetGroupsFromTheToken(t *testing.T) {
     	})
     }
     
    -func TestAccountManager_GetAccountFromPAT(t *testing.T) {
    -	store, cleanup, err := store.NewTestStoreFromSQL(context.Background(), "", t.TempDir())
    -	if err != nil {
    -		t.Fatalf("Error when creating store: %s", err)
    -	}
    -	t.Cleanup(cleanup)
    -	account := newAccountWithId(context.Background(), "account_id", "testuser", "")
    -
    -	token := "nbp_9999EUDNdkeusjentDLSJEn1902u84390W6W"
    -	hashedToken := sha256.Sum256([]byte(token))
    -	encodedHashedToken := b64.StdEncoding.EncodeToString(hashedToken[:])
    -	account.Users["someUser"] = &types.User{
    -		Id: "someUser",
    -		PATs: map[string]*types.PersonalAccessToken{
    -			"tokenId": {
    -				ID:          "tokenId",
    -				UserID:      "someUser",
    -				HashedToken: encodedHashedToken,
    -			},
    -		},
    -	}
    -	err = store.SaveAccount(context.Background(), account)
    -	if err != nil {
    -		t.Fatalf("Error when saving account: %s", err)
    -	}
    -
    -	am := DefaultAccountManager{
    -		Store: store,
    -	}
    -
    -	user, pat, _, _, err := am.GetPATInfo(context.Background(), token)
    -	if err != nil {
    -		t.Fatalf("Error when getting Account from PAT: %s", err)
    -	}
    -
    -	assert.Equal(t, "account_id", user.AccountID)
    -	assert.Equal(t, "someUser", user.Id)
    -	assert.Equal(t, account.Users["someUser"].PATs["tokenId"].ID, pat.ID)
    -}
    -
    -func TestDefaultAccountManager_MarkPATUsed(t *testing.T) {
    -	store, cleanup, err := store.NewTestStoreFromSQL(context.Background(), "", t.TempDir())
    -	if err != nil {
    -		t.Fatalf("Error when creating store: %s", err)
    -	}
    -	t.Cleanup(cleanup)
    -
    -	account := newAccountWithId(context.Background(), "account_id", "testuser", "")
    -
    -	token := "nbp_9999EUDNdkeusjentDLSJEn1902u84390W6W"
    -	hashedToken := sha256.Sum256([]byte(token))
    -	encodedHashedToken := b64.StdEncoding.EncodeToString(hashedToken[:])
    -	account.Users["someUser"] = &types.User{
    -		Id: "someUser",
    -		PATs: map[string]*types.PersonalAccessToken{
    -			"tokenId": {
    -				ID:          "tokenId",
    -				HashedToken: encodedHashedToken,
    -			},
    -		},
    -	}
    -	err = store.SaveAccount(context.Background(), account)
    -	if err != nil {
    -		t.Fatalf("Error when saving account: %s", err)
    -	}
    -
    -	am := DefaultAccountManager{
    -		Store: store,
    -	}
    -
    -	err = am.MarkPATUsed(context.Background(), "tokenId")
    -	if err != nil {
    -		t.Fatalf("Error when marking PAT used: %s", err)
    -	}
    -
    -	account, err = am.Store.GetAccount(context.Background(), "account_id")
    -	if err != nil {
    -		t.Fatalf("Error when getting account: %s", err)
    -	}
    -	assert.True(t, !account.Users["someUser"].PATs["tokenId"].GetLastUsed().IsZero())
    -}
    -
     func TestAccountManager_PrivateAccount(t *testing.T) {
     	manager, err := createManager(t)
     	if err != nil {
    @@ -962,13 +859,13 @@ func TestAccountManager_DeleteAccount(t *testing.T) {
     }
     
     func BenchmarkTest_GetAccountWithclaims(b *testing.B) {
    -	claims := jwtclaims.AuthorizationClaims{
    +	claims := nbcontext.UserAuth{
     		Domain:         "example.com",
     		UserId:         "pvt-domain-user",
     		DomainCategory: types.PrivateCategory,
     	}
     
    -	publicClaims := jwtclaims.AuthorizationClaims{
    +	publicClaims := nbcontext.UserAuth{
     		Domain:         "test.com",
     		UserId:         "public-domain-user",
     		DomainCategory: types.PublicCategory,
    @@ -2683,11 +2580,13 @@ func TestAccount_SetJWTGroups(t *testing.T) {
     	assert.NoError(t, manager.Store.SaveAccount(context.Background(), account), "unable to save account")
     
     	t.Run("skip sync for token auth type", func(t *testing.T) {
    -		claims := jwtclaims.AuthorizationClaims{
    -			UserId: "user1",
    -			Raw:    jwt.MapClaims{"groups": []interface{}{"group3"}, "is_token": true},
    +		claims := nbcontext.UserAuth{
    +			UserId:    "user1",
    +			AccountId: "accountID",
    +			Groups:    []string{"group3"},
    +			IsPAT:     true,
     		}
    -		err = manager.syncJWTGroups(context.Background(), "accountID", claims)
    +		err = manager.SyncUserJWTGroups(context.Background(), claims)
     		assert.NoError(t, err, "unable to sync jwt groups")
     
     		user, err := manager.Store.GetUserByUserID(context.Background(), store.LockingStrengthShare, "user1")
    @@ -2696,11 +2595,12 @@ func TestAccount_SetJWTGroups(t *testing.T) {
     	})
     
     	t.Run("empty jwt groups", func(t *testing.T) {
    -		claims := jwtclaims.AuthorizationClaims{
    -			UserId: "user1",
    -			Raw:    jwt.MapClaims{"groups": []interface{}{}},
    +		claims := nbcontext.UserAuth{
    +			UserId:    "user1",
    +			AccountId: "accountID",
    +			Groups:    []string{},
     		}
    -		err := manager.syncJWTGroups(context.Background(), "accountID", claims)
    +		err := manager.SyncUserJWTGroups(context.Background(), claims)
     		assert.NoError(t, err, "unable to sync jwt groups")
     
     		user, err := manager.Store.GetUserByUserID(context.Background(), store.LockingStrengthShare, "user1")
    @@ -2709,11 +2609,12 @@ func TestAccount_SetJWTGroups(t *testing.T) {
     	})
     
     	t.Run("jwt match existing api group", func(t *testing.T) {
    -		claims := jwtclaims.AuthorizationClaims{
    -			UserId: "user1",
    -			Raw:    jwt.MapClaims{"groups": []interface{}{"group1"}},
    +		claims := nbcontext.UserAuth{
    +			UserId:    "user1",
    +			AccountId: "accountID",
    +			Groups:    []string{"group1"},
     		}
    -		err := manager.syncJWTGroups(context.Background(), "accountID", claims)
    +		err := manager.SyncUserJWTGroups(context.Background(), claims)
     		assert.NoError(t, err, "unable to sync jwt groups")
     
     		user, err := manager.Store.GetUserByUserID(context.Background(), store.LockingStrengthShare, "user1")
    @@ -2729,11 +2630,12 @@ func TestAccount_SetJWTGroups(t *testing.T) {
     		account.Users["user1"].AutoGroups = []string{"group1"}
     		assert.NoError(t, manager.Store.SaveUser(context.Background(), store.LockingStrengthUpdate, account.Users["user1"]))
     
    -		claims := jwtclaims.AuthorizationClaims{
    -			UserId: "user1",
    -			Raw:    jwt.MapClaims{"groups": []interface{}{"group1"}},
    +		claims := nbcontext.UserAuth{
    +			UserId:    "user1",
    +			AccountId: "accountID",
    +			Groups:    []string{"group1"},
     		}
    -		err = manager.syncJWTGroups(context.Background(), "accountID", claims)
    +		err = manager.SyncUserJWTGroups(context.Background(), claims)
     		assert.NoError(t, err, "unable to sync jwt groups")
     
     		user, err := manager.Store.GetUserByUserID(context.Background(), store.LockingStrengthShare, "user1")
    @@ -2746,11 +2648,12 @@ func TestAccount_SetJWTGroups(t *testing.T) {
     	})
     
     	t.Run("add jwt group", func(t *testing.T) {
    -		claims := jwtclaims.AuthorizationClaims{
    -			UserId: "user1",
    -			Raw:    jwt.MapClaims{"groups": []interface{}{"group1", "group2"}},
    +		claims := nbcontext.UserAuth{
    +			UserId:    "user1",
    +			AccountId: "accountID",
    +			Groups:    []string{"group1", "group2"},
     		}
    -		err = manager.syncJWTGroups(context.Background(), "accountID", claims)
    +		err = manager.SyncUserJWTGroups(context.Background(), claims)
     		assert.NoError(t, err, "unable to sync jwt groups")
     
     		user, err := manager.Store.GetUserByUserID(context.Background(), store.LockingStrengthShare, "user1")
    @@ -2759,11 +2662,12 @@ func TestAccount_SetJWTGroups(t *testing.T) {
     	})
     
     	t.Run("existed group not update", func(t *testing.T) {
    -		claims := jwtclaims.AuthorizationClaims{
    -			UserId: "user1",
    -			Raw:    jwt.MapClaims{"groups": []interface{}{"group2"}},
    +		claims := nbcontext.UserAuth{
    +			UserId:    "user1",
    +			AccountId: "accountID",
    +			Groups:    []string{"group2"},
     		}
    -		err = manager.syncJWTGroups(context.Background(), "accountID", claims)
    +		err = manager.SyncUserJWTGroups(context.Background(), claims)
     		assert.NoError(t, err, "unable to sync jwt groups")
     
     		user, err := manager.Store.GetUserByUserID(context.Background(), store.LockingStrengthShare, "user1")
    @@ -2772,11 +2676,12 @@ func TestAccount_SetJWTGroups(t *testing.T) {
     	})
     
     	t.Run("add new group", func(t *testing.T) {
    -		claims := jwtclaims.AuthorizationClaims{
    -			UserId: "user2",
    -			Raw:    jwt.MapClaims{"groups": []interface{}{"group1", "group3"}},
    +		claims := nbcontext.UserAuth{
    +			UserId:    "user2",
    +			AccountId: "accountID",
    +			Groups:    []string{"group1", "group3"},
     		}
    -		err = manager.syncJWTGroups(context.Background(), "accountID", claims)
    +		err = manager.SyncUserJWTGroups(context.Background(), claims)
     		assert.NoError(t, err, "unable to sync jwt groups")
     
     		groups, err := manager.Store.GetAccountGroups(context.Background(), store.LockingStrengthShare, "accountID")
    @@ -2789,11 +2694,12 @@ func TestAccount_SetJWTGroups(t *testing.T) {
     	})
     
     	t.Run("remove all JWT groups when list is empty", func(t *testing.T) {
    -		claims := jwtclaims.AuthorizationClaims{
    -			UserId: "user1",
    -			Raw:    jwt.MapClaims{"groups": []interface{}{}},
    +		claims := nbcontext.UserAuth{
    +			UserId:    "user1",
    +			AccountId: "accountID",
    +			Groups:    []string{},
     		}
    -		err = manager.syncJWTGroups(context.Background(), "accountID", claims)
    +		err = manager.SyncUserJWTGroups(context.Background(), claims)
     		assert.NoError(t, err, "unable to sync jwt groups")
     
     		user, err := manager.Store.GetUserByUserID(context.Background(), store.LockingStrengthShare, "user1")
    @@ -2803,11 +2709,12 @@ func TestAccount_SetJWTGroups(t *testing.T) {
     	})
     
     	t.Run("remove all JWT groups when claim does not exist", func(t *testing.T) {
    -		claims := jwtclaims.AuthorizationClaims{
    -			UserId: "user2",
    -			Raw:    jwt.MapClaims{},
    +		claims := nbcontext.UserAuth{
    +			UserId:    "user2",
    +			AccountId: "accountID",
    +			Groups:    []string{},
     		}
    -		err = manager.syncJWTGroups(context.Background(), "accountID", claims)
    +		err = manager.SyncUserJWTGroups(context.Background(), claims)
     		assert.NoError(t, err, "unable to sync jwt groups")
     
     		user, err := manager.Store.GetUserByUserID(context.Background(), store.LockingStrengthShare, "user2")
    diff --git a/management/server/jwtclaims/extractor.go b/management/server/auth/jwt/extractor.go
    similarity index 51%
    rename from management/server/jwtclaims/extractor.go
    rename to management/server/auth/jwt/extractor.go
    index 18214b434..fab429125 100644
    --- a/management/server/jwtclaims/extractor.go
    +++ b/management/server/auth/jwt/extractor.go
    @@ -1,15 +1,17 @@
    -package jwtclaims
    +package jwt
     
     import (
    -	"net/http"
    +	"errors"
    +	"net/url"
     	"time"
     
     	"github.com/golang-jwt/jwt"
    +	log "github.com/sirupsen/logrus"
    +
    +	nbcontext "github.com/netbirdio/netbird/management/server/context"
     )
     
     const (
    -	// TokenUserProperty key for the user property in the request context
    -	TokenUserProperty = "user"
     	// AccountIDSuffix suffix for the account id claim
     	AccountIDSuffix = "wt_account_id"
     	// DomainIDSuffix suffix for the domain id claim
    @@ -22,19 +24,16 @@ const (
     	LastLoginSuffix = "nb_last_login"
     	// Invited claim indicates that an incoming JWT is from a user that just accepted an invitation
     	Invited = "nb_invited"
    -	// IsToken claim indicates that auth type from the user is a token
    -	IsToken = "is_token"
     )
     
    -// ExtractClaims Extract function type
    -type ExtractClaims func(r *http.Request) AuthorizationClaims
    +var (
    +	errUserIDClaimEmpty = errors.New("user ID claim token value is empty")
    +)
     
     // ClaimsExtractor struct that holds the extract function
     type ClaimsExtractor struct {
     	authAudience string
     	userIDClaim  string
    -
    -	FromRequestContext ExtractClaims
     }
     
     // ClaimsExtractorOption is a function that configures the ClaimsExtractor
    @@ -54,13 +53,6 @@ func WithUserIDClaim(userIDClaim string) ClaimsExtractorOption {
     	}
     }
     
    -// WithFromRequestContext sets the function that extracts claims from the request context
    -func WithFromRequestContext(ec ExtractClaims) ClaimsExtractorOption {
    -	return func(c *ClaimsExtractor) {
    -		c.FromRequestContext = ec
    -	}
    -}
    -
     // NewClaimsExtractor returns an extractor, and if provided with a function with ExtractClaims signature,
     // then it will use that logic. Uses ExtractClaimsFromRequestContext by default
     func NewClaimsExtractor(options ...ClaimsExtractorOption) *ClaimsExtractor {
    @@ -68,49 +60,13 @@ func NewClaimsExtractor(options ...ClaimsExtractorOption) *ClaimsExtractor {
     	for _, option := range options {
     		option(ce)
     	}
    -	if ce.FromRequestContext == nil {
    -		ce.FromRequestContext = ce.fromRequestContext
    -	}
    +
     	if ce.userIDClaim == "" {
     		ce.userIDClaim = UserIDClaim
     	}
     	return ce
     }
     
    -// FromToken extracts claims from the token (after auth)
    -func (c *ClaimsExtractor) FromToken(token *jwt.Token) AuthorizationClaims {
    -	claims := token.Claims.(jwt.MapClaims)
    -	jwtClaims := AuthorizationClaims{
    -		Raw: claims,
    -	}
    -	userID, ok := claims[c.userIDClaim].(string)
    -	if !ok {
    -		return jwtClaims
    -	}
    -	jwtClaims.UserId = userID
    -	accountIDClaim, ok := claims[c.authAudience+AccountIDSuffix]
    -	if ok {
    -		jwtClaims.AccountId = accountIDClaim.(string)
    -	}
    -	domainClaim, ok := claims[c.authAudience+DomainIDSuffix]
    -	if ok {
    -		jwtClaims.Domain = domainClaim.(string)
    -	}
    -	domainCategoryClaim, ok := claims[c.authAudience+DomainCategorySuffix]
    -	if ok {
    -		jwtClaims.DomainCategory = domainCategoryClaim.(string)
    -	}
    -	LastLoginClaimString, ok := claims[c.authAudience+LastLoginSuffix]
    -	if ok {
    -		jwtClaims.LastLogin = parseTime(LastLoginClaimString.(string))
    -	}
    -	invitedBool, ok := claims[c.authAudience+Invited]
    -	if ok {
    -		jwtClaims.Invited = invitedBool.(bool)
    -	}
    -	return jwtClaims
    -}
    -
     func parseTime(timeString string) time.Time {
     	if timeString == "" {
     		return time.Time{}
    @@ -122,11 +78,67 @@ func parseTime(timeString string) time.Time {
     	return parsedTime
     }
     
    -// fromRequestContext extracts claims from the request context previously filled by the JWT token (after auth)
    -func (c *ClaimsExtractor) fromRequestContext(r *http.Request) AuthorizationClaims {
    -	if r.Context().Value(TokenUserProperty) == nil {
    -		return AuthorizationClaims{}
    +func (c ClaimsExtractor) audienceClaim(claimName string) string {
    +	url, err := url.JoinPath(c.authAudience, claimName)
    +	if err != nil {
    +		return c.authAudience + claimName // as it was previously
     	}
    -	token := r.Context().Value(TokenUserProperty).(*jwt.Token)
    -	return c.FromToken(token)
    +
    +	return url
    +}
    +
    +func (c *ClaimsExtractor) ToUserAuth(token *jwt.Token) (nbcontext.UserAuth, error) {
    +	claims := token.Claims.(jwt.MapClaims)
    +	userAuth := nbcontext.UserAuth{}
    +
    +	userID, ok := claims[c.userIDClaim].(string)
    +	if !ok {
    +		return userAuth, errUserIDClaimEmpty
    +	}
    +	userAuth.UserId = userID
    +
    +	if accountIDClaim, ok := claims[c.audienceClaim(AccountIDSuffix)]; ok {
    +		userAuth.AccountId = accountIDClaim.(string)
    +	}
    +
    +	if domainClaim, ok := claims[c.audienceClaim(DomainIDSuffix)]; ok {
    +		userAuth.Domain = domainClaim.(string)
    +	}
    +
    +	if domainCategoryClaim, ok := claims[c.audienceClaim(DomainCategorySuffix)]; ok {
    +		userAuth.DomainCategory = domainCategoryClaim.(string)
    +	}
    +
    +	if lastLoginClaimString, ok := claims[c.audienceClaim(LastLoginSuffix)]; ok {
    +		userAuth.LastLogin = parseTime(lastLoginClaimString.(string))
    +	}
    +
    +	if invitedBool, ok := claims[c.audienceClaim(Invited)]; ok {
    +		if value, ok := invitedBool.(bool); ok {
    +			userAuth.Invited = value
    +		}
    +	}
    +
    +	return userAuth, nil
    +}
    +
    +func (c *ClaimsExtractor) ToGroups(token *jwt.Token, claimName string) []string {
    +	claims := token.Claims.(jwt.MapClaims)
    +	userJWTGroups := make([]string, 0)
    +
    +	if claim, ok := claims[claimName]; ok {
    +		if claimGroups, ok := claim.([]interface{}); ok {
    +			for _, g := range claimGroups {
    +				if group, ok := g.(string); ok {
    +					userJWTGroups = append(userJWTGroups, group)
    +				} else {
    +					log.Debugf("JWT claim %q contains a non-string group (type: %T): %v", claimName, g, g)
    +				}
    +			}
    +		}
    +	} else {
    +		log.Debugf("JWT claim %q is not a string array", claimName)
    +	}
    +
    +	return userJWTGroups
     }
    diff --git a/management/server/auth/jwt/validator.go b/management/server/auth/jwt/validator.go
    new file mode 100644
    index 000000000..5b38ca786
    --- /dev/null
    +++ b/management/server/auth/jwt/validator.go
    @@ -0,0 +1,302 @@
    +package jwt
    +
    +import (
    +	"context"
    +	"crypto/ecdsa"
    +	"crypto/elliptic"
    +	"crypto/rsa"
    +	"encoding/base64"
    +	"encoding/json"
    +	"errors"
    +	"fmt"
    +	"math/big"
    +	"net/http"
    +	"net/url"
    +	"strconv"
    +	"strings"
    +	"sync"
    +	"time"
    +
    +	"github.com/golang-jwt/jwt"
    +
    +	log "github.com/sirupsen/logrus"
    +)
    +
    +// Jwks is a collection of JSONWebKey obtained from Config.HttpServerConfig.AuthKeysLocation
    +type Jwks struct {
    +	Keys          []JSONWebKey `json:"keys"`
    +	expiresInTime time.Time
    +}
    +
    +// The supported elliptic curves types
    +const (
    +	// p256 represents a cryptographic elliptical curve type.
    +	p256 = "P-256"
    +
    +	// p384 represents a cryptographic elliptical curve type.
    +	p384 = "P-384"
    +
    +	// p521 represents a cryptographic elliptical curve type.
    +	p521 = "P-521"
    +)
    +
    +// JSONWebKey is a representation of a Jason Web Key
    +type JSONWebKey struct {
    +	Kty string   `json:"kty"`
    +	Kid string   `json:"kid"`
    +	Use string   `json:"use"`
    +	N   string   `json:"n"`
    +	E   string   `json:"e"`
    +	Crv string   `json:"crv"`
    +	X   string   `json:"x"`
    +	Y   string   `json:"y"`
    +	X5c []string `json:"x5c"`
    +}
    +
    +type Validator struct {
    +	lock                     sync.Mutex
    +	issuer                   string
    +	audienceList             []string
    +	keysLocation             string
    +	idpSignkeyRefreshEnabled bool
    +	keys                     *Jwks
    +}
    +
    +var (
    +	errKeyNotFound     = errors.New("unable to find appropriate key")
    +	errInvalidAudience = errors.New("invalid audience")
    +	errInvalidIssuer   = errors.New("invalid issuer")
    +	errTokenEmpty      = errors.New("required authorization token not found")
    +	errTokenInvalid    = errors.New("token is invalid")
    +	errTokenParsing    = errors.New("token could not be parsed")
    +)
    +
    +func NewValidator(issuer string, audienceList []string, keysLocation string, idpSignkeyRefreshEnabled bool) *Validator {
    +	keys, err := getPemKeys(keysLocation)
    +	if err != nil {
    +		log.WithField("keysLocation", keysLocation).Errorf("could not get keys from location: %s", err)
    +	}
    +
    +	return &Validator{
    +		keys:                     keys,
    +		issuer:                   issuer,
    +		audienceList:             audienceList,
    +		keysLocation:             keysLocation,
    +		idpSignkeyRefreshEnabled: idpSignkeyRefreshEnabled,
    +	}
    +}
    +
    +func (v *Validator) getKeyFunc(ctx context.Context) jwt.Keyfunc {
    +	return func(token *jwt.Token) (interface{}, error) {
    +		// Verify 'aud' claim
    +		var checkAud bool
    +		for _, audience := range v.audienceList {
    +			checkAud = token.Claims.(jwt.MapClaims).VerifyAudience(audience, false)
    +			if checkAud {
    +				break
    +			}
    +		}
    +		if !checkAud {
    +			return token, errInvalidAudience
    +		}
    +
    +		// Verify 'issuer' claim
    +		checkIss := token.Claims.(jwt.MapClaims).VerifyIssuer(v.issuer, false)
    +		if !checkIss {
    +			return token, errInvalidIssuer
    +		}
    +
    +		// If keys are rotated, verify the keys prior to token validation
    +		if v.idpSignkeyRefreshEnabled {
    +			// If the keys are invalid, retrieve new ones
    +			// @todo propose a separate go routine to regularly check these to prevent blocking when actually
    +			// validating the token
    +			if !v.keys.stillValid() {
    +				v.lock.Lock()
    +				defer v.lock.Unlock()
    +
    +				refreshedKeys, err := getPemKeys(v.keysLocation)
    +				if err != nil {
    +					log.WithContext(ctx).Debugf("cannot get JSONWebKey: %v, falling back to old keys", err)
    +					refreshedKeys = v.keys
    +				}
    +
    +				log.WithContext(ctx).Debugf("keys refreshed, new UTC expiration time: %s", refreshedKeys.expiresInTime.UTC())
    +
    +				v.keys = refreshedKeys
    +			}
    +		}
    +
    +		publicKey, err := getPublicKey(token, v.keys)
    +		if err == nil {
    +			return publicKey, nil
    +		}
    +
    +		msg := fmt.Sprintf("getPublicKey error: %s", err)
    +		if errors.Is(err, errKeyNotFound) && !v.idpSignkeyRefreshEnabled {
    +			msg = fmt.Sprintf("getPublicKey error: %s. You can enable key refresh by setting HttpServerConfig.IdpSignKeyRefreshEnabled to true in your management.json file and restart the service", err)
    +		}
    +
    +		log.WithContext(ctx).Error(msg)
    +
    +		return nil, err
    +	}
    +}
    +
    +// ValidateAndParse validates the token and returns the parsed token
    +func (m *Validator) ValidateAndParse(ctx context.Context, token string) (*jwt.Token, error) {
    +	// If the token is empty...
    +	if token == "" {
    +		// If we get here, the required token is missing
    +		log.WithContext(ctx).Debugf("  Error: No credentials found (CredentialsOptional=false)")
    +		return nil, errTokenEmpty
    +	}
    +
    +	// Now parse the token
    +	parsedToken, err := jwt.Parse(token, m.getKeyFunc(ctx))
    +
    +	// Check if there was an error in parsing...
    +	if err != nil {
    +		err = fmt.Errorf("%w: %s", errTokenParsing, err)
    +		log.WithContext(ctx).Error(err.Error())
    +		return nil, err
    +	}
    +
    +	// Check if the parsed token is valid...
    +	if !parsedToken.Valid {
    +		log.WithContext(ctx).Debug(errTokenInvalid.Error())
    +		return nil, errTokenInvalid
    +	}
    +
    +	return parsedToken, nil
    +}
    +
    +// stillValid returns true if the JSONWebKey still valid and have enough time to be used
    +func (jwks *Jwks) stillValid() bool {
    +	return !jwks.expiresInTime.IsZero() && time.Now().Add(5*time.Second).Before(jwks.expiresInTime)
    +}
    +
    +func getPemKeys(keysLocation string) (*Jwks, error) {
    +	jwks := &Jwks{}
    +
    +	url, err := url.ParseRequestURI(keysLocation)
    +	if err != nil {
    +		return jwks, err
    +	}
    +
    +	resp, err := http.Get(url.String())
    +	if err != nil {
    +		return jwks, err
    +	}
    +	defer resp.Body.Close()
    +
    +	err = json.NewDecoder(resp.Body).Decode(jwks)
    +	if err != nil {
    +		return jwks, err
    +	}
    +
    +	cacheControlHeader := resp.Header.Get("Cache-Control")
    +	expiresIn := getMaxAgeFromCacheHeader(cacheControlHeader)
    +	jwks.expiresInTime = time.Now().Add(time.Duration(expiresIn) * time.Second)
    +
    +	return jwks, nil
    +}
    +
    +func getPublicKey(token *jwt.Token, jwks *Jwks) (interface{}, error) {
    +	// todo as we load the jkws when the server is starting, we should build a JKS map with the pem cert at the boot time
    +	for k := range jwks.Keys {
    +		if token.Header["kid"] != jwks.Keys[k].Kid {
    +			continue
    +		}
    +
    +		if len(jwks.Keys[k].X5c) != 0 {
    +			cert := "-----BEGIN CERTIFICATE-----\n" + jwks.Keys[k].X5c[0] + "\n-----END CERTIFICATE-----"
    +			return jwt.ParseRSAPublicKeyFromPEM([]byte(cert))
    +		}
    +
    +		if jwks.Keys[k].Kty == "RSA" {
    +			return getPublicKeyFromRSA(jwks.Keys[k])
    +		}
    +		if jwks.Keys[k].Kty == "EC" {
    +			return getPublicKeyFromECDSA(jwks.Keys[k])
    +		}
    +	}
    +
    +	return nil, errKeyNotFound
    +}
    +
    +func getPublicKeyFromECDSA(jwk JSONWebKey) (publicKey *ecdsa.PublicKey, err error) {
    +	if jwk.X == "" || jwk.Y == "" || jwk.Crv == "" {
    +		return nil, fmt.Errorf("ecdsa key incomplete")
    +	}
    +
    +	var xCoordinate []byte
    +	if xCoordinate, err = base64.RawURLEncoding.DecodeString(jwk.X); err != nil {
    +		return nil, err
    +	}
    +
    +	var yCoordinate []byte
    +	if yCoordinate, err = base64.RawURLEncoding.DecodeString(jwk.Y); err != nil {
    +		return nil, err
    +	}
    +
    +	publicKey = &ecdsa.PublicKey{}
    +
    +	var curve elliptic.Curve
    +	switch jwk.Crv {
    +	case p256:
    +		curve = elliptic.P256()
    +	case p384:
    +		curve = elliptic.P384()
    +	case p521:
    +		curve = elliptic.P521()
    +	}
    +
    +	publicKey.Curve = curve
    +	publicKey.X = big.NewInt(0).SetBytes(xCoordinate)
    +	publicKey.Y = big.NewInt(0).SetBytes(yCoordinate)
    +
    +	return publicKey, nil
    +}
    +
    +func getPublicKeyFromRSA(jwk JSONWebKey) (*rsa.PublicKey, error) {
    +	decodedE, err := base64.RawURLEncoding.DecodeString(jwk.E)
    +	if err != nil {
    +		return nil, err
    +	}
    +	decodedN, err := base64.RawURLEncoding.DecodeString(jwk.N)
    +	if err != nil {
    +		return nil, err
    +	}
    +
    +	var n, e big.Int
    +	e.SetBytes(decodedE)
    +	n.SetBytes(decodedN)
    +
    +	return &rsa.PublicKey{
    +		E: int(e.Int64()),
    +		N: &n,
    +	}, nil
    +}
    +
    +// getMaxAgeFromCacheHeader extracts max-age directive from the Cache-Control header
    +func getMaxAgeFromCacheHeader(cacheControl string) int {
    +	// Split into individual directives
    +	directives := strings.Split(cacheControl, ",")
    +
    +	for _, directive := range directives {
    +		directive = strings.TrimSpace(directive)
    +		if strings.HasPrefix(directive, "max-age=") {
    +			// Extract the max-age value
    +			maxAgeStr := strings.TrimPrefix(directive, "max-age=")
    +			maxAge, err := strconv.Atoi(maxAgeStr)
    +			if err != nil {
    +				return 0
    +			}
    +
    +			return maxAge
    +		}
    +	}
    +
    +	return 0
    +}
    diff --git a/management/server/auth/manager.go b/management/server/auth/manager.go
    new file mode 100644
    index 000000000..6835a3ced
    --- /dev/null
    +++ b/management/server/auth/manager.go
    @@ -0,0 +1,170 @@
    +package auth
    +
    +import (
    +	"context"
    +	"crypto/sha256"
    +	"encoding/base64"
    +	"fmt"
    +	"hash/crc32"
    +
    +	"github.com/golang-jwt/jwt"
    +
    +	"github.com/netbirdio/netbird/base62"
    +	nbjwt "github.com/netbirdio/netbird/management/server/auth/jwt"
    +	nbcontext "github.com/netbirdio/netbird/management/server/context"
    +	"github.com/netbirdio/netbird/management/server/store"
    +	"github.com/netbirdio/netbird/management/server/types"
    +)
    +
    +var _ Manager = (*manager)(nil)
    +
    +type Manager interface {
    +	ValidateAndParseToken(ctx context.Context, value string) (nbcontext.UserAuth, *jwt.Token, error)
    +	EnsureUserAccessByJWTGroups(ctx context.Context, userAuth nbcontext.UserAuth, token *jwt.Token) (nbcontext.UserAuth, error)
    +	MarkPATUsed(ctx context.Context, tokenID string) error
    +	GetPATInfo(ctx context.Context, token string) (user *types.User, pat *types.PersonalAccessToken, domain string, category string, err error)
    +}
    +
    +type manager struct {
    +	store store.Store
    +
    +	validator *nbjwt.Validator
    +	extractor *nbjwt.ClaimsExtractor
    +}
    +
    +func NewManager(store store.Store, issuer, audience, keysLocation, userIdClaim string, allAudiences []string, idpRefreshKeys bool) Manager {
    +	// @note if invalid/missing parameters are sent the validator will instantiate
    +	// but it will fail when validating and parsing the token
    +	jwtValidator := nbjwt.NewValidator(
    +		issuer,
    +		allAudiences,
    +		keysLocation,
    +		idpRefreshKeys,
    +	)
    +
    +	claimsExtractor := nbjwt.NewClaimsExtractor(
    +		nbjwt.WithAudience(audience),
    +		nbjwt.WithUserIDClaim(userIdClaim),
    +	)
    +
    +	return &manager{
    +		store: store,
    +
    +		validator: jwtValidator,
    +		extractor: claimsExtractor,
    +	}
    +}
    +
    +func (m *manager) ValidateAndParseToken(ctx context.Context, value string) (nbcontext.UserAuth, *jwt.Token, error) {
    +	token, err := m.validator.ValidateAndParse(ctx, value)
    +	if err != nil {
    +		return nbcontext.UserAuth{}, nil, err
    +	}
    +
    +	userAuth, err := m.extractor.ToUserAuth(token)
    +	if err != nil {
    +		return nbcontext.UserAuth{}, nil, err
    +	}
    +	return userAuth, token, err
    +}
    +
    +func (m *manager) EnsureUserAccessByJWTGroups(ctx context.Context, userAuth nbcontext.UserAuth, token *jwt.Token) (nbcontext.UserAuth, error) {
    +	if userAuth.IsChild || userAuth.IsPAT {
    +		return userAuth, nil
    +	}
    +
    +	settings, err := m.store.GetAccountSettings(ctx, store.LockingStrengthShare, userAuth.AccountId)
    +	if err != nil {
    +		return userAuth, err
    +	}
    +
    +	// Ensures JWT group synchronization to the management is enabled before,
    +	// filtering access based on the allowed groups.
    +	if settings != nil && settings.JWTGroupsEnabled {
    +		userAuth.Groups = m.extractor.ToGroups(token, settings.JWTGroupsClaimName)
    +		if allowedGroups := settings.JWTAllowGroups; len(allowedGroups) > 0 {
    +			if !userHasAllowedGroup(allowedGroups, userAuth.Groups) {
    +				return userAuth, fmt.Errorf("user does not belong to any of the allowed JWT groups")
    +			}
    +		}
    +	}
    +
    +	return userAuth, nil
    +}
    +
    +// MarkPATUsed marks a personal access token as used
    +func (am *manager) MarkPATUsed(ctx context.Context, tokenID string) error {
    +	return am.store.MarkPATUsed(ctx, store.LockingStrengthUpdate, tokenID)
    +}
    +
    +// GetPATInfo retrieves user, personal access token, domain, and category details from a personal access token.
    +func (am *manager) GetPATInfo(ctx context.Context, token string) (user *types.User, pat *types.PersonalAccessToken, domain string, category string, err error) {
    +	user, pat, err = am.extractPATFromToken(ctx, token)
    +	if err != nil {
    +		return nil, nil, "", "", err
    +	}
    +
    +	domain, category, err = am.store.GetAccountDomainAndCategory(ctx, store.LockingStrengthShare, user.AccountID)
    +	if err != nil {
    +		return nil, nil, "", "", err
    +	}
    +
    +	return user, pat, domain, category, nil
    +}
    +
    +// extractPATFromToken validates the token structure and retrieves associated User and PAT.
    +func (am *manager) extractPATFromToken(ctx context.Context, token string) (*types.User, *types.PersonalAccessToken, error) {
    +	if len(token) != types.PATLength {
    +		return nil, nil, fmt.Errorf("PAT has incorrect length")
    +	}
    +
    +	prefix := token[:len(types.PATPrefix)]
    +	if prefix != types.PATPrefix {
    +		return nil, nil, fmt.Errorf("PAT has wrong prefix")
    +	}
    +	secret := token[len(types.PATPrefix) : len(types.PATPrefix)+types.PATSecretLength]
    +	encodedChecksum := token[len(types.PATPrefix)+types.PATSecretLength : len(types.PATPrefix)+types.PATSecretLength+types.PATChecksumLength]
    +
    +	verificationChecksum, err := base62.Decode(encodedChecksum)
    +	if err != nil {
    +		return nil, nil, fmt.Errorf("PAT checksum decoding failed: %w", err)
    +	}
    +
    +	secretChecksum := crc32.ChecksumIEEE([]byte(secret))
    +	if secretChecksum != verificationChecksum {
    +		return nil, nil, fmt.Errorf("PAT checksum does not match")
    +	}
    +
    +	hashedToken := sha256.Sum256([]byte(token))
    +	encodedHashedToken := base64.StdEncoding.EncodeToString(hashedToken[:])
    +
    +	var user *types.User
    +	var pat *types.PersonalAccessToken
    +
    +	err = am.store.ExecuteInTransaction(ctx, func(transaction store.Store) error {
    +		pat, err = transaction.GetPATByHashedToken(ctx, store.LockingStrengthShare, encodedHashedToken)
    +		if err != nil {
    +			return err
    +		}
    +
    +		user, err = transaction.GetUserByPATID(ctx, store.LockingStrengthShare, pat.ID)
    +		return err
    +	})
    +	if err != nil {
    +		return nil, nil, err
    +	}
    +
    +	return user, pat, nil
    +}
    +
    +// userHasAllowedGroup checks if a user belongs to any of the allowed groups.
    +func userHasAllowedGroup(allowedGroups []string, userGroups []string) bool {
    +	for _, userGroup := range userGroups {
    +		for _, allowedGroup := range allowedGroups {
    +			if userGroup == allowedGroup {
    +				return true
    +			}
    +		}
    +	}
    +	return false
    +}
    diff --git a/management/server/auth/manager_mock.go b/management/server/auth/manager_mock.go
    new file mode 100644
    index 000000000..bc7066548
    --- /dev/null
    +++ b/management/server/auth/manager_mock.go
    @@ -0,0 +1,54 @@
    +package auth
    +
    +import (
    +	"context"
    +
    +	"github.com/golang-jwt/jwt"
    +
    +	nbcontext "github.com/netbirdio/netbird/management/server/context"
    +	"github.com/netbirdio/netbird/management/server/types"
    +)
    +
    +var (
    +	_ Manager = (*MockManager)(nil)
    +)
    +
    +// @note really dislike this mocking approach but rather than have to do additional test refactoring.
    +type MockManager struct {
    +	ValidateAndParseTokenFunc       func(ctx context.Context, value string) (nbcontext.UserAuth, *jwt.Token, error)
    +	EnsureUserAccessByJWTGroupsFunc func(ctx context.Context, userAuth nbcontext.UserAuth, token *jwt.Token) (nbcontext.UserAuth, error)
    +	MarkPATUsedFunc                 func(ctx context.Context, tokenID string) error
    +	GetPATInfoFunc                  func(ctx context.Context, token string) (user *types.User, pat *types.PersonalAccessToken, domain string, category string, err error)
    +}
    +
    +// EnsureUserAccessByJWTGroups implements Manager.
    +func (m *MockManager) EnsureUserAccessByJWTGroups(ctx context.Context, userAuth nbcontext.UserAuth, token *jwt.Token) (nbcontext.UserAuth, error) {
    +	if m.EnsureUserAccessByJWTGroupsFunc != nil {
    +		return m.EnsureUserAccessByJWTGroupsFunc(ctx, userAuth, token)
    +	}
    +	return nbcontext.UserAuth{}, nil
    +}
    +
    +// GetPATInfo implements Manager.
    +func (m *MockManager) GetPATInfo(ctx context.Context, token string) (user *types.User, pat *types.PersonalAccessToken, domain string, category string, err error) {
    +	if m.GetPATInfoFunc != nil {
    +		return m.GetPATInfoFunc(ctx, token)
    +	}
    +	return &types.User{}, &types.PersonalAccessToken{}, "", "", nil
    +}
    +
    +// MarkPATUsed implements Manager.
    +func (m *MockManager) MarkPATUsed(ctx context.Context, tokenID string) error {
    +	if m.MarkPATUsedFunc != nil {
    +		return m.MarkPATUsedFunc(ctx, tokenID)
    +	}
    +	return nil
    +}
    +
    +// ValidateAndParseToken implements Manager.
    +func (m *MockManager) ValidateAndParseToken(ctx context.Context, value string) (nbcontext.UserAuth, *jwt.Token, error) {
    +	if m.ValidateAndParseTokenFunc != nil {
    +		return m.ValidateAndParseTokenFunc(ctx, value)
    +	}
    +	return nbcontext.UserAuth{}, &jwt.Token{}, nil
    +}
    diff --git a/management/server/auth/manager_test.go b/management/server/auth/manager_test.go
    new file mode 100644
    index 000000000..55fb1e31a
    --- /dev/null
    +++ b/management/server/auth/manager_test.go
    @@ -0,0 +1,407 @@
    +package auth_test
    +
    +import (
    +	"context"
    +	"crypto/sha256"
    +	"encoding/base64"
    +	"fmt"
    +	"net/http"
    +	"net/http/httptest"
    +	"os"
    +	"strings"
    +	"testing"
    +	"time"
    +
    +	"github.com/golang-jwt/jwt"
    +	"github.com/stretchr/testify/assert"
    +	"github.com/stretchr/testify/require"
    +
    +	"github.com/netbirdio/netbird/management/server/auth"
    +	nbjwt "github.com/netbirdio/netbird/management/server/auth/jwt"
    +	nbcontext "github.com/netbirdio/netbird/management/server/context"
    +	"github.com/netbirdio/netbird/management/server/store"
    +	"github.com/netbirdio/netbird/management/server/types"
    +)
    +
    +func TestAuthManager_GetAccountInfoFromPAT(t *testing.T) {
    +	store, cleanup, err := store.NewTestStoreFromSQL(context.Background(), "", t.TempDir())
    +	if err != nil {
    +		t.Fatalf("Error when creating store: %s", err)
    +	}
    +	t.Cleanup(cleanup)
    +
    +	token := "nbp_9999EUDNdkeusjentDLSJEn1902u84390W6W"
    +	hashedToken := sha256.Sum256([]byte(token))
    +	encodedHashedToken := base64.StdEncoding.EncodeToString(hashedToken[:])
    +	account := &types.Account{
    +		Id: "account_id",
    +		Users: map[string]*types.User{"someUser": {
    +			Id: "someUser",
    +			PATs: map[string]*types.PersonalAccessToken{
    +				"tokenId": {
    +					ID:          "tokenId",
    +					UserID:      "someUser",
    +					HashedToken: encodedHashedToken,
    +				},
    +			},
    +		}},
    +	}
    +
    +	err = store.SaveAccount(context.Background(), account)
    +	if err != nil {
    +		t.Fatalf("Error when saving account: %s", err)
    +	}
    +
    +	manager := auth.NewManager(store, "", "", "", "", []string{}, false)
    +
    +	user, pat, _, _, err := manager.GetPATInfo(context.Background(), token)
    +	if err != nil {
    +		t.Fatalf("Error when getting Account from PAT: %s", err)
    +	}
    +
    +	assert.Equal(t, "account_id", user.AccountID)
    +	assert.Equal(t, "someUser", user.Id)
    +	assert.Equal(t, account.Users["someUser"].PATs["tokenId"].ID, pat.ID)
    +}
    +
    +func TestAuthManager_MarkPATUsed(t *testing.T) {
    +	store, cleanup, err := store.NewTestStoreFromSQL(context.Background(), "", t.TempDir())
    +	if err != nil {
    +		t.Fatalf("Error when creating store: %s", err)
    +	}
    +	t.Cleanup(cleanup)
    +
    +	token := "nbp_9999EUDNdkeusjentDLSJEn1902u84390W6W"
    +	hashedToken := sha256.Sum256([]byte(token))
    +	encodedHashedToken := base64.StdEncoding.EncodeToString(hashedToken[:])
    +	account := &types.Account{
    +		Id: "account_id",
    +		Users: map[string]*types.User{"someUser": {
    +			Id: "someUser",
    +			PATs: map[string]*types.PersonalAccessToken{
    +				"tokenId": {
    +					ID:          "tokenId",
    +					HashedToken: encodedHashedToken,
    +				},
    +			},
    +		}},
    +	}
    +
    +	err = store.SaveAccount(context.Background(), account)
    +	if err != nil {
    +		t.Fatalf("Error when saving account: %s", err)
    +	}
    +
    +	manager := auth.NewManager(store, "", "", "", "", []string{}, false)
    +
    +	err = manager.MarkPATUsed(context.Background(), "tokenId")
    +	if err != nil {
    +		t.Fatalf("Error when marking PAT used: %s", err)
    +	}
    +
    +	account, err = store.GetAccount(context.Background(), "account_id")
    +	if err != nil {
    +		t.Fatalf("Error when getting account: %s", err)
    +	}
    +	assert.True(t, !account.Users["someUser"].PATs["tokenId"].GetLastUsed().IsZero())
    +}
    +
    +func TestAuthManager_EnsureUserAccessByJWTGroups(t *testing.T) {
    +	store, cleanup, err := store.NewTestStoreFromSQL(context.Background(), "", t.TempDir())
    +	if err != nil {
    +		t.Fatalf("Error when creating store: %s", err)
    +	}
    +	t.Cleanup(cleanup)
    +
    +	userId := "user-id"
    +	domain := "test.domain"
    +
    +	account := &types.Account{
    +		Id:     "account_id",
    +		Domain: domain,
    +		Users: map[string]*types.User{"someUser": {
    +			Id: "someUser",
    +		}},
    +		Settings: &types.Settings{},
    +	}
    +
    +	err = store.SaveAccount(context.Background(), account)
    +	if err != nil {
    +		t.Fatalf("Error when saving account: %s", err)
    +	}
    +
    +	// this has been validated and parsed by ValidateAndParseToken
    +	userAuth := nbcontext.UserAuth{
    +		AccountId:      account.Id,
    +		Domain:         domain,
    +		UserId:         userId,
    +		DomainCategory: "test-category",
    +		// Groups:         []string{"group1", "group2"},
    +	}
    +
    +	// these tests only assert groups are parsed from token as per account settings
    +	token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{"idp-groups": []interface{}{"group1", "group2"}})
    +
    +	manager := auth.NewManager(store, "", "", "", "", []string{}, false)
    +
    +	t.Run("JWT groups disabled", func(t *testing.T) {
    +		userAuth, err := manager.EnsureUserAccessByJWTGroups(context.Background(), userAuth, token)
    +		require.NoError(t, err, "ensure user access by JWT groups failed")
    +		require.Len(t, userAuth.Groups, 0, "account not enabled to ensure access by groups")
    +	})
    +
    +	t.Run("User impersonated", func(t *testing.T) {
    +		userAuth, err := manager.EnsureUserAccessByJWTGroups(context.Background(), userAuth, token)
    +		require.NoError(t, err, "ensure user access by JWT groups failed")
    +		require.Len(t, userAuth.Groups, 0, "account not enabled to ensure access by groups")
    +	})
    +
    +	t.Run("User PAT", func(t *testing.T) {
    +		userAuth, err := manager.EnsureUserAccessByJWTGroups(context.Background(), userAuth, token)
    +		require.NoError(t, err, "ensure user access by JWT groups failed")
    +		require.Len(t, userAuth.Groups, 0, "account not enabled to ensure access by groups")
    +	})
    +
    +	t.Run("JWT groups enabled without claim name", func(t *testing.T) {
    +		account.Settings.JWTGroupsEnabled = true
    +		err := store.SaveAccount(context.Background(), account)
    +		require.NoError(t, err, "save account failed")
    +
    +		userAuth, err := manager.EnsureUserAccessByJWTGroups(context.Background(), userAuth, token)
    +		require.NoError(t, err, "ensure user access by JWT groups failed")
    +		require.Len(t, userAuth.Groups, 0, "account missing groups claim name")
    +	})
    +
    +	t.Run("JWT groups enabled without allowed groups", func(t *testing.T) {
    +		account.Settings.JWTGroupsEnabled = true
    +		account.Settings.JWTGroupsClaimName = "idp-groups"
    +		err := store.SaveAccount(context.Background(), account)
    +		require.NoError(t, err, "save account failed")
    +
    +		userAuth, err := manager.EnsureUserAccessByJWTGroups(context.Background(), userAuth, token)
    +		require.NoError(t, err, "ensure user access by JWT groups failed")
    +		require.Equal(t, []string{"group1", "group2"}, userAuth.Groups, "group parsed do not match")
    +	})
    +
    +	t.Run("User in allowed JWT groups", func(t *testing.T) {
    +		account.Settings.JWTGroupsEnabled = true
    +		account.Settings.JWTGroupsClaimName = "idp-groups"
    +		account.Settings.JWTAllowGroups = []string{"group1"}
    +		err := store.SaveAccount(context.Background(), account)
    +		require.NoError(t, err, "save account failed")
    +
    +		userAuth, err := manager.EnsureUserAccessByJWTGroups(context.Background(), userAuth, token)
    +		require.NoError(t, err, "ensure user access by JWT groups failed")
    +
    +		require.Equal(t, []string{"group1", "group2"}, userAuth.Groups, "group parsed do not match")
    +	})
    +
    +	t.Run("User not in allowed JWT groups", func(t *testing.T) {
    +		account.Settings.JWTGroupsEnabled = true
    +		account.Settings.JWTGroupsClaimName = "idp-groups"
    +		account.Settings.JWTAllowGroups = []string{"not-a-group"}
    +		err := store.SaveAccount(context.Background(), account)
    +		require.NoError(t, err, "save account failed")
    +
    +		_, err = manager.EnsureUserAccessByJWTGroups(context.Background(), userAuth, token)
    +		require.Error(t, err, "ensure user access is not in allowed groups")
    +	})
    +}
    +
    +func TestAuthManager_ValidateAndParseToken(t *testing.T) {
    +	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    +		w.Header().Add("Cache-Control", "max-age=30") // set a 30s expiry to these keys
    +		http.ServeFile(w, r, "test_data/jwks.json")
    +	}))
    +	defer server.Close()
    +
    +	issuer := "http://issuer.local"
    +	audience := "http://audience.local"
    +	userIdClaim := "" // defaults to "sub"
    +
    +	// we're only testing with RSA256
    +	keyData, _ := os.ReadFile("test_data/sample_key")
    +	key, _ := jwt.ParseRSAPrivateKeyFromPEM(keyData)
    +	keyId := "test-key"
    +
    +	// note, we can use a nil store because ValidateAndParseToken does not use it in it's flow
    +	manager := auth.NewManager(nil, issuer, audience, server.URL, userIdClaim, []string{audience}, false)
    +
    +	customClaim := func(name string) string {
    +		return fmt.Sprintf("%s/%s", audience, name)
    +	}
    +
    +	lastLogin := time.Date(2025, 2, 12, 14, 25, 26, 0, time.UTC) //"2025-02-12T14:25:26.186Z"
    +
    +	tests := []struct {
    +		name      string
    +		tokenFunc func() string
    +		expected  *nbcontext.UserAuth // nil indicates expected error
    +	}{
    +		{
    +			name: "Valid with custom claims",
    +			tokenFunc: func() string {
    +				token := jwt.New(jwt.SigningMethodRS256)
    +				token.Header["kid"] = keyId
    +				token.Claims = jwt.MapClaims{
    +					"iss":                                   issuer,
    +					"aud":                                   []string{audience},
    +					"iat":                                   time.Now().Unix(),
    +					"exp":                                   time.Now().Add(time.Hour * 1).Unix(),
    +					"sub":                                   "user-id|123",
    +					customClaim(nbjwt.AccountIDSuffix):      "account-id|567",
    +					customClaim(nbjwt.DomainIDSuffix):       "http://localhost",
    +					customClaim(nbjwt.DomainCategorySuffix): "private",
    +					customClaim(nbjwt.LastLoginSuffix):      lastLogin.Format(time.RFC3339),
    +					customClaim(nbjwt.Invited):              false,
    +				}
    +				tokenString, _ := token.SignedString(key)
    +				return tokenString
    +			},
    +			expected: &nbcontext.UserAuth{
    +				UserId:         "user-id|123",
    +				AccountId:      "account-id|567",
    +				Domain:         "http://localhost",
    +				DomainCategory: "private",
    +				LastLogin:      lastLogin,
    +				Invited:        false,
    +			},
    +		},
    +		{
    +			name: "Valid without custom claims",
    +			tokenFunc: func() string {
    +				token := jwt.New(jwt.SigningMethodRS256)
    +				token.Header["kid"] = keyId
    +				token.Claims = jwt.MapClaims{
    +					"iss": issuer,
    +					"aud": []string{audience},
    +					"iat": time.Now().Unix(),
    +					"exp": time.Now().Add(time.Hour).Unix(),
    +					"sub": "user-id|123",
    +				}
    +				tokenString, _ := token.SignedString(key)
    +				return tokenString
    +			},
    +			expected: &nbcontext.UserAuth{
    +				UserId: "user-id|123",
    +			},
    +		},
    +		{
    +			name: "Expired token",
    +			tokenFunc: func() string {
    +				token := jwt.New(jwt.SigningMethodRS256)
    +				token.Header["kid"] = keyId
    +				token.Claims = jwt.MapClaims{
    +					"iss": issuer,
    +					"aud": []string{audience},
    +					"iat": time.Now().Add(time.Hour * -2).Unix(),
    +					"exp": time.Now().Add(time.Hour * -1).Unix(),
    +					"sub": "user-id|123",
    +				}
    +				tokenString, _ := token.SignedString(key)
    +				return tokenString
    +			},
    +		},
    +		{
    +			name: "Not yet valid",
    +			tokenFunc: func() string {
    +				token := jwt.New(jwt.SigningMethodRS256)
    +				token.Header["kid"] = keyId
    +				token.Claims = jwt.MapClaims{
    +					"iss": issuer,
    +					"aud": []string{audience},
    +					"iat": time.Now().Add(time.Hour).Unix(),
    +					"exp": time.Now().Add(time.Hour * 2).Unix(),
    +					"sub": "user-id|123",
    +				}
    +				tokenString, _ := token.SignedString(key)
    +				return tokenString
    +			},
    +		},
    +		{
    +			name: "Invalid signature",
    +			tokenFunc: func() string {
    +				token := jwt.New(jwt.SigningMethodRS256)
    +				token.Header["kid"] = keyId
    +				token.Claims = jwt.MapClaims{
    +					"iss": issuer,
    +					"aud": []string{audience},
    +					"iat": time.Now().Unix(),
    +					"exp": time.Now().Add(time.Hour).Unix(),
    +					"sub": "user-id|123",
    +				}
    +				tokenString, _ := token.SignedString(key)
    +				parts := strings.Split(tokenString, ".")
    +				parts[2] = "invalid-signature"
    +				return strings.Join(parts, ".")
    +			},
    +		},
    +		{
    +			name: "Invalid issuer",
    +			tokenFunc: func() string {
    +				token := jwt.New(jwt.SigningMethodRS256)
    +				token.Header["kid"] = keyId
    +				token.Claims = jwt.MapClaims{
    +					"iss": "not-the-issuer",
    +					"aud": []string{audience},
    +					"iat": time.Now().Unix(),
    +					"exp": time.Now().Add(time.Hour).Unix(),
    +					"sub": "user-id|123",
    +				}
    +				tokenString, _ := token.SignedString(key)
    +				return tokenString
    +			},
    +		},
    +		{
    +			name: "Invalid audience",
    +			tokenFunc: func() string {
    +				token := jwt.New(jwt.SigningMethodRS256)
    +				token.Header["kid"] = keyId
    +				token.Claims = jwt.MapClaims{
    +					"iss": issuer,
    +					"aud": []string{"not-the-audience"},
    +					"iat": time.Now().Unix(),
    +					"exp": time.Now().Add(time.Hour).Unix(),
    +					"sub": "user-id|123",
    +				}
    +				tokenString, _ := token.SignedString(key)
    +				return tokenString
    +			},
    +		},
    +		{
    +			name: "Invalid user claim",
    +			tokenFunc: func() string {
    +				token := jwt.New(jwt.SigningMethodRS256)
    +				token.Header["kid"] = keyId
    +				token.Claims = jwt.MapClaims{
    +					"iss":     issuer,
    +					"aud":     []string{audience},
    +					"iat":     time.Now().Unix(),
    +					"exp":     time.Now().Add(time.Hour).Unix(),
    +					"not-sub": "user-id|123",
    +				}
    +				tokenString, _ := token.SignedString(key)
    +				return tokenString
    +			},
    +		},
    +	}
    +
    +	for _, tt := range tests {
    +		t.Run(tt.name, func(t *testing.T) {
    +			tokenString := tt.tokenFunc()
    +
    +			userAuth, token, err := manager.ValidateAndParseToken(context.Background(), tokenString)
    +
    +			if tt.expected != nil {
    +				assert.NoError(t, err)
    +				assert.True(t, token.Valid)
    +				assert.Equal(t, *tt.expected, userAuth)
    +			} else {
    +				assert.Error(t, err)
    +				assert.Nil(t, token)
    +				assert.Empty(t, userAuth)
    +			}
    +		})
    +	}
    +
    +}
    diff --git a/management/server/auth/test_data/jwks.json b/management/server/auth/test_data/jwks.json
    new file mode 100644
    index 000000000..8080f5599
    --- /dev/null
    +++ b/management/server/auth/test_data/jwks.json
    @@ -0,0 +1,11 @@
    +{
    +    "keys": [
    +        {
    +            "kty": "RSA",
    +            "kid": "test-key",
    +            "use": "sig",
    +            "n": "4f5wg5l2hKsTeNem_V41fGnJm6gOdrj8ym3rFkEU_wT8RDtnSgFEZOQpHEgQ7JL38xUfU0Y3g6aYw9QT0hJ7mCpz9Er5qLaMXJwZxzHzAahlfA0icqabvJOMvQtzD6uQv6wPEyZtDTWiQi9AXwBpHssPnpYGIn20ZZuNlX2BrClciHhCPUIIZOQn_MmqTD31jSyjoQoV7MhhMTATKJx2XrHhR-1DcKJzQBSTAGnpYVaqpsARap-nwRipr3nUTuxyGohBTSmjJ2usSeQXHI3bODIRe1AuTyHceAbewn8b462yEWKARdpd9AjQW5SIVPfdsz5B6GlYQ5LdYKtznTuy7w",
    +            "e": "AQAB"
    +        }
    +    ]
    +}
    \ No newline at end of file
    diff --git a/management/server/auth/test_data/sample_key b/management/server/auth/test_data/sample_key
    new file mode 100644
    index 000000000..e69284a3f
    --- /dev/null
    +++ b/management/server/auth/test_data/sample_key
    @@ -0,0 +1,27 @@
    +-----BEGIN RSA PRIVATE KEY-----
    +MIIEowIBAAKCAQEA4f5wg5l2hKsTeNem/V41fGnJm6gOdrj8ym3rFkEU/wT8RDtn
    +SgFEZOQpHEgQ7JL38xUfU0Y3g6aYw9QT0hJ7mCpz9Er5qLaMXJwZxzHzAahlfA0i
    +cqabvJOMvQtzD6uQv6wPEyZtDTWiQi9AXwBpHssPnpYGIn20ZZuNlX2BrClciHhC
    +PUIIZOQn/MmqTD31jSyjoQoV7MhhMTATKJx2XrHhR+1DcKJzQBSTAGnpYVaqpsAR
    +ap+nwRipr3nUTuxyGohBTSmjJ2usSeQXHI3bODIRe1AuTyHceAbewn8b462yEWKA
    +Rdpd9AjQW5SIVPfdsz5B6GlYQ5LdYKtznTuy7wIDAQABAoIBAQCwia1k7+2oZ2d3
    +n6agCAbqIE1QXfCmh41ZqJHbOY3oRQG3X1wpcGH4Gk+O+zDVTV2JszdcOt7E5dAy
    +MaomETAhRxB7hlIOnEN7WKm+dGNrKRvV0wDU5ReFMRHg31/Lnu8c+5BvGjZX+ky9
    +POIhFFYJqwCRlopGSUIxmVj5rSgtzk3iWOQXr+ah1bjEXvlxDOWkHN6YfpV5ThdE
    +KdBIPGEVqa63r9n2h+qazKrtiRqJqGnOrHzOECYbRFYhexsNFz7YT02xdfSHn7gM
    +IvabDDP/Qp0PjE1jdouiMaFHYnLBbgvlnZW9yuVf/rpXTUq/njxIXMmvmEyyvSDn
    +FcFikB8pAoGBAPF77hK4m3/rdGT7X8a/gwvZ2R121aBcdPwEaUhvj/36dx596zvY
    +mEOjrWfZhF083/nYWE2kVquj2wjs+otCLfifEEgXcVPTnEOPO9Zg3uNSL0nNQghj
    +FuD3iGLTUBCtM66oTe0jLSslHe8gLGEQqyMzHOzYxNqibxcOZIe8Qt0NAoGBAO+U
    +I5+XWjWEgDmvyC3TrOSf/KCGjtu0TSv30ipv27bDLMrpvPmD/5lpptTFwcxvVhCs
    +2b+chCjlghFSWFbBULBrfci2FtliClOVMYrlNBdUSJhf3aYSG2Doe6Bgt1n2CpNn
    +/iu37Y3NfemZBJA7hNl4dYe+f+uzM87cdQ214+jrAoGAXA0XxX8ll2+ToOLJsaNT
    +OvNB9h9Uc5qK5X5w+7G7O998BN2PC/MWp8H+2fVqpXgNENpNXttkRm1hk1dych86
    +EunfdPuqsX+as44oCyJGFHVBnWpm33eWQw9YqANRI+pCJzP08I5WK3osnPiwshd+
    +hR54yjgfYhBFNI7B95PmEQkCgYBzFSz7h1+s34Ycr8SvxsOBWxymG5zaCsUbPsL0
    +4aCgLScCHb9J+E86aVbbVFdglYa5Id7DPTL61ixhl7WZjujspeXZGSbmq0Kcnckb
    +mDgqkLECiOJW2NHP/j0McAkDLL4tysF8TLDO8gvuvzNC+WQ6drO2ThrypLVZQ+ry
    +eBIPmwKBgEZxhqa0gVvHQG/7Od69KWj4eJP28kq13RhKay8JOoN0vPmspXJo1HY3
    +CKuHRG+AP579dncdUnOMvfXOtkdM4vk0+hWASBQzM9xzVcztCa+koAugjVaLS9A+
    +9uQoqEeVNTckxx0S2bYevRy7hGQmUJTyQm3j1zEUR5jpdbL83Fbq
    +-----END RSA PRIVATE KEY-----
    \ No newline at end of file
    diff --git a/management/server/auth/test_data/sample_key.pub b/management/server/auth/test_data/sample_key.pub
    new file mode 100644
    index 000000000..d5b7f7102
    --- /dev/null
    +++ b/management/server/auth/test_data/sample_key.pub
    @@ -0,0 +1,9 @@
    +-----BEGIN PUBLIC KEY-----
    +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4f5wg5l2hKsTeNem/V41
    +fGnJm6gOdrj8ym3rFkEU/wT8RDtnSgFEZOQpHEgQ7JL38xUfU0Y3g6aYw9QT0hJ7
    +mCpz9Er5qLaMXJwZxzHzAahlfA0icqabvJOMvQtzD6uQv6wPEyZtDTWiQi9AXwBp
    +HssPnpYGIn20ZZuNlX2BrClciHhCPUIIZOQn/MmqTD31jSyjoQoV7MhhMTATKJx2
    +XrHhR+1DcKJzQBSTAGnpYVaqpsARap+nwRipr3nUTuxyGohBTSmjJ2usSeQXHI3b
    +ODIRe1AuTyHceAbewn8b462yEWKARdpd9AjQW5SIVPfdsz5B6GlYQ5LdYKtznTuy
    +7wIDAQAB
    +-----END PUBLIC KEY-----
    \ No newline at end of file
    diff --git a/management/server/config.go b/management/server/config.go
    index 397b5f0e6..ce2ff4d16 100644
    --- a/management/server/config.go
    +++ b/management/server/config.go
    @@ -2,7 +2,6 @@ package server
     
     import (
     	"net/netip"
    -	"net/url"
     
     	"github.com/netbirdio/netbird/management/server/idp"
     	"github.com/netbirdio/netbird/management/server/store"
    @@ -180,9 +179,3 @@ type ReverseProxy struct {
     	// trusted IP prefixes.
     	TrustedPeers []netip.Prefix
     }
    -
    -// validateURL validates input http url
    -func validateURL(httpURL string) bool {
    -	_, err := url.ParseRequestURI(httpURL)
    -	return err == nil
    -}
    diff --git a/management/server/context/auth.go b/management/server/context/auth.go
    new file mode 100644
    index 000000000..5cb28ddb7
    --- /dev/null
    +++ b/management/server/context/auth.go
    @@ -0,0 +1,60 @@
    +package context
    +
    +import (
    +	"context"
    +	"fmt"
    +	"net/http"
    +	"time"
    +)
    +
    +type key int
    +
    +const (
    +	UserAuthContextKey key = iota
    +)
    +
    +type UserAuth struct {
    +	// The account id the user is accessing
    +	AccountId string
    +	// The account domain
    +	Domain string
    +	// The account domain category, TBC values
    +	DomainCategory string
    +	// Indicates whether this user was invited, TBC logic
    +	Invited bool
    +	// Indicates whether this is a child account
    +	IsChild bool
    +
    +	// The user id
    +	UserId string
    +	// Last login time for this user
    +	LastLogin time.Time
    +	// The Groups the user belongs to on this account
    +	Groups []string
    +
    +	// Indicates whether this user has authenticated with a Personal Access Token
    +	IsPAT bool
    +}
    +
    +func GetUserAuthFromRequest(r *http.Request) (UserAuth, error) {
    +	return GetUserAuthFromContext(r.Context())
    +}
    +
    +func SetUserAuthInRequest(r *http.Request, userAuth UserAuth) *http.Request {
    +	return r.WithContext(SetUserAuthInContext(r.Context(), userAuth))
    +}
    +
    +func GetUserAuthFromContext(ctx context.Context) (UserAuth, error) {
    +	if userAuth, ok := ctx.Value(UserAuthContextKey).(UserAuth); ok {
    +		return userAuth, nil
    +	}
    +	return UserAuth{}, fmt.Errorf("user auth not in context")
    +}
    +
    +func SetUserAuthInContext(ctx context.Context, userAuth UserAuth) context.Context {
    +	//nolint
    +	ctx = context.WithValue(ctx, UserIDKey, userAuth.UserId)
    +	//nolint
    +	ctx = context.WithValue(ctx, AccountIDKey, userAuth.AccountId)
    +	return context.WithValue(ctx, UserAuthContextKey, userAuth)
    +}
    diff --git a/management/server/grpcserver.go b/management/server/grpcserver.go
    index 8f5fae3e4..9d1bc1deb 100644
    --- a/management/server/grpcserver.go
    +++ b/management/server/grpcserver.go
    @@ -20,8 +20,8 @@ import (
     
     	"github.com/netbirdio/netbird/encryption"
     	"github.com/netbirdio/netbird/management/proto"
    +	"github.com/netbirdio/netbird/management/server/auth"
     	nbContext "github.com/netbirdio/netbird/management/server/context"
    -	"github.com/netbirdio/netbird/management/server/jwtclaims"
     	nbpeer "github.com/netbirdio/netbird/management/server/peer"
     	"github.com/netbirdio/netbird/management/server/posture"
     	"github.com/netbirdio/netbird/management/server/settings"
    @@ -39,11 +39,10 @@ type GRPCServer struct {
     	peersUpdateManager *PeersUpdateManager
     	config             *Config
     	secretsManager     SecretsManager
    -	jwtValidator       jwtclaims.JWTValidator
    -	jwtClaimsExtractor *jwtclaims.ClaimsExtractor
     	appMetrics         telemetry.AppMetrics
     	ephemeralManager   *EphemeralManager
     	peerLocks          sync.Map
    +	authManager        auth.Manager
     }
     
     // NewServer creates a new Management server
    @@ -56,29 +55,13 @@ func NewServer(
     	secretsManager SecretsManager,
     	appMetrics telemetry.AppMetrics,
     	ephemeralManager *EphemeralManager,
    +	authManager auth.Manager,
     ) (*GRPCServer, error) {
     	key, err := wgtypes.GeneratePrivateKey()
     	if err != nil {
     		return nil, err
     	}
     
    -	var jwtValidator jwtclaims.JWTValidator
    -
    -	if config.HttpConfig != nil && config.HttpConfig.AuthIssuer != "" && config.HttpConfig.AuthAudience != "" && validateURL(config.HttpConfig.AuthKeysLocation) {
    -		jwtValidator, err = jwtclaims.NewJWTValidator(
    -			ctx,
    -			config.HttpConfig.AuthIssuer,
    -			config.GetAuthAudiences(),
    -			config.HttpConfig.AuthKeysLocation,
    -			config.HttpConfig.IdpSignKeyRefreshEnabled,
    -		)
    -		if err != nil {
    -			return nil, status.Errorf(codes.Internal, "unable to create new jwt middleware, err: %v", err)
    -		}
    -	} else {
    -		log.WithContext(ctx).Debug("unable to use http config to create new jwt middleware")
    -	}
    -
     	if appMetrics != nil {
     		// update gauge based on number of connected peers which is equal to open gRPC streams
     		err = appMetrics.GRPCMetrics().RegisterConnectedStreams(func() int64 {
    @@ -89,16 +72,6 @@ func NewServer(
     		}
     	}
     
    -	var audience, userIDClaim string
    -	if config.HttpConfig != nil {
    -		audience = config.HttpConfig.AuthAudience
    -		userIDClaim = config.HttpConfig.AuthUserIDClaim
    -	}
    -	jwtClaimsExtractor := jwtclaims.NewClaimsExtractor(
    -		jwtclaims.WithAudience(audience),
    -		jwtclaims.WithUserIDClaim(userIDClaim),
    -	)
    -
     	return &GRPCServer{
     		wgKey: key,
     		// peerKey -> event channel
    @@ -107,8 +80,7 @@ func NewServer(
     		settingsManager:    settingsManager,
     		config:             config,
     		secretsManager:     secretsManager,
    -		jwtValidator:       jwtValidator,
    -		jwtClaimsExtractor: jwtClaimsExtractor,
    +		authManager:        authManager,
     		appMetrics:         appMetrics,
     		ephemeralManager:   ephemeralManager,
     	}, nil
    @@ -294,26 +266,32 @@ func (s *GRPCServer) cancelPeerRoutines(ctx context.Context, accountID string, p
     }
     
     func (s *GRPCServer) validateToken(ctx context.Context, jwtToken string) (string, error) {
    -	if s.jwtValidator == nil {
    -		return "", status.Error(codes.Internal, "no jwt validator set")
    +	if s.authManager == nil {
    +		return "", status.Errorf(codes.Internal, "missing auth manager")
     	}
     
    -	token, err := s.jwtValidator.ValidateAndParse(ctx, jwtToken)
    +	userAuth, token, err := s.authManager.ValidateAndParseToken(ctx, jwtToken)
     	if err != nil {
     		return "", status.Errorf(codes.InvalidArgument, "invalid jwt token, err: %v", err)
     	}
    -	claims := s.jwtClaimsExtractor.FromToken(token)
    +
     	// we need to call this method because if user is new, we will automatically add it to existing or create a new account
    -	_, _, err = s.accountManager.GetAccountIDFromToken(ctx, claims)
    +	_, _, err = s.accountManager.GetAccountIDFromUserAuth(ctx, userAuth)
     	if err != nil {
     		return "", status.Errorf(codes.Internal, "unable to fetch account with claims, err: %v", err)
     	}
     
    -	if err := s.accountManager.CheckUserAccessByJWTGroups(ctx, claims); err != nil {
    +	userAuth, err = s.authManager.EnsureUserAccessByJWTGroups(ctx, userAuth, token)
    +	if err != nil {
     		return "", status.Error(codes.PermissionDenied, err.Error())
     	}
     
    -	return claims.UserId, nil
    +	err = s.accountManager.SyncUserJWTGroups(ctx, userAuth)
    +	if err != nil {
    +		log.WithContext(ctx).Errorf("gRPC server failed to sync user JWT groups: %s", err)
    +	}
    +
    +	return userAuth.UserId, nil
     }
     
     func (s *GRPCServer) acquirePeerLockByUID(ctx context.Context, uniqueID string) (unlock func()) {
    diff --git a/management/server/http/handler.go b/management/server/http/handler.go
    index 7ce09fffa..2b87c5f25 100644
    --- a/management/server/http/handler.go
    +++ b/management/server/http/handler.go
    @@ -11,9 +11,9 @@ import (
     	"github.com/netbirdio/management-integrations/integrations"
     
     	s "github.com/netbirdio/netbird/management/server"
    +	"github.com/netbirdio/netbird/management/server/auth"
     	"github.com/netbirdio/netbird/management/server/geolocation"
     	nbgroups "github.com/netbirdio/netbird/management/server/groups"
    -	"github.com/netbirdio/netbird/management/server/http/configs"
     	"github.com/netbirdio/netbird/management/server/http/handlers/accounts"
     	"github.com/netbirdio/netbird/management/server/http/handlers/dns"
     	"github.com/netbirdio/netbird/management/server/http/handlers/events"
    @@ -26,7 +26,6 @@ import (
     	"github.com/netbirdio/netbird/management/server/http/handlers/users"
     	"github.com/netbirdio/netbird/management/server/http/middleware"
     	"github.com/netbirdio/netbird/management/server/integrated_validator"
    -	"github.com/netbirdio/netbird/management/server/jwtclaims"
     	nbnetworks "github.com/netbirdio/netbird/management/server/networks"
     	"github.com/netbirdio/netbird/management/server/networks/resources"
     	"github.com/netbirdio/netbird/management/server/networks/routers"
    @@ -36,55 +35,51 @@ import (
     const apiPrefix = "/api"
     
     // NewAPIHandler creates the Management service HTTP API handler registering all the available endpoints.
    -func NewAPIHandler(ctx context.Context, accountManager s.AccountManager, networksManager nbnetworks.Manager, resourceManager resources.Manager, routerManager routers.Manager, groupsManager nbgroups.Manager, LocationManager geolocation.Geolocation, jwtValidator jwtclaims.JWTValidator, appMetrics telemetry.AppMetrics, authCfg configs.AuthCfg, integratedValidator integrated_validator.IntegratedValidator) (http.Handler, error) {
    -	claimsExtractor := jwtclaims.NewClaimsExtractor(
    -		jwtclaims.WithAudience(authCfg.Audience),
    -		jwtclaims.WithUserIDClaim(authCfg.UserIDClaim),
    -	)
    +func NewAPIHandler(
    +	ctx context.Context,
    +	accountManager s.AccountManager,
    +	networksManager nbnetworks.Manager,
    +	resourceManager resources.Manager,
    +	routerManager routers.Manager,
    +	groupsManager nbgroups.Manager,
    +	LocationManager geolocation.Geolocation,
    +	authManager auth.Manager,
    +	appMetrics telemetry.AppMetrics,
    +	config *s.Config,
    +	integratedValidator integrated_validator.IntegratedValidator) (http.Handler, error) {
     
     	authMiddleware := middleware.NewAuthMiddleware(
    -		accountManager.GetPATInfo,
    -		jwtValidator.ValidateAndParse,
    -		accountManager.MarkPATUsed,
    -		accountManager.CheckUserAccessByJWTGroups,
    -		claimsExtractor,
    -		authCfg.Audience,
    -		authCfg.UserIDClaim,
    +		authManager,
    +		accountManager.GetAccountIDFromUserAuth,
    +		accountManager.SyncUserJWTGroups,
     	)
     
     	corsMiddleware := cors.AllowAll()
     
    -	claimsExtractor = jwtclaims.NewClaimsExtractor(
    -		jwtclaims.WithAudience(authCfg.Audience),
    -		jwtclaims.WithUserIDClaim(authCfg.UserIDClaim),
    -	)
    -
    -	acMiddleware := middleware.NewAccessControl(
    -		authCfg.Audience,
    -		authCfg.UserIDClaim,
    -		accountManager.GetUser)
    +	acMiddleware := middleware.NewAccessControl(accountManager.GetUserFromUserAuth)
     
     	rootRouter := mux.NewRouter()
     	metricsMiddleware := appMetrics.HTTPMiddleware()
     
     	prefix := apiPrefix
     	router := rootRouter.PathPrefix(prefix).Subrouter()
    +
     	router.Use(metricsMiddleware.Handler, corsMiddleware.Handler, authMiddleware.Handler, acMiddleware.Handler)
     
    -	if _, err := integrations.RegisterHandlers(ctx, prefix, router, accountManager, claimsExtractor, integratedValidator, appMetrics.GetMeter()); err != nil {
    +	if _, err := integrations.RegisterHandlers(ctx, prefix, router, accountManager, integratedValidator, appMetrics.GetMeter()); err != nil {
     		return nil, fmt.Errorf("register integrations endpoints: %w", err)
     	}
     
    -	accounts.AddEndpoints(accountManager, authCfg, router)
    -	peers.AddEndpoints(accountManager, authCfg, router)
    -	users.AddEndpoints(accountManager, authCfg, router)
    -	setup_keys.AddEndpoints(accountManager, authCfg, router)
    -	policies.AddEndpoints(accountManager, LocationManager, authCfg, router)
    -	groups.AddEndpoints(accountManager, authCfg, router)
    -	routes.AddEndpoints(accountManager, authCfg, router)
    -	dns.AddEndpoints(accountManager, authCfg, router)
    -	events.AddEndpoints(accountManager, authCfg, router)
    -	networks.AddEndpoints(networksManager, resourceManager, routerManager, groupsManager, accountManager, accountManager.GetAccountIDFromToken, authCfg, router)
    +	accounts.AddEndpoints(accountManager, router)
    +	peers.AddEndpoints(accountManager, router)
    +	users.AddEndpoints(accountManager, router)
    +	setup_keys.AddEndpoints(accountManager, router)
    +	policies.AddEndpoints(accountManager, LocationManager, router)
    +	groups.AddEndpoints(accountManager, router)
    +	routes.AddEndpoints(accountManager, router)
    +	dns.AddEndpoints(accountManager, router)
    +	events.AddEndpoints(accountManager, router)
    +	networks.AddEndpoints(networksManager, resourceManager, routerManager, groupsManager, accountManager, router)
     
     	return rootRouter, nil
     }
    diff --git a/management/server/http/handlers/accounts/accounts_handler.go b/management/server/http/handlers/accounts/accounts_handler.go
    index a23628cdc..bc0054a7f 100644
    --- a/management/server/http/handlers/accounts/accounts_handler.go
    +++ b/management/server/http/handlers/accounts/accounts_handler.go
    @@ -9,47 +9,42 @@ import (
     
     	"github.com/netbirdio/netbird/management/server"
     	"github.com/netbirdio/netbird/management/server/account"
    +	nbcontext "github.com/netbirdio/netbird/management/server/context"
     	"github.com/netbirdio/netbird/management/server/http/api"
    -	"github.com/netbirdio/netbird/management/server/http/configs"
     	"github.com/netbirdio/netbird/management/server/http/util"
    -	"github.com/netbirdio/netbird/management/server/jwtclaims"
     	"github.com/netbirdio/netbird/management/server/status"
     	"github.com/netbirdio/netbird/management/server/types"
     )
     
     // handler is a handler that handles the server.Account HTTP endpoints
     type handler struct {
    -	accountManager  server.AccountManager
    -	claimsExtractor *jwtclaims.ClaimsExtractor
    +	accountManager server.AccountManager
     }
     
    -func AddEndpoints(accountManager server.AccountManager, authCfg configs.AuthCfg, router *mux.Router) {
    -	accountsHandler := newHandler(accountManager, authCfg)
    +func AddEndpoints(accountManager server.AccountManager, router *mux.Router) {
    +	accountsHandler := newHandler(accountManager)
     	router.HandleFunc("/accounts/{accountId}", accountsHandler.updateAccount).Methods("PUT", "OPTIONS")
     	router.HandleFunc("/accounts/{accountId}", accountsHandler.deleteAccount).Methods("DELETE", "OPTIONS")
     	router.HandleFunc("/accounts", accountsHandler.getAllAccounts).Methods("GET", "OPTIONS")
     }
     
     // newHandler creates a new handler HTTP handler
    -func newHandler(accountManager server.AccountManager, authCfg configs.AuthCfg) *handler {
    +func newHandler(accountManager server.AccountManager) *handler {
     	return &handler{
     		accountManager: accountManager,
    -		claimsExtractor: jwtclaims.NewClaimsExtractor(
    -			jwtclaims.WithAudience(authCfg.Audience),
    -			jwtclaims.WithUserIDClaim(authCfg.UserIDClaim),
    -		),
     	}
     }
     
     // getAllAccounts is HTTP GET handler that returns a list of accounts. Effectively returns just a single account.
     func (h *handler) getAllAccounts(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
    +
     	settings, err := h.accountManager.GetAccountSettings(r.Context(), accountID, userID)
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
    @@ -62,13 +57,14 @@ func (h *handler) getAllAccounts(w http.ResponseWriter, r *http.Request) {
     
     // updateAccount is HTTP PUT handler that updates the provided account. Updates only account settings (server.Settings)
     func (h *handler) updateAccount(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	_, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
     
    +	_, userID := userAuth.AccountId, userAuth.UserId
    +
     	vars := mux.Vars(r)
     	accountID := vars["accountId"]
     	if len(accountID) == 0 {
    @@ -125,7 +121,12 @@ func (h *handler) updateAccount(w http.ResponseWriter, r *http.Request) {
     
     // deleteAccount is a HTTP DELETE handler to delete an account
     func (h *handler) deleteAccount(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
    +	if err != nil {
    +		util.WriteError(r.Context(), err, w)
    +		return
    +	}
    +
     	vars := mux.Vars(r)
     	targetAccountID := vars["accountId"]
     	if len(targetAccountID) == 0 {
    @@ -133,7 +134,7 @@ func (h *handler) deleteAccount(w http.ResponseWriter, r *http.Request) {
     		return
     	}
     
    -	err := h.accountManager.DeleteAccount(r.Context(), targetAccountID, claims.UserId)
    +	err = h.accountManager.DeleteAccount(r.Context(), targetAccountID, userAuth.UserId)
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
    diff --git a/management/server/http/handlers/accounts/accounts_handler_test.go b/management/server/http/handlers/accounts/accounts_handler_test.go
    index e8a599863..a8d57a13f 100644
    --- a/management/server/http/handlers/accounts/accounts_handler_test.go
    +++ b/management/server/http/handlers/accounts/accounts_handler_test.go
    @@ -13,19 +13,16 @@ import (
     	"github.com/gorilla/mux"
     	"github.com/stretchr/testify/assert"
     
    +	nbcontext "github.com/netbirdio/netbird/management/server/context"
     	"github.com/netbirdio/netbird/management/server/http/api"
    -	"github.com/netbirdio/netbird/management/server/jwtclaims"
     	"github.com/netbirdio/netbird/management/server/mock_server"
     	"github.com/netbirdio/netbird/management/server/status"
     	"github.com/netbirdio/netbird/management/server/types"
     )
     
    -func initAccountsTestData(account *types.Account, admin *types.User) *handler {
    +func initAccountsTestData(account *types.Account) *handler {
     	return &handler{
     		accountManager: &mock_server.MockAccountManager{
    -			GetAccountIDFromTokenFunc: func(ctx context.Context, claims jwtclaims.AuthorizationClaims) (string, string, error) {
    -				return account.Id, admin.Id, nil
    -			},
     			GetAccountSettingsFunc: func(ctx context.Context, accountID string, userID string) (*types.Settings, error) {
     				return account.Settings, nil
     			},
    @@ -44,15 +41,6 @@ func initAccountsTestData(account *types.Account, admin *types.User) *handler {
     				return accCopy, nil
     			},
     		},
    -		claimsExtractor: jwtclaims.NewClaimsExtractor(
    -			jwtclaims.WithFromRequestContext(func(r *http.Request) jwtclaims.AuthorizationClaims {
    -				return jwtclaims.AuthorizationClaims{
    -					UserId:    "test_user",
    -					Domain:    "hotmail.com",
    -					AccountId: "test_account",
    -				}
    -			}),
    -		),
     	}
     }
     
    @@ -75,7 +63,7 @@ func TestAccounts_AccountsHandler(t *testing.T) {
     			PeerLoginExpiration:        time.Hour,
     			RegularUsersViewBlocked:    true,
     		},
    -	}, adminUser)
    +	})
     
     	tt := []struct {
     		name             string
    @@ -191,6 +179,11 @@ func TestAccounts_AccountsHandler(t *testing.T) {
     		t.Run(tc.name, func(t *testing.T) {
     			recorder := httptest.NewRecorder()
     			req := httptest.NewRequest(tc.requestType, tc.requestPath, tc.requestBody)
    +			req = nbcontext.SetUserAuthInRequest(req, nbcontext.UserAuth{
    +				UserId:    adminUser.Id,
    +				AccountId: accountID,
    +				Domain:    "hotmail.com",
    +			})
     
     			router := mux.NewRouter()
     			router.HandleFunc("/api/accounts", handler.getAllAccounts).Methods("GET")
    diff --git a/management/server/http/handlers/dns/dns_settings_handler.go b/management/server/http/handlers/dns/dns_settings_handler.go
    index 112eee179..6ff938369 100644
    --- a/management/server/http/handlers/dns/dns_settings_handler.go
    +++ b/management/server/http/handlers/dns/dns_settings_handler.go
    @@ -8,51 +8,44 @@ import (
     	log "github.com/sirupsen/logrus"
     
     	"github.com/netbirdio/netbird/management/server"
    +	nbcontext "github.com/netbirdio/netbird/management/server/context"
     	"github.com/netbirdio/netbird/management/server/http/api"
    -	"github.com/netbirdio/netbird/management/server/http/configs"
     	"github.com/netbirdio/netbird/management/server/http/util"
    -	"github.com/netbirdio/netbird/management/server/jwtclaims"
     	"github.com/netbirdio/netbird/management/server/types"
     )
     
     // dnsSettingsHandler is a handler that returns the DNS settings of the account
     type dnsSettingsHandler struct {
    -	accountManager  server.AccountManager
    -	claimsExtractor *jwtclaims.ClaimsExtractor
    +	accountManager server.AccountManager
     }
     
    -func AddEndpoints(accountManager server.AccountManager, authCfg configs.AuthCfg, router *mux.Router) {
    -	addDNSSettingEndpoint(accountManager, authCfg, router)
    -	addDNSNameserversEndpoint(accountManager, authCfg, router)
    +func AddEndpoints(accountManager server.AccountManager, router *mux.Router) {
    +	addDNSSettingEndpoint(accountManager, router)
    +	addDNSNameserversEndpoint(accountManager, router)
     }
     
    -func addDNSSettingEndpoint(accountManager server.AccountManager, authCfg configs.AuthCfg, router *mux.Router) {
    -	dnsSettingsHandler := newDNSSettingsHandler(accountManager, authCfg)
    +func addDNSSettingEndpoint(accountManager server.AccountManager, router *mux.Router) {
    +	dnsSettingsHandler := newDNSSettingsHandler(accountManager)
     	router.HandleFunc("/dns/settings", dnsSettingsHandler.getDNSSettings).Methods("GET", "OPTIONS")
     	router.HandleFunc("/dns/settings", dnsSettingsHandler.updateDNSSettings).Methods("PUT", "OPTIONS")
     }
     
     // newDNSSettingsHandler returns a new instance of dnsSettingsHandler handler
    -func newDNSSettingsHandler(accountManager server.AccountManager, authCfg configs.AuthCfg) *dnsSettingsHandler {
    -	return &dnsSettingsHandler{
    -		accountManager: accountManager,
    -		claimsExtractor: jwtclaims.NewClaimsExtractor(
    -			jwtclaims.WithAudience(authCfg.Audience),
    -			jwtclaims.WithUserIDClaim(authCfg.UserIDClaim),
    -		),
    -	}
    +func newDNSSettingsHandler(accountManager server.AccountManager) *dnsSettingsHandler {
    +	return &dnsSettingsHandler{accountManager: accountManager}
     }
     
     // getDNSSettings returns the DNS settings for the account
     func (h *dnsSettingsHandler) getDNSSettings(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		log.WithContext(r.Context()).Error(err)
     		http.Redirect(w, r, "/", http.StatusInternalServerError)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
    +
     	dnsSettings, err := h.accountManager.GetDNSSettings(r.Context(), accountID, userID)
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
    @@ -68,13 +61,14 @@ func (h *dnsSettingsHandler) getDNSSettings(w http.ResponseWriter, r *http.Reque
     
     // updateDNSSettings handles update to DNS settings of an account
     func (h *dnsSettingsHandler) updateDNSSettings(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
    +
     	var req api.PutApiDnsSettingsJSONRequestBody
     	err = json.NewDecoder(r.Body).Decode(&req)
     	if err != nil {
    diff --git a/management/server/http/handlers/dns/dns_settings_handler_test.go b/management/server/http/handlers/dns/dns_settings_handler_test.go
    index 9ca1dc032..ca81adf43 100644
    --- a/management/server/http/handlers/dns/dns_settings_handler_test.go
    +++ b/management/server/http/handlers/dns/dns_settings_handler_test.go
    @@ -17,7 +17,8 @@ import (
     
     	"github.com/gorilla/mux"
     
    -	"github.com/netbirdio/netbird/management/server/jwtclaims"
    +	nbcontext "github.com/netbirdio/netbird/management/server/context"
    +
     	"github.com/netbirdio/netbird/management/server/mock_server"
     )
     
    @@ -52,19 +53,7 @@ func initDNSSettingsTestData() *dnsSettingsHandler {
     				}
     				return status.Errorf(status.InvalidArgument, "the dns settings provided are nil")
     			},
    -			GetAccountIDFromTokenFunc: func(ctx context.Context, _ jwtclaims.AuthorizationClaims) (string, string, error) {
    -				return testingDNSSettingsAccount.Id, testingDNSSettingsAccount.Users[testDNSSettingsUserID].Id, nil
    -			},
     		},
    -		claimsExtractor: jwtclaims.NewClaimsExtractor(
    -			jwtclaims.WithFromRequestContext(func(r *http.Request) jwtclaims.AuthorizationClaims {
    -				return jwtclaims.AuthorizationClaims{
    -					UserId:    "test_user",
    -					Domain:    "hotmail.com",
    -					AccountId: testDNSSettingsAccountID,
    -				}
    -			}),
    -		),
     	}
     }
     
    @@ -118,6 +107,11 @@ func TestDNSSettingsHandlers(t *testing.T) {
     		t.Run(tc.name, func(t *testing.T) {
     			recorder := httptest.NewRecorder()
     			req := httptest.NewRequest(tc.requestType, tc.requestPath, tc.requestBody)
    +			req = nbcontext.SetUserAuthInRequest(req, nbcontext.UserAuth{
    +				UserId:    testingDNSSettingsAccount.Users[testDNSSettingsUserID].Id,
    +				AccountId: testingDNSSettingsAccount.Id,
    +				Domain:    testingDNSSettingsAccount.Domain,
    +			})
     
     			router := mux.NewRouter()
     			router.HandleFunc("/api/dns/settings", p.getDNSSettings).Methods("GET")
    diff --git a/management/server/http/handlers/dns/nameservers_handler.go b/management/server/http/handlers/dns/nameservers_handler.go
    index 09047e231..33d070477 100644
    --- a/management/server/http/handlers/dns/nameservers_handler.go
    +++ b/management/server/http/handlers/dns/nameservers_handler.go
    @@ -10,21 +10,19 @@ import (
     
     	nbdns "github.com/netbirdio/netbird/dns"
     	"github.com/netbirdio/netbird/management/server"
    +	nbcontext "github.com/netbirdio/netbird/management/server/context"
     	"github.com/netbirdio/netbird/management/server/http/api"
    -	"github.com/netbirdio/netbird/management/server/http/configs"
     	"github.com/netbirdio/netbird/management/server/http/util"
    -	"github.com/netbirdio/netbird/management/server/jwtclaims"
     	"github.com/netbirdio/netbird/management/server/status"
     )
     
     // nameserversHandler is the nameserver group handler of the account
     type nameserversHandler struct {
    -	accountManager  server.AccountManager
    -	claimsExtractor *jwtclaims.ClaimsExtractor
    +	accountManager server.AccountManager
     }
     
    -func addDNSNameserversEndpoint(accountManager server.AccountManager, authCfg configs.AuthCfg, router *mux.Router) {
    -	nameserversHandler := newNameserversHandler(accountManager, authCfg)
    +func addDNSNameserversEndpoint(accountManager server.AccountManager, router *mux.Router) {
    +	nameserversHandler := newNameserversHandler(accountManager)
     	router.HandleFunc("/dns/nameservers", nameserversHandler.getAllNameservers).Methods("GET", "OPTIONS")
     	router.HandleFunc("/dns/nameservers", nameserversHandler.createNameserverGroup).Methods("POST", "OPTIONS")
     	router.HandleFunc("/dns/nameservers/{nsgroupId}", nameserversHandler.updateNameserverGroup).Methods("PUT", "OPTIONS")
    @@ -33,26 +31,21 @@ func addDNSNameserversEndpoint(accountManager server.AccountManager, authCfg con
     }
     
     // newNameserversHandler returns a new instance of nameserversHandler handler
    -func newNameserversHandler(accountManager server.AccountManager, authCfg configs.AuthCfg) *nameserversHandler {
    -	return &nameserversHandler{
    -		accountManager: accountManager,
    -		claimsExtractor: jwtclaims.NewClaimsExtractor(
    -			jwtclaims.WithAudience(authCfg.Audience),
    -			jwtclaims.WithUserIDClaim(authCfg.UserIDClaim),
    -		),
    -	}
    +func newNameserversHandler(accountManager server.AccountManager) *nameserversHandler {
    +	return &nameserversHandler{accountManager: accountManager}
     }
     
     // getAllNameservers returns the list of nameserver groups for the account
     func (h *nameserversHandler) getAllNameservers(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		log.WithContext(r.Context()).Error(err)
     		http.Redirect(w, r, "/", http.StatusInternalServerError)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
    +
     	nsGroups, err := h.accountManager.ListNameServerGroups(r.Context(), accountID, userID)
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
    @@ -69,13 +62,14 @@ func (h *nameserversHandler) getAllNameservers(w http.ResponseWriter, r *http.Re
     
     // createNameserverGroup handles nameserver group creation request
     func (h *nameserversHandler) createNameserverGroup(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
    +
     	var req api.PostApiDnsNameserversJSONRequestBody
     	err = json.NewDecoder(r.Body).Decode(&req)
     	if err != nil {
    @@ -102,13 +96,14 @@ func (h *nameserversHandler) createNameserverGroup(w http.ResponseWriter, r *htt
     
     // updateNameserverGroup handles update to a nameserver group identified by a given ID
     func (h *nameserversHandler) updateNameserverGroup(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
    +
     	nsGroupID := mux.Vars(r)["nsgroupId"]
     	if len(nsGroupID) == 0 {
     		util.WriteError(r.Context(), status.Errorf(status.InvalidArgument, "invalid nameserver group ID"), w)
    @@ -153,13 +148,14 @@ func (h *nameserversHandler) updateNameserverGroup(w http.ResponseWriter, r *htt
     
     // deleteNameserverGroup handles nameserver group deletion request
     func (h *nameserversHandler) deleteNameserverGroup(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
    +
     	nsGroupID := mux.Vars(r)["nsgroupId"]
     	if len(nsGroupID) == 0 {
     		util.WriteError(r.Context(), status.Errorf(status.InvalidArgument, "invalid nameserver group ID"), w)
    @@ -177,14 +173,14 @@ func (h *nameserversHandler) deleteNameserverGroup(w http.ResponseWriter, r *htt
     
     // getNameserverGroup handles a nameserver group Get request identified by ID
     func (h *nameserversHandler) getNameserverGroup(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
    -		log.WithContext(r.Context()).Error(err)
    -		http.Redirect(w, r, "/", http.StatusInternalServerError)
    +		util.WriteError(r.Context(), err, w)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
    +
     	nsGroupID := mux.Vars(r)["nsgroupId"]
     	if len(nsGroupID) == 0 {
     		util.WriteError(r.Context(), status.Errorf(status.InvalidArgument, "invalid nameserver group ID"), w)
    diff --git a/management/server/http/handlers/dns/nameservers_handler_test.go b/management/server/http/handlers/dns/nameservers_handler_test.go
    index c6561e4d8..45283bc37 100644
    --- a/management/server/http/handlers/dns/nameservers_handler_test.go
    +++ b/management/server/http/handlers/dns/nameservers_handler_test.go
    @@ -18,7 +18,8 @@ import (
     
     	"github.com/gorilla/mux"
     
    -	"github.com/netbirdio/netbird/management/server/jwtclaims"
    +	nbcontext "github.com/netbirdio/netbird/management/server/context"
    +
     	"github.com/netbirdio/netbird/management/server/mock_server"
     )
     
    @@ -81,19 +82,7 @@ func initNameserversTestData() *nameserversHandler {
     				}
     				return status.Errorf(status.NotFound, "nameserver group with ID %s was not found", nsGroupToSave.ID)
     			},
    -			GetAccountIDFromTokenFunc: func(_ context.Context, claims jwtclaims.AuthorizationClaims) (string, string, error) {
    -				return claims.AccountId, claims.UserId, nil
    -			},
     		},
    -		claimsExtractor: jwtclaims.NewClaimsExtractor(
    -			jwtclaims.WithFromRequestContext(func(r *http.Request) jwtclaims.AuthorizationClaims {
    -				return jwtclaims.AuthorizationClaims{
    -					UserId:    "test_user",
    -					Domain:    "hotmail.com",
    -					AccountId: testNSGroupAccountID,
    -				}
    -			}),
    -		),
     	}
     }
     
    @@ -204,6 +193,11 @@ func TestNameserversHandlers(t *testing.T) {
     		t.Run(tc.name, func(t *testing.T) {
     			recorder := httptest.NewRecorder()
     			req := httptest.NewRequest(tc.requestType, tc.requestPath, tc.requestBody)
    +			req = nbcontext.SetUserAuthInRequest(req, nbcontext.UserAuth{
    +				UserId:    "test_user",
    +				AccountId: testNSGroupAccountID,
    +				Domain:    "hotmail.com",
    +			})
     
     			router := mux.NewRouter()
     			router.HandleFunc("/api/dns/nameservers/{nsgroupId}", p.getNameserverGroup).Methods("GET")
    diff --git a/management/server/http/handlers/events/events_handler.go b/management/server/http/handlers/events/events_handler.go
    index 62da59535..0fb2295a8 100644
    --- a/management/server/http/handlers/events/events_handler.go
    +++ b/management/server/http/handlers/events/events_handler.go
    @@ -10,44 +10,37 @@ import (
     
     	"github.com/netbirdio/netbird/management/server"
     	"github.com/netbirdio/netbird/management/server/activity"
    +	nbcontext "github.com/netbirdio/netbird/management/server/context"
     	"github.com/netbirdio/netbird/management/server/http/api"
    -	"github.com/netbirdio/netbird/management/server/http/configs"
     	"github.com/netbirdio/netbird/management/server/http/util"
    -	"github.com/netbirdio/netbird/management/server/jwtclaims"
     )
     
     // handler HTTP handler
     type handler struct {
    -	accountManager  server.AccountManager
    -	claimsExtractor *jwtclaims.ClaimsExtractor
    +	accountManager server.AccountManager
     }
     
    -func AddEndpoints(accountManager server.AccountManager, authCfg configs.AuthCfg, router *mux.Router) {
    -	eventsHandler := newHandler(accountManager, authCfg)
    +func AddEndpoints(accountManager server.AccountManager, router *mux.Router) {
    +	eventsHandler := newHandler(accountManager)
     	router.HandleFunc("/events", eventsHandler.getAllEvents).Methods("GET", "OPTIONS")
     }
     
     // newHandler creates a new events handler
    -func newHandler(accountManager server.AccountManager, authCfg configs.AuthCfg) *handler {
    -	return &handler{
    -		accountManager: accountManager,
    -		claimsExtractor: jwtclaims.NewClaimsExtractor(
    -			jwtclaims.WithAudience(authCfg.Audience),
    -			jwtclaims.WithUserIDClaim(authCfg.UserIDClaim),
    -		),
    -	}
    +func newHandler(accountManager server.AccountManager) *handler {
    +	return &handler{accountManager: accountManager}
     }
     
     // getAllEvents list of the given account
     func (h *handler) getAllEvents(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		log.WithContext(r.Context()).Error(err)
     		http.Redirect(w, r, "/", http.StatusInternalServerError)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
    +
     	accountEvents, err := h.accountManager.GetEvents(r.Context(), accountID, userID)
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
    diff --git a/management/server/http/handlers/events/events_handler_test.go b/management/server/http/handlers/events/events_handler_test.go
    index fd603f289..3a643fe90 100644
    --- a/management/server/http/handlers/events/events_handler_test.go
    +++ b/management/server/http/handlers/events/events_handler_test.go
    @@ -13,9 +13,10 @@ import (
     	"github.com/gorilla/mux"
     	"github.com/stretchr/testify/assert"
     
    +	nbcontext "github.com/netbirdio/netbird/management/server/context"
    +
     	"github.com/netbirdio/netbird/management/server/activity"
     	"github.com/netbirdio/netbird/management/server/http/api"
    -	"github.com/netbirdio/netbird/management/server/jwtclaims"
     	"github.com/netbirdio/netbird/management/server/mock_server"
     	"github.com/netbirdio/netbird/management/server/types"
     )
    @@ -29,22 +30,10 @@ func initEventsTestData(account string, events ...*activity.Event) *handler {
     				}
     				return []*activity.Event{}, nil
     			},
    -			GetAccountIDFromTokenFunc: func(_ context.Context, claims jwtclaims.AuthorizationClaims) (string, string, error) {
    -				return claims.AccountId, claims.UserId, nil
    -			},
     			GetUsersFromAccountFunc: func(_ context.Context, accountID, userID string) (map[string]*types.UserInfo, error) {
     				return make(map[string]*types.UserInfo), nil
     			},
     		},
    -		claimsExtractor: jwtclaims.NewClaimsExtractor(
    -			jwtclaims.WithFromRequestContext(func(r *http.Request) jwtclaims.AuthorizationClaims {
    -				return jwtclaims.AuthorizationClaims{
    -					UserId:    "test_user",
    -					Domain:    "hotmail.com",
    -					AccountId: "test_account",
    -				}
    -			}),
    -		),
     	}
     }
     
    @@ -199,6 +188,11 @@ func TestEvents_GetEvents(t *testing.T) {
     		t.Run(tc.name, func(t *testing.T) {
     			recorder := httptest.NewRecorder()
     			req := httptest.NewRequest(tc.requestType, tc.requestPath, tc.requestBody)
    +			req = nbcontext.SetUserAuthInRequest(req, nbcontext.UserAuth{
    +				UserId:    "test_user",
    +				Domain:    "hotmail.com",
    +				AccountId: "test_account",
    +			})
     
     			router := mux.NewRouter()
     			router.HandleFunc("/api/events/", handler.getAllEvents).Methods("GET")
    diff --git a/management/server/http/handlers/groups/groups_handler.go b/management/server/http/handlers/groups/groups_handler.go
    index ec635a358..040c08b87 100644
    --- a/management/server/http/handlers/groups/groups_handler.go
    +++ b/management/server/http/handlers/groups/groups_handler.go
    @@ -7,24 +7,23 @@ import (
     	"github.com/gorilla/mux"
     	log "github.com/sirupsen/logrus"
     
    +	nbcontext "github.com/netbirdio/netbird/management/server/context"
    +	nbpeer "github.com/netbirdio/netbird/management/server/peer"
    +
     	"github.com/netbirdio/netbird/management/server"
     	"github.com/netbirdio/netbird/management/server/http/api"
    -	"github.com/netbirdio/netbird/management/server/http/configs"
     	"github.com/netbirdio/netbird/management/server/http/util"
    -	"github.com/netbirdio/netbird/management/server/jwtclaims"
    -	nbpeer "github.com/netbirdio/netbird/management/server/peer"
     	"github.com/netbirdio/netbird/management/server/status"
     	"github.com/netbirdio/netbird/management/server/types"
     )
     
     // handler is a handler that returns groups of the account
     type handler struct {
    -	accountManager  server.AccountManager
    -	claimsExtractor *jwtclaims.ClaimsExtractor
    +	accountManager server.AccountManager
     }
     
    -func AddEndpoints(accountManager server.AccountManager, authCfg configs.AuthCfg, router *mux.Router) {
    -	groupsHandler := newHandler(accountManager, authCfg)
    +func AddEndpoints(accountManager server.AccountManager, router *mux.Router) {
    +	groupsHandler := newHandler(accountManager)
     	router.HandleFunc("/groups", groupsHandler.getAllGroups).Methods("GET", "OPTIONS")
     	router.HandleFunc("/groups", groupsHandler.createGroup).Methods("POST", "OPTIONS")
     	router.HandleFunc("/groups/{groupId}", groupsHandler.updateGroup).Methods("PUT", "OPTIONS")
    @@ -33,25 +32,21 @@ func AddEndpoints(accountManager server.AccountManager, authCfg configs.AuthCfg,
     }
     
     // newHandler creates a new groups handler
    -func newHandler(accountManager server.AccountManager, authCfg configs.AuthCfg) *handler {
    +func newHandler(accountManager server.AccountManager) *handler {
     	return &handler{
     		accountManager: accountManager,
    -		claimsExtractor: jwtclaims.NewClaimsExtractor(
    -			jwtclaims.WithAudience(authCfg.Audience),
    -			jwtclaims.WithUserIDClaim(authCfg.UserIDClaim),
    -		),
     	}
     }
     
     // getAllGroups list for the account
     func (h *handler) getAllGroups(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		log.WithContext(r.Context()).Error(err)
     		http.Redirect(w, r, "/", http.StatusInternalServerError)
     		return
     	}
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
     
     	groups, err := h.accountManager.GetAllGroups(r.Context(), accountID, userID)
     	if err != nil {
    @@ -75,13 +70,14 @@ func (h *handler) getAllGroups(w http.ResponseWriter, r *http.Request) {
     
     // updateGroup handles update to a group identified by a given ID
     func (h *handler) updateGroup(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
    +
     	vars := mux.Vars(r)
     	groupID, ok := vars["groupId"]
     	if !ok {
    @@ -164,13 +160,14 @@ func (h *handler) updateGroup(w http.ResponseWriter, r *http.Request) {
     
     // createGroup handles group creation request
     func (h *handler) createGroup(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
    +
     	var req api.PostApiGroupsJSONRequestBody
     	err = json.NewDecoder(r.Body).Decode(&req)
     	if err != nil {
    @@ -223,13 +220,14 @@ func (h *handler) createGroup(w http.ResponseWriter, r *http.Request) {
     
     // deleteGroup handles group deletion request
     func (h *handler) deleteGroup(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
    +
     	groupID := mux.Vars(r)["groupId"]
     	if len(groupID) == 0 {
     		util.WriteError(r.Context(), status.Errorf(status.InvalidArgument, "invalid group ID"), w)
    @@ -253,12 +251,13 @@ func (h *handler) deleteGroup(w http.ResponseWriter, r *http.Request) {
     
     // getGroup returns a group
     func (h *handler) getGroup(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
    +
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
     	groupID := mux.Vars(r)["groupId"]
     	if len(groupID) == 0 {
     		util.WriteError(r.Context(), status.Errorf(status.InvalidArgument, "invalid group ID"), w)
    diff --git a/management/server/http/handlers/groups/groups_handler_test.go b/management/server/http/handlers/groups/groups_handler_test.go
    index 0668982f3..c4b9e46ab 100644
    --- a/management/server/http/handlers/groups/groups_handler_test.go
    +++ b/management/server/http/handlers/groups/groups_handler_test.go
    @@ -18,9 +18,9 @@ import (
     	"golang.org/x/exp/maps"
     
     	"github.com/netbirdio/netbird/management/server"
    +	nbcontext "github.com/netbirdio/netbird/management/server/context"
     	"github.com/netbirdio/netbird/management/server/http/api"
     	"github.com/netbirdio/netbird/management/server/http/util"
    -	"github.com/netbirdio/netbird/management/server/jwtclaims"
     	"github.com/netbirdio/netbird/management/server/mock_server"
     	nbpeer "github.com/netbirdio/netbird/management/server/peer"
     	"github.com/netbirdio/netbird/management/server/status"
    @@ -59,9 +59,6 @@ func initGroupTestData(initGroups ...*types.Group) *handler {
     
     				return group, nil
     			},
    -			GetAccountIDFromTokenFunc: func(_ context.Context, claims jwtclaims.AuthorizationClaims) (string, string, error) {
    -				return claims.AccountId, claims.UserId, nil
    -			},
     			GetGroupByNameFunc: func(ctx context.Context, groupName, _ string) (*types.Group, error) {
     				if groupName == "All" {
     					return &types.Group{ID: "id-all", Name: "All", Issued: types.GroupIssuedAPI}, nil
    @@ -87,15 +84,6 @@ func initGroupTestData(initGroups ...*types.Group) *handler {
     				return nil
     			},
     		},
    -		claimsExtractor: jwtclaims.NewClaimsExtractor(
    -			jwtclaims.WithFromRequestContext(func(r *http.Request) jwtclaims.AuthorizationClaims {
    -				return jwtclaims.AuthorizationClaims{
    -					UserId:    "test_user",
    -					Domain:    "hotmail.com",
    -					AccountId: "test_id",
    -				}
    -			}),
    -		),
     	}
     }
     
    @@ -134,6 +122,11 @@ func TestGetGroup(t *testing.T) {
     		t.Run(tc.name, func(t *testing.T) {
     			recorder := httptest.NewRecorder()
     			req := httptest.NewRequest(tc.requestType, tc.requestPath, tc.requestBody)
    +			req = nbcontext.SetUserAuthInRequest(req, nbcontext.UserAuth{
    +				UserId:    "test_user",
    +				Domain:    "hotmail.com",
    +				AccountId: "test_id",
    +			})
     
     			router := mux.NewRouter()
     			router.HandleFunc("/api/groups/{groupId}", p.getGroup).Methods("GET")
    @@ -255,6 +248,11 @@ func TestWriteGroup(t *testing.T) {
     		t.Run(tc.name, func(t *testing.T) {
     			recorder := httptest.NewRecorder()
     			req := httptest.NewRequest(tc.requestType, tc.requestPath, tc.requestBody)
    +			req = nbcontext.SetUserAuthInRequest(req, nbcontext.UserAuth{
    +				UserId:    "test_user",
    +				Domain:    "hotmail.com",
    +				AccountId: "test_id",
    +			})
     
     			router := mux.NewRouter()
     			router.HandleFunc("/api/groups", p.createGroup).Methods("POST")
    @@ -332,7 +330,11 @@ func TestDeleteGroup(t *testing.T) {
     		t.Run(tc.name, func(t *testing.T) {
     			recorder := httptest.NewRecorder()
     			req := httptest.NewRequest(tc.requestType, tc.requestPath, nil)
    -
    +			req = nbcontext.SetUserAuthInRequest(req, nbcontext.UserAuth{
    +				UserId:    "test_user",
    +				Domain:    "hotmail.com",
    +				AccountId: "test_id",
    +			})
     			router := mux.NewRouter()
     			router.HandleFunc("/api/groups/{groupId}", p.deleteGroup).Methods("DELETE")
     			router.ServeHTTP(recorder, req)
    diff --git a/management/server/http/handlers/networks/handler.go b/management/server/http/handlers/networks/handler.go
    index f716348d6..bb6b97267 100644
    --- a/management/server/http/handlers/networks/handler.go
    +++ b/management/server/http/handlers/networks/handler.go
    @@ -10,11 +10,10 @@ import (
     	log "github.com/sirupsen/logrus"
     
     	s "github.com/netbirdio/netbird/management/server"
    +	nbcontext "github.com/netbirdio/netbird/management/server/context"
     	"github.com/netbirdio/netbird/management/server/groups"
     	"github.com/netbirdio/netbird/management/server/http/api"
    -	"github.com/netbirdio/netbird/management/server/http/configs"
     	"github.com/netbirdio/netbird/management/server/http/util"
    -	"github.com/netbirdio/netbird/management/server/jwtclaims"
     	"github.com/netbirdio/netbird/management/server/networks"
     	"github.com/netbirdio/netbird/management/server/networks/resources"
     	"github.com/netbirdio/netbird/management/server/networks/routers"
    @@ -31,16 +30,14 @@ type handler struct {
     	routerManager   routers.Manager
     	accountManager  s.AccountManager
     
    -	groupsManager    groups.Manager
    -	extractFromToken func(ctx context.Context, claims jwtclaims.AuthorizationClaims) (string, string, error)
    -	claimsExtractor  *jwtclaims.ClaimsExtractor
    +	groupsManager groups.Manager
     }
     
    -func AddEndpoints(networksManager networks.Manager, resourceManager resources.Manager, routerManager routers.Manager, groupsManager groups.Manager, accountManager s.AccountManager, extractFromToken func(ctx context.Context, claims jwtclaims.AuthorizationClaims) (string, string, error), authCfg configs.AuthCfg, router *mux.Router) {
    -	addRouterEndpoints(routerManager, extractFromToken, authCfg, router)
    -	addResourceEndpoints(resourceManager, groupsManager, extractFromToken, authCfg, router)
    +func AddEndpoints(networksManager networks.Manager, resourceManager resources.Manager, routerManager routers.Manager, groupsManager groups.Manager, accountManager s.AccountManager, router *mux.Router) {
    +	addRouterEndpoints(routerManager, router)
    +	addResourceEndpoints(resourceManager, groupsManager, router)
     
    -	networksHandler := newHandler(networksManager, resourceManager, routerManager, groupsManager, accountManager, extractFromToken, authCfg)
    +	networksHandler := newHandler(networksManager, resourceManager, routerManager, groupsManager, accountManager)
     	router.HandleFunc("/networks", networksHandler.getAllNetworks).Methods("GET", "OPTIONS")
     	router.HandleFunc("/networks", networksHandler.createNetwork).Methods("POST", "OPTIONS")
     	router.HandleFunc("/networks/{networkId}", networksHandler.getNetwork).Methods("GET", "OPTIONS")
    @@ -48,29 +45,25 @@ func AddEndpoints(networksManager networks.Manager, resourceManager resources.Ma
     	router.HandleFunc("/networks/{networkId}", networksHandler.deleteNetwork).Methods("DELETE", "OPTIONS")
     }
     
    -func newHandler(networksManager networks.Manager, resourceManager resources.Manager, routerManager routers.Manager, groupsManager groups.Manager, accountManager s.AccountManager, extractFromToken func(ctx context.Context, claims jwtclaims.AuthorizationClaims) (string, string, error), authCfg configs.AuthCfg) *handler {
    +func newHandler(networksManager networks.Manager, resourceManager resources.Manager, routerManager routers.Manager, groupsManager groups.Manager, accountManager s.AccountManager) *handler {
     	return &handler{
    -		networksManager:  networksManager,
    -		resourceManager:  resourceManager,
    -		routerManager:    routerManager,
    -		groupsManager:    groupsManager,
    -		accountManager:   accountManager,
    -		extractFromToken: extractFromToken,
    -		claimsExtractor: jwtclaims.NewClaimsExtractor(
    -			jwtclaims.WithAudience(authCfg.Audience),
    -			jwtclaims.WithUserIDClaim(authCfg.UserIDClaim),
    -		),
    +		networksManager: networksManager,
    +		resourceManager: resourceManager,
    +		routerManager:   routerManager,
    +		groupsManager:   groupsManager,
    +		accountManager:  accountManager,
     	}
     }
     
     func (h *handler) getAllNetworks(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.extractFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
    +
     	networks, err := h.networksManager.GetAllNetworks(r.Context(), accountID, userID)
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
    @@ -105,12 +98,12 @@ func (h *handler) getAllNetworks(w http.ResponseWriter, r *http.Request) {
     }
     
     func (h *handler) createNetwork(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.extractFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
     
     	var req api.NetworkRequest
     	err = json.NewDecoder(r.Body).Decode(&req)
    @@ -141,12 +134,12 @@ func (h *handler) createNetwork(w http.ResponseWriter, r *http.Request) {
     }
     
     func (h *handler) getNetwork(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.extractFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
     
     	vars := mux.Vars(r)
     	networkID := vars["networkId"]
    @@ -179,13 +172,13 @@ func (h *handler) getNetwork(w http.ResponseWriter, r *http.Request) {
     }
     
     func (h *handler) updateNetwork(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.extractFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
     	vars := mux.Vars(r)
     	networkID := vars["networkId"]
     	if len(networkID) == 0 {
    @@ -229,13 +222,13 @@ func (h *handler) updateNetwork(w http.ResponseWriter, r *http.Request) {
     }
     
     func (h *handler) deleteNetwork(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.extractFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
     	vars := mux.Vars(r)
     	networkID := vars["networkId"]
     	if len(networkID) == 0 {
    diff --git a/management/server/http/handlers/networks/resources_handler.go b/management/server/http/handlers/networks/resources_handler.go
    index f2dc8e3b8..fba7026e8 100644
    --- a/management/server/http/handlers/networks/resources_handler.go
    +++ b/management/server/http/handlers/networks/resources_handler.go
    @@ -1,30 +1,26 @@
     package networks
     
     import (
    -	"context"
     	"encoding/json"
     	"net/http"
     
     	"github.com/gorilla/mux"
     
    +	nbcontext "github.com/netbirdio/netbird/management/server/context"
     	"github.com/netbirdio/netbird/management/server/groups"
     	"github.com/netbirdio/netbird/management/server/http/api"
    -	"github.com/netbirdio/netbird/management/server/http/configs"
     	"github.com/netbirdio/netbird/management/server/http/util"
    -	"github.com/netbirdio/netbird/management/server/jwtclaims"
     	"github.com/netbirdio/netbird/management/server/networks/resources"
     	"github.com/netbirdio/netbird/management/server/networks/resources/types"
     )
     
     type resourceHandler struct {
    -	resourceManager  resources.Manager
    -	groupsManager    groups.Manager
    -	extractFromToken func(ctx context.Context, claims jwtclaims.AuthorizationClaims) (string, string, error)
    -	claimsExtractor  *jwtclaims.ClaimsExtractor
    +	resourceManager resources.Manager
    +	groupsManager   groups.Manager
     }
     
    -func addResourceEndpoints(resourcesManager resources.Manager, groupsManager groups.Manager, extractFromToken func(ctx context.Context, claims jwtclaims.AuthorizationClaims) (string, string, error), authCfg configs.AuthCfg, router *mux.Router) {
    -	resourceHandler := newResourceHandler(resourcesManager, groupsManager, extractFromToken, authCfg)
    +func addResourceEndpoints(resourcesManager resources.Manager, groupsManager groups.Manager, router *mux.Router) {
    +	resourceHandler := newResourceHandler(resourcesManager, groupsManager)
     	router.HandleFunc("/networks/resources", resourceHandler.getAllResourcesInAccount).Methods("GET", "OPTIONS")
     	router.HandleFunc("/networks/{networkId}/resources", resourceHandler.getAllResourcesInNetwork).Methods("GET", "OPTIONS")
     	router.HandleFunc("/networks/{networkId}/resources", resourceHandler.createResource).Methods("POST", "OPTIONS")
    @@ -33,26 +29,21 @@ func addResourceEndpoints(resourcesManager resources.Manager, groupsManager grou
     	router.HandleFunc("/networks/{networkId}/resources/{resourceId}", resourceHandler.deleteResource).Methods("DELETE", "OPTIONS")
     }
     
    -func newResourceHandler(resourceManager resources.Manager, groupsManager groups.Manager, extractFromToken func(ctx context.Context, claims jwtclaims.AuthorizationClaims) (string, string, error), authCfg configs.AuthCfg) *resourceHandler {
    +func newResourceHandler(resourceManager resources.Manager, groupsManager groups.Manager) *resourceHandler {
     	return &resourceHandler{
    -		resourceManager:  resourceManager,
    -		groupsManager:    groupsManager,
    -		extractFromToken: extractFromToken,
    -		claimsExtractor: jwtclaims.NewClaimsExtractor(
    -			jwtclaims.WithAudience(authCfg.Audience),
    -			jwtclaims.WithUserIDClaim(authCfg.UserIDClaim),
    -		),
    +		resourceManager: resourceManager,
    +		groupsManager:   groupsManager,
     	}
     }
     
     func (h *resourceHandler) getAllResourcesInNetwork(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.extractFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
     	networkID := mux.Vars(r)["networkId"]
     	resources, err := h.resourceManager.GetAllResourcesInNetwork(r.Context(), accountID, userID, networkID)
     	if err != nil {
    @@ -76,13 +67,14 @@ func (h *resourceHandler) getAllResourcesInNetwork(w http.ResponseWriter, r *htt
     	util.WriteJSONObject(r.Context(), w, resourcesResponse)
     }
     func (h *resourceHandler) getAllResourcesInAccount(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.extractFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
    +
     	resources, err := h.resourceManager.GetAllResourcesInAccount(r.Context(), accountID, userID)
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
    @@ -106,13 +98,14 @@ func (h *resourceHandler) getAllResourcesInAccount(w http.ResponseWriter, r *htt
     }
     
     func (h *resourceHandler) createResource(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.extractFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
    +
     	var req api.NetworkResourceRequest
     	err = json.NewDecoder(r.Body).Decode(&req)
     	if err != nil {
    @@ -144,13 +137,13 @@ func (h *resourceHandler) createResource(w http.ResponseWriter, r *http.Request)
     }
     
     func (h *resourceHandler) getResource(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.extractFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
     	networkID := mux.Vars(r)["networkId"]
     	resourceID := mux.Vars(r)["resourceId"]
     	resource, err := h.resourceManager.GetResource(r.Context(), accountID, userID, networkID, resourceID)
    @@ -171,13 +164,13 @@ func (h *resourceHandler) getResource(w http.ResponseWriter, r *http.Request) {
     }
     
     func (h *resourceHandler) updateResource(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.extractFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
     	var req api.NetworkResourceRequest
     	err = json.NewDecoder(r.Body).Decode(&req)
     	if err != nil {
    @@ -209,12 +202,12 @@ func (h *resourceHandler) updateResource(w http.ResponseWriter, r *http.Request)
     }
     
     func (h *resourceHandler) deleteResource(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.extractFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
     
     	networkID := mux.Vars(r)["networkId"]
     	resourceID := mux.Vars(r)["resourceId"]
    diff --git a/management/server/http/handlers/networks/routers_handler.go b/management/server/http/handlers/networks/routers_handler.go
    index 7ca95d902..f98da4966 100644
    --- a/management/server/http/handlers/networks/routers_handler.go
    +++ b/management/server/http/handlers/networks/routers_handler.go
    @@ -1,28 +1,24 @@
     package networks
     
     import (
    -	"context"
     	"encoding/json"
     	"net/http"
     
     	"github.com/gorilla/mux"
     
    +	nbcontext "github.com/netbirdio/netbird/management/server/context"
     	"github.com/netbirdio/netbird/management/server/http/api"
    -	"github.com/netbirdio/netbird/management/server/http/configs"
     	"github.com/netbirdio/netbird/management/server/http/util"
    -	"github.com/netbirdio/netbird/management/server/jwtclaims"
     	"github.com/netbirdio/netbird/management/server/networks/routers"
     	"github.com/netbirdio/netbird/management/server/networks/routers/types"
     )
     
     type routersHandler struct {
    -	routersManager   routers.Manager
    -	extractFromToken func(ctx context.Context, claims jwtclaims.AuthorizationClaims) (string, string, error)
    -	claimsExtractor  *jwtclaims.ClaimsExtractor
    +	routersManager routers.Manager
     }
     
    -func addRouterEndpoints(routersManager routers.Manager, extractFromToken func(ctx context.Context, claims jwtclaims.AuthorizationClaims) (string, string, error), authCfg configs.AuthCfg, router *mux.Router) {
    -	routersHandler := newRoutersHandler(routersManager, extractFromToken, authCfg)
    +func addRouterEndpoints(routersManager routers.Manager, router *mux.Router) {
    +	routersHandler := newRoutersHandler(routersManager)
     	router.HandleFunc("/networks/{networkId}/routers", routersHandler.getAllRouters).Methods("GET", "OPTIONS")
     	router.HandleFunc("/networks/{networkId}/routers", routersHandler.createRouter).Methods("POST", "OPTIONS")
     	router.HandleFunc("/networks/{networkId}/routers/{routerId}", routersHandler.getRouter).Methods("GET", "OPTIONS")
    @@ -30,25 +26,21 @@ func addRouterEndpoints(routersManager routers.Manager, extractFromToken func(ct
     	router.HandleFunc("/networks/{networkId}/routers/{routerId}", routersHandler.deleteRouter).Methods("DELETE", "OPTIONS")
     }
     
    -func newRoutersHandler(routersManager routers.Manager, extractFromToken func(ctx context.Context, claims jwtclaims.AuthorizationClaims) (string, string, error), authCfg configs.AuthCfg) *routersHandler {
    +func newRoutersHandler(routersManager routers.Manager) *routersHandler {
     	return &routersHandler{
    -		routersManager:   routersManager,
    -		extractFromToken: extractFromToken,
    -		claimsExtractor: jwtclaims.NewClaimsExtractor(
    -			jwtclaims.WithAudience(authCfg.Audience),
    -			jwtclaims.WithUserIDClaim(authCfg.UserIDClaim),
    -		),
    +		routersManager: routersManager,
     	}
     }
     
     func (h *routersHandler) getAllRouters(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.extractFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
    +
     	networkID := mux.Vars(r)["networkId"]
     	routers, err := h.routersManager.GetAllRoutersInNetwork(r.Context(), accountID, userID, networkID)
     	if err != nil {
    @@ -65,13 +57,14 @@ func (h *routersHandler) getAllRouters(w http.ResponseWriter, r *http.Request) {
     }
     
     func (h *routersHandler) createRouter(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.extractFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
    +
     	networkID := mux.Vars(r)["networkId"]
     	var req api.NetworkRouterRequest
     	err = json.NewDecoder(r.Body).Decode(&req)
    @@ -96,13 +89,14 @@ func (h *routersHandler) createRouter(w http.ResponseWriter, r *http.Request) {
     }
     
     func (h *routersHandler) getRouter(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.extractFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
    +
     	routerID := mux.Vars(r)["routerId"]
     	networkID := mux.Vars(r)["networkId"]
     	router, err := h.routersManager.GetRouter(r.Context(), accountID, userID, networkID, routerID)
    @@ -115,13 +109,14 @@ func (h *routersHandler) getRouter(w http.ResponseWriter, r *http.Request) {
     }
     
     func (h *routersHandler) updateRouter(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.extractFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
    +
     	var req api.NetworkRouterRequest
     	err = json.NewDecoder(r.Body).Decode(&req)
     	if err != nil {
    @@ -146,13 +141,13 @@ func (h *routersHandler) updateRouter(w http.ResponseWriter, r *http.Request) {
     }
     
     func (h *routersHandler) deleteRouter(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.extractFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
     	routerID := mux.Vars(r)["routerId"]
     	networkID := mux.Vars(r)["networkId"]
     	err = h.routersManager.DeleteRouter(r.Context(), accountID, userID, networkID, routerID)
    diff --git a/management/server/http/handlers/peers/peers_handler.go b/management/server/http/handlers/peers/peers_handler.go
    index 26153d0a1..709ba64d0 100644
    --- a/management/server/http/handlers/peers/peers_handler.go
    +++ b/management/server/http/handlers/peers/peers_handler.go
    @@ -10,11 +10,10 @@ import (
     	log "github.com/sirupsen/logrus"
     
     	"github.com/netbirdio/netbird/management/server"
    +	nbcontext "github.com/netbirdio/netbird/management/server/context"
     	"github.com/netbirdio/netbird/management/server/groups"
     	"github.com/netbirdio/netbird/management/server/http/api"
    -	"github.com/netbirdio/netbird/management/server/http/configs"
     	"github.com/netbirdio/netbird/management/server/http/util"
    -	"github.com/netbirdio/netbird/management/server/jwtclaims"
     	nbpeer "github.com/netbirdio/netbird/management/server/peer"
     	"github.com/netbirdio/netbird/management/server/status"
     	"github.com/netbirdio/netbird/management/server/types"
    @@ -22,12 +21,11 @@ import (
     
     // Handler is a handler that returns peers of the account
     type Handler struct {
    -	accountManager  server.AccountManager
    -	claimsExtractor *jwtclaims.ClaimsExtractor
    +	accountManager server.AccountManager
     }
     
    -func AddEndpoints(accountManager server.AccountManager, authCfg configs.AuthCfg, router *mux.Router) {
    -	peersHandler := NewHandler(accountManager, authCfg)
    +func AddEndpoints(accountManager server.AccountManager, router *mux.Router) {
    +	peersHandler := NewHandler(accountManager)
     	router.HandleFunc("/peers", peersHandler.GetAllPeers).Methods("GET", "OPTIONS")
     	router.HandleFunc("/peers/{peerId}", peersHandler.HandlePeer).
     		Methods("GET", "PUT", "DELETE", "OPTIONS")
    @@ -35,13 +33,9 @@ func AddEndpoints(accountManager server.AccountManager, authCfg configs.AuthCfg,
     }
     
     // NewHandler creates a new peers Handler
    -func NewHandler(accountManager server.AccountManager, authCfg configs.AuthCfg) *Handler {
    +func NewHandler(accountManager server.AccountManager) *Handler {
     	return &Handler{
     		accountManager: accountManager,
    -		claimsExtractor: jwtclaims.NewClaimsExtractor(
    -			jwtclaims.WithAudience(authCfg.Audience),
    -			jwtclaims.WithUserIDClaim(authCfg.UserIDClaim),
    -		),
     	}
     }
     
    @@ -149,12 +143,13 @@ func (h *Handler) deletePeer(ctx context.Context, accountID, userID string, peer
     
     // HandlePeer handles all peer requests for GET, PUT and DELETE operations
     func (h *Handler) HandlePeer(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
    +
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
     	vars := mux.Vars(r)
     	peerID := vars["peerId"]
     	if len(peerID) == 0 {
    @@ -179,13 +174,14 @@ func (h *Handler) HandlePeer(w http.ResponseWriter, r *http.Request) {
     
     // GetAllPeers returns a list of all peers associated with a provided account
     func (h *Handler) GetAllPeers(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
    +
     	peers, err := h.accountManager.GetPeers(r.Context(), accountID, userID)
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
    @@ -230,13 +226,14 @@ func (h *Handler) setApprovalRequiredFlag(respBody []*api.PeerBatch, approvedPee
     
     // GetAccessiblePeers returns a list of all peers that the specified peer can connect to within the network.
     func (h *Handler) GetAccessiblePeers(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
    +
     	vars := mux.Vars(r)
     	peerID := vars["peerId"]
     	if len(peerID) == 0 {
    diff --git a/management/server/http/handlers/peers/peers_handler_test.go b/management/server/http/handlers/peers/peers_handler_test.go
    index 16065a677..63b8c0ab3 100644
    --- a/management/server/http/handlers/peers/peers_handler_test.go
    +++ b/management/server/http/handlers/peers/peers_handler_test.go
    @@ -15,8 +15,8 @@ import (
     	"github.com/gorilla/mux"
     	"golang.org/x/exp/maps"
     
    +	nbcontext "github.com/netbirdio/netbird/management/server/context"
     	"github.com/netbirdio/netbird/management/server/http/api"
    -	"github.com/netbirdio/netbird/management/server/jwtclaims"
     	nbpeer "github.com/netbirdio/netbird/management/server/peer"
     	"github.com/netbirdio/netbird/management/server/types"
     
    @@ -25,16 +25,13 @@ import (
     	"github.com/netbirdio/netbird/management/server/mock_server"
     )
     
    -type ctxKey string
    -
     const (
     	testPeerID                = "test_peer"
     	noUpdateChannelTestPeerID = "no-update-channel"
     
    -	adminUser          = "admin_user"
    -	regularUser        = "regular_user"
    -	serviceUser        = "service_user"
    -	userIDKey   ctxKey = "user_id"
    +	adminUser   = "admin_user"
    +	regularUser = "regular_user"
    +	serviceUser = "service_user"
     )
     
     func initTestMetaData(peers ...*nbpeer.Peer) *Handler {
    @@ -146,9 +143,6 @@ func initTestMetaData(peers ...*nbpeer.Peer) *Handler {
     			GetDNSDomainFunc: func() string {
     				return "netbird.selfhosted"
     			},
    -			GetAccountIDFromTokenFunc: func(_ context.Context, claims jwtclaims.AuthorizationClaims) (string, string, error) {
    -				return claims.AccountId, claims.UserId, nil
    -			},
     			GetAccountFunc: func(ctx context.Context, accountID string) (*types.Account, error) {
     				return account, nil
     			},
    @@ -167,16 +161,6 @@ func initTestMetaData(peers ...*nbpeer.Peer) *Handler {
     				return ok
     			},
     		},
    -		claimsExtractor: jwtclaims.NewClaimsExtractor(
    -			jwtclaims.WithFromRequestContext(func(r *http.Request) jwtclaims.AuthorizationClaims {
    -				userID := r.Context().Value(userIDKey).(string)
    -				return jwtclaims.AuthorizationClaims{
    -					UserId:    userID,
    -					Domain:    "hotmail.com",
    -					AccountId: "test_id",
    -				}
    -			}),
    -		),
     	}
     }
     
    @@ -267,8 +251,11 @@ func TestGetPeers(t *testing.T) {
     
     			recorder := httptest.NewRecorder()
     			req := httptest.NewRequest(tc.requestType, tc.requestPath, tc.requestBody)
    -			ctx := context.WithValue(context.Background(), userIDKey, "admin_user")
    -			req = req.WithContext(ctx)
    +			req = nbcontext.SetUserAuthInRequest(req, nbcontext.UserAuth{
    +				UserId:    "admin_user",
    +				Domain:    "hotmail.com",
    +				AccountId: "test_id",
    +			})
     
     			router := mux.NewRouter()
     			router.HandleFunc("/api/peers/", p.GetAllPeers).Methods("GET")
    @@ -412,8 +399,11 @@ func TestGetAccessiblePeers(t *testing.T) {
     
     			recorder := httptest.NewRecorder()
     			req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/api/peers/%s/accessible-peers", tc.peerID), nil)
    -			ctx := context.WithValue(context.Background(), userIDKey, tc.callerUserID)
    -			req = req.WithContext(ctx)
    +			req = nbcontext.SetUserAuthInRequest(req, nbcontext.UserAuth{
    +				UserId:    tc.callerUserID,
    +				Domain:    "hotmail.com",
    +				AccountId: "test_id",
    +			})
     
     			router := mux.NewRouter()
     			router.HandleFunc("/api/peers/{peerId}/accessible-peers", p.GetAccessiblePeers).Methods("GET")
    diff --git a/management/server/http/handlers/policies/geolocation_handler_test.go b/management/server/http/handlers/policies/geolocation_handler_test.go
    index fc5839baa..fbdc324d6 100644
    --- a/management/server/http/handlers/policies/geolocation_handler_test.go
    +++ b/management/server/http/handlers/policies/geolocation_handler_test.go
    @@ -13,9 +13,9 @@ import (
     	"github.com/gorilla/mux"
     	"github.com/stretchr/testify/assert"
     
    +	nbcontext "github.com/netbirdio/netbird/management/server/context"
     	"github.com/netbirdio/netbird/management/server/geolocation"
     	"github.com/netbirdio/netbird/management/server/http/api"
    -	"github.com/netbirdio/netbird/management/server/jwtclaims"
     	"github.com/netbirdio/netbird/management/server/mock_server"
     	"github.com/netbirdio/netbird/management/server/types"
     	"github.com/netbirdio/netbird/util"
    @@ -43,23 +43,11 @@ func initGeolocationTestData(t *testing.T) *geolocationsHandler {
     
     	return &geolocationsHandler{
     		accountManager: &mock_server.MockAccountManager{
    -			GetAccountIDFromTokenFunc: func(_ context.Context, claims jwtclaims.AuthorizationClaims) (string, string, error) {
    -				return claims.AccountId, claims.UserId, nil
    -			},
     			GetUserByIDFunc: func(ctx context.Context, id string) (*types.User, error) {
     				return types.NewAdminUser(id), nil
     			},
     		},
     		geolocationManager: geo,
    -		claimsExtractor: jwtclaims.NewClaimsExtractor(
    -			jwtclaims.WithFromRequestContext(func(r *http.Request) jwtclaims.AuthorizationClaims {
    -				return jwtclaims.AuthorizationClaims{
    -					UserId:    "test_user",
    -					Domain:    "hotmail.com",
    -					AccountId: "test_id",
    -				}
    -			}),
    -		),
     	}
     }
     
    @@ -112,6 +100,11 @@ func TestGetCitiesByCountry(t *testing.T) {
     		t.Run(tc.name, func(t *testing.T) {
     			recorder := httptest.NewRecorder()
     			req := httptest.NewRequest(tc.requestType, tc.requestPath, nil)
    +			req = nbcontext.SetUserAuthInRequest(req, nbcontext.UserAuth{
    +				UserId:    "test_user",
    +				Domain:    "hotmail.com",
    +				AccountId: "test_id",
    +			})
     
     			router := mux.NewRouter()
     			router.HandleFunc("/api/locations/countries/{country}/cities", geolocationHandler.getCitiesByCountry).Methods("GET")
    @@ -200,6 +193,11 @@ func TestGetAllCountries(t *testing.T) {
     		t.Run(tc.name, func(t *testing.T) {
     			recorder := httptest.NewRecorder()
     			req := httptest.NewRequest(tc.requestType, tc.requestPath, nil)
    +			req = nbcontext.SetUserAuthInRequest(req, nbcontext.UserAuth{
    +				UserId:    "test_user",
    +				Domain:    "hotmail.com",
    +				AccountId: "test_id",
    +			})
     
     			router := mux.NewRouter()
     			router.HandleFunc("/api/locations/countries", geolocationHandler.getAllCountries).Methods("GET")
    diff --git a/management/server/http/handlers/policies/geolocations_handler.go b/management/server/http/handlers/policies/geolocations_handler.go
    index 161d97402..c4868f879 100644
    --- a/management/server/http/handlers/policies/geolocations_handler.go
    +++ b/management/server/http/handlers/policies/geolocations_handler.go
    @@ -7,11 +7,10 @@ import (
     	"github.com/gorilla/mux"
     
     	"github.com/netbirdio/netbird/management/server"
    +	nbcontext "github.com/netbirdio/netbird/management/server/context"
     	"github.com/netbirdio/netbird/management/server/geolocation"
     	"github.com/netbirdio/netbird/management/server/http/api"
    -	"github.com/netbirdio/netbird/management/server/http/configs"
     	"github.com/netbirdio/netbird/management/server/http/util"
    -	"github.com/netbirdio/netbird/management/server/jwtclaims"
     	"github.com/netbirdio/netbird/management/server/status"
     )
     
    @@ -23,24 +22,19 @@ var (
     type geolocationsHandler struct {
     	accountManager     server.AccountManager
     	geolocationManager geolocation.Geolocation
    -	claimsExtractor    *jwtclaims.ClaimsExtractor
     }
     
    -func addLocationsEndpoint(accountManager server.AccountManager, locationManager geolocation.Geolocation, authCfg configs.AuthCfg, router *mux.Router) {
    -	locationHandler := newGeolocationsHandlerHandler(accountManager, locationManager, authCfg)
    +func addLocationsEndpoint(accountManager server.AccountManager, locationManager geolocation.Geolocation, router *mux.Router) {
    +	locationHandler := newGeolocationsHandlerHandler(accountManager, locationManager)
     	router.HandleFunc("/locations/countries", locationHandler.getAllCountries).Methods("GET", "OPTIONS")
     	router.HandleFunc("/locations/countries/{country}/cities", locationHandler.getCitiesByCountry).Methods("GET", "OPTIONS")
     }
     
     // newGeolocationsHandlerHandler creates a new Geolocations handler
    -func newGeolocationsHandlerHandler(accountManager server.AccountManager, geolocationManager geolocation.Geolocation, authCfg configs.AuthCfg) *geolocationsHandler {
    +func newGeolocationsHandlerHandler(accountManager server.AccountManager, geolocationManager geolocation.Geolocation) *geolocationsHandler {
     	return &geolocationsHandler{
     		accountManager:     accountManager,
     		geolocationManager: geolocationManager,
    -		claimsExtractor: jwtclaims.NewClaimsExtractor(
    -			jwtclaims.WithAudience(authCfg.Audience),
    -			jwtclaims.WithUserIDClaim(authCfg.UserIDClaim),
    -		),
     	}
     }
     
    @@ -104,12 +98,13 @@ func (l *geolocationsHandler) getCitiesByCountry(w http.ResponseWriter, r *http.
     }
     
     func (l *geolocationsHandler) authenticateUser(r *http.Request) error {
    -	claims := l.claimsExtractor.FromRequestContext(r)
    -	_, userID, err := l.accountManager.GetAccountIDFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		return err
     	}
     
    +	_, userID := userAuth.AccountId, userAuth.UserId
    +
     	user, err := l.accountManager.GetUserByID(r.Context(), userID)
     	if err != nil {
     		return err
    diff --git a/management/server/http/handlers/policies/policies_handler.go b/management/server/http/handlers/policies/policies_handler.go
    index a748e73b8..63fc8a03b 100644
    --- a/management/server/http/handlers/policies/policies_handler.go
    +++ b/management/server/http/handlers/policies/policies_handler.go
    @@ -8,51 +8,46 @@ import (
     	"github.com/gorilla/mux"
     
     	"github.com/netbirdio/netbird/management/server"
    +	nbcontext "github.com/netbirdio/netbird/management/server/context"
     	"github.com/netbirdio/netbird/management/server/geolocation"
     	"github.com/netbirdio/netbird/management/server/http/api"
    -	"github.com/netbirdio/netbird/management/server/http/configs"
     	"github.com/netbirdio/netbird/management/server/http/util"
    -	"github.com/netbirdio/netbird/management/server/jwtclaims"
     	"github.com/netbirdio/netbird/management/server/status"
     	"github.com/netbirdio/netbird/management/server/types"
     )
     
     // handler is a handler that returns policy of the account
     type handler struct {
    -	accountManager  server.AccountManager
    -	claimsExtractor *jwtclaims.ClaimsExtractor
    +	accountManager server.AccountManager
     }
     
    -func AddEndpoints(accountManager server.AccountManager, locationManager geolocation.Geolocation, authCfg configs.AuthCfg, router *mux.Router) {
    -	policiesHandler := newHandler(accountManager, authCfg)
    +func AddEndpoints(accountManager server.AccountManager, locationManager geolocation.Geolocation, router *mux.Router) {
    +	policiesHandler := newHandler(accountManager)
     	router.HandleFunc("/policies", policiesHandler.getAllPolicies).Methods("GET", "OPTIONS")
     	router.HandleFunc("/policies", policiesHandler.createPolicy).Methods("POST", "OPTIONS")
     	router.HandleFunc("/policies/{policyId}", policiesHandler.updatePolicy).Methods("PUT", "OPTIONS")
     	router.HandleFunc("/policies/{policyId}", policiesHandler.getPolicy).Methods("GET", "OPTIONS")
     	router.HandleFunc("/policies/{policyId}", policiesHandler.deletePolicy).Methods("DELETE", "OPTIONS")
    -	addPostureCheckEndpoint(accountManager, locationManager, authCfg, router)
    +	addPostureCheckEndpoint(accountManager, locationManager, router)
     }
     
     // newHandler creates a new policies handler
    -func newHandler(accountManager server.AccountManager, authCfg configs.AuthCfg) *handler {
    +func newHandler(accountManager server.AccountManager) *handler {
     	return &handler{
     		accountManager: accountManager,
    -		claimsExtractor: jwtclaims.NewClaimsExtractor(
    -			jwtclaims.WithAudience(authCfg.Audience),
    -			jwtclaims.WithUserIDClaim(authCfg.UserIDClaim),
    -		),
     	}
     }
     
     // getAllPolicies list for the account
     func (h *handler) getAllPolicies(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
    +
     	listPolicies, err := h.accountManager.ListPolicies(r.Context(), accountID, userID)
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
    @@ -80,13 +75,14 @@ func (h *handler) getAllPolicies(w http.ResponseWriter, r *http.Request) {
     
     // updatePolicy handles update to a policy identified by a given ID
     func (h *handler) updatePolicy(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
    +
     	vars := mux.Vars(r)
     	policyID := vars["policyId"]
     	if len(policyID) == 0 {
    @@ -105,13 +101,14 @@ func (h *handler) updatePolicy(w http.ResponseWriter, r *http.Request) {
     
     // createPolicy handles policy creation request
     func (h *handler) createPolicy(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
    +
     	h.savePolicy(w, r, accountID, userID, "")
     }
     
    @@ -306,13 +303,13 @@ func (h *handler) savePolicy(w http.ResponseWriter, r *http.Request, accountID s
     
     // deletePolicy handles policy deletion request
     func (h *handler) deletePolicy(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
     	vars := mux.Vars(r)
     	policyID := vars["policyId"]
     	if len(policyID) == 0 {
    @@ -330,13 +327,14 @@ func (h *handler) deletePolicy(w http.ResponseWriter, r *http.Request) {
     
     // getPolicy handles a group Get request identified by ID
     func (h *handler) getPolicy(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
    +
     	vars := mux.Vars(r)
     	policyID := vars["policyId"]
     	if len(policyID) == 0 {
    diff --git a/management/server/http/handlers/policies/policies_handler_test.go b/management/server/http/handlers/policies/policies_handler_test.go
    index 8fbf84d4b..6450295eb 100644
    --- a/management/server/http/handlers/policies/policies_handler_test.go
    +++ b/management/server/http/handlers/policies/policies_handler_test.go
    @@ -13,8 +13,8 @@ import (
     	"github.com/gorilla/mux"
     	"github.com/stretchr/testify/assert"
     
    +	nbcontext "github.com/netbirdio/netbird/management/server/context"
     	"github.com/netbirdio/netbird/management/server/http/api"
    -	"github.com/netbirdio/netbird/management/server/jwtclaims"
     	"github.com/netbirdio/netbird/management/server/mock_server"
     	"github.com/netbirdio/netbird/management/server/status"
     	"github.com/netbirdio/netbird/management/server/types"
    @@ -44,9 +44,6 @@ func initPoliciesTestData(policies ...*types.Policy) *handler {
     			GetAllGroupsFunc: func(ctx context.Context, accountID, userID string) ([]*types.Group, error) {
     				return []*types.Group{{ID: "F"}, {ID: "G"}}, nil
     			},
    -			GetAccountIDFromTokenFunc: func(_ context.Context, claims jwtclaims.AuthorizationClaims) (string, string, error) {
    -				return claims.AccountId, claims.UserId, nil
    -			},
     			GetAccountByIDFunc: func(ctx context.Context, accountID string, userID string) (*types.Account, error) {
     				user := types.NewAdminUser(userID)
     				return &types.Account{
    @@ -65,15 +62,6 @@ func initPoliciesTestData(policies ...*types.Policy) *handler {
     				}, nil
     			},
     		},
    -		claimsExtractor: jwtclaims.NewClaimsExtractor(
    -			jwtclaims.WithFromRequestContext(func(r *http.Request) jwtclaims.AuthorizationClaims {
    -				return jwtclaims.AuthorizationClaims{
    -					UserId:    "test_user",
    -					Domain:    "hotmail.com",
    -					AccountId: "test_id",
    -				}
    -			}),
    -		),
     	}
     }
     
    @@ -115,6 +103,11 @@ func TestPoliciesGetPolicy(t *testing.T) {
     		t.Run(tc.name, func(t *testing.T) {
     			recorder := httptest.NewRecorder()
     			req := httptest.NewRequest(tc.requestType, tc.requestPath, tc.requestBody)
    +			req = nbcontext.SetUserAuthInRequest(req, nbcontext.UserAuth{
    +				UserId:    "test_user",
    +				Domain:    "hotmail.com",
    +				AccountId: "test_id",
    +			})
     
     			router := mux.NewRouter()
     			router.HandleFunc("/api/policies/{policyId}", p.getPolicy).Methods("GET")
    @@ -274,6 +267,11 @@ func TestPoliciesWritePolicy(t *testing.T) {
     		t.Run(tc.name, func(t *testing.T) {
     			recorder := httptest.NewRecorder()
     			req := httptest.NewRequest(tc.requestType, tc.requestPath, tc.requestBody)
    +			req = nbcontext.SetUserAuthInRequest(req, nbcontext.UserAuth{
    +				UserId:    "test_user",
    +				Domain:    "hotmail.com",
    +				AccountId: "test_id",
    +			})
     
     			router := mux.NewRouter()
     			router.HandleFunc("/api/policies", p.createPolicy).Methods("POST")
    diff --git a/management/server/http/handlers/policies/posture_checks_handler.go b/management/server/http/handlers/policies/posture_checks_handler.go
    index ce0d4878c..e6e58da58 100644
    --- a/management/server/http/handlers/policies/posture_checks_handler.go
    +++ b/management/server/http/handlers/policies/posture_checks_handler.go
    @@ -7,11 +7,10 @@ import (
     	"github.com/gorilla/mux"
     
     	"github.com/netbirdio/netbird/management/server"
    +	nbcontext "github.com/netbirdio/netbird/management/server/context"
     	"github.com/netbirdio/netbird/management/server/geolocation"
     	"github.com/netbirdio/netbird/management/server/http/api"
    -	"github.com/netbirdio/netbird/management/server/http/configs"
     	"github.com/netbirdio/netbird/management/server/http/util"
    -	"github.com/netbirdio/netbird/management/server/jwtclaims"
     	"github.com/netbirdio/netbird/management/server/posture"
     	"github.com/netbirdio/netbird/management/server/status"
     )
    @@ -20,40 +19,35 @@ import (
     type postureChecksHandler struct {
     	accountManager     server.AccountManager
     	geolocationManager geolocation.Geolocation
    -	claimsExtractor    *jwtclaims.ClaimsExtractor
     }
     
    -func addPostureCheckEndpoint(accountManager server.AccountManager, locationManager geolocation.Geolocation, authCfg configs.AuthCfg, router *mux.Router) {
    -	postureCheckHandler := newPostureChecksHandler(accountManager, locationManager, authCfg)
    +func addPostureCheckEndpoint(accountManager server.AccountManager, locationManager geolocation.Geolocation, router *mux.Router) {
    +	postureCheckHandler := newPostureChecksHandler(accountManager, locationManager)
     	router.HandleFunc("/posture-checks", postureCheckHandler.getAllPostureChecks).Methods("GET", "OPTIONS")
     	router.HandleFunc("/posture-checks", postureCheckHandler.createPostureCheck).Methods("POST", "OPTIONS")
     	router.HandleFunc("/posture-checks/{postureCheckId}", postureCheckHandler.updatePostureCheck).Methods("PUT", "OPTIONS")
     	router.HandleFunc("/posture-checks/{postureCheckId}", postureCheckHandler.getPostureCheck).Methods("GET", "OPTIONS")
     	router.HandleFunc("/posture-checks/{postureCheckId}", postureCheckHandler.deletePostureCheck).Methods("DELETE", "OPTIONS")
    -	addLocationsEndpoint(accountManager, locationManager, authCfg, router)
    +	addLocationsEndpoint(accountManager, locationManager, router)
     }
     
     // newPostureChecksHandler creates a new PostureChecks handler
    -func newPostureChecksHandler(accountManager server.AccountManager, geolocationManager geolocation.Geolocation, authCfg configs.AuthCfg) *postureChecksHandler {
    +func newPostureChecksHandler(accountManager server.AccountManager, geolocationManager geolocation.Geolocation) *postureChecksHandler {
     	return &postureChecksHandler{
     		accountManager:     accountManager,
     		geolocationManager: geolocationManager,
    -		claimsExtractor: jwtclaims.NewClaimsExtractor(
    -			jwtclaims.WithAudience(authCfg.Audience),
    -			jwtclaims.WithUserIDClaim(authCfg.UserIDClaim),
    -		),
     	}
     }
     
     // getAllPostureChecks list for the account
     func (p *postureChecksHandler) getAllPostureChecks(w http.ResponseWriter, r *http.Request) {
    -	claims := p.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := p.accountManager.GetAccountIDFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
     	listPostureChecks, err := p.accountManager.ListPostureChecks(r.Context(), accountID, userID)
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
    @@ -70,13 +64,14 @@ func (p *postureChecksHandler) getAllPostureChecks(w http.ResponseWriter, r *htt
     
     // updatePostureCheck handles update to a posture check identified by a given ID
     func (p *postureChecksHandler) updatePostureCheck(w http.ResponseWriter, r *http.Request) {
    -	claims := p.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := p.accountManager.GetAccountIDFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
    +
     	vars := mux.Vars(r)
     	postureChecksID := vars["postureCheckId"]
     	if len(postureChecksID) == 0 {
    @@ -95,25 +90,26 @@ func (p *postureChecksHandler) updatePostureCheck(w http.ResponseWriter, r *http
     
     // createPostureCheck handles posture check creation request
     func (p *postureChecksHandler) createPostureCheck(w http.ResponseWriter, r *http.Request) {
    -	claims := p.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := p.accountManager.GetAccountIDFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
    +
     	p.savePostureChecks(w, r, accountID, userID, "")
     }
     
     // getPostureCheck handles a posture check Get request identified by ID
     func (p *postureChecksHandler) getPostureCheck(w http.ResponseWriter, r *http.Request) {
    -	claims := p.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := p.accountManager.GetAccountIDFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
     	vars := mux.Vars(r)
     	postureChecksID := vars["postureCheckId"]
     	if len(postureChecksID) == 0 {
    @@ -132,13 +128,13 @@ func (p *postureChecksHandler) getPostureCheck(w http.ResponseWriter, r *http.Re
     
     // deletePostureCheck handles posture check deletion request
     func (p *postureChecksHandler) deletePostureCheck(w http.ResponseWriter, r *http.Request) {
    -	claims := p.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := p.accountManager.GetAccountIDFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
     	vars := mux.Vars(r)
     	postureChecksID := vars["postureCheckId"]
     	if len(postureChecksID) == 0 {
    diff --git a/management/server/http/handlers/policies/posture_checks_handler_test.go b/management/server/http/handlers/policies/posture_checks_handler_test.go
    index 237687fd4..e3844caa2 100644
    --- a/management/server/http/handlers/policies/posture_checks_handler_test.go
    +++ b/management/server/http/handlers/policies/posture_checks_handler_test.go
    @@ -14,9 +14,9 @@ import (
     	"github.com/gorilla/mux"
     	"github.com/stretchr/testify/assert"
     
    +	nbcontext "github.com/netbirdio/netbird/management/server/context"
     	"github.com/netbirdio/netbird/management/server/geolocation"
     	"github.com/netbirdio/netbird/management/server/http/api"
    -	"github.com/netbirdio/netbird/management/server/jwtclaims"
     	"github.com/netbirdio/netbird/management/server/mock_server"
     	"github.com/netbirdio/netbird/management/server/posture"
     	"github.com/netbirdio/netbird/management/server/status"
    @@ -66,20 +66,8 @@ func initPostureChecksTestData(postureChecks ...*posture.Checks) *postureChecksH
     				}
     				return accountPostureChecks, nil
     			},
    -			GetAccountIDFromTokenFunc: func(_ context.Context, claims jwtclaims.AuthorizationClaims) (string, string, error) {
    -				return claims.AccountId, claims.UserId, nil
    -			},
     		},
     		geolocationManager: &geolocation.Mock{},
    -		claimsExtractor: jwtclaims.NewClaimsExtractor(
    -			jwtclaims.WithFromRequestContext(func(r *http.Request) jwtclaims.AuthorizationClaims {
    -				return jwtclaims.AuthorizationClaims{
    -					UserId:    "test_user",
    -					Domain:    "hotmail.com",
    -					AccountId: "test_id",
    -				}
    -			}),
    -		),
     	}
     }
     
    @@ -187,6 +175,11 @@ func TestGetPostureCheck(t *testing.T) {
     		t.Run(tc.name, func(t *testing.T) {
     			recorder := httptest.NewRecorder()
     			req := httptest.NewRequest(http.MethodGet, "/api/posture-checks/"+tc.id, tc.requestBody)
    +			req = nbcontext.SetUserAuthInRequest(req, nbcontext.UserAuth{
    +				UserId:    "test_user",
    +				Domain:    "hotmail.com",
    +				AccountId: "test_id",
    +			})
     
     			router := mux.NewRouter()
     			router.HandleFunc("/api/posture-checks/{postureCheckId}", p.getPostureCheck).Methods("GET")
    @@ -835,6 +828,11 @@ func TestPostureCheckUpdate(t *testing.T) {
     		t.Run(tc.name, func(t *testing.T) {
     			recorder := httptest.NewRecorder()
     			req := httptest.NewRequest(tc.requestType, tc.requestPath, tc.requestBody)
    +			req = nbcontext.SetUserAuthInRequest(req, nbcontext.UserAuth{
    +				UserId:    "test_user",
    +				Domain:    "hotmail.com",
    +				AccountId: "test_id",
    +			})
     
     			defaultHandler := *p
     			if tc.setupHandlerFunc != nil {
    diff --git a/management/server/http/handlers/routes/routes_handler.go b/management/server/http/handlers/routes/routes_handler.go
    index 6b6c37910..0f0d24780 100644
    --- a/management/server/http/handlers/routes/routes_handler.go
    +++ b/management/server/http/handlers/routes/routes_handler.go
    @@ -10,10 +10,9 @@ import (
     
     	"github.com/netbirdio/netbird/management/domain"
     	"github.com/netbirdio/netbird/management/server"
    +	nbcontext "github.com/netbirdio/netbird/management/server/context"
     	"github.com/netbirdio/netbird/management/server/http/api"
    -	"github.com/netbirdio/netbird/management/server/http/configs"
     	"github.com/netbirdio/netbird/management/server/http/util"
    -	"github.com/netbirdio/netbird/management/server/jwtclaims"
     	"github.com/netbirdio/netbird/management/server/status"
     	"github.com/netbirdio/netbird/route"
     )
    @@ -22,12 +21,11 @@ const failedToConvertRoute = "failed to convert route to response: %v"
     
     // handler is the routes handler of the account
     type handler struct {
    -	accountManager  server.AccountManager
    -	claimsExtractor *jwtclaims.ClaimsExtractor
    +	accountManager server.AccountManager
     }
     
    -func AddEndpoints(accountManager server.AccountManager, authCfg configs.AuthCfg, router *mux.Router) {
    -	routesHandler := newHandler(accountManager, authCfg)
    +func AddEndpoints(accountManager server.AccountManager, router *mux.Router) {
    +	routesHandler := newHandler(accountManager)
     	router.HandleFunc("/routes", routesHandler.getAllRoutes).Methods("GET", "OPTIONS")
     	router.HandleFunc("/routes", routesHandler.createRoute).Methods("POST", "OPTIONS")
     	router.HandleFunc("/routes/{routeId}", routesHandler.updateRoute).Methods("PUT", "OPTIONS")
    @@ -36,25 +34,22 @@ func AddEndpoints(accountManager server.AccountManager, authCfg configs.AuthCfg,
     }
     
     // newHandler returns a new instance of routes handler
    -func newHandler(accountManager server.AccountManager, authCfg configs.AuthCfg) *handler {
    +func newHandler(accountManager server.AccountManager) *handler {
     	return &handler{
     		accountManager: accountManager,
    -		claimsExtractor: jwtclaims.NewClaimsExtractor(
    -			jwtclaims.WithAudience(authCfg.Audience),
    -			jwtclaims.WithUserIDClaim(authCfg.UserIDClaim),
    -		),
     	}
     }
     
     // getAllRoutes returns the list of routes for the account
     func (h *handler) getAllRoutes(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
    +
     	routes, err := h.accountManager.ListRoutes(r.Context(), accountID, userID)
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
    @@ -75,13 +70,14 @@ func (h *handler) getAllRoutes(w http.ResponseWriter, r *http.Request) {
     
     // createRoute handles route creation request
     func (h *handler) createRoute(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
    +
     	var req api.PostApiRoutesJSONRequestBody
     	err = json.NewDecoder(r.Body).Decode(&req)
     	if err != nil {
    @@ -172,13 +168,13 @@ func (h *handler) validateRoute(req api.PostApiRoutesJSONRequestBody) error {
     
     // updateRoute handles update to a route identified by a given ID
     func (h *handler) updateRoute(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
     	vars := mux.Vars(r)
     	routeID := vars["routeId"]
     	if len(routeID) == 0 {
    @@ -265,13 +261,13 @@ func (h *handler) updateRoute(w http.ResponseWriter, r *http.Request) {
     
     // deleteRoute handles route deletion request
     func (h *handler) deleteRoute(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
     	routeID := mux.Vars(r)["routeId"]
     	if len(routeID) == 0 {
     		util.WriteError(r.Context(), status.Errorf(status.InvalidArgument, "invalid route ID"), w)
    @@ -289,13 +285,14 @@ func (h *handler) deleteRoute(w http.ResponseWriter, r *http.Request) {
     
     // getRoute handles a route Get request identified by ID
     func (h *handler) getRoute(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
    +
     	routeID := mux.Vars(r)["routeId"]
     	if len(routeID) == 0 {
     		util.WriteError(r.Context(), status.Errorf(status.InvalidArgument, "invalid route ID"), w)
    diff --git a/management/server/http/handlers/routes/routes_handler_test.go b/management/server/http/handlers/routes/routes_handler_test.go
    index f3bd79ee4..ad1f8912d 100644
    --- a/management/server/http/handlers/routes/routes_handler_test.go
    +++ b/management/server/http/handlers/routes/routes_handler_test.go
    @@ -16,12 +16,10 @@ import (
     	"github.com/stretchr/testify/require"
     
     	"github.com/netbirdio/netbird/management/domain"
    +	nbcontext "github.com/netbirdio/netbird/management/server/context"
     	"github.com/netbirdio/netbird/management/server/http/api"
    -	"github.com/netbirdio/netbird/management/server/jwtclaims"
     	"github.com/netbirdio/netbird/management/server/mock_server"
    -	nbpeer "github.com/netbirdio/netbird/management/server/peer"
     	"github.com/netbirdio/netbird/management/server/status"
    -	"github.com/netbirdio/netbird/management/server/types"
     	"github.com/netbirdio/netbird/management/server/util"
     	"github.com/netbirdio/netbird/route"
     )
    @@ -60,32 +58,6 @@ var baseExistingRoute = &route.Route{
     	Groups:      []string{existingGroupID},
     }
     
    -var testingAccount = &types.Account{
    -	Id:     testAccountID,
    -	Domain: "hotmail.com",
    -	Peers: map[string]*nbpeer.Peer{
    -		existingPeerID: {
    -			Key: existingPeerKey,
    -			IP:  netip.MustParseAddr(existingPeerIP1).AsSlice(),
    -			ID:  existingPeerID,
    -			Meta: nbpeer.PeerSystemMeta{
    -				GoOS: "linux",
    -			},
    -		},
    -		nonLinuxExistingPeerID: {
    -			Key: nonLinuxExistingPeerID,
    -			IP:  netip.MustParseAddr(existingPeerIP2).AsSlice(),
    -			ID:  nonLinuxExistingPeerID,
    -			Meta: nbpeer.PeerSystemMeta{
    -				GoOS: "darwin",
    -			},
    -		},
    -	},
    -	Users: map[string]*types.User{
    -		"test_user": types.NewAdminUser("test_user"),
    -	},
    -}
    -
     func initRoutesTestData() *handler {
     	return &handler{
     		accountManager: &mock_server.MockAccountManager{
    @@ -150,20 +122,7 @@ func initRoutesTestData() *handler {
     				}
     				return nil
     			},
    -			GetAccountIDFromTokenFunc: func(_ context.Context, _ jwtclaims.AuthorizationClaims) (string, string, error) {
    -				// return testingAccount, testingAccount.Users["test_user"], nil
    -				return testingAccount.Id, testingAccount.Users["test_user"].Id, nil
    -			},
     		},
    -		claimsExtractor: jwtclaims.NewClaimsExtractor(
    -			jwtclaims.WithFromRequestContext(func(r *http.Request) jwtclaims.AuthorizationClaims {
    -				return jwtclaims.AuthorizationClaims{
    -					UserId:    "test_user",
    -					Domain:    "hotmail.com",
    -					AccountId: testAccountID,
    -				}
    -			}),
    -		),
     	}
     }
     
    @@ -526,6 +485,11 @@ func TestRoutesHandlers(t *testing.T) {
     		t.Run(tc.name, func(t *testing.T) {
     			recorder := httptest.NewRecorder()
     			req := httptest.NewRequest(tc.requestType, tc.requestPath, tc.requestBody)
    +			req = nbcontext.SetUserAuthInRequest(req, nbcontext.UserAuth{
    +				UserId:    "test_user",
    +				Domain:    "hotmail.com",
    +				AccountId: testAccountID,
    +			})
     
     			router := mux.NewRouter()
     			router.HandleFunc("/api/routes/{routeId}", p.getRoute).Methods("GET")
    diff --git a/management/server/http/handlers/setup_keys/setupkeys_handler.go b/management/server/http/handlers/setup_keys/setupkeys_handler.go
    index 3bd3ef589..8095f43b0 100644
    --- a/management/server/http/handlers/setup_keys/setupkeys_handler.go
    +++ b/management/server/http/handlers/setup_keys/setupkeys_handler.go
    @@ -10,22 +10,20 @@ import (
     	"github.com/gorilla/mux"
     
     	"github.com/netbirdio/netbird/management/server"
    +	nbcontext "github.com/netbirdio/netbird/management/server/context"
     	"github.com/netbirdio/netbird/management/server/http/api"
    -	"github.com/netbirdio/netbird/management/server/http/configs"
     	"github.com/netbirdio/netbird/management/server/http/util"
    -	"github.com/netbirdio/netbird/management/server/jwtclaims"
     	"github.com/netbirdio/netbird/management/server/status"
     	"github.com/netbirdio/netbird/management/server/types"
     )
     
     // handler is a handler that returns a list of setup keys of the account
     type handler struct {
    -	accountManager  server.AccountManager
    -	claimsExtractor *jwtclaims.ClaimsExtractor
    +	accountManager server.AccountManager
     }
     
    -func AddEndpoints(accountManager server.AccountManager, authCfg configs.AuthCfg, router *mux.Router) {
    -	keysHandler := newHandler(accountManager, authCfg)
    +func AddEndpoints(accountManager server.AccountManager, router *mux.Router) {
    +	keysHandler := newHandler(accountManager)
     	router.HandleFunc("/setup-keys", keysHandler.getAllSetupKeys).Methods("GET", "OPTIONS")
     	router.HandleFunc("/setup-keys", keysHandler.createSetupKey).Methods("POST", "OPTIONS")
     	router.HandleFunc("/setup-keys/{keyId}", keysHandler.getSetupKey).Methods("GET", "OPTIONS")
    @@ -34,25 +32,21 @@ func AddEndpoints(accountManager server.AccountManager, authCfg configs.AuthCfg,
     }
     
     // newHandler creates a new setup key handler
    -func newHandler(accountManager server.AccountManager, authCfg configs.AuthCfg) *handler {
    +func newHandler(accountManager server.AccountManager) *handler {
     	return &handler{
     		accountManager: accountManager,
    -		claimsExtractor: jwtclaims.NewClaimsExtractor(
    -			jwtclaims.WithAudience(authCfg.Audience),
    -			jwtclaims.WithUserIDClaim(authCfg.UserIDClaim),
    -		),
     	}
     }
     
     // createSetupKey is a POST requests that creates a new SetupKey
     func (h *handler) createSetupKey(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
     	req := &api.PostApiSetupKeysJSONRequestBody{}
     	err = json.NewDecoder(r.Body).Decode(&req)
     	if err != nil {
    @@ -108,12 +102,12 @@ func (h *handler) createSetupKey(w http.ResponseWriter, r *http.Request) {
     
     // getSetupKey is a GET request to get a SetupKey by ID
     func (h *handler) getSetupKey(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
     
     	vars := mux.Vars(r)
     	keyID := vars["keyId"]
    @@ -133,13 +127,13 @@ func (h *handler) getSetupKey(w http.ResponseWriter, r *http.Request) {
     
     // updateSetupKey is a PUT request to update server.SetupKey
     func (h *handler) updateSetupKey(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
     	vars := mux.Vars(r)
     	keyID := vars["keyId"]
     	if len(keyID) == 0 {
    @@ -174,13 +168,13 @@ func (h *handler) updateSetupKey(w http.ResponseWriter, r *http.Request) {
     
     // getAllSetupKeys is a GET request that returns a list of SetupKey
     func (h *handler) getAllSetupKeys(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
     	setupKeys, err := h.accountManager.ListSetupKeys(r.Context(), accountID, userID)
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
    @@ -196,13 +190,13 @@ func (h *handler) getAllSetupKeys(w http.ResponseWriter, r *http.Request) {
     }
     
     func (h *handler) deleteSetupKey(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
     	vars := mux.Vars(r)
     	keyID := vars["keyId"]
     	if len(keyID) == 0 {
    diff --git a/management/server/http/handlers/setup_keys/setupkeys_handler_test.go b/management/server/http/handlers/setup_keys/setupkeys_handler_test.go
    index 4912f9639..e9135469f 100644
    --- a/management/server/http/handlers/setup_keys/setupkeys_handler_test.go
    +++ b/management/server/http/handlers/setup_keys/setupkeys_handler_test.go
    @@ -14,8 +14,8 @@ import (
     	"github.com/gorilla/mux"
     	"github.com/stretchr/testify/assert"
     
    +	nbcontext "github.com/netbirdio/netbird/management/server/context"
     	"github.com/netbirdio/netbird/management/server/http/api"
    -	"github.com/netbirdio/netbird/management/server/jwtclaims"
     	"github.com/netbirdio/netbird/management/server/mock_server"
     	"github.com/netbirdio/netbird/management/server/status"
     	"github.com/netbirdio/netbird/management/server/types"
    @@ -28,14 +28,9 @@ const (
     	notFoundSetupKeyID  = "notFoundSetupKeyID"
     )
     
    -func initSetupKeysTestMetaData(defaultKey *types.SetupKey, newKey *types.SetupKey, updatedSetupKey *types.SetupKey,
    -	user *types.User,
    -) *handler {
    +func initSetupKeysTestMetaData(defaultKey *types.SetupKey, newKey *types.SetupKey, updatedSetupKey *types.SetupKey) *handler {
     	return &handler{
     		accountManager: &mock_server.MockAccountManager{
    -			GetAccountIDFromTokenFunc: func(_ context.Context, claims jwtclaims.AuthorizationClaims) (string, string, error) {
    -				return claims.AccountId, claims.UserId, nil
    -			},
     			CreateSetupKeyFunc: func(_ context.Context, _ string, keyName string, typ types.SetupKeyType, _ time.Duration, _ []string,
     				_ int, _ string, ephemeral bool, allowExtraDNSLabels bool,
     			) (*types.SetupKey, error) {
    @@ -76,15 +71,6 @@ func initSetupKeysTestMetaData(defaultKey *types.SetupKey, newKey *types.SetupKe
     				return status.Errorf(status.NotFound, "key %s not found", keyID)
     			},
     		},
    -		claimsExtractor: jwtclaims.NewClaimsExtractor(
    -			jwtclaims.WithFromRequestContext(func(r *http.Request) jwtclaims.AuthorizationClaims {
    -				return jwtclaims.AuthorizationClaims{
    -					UserId:    user.Id,
    -					Domain:    "hotmail.com",
    -					AccountId: "testAccountId",
    -				}
    -			}),
    -		),
     	}
     }
     
    @@ -171,12 +157,17 @@ func TestSetupKeysHandlers(t *testing.T) {
     		},
     	}
     
    -	handler := initSetupKeysTestMetaData(defaultSetupKey, newSetupKey, updatedDefaultSetupKey, adminUser)
    +	handler := initSetupKeysTestMetaData(defaultSetupKey, newSetupKey, updatedDefaultSetupKey)
     
     	for _, tc := range tt {
     		t.Run(tc.name, func(t *testing.T) {
     			recorder := httptest.NewRecorder()
     			req := httptest.NewRequest(tc.requestType, tc.requestPath, tc.requestBody)
    +			req = nbcontext.SetUserAuthInRequest(req, nbcontext.UserAuth{
    +				UserId:    adminUser.Id,
    +				Domain:    "hotmail.com",
    +				AccountId: "testAccountId",
    +			})
     
     			router := mux.NewRouter()
     			router.HandleFunc("/api/setup-keys", handler.getAllSetupKeys).Methods("GET", "OPTIONS")
    diff --git a/management/server/http/handlers/users/pat_handler.go b/management/server/http/handlers/users/pat_handler.go
    index 7b93d2ae1..84fbef93e 100644
    --- a/management/server/http/handlers/users/pat_handler.go
    +++ b/management/server/http/handlers/users/pat_handler.go
    @@ -7,22 +7,20 @@ import (
     	"github.com/gorilla/mux"
     
     	"github.com/netbirdio/netbird/management/server"
    +	nbcontext "github.com/netbirdio/netbird/management/server/context"
     	"github.com/netbirdio/netbird/management/server/http/api"
    -	"github.com/netbirdio/netbird/management/server/http/configs"
     	"github.com/netbirdio/netbird/management/server/http/util"
    -	"github.com/netbirdio/netbird/management/server/jwtclaims"
     	"github.com/netbirdio/netbird/management/server/status"
     	"github.com/netbirdio/netbird/management/server/types"
     )
     
     // patHandler is the nameserver group handler of the account
     type patHandler struct {
    -	accountManager  server.AccountManager
    -	claimsExtractor *jwtclaims.ClaimsExtractor
    +	accountManager server.AccountManager
     }
     
    -func addUsersTokensEndpoint(accountManager server.AccountManager, authCfg configs.AuthCfg, router *mux.Router) {
    -	tokenHandler := newPATsHandler(accountManager, authCfg)
    +func addUsersTokensEndpoint(accountManager server.AccountManager, router *mux.Router) {
    +	tokenHandler := newPATsHandler(accountManager)
     	router.HandleFunc("/users/{userId}/tokens", tokenHandler.getAllTokens).Methods("GET", "OPTIONS")
     	router.HandleFunc("/users/{userId}/tokens", tokenHandler.createToken).Methods("POST", "OPTIONS")
     	router.HandleFunc("/users/{userId}/tokens/{tokenId}", tokenHandler.getToken).Methods("GET", "OPTIONS")
    @@ -30,25 +28,21 @@ func addUsersTokensEndpoint(accountManager server.AccountManager, authCfg config
     }
     
     // newPATsHandler creates a new patHandler HTTP handler
    -func newPATsHandler(accountManager server.AccountManager, authCfg configs.AuthCfg) *patHandler {
    +func newPATsHandler(accountManager server.AccountManager) *patHandler {
     	return &patHandler{
     		accountManager: accountManager,
    -		claimsExtractor: jwtclaims.NewClaimsExtractor(
    -			jwtclaims.WithAudience(authCfg.Audience),
    -			jwtclaims.WithUserIDClaim(authCfg.UserIDClaim),
    -		),
     	}
     }
     
     // getAllTokens is HTTP GET handler that returns a list of all personal access tokens for the given user
     func (h *patHandler) getAllTokens(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
     	vars := mux.Vars(r)
     	targetUserID := vars["userId"]
     	if len(userID) == 0 {
    @@ -72,13 +66,13 @@ func (h *patHandler) getAllTokens(w http.ResponseWriter, r *http.Request) {
     
     // getToken is HTTP GET handler that returns a personal access token for the given user
     func (h *patHandler) getToken(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
     	vars := mux.Vars(r)
     	targetUserID := vars["userId"]
     	if len(targetUserID) == 0 {
    @@ -103,13 +97,13 @@ func (h *patHandler) getToken(w http.ResponseWriter, r *http.Request) {
     
     // createToken is HTTP POST handler that creates a personal access token for the given user
     func (h *patHandler) createToken(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
     	vars := mux.Vars(r)
     	targetUserID := vars["userId"]
     	if len(targetUserID) == 0 {
    @@ -135,13 +129,13 @@ func (h *patHandler) createToken(w http.ResponseWriter, r *http.Request) {
     
     // deleteToken is HTTP DELETE handler that deletes a personal access token for the given user
     func (h *patHandler) deleteToken(w http.ResponseWriter, r *http.Request) {
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
     	vars := mux.Vars(r)
     	targetUserID := vars["userId"]
     	if len(targetUserID) == 0 {
    diff --git a/management/server/http/handlers/users/pat_handler_test.go b/management/server/http/handlers/users/pat_handler_test.go
    index 9388067a4..6593de64a 100644
    --- a/management/server/http/handlers/users/pat_handler_test.go
    +++ b/management/server/http/handlers/users/pat_handler_test.go
    @@ -12,11 +12,12 @@ import (
     
     	"github.com/google/go-cmp/cmp"
     	"github.com/gorilla/mux"
    -	"github.com/netbirdio/netbird/management/server/util"
     	"github.com/stretchr/testify/assert"
     
    +	"github.com/netbirdio/netbird/management/server/util"
    +
    +	nbcontext "github.com/netbirdio/netbird/management/server/context"
     	"github.com/netbirdio/netbird/management/server/http/api"
    -	"github.com/netbirdio/netbird/management/server/jwtclaims"
     	"github.com/netbirdio/netbird/management/server/mock_server"
     	"github.com/netbirdio/netbird/management/server/status"
     	"github.com/netbirdio/netbird/management/server/types"
    @@ -77,10 +78,6 @@ func initPATTestData() *patHandler {
     					PersonalAccessToken: types.PersonalAccessToken{},
     				}, nil
     			},
    -
    -			GetAccountIDFromTokenFunc: func(_ context.Context, claims jwtclaims.AuthorizationClaims) (string, string, error) {
    -				return claims.AccountId, claims.UserId, nil
    -			},
     			DeletePATFunc: func(_ context.Context, accountID string, initiatorUserID string, targetUserID string, tokenID string) error {
     				if accountID != existingAccountID {
     					return status.Errorf(status.NotFound, "account with ID %s not found", accountID)
    @@ -115,15 +112,6 @@ func initPATTestData() *patHandler {
     				return []*types.PersonalAccessToken{testAccount.Users[existingUserID].PATs[existingTokenID], testAccount.Users[existingUserID].PATs["token2"]}, nil
     			},
     		},
    -		claimsExtractor: jwtclaims.NewClaimsExtractor(
    -			jwtclaims.WithFromRequestContext(func(r *http.Request) jwtclaims.AuthorizationClaims {
    -				return jwtclaims.AuthorizationClaims{
    -					UserId:    existingUserID,
    -					Domain:    testDomain,
    -					AccountId: existingAccountID,
    -				}
    -			}),
    -		),
     	}
     }
     
    @@ -185,6 +173,11 @@ func TestTokenHandlers(t *testing.T) {
     		t.Run(tc.name, func(t *testing.T) {
     			recorder := httptest.NewRecorder()
     			req := httptest.NewRequest(tc.requestType, tc.requestPath, tc.requestBody)
    +			req = nbcontext.SetUserAuthInRequest(req, nbcontext.UserAuth{
    +				UserId:    existingUserID,
    +				Domain:    testDomain,
    +				AccountId: existingAccountID,
    +			})
     
     			router := mux.NewRouter()
     			router.HandleFunc("/api/users/{userId}/tokens", p.getAllTokens).Methods("GET")
    diff --git a/management/server/http/handlers/users/users_handler.go b/management/server/http/handlers/users/users_handler.go
    index 7380dd97e..3869f21f0 100644
    --- a/management/server/http/handlers/users/users_handler.go
    +++ b/management/server/http/handlers/users/users_handler.go
    @@ -9,39 +9,33 @@ import (
     	log "github.com/sirupsen/logrus"
     
     	"github.com/netbirdio/netbird/management/server/http/api"
    -	"github.com/netbirdio/netbird/management/server/http/configs"
     	"github.com/netbirdio/netbird/management/server/http/util"
     	"github.com/netbirdio/netbird/management/server/status"
     	"github.com/netbirdio/netbird/management/server/types"
     
     	"github.com/netbirdio/netbird/management/server"
    -	"github.com/netbirdio/netbird/management/server/jwtclaims"
    +	nbcontext "github.com/netbirdio/netbird/management/server/context"
     )
     
     // handler is a handler that returns users of the account
     type handler struct {
    -	accountManager  server.AccountManager
    -	claimsExtractor *jwtclaims.ClaimsExtractor
    +	accountManager server.AccountManager
     }
     
    -func AddEndpoints(accountManager server.AccountManager, authCfg configs.AuthCfg, router *mux.Router) {
    -	userHandler := newHandler(accountManager, authCfg)
    +func AddEndpoints(accountManager server.AccountManager, router *mux.Router) {
    +	userHandler := newHandler(accountManager)
     	router.HandleFunc("/users", userHandler.getAllUsers).Methods("GET", "OPTIONS")
     	router.HandleFunc("/users/{userId}", userHandler.updateUser).Methods("PUT", "OPTIONS")
     	router.HandleFunc("/users/{userId}", userHandler.deleteUser).Methods("DELETE", "OPTIONS")
     	router.HandleFunc("/users", userHandler.createUser).Methods("POST", "OPTIONS")
     	router.HandleFunc("/users/{userId}/invite", userHandler.inviteUser).Methods("POST", "OPTIONS")
    -	addUsersTokensEndpoint(accountManager, authCfg, router)
    +	addUsersTokensEndpoint(accountManager, router)
     }
     
     // newHandler creates a new UsersHandler HTTP handler
    -func newHandler(accountManager server.AccountManager, authCfg configs.AuthCfg) *handler {
    +func newHandler(accountManager server.AccountManager) *handler {
     	return &handler{
     		accountManager: accountManager,
    -		claimsExtractor: jwtclaims.NewClaimsExtractor(
    -			jwtclaims.WithAudience(authCfg.Audience),
    -			jwtclaims.WithUserIDClaim(authCfg.UserIDClaim),
    -		),
     	}
     }
     
    @@ -52,13 +46,13 @@ func (h *handler) updateUser(w http.ResponseWriter, r *http.Request) {
     		return
     	}
     
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
     	vars := mux.Vars(r)
     	targetUserID := vars["userId"]
     	if len(targetUserID) == 0 {
    @@ -103,7 +97,7 @@ func (h *handler) updateUser(w http.ResponseWriter, r *http.Request) {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
    -	util.WriteJSONObject(r.Context(), w, toUserResponse(newUser, claims.UserId))
    +	util.WriteJSONObject(r.Context(), w, toUserResponse(newUser, userID))
     }
     
     // deleteUser is a DELETE request to delete a user
    @@ -113,13 +107,13 @@ func (h *handler) deleteUser(w http.ResponseWriter, r *http.Request) {
     		return
     	}
     
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
     	vars := mux.Vars(r)
     	targetUserID := vars["userId"]
     	if len(targetUserID) == 0 {
    @@ -143,12 +137,12 @@ func (h *handler) createUser(w http.ResponseWriter, r *http.Request) {
     		return
     	}
     
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
     
     	req := &api.PostApiUsersJSONRequestBody{}
     	err = json.NewDecoder(r.Body).Decode(&req)
    @@ -184,7 +178,7 @@ func (h *handler) createUser(w http.ResponseWriter, r *http.Request) {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
    -	util.WriteJSONObject(r.Context(), w, toUserResponse(newUser, claims.UserId))
    +	util.WriteJSONObject(r.Context(), w, toUserResponse(newUser, userID))
     }
     
     // getAllUsers returns a list of users of the account this user belongs to.
    @@ -195,13 +189,13 @@ func (h *handler) getAllUsers(w http.ResponseWriter, r *http.Request) {
     		return
     	}
     
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
     
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
     	data, err := h.accountManager.GetUsersFromAccount(r.Context(), accountID, userID)
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
    @@ -216,7 +210,7 @@ func (h *handler) getAllUsers(w http.ResponseWriter, r *http.Request) {
     			continue
     		}
     		if serviceUser == "" {
    -			users = append(users, toUserResponse(d, claims.UserId))
    +			users = append(users, toUserResponse(d, userID))
     			continue
     		}
     
    @@ -227,7 +221,7 @@ func (h *handler) getAllUsers(w http.ResponseWriter, r *http.Request) {
     			return
     		}
     		if includeServiceUser == d.IsServiceUser {
    -			users = append(users, toUserResponse(d, claims.UserId))
    +			users = append(users, toUserResponse(d, userID))
     		}
     	}
     
    @@ -242,12 +236,12 @@ func (h *handler) inviteUser(w http.ResponseWriter, r *http.Request) {
     		return
     	}
     
    -	claims := h.claimsExtractor.FromRequestContext(r)
    -	accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
    +	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
     	if err != nil {
     		util.WriteError(r.Context(), err, w)
     		return
     	}
    +	accountID, userID := userAuth.AccountId, userAuth.UserId
     
     	vars := mux.Vars(r)
     	targetUserID := vars["userId"]
    diff --git a/management/server/http/handlers/users/users_handler_test.go b/management/server/http/handlers/users/users_handler_test.go
    index ff77cedff..a6a904a4c 100644
    --- a/management/server/http/handlers/users/users_handler_test.go
    +++ b/management/server/http/handlers/users/users_handler_test.go
    @@ -13,8 +13,8 @@ import (
     	"github.com/gorilla/mux"
     	"github.com/stretchr/testify/assert"
     
    +	nbcontext "github.com/netbirdio/netbird/management/server/context"
     	"github.com/netbirdio/netbird/management/server/http/api"
    -	"github.com/netbirdio/netbird/management/server/jwtclaims"
     	"github.com/netbirdio/netbird/management/server/mock_server"
     	"github.com/netbirdio/netbird/management/server/status"
     	"github.com/netbirdio/netbird/management/server/types"
    @@ -64,9 +64,6 @@ var usersTestAccount = &types.Account{
     func initUsersTestData() *handler {
     	return &handler{
     		accountManager: &mock_server.MockAccountManager{
    -			GetAccountIDFromTokenFunc: func(_ context.Context, claims jwtclaims.AuthorizationClaims) (string, string, error) {
    -				return usersTestAccount.Id, claims.UserId, nil
    -			},
     			GetUserByIDFunc: func(ctx context.Context, id string) (*types.User, error) {
     				return usersTestAccount.Users[id], nil
     			},
    @@ -127,15 +124,6 @@ func initUsersTestData() *handler {
     				return nil
     			},
     		},
    -		claimsExtractor: jwtclaims.NewClaimsExtractor(
    -			jwtclaims.WithFromRequestContext(func(r *http.Request) jwtclaims.AuthorizationClaims {
    -				return jwtclaims.AuthorizationClaims{
    -					UserId:    existingUserID,
    -					Domain:    testDomain,
    -					AccountId: existingAccountID,
    -				}
    -			}),
    -		),
     	}
     }
     
    @@ -158,6 +146,11 @@ func TestGetUsers(t *testing.T) {
     		t.Run(tc.name, func(t *testing.T) {
     			recorder := httptest.NewRecorder()
     			req := httptest.NewRequest(tc.requestType, tc.requestPath, nil)
    +			req = nbcontext.SetUserAuthInRequest(req, nbcontext.UserAuth{
    +				UserId:    existingUserID,
    +				Domain:    testDomain,
    +				AccountId: existingAccountID,
    +			})
     
     			userHandler.getAllUsers(recorder, req)
     
    @@ -263,6 +256,11 @@ func TestUpdateUser(t *testing.T) {
     		t.Run(tc.name, func(t *testing.T) {
     			recorder := httptest.NewRecorder()
     			req := httptest.NewRequest(tc.requestType, tc.requestPath, tc.requestBody)
    +			req = nbcontext.SetUserAuthInRequest(req, nbcontext.UserAuth{
    +				UserId:    existingUserID,
    +				Domain:    testDomain,
    +				AccountId: existingAccountID,
    +			})
     
     			router := mux.NewRouter()
     			router.HandleFunc("/api/users/{userId}", userHandler.updateUser).Methods("PUT")
    @@ -355,6 +353,11 @@ func TestCreateUser(t *testing.T) {
     		t.Run(tc.name, func(t *testing.T) {
     			req := httptest.NewRequest(tc.requestType, tc.requestPath, tc.requestBody)
     			rr := httptest.NewRecorder()
    +			req = nbcontext.SetUserAuthInRequest(req, nbcontext.UserAuth{
    +				UserId:    existingUserID,
    +				Domain:    testDomain,
    +				AccountId: existingAccountID,
    +			})
     
     			userHandler.createUser(rr, req)
     
    @@ -399,6 +402,12 @@ func TestInviteUser(t *testing.T) {
     		t.Run(tc.name, func(t *testing.T) {
     			req := httptest.NewRequest(tc.requestType, tc.requestPath, nil)
     			req = mux.SetURLVars(req, tc.requestVars)
    +			req = nbcontext.SetUserAuthInRequest(req, nbcontext.UserAuth{
    +				UserId:    existingUserID,
    +				Domain:    testDomain,
    +				AccountId: existingAccountID,
    +			})
    +
     			rr := httptest.NewRecorder()
     
     			userHandler.inviteUser(rr, req)
    @@ -452,6 +461,12 @@ func TestDeleteUser(t *testing.T) {
     		t.Run(tc.name, func(t *testing.T) {
     			req := httptest.NewRequest(tc.requestType, tc.requestPath, nil)
     			req = mux.SetURLVars(req, tc.requestVars)
    +			req = nbcontext.SetUserAuthInRequest(req, nbcontext.UserAuth{
    +				UserId:    existingUserID,
    +				Domain:    testDomain,
    +				AccountId: existingAccountID,
    +			})
    +
     			rr := httptest.NewRecorder()
     
     			userHandler.deleteUser(rr, req)
    diff --git a/management/server/http/middleware/access_control.go b/management/server/http/middleware/access_control.go
    index c5bdf5fe7..4ed90f47b 100644
    --- a/management/server/http/middleware/access_control.go
    +++ b/management/server/http/middleware/access_control.go
    @@ -7,30 +7,24 @@ import (
     
     	log "github.com/sirupsen/logrus"
     
    +	nbcontext "github.com/netbirdio/netbird/management/server/context"
     	"github.com/netbirdio/netbird/management/server/http/middleware/bypass"
     	"github.com/netbirdio/netbird/management/server/http/util"
     	"github.com/netbirdio/netbird/management/server/status"
     	"github.com/netbirdio/netbird/management/server/types"
    -
    -	"github.com/netbirdio/netbird/management/server/jwtclaims"
     )
     
     // GetUser function defines a function to fetch user from Account by jwtclaims.AuthorizationClaims
    -type GetUser func(ctx context.Context, claims jwtclaims.AuthorizationClaims) (*types.User, error)
    +type GetUser func(ctx context.Context, userAuth nbcontext.UserAuth) (*types.User, error)
     
     // AccessControl middleware to restrict to make POST/PUT/DELETE requests by admin only
     type AccessControl struct {
    -	claimsExtract jwtclaims.ClaimsExtractor
    -	getUser       GetUser
    +	getUser GetUser
     }
     
     // NewAccessControl instance constructor
    -func NewAccessControl(audience, userIDClaim string, getUser GetUser) *AccessControl {
    +func NewAccessControl(getUser GetUser) *AccessControl {
     	return &AccessControl{
    -		claimsExtract: *jwtclaims.NewClaimsExtractor(
    -			jwtclaims.WithAudience(audience),
    -			jwtclaims.WithUserIDClaim(userIDClaim),
    -		),
     		getUser: getUser,
     	}
     }
    @@ -45,12 +39,16 @@ func (a *AccessControl) Handler(h http.Handler) http.Handler {
     			return
     		}
     
    -		claims := a.claimsExtract.FromRequestContext(r)
    -
    -		user, err := a.getUser(r.Context(), claims)
    +		userAuth, err := nbcontext.GetUserAuthFromRequest(r)
     		if err != nil {
    -			log.WithContext(r.Context()).Errorf("failed to get user from claims: %s", err)
    -			util.WriteError(r.Context(), status.Errorf(status.Unauthorized, "invalid JWT"), w)
    +			log.WithContext(r.Context()).Errorf("failed to get user auth from request: %s", err)
    +			util.WriteError(r.Context(), status.Errorf(status.Unauthorized, "invalid user auth"), w)
    +		}
    +
    +		user, err := a.getUser(r.Context(), userAuth)
    +		if err != nil {
    +			log.WithContext(r.Context()).Errorf("failed to get user: %s", err)
    +			util.WriteError(r.Context(), status.Errorf(status.Unauthorized, "invalid user auth"), w)
     			return
     		}
     
    diff --git a/management/server/http/middleware/auth_middleware.go b/management/server/http/middleware/auth_middleware.go
    index dcf73259a..a8e6790a9 100644
    --- a/management/server/http/middleware/auth_middleware.go
    +++ b/management/server/http/middleware/auth_middleware.go
    @@ -8,67 +8,41 @@ import (
     	"strings"
     	"time"
     
    -	"github.com/golang-jwt/jwt"
     	log "github.com/sirupsen/logrus"
     
    -	nbContext "github.com/netbirdio/netbird/management/server/context"
    +	"github.com/netbirdio/netbird/management/server/auth"
    +	nbcontext "github.com/netbirdio/netbird/management/server/context"
     	"github.com/netbirdio/netbird/management/server/http/middleware/bypass"
     	"github.com/netbirdio/netbird/management/server/http/util"
    -	"github.com/netbirdio/netbird/management/server/jwtclaims"
     	"github.com/netbirdio/netbird/management/server/status"
    -	"github.com/netbirdio/netbird/management/server/types"
     )
     
    -// GetAccountInfoFromPATFunc function
    -type GetAccountInfoFromPATFunc func(ctx context.Context, token string) (user *types.User, pat *types.PersonalAccessToken, domain string, category string, err error)
    -
    -// ValidateAndParseTokenFunc function
    -type ValidateAndParseTokenFunc func(ctx context.Context, token string) (*jwt.Token, error)
    -
    -// MarkPATUsedFunc function
    -type MarkPATUsedFunc func(ctx context.Context, token string) error
    -
    -// CheckUserAccessByJWTGroupsFunc function
    -type CheckUserAccessByJWTGroupsFunc func(ctx context.Context, claims jwtclaims.AuthorizationClaims) error
    +type EnsureAccountFunc func(ctx context.Context, userAuth nbcontext.UserAuth) (string, string, error)
    +type SyncUserJWTGroupsFunc func(ctx context.Context, userAuth nbcontext.UserAuth) error
     
     // AuthMiddleware middleware to verify personal access tokens (PAT) and JWT tokens
     type AuthMiddleware struct {
    -	getAccountInfoFromPAT      GetAccountInfoFromPATFunc
    -	validateAndParseToken      ValidateAndParseTokenFunc
    -	markPATUsed                MarkPATUsedFunc
    -	checkUserAccessByJWTGroups CheckUserAccessByJWTGroupsFunc
    -	claimsExtractor            *jwtclaims.ClaimsExtractor
    -	audience                   string
    -	userIDClaim                string
    +	authManager       auth.Manager
    +	ensureAccount     EnsureAccountFunc
    +	syncUserJWTGroups SyncUserJWTGroupsFunc
     }
     
    -const (
    -	userProperty = "user"
    -)
    -
     // NewAuthMiddleware instance constructor
    -func NewAuthMiddleware(getAccountInfoFromPAT GetAccountInfoFromPATFunc, validateAndParseToken ValidateAndParseTokenFunc,
    -	markPATUsed MarkPATUsedFunc, checkUserAccessByJWTGroups CheckUserAccessByJWTGroupsFunc, claimsExtractor *jwtclaims.ClaimsExtractor,
    -	audience string, userIdClaim string) *AuthMiddleware {
    -	if userIdClaim == "" {
    -		userIdClaim = jwtclaims.UserIDClaim
    -	}
    -
    +func NewAuthMiddleware(
    +	authManager auth.Manager,
    +	ensureAccount EnsureAccountFunc,
    +	syncUserJWTGroups SyncUserJWTGroupsFunc,
    +) *AuthMiddleware {
     	return &AuthMiddleware{
    -		getAccountInfoFromPAT:      getAccountInfoFromPAT,
    -		validateAndParseToken:      validateAndParseToken,
    -		markPATUsed:                markPATUsed,
    -		checkUserAccessByJWTGroups: checkUserAccessByJWTGroups,
    -		claimsExtractor:            claimsExtractor,
    -		audience:                   audience,
    -		userIDClaim:                userIdClaim,
    +		authManager:       authManager,
    +		ensureAccount:     ensureAccount,
    +		syncUserJWTGroups: syncUserJWTGroups,
     	}
     }
     
     // Handler method of the middleware which authenticates a user either by JWT claims or by PAT
     func (m *AuthMiddleware) Handler(h http.Handler) http.Handler {
     	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    -
     		if bypass.ShouldBypass(r.URL.Path, h, w, r) {
     			return
     		}
    @@ -84,108 +58,111 @@ func (m *AuthMiddleware) Handler(h http.Handler) http.Handler {
     
     		switch authType {
     		case "bearer":
    -			err := m.checkJWTFromRequest(w, r, auth)
    +			request, err := m.checkJWTFromRequest(r, auth)
     			if err != nil {
    -				log.WithContext(r.Context()).Errorf("Error when validating JWT claims: %s", err.Error())
    +				log.WithContext(r.Context()).Errorf("Error when validating JWT: %s", err.Error())
     				util.WriteError(r.Context(), status.Errorf(status.Unauthorized, "token invalid"), w)
     				return
     			}
    +
    +			h.ServeHTTP(w, request)
     		case "token":
    -			err := m.checkPATFromRequest(w, r, auth)
    +			request, err := m.checkPATFromRequest(r, auth)
     			if err != nil {
    -				log.WithContext(r.Context()).Debugf("Error when validating PAT claims: %s", err.Error())
    +				log.WithContext(r.Context()).Debugf("Error when validating PAT: %s", err.Error())
     				util.WriteError(r.Context(), status.Errorf(status.Unauthorized, "token invalid"), w)
     				return
     			}
    +			h.ServeHTTP(w, request)
     		default:
     			util.WriteError(r.Context(), status.Errorf(status.Unauthorized, "no valid authentication provided"), w)
     			return
     		}
    -		claims := m.claimsExtractor.FromRequestContext(r)
    -		//nolint
    -		ctx := context.WithValue(r.Context(), nbContext.UserIDKey, claims.UserId)
    -		//nolint
    -		ctx = context.WithValue(ctx, nbContext.AccountIDKey, claims.AccountId)
    -		h.ServeHTTP(w, r.WithContext(ctx))
     	})
     }
     
     // CheckJWTFromRequest checks if the JWT is valid
    -func (m *AuthMiddleware) checkJWTFromRequest(w http.ResponseWriter, r *http.Request, auth []string) error {
    +func (m *AuthMiddleware) checkJWTFromRequest(r *http.Request, auth []string) (*http.Request, error) {
     	token, err := getTokenFromJWTRequest(auth)
     
     	// If an error occurs, call the error handler and return an error
     	if err != nil {
    -		return fmt.Errorf("Error extracting token: %w", err)
    +		return r, fmt.Errorf("error extracting token: %w", err)
     	}
     
    -	validatedToken, err := m.validateAndParseToken(r.Context(), token)
    +	ctx := r.Context()
    +
    +	userAuth, validatedToken, err := m.authManager.ValidateAndParseToken(ctx, token)
     	if err != nil {
    -		return err
    +		return r, err
     	}
     
    -	if validatedToken == nil {
    -		return nil
    +	if impersonate, ok := r.URL.Query()["account"]; ok && len(impersonate) == 1 {
    +		userAuth.AccountId = impersonate[0]
    +		userAuth.IsChild = ok
     	}
     
    -	if err := m.verifyUserAccess(r.Context(), validatedToken); err != nil {
    -		return err
    +	// we need to call this method because if user is new, we will automatically add it to existing or create a new account
    +	accountId, _, err := m.ensureAccount(ctx, userAuth)
    +	if err != nil {
    +		return r, err
     	}
     
    -	// If we get here, everything worked and we can set the
    -	// user property in context.
    -	newRequest := r.WithContext(context.WithValue(r.Context(), userProperty, validatedToken)) //nolint
    -	// Update the current request with the new context information.
    -	*r = *newRequest
    -	return nil
    -}
    +	if userAuth.AccountId != accountId {
    +		log.WithContext(ctx).Debugf("Auth middleware sets accountId from ensure, before %s, now %s", userAuth.AccountId, accountId)
    +		userAuth.AccountId = accountId
    +	}
     
    -// verifyUserAccess checks if a user, based on a validated JWT token,
    -// is allowed access, particularly in cases where the admin enabled JWT
    -// group propagation and designated certain groups with access permissions.
    -func (m *AuthMiddleware) verifyUserAccess(ctx context.Context, validatedToken *jwt.Token) error {
    -	authClaims := m.claimsExtractor.FromToken(validatedToken)
    -	return m.checkUserAccessByJWTGroups(ctx, authClaims)
    +	userAuth, err = m.authManager.EnsureUserAccessByJWTGroups(ctx, userAuth, validatedToken)
    +	if err != nil {
    +		return r, err
    +	}
    +
    +	err = m.syncUserJWTGroups(ctx, userAuth)
    +	if err != nil {
    +		log.WithContext(ctx).Errorf("HTTP server failed to sync user JWT groups: %s", err)
    +	}
    +
    +	return nbcontext.SetUserAuthInRequest(r, userAuth), nil
     }
     
     // CheckPATFromRequest checks if the PAT is valid
    -func (m *AuthMiddleware) checkPATFromRequest(w http.ResponseWriter, r *http.Request, auth []string) error {
    +func (m *AuthMiddleware) checkPATFromRequest(r *http.Request, auth []string) (*http.Request, error) {
     	token, err := getTokenFromPATRequest(auth)
     	if err != nil {
    -		return fmt.Errorf("error extracting token: %w", err)
    +		return r, fmt.Errorf("error extracting token: %w", err)
     	}
     
    -	user, pat, accDomain, accCategory, err := m.getAccountInfoFromPAT(r.Context(), token)
    +	ctx := r.Context()
    +	user, pat, accDomain, accCategory, err := m.authManager.GetPATInfo(ctx, token)
     	if err != nil {
    -		return fmt.Errorf("invalid Token: %w", err)
    +		return r, fmt.Errorf("invalid Token: %w", err)
     	}
     	if time.Now().After(pat.GetExpirationDate()) {
    -		return fmt.Errorf("token expired")
    +		return r, fmt.Errorf("token expired")
     	}
     
    -	err = m.markPATUsed(r.Context(), pat.ID)
    +	err = m.authManager.MarkPATUsed(ctx, pat.ID)
     	if err != nil {
    -		return err
    +		return r, err
     	}
     
    -	claimMaps := jwt.MapClaims{}
    -	claimMaps[m.userIDClaim] = user.Id
    -	claimMaps[m.audience+jwtclaims.AccountIDSuffix] = user.AccountID
    -	claimMaps[m.audience+jwtclaims.DomainIDSuffix] = accDomain
    -	claimMaps[m.audience+jwtclaims.DomainCategorySuffix] = accCategory
    -	claimMaps[jwtclaims.IsToken] = true
    -	jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, claimMaps)
    -	newRequest := r.WithContext(context.WithValue(r.Context(), jwtclaims.TokenUserProperty, jwtToken)) //nolint
    -	// Update the current request with the new context information.
    -	*r = *newRequest
    -	return nil
    +	userAuth := nbcontext.UserAuth{
    +		UserId:         user.Id,
    +		AccountId:      user.AccountID,
    +		Domain:         accDomain,
    +		DomainCategory: accCategory,
    +		IsPAT:          true,
    +	}
    +
    +	return nbcontext.SetUserAuthInRequest(r, userAuth), nil
     }
     
     // getTokenFromJWTRequest is a "TokenExtractor" that takes auth header parts and extracts
     // the JWT token from the Authorization header.
     func getTokenFromJWTRequest(authHeaderParts []string) (string, error) {
     	if len(authHeaderParts) != 2 || strings.ToLower(authHeaderParts[0]) != "bearer" {
    -		return "", errors.New("Authorization header format must be Bearer {token}")
    +		return "", errors.New("authorization header format must be Bearer {token}")
     	}
     
     	return authHeaderParts[1], nil
    @@ -195,7 +172,7 @@ func getTokenFromJWTRequest(authHeaderParts []string) (string, error) {
     // the PAT token from the Authorization header.
     func getTokenFromPATRequest(authHeaderParts []string) (string, error) {
     	if len(authHeaderParts) != 2 || strings.ToLower(authHeaderParts[0]) != "token" {
    -		return "", errors.New("Authorization header format must be Token {token}")
    +		return "", errors.New("authorization header format must be Token {token}")
     	}
     
     	return authHeaderParts[1], nil
    diff --git a/management/server/http/middleware/auth_middleware_test.go b/management/server/http/middleware/auth_middleware_test.go
    index c1686ed44..3dc7d51cb 100644
    --- a/management/server/http/middleware/auth_middleware_test.go
    +++ b/management/server/http/middleware/auth_middleware_test.go
    @@ -9,10 +9,14 @@ import (
     	"time"
     
     	"github.com/golang-jwt/jwt"
    +	"github.com/stretchr/testify/assert"
    +
    +	"github.com/netbirdio/netbird/management/server/auth"
    +	nbjwt "github.com/netbirdio/netbird/management/server/auth/jwt"
    +	nbcontext "github.com/netbirdio/netbird/management/server/context"
     	"github.com/netbirdio/netbird/management/server/util"
     
     	"github.com/netbirdio/netbird/management/server/http/middleware/bypass"
    -	"github.com/netbirdio/netbird/management/server/jwtclaims"
     	"github.com/netbirdio/netbird/management/server/types"
     )
     
    @@ -58,17 +62,23 @@ func mockGetAccountInfoFromPAT(_ context.Context, token string) (user *types.Use
     	return nil, nil, "", "", fmt.Errorf("PAT invalid")
     }
     
    -func mockValidateAndParseToken(_ context.Context, token string) (*jwt.Token, error) {
    +func mockValidateAndParseToken(_ context.Context, token string) (nbcontext.UserAuth, *jwt.Token, error) {
     	if token == JWT {
    -		return &jwt.Token{
    -			Claims: jwt.MapClaims{
    -				userIDClaim:                          userID,
    -				audience + jwtclaims.AccountIDSuffix: accountID,
    +		return nbcontext.UserAuth{
    +				UserId:         userID,
    +				AccountId:      accountID,
    +				Domain:         testAccount.Domain,
    +				DomainCategory: testAccount.DomainCategory,
     			},
    -			Valid: true,
    -		}, nil
    +			&jwt.Token{
    +				Claims: jwt.MapClaims{
    +					userIDClaim:                      userID,
    +					audience + nbjwt.AccountIDSuffix: accountID,
    +				},
    +				Valid: true,
    +			}, nil
     	}
    -	return nil, fmt.Errorf("JWT invalid")
    +	return nbcontext.UserAuth{}, nil, fmt.Errorf("JWT invalid")
     }
     
     func mockMarkPATUsed(_ context.Context, token string) error {
    @@ -78,16 +88,20 @@ func mockMarkPATUsed(_ context.Context, token string) error {
     	return fmt.Errorf("Should never get reached")
     }
     
    -func mockCheckUserAccessByJWTGroups(_ context.Context, claims jwtclaims.AuthorizationClaims) error {
    -	if testAccount.Id != claims.AccountId {
    -		return fmt.Errorf("account with id %s does not exist", claims.AccountId)
    +func mockEnsureUserAccessByJWTGroups(_ context.Context, userAuth nbcontext.UserAuth, token *jwt.Token) (nbcontext.UserAuth, error) {
    +	if userAuth.IsChild || userAuth.IsPAT {
    +		return userAuth, nil
     	}
     
    -	if _, ok := testAccount.Users[claims.UserId]; !ok {
    -		return fmt.Errorf("user with id %s does not exist", claims.UserId)
    +	if testAccount.Id != userAuth.AccountId {
    +		return userAuth, fmt.Errorf("account with id %s does not exist", userAuth.AccountId)
     	}
     
    -	return nil
    +	if _, ok := testAccount.Users[userAuth.UserId]; !ok {
    +		return userAuth, fmt.Errorf("user with id %s does not exist", userAuth.UserId)
    +	}
    +
    +	return userAuth, nil
     }
     
     func TestAuthMiddleware_Handler(t *testing.T) {
    @@ -158,22 +172,24 @@ func TestAuthMiddleware_Handler(t *testing.T) {
     	}
     
     	nextHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    -		// do nothing
    +
     	})
     
    -	claimsExtractor := jwtclaims.NewClaimsExtractor(
    -		jwtclaims.WithAudience(audience),
    -		jwtclaims.WithUserIDClaim(userIDClaim),
    -	)
    +	mockAuth := &auth.MockManager{
    +		ValidateAndParseTokenFunc:       mockValidateAndParseToken,
    +		EnsureUserAccessByJWTGroupsFunc: mockEnsureUserAccessByJWTGroups,
    +		MarkPATUsedFunc:                 mockMarkPATUsed,
    +		GetPATInfoFunc:                  mockGetAccountInfoFromPAT,
    +	}
     
     	authMiddleware := NewAuthMiddleware(
    -		mockGetAccountInfoFromPAT,
    -		mockValidateAndParseToken,
    -		mockMarkPATUsed,
    -		mockCheckUserAccessByJWTGroups,
    -		claimsExtractor,
    -		audience,
    -		userIDClaim,
    +		mockAuth,
    +		func(ctx context.Context, userAuth nbcontext.UserAuth) (string, string, error) {
    +			return userAuth.AccountId, userAuth.UserId, nil
    +		},
    +		func(ctx context.Context, userAuth nbcontext.UserAuth) error {
    +			return nil
    +		},
     	)
     
     	handlerToTest := authMiddleware.Handler(nextHandler)
    @@ -195,9 +211,115 @@ func TestAuthMiddleware_Handler(t *testing.T) {
     
     			result := rec.Result()
     			defer result.Body.Close()
    +
     			if result.StatusCode != tc.expectedStatusCode {
     				t.Errorf("expected status code %d, got %d", tc.expectedStatusCode, result.StatusCode)
     			}
     		})
     	}
     }
    +
    +func TestAuthMiddleware_Handler_Child(t *testing.T) {
    +	tt := []struct {
    +		name             string
    +		path             string
    +		authHeader       string
    +		expectedUserAuth *nbcontext.UserAuth // nil expects 401 response status
    +	}{
    +		{
    +			name:       "Valid PAT Token",
    +			path:       "/test",
    +			authHeader: "Token " + PAT,
    +			expectedUserAuth: &nbcontext.UserAuth{
    +				AccountId:      accountID,
    +				UserId:         userID,
    +				Domain:         testAccount.Domain,
    +				DomainCategory: testAccount.DomainCategory,
    +				IsPAT:          true,
    +			},
    +		},
    +		{
    +			name:       "Valid PAT Token ignores child",
    +			path:       "/test?account=xyz",
    +			authHeader: "Token " + PAT,
    +			expectedUserAuth: &nbcontext.UserAuth{
    +				AccountId:      accountID,
    +				UserId:         userID,
    +				Domain:         testAccount.Domain,
    +				DomainCategory: testAccount.DomainCategory,
    +				IsPAT:          true,
    +			},
    +		},
    +		{
    +			name:       "Valid JWT Token",
    +			path:       "/test",
    +			authHeader: "Bearer " + JWT,
    +			expectedUserAuth: &nbcontext.UserAuth{
    +				AccountId:      accountID,
    +				UserId:         userID,
    +				Domain:         testAccount.Domain,
    +				DomainCategory: testAccount.DomainCategory,
    +			},
    +		},
    +
    +		{
    +			name:       "Valid JWT Token with child",
    +			path:       "/test?account=xyz",
    +			authHeader: "Bearer " + JWT,
    +			expectedUserAuth: &nbcontext.UserAuth{
    +				AccountId:      "xyz",
    +				UserId:         userID,
    +				Domain:         testAccount.Domain,
    +				DomainCategory: testAccount.DomainCategory,
    +				IsChild:        true,
    +			},
    +		},
    +	}
    +
    +	mockAuth := &auth.MockManager{
    +		ValidateAndParseTokenFunc:       mockValidateAndParseToken,
    +		EnsureUserAccessByJWTGroupsFunc: mockEnsureUserAccessByJWTGroups,
    +		MarkPATUsedFunc:                 mockMarkPATUsed,
    +		GetPATInfoFunc:                  mockGetAccountInfoFromPAT,
    +	}
    +
    +	authMiddleware := NewAuthMiddleware(
    +		mockAuth,
    +		func(ctx context.Context, userAuth nbcontext.UserAuth) (string, string, error) {
    +			return userAuth.AccountId, userAuth.UserId, nil
    +		},
    +		func(ctx context.Context, userAuth nbcontext.UserAuth) error {
    +			return nil
    +		},
    +	)
    +
    +	for _, tc := range tt {
    +		t.Run(tc.name, func(t *testing.T) {
    +			handlerToTest := authMiddleware.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    +				userAuth, err := nbcontext.GetUserAuthFromRequest(r)
    +				if tc.expectedUserAuth != nil {
    +					assert.NoError(t, err)
    +					assert.Equal(t, *tc.expectedUserAuth, userAuth)
    +				} else {
    +					assert.Error(t, err)
    +					assert.Empty(t, userAuth)
    +				}
    +			}))
    +
    +			req := httptest.NewRequest("GET", "http://testing"+tc.path, nil)
    +			req.Header.Set("Authorization", tc.authHeader)
    +			rec := httptest.NewRecorder()
    +
    +			handlerToTest.ServeHTTP(rec, req)
    +
    +			result := rec.Result()
    +			defer result.Body.Close()
    +
    +			if tc.expectedUserAuth != nil {
    +				assert.Equal(t, 200, result.StatusCode)
    +			} else {
    +				assert.Equal(t, 401, result.StatusCode)
    +			}
    +		})
    +	}
    +}
    diff --git a/management/server/http/testing/benchmarks/peers_handler_benchmark_test.go b/management/server/http/testing/benchmarks/peers_handler_benchmark_test.go
    index 7f8eee6e7..e2c2c1d85 100644
    --- a/management/server/http/testing/benchmarks/peers_handler_benchmark_test.go
    +++ b/management/server/http/testing/benchmarks/peers_handler_benchmark_test.go
    @@ -77,13 +77,13 @@ func BenchmarkUpdatePeer(b *testing.B) {
     
     func BenchmarkGetOnePeer(b *testing.B) {
     	var expectedMetrics = map[string]testing_tools.PerformanceMetrics{
    -		"Peers - XS":     {MinMsPerOpLocal: 15, MaxMsPerOpLocal: 40, MinMsPerOpCICD: 30, MaxMsPerOpCICD: 70},
    -		"Peers - S":      {MinMsPerOpLocal: 1, MaxMsPerOpLocal: 5, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 30},
    -		"Peers - M":      {MinMsPerOpLocal: 9, MaxMsPerOpLocal: 18, MinMsPerOpCICD: 15, MaxMsPerOpCICD: 50},
    -		"Peers - L":      {MinMsPerOpLocal: 40, MaxMsPerOpLocal: 90, MinMsPerOpCICD: 50, MaxMsPerOpCICD: 130},
    -		"Groups - L":     {MinMsPerOpLocal: 80, MaxMsPerOpLocal: 130, MinMsPerOpCICD: 30, MaxMsPerOpCICD: 200},
    -		"Users - L":      {MinMsPerOpLocal: 40, MaxMsPerOpLocal: 90, MinMsPerOpCICD: 50, MaxMsPerOpCICD: 130},
    -		"Setup Keys - L": {MinMsPerOpLocal: 40, MaxMsPerOpLocal: 90, MinMsPerOpCICD: 50, MaxMsPerOpCICD: 130},
    +		"Peers - XS":     {MinMsPerOpLocal: 15, MaxMsPerOpLocal: 40, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 70},
    +		"Peers - S":      {MinMsPerOpLocal: 1, MaxMsPerOpLocal: 5, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 70},
    +		"Peers - M":      {MinMsPerOpLocal: 9, MaxMsPerOpLocal: 18, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 70},
    +		"Peers - L":      {MinMsPerOpLocal: 40, MaxMsPerOpLocal: 90, MinMsPerOpCICD: 5, MaxMsPerOpCICD: 200},
    +		"Groups - L":     {MinMsPerOpLocal: 80, MaxMsPerOpLocal: 130, MinMsPerOpCICD: 5, MaxMsPerOpCICD: 200},
    +		"Users - L":      {MinMsPerOpLocal: 40, MaxMsPerOpLocal: 90, MinMsPerOpCICD: 5, MaxMsPerOpCICD: 200},
    +		"Setup Keys - L": {MinMsPerOpLocal: 40, MaxMsPerOpLocal: 90, MinMsPerOpCICD: 5, MaxMsPerOpCICD: 200},
     		"Peers - XL":     {MinMsPerOpLocal: 200, MaxMsPerOpLocal: 400, MinMsPerOpCICD: 200, MaxMsPerOpCICD: 750},
     	}
     
    @@ -111,9 +111,9 @@ func BenchmarkGetOnePeer(b *testing.B) {
     
     func BenchmarkGetAllPeers(b *testing.B) {
     	var expectedMetrics = map[string]testing_tools.PerformanceMetrics{
    -		"Peers - XS":     {MinMsPerOpLocal: 40, MaxMsPerOpLocal: 70, MinMsPerOpCICD: 50, MaxMsPerOpCICD: 150},
    -		"Peers - S":      {MinMsPerOpLocal: 2, MaxMsPerOpLocal: 10, MinMsPerOpCICD: 5, MaxMsPerOpCICD: 30},
    -		"Peers - M":      {MinMsPerOpLocal: 20, MaxMsPerOpLocal: 50, MinMsPerOpCICD: 20, MaxMsPerOpCICD: 70},
    +		"Peers - XS":     {MinMsPerOpLocal: 40, MaxMsPerOpLocal: 70, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 100},
    +		"Peers - S":      {MinMsPerOpLocal: 2, MaxMsPerOpLocal: 10, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 100},
    +		"Peers - M":      {MinMsPerOpLocal: 20, MaxMsPerOpLocal: 50, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 100},
     		"Peers - L":      {MinMsPerOpLocal: 110, MaxMsPerOpLocal: 150, MinMsPerOpCICD: 100, MaxMsPerOpCICD: 300},
     		"Groups - L":     {MinMsPerOpLocal: 150, MaxMsPerOpLocal: 200, MinMsPerOpCICD: 130, MaxMsPerOpCICD: 500},
     		"Users - L":      {MinMsPerOpLocal: 100, MaxMsPerOpLocal: 170, MinMsPerOpCICD: 100, MaxMsPerOpCICD: 400},
    diff --git a/management/server/http/testing/benchmarks/users_handler_benchmark_test.go b/management/server/http/testing/benchmarks/users_handler_benchmark_test.go
    index 0baf76328..b7deab334 100644
    --- a/management/server/http/testing/benchmarks/users_handler_benchmark_test.go
    +++ b/management/server/http/testing/benchmarks/users_handler_benchmark_test.go
    @@ -48,13 +48,12 @@ func BenchmarkUpdateUser(b *testing.B) {
     	log.SetOutput(io.Discard)
     	defer log.SetOutput(os.Stderr)
     
    -	recorder := httptest.NewRecorder()
    -
     	for name, bc := range benchCasesUsers {
     		b.Run(name, func(b *testing.B) {
     			apiHandler, am, _ := testing_tools.BuildApiBlackBoxWithDBState(b, "../testdata/users.sql", nil, false)
     			testing_tools.PopulateTestData(b, am.(*server.DefaultAccountManager), bc.Peers, bc.Groups, bc.Users, bc.SetupKeys)
     
    +			recorder := httptest.NewRecorder()
     			b.ResetTimer()
     			start := time.Now()
     			for i := 0; i < b.N; i++ {
    @@ -97,13 +96,12 @@ func BenchmarkGetOneUser(b *testing.B) {
     	log.SetOutput(io.Discard)
     	defer log.SetOutput(os.Stderr)
     
    -	recorder := httptest.NewRecorder()
    -
     	for name, bc := range benchCasesUsers {
     		b.Run(name, func(b *testing.B) {
     			apiHandler, am, _ := testing_tools.BuildApiBlackBoxWithDBState(b, "../testdata/users.sql", nil, false)
     			testing_tools.PopulateTestData(b, am.(*server.DefaultAccountManager), bc.Peers, bc.Groups, bc.Users, bc.SetupKeys)
     
    +			recorder := httptest.NewRecorder()
     			b.ResetTimer()
     			start := time.Now()
     			for i := 0; i < b.N; i++ {
    @@ -118,26 +116,25 @@ func BenchmarkGetOneUser(b *testing.B) {
     
     func BenchmarkGetAllUsers(b *testing.B) {
     	var expectedMetrics = map[string]testing_tools.PerformanceMetrics{
    -		"Users - XS":     {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 0, MaxMsPerOpCICD: 10},
    -		"Users - S":      {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 0, MaxMsPerOpCICD: 10},
    -		"Users - M":      {MinMsPerOpLocal: 3, MaxMsPerOpLocal: 10, MinMsPerOpCICD: 5, MaxMsPerOpCICD: 15},
    -		"Users - L":      {MinMsPerOpLocal: 10, MaxMsPerOpLocal: 20, MinMsPerOpCICD: 20, MaxMsPerOpCICD: 50},
    -		"Peers - L":      {MinMsPerOpLocal: 15, MaxMsPerOpLocal: 25, MinMsPerOpCICD: 20, MaxMsPerOpCICD: 55},
    -		"Groups - L":     {MinMsPerOpLocal: 15, MaxMsPerOpLocal: 25, MinMsPerOpCICD: 25, MaxMsPerOpCICD: 55},
    -		"Setup Keys - L": {MinMsPerOpLocal: 15, MaxMsPerOpLocal: 25, MinMsPerOpCICD: 25, MaxMsPerOpCICD: 55},
    -		"Users - XL":     {MinMsPerOpLocal: 80, MaxMsPerOpLocal: 120, MinMsPerOpCICD: 100, MaxMsPerOpCICD: 300},
    +		"Users - XS":     {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 0, MaxMsPerOpCICD: 75},
    +		"Users - S":      {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 0, MaxMsPerOpCICD: 75},
    +		"Users - M":      {MinMsPerOpLocal: 3, MaxMsPerOpLocal: 10, MinMsPerOpCICD: 0, MaxMsPerOpCICD: 75},
    +		"Users - L":      {MinMsPerOpLocal: 10, MaxMsPerOpLocal: 20, MinMsPerOpCICD: 10, MaxMsPerOpCICD: 100},
    +		"Peers - L":      {MinMsPerOpLocal: 15, MaxMsPerOpLocal: 25, MinMsPerOpCICD: 10, MaxMsPerOpCICD: 100},
    +		"Groups - L":     {MinMsPerOpLocal: 15, MaxMsPerOpLocal: 25, MinMsPerOpCICD: 10, MaxMsPerOpCICD: 100},
    +		"Setup Keys - L": {MinMsPerOpLocal: 15, MaxMsPerOpLocal: 25, MinMsPerOpCICD: 10, MaxMsPerOpCICD: 100},
    +		"Users - XL":     {MinMsPerOpLocal: 80, MaxMsPerOpLocal: 120, MinMsPerOpCICD: 50, MaxMsPerOpCICD: 300},
     	}
     
     	log.SetOutput(io.Discard)
     	defer log.SetOutput(os.Stderr)
     
    -	recorder := httptest.NewRecorder()
    -
     	for name, bc := range benchCasesUsers {
     		b.Run(name, func(b *testing.B) {
     			apiHandler, am, _ := testing_tools.BuildApiBlackBoxWithDBState(b, "../testdata/users.sql", nil, false)
     			testing_tools.PopulateTestData(b, am.(*server.DefaultAccountManager), bc.Peers, bc.Groups, bc.Users, bc.SetupKeys)
     
    +			recorder := httptest.NewRecorder()
     			b.ResetTimer()
     			start := time.Now()
     			for i := 0; i < b.N; i++ {
    @@ -152,26 +149,25 @@ func BenchmarkGetAllUsers(b *testing.B) {
     
     func BenchmarkDeleteUsers(b *testing.B) {
     	var expectedMetrics = map[string]testing_tools.PerformanceMetrics{
    -		"Users - XS":     {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 5, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 15},
    -		"Users - S":      {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 5, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 15},
    -		"Users - M":      {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 5, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 15},
    -		"Users - L":      {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 5, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 15},
    -		"Peers - L":      {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 5, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 15},
    -		"Groups - L":     {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 5, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 15},
    -		"Setup Keys - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 5, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 15},
    -		"Users - XL":     {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 5, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 15},
    +		"Users - XS":     {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 5, MinMsPerOpCICD: 1, MaxMsPerOpCICD: 50},
    +		"Users - S":      {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 5, MinMsPerOpCICD: 1, MaxMsPerOpCICD: 50},
    +		"Users - M":      {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 5, MinMsPerOpCICD: 1, MaxMsPerOpCICD: 50},
    +		"Users - L":      {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 5, MinMsPerOpCICD: 1, MaxMsPerOpCICD: 50},
    +		"Peers - L":      {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 5, MinMsPerOpCICD: 1, MaxMsPerOpCICD: 50},
    +		"Groups - L":     {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 5, MinMsPerOpCICD: 1, MaxMsPerOpCICD: 50},
    +		"Setup Keys - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 5, MinMsPerOpCICD: 1, MaxMsPerOpCICD: 50},
    +		"Users - XL":     {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 5, MinMsPerOpCICD: 1, MaxMsPerOpCICD: 50},
     	}
     
     	log.SetOutput(io.Discard)
     	defer log.SetOutput(os.Stderr)
     
    -	recorder := httptest.NewRecorder()
    -
     	for name, bc := range benchCasesUsers {
     		b.Run(name, func(b *testing.B) {
     			apiHandler, am, _ := testing_tools.BuildApiBlackBoxWithDBState(b, "../testdata/users.sql", nil, false)
     			testing_tools.PopulateTestData(b, am.(*server.DefaultAccountManager), bc.Peers, bc.Groups, 1000, bc.SetupKeys)
     
    +			recorder := httptest.NewRecorder()
     			b.ResetTimer()
     			start := time.Now()
     			for i := 0; i < b.N; i++ {
    diff --git a/management/server/http/testing/testing_tools/tools.go b/management/server/http/testing/testing_tools/tools.go
    index 006d5679c..e534dac46 100644
    --- a/management/server/http/testing/testing_tools/tools.go
    +++ b/management/server/http/testing/testing_tools/tools.go
    @@ -3,6 +3,7 @@ package testing_tools
     import (
     	"bytes"
     	"context"
    +	"errors"
     	"fmt"
     	"io"
     	"net"
    @@ -13,17 +14,17 @@ import (
     	"testing"
     	"time"
     
    -	"github.com/netbirdio/netbird/management/server/util"
    +	"github.com/golang-jwt/jwt"
     	"github.com/stretchr/testify/assert"
     	"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
     
     	"github.com/netbirdio/netbird/management/server"
     	"github.com/netbirdio/netbird/management/server/activity"
    +	"github.com/netbirdio/netbird/management/server/auth"
    +	nbcontext "github.com/netbirdio/netbird/management/server/context"
     	"github.com/netbirdio/netbird/management/server/geolocation"
     	"github.com/netbirdio/netbird/management/server/groups"
     	nbhttp "github.com/netbirdio/netbird/management/server/http"
    -	"github.com/netbirdio/netbird/management/server/http/configs"
    -	"github.com/netbirdio/netbird/management/server/jwtclaims"
     	"github.com/netbirdio/netbird/management/server/networks"
     	"github.com/netbirdio/netbird/management/server/networks/resources"
     	"github.com/netbirdio/netbird/management/server/networks/routers"
    @@ -32,6 +33,7 @@ import (
     	"github.com/netbirdio/netbird/management/server/store"
     	"github.com/netbirdio/netbird/management/server/telemetry"
     	"github.com/netbirdio/netbird/management/server/types"
    +	"github.com/netbirdio/netbird/management/server/util"
     )
     
     const (
    @@ -115,11 +117,20 @@ func BuildApiBlackBoxWithDBState(t TB, sqlFile string, expectedPeerUpdate *serve
     		t.Fatalf("Failed to create manager: %v", err)
     	}
     
    +	// @note this is required so that PAT's validate from store, but JWT's are mocked
    +	authManager := auth.NewManager(store, "", "", "", "", []string{}, false)
    +	authManagerMock := &auth.MockManager{
    +		ValidateAndParseTokenFunc:       mockValidateAndParseToken,
    +		EnsureUserAccessByJWTGroupsFunc: authManager.EnsureUserAccessByJWTGroups,
    +		MarkPATUsedFunc:                 authManager.MarkPATUsed,
    +		GetPATInfoFunc:                  authManager.GetPATInfo,
    +	}
    +
     	networksManagerMock := networks.NewManagerMock()
     	resourcesManagerMock := resources.NewManagerMock()
     	routersManagerMock := routers.NewManagerMock()
     	groupsManagerMock := groups.NewManagerMock()
    -	apiHandler, err := nbhttp.NewAPIHandler(context.Background(), am, networksManagerMock, resourcesManagerMock, routersManagerMock, groupsManagerMock, geoMock, &jwtclaims.JwtValidatorMock{}, metrics, configs.AuthCfg{}, validatorMock)
    +	apiHandler, err := nbhttp.NewAPIHandler(context.Background(), am, networksManagerMock, resourcesManagerMock, routersManagerMock, groupsManagerMock, geoMock, authManagerMock, metrics, &server.Config{}, validatorMock)
     	if err != nil {
     		t.Fatalf("Failed to create API handler: %v", err)
     	}
    @@ -309,3 +320,25 @@ func EvaluateBenchmarkResults(b *testing.B, name string, duration time.Duration,
     		b.Fatalf("Benchmark %s failed: too slow (%.2f ms/op, maximum %.2f ms/op)", name, msPerOp, maxExpected)
     	}
     }
    +
    +func mockValidateAndParseToken(_ context.Context, token string) (nbcontext.UserAuth, *jwt.Token, error) {
    +	userAuth := nbcontext.UserAuth{}
    +
    +	switch token {
    +	case "testUserId", "testAdminId", "testOwnerId", "testServiceUserId", "testServiceAdminId", "blockedUserId":
    +		userAuth.UserId = token
    +		userAuth.AccountId = "testAccountId"
    +		userAuth.Domain = "test.com"
    +		userAuth.DomainCategory = "private"
    +	case "otherUserId":
    +		userAuth.UserId = "otherUserId"
    +		userAuth.AccountId = "otherAccountId"
    +		userAuth.Domain = "other.com"
    +		userAuth.DomainCategory = "private"
    +	case "invalidToken":
    +		return userAuth, nil, errors.New("invalid token")
    +	}
    +
    +	jwtToken := jwt.New(jwt.SigningMethodHS256)
    +	return userAuth, jwtToken, nil
    +}
    diff --git a/management/server/jwtclaims/claims.go b/management/server/jwtclaims/claims.go
    deleted file mode 100644
    index 2527acbe3..000000000
    --- a/management/server/jwtclaims/claims.go
    +++ /dev/null
    @@ -1,19 +0,0 @@
    -package jwtclaims
    -
    -import (
    -	"time"
    -
    -	"github.com/golang-jwt/jwt"
    -)
    -
    -// AuthorizationClaims stores authorization information from JWTs
    -type AuthorizationClaims struct {
    -	UserId         string
    -	AccountId      string
    -	Domain         string
    -	DomainCategory string
    -	LastLogin      time.Time
    -	Invited        bool
    -
    -	Raw jwt.MapClaims
    -}
    diff --git a/management/server/jwtclaims/extractor_test.go b/management/server/jwtclaims/extractor_test.go
    deleted file mode 100644
    index eccd7c9e7..000000000
    --- a/management/server/jwtclaims/extractor_test.go
    +++ /dev/null
    @@ -1,227 +0,0 @@
    -package jwtclaims
    -
    -import (
    -	"context"
    -	"net/http"
    -	"testing"
    -	"time"
    -
    -	"github.com/golang-jwt/jwt"
    -	"github.com/stretchr/testify/require"
    -)
    -
    -func newTestRequestWithJWT(t *testing.T, claims AuthorizationClaims, audience string) *http.Request {
    -	t.Helper()
    -	const layout = "2006-01-02T15:04:05.999Z"
    -
    -	claimMaps := jwt.MapClaims{}
    -	if claims.UserId != "" {
    -		claimMaps[UserIDClaim] = claims.UserId
    -	}
    -	if claims.AccountId != "" {
    -		claimMaps[audience+AccountIDSuffix] = claims.AccountId
    -	}
    -	if claims.Domain != "" {
    -		claimMaps[audience+DomainIDSuffix] = claims.Domain
    -	}
    -	if claims.DomainCategory != "" {
    -		claimMaps[audience+DomainCategorySuffix] = claims.DomainCategory
    -	}
    -	if claims.LastLogin != (time.Time{}) {
    -		claimMaps[audience+LastLoginSuffix] = claims.LastLogin.Format(layout)
    -	}
    -
    -	if claims.Invited {
    -		claimMaps[audience+Invited] = true
    -	}
    -	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claimMaps)
    -	r, err := http.NewRequest(http.MethodGet, "http://localhost", nil)
    -	require.NoError(t, err, "creating testing request failed")
    -	testRequest := r.WithContext(context.WithValue(r.Context(), TokenUserProperty, token)) // nolint
    -
    -	return testRequest
    -}
    -
    -func TestExtractClaimsFromRequestContext(t *testing.T) {
    -	type test struct {
    -		name                     string
    -		inputAuthorizationClaims AuthorizationClaims
    -		inputAudiance            string
    -		testingFunc              require.ComparisonAssertionFunc
    -		expectedMSG              string
    -	}
    -
    -	const layout = "2006-01-02T15:04:05.999Z"
    -	lastLogin, _ := time.Parse(layout, "2023-08-17T09:30:40.465Z")
    -
    -	testCase1 := test{
    -		name:          "All Claim Fields",
    -		inputAudiance: "https://login/",
    -		inputAuthorizationClaims: AuthorizationClaims{
    -			UserId:         "test",
    -			Domain:         "test.com",
    -			AccountId:      "testAcc",
    -			LastLogin:      lastLogin,
    -			DomainCategory: "public",
    -			Invited:        true,
    -			Raw: jwt.MapClaims{
    -				"https://login/wt_account_domain":          "test.com",
    -				"https://login/wt_account_domain_category": "public",
    -				"https://login/wt_account_id":              "testAcc",
    -				"https://login/nb_last_login":              lastLogin.Format(layout),
    -				"sub":                                      "test",
    -				"https://login/" + Invited:                 true,
    -			},
    -		},
    -		testingFunc: require.EqualValues,
    -		expectedMSG: "extracted claims should match input claims",
    -	}
    -
    -	testCase2 := test{
    -		name:          "Domain Is Empty",
    -		inputAudiance: "https://login/",
    -		inputAuthorizationClaims: AuthorizationClaims{
    -			UserId:    "test",
    -			AccountId: "testAcc",
    -			Raw: jwt.MapClaims{
    -				"https://login/wt_account_id": "testAcc",
    -				"sub":                         "test",
    -			},
    -		},
    -		testingFunc: require.EqualValues,
    -		expectedMSG: "extracted claims should match input claims",
    -	}
    -
    -	testCase3 := test{
    -		name:          "Account ID Is Empty",
    -		inputAudiance: "https://login/",
    -		inputAuthorizationClaims: AuthorizationClaims{
    -			UserId: "test",
    -			Domain: "test.com",
    -			Raw: jwt.MapClaims{
    -				"https://login/wt_account_domain": "test.com",
    -				"sub":                             "test",
    -			},
    -		},
    -		testingFunc: require.EqualValues,
    -		expectedMSG: "extracted claims should match input claims",
    -	}
    -
    -	testCase4 := test{
    -		name:          "Category Is Empty",
    -		inputAudiance: "https://login/",
    -		inputAuthorizationClaims: AuthorizationClaims{
    -			UserId:    "test",
    -			Domain:    "test.com",
    -			AccountId: "testAcc",
    -			Raw: jwt.MapClaims{
    -				"https://login/wt_account_domain": "test.com",
    -				"https://login/wt_account_id":     "testAcc",
    -				"sub":                             "test",
    -			},
    -		},
    -		testingFunc: require.EqualValues,
    -		expectedMSG: "extracted claims should match input claims",
    -	}
    -
    -	testCase5 := test{
    -		name:          "Only User ID Is set",
    -		inputAudiance: "https://login/",
    -		inputAuthorizationClaims: AuthorizationClaims{
    -			UserId: "test",
    -			Raw: jwt.MapClaims{
    -				"sub": "test",
    -			},
    -		},
    -		testingFunc: require.EqualValues,
    -		expectedMSG: "extracted claims should match input claims",
    -	}
    -
    -	for _, testCase := range []test{testCase1, testCase2, testCase3, testCase4, testCase5} {
    -		t.Run(testCase.name, func(t *testing.T) {
    -			request := newTestRequestWithJWT(t, testCase.inputAuthorizationClaims, testCase.inputAudiance)
    -
    -			extractor := NewClaimsExtractor(WithAudience(testCase.inputAudiance))
    -			extractedClaims := extractor.FromRequestContext(request)
    -
    -			testCase.testingFunc(t, testCase.inputAuthorizationClaims, extractedClaims, testCase.expectedMSG)
    -		})
    -	}
    -}
    -
    -func TestExtractClaimsSetOptions(t *testing.T) {
    -	t.Helper()
    -	type test struct {
    -		name      string
    -		extractor *ClaimsExtractor
    -		check     func(t *testing.T, c test)
    -	}
    -
    -	testCase1 := test{
    -		name:      "No custom options",
    -		extractor: NewClaimsExtractor(),
    -		check: func(t *testing.T, c test) {
    -			t.Helper()
    -			if c.extractor.authAudience != "" {
    -				t.Error("audience should be empty")
    -				return
    -			}
    -			if c.extractor.userIDClaim != UserIDClaim {
    -				t.Errorf("user id claim should be default, expected %s, got %s", UserIDClaim, c.extractor.userIDClaim)
    -				return
    -			}
    -			if c.extractor.FromRequestContext == nil {
    -				t.Error("from request context should not be nil")
    -				return
    -			}
    -		},
    -	}
    -
    -	testCase2 := test{
    -		name:      "Custom audience",
    -		extractor: NewClaimsExtractor(WithAudience("https://login/")),
    -		check: func(t *testing.T, c test) {
    -			t.Helper()
    -			if c.extractor.authAudience != "https://login/" {
    -				t.Errorf("audience expected %s, got %s", "https://login/", c.extractor.authAudience)
    -				return
    -			}
    -		},
    -	}
    -
    -	testCase3 := test{
    -		name:      "Custom user id claim",
    -		extractor: NewClaimsExtractor(WithUserIDClaim("customUserId")),
    -		check: func(t *testing.T, c test) {
    -			t.Helper()
    -			if c.extractor.userIDClaim != "customUserId" {
    -				t.Errorf("user id claim expected %s, got %s", "customUserId", c.extractor.userIDClaim)
    -				return
    -			}
    -		},
    -	}
    -
    -	testCase4 := test{
    -		name: "Custom extractor from request context",
    -		extractor: NewClaimsExtractor(
    -			WithFromRequestContext(func(r *http.Request) AuthorizationClaims {
    -				return AuthorizationClaims{
    -					UserId: "testCustomRequest",
    -				}
    -			})),
    -		check: func(t *testing.T, c test) {
    -			t.Helper()
    -			claims := c.extractor.FromRequestContext(&http.Request{})
    -			if claims.UserId != "testCustomRequest" {
    -				t.Errorf("user id claim expected %s, got %s", "testCustomRequest", claims.UserId)
    -				return
    -			}
    -		},
    -	}
    -
    -	for _, testCase := range []test{testCase1, testCase2, testCase3, testCase4} {
    -		t.Run(testCase.name, func(t *testing.T) {
    -			testCase.check(t, testCase)
    -		})
    -	}
    -}
    diff --git a/management/server/jwtclaims/jwtValidator.go b/management/server/jwtclaims/jwtValidator.go
    deleted file mode 100644
    index 79e59e76f..000000000
    --- a/management/server/jwtclaims/jwtValidator.go
    +++ /dev/null
    @@ -1,349 +0,0 @@
    -package jwtclaims
    -
    -import (
    -	"context"
    -	"crypto/ecdsa"
    -	"crypto/elliptic"
    -	"crypto/rsa"
    -	"encoding/base64"
    -	"encoding/json"
    -	"errors"
    -	"fmt"
    -	"math/big"
    -	"net/http"
    -	"strconv"
    -	"strings"
    -	"sync"
    -	"time"
    -
    -	"github.com/golang-jwt/jwt"
    -	log "github.com/sirupsen/logrus"
    -)
    -
    -// Options is a struct for specifying configuration options for the middleware.
    -type Options struct {
    -	// The function that will return the Key to validate the JWT.
    -	// It can be either a shared secret or a public key.
    -	// Default value: nil
    -	ValidationKeyGetter jwt.Keyfunc
    -	// The name of the property in the request where the user information
    -	// from the JWT will be stored.
    -	// Default value: "user"
    -	UserProperty string
    -	// The function that will be called when there's an error validating the token
    -	// Default value:
    -	CredentialsOptional bool
    -	// A function that extracts the token from the request
    -	// Default: FromAuthHeader (i.e., from Authorization header as bearer token)
    -	Debug bool
    -	// When set, all requests with the OPTIONS method will use authentication
    -	// Default: false
    -	EnableAuthOnOptions bool
    -}
    -
    -// Jwks is a collection of JSONWebKey obtained from Config.HttpServerConfig.AuthKeysLocation
    -type Jwks struct {
    -	Keys          []JSONWebKey `json:"keys"`
    -	expiresInTime time.Time
    -}
    -
    -// The supported elliptic curves types
    -const (
    -	// p256 represents a cryptographic elliptical curve type.
    -	p256 = "P-256"
    -
    -	// p384 represents a cryptographic elliptical curve type.
    -	p384 = "P-384"
    -
    -	// p521 represents a cryptographic elliptical curve type.
    -	p521 = "P-521"
    -)
    -
    -// JSONWebKey is a representation of a Jason Web Key
    -type JSONWebKey struct {
    -	Kty string   `json:"kty"`
    -	Kid string   `json:"kid"`
    -	Use string   `json:"use"`
    -	N   string   `json:"n"`
    -	E   string   `json:"e"`
    -	Crv string   `json:"crv"`
    -	X   string   `json:"x"`
    -	Y   string   `json:"y"`
    -	X5c []string `json:"x5c"`
    -}
    -
    -type JWTValidator interface {
    -	ValidateAndParse(ctx context.Context, token string) (*jwt.Token, error)
    -}
    -
    -// jwtValidatorImpl struct to handle token validation and parsing
    -type jwtValidatorImpl struct {
    -	options Options
    -}
    -
    -var keyNotFound = errors.New("unable to find appropriate key")
    -
    -// NewJWTValidator constructor
    -func NewJWTValidator(ctx context.Context, issuer string, audienceList []string, keysLocation string, idpSignkeyRefreshEnabled bool) (JWTValidator, error) {
    -	keys, err := getPemKeys(ctx, keysLocation)
    -	if err != nil {
    -		return nil, err
    -	}
    -
    -	var lock sync.Mutex
    -	options := Options{
    -		ValidationKeyGetter: func(token *jwt.Token) (interface{}, error) {
    -			// Verify 'aud' claim
    -			var checkAud bool
    -			for _, audience := range audienceList {
    -				checkAud = token.Claims.(jwt.MapClaims).VerifyAudience(audience, false)
    -				if checkAud {
    -					break
    -				}
    -			}
    -			if !checkAud {
    -				return token, errors.New("invalid audience")
    -			}
    -			// Verify 'issuer' claim
    -			checkIss := token.Claims.(jwt.MapClaims).VerifyIssuer(issuer, false)
    -			if !checkIss {
    -				return token, errors.New("invalid issuer")
    -			}
    -
    -			// If keys are rotated, verify the keys prior to token validation
    -			if idpSignkeyRefreshEnabled {
    -				// If the keys are invalid, retrieve new ones
    -				if !keys.stillValid() {
    -					lock.Lock()
    -					defer lock.Unlock()
    -
    -					refreshedKeys, err := getPemKeys(ctx, keysLocation)
    -					if err != nil {
    -						log.WithContext(ctx).Debugf("cannot get JSONWebKey: %v, falling back to old keys", err)
    -						refreshedKeys = keys
    -					}
    -
    -					log.WithContext(ctx).Debugf("keys refreshed, new UTC expiration time: %s", refreshedKeys.expiresInTime.UTC())
    -
    -					keys = refreshedKeys
    -				}
    -			}
    -
    -			publicKey, err := getPublicKey(ctx, token, keys)
    -			if err == nil {
    -				return publicKey, nil
    -			}
    -
    -			msg := fmt.Sprintf("getPublicKey error: %s", err)
    -			if errors.Is(err, keyNotFound) && !idpSignkeyRefreshEnabled {
    -				msg = fmt.Sprintf("getPublicKey error: %s. You can enable key refresh by setting HttpServerConfig.IdpSignKeyRefreshEnabled to true in your management.json file and restart the service", err)
    -			}
    -
    -			log.WithContext(ctx).Error(msg)
    -
    -			return nil, err
    -		},
    -		EnableAuthOnOptions: false,
    -	}
    -
    -	if options.UserProperty == "" {
    -		options.UserProperty = "user"
    -	}
    -
    -	return &jwtValidatorImpl{
    -		options: options,
    -	}, nil
    -}
    -
    -// ValidateAndParse validates the token and returns the parsed token
    -func (m *jwtValidatorImpl) ValidateAndParse(ctx context.Context, token string) (*jwt.Token, error) {
    -	// If the token is empty...
    -	if token == "" {
    -		// Check if it was required
    -		if m.options.CredentialsOptional {
    -			log.WithContext(ctx).Debugf("no credentials found (CredentialsOptional=true)")
    -			// No error, just no token (and that is ok given that CredentialsOptional is true)
    -			return nil, nil //nolint:nilnil
    -		}
    -
    -		// If we get here, the required token is missing
    -		errorMsg := "required authorization token not found"
    -		log.WithContext(ctx).Debugf("  Error: No credentials found (CredentialsOptional=false)")
    -		return nil, errors.New(errorMsg)
    -	}
    -
    -	// Now parse the token
    -	parsedToken, err := jwt.Parse(token, m.options.ValidationKeyGetter)
    -
    -	// Check if there was an error in parsing...
    -	if err != nil {
    -		log.WithContext(ctx).Errorf("error parsing token: %v", err)
    -		return nil, fmt.Errorf("error parsing token: %w", err)
    -	}
    -
    -	// Check if the parsed token is valid...
    -	if !parsedToken.Valid {
    -		errorMsg := "token is invalid"
    -		log.WithContext(ctx).Debug(errorMsg)
    -		return nil, errors.New(errorMsg)
    -	}
    -
    -	return parsedToken, nil
    -}
    -
    -// stillValid returns true if the JSONWebKey still valid and have enough time to be used
    -func (jwks *Jwks) stillValid() bool {
    -	return !jwks.expiresInTime.IsZero() && time.Now().Add(5*time.Second).Before(jwks.expiresInTime)
    -}
    -
    -func getPemKeys(ctx context.Context, keysLocation string) (*Jwks, error) {
    -	resp, err := http.Get(keysLocation)
    -	if err != nil {
    -		return nil, err
    -	}
    -	defer resp.Body.Close()
    -
    -	jwks := &Jwks{}
    -	err = json.NewDecoder(resp.Body).Decode(jwks)
    -	if err != nil {
    -		return jwks, err
    -	}
    -
    -	cacheControlHeader := resp.Header.Get("Cache-Control")
    -	expiresIn := getMaxAgeFromCacheHeader(ctx, cacheControlHeader)
    -	jwks.expiresInTime = time.Now().Add(time.Duration(expiresIn) * time.Second)
    -
    -	return jwks, err
    -}
    -
    -func getPublicKey(ctx context.Context, token *jwt.Token, jwks *Jwks) (interface{}, error) {
    -	// todo as we load the jkws when the server is starting, we should build a JKS map with the pem cert at the boot time
    -
    -	for k := range jwks.Keys {
    -		if token.Header["kid"] != jwks.Keys[k].Kid {
    -			continue
    -		}
    -
    -		if len(jwks.Keys[k].X5c) != 0 {
    -			cert := "-----BEGIN CERTIFICATE-----\n" + jwks.Keys[k].X5c[0] + "\n-----END CERTIFICATE-----"
    -			return jwt.ParseRSAPublicKeyFromPEM([]byte(cert))
    -		}
    -
    -		if jwks.Keys[k].Kty == "RSA" {
    -			log.WithContext(ctx).Debugf("generating PublicKey from RSA JWK")
    -			return getPublicKeyFromRSA(jwks.Keys[k])
    -		}
    -		if jwks.Keys[k].Kty == "EC" {
    -			log.WithContext(ctx).Debugf("generating PublicKey from ECDSA JWK")
    -			return getPublicKeyFromECDSA(jwks.Keys[k])
    -		}
    -
    -		log.WithContext(ctx).Debugf("Key Type: %s not yet supported, please raise ticket!", jwks.Keys[k].Kty)
    -	}
    -
    -	return nil, keyNotFound
    -}
    -
    -func getPublicKeyFromECDSA(jwk JSONWebKey) (publicKey *ecdsa.PublicKey, err error) {
    -
    -	if jwk.X == "" || jwk.Y == "" || jwk.Crv == "" {
    -		return nil, fmt.Errorf("ecdsa key incomplete")
    -	}
    -
    -	var xCoordinate []byte
    -	if xCoordinate, err = base64.RawURLEncoding.DecodeString(jwk.X); err != nil {
    -		return nil, err
    -	}
    -
    -	var yCoordinate []byte
    -	if yCoordinate, err = base64.RawURLEncoding.DecodeString(jwk.Y); err != nil {
    -		return nil, err
    -	}
    -
    -	publicKey = &ecdsa.PublicKey{}
    -
    -	var curve elliptic.Curve
    -	switch jwk.Crv {
    -	case p256:
    -		curve = elliptic.P256()
    -	case p384:
    -		curve = elliptic.P384()
    -	case p521:
    -		curve = elliptic.P521()
    -	}
    -
    -	publicKey.Curve = curve
    -	publicKey.X = big.NewInt(0).SetBytes(xCoordinate)
    -	publicKey.Y = big.NewInt(0).SetBytes(yCoordinate)
    -
    -	return publicKey, nil
    -}
    -
    -func getPublicKeyFromRSA(jwk JSONWebKey) (*rsa.PublicKey, error) {
    -
    -	decodedE, err := base64.RawURLEncoding.DecodeString(jwk.E)
    -	if err != nil {
    -		return nil, err
    -	}
    -	decodedN, err := base64.RawURLEncoding.DecodeString(jwk.N)
    -	if err != nil {
    -		return nil, err
    -	}
    -
    -	var n, e big.Int
    -	e.SetBytes(decodedE)
    -	n.SetBytes(decodedN)
    -
    -	return &rsa.PublicKey{
    -		E: int(e.Int64()),
    -		N: &n,
    -	}, nil
    -}
    -
    -// getMaxAgeFromCacheHeader extracts max-age directive from the Cache-Control header
    -func getMaxAgeFromCacheHeader(ctx context.Context, cacheControl string) int {
    -	// Split into individual directives
    -	directives := strings.Split(cacheControl, ",")
    -
    -	for _, directive := range directives {
    -		directive = strings.TrimSpace(directive)
    -		if strings.HasPrefix(directive, "max-age=") {
    -			// Extract the max-age value
    -			maxAgeStr := strings.TrimPrefix(directive, "max-age=")
    -			maxAge, err := strconv.Atoi(maxAgeStr)
    -			if err != nil {
    -				log.WithContext(ctx).Debugf("error parsing max-age: %v", err)
    -				return 0
    -			}
    -
    -			return maxAge
    -		}
    -	}
    -
    -	return 0
    -}
    -
    -type JwtValidatorMock struct{}
    -
    -func (j *JwtValidatorMock) ValidateAndParse(ctx context.Context, token string) (*jwt.Token, error) {
    -	claimMaps := jwt.MapClaims{}
    -
    -	switch token {
    -	case "testUserId", "testAdminId", "testOwnerId", "testServiceUserId", "testServiceAdminId", "blockedUserId":
    -		claimMaps[UserIDClaim] = token
    -		claimMaps[AccountIDSuffix] = "testAccountId"
    -		claimMaps[DomainIDSuffix] = "test.com"
    -		claimMaps[DomainCategorySuffix] = "private"
    -	case "otherUserId":
    -		claimMaps[UserIDClaim] = "otherUserId"
    -		claimMaps[AccountIDSuffix] = "otherAccountId"
    -		claimMaps[DomainIDSuffix] = "other.com"
    -		claimMaps[DomainCategorySuffix] = "private"
    -	case "invalidToken":
    -		return nil, errors.New("invalid token")
    -	}
    -
    -	jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, claimMaps)
    -	return jwtToken, nil
    -}
    -
    diff --git a/management/server/management_proto_test.go b/management/server/management_proto_test.go
    index 9c2ce5ad2..4d0630f0f 100644
    --- a/management/server/management_proto_test.go
    +++ b/management/server/management_proto_test.go
    @@ -440,7 +440,7 @@ func startManagementForTest(t *testing.T, testFile string, config *Config) (*grp
     	secretsManager := NewTimeBasedAuthSecretsManager(peersUpdateManager, config.TURNConfig, config.Relay)
     
     	ephemeralMgr := NewEphemeralManager(store, accountManager)
    -	mgmtServer, err := NewServer(context.Background(), config, accountManager, settings.NewManager(store), peersUpdateManager, secretsManager, nil, ephemeralMgr)
    +	mgmtServer, err := NewServer(context.Background(), config, accountManager, settings.NewManager(store), peersUpdateManager, secretsManager, nil, ephemeralMgr, nil)
     	if err != nil {
     		return nil, nil, "", cleanup, err
     	}
    diff --git a/management/server/management_test.go b/management/server/management_test.go
    index 1b91b3447..fd82d8037 100644
    --- a/management/server/management_test.go
    +++ b/management/server/management_test.go
    @@ -204,6 +204,7 @@ func startServer(
     		secretsManager,
     		nil,
     		nil,
    +		nil,
     	)
     	if err != nil {
     		t.Fatalf("failed creating management server: %v", err)
    diff --git a/management/server/mock_server/account_mock.go b/management/server/mock_server/account_mock.go
    index b2a90f156..67c23b95d 100644
    --- a/management/server/mock_server/account_mock.go
    +++ b/management/server/mock_server/account_mock.go
    @@ -13,14 +13,16 @@ import (
     	"github.com/netbirdio/netbird/management/domain"
     	"github.com/netbirdio/netbird/management/server"
     	"github.com/netbirdio/netbird/management/server/activity"
    +	nbcontext "github.com/netbirdio/netbird/management/server/context"
     	"github.com/netbirdio/netbird/management/server/idp"
    -	"github.com/netbirdio/netbird/management/server/jwtclaims"
     	nbpeer "github.com/netbirdio/netbird/management/server/peer"
     	"github.com/netbirdio/netbird/management/server/posture"
     	"github.com/netbirdio/netbird/management/server/types"
     	"github.com/netbirdio/netbird/route"
     )
     
    +var _ server.AccountManager = (*MockAccountManager)(nil)
    +
     type MockAccountManager struct {
     	GetOrCreateAccountByUserFunc func(ctx context.Context, userId, domain string) (*types.Account, error)
     	GetAccountFunc               func(ctx context.Context, accountID string) (*types.Account, error)
    @@ -29,7 +31,7 @@ type MockAccountManager struct {
     	GetSetupKeyFunc                     func(ctx context.Context, accountID, userID, keyID string) (*types.SetupKey, error)
     	AccountExistsFunc                   func(ctx context.Context, accountID string) (bool, error)
     	GetAccountIDByUserIdFunc            func(ctx context.Context, userId, domain string) (string, error)
    -	GetUserFunc                         func(ctx context.Context, claims jwtclaims.AuthorizationClaims) (*types.User, error)
    +	GetUserFromUserAuthFunc             func(ctx context.Context, userAuth nbcontext.UserAuth) (*types.User, error)
     	ListUsersFunc                       func(ctx context.Context, accountID string) ([]*types.User, error)
     	GetPeersFunc                        func(ctx context.Context, accountID, userID string) ([]*nbpeer.Peer, error)
     	MarkPeerConnectedFunc               func(ctx context.Context, peerKey string, connected bool, realIP net.IP) error
    @@ -54,8 +56,6 @@ type MockAccountManager struct {
     	DeletePolicyFunc                    func(ctx context.Context, accountID, policyID, userID string) error
     	ListPoliciesFunc                    func(ctx context.Context, accountID, userID string) ([]*types.Policy, error)
     	GetUsersFromAccountFunc             func(ctx context.Context, accountID, userID string) (map[string]*types.UserInfo, error)
    -	GetPATInfoFunc                      func(ctx context.Context, token string) (*types.User, *types.PersonalAccessToken, string, string, error)
    -	MarkPATUsedFunc                     func(ctx context.Context, pat string) error
     	UpdatePeerMetaFunc                  func(ctx context.Context, peerID string, meta nbpeer.PeerSystemMeta) error
     	UpdatePeerFunc                      func(ctx context.Context, accountID, userID string, peer *nbpeer.Peer) (*nbpeer.Peer, error)
     	CreateRouteFunc                     func(ctx context.Context, accountID string, prefix netip.Prefix, networkType route.NetworkType, domains domain.List, peer string, peerGroups []string, description string, netID route.NetID, masquerade bool, metric int, groups, accessControlGroupIDs []string, enabled bool, userID string, keepRoute bool) (*route.Route, error)
    @@ -80,8 +80,7 @@ type MockAccountManager struct {
     	DeleteNameServerGroupFunc           func(ctx context.Context, accountID, nsGroupID, userID string) error
     	ListNameServerGroupsFunc            func(ctx context.Context, accountID string, userID string) ([]*nbdns.NameServerGroup, error)
     	CreateUserFunc                      func(ctx context.Context, accountID, userID string, key *types.UserInfo) (*types.UserInfo, error)
    -	GetAccountIDFromTokenFunc           func(ctx context.Context, claims jwtclaims.AuthorizationClaims) (string, string, error)
    -	CheckUserAccessByJWTGroupsFunc      func(ctx context.Context, claims jwtclaims.AuthorizationClaims) error
    +	GetAccountIDFromUserAuthFunc        func(ctx context.Context, userAuth nbcontext.UserAuth) (string, string, error)
     	DeleteAccountFunc                   func(ctx context.Context, accountID, userID string) error
     	GetDNSDomainFunc                    func() string
     	StoreEventFunc                      func(ctx context.Context, initiatorID, targetID, accountID string, activityID activity.ActivityDescriber, meta map[string]any)
    @@ -240,14 +239,6 @@ func (am *MockAccountManager) MarkPeerConnected(ctx context.Context, peerKey str
     	return status.Errorf(codes.Unimplemented, "method MarkPeerConnected is not implemented")
     }
     
    -// GetPATInfo mock implementation of GetPATInfo from server.AccountManager interface
    -func (am *MockAccountManager) GetPATInfo(ctx context.Context, pat string) (*types.User, *types.PersonalAccessToken, string, string, error) {
    -	if am.GetPATInfoFunc != nil {
    -		return am.GetPATInfoFunc(ctx, pat)
    -	}
    -	return nil, nil, "", "", status.Errorf(codes.Unimplemented, "method GetPATInfo is not implemented")
    -}
    -
     // DeleteAccount mock implementation of DeleteAccount from server.AccountManager interface
     func (am *MockAccountManager) DeleteAccount(ctx context.Context, accountID, userID string) error {
     	if am.DeleteAccountFunc != nil {
    @@ -256,14 +247,6 @@ func (am *MockAccountManager) DeleteAccount(ctx context.Context, accountID, user
     	return status.Errorf(codes.Unimplemented, "method DeleteAccount is not implemented")
     }
     
    -// MarkPATUsed mock implementation of MarkPATUsed from server.AccountManager interface
    -func (am *MockAccountManager) MarkPATUsed(ctx context.Context, pat string) error {
    -	if am.MarkPATUsedFunc != nil {
    -		return am.MarkPATUsedFunc(ctx, pat)
    -	}
    -	return status.Errorf(codes.Unimplemented, "method MarkPATUsed is not implemented")
    -}
    -
     // CreatePAT mock implementation of GetPAT from server.AccountManager interface
     func (am *MockAccountManager) CreatePAT(ctx context.Context, accountID string, initiatorUserID string, targetUserID string, name string, expiresIn int) (*types.PersonalAccessTokenGenerated, error) {
     	if am.CreatePATFunc != nil {
    @@ -430,11 +413,11 @@ func (am *MockAccountManager) UpdatePeerMeta(ctx context.Context, peerID string,
     }
     
     // GetUser mock implementation of GetUser from server.AccountManager interface
    -func (am *MockAccountManager) GetUser(ctx context.Context, claims jwtclaims.AuthorizationClaims) (*types.User, error) {
    -	if am.GetUserFunc != nil {
    -		return am.GetUserFunc(ctx, claims)
    +func (am *MockAccountManager) GetUserFromUserAuth(ctx context.Context, userAuth nbcontext.UserAuth) (*types.User, error) {
    +	if am.GetUserFromUserAuthFunc != nil {
    +		return am.GetUserFromUserAuthFunc(ctx, userAuth)
     	}
    -	return nil, status.Errorf(codes.Unimplemented, "method GetUser is not implemented")
    +	return nil, status.Errorf(codes.Unimplemented, "method GetUserFromUserAuth is not implemented")
     }
     
     func (am *MockAccountManager) ListUsers(ctx context.Context, accountID string) ([]*types.User, error) {
    @@ -614,19 +597,11 @@ func (am *MockAccountManager) CreateUser(ctx context.Context, accountID, userID
     	return nil, status.Errorf(codes.Unimplemented, "method CreateUser is not implemented")
     }
     
    -// GetAccountIDFromToken mocks GetAccountIDFromToken of the AccountManager interface
    -func (am *MockAccountManager) GetAccountIDFromToken(ctx context.Context, claims jwtclaims.AuthorizationClaims) (string, string, error) {
    -	if am.GetAccountIDFromTokenFunc != nil {
    -		return am.GetAccountIDFromTokenFunc(ctx, claims)
    +func (am *MockAccountManager) GetAccountIDFromUserAuth(ctx context.Context, userAuth nbcontext.UserAuth) (string, string, error) {
    +	if am.GetAccountIDFromUserAuthFunc != nil {
    +		return am.GetAccountIDFromUserAuthFunc(ctx, userAuth)
     	}
    -	return "", "", status.Errorf(codes.Unimplemented, "method GetAccountIDFromToken is not implemented")
    -}
    -
    -func (am *MockAccountManager) CheckUserAccessByJWTGroups(ctx context.Context, claims jwtclaims.AuthorizationClaims) error {
    -	if am.CheckUserAccessByJWTGroupsFunc != nil {
    -		return am.CheckUserAccessByJWTGroupsFunc(ctx, claims)
    -	}
    -	return status.Errorf(codes.Unimplemented, "method CheckUserAccessByJWTGroups is not implemented")
    +	return "", "", status.Errorf(codes.Unimplemented, "method GetAccountIDFromUserAuth is not implemented")
     }
     
     // GetPeers mocks GetPeers of the AccountManager interface
    @@ -859,3 +834,7 @@ func (am *MockAccountManager) BuildUserInfosForAccount(ctx context.Context, acco
     	}
     	return nil, status.Errorf(codes.Unimplemented, "method BuildUserInfosForAccount is not implemented")
     }
    +
    +func (am *MockAccountManager) SyncUserJWTGroups(ctx context.Context, userAuth nbcontext.UserAuth) error {
    +	return status.Errorf(codes.Unimplemented, "method SyncUserJWTGroups is not implemented")
    +}
    diff --git a/management/server/user.go b/management/server/user.go
    index 6ba9b68d3..381879ae6 100644
    --- a/management/server/user.go
    +++ b/management/server/user.go
    @@ -8,16 +8,16 @@ import (
     	"time"
     
     	"github.com/google/uuid"
    +	log "github.com/sirupsen/logrus"
    +
     	"github.com/netbirdio/netbird/management/server/activity"
     	nbContext "github.com/netbirdio/netbird/management/server/context"
     	"github.com/netbirdio/netbird/management/server/idp"
    -	"github.com/netbirdio/netbird/management/server/jwtclaims"
     	nbpeer "github.com/netbirdio/netbird/management/server/peer"
     	"github.com/netbirdio/netbird/management/server/status"
     	"github.com/netbirdio/netbird/management/server/store"
     	"github.com/netbirdio/netbird/management/server/types"
     	"github.com/netbirdio/netbird/management/server/util"
    -	log "github.com/sirupsen/logrus"
     )
     
     // createServiceUser creates a new service user under the given account.
    @@ -174,31 +174,26 @@ func (am *DefaultAccountManager) GetUserByID(ctx context.Context, id string) (*t
     	return am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, id)
     }
     
    -// GetUser looks up a user by provided authorization claims.
    -// It will also create an account if didn't exist for this user before.
    -func (am *DefaultAccountManager) GetUser(ctx context.Context, claims jwtclaims.AuthorizationClaims) (*types.User, error) {
    -	accountID, userID, err := am.GetAccountIDFromToken(ctx, claims)
    -	if err != nil {
    -		return nil, fmt.Errorf("failed to get account with token claims %v", err)
    -	}
    -
    -	user, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, userID)
    +// GetUser looks up a user by provided nbContext.UserAuths.
    +// Expects account to have been created already.
    +func (am *DefaultAccountManager) GetUserFromUserAuth(ctx context.Context, userAuth nbContext.UserAuth) (*types.User, error) {
    +	user, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, userAuth.UserId)
     	if err != nil {
     		return nil, err
     	}
     
     	// this code should be outside of the am.GetAccountIDFromToken(claims) because this method is called also by the gRPC
     	// server when user authenticates a device. And we need to separate the Dashboard login event from the Device login event.
    -	newLogin := user.LastDashboardLoginChanged(claims.LastLogin)
    +	newLogin := user.LastDashboardLoginChanged(userAuth.LastLogin)
     
    -	err = am.Store.SaveUserLastLogin(ctx, accountID, userID, claims.LastLogin)
    +	err = am.Store.SaveUserLastLogin(ctx, userAuth.AccountId, userAuth.UserId, userAuth.LastLogin)
     	if err != nil {
     		log.WithContext(ctx).Errorf("failed saving user last login: %v", err)
     	}
     
     	if newLogin {
    -		meta := map[string]any{"timestamp": claims.LastLogin}
    -		am.StoreEvent(ctx, claims.UserId, claims.UserId, accountID, activity.DashboardLogin, meta)
    +		meta := map[string]any{"timestamp": userAuth.LastLogin}
    +		am.StoreEvent(ctx, userAuth.UserId, userAuth.UserId, userAuth.AccountId, activity.DashboardLogin, meta)
     	}
     
     	return user, nil
    diff --git a/management/server/user_test.go b/management/server/user_test.go
    index 4a532c8a6..a180a761a 100644
    --- a/management/server/user_test.go
    +++ b/management/server/user_test.go
    @@ -10,6 +10,8 @@ import (
     	"github.com/eko/gocache/v3/cache"
     	cacheStore "github.com/eko/gocache/v3/store"
     	"github.com/google/go-cmp/cmp"
    +
    +	nbcontext "github.com/netbirdio/netbird/management/server/context"
     	"github.com/netbirdio/netbird/management/server/util"
     	"golang.org/x/exp/maps"
     
    @@ -25,7 +27,6 @@ import (
     	"github.com/netbirdio/netbird/management/server/activity"
     	"github.com/netbirdio/netbird/management/server/idp"
     	"github.com/netbirdio/netbird/management/server/integration_reference"
    -	"github.com/netbirdio/netbird/management/server/jwtclaims"
     )
     
     const (
    @@ -925,11 +926,12 @@ func TestDefaultAccountManager_GetUser(t *testing.T) {
     		eventStore: &activity.InMemoryEventStore{},
     	}
     
    -	claims := jwtclaims.AuthorizationClaims{
    -		UserId: mockUserID,
    +	claims := nbcontext.UserAuth{
    +		UserId:    mockUserID,
    +		AccountId: mockAccountID,
     	}
     
    -	user, err := am.GetUser(context.Background(), claims)
    +	user, err := am.GetUserFromUserAuth(context.Background(), claims)
     	if err != nil {
     		t.Fatalf("Error when checking user role: %s", err)
     	}
    
    From 96de928cb3f87bb8454ab05e21ad5a050262cd29 Mon Sep 17 00:00:00 2001
    From: Zoltan Papp 
    Date: Fri, 21 Feb 2025 10:19:38 +0100
    Subject: [PATCH 78/92] Interface code cleaning (#3358)
    
    Code cleaning in interfaces files
    ---
     client/iface/iface_moc.go                     | 123 ------------------
     client/iface/iwginterface_windows.go          |  39 ------
     client/internal/engine.go                     |   2 +-
     client/internal/engine_test.go                | 115 +++++++++++++++-
     client/internal/iface.go                      |   8 ++
     .../iface_common.go}                          |   6 +-
     client/internal/iface_windows.go              |   6 +
     client/internal/peer/conn.go                  |   3 +-
     client/internal/peer/iface.go                 |  17 +++
     client/internal/routemanager/client.go        |  10 +-
     client/internal/routemanager/dynamic/route.go |   6 +-
     client/internal/routemanager/iface/iface.go   |   9 ++
     .../routemanager/iface/iface_common.go        |  22 ++++
     .../routemanager/iface/iface_windows.go       |   7 +
     client/internal/routemanager/manager.go       |   6 +-
     .../internal/routemanager/server_android.go   |   4 +-
     .../routemanager/server_nonandroid.go         |   6 +-
     .../routemanager/sysctl/sysctl_linux.go       |   4 +-
     .../routemanager/systemops/systemops.go       |   6 +-
     .../systemops/systemops_generic.go            |   4 +-
     20 files changed, 209 insertions(+), 194 deletions(-)
     delete mode 100644 client/iface/iface_moc.go
     delete mode 100644 client/iface/iwginterface_windows.go
     create mode 100644 client/internal/iface.go
     rename client/{iface/iwginterface.go => internal/iface_common.go} (95%)
     create mode 100644 client/internal/iface_windows.go
     create mode 100644 client/internal/peer/iface.go
     create mode 100644 client/internal/routemanager/iface/iface.go
     create mode 100644 client/internal/routemanager/iface/iface_common.go
     create mode 100644 client/internal/routemanager/iface/iface_windows.go
    
    diff --git a/client/iface/iface_moc.go b/client/iface/iface_moc.go
    deleted file mode 100644
    index f92a8cfc8..000000000
    --- a/client/iface/iface_moc.go
    +++ /dev/null
    @@ -1,123 +0,0 @@
    -package iface
    -
    -import (
    -	"net"
    -	"time"
    -
    -	wgdevice "golang.zx2c4.com/wireguard/device"
    -	"golang.zx2c4.com/wireguard/tun/netstack"
    -	"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
    -
    -	"github.com/netbirdio/netbird/client/iface/bind"
    -	"github.com/netbirdio/netbird/client/iface/configurer"
    -	"github.com/netbirdio/netbird/client/iface/device"
    -	"github.com/netbirdio/netbird/client/iface/wgproxy"
    -)
    -
    -type MockWGIface struct {
    -	CreateFunc                 func() error
    -	CreateOnAndroidFunc        func(routeRange []string, ip string, domains []string) error
    -	IsUserspaceBindFunc        func() bool
    -	NameFunc                   func() string
    -	AddressFunc                func() device.WGAddress
    -	ToInterfaceFunc            func() *net.Interface
    -	UpFunc                     func() (*bind.UniversalUDPMuxDefault, error)
    -	UpdateAddrFunc             func(newAddr string) error
    -	UpdatePeerFunc             func(peerKey string, allowedIps string, keepAlive time.Duration, endpoint *net.UDPAddr, preSharedKey *wgtypes.Key) error
    -	RemovePeerFunc             func(peerKey string) error
    -	AddAllowedIPFunc           func(peerKey string, allowedIP string) error
    -	RemoveAllowedIPFunc        func(peerKey string, allowedIP string) error
    -	CloseFunc                  func() error
    -	SetFilterFunc              func(filter device.PacketFilter) error
    -	GetFilterFunc              func() device.PacketFilter
    -	GetDeviceFunc              func() *device.FilteredDevice
    -	GetWGDeviceFunc            func() *wgdevice.Device
    -	GetStatsFunc               func(peerKey string) (configurer.WGStats, error)
    -	GetInterfaceGUIDStringFunc func() (string, error)
    -	GetProxyFunc               func() wgproxy.Proxy
    -	GetNetFunc                 func() *netstack.Net
    -}
    -
    -func (m *MockWGIface) GetInterfaceGUIDString() (string, error) {
    -	return m.GetInterfaceGUIDStringFunc()
    -}
    -
    -func (m *MockWGIface) Create() error {
    -	return m.CreateFunc()
    -}
    -
    -func (m *MockWGIface) CreateOnAndroid(routeRange []string, ip string, domains []string) error {
    -	return m.CreateOnAndroidFunc(routeRange, ip, domains)
    -}
    -
    -func (m *MockWGIface) IsUserspaceBind() bool {
    -	return m.IsUserspaceBindFunc()
    -}
    -
    -func (m *MockWGIface) Name() string {
    -	return m.NameFunc()
    -}
    -
    -func (m *MockWGIface) Address() device.WGAddress {
    -	return m.AddressFunc()
    -}
    -
    -func (m *MockWGIface) ToInterface() *net.Interface {
    -	return m.ToInterfaceFunc()
    -}
    -
    -func (m *MockWGIface) Up() (*bind.UniversalUDPMuxDefault, error) {
    -	return m.UpFunc()
    -}
    -
    -func (m *MockWGIface) UpdateAddr(newAddr string) error {
    -	return m.UpdateAddrFunc(newAddr)
    -}
    -
    -func (m *MockWGIface) UpdatePeer(peerKey string, allowedIps string, keepAlive time.Duration, endpoint *net.UDPAddr, preSharedKey *wgtypes.Key) error {
    -	return m.UpdatePeerFunc(peerKey, allowedIps, keepAlive, endpoint, preSharedKey)
    -}
    -
    -func (m *MockWGIface) RemovePeer(peerKey string) error {
    -	return m.RemovePeerFunc(peerKey)
    -}
    -
    -func (m *MockWGIface) AddAllowedIP(peerKey string, allowedIP string) error {
    -	return m.AddAllowedIPFunc(peerKey, allowedIP)
    -}
    -
    -func (m *MockWGIface) RemoveAllowedIP(peerKey string, allowedIP string) error {
    -	return m.RemoveAllowedIPFunc(peerKey, allowedIP)
    -}
    -
    -func (m *MockWGIface) Close() error {
    -	return m.CloseFunc()
    -}
    -
    -func (m *MockWGIface) SetFilter(filter device.PacketFilter) error {
    -	return m.SetFilterFunc(filter)
    -}
    -
    -func (m *MockWGIface) GetFilter() device.PacketFilter {
    -	return m.GetFilterFunc()
    -}
    -
    -func (m *MockWGIface) GetDevice() *device.FilteredDevice {
    -	return m.GetDeviceFunc()
    -}
    -
    -func (m *MockWGIface) GetWGDevice() *wgdevice.Device {
    -	return m.GetWGDeviceFunc()
    -}
    -
    -func (m *MockWGIface) GetStats(peerKey string) (configurer.WGStats, error) {
    -	return m.GetStatsFunc(peerKey)
    -}
    -
    -func (m *MockWGIface) GetProxy() wgproxy.Proxy {
    -	return m.GetProxyFunc()
    -}
    -
    -func (m *MockWGIface) GetNet() *netstack.Net {
    -	return m.GetNetFunc()
    -}
    diff --git a/client/iface/iwginterface_windows.go b/client/iface/iwginterface_windows.go
    deleted file mode 100644
    index cac096b54..000000000
    --- a/client/iface/iwginterface_windows.go
    +++ /dev/null
    @@ -1,39 +0,0 @@
    -package iface
    -
    -import (
    -	"net"
    -	"time"
    -
    -	wgdevice "golang.zx2c4.com/wireguard/device"
    -	"golang.zx2c4.com/wireguard/tun/netstack"
    -	"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
    -
    -	"github.com/netbirdio/netbird/client/iface/bind"
    -	"github.com/netbirdio/netbird/client/iface/configurer"
    -	"github.com/netbirdio/netbird/client/iface/device"
    -	"github.com/netbirdio/netbird/client/iface/wgproxy"
    -)
    -
    -type IWGIface interface {
    -	Create() error
    -	CreateOnAndroid(routeRange []string, ip string, domains []string) error
    -	IsUserspaceBind() bool
    -	Name() string
    -	Address() device.WGAddress
    -	ToInterface() *net.Interface
    -	Up() (*bind.UniversalUDPMuxDefault, error)
    -	UpdateAddr(newAddr string) error
    -	GetProxy() wgproxy.Proxy
    -	UpdatePeer(peerKey string, allowedIps string, keepAlive time.Duration, endpoint *net.UDPAddr, preSharedKey *wgtypes.Key) error
    -	RemovePeer(peerKey string) error
    -	AddAllowedIP(peerKey string, allowedIP string) error
    -	RemoveAllowedIP(peerKey string, allowedIP string) error
    -	Close() error
    -	SetFilter(filter device.PacketFilter) error
    -	GetFilter() device.PacketFilter
    -	GetDevice() *device.FilteredDevice
    -	GetWGDevice() *wgdevice.Device
    -	GetStats(peerKey string) (configurer.WGStats, error)
    -	GetInterfaceGUIDString() (string, error)
    -	GetNet() *netstack.Net
    -}
    diff --git a/client/internal/engine.go b/client/internal/engine.go
    index d590c0db6..90f70a827 100644
    --- a/client/internal/engine.go
    +++ b/client/internal/engine.go
    @@ -154,7 +154,7 @@ type Engine struct {
     	ctx    context.Context
     	cancel context.CancelFunc
     
    -	wgInterface iface.IWGIface
    +	wgInterface WGIface
     
     	udpMux *bind.UniversalUDPMuxDefault
     
    diff --git a/client/internal/engine_test.go b/client/internal/engine_test.go
    index e32e262b9..2b1d3b098 100644
    --- a/client/internal/engine_test.go
    +++ b/client/internal/engine_test.go
    @@ -23,10 +23,11 @@ import (
     	"google.golang.org/grpc/keepalive"
     
     	"github.com/netbirdio/management-integrations/integrations"
    -
     	"github.com/netbirdio/netbird/client/iface"
     	"github.com/netbirdio/netbird/client/iface/bind"
    +	"github.com/netbirdio/netbird/client/iface/configurer"
     	"github.com/netbirdio/netbird/client/iface/device"
    +	"github.com/netbirdio/netbird/client/iface/wgproxy"
     	"github.com/netbirdio/netbird/client/internal/dns"
     	"github.com/netbirdio/netbird/client/internal/peer"
     	"github.com/netbirdio/netbird/client/internal/peer/guard"
    @@ -48,6 +49,8 @@ import (
     	"github.com/netbirdio/netbird/signal/proto"
     	signalServer "github.com/netbirdio/netbird/signal/server"
     	"github.com/netbirdio/netbird/util"
    +	wgdevice "golang.zx2c4.com/wireguard/device"
    +	"golang.zx2c4.com/wireguard/tun/netstack"
     )
     
     var (
    @@ -64,6 +67,114 @@ var (
     	}
     )
     
    +type MockWGIface struct {
    +	CreateFunc                 func() error
    +	CreateOnAndroidFunc        func(routeRange []string, ip string, domains []string) error
    +	IsUserspaceBindFunc        func() bool
    +	NameFunc                   func() string
    +	AddressFunc                func() device.WGAddress
    +	ToInterfaceFunc            func() *net.Interface
    +	UpFunc                     func() (*bind.UniversalUDPMuxDefault, error)
    +	UpdateAddrFunc             func(newAddr string) error
    +	UpdatePeerFunc             func(peerKey string, allowedIps string, keepAlive time.Duration, endpoint *net.UDPAddr, preSharedKey *wgtypes.Key) error
    +	RemovePeerFunc             func(peerKey string) error
    +	AddAllowedIPFunc           func(peerKey string, allowedIP string) error
    +	RemoveAllowedIPFunc        func(peerKey string, allowedIP string) error
    +	CloseFunc                  func() error
    +	SetFilterFunc              func(filter device.PacketFilter) error
    +	GetFilterFunc              func() device.PacketFilter
    +	GetDeviceFunc              func() *device.FilteredDevice
    +	GetWGDeviceFunc            func() *wgdevice.Device
    +	GetStatsFunc               func(peerKey string) (configurer.WGStats, error)
    +	GetInterfaceGUIDStringFunc func() (string, error)
    +	GetProxyFunc               func() wgproxy.Proxy
    +	GetNetFunc                 func() *netstack.Net
    +}
    +
    +func (m *MockWGIface) GetInterfaceGUIDString() (string, error) {
    +	return m.GetInterfaceGUIDStringFunc()
    +}
    +
    +func (m *MockWGIface) Create() error {
    +	return m.CreateFunc()
    +}
    +
    +func (m *MockWGIface) CreateOnAndroid(routeRange []string, ip string, domains []string) error {
    +	return m.CreateOnAndroidFunc(routeRange, ip, domains)
    +}
    +
    +func (m *MockWGIface) IsUserspaceBind() bool {
    +	return m.IsUserspaceBindFunc()
    +}
    +
    +func (m *MockWGIface) Name() string {
    +	return m.NameFunc()
    +}
    +
    +func (m *MockWGIface) Address() device.WGAddress {
    +	return m.AddressFunc()
    +}
    +
    +func (m *MockWGIface) ToInterface() *net.Interface {
    +	return m.ToInterfaceFunc()
    +}
    +
    +func (m *MockWGIface) Up() (*bind.UniversalUDPMuxDefault, error) {
    +	return m.UpFunc()
    +}
    +
    +func (m *MockWGIface) UpdateAddr(newAddr string) error {
    +	return m.UpdateAddrFunc(newAddr)
    +}
    +
    +func (m *MockWGIface) UpdatePeer(peerKey string, allowedIps string, keepAlive time.Duration, endpoint *net.UDPAddr, preSharedKey *wgtypes.Key) error {
    +	return m.UpdatePeerFunc(peerKey, allowedIps, keepAlive, endpoint, preSharedKey)
    +}
    +
    +func (m *MockWGIface) RemovePeer(peerKey string) error {
    +	return m.RemovePeerFunc(peerKey)
    +}
    +
    +func (m *MockWGIface) AddAllowedIP(peerKey string, allowedIP string) error {
    +	return m.AddAllowedIPFunc(peerKey, allowedIP)
    +}
    +
    +func (m *MockWGIface) RemoveAllowedIP(peerKey string, allowedIP string) error {
    +	return m.RemoveAllowedIPFunc(peerKey, allowedIP)
    +}
    +
    +func (m *MockWGIface) Close() error {
    +	return m.CloseFunc()
    +}
    +
    +func (m *MockWGIface) SetFilter(filter device.PacketFilter) error {
    +	return m.SetFilterFunc(filter)
    +}
    +
    +func (m *MockWGIface) GetFilter() device.PacketFilter {
    +	return m.GetFilterFunc()
    +}
    +
    +func (m *MockWGIface) GetDevice() *device.FilteredDevice {
    +	return m.GetDeviceFunc()
    +}
    +
    +func (m *MockWGIface) GetWGDevice() *wgdevice.Device {
    +	return m.GetWGDeviceFunc()
    +}
    +
    +func (m *MockWGIface) GetStats(peerKey string) (configurer.WGStats, error) {
    +	return m.GetStatsFunc(peerKey)
    +}
    +
    +func (m *MockWGIface) GetProxy() wgproxy.Proxy {
    +	return m.GetProxyFunc()
    +}
    +
    +func (m *MockWGIface) GetNet() *netstack.Net {
    +	return m.GetNetFunc()
    +}
    +
     func TestMain(m *testing.M) {
     	_ = util.InitLog("debug", "console")
     	code := m.Run()
    @@ -245,7 +356,7 @@ func TestEngine_UpdateNetworkMap(t *testing.T) {
     		peer.NewRecorder("https://mgm"),
     		nil)
     
    -	wgIface := &iface.MockWGIface{
    +	wgIface := &MockWGIface{
     		NameFunc: func() string { return "utun102" },
     		RemovePeerFunc: func(peerKey string) error {
     			return nil
    diff --git a/client/internal/iface.go b/client/internal/iface.go
    new file mode 100644
    index 000000000..bd0069c19
    --- /dev/null
    +++ b/client/internal/iface.go
    @@ -0,0 +1,8 @@
    +//go:build !windows
    +// +build !windows
    +
    +package internal
    +
    +type WGIface interface {
    +	wgIfaceBase
    +}
    diff --git a/client/iface/iwginterface.go b/client/internal/iface_common.go
    similarity index 95%
    rename from client/iface/iwginterface.go
    rename to client/internal/iface_common.go
    index 2b919ac9e..a66342707 100644
    --- a/client/iface/iwginterface.go
    +++ b/client/internal/iface_common.go
    @@ -1,6 +1,4 @@
    -//go:build !windows
    -
    -package iface
    +package internal
     
     import (
     	"net"
    @@ -16,7 +14,7 @@ import (
     	"github.com/netbirdio/netbird/client/iface/wgproxy"
     )
     
    -type IWGIface interface {
    +type wgIfaceBase interface {
     	Create() error
     	CreateOnAndroid(routeRange []string, ip string, domains []string) error
     	IsUserspaceBind() bool
    diff --git a/client/internal/iface_windows.go b/client/internal/iface_windows.go
    new file mode 100644
    index 000000000..113217815
    --- /dev/null
    +++ b/client/internal/iface_windows.go
    @@ -0,0 +1,6 @@
    +package internal
    +
    +type WGIface interface {
    +	wgIfaceBase
    +	GetInterfaceGUIDString() (string, error)
    +}
    diff --git a/client/internal/peer/conn.go b/client/internal/peer/conn.go
    index 8bbea6a2b..0337960bb 100644
    --- a/client/internal/peer/conn.go
    +++ b/client/internal/peer/conn.go
    @@ -15,7 +15,6 @@ import (
     	log "github.com/sirupsen/logrus"
     	"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
     
    -	"github.com/netbirdio/netbird/client/iface"
     	"github.com/netbirdio/netbird/client/iface/configurer"
     	"github.com/netbirdio/netbird/client/iface/wgproxy"
     	"github.com/netbirdio/netbird/client/internal/peer/guard"
    @@ -56,7 +55,7 @@ const (
     type WgConfig struct {
     	WgListenPort int
     	RemoteKey    string
    -	WgInterface  iface.IWGIface
    +	WgInterface  WGIface
     	AllowedIps   string
     	PreSharedKey *wgtypes.Key
     }
    diff --git a/client/internal/peer/iface.go b/client/internal/peer/iface.go
    new file mode 100644
    index 000000000..ae6b3bd0a
    --- /dev/null
    +++ b/client/internal/peer/iface.go
    @@ -0,0 +1,17 @@
    +package peer
    +
    +import (
    +	"net"
    +	"time"
    +
    +	"github.com/netbirdio/netbird/client/iface/configurer"
    +	"github.com/netbirdio/netbird/client/iface/wgproxy"
    +	"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
    +)
    +
    +type WGIface interface {
    +	UpdatePeer(peerKey string, allowedIps string, keepAlive time.Duration, endpoint *net.UDPAddr, preSharedKey *wgtypes.Key) error
    +	RemovePeer(peerKey string) error
    +	GetStats(peerKey string) (configurer.WGStats, error)
    +	GetProxy() wgproxy.Proxy
    +}
    diff --git a/client/internal/routemanager/client.go b/client/internal/routemanager/client.go
    index 3238dd831..24a7ef467 100644
    --- a/client/internal/routemanager/client.go
    +++ b/client/internal/routemanager/client.go
    @@ -4,19 +4,19 @@ import (
     	"context"
     	"fmt"
     	"reflect"
    -	runtime "runtime"
    +	"runtime"
     	"time"
     
     	"github.com/hashicorp/go-multierror"
     	log "github.com/sirupsen/logrus"
     
     	nberrors "github.com/netbirdio/netbird/client/errors"
    -	"github.com/netbirdio/netbird/client/iface"
     	nbdns "github.com/netbirdio/netbird/client/internal/dns"
     	"github.com/netbirdio/netbird/client/internal/peer"
     	"github.com/netbirdio/netbird/client/internal/peerstore"
     	"github.com/netbirdio/netbird/client/internal/routemanager/dnsinterceptor"
     	"github.com/netbirdio/netbird/client/internal/routemanager/dynamic"
    +	"github.com/netbirdio/netbird/client/internal/routemanager/iface"
     	"github.com/netbirdio/netbird/client/internal/routemanager/refcounter"
     	"github.com/netbirdio/netbird/client/internal/routemanager/static"
     	"github.com/netbirdio/netbird/client/proto"
    @@ -62,7 +62,7 @@ type clientNetwork struct {
     	ctx                 context.Context
     	cancel              context.CancelFunc
     	statusRecorder      *peer.Status
    -	wgInterface         iface.IWGIface
    +	wgInterface         iface.WGIface
     	routes              map[route.ID]*route.Route
     	routeUpdate         chan routesUpdate
     	peerStateUpdate     chan struct{}
    @@ -75,7 +75,7 @@ type clientNetwork struct {
     func newClientNetworkWatcher(
     	ctx context.Context,
     	dnsRouteInterval time.Duration,
    -	wgInterface iface.IWGIface,
    +	wgInterface iface.WGIface,
     	statusRecorder *peer.Status,
     	rt *route.Route,
     	routeRefCounter *refcounter.RouteRefCounter,
    @@ -468,7 +468,7 @@ func handlerFromRoute(
     	allowedIPsRefCounter *refcounter.AllowedIPsRefCounter,
     	dnsRouterInteval time.Duration,
     	statusRecorder *peer.Status,
    -	wgInterface iface.IWGIface,
    +	wgInterface iface.WGIface,
     	dnsServer nbdns.Server,
     	peerStore *peerstore.Store,
     	useNewDNSRoute bool,
    diff --git a/client/internal/routemanager/dynamic/route.go b/client/internal/routemanager/dynamic/route.go
    index a0fff7713..5ef18a47e 100644
    --- a/client/internal/routemanager/dynamic/route.go
    +++ b/client/internal/routemanager/dynamic/route.go
    @@ -13,8 +13,8 @@ import (
     	log "github.com/sirupsen/logrus"
     
     	nberrors "github.com/netbirdio/netbird/client/errors"
    -	"github.com/netbirdio/netbird/client/iface"
     	"github.com/netbirdio/netbird/client/internal/peer"
    +	"github.com/netbirdio/netbird/client/internal/routemanager/iface"
     	"github.com/netbirdio/netbird/client/internal/routemanager/refcounter"
     	"github.com/netbirdio/netbird/client/internal/routemanager/util"
     	"github.com/netbirdio/netbird/management/domain"
    @@ -48,7 +48,7 @@ type Route struct {
     	currentPeerKey       string
     	cancel               context.CancelFunc
     	statusRecorder       *peer.Status
    -	wgInterface          iface.IWGIface
    +	wgInterface          iface.WGIface
     	resolverAddr         string
     }
     
    @@ -58,7 +58,7 @@ func NewRoute(
     	allowedIPsRefCounter *refcounter.AllowedIPsRefCounter,
     	interval time.Duration,
     	statusRecorder *peer.Status,
    -	wgInterface iface.IWGIface,
    +	wgInterface iface.WGIface,
     	resolverAddr string,
     ) *Route {
     	return &Route{
    diff --git a/client/internal/routemanager/iface/iface.go b/client/internal/routemanager/iface/iface.go
    new file mode 100644
    index 000000000..57dbec03d
    --- /dev/null
    +++ b/client/internal/routemanager/iface/iface.go
    @@ -0,0 +1,9 @@
    +//go:build !windows
    +// +build !windows
    +
    +package iface
    +
    +// WGIface defines subset methods of interface required for router
    +type WGIface interface {
    +	wgIfaceBase
    +}
    diff --git a/client/internal/routemanager/iface/iface_common.go b/client/internal/routemanager/iface/iface_common.go
    new file mode 100644
    index 000000000..8b2dc9714
    --- /dev/null
    +++ b/client/internal/routemanager/iface/iface_common.go
    @@ -0,0 +1,22 @@
    +package iface
    +
    +import (
    +	"net"
    +
    +	"github.com/netbirdio/netbird/client/iface"
    +	"github.com/netbirdio/netbird/client/iface/configurer"
    +	"github.com/netbirdio/netbird/client/iface/device"
    +)
    +
    +type wgIfaceBase interface {
    +	AddAllowedIP(peerKey string, allowedIP string) error
    +	RemoveAllowedIP(peerKey string, allowedIP string) error
    +
    +	Name() string
    +	Address() iface.WGAddress
    +	ToInterface() *net.Interface
    +	IsUserspaceBind() bool
    +	GetFilter() device.PacketFilter
    +	GetDevice() *device.FilteredDevice
    +	GetStats(peerKey string) (configurer.WGStats, error)
    +}
    diff --git a/client/internal/routemanager/iface/iface_windows.go b/client/internal/routemanager/iface/iface_windows.go
    new file mode 100644
    index 000000000..7ab7e239c
    --- /dev/null
    +++ b/client/internal/routemanager/iface/iface_windows.go
    @@ -0,0 +1,7 @@
    +package iface
    +
    +// WGIface defines subset methods of interface required for router
    +type WGIface interface {
    +	wgIfaceBase
    +	GetInterfaceGUIDString() (string, error)
    +}
    diff --git a/client/internal/routemanager/manager.go b/client/internal/routemanager/manager.go
    index 52de0948b..ae0d1d220 100644
    --- a/client/internal/routemanager/manager.go
    +++ b/client/internal/routemanager/manager.go
    @@ -15,13 +15,13 @@ import (
     	"golang.org/x/exp/maps"
     
     	firewall "github.com/netbirdio/netbird/client/firewall/manager"
    -	"github.com/netbirdio/netbird/client/iface"
     	"github.com/netbirdio/netbird/client/iface/configurer"
     	"github.com/netbirdio/netbird/client/iface/netstack"
     	"github.com/netbirdio/netbird/client/internal/dns"
     	"github.com/netbirdio/netbird/client/internal/listener"
     	"github.com/netbirdio/netbird/client/internal/peer"
     	"github.com/netbirdio/netbird/client/internal/peerstore"
    +	"github.com/netbirdio/netbird/client/internal/routemanager/iface"
     	"github.com/netbirdio/netbird/client/internal/routemanager/notifier"
     	"github.com/netbirdio/netbird/client/internal/routemanager/refcounter"
     	"github.com/netbirdio/netbird/client/internal/routemanager/systemops"
    @@ -52,7 +52,7 @@ type ManagerConfig struct {
     	Context             context.Context
     	PublicKey           string
     	DNSRouteInterval    time.Duration
    -	WGInterface         iface.IWGIface
    +	WGInterface         iface.WGIface
     	StatusRecorder      *peer.Status
     	RelayManager        *relayClient.Manager
     	InitialRoutes       []*route.Route
    @@ -74,7 +74,7 @@ type DefaultManager struct {
     	sysOps               *systemops.SysOps
     	statusRecorder       *peer.Status
     	relayMgr             *relayClient.Manager
    -	wgInterface          iface.IWGIface
    +	wgInterface          iface.WGIface
     	pubKey               string
     	notifier             *notifier.Notifier
     	routeRefCounter      *refcounter.RouteRefCounter
    diff --git a/client/internal/routemanager/server_android.go b/client/internal/routemanager/server_android.go
    index e9cfa0826..48bb0380d 100644
    --- a/client/internal/routemanager/server_android.go
    +++ b/client/internal/routemanager/server_android.go
    @@ -7,8 +7,8 @@ import (
     	"fmt"
     
     	firewall "github.com/netbirdio/netbird/client/firewall/manager"
    -	"github.com/netbirdio/netbird/client/iface"
     	"github.com/netbirdio/netbird/client/internal/peer"
    +	"github.com/netbirdio/netbird/client/internal/routemanager/iface"
     	"github.com/netbirdio/netbird/route"
     )
     
    @@ -22,6 +22,6 @@ func (r serverRouter) updateRoutes(map[route.ID]*route.Route) error {
     	return nil
     }
     
    -func newServerRouter(context.Context, iface.IWGIface, firewall.Manager, *peer.Status) (*serverRouter, error) {
    +func newServerRouter(context.Context, iface.WGIface, firewall.Manager, *peer.Status) (*serverRouter, error) {
     	return nil, fmt.Errorf("server route not supported on this os")
     }
    diff --git a/client/internal/routemanager/server_nonandroid.go b/client/internal/routemanager/server_nonandroid.go
    index 4690e3f0e..c9bbe10a6 100644
    --- a/client/internal/routemanager/server_nonandroid.go
    +++ b/client/internal/routemanager/server_nonandroid.go
    @@ -11,8 +11,8 @@ import (
     	log "github.com/sirupsen/logrus"
     
     	firewall "github.com/netbirdio/netbird/client/firewall/manager"
    -	"github.com/netbirdio/netbird/client/iface"
     	"github.com/netbirdio/netbird/client/internal/peer"
    +	"github.com/netbirdio/netbird/client/internal/routemanager/iface"
     	"github.com/netbirdio/netbird/client/internal/routemanager/systemops"
     	"github.com/netbirdio/netbird/route"
     )
    @@ -22,11 +22,11 @@ type serverRouter struct {
     	ctx            context.Context
     	routes         map[route.ID]*route.Route
     	firewall       firewall.Manager
    -	wgInterface    iface.IWGIface
    +	wgInterface    iface.WGIface
     	statusRecorder *peer.Status
     }
     
    -func newServerRouter(ctx context.Context, wgInterface iface.IWGIface, firewall firewall.Manager, statusRecorder *peer.Status) (*serverRouter, error) {
    +func newServerRouter(ctx context.Context, wgInterface iface.WGIface, firewall firewall.Manager, statusRecorder *peer.Status) (*serverRouter, error) {
     	return &serverRouter{
     		ctx:            ctx,
     		routes:         make(map[route.ID]*route.Route),
    diff --git a/client/internal/routemanager/sysctl/sysctl_linux.go b/client/internal/routemanager/sysctl/sysctl_linux.go
    index bb620ee68..ea63f02fc 100644
    --- a/client/internal/routemanager/sysctl/sysctl_linux.go
    +++ b/client/internal/routemanager/sysctl/sysctl_linux.go
    @@ -13,7 +13,7 @@ import (
     	log "github.com/sirupsen/logrus"
     
     	nberrors "github.com/netbirdio/netbird/client/errors"
    -	"github.com/netbirdio/netbird/client/iface"
    +	"github.com/netbirdio/netbird/client/internal/routemanager/iface"
     )
     
     const (
    @@ -23,7 +23,7 @@ const (
     )
     
     // Setup configures sysctl settings for RP filtering and source validation.
    -func Setup(wgIface iface.IWGIface) (map[string]int, error) {
    +func Setup(wgIface iface.WGIface) (map[string]int, error) {
     	keys := map[string]int{}
     	var result *multierror.Error
     
    diff --git a/client/internal/routemanager/systemops/systemops.go b/client/internal/routemanager/systemops/systemops.go
    index d1cb83bfb..5c117b94d 100644
    --- a/client/internal/routemanager/systemops/systemops.go
    +++ b/client/internal/routemanager/systemops/systemops.go
    @@ -5,7 +5,7 @@ import (
     	"net/netip"
     	"sync"
     
    -	"github.com/netbirdio/netbird/client/iface"
    +	"github.com/netbirdio/netbird/client/internal/routemanager/iface"
     	"github.com/netbirdio/netbird/client/internal/routemanager/notifier"
     	"github.com/netbirdio/netbird/client/internal/routemanager/refcounter"
     )
    @@ -19,7 +19,7 @@ type ExclusionCounter = refcounter.Counter[netip.Prefix, struct{}, Nexthop]
     
     type SysOps struct {
     	refCounter  *ExclusionCounter
    -	wgInterface iface.IWGIface
    +	wgInterface iface.WGIface
     	// prefixes is tracking all the current added prefixes im memory
     	// (this is used in iOS as all route updates require a full table update)
     	//nolint
    @@ -30,7 +30,7 @@ type SysOps struct {
     	notifier *notifier.Notifier
     }
     
    -func NewSysOps(wgInterface iface.IWGIface, notifier *notifier.Notifier) *SysOps {
    +func NewSysOps(wgInterface iface.WGIface, notifier *notifier.Notifier) *SysOps {
     	return &SysOps{
     		wgInterface: wgInterface,
     		notifier:    notifier,
    diff --git a/client/internal/routemanager/systemops/systemops_generic.go b/client/internal/routemanager/systemops/systemops_generic.go
    index 31b7f3ac2..eaef01815 100644
    --- a/client/internal/routemanager/systemops/systemops_generic.go
    +++ b/client/internal/routemanager/systemops/systemops_generic.go
    @@ -16,8 +16,8 @@ import (
     	log "github.com/sirupsen/logrus"
     
     	nberrors "github.com/netbirdio/netbird/client/errors"
    -	"github.com/netbirdio/netbird/client/iface"
     	"github.com/netbirdio/netbird/client/iface/netstack"
    +	"github.com/netbirdio/netbird/client/internal/routemanager/iface"
     	"github.com/netbirdio/netbird/client/internal/routemanager/refcounter"
     	"github.com/netbirdio/netbird/client/internal/routemanager/util"
     	"github.com/netbirdio/netbird/client/internal/routemanager/vars"
    @@ -149,7 +149,7 @@ func (r *SysOps) addRouteForCurrentDefaultGateway(prefix netip.Prefix) error {
     
     // addRouteToNonVPNIntf adds a new route to the routing table for the given prefix and returns the next hop and interface.
     // If the next hop or interface is pointing to the VPN interface, it will return the initial values.
    -func (r *SysOps) addRouteToNonVPNIntf(prefix netip.Prefix, vpnIntf iface.IWGIface, initialNextHop Nexthop) (Nexthop, error) {
    +func (r *SysOps) addRouteToNonVPNIntf(prefix netip.Prefix, vpnIntf iface.WGIface, initialNextHop Nexthop) (Nexthop, error) {
     	addr := prefix.Addr()
     	switch {
     	case addr.IsLoopback(),
    
    From a0b48f971c47025d813e5e567f659344daf9769c Mon Sep 17 00:00:00 2001
    From: Misha Bragin 
    Date: Fri, 21 Feb 2025 11:13:02 +0100
    Subject: [PATCH 79/92] Add K8s webinar to Readme
    
    ---
     README.md | 5 +++++
     1 file changed, 5 insertions(+)
    
    diff --git a/README.md b/README.md
    index 0537710e9..7cee2f8dc 100644
    --- a/README.md
    +++ b/README.md
    @@ -1,4 +1,9 @@
     
    + + Webinar: How to Achieve Zero Trust Access to Kubernetes — Effortlessly + +
    +

    From a854660402f68400ee1e0ab618e151c9bc7e67d6 Mon Sep 17 00:00:00 2001 From: Christian Stewart Date: Fri, 21 Feb 2025 03:02:50 -0800 Subject: [PATCH 80/92] [client, signal, management] Update google.golang.org/api to latest (#3288) * [misc] Add vendor/ to .gitignore Ignore the vendor/ tree created if someone runs "go mod vendor" Signed-off-by: Christian Stewart * [client, signal, management] Update google.golang.org/protobuf to latest Updating protobuf runtime library as a dependency of eventually updating google.golang.org/api in a future commit. Signed-off-by: Christian Stewart * [client, signal, management] Update google.golang.org/grpc to latest Updating grpc library as a dependency of eventually updating google.golang.org/api in a future commit. Signed-off-by: Christian Stewart * [client, signal, management] Update golang.org/x/net to latest Updating x/net library as a dependency of eventually updating google.golang.org/api in a future commit. Signed-off-by: Christian Stewart * [client, signal, management] Update golang.org/x/oauth2 to latest Updating x/oauth2 library as a dependency of eventually updating google.golang.org/api in a future commit. Signed-off-by: Christian Stewart * [client, signal, management] Update github.com/stretchr/testify to latest Updating testify library as a dependency of eventually updating google.golang.org/api in a future commit. Signed-off-by: Christian Stewart * [client, signal, management] Update opentelemetry to latest Updating otel library as a dependency of eventually updating google.golang.org/api in a future commit. Signed-off-by: Christian Stewart * [client, signal, management] Update golang.org/x/time to latest Updating x/time library as a dependency of eventually updating google.golang.org/api in a future commit. Signed-off-by: Christian Stewart * [management] Update google.golang.org/api to latest Updating google.golang.org/api library to fix indirect dependency issues with older versions of OpenTelemetry. See: #3240 Signed-off-by: Christian Stewart --------- Signed-off-by: Christian Stewart --- .gitignore | 1 + go.mod | 48 ++++++++++++------------ go.sum | 107 ++++++++++++++++++++++++++--------------------------- 3 files changed, 77 insertions(+), 79 deletions(-) diff --git a/.gitignore b/.gitignore index d0b4f82dd..abb728b19 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,4 @@ infrastructure_files/setup.env infrastructure_files/setup-*.env .vscode .DS_Store +vendor/ diff --git a/go.mod b/go.mod index 25e5dd1d2..3d71e8eb1 100644 --- a/go.mod +++ b/go.mod @@ -24,8 +24,8 @@ require ( golang.zx2c4.com/wireguard v0.0.0-20230704135630-469159ecf7d1 golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 golang.zx2c4.com/wireguard/windows v0.5.3 - google.golang.org/grpc v1.64.1 - google.golang.org/protobuf v1.34.2 + google.golang.org/grpc v1.70.0 + google.golang.org/protobuf v1.36.4 gopkg.in/natefinch/lumberjack.v2 v2.0.0 ) @@ -76,27 +76,27 @@ require ( github.com/shirou/gopsutil/v3 v3.24.4 github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 - github.com/stretchr/testify v1.9.0 + github.com/stretchr/testify v1.10.0 github.com/testcontainers/testcontainers-go v0.31.0 github.com/testcontainers/testcontainers-go/modules/mysql v0.31.0 github.com/testcontainers/testcontainers-go/modules/postgres v0.31.0 github.com/things-go/go-socks5 v0.0.4 github.com/yusufpapurcu/wmi v1.2.4 github.com/zcalusic/sysinfo v1.1.3 - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 - go.opentelemetry.io/otel v1.26.0 + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0 + go.opentelemetry.io/otel v1.34.0 go.opentelemetry.io/otel/exporters/prometheus v0.48.0 - go.opentelemetry.io/otel/metric v1.26.0 - go.opentelemetry.io/otel/sdk/metric v1.26.0 + go.opentelemetry.io/otel/metric v1.34.0 + go.opentelemetry.io/otel/sdk/metric v1.32.0 go.uber.org/zap v1.27.0 goauthentik.io/api/v3 v3.2023051.3 golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 golang.org/x/mobile v0.0.0-20231127183840-76ac6878050a - golang.org/x/net v0.33.0 - golang.org/x/oauth2 v0.19.0 + golang.org/x/net v0.34.0 + golang.org/x/oauth2 v0.26.0 golang.org/x/sync v0.10.0 golang.org/x/term v0.28.0 - google.golang.org/api v0.177.0 + google.golang.org/api v0.220.0 gopkg.in/yaml.v3 v3.0.1 gorm.io/driver/mysql v1.5.7 gorm.io/driver/postgres v1.5.7 @@ -106,9 +106,9 @@ require ( ) require ( - cloud.google.com/go/auth v0.3.0 // indirect - cloud.google.com/go/auth/oauth2adapt v0.2.2 // indirect - cloud.google.com/go/compute/metadata v0.3.0 // indirect + cloud.google.com/go/auth v0.14.1 // indirect + cloud.google.com/go/auth/oauth2adapt v0.2.7 // indirect + cloud.google.com/go/compute/metadata v0.6.0 // indirect dario.cat/mergo v1.0.0 // indirect filippo.io/edwards25519 v1.1.0 // indirect github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect @@ -151,7 +151,7 @@ require ( github.com/fyne-io/image v0.0.0-20220602074514-4956b0afb3d2 // indirect github.com/go-gl/gl v0.0.0-20211210172815-726fda9656d6 // indirect github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a // indirect - github.com/go-logr/logr v1.4.1 // indirect + github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.3.0 // indirect github.com/go-redis/redis/v8 v8.11.5 // indirect @@ -160,12 +160,11 @@ require ( github.com/go-text/render v0.2.0 // indirect github.com/go-text/typesetting v0.2.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/google/btree v1.1.2 // indirect github.com/google/pprof v0.0.0-20211214055906-6f57359322fd // indirect - github.com/google/s2a-go v0.1.7 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect - github.com/googleapis/gax-go/v2 v2.12.3 // indirect + github.com/google/s2a-go v0.1.9 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect + github.com/googleapis/gax-go/v2 v2.14.1 // indirect github.com/gopherjs/gopherjs v1.17.2 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-uuid v1.0.3 // indirect @@ -221,20 +220,19 @@ require ( github.com/vishvananda/netns v0.0.4 // indirect github.com/yuin/goldmark v1.7.1 // indirect github.com/zeebo/blake3 v0.2.3 // indirect - go.opencensus.io v0.24.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0 // indirect - go.opentelemetry.io/otel/sdk v1.26.0 // indirect - go.opentelemetry.io/otel/trace v1.26.0 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 // indirect + go.opentelemetry.io/otel/sdk v1.34.0 // indirect + go.opentelemetry.io/otel/trace v1.34.0 // indirect go.uber.org/mock v0.4.0 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/image v0.18.0 // indirect golang.org/x/mod v0.17.0 // indirect golang.org/x/text v0.21.0 // indirect - golang.org/x/time v0.5.0 // indirect + golang.org/x/time v0.10.0 // indirect golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240509183442-62759503f434 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250127172529-29210b9bc287 // indirect gopkg.in/square/go-jose.v2 v2.6.0 // indirect gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637 // indirect diff --git a/go.sum b/go.sum index 4057517d3..36bca22d3 100644 --- a/go.sum +++ b/go.sum @@ -18,10 +18,10 @@ cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmW cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= -cloud.google.com/go/auth v0.3.0 h1:PRyzEpGfx/Z9e8+lHsbkoUVXD0gnu4MNmm7Gp8TQNIs= -cloud.google.com/go/auth v0.3.0/go.mod h1:lBv6NKTWp8E3LPzmO1TbiiRKc4drLOfHsgmlH9ogv5w= -cloud.google.com/go/auth/oauth2adapt v0.2.2 h1:+TTV8aXpjeChS9M+aTtN/TjdQnzJvmzKFt//oWu7HX4= -cloud.google.com/go/auth/oauth2adapt v0.2.2/go.mod h1:wcYjgpZI9+Yu7LyYBg4pqSiaRkfEK3GQcpb7C/uyF1Q= +cloud.google.com/go/auth v0.14.1 h1:AwoJbzUdxA/whv1qj3TLKwh3XX5sikny2fc40wUl+h0= +cloud.google.com/go/auth v0.14.1/go.mod h1:4JHUxlGXisL0AW8kXPtUF6ztuOksyfUQNFjfsOCXkPM= +cloud.google.com/go/auth/oauth2adapt v0.2.7 h1:/Lc7xODdqcEw8IrZ9SvwnlLX6j9FHQM74z6cBk9Rw6M= +cloud.google.com/go/auth/oauth2adapt v0.2.7/go.mod h1:NTbTTzfvPl1Y3V1nPpOgl2w6d/FjO7NNUQaWSox6ZMc= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= @@ -29,8 +29,8 @@ cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUM cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= -cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc= -cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= +cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I= +cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= @@ -225,8 +225,8 @@ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a h1:vxnBhFDDT+ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= -github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= @@ -263,14 +263,12 @@ github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69 github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/glog v1.2.0 h1:uCdmnmatrKCgMBlM4rMuJZWOkPDqdbZPnrMXDY4gI68= -github.com/golang/glog v1.2.0/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= +github.com/golang/glog v1.2.3 h1:oDTdz9f5VGVVNGu/Q7UXKWYsD0873HXLHdJUNBsSEKM= +github.com/golang/glog v1.2.3/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= @@ -345,18 +343,18 @@ github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20211214055906-6f57359322fd h1:1FjCyPC+syAzJ5/2S8fqdZK1R22vvA0J7JZKcuOIQ7Y= github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= -github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= +github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= +github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs= -github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= +github.com/googleapis/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gTgghdIA6Stxb52D5RnLI1SLyw= +github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/googleapis/gax-go/v2 v2.12.3 h1:5/zPPDvw8Q1SuXjrqrZslrqT7dL/uJT2CQii/cLCKqA= -github.com/googleapis/gax-go/v2 v2.12.3/go.mod h1:AKloxT6GtNbaLm8QTNSidHUVsHYcBHwWRvkNFJUQcS4= +github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q= +github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA= github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= github.com/gopacket/gopacket v1.1.1 h1:zbx9F9d6A7sWNkFKrvMBZTfGgxFoY4NgUudFVVHMfcw= github.com/gopacket/gopacket v1.1.1/go.mod h1:HavMeONEl7W9036of9LbSWoonqhH7HA1+ZRO+rMIvFs= @@ -617,8 +615,8 @@ github.com/quic-go/quic-go v0.48.2 h1:wsKXZPeGWpMpCGSWqOcqpW2wZYic/8T3aqiOID0/KW github.com/quic-go/quic-go v0.48.2/go.mod h1:yBgs3rWBOADpga7F+jJsb6Ybg1LSYiQvwWlLX+/6HMs= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= -github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/rs/cors v1.8.0 h1:P2KMzcFwrPoSjkF1WLRPsp3UMLyql8L4v9hQpVeK5so= github.com/rs/cors v1.8.0/go.mod h1:EBwu+T5AvHOcXwvZIkQFjUN6s8Czyqw12GL/Y0tUyRM= github.com/rs/xid v1.3.0 h1:6NjYksEUlhurdVehpc7S7dk6DAmcKv8V9gG0FsVN2U4= @@ -683,11 +681,11 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/testcontainers/testcontainers-go v0.31.0 h1:W0VwIhcEVhRflwL9as3dhY6jXjVCA27AkmbnZ+UTh3U= github.com/testcontainers/testcontainers-go v0.31.0/go.mod h1:D2lAoA0zUFiSY+eAflqK5mcUx/A5hrrORaEQrd0SefI= @@ -739,28 +737,28 @@ go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= -go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= -go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 h1:4Pp6oUg3+e/6M4C0A/3kJ2VYa++dsWVTtGgLVj5xtHg= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0 h1:Xs2Ncz0gNihqu9iosIZ5SkBbWo5T8JhhLJFMQL1qmLI= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0/go.mod h1:vy+2G/6NvVMpwGX/NyLqcC41fxepnuKHk16E6IZUcJc= -go.opentelemetry.io/otel v1.26.0 h1:LQwgL5s/1W7YiiRwxf03QGnWLb2HW4pLiAhaA5cZXBs= -go.opentelemetry.io/otel v1.26.0/go.mod h1:UmLkJHUAidDval2EICqBMbnAd0/m2vmpf/dAM+fvFs4= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0 h1:PS8wXpbyaDJQ2VDHHncMe9Vct0Zn1fEjpsjrLxGJoSc= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0/go.mod h1:HDBUsEjOuRC0EzKZ1bSaRGZWUBAzo+MhAcUUORSr4D0= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q= +go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= +go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU= go.opentelemetry.io/otel/exporters/prometheus v0.48.0 h1:sBQe3VNGUjY9IKWQC6z2lNqa5iGbDSxhs60ABwK4y0s= go.opentelemetry.io/otel/exporters/prometheus v0.48.0/go.mod h1:DtrbMzoZWwQHyrQmCfLam5DZbnmorsGbOtTbYHycU5o= -go.opentelemetry.io/otel/metric v1.26.0 h1:7S39CLuY5Jgg9CrnA9HHiEjGMF/X2VHvoXGgSllRz30= -go.opentelemetry.io/otel/metric v1.26.0/go.mod h1:SY+rHOI4cEawI9a7N1A4nIg/nTQXe1ccCNWYOJUrpX4= -go.opentelemetry.io/otel/sdk v1.26.0 h1:Y7bumHf5tAiDlRYFmGqetNcLaVUZmh4iYfmGxtmz7F8= -go.opentelemetry.io/otel/sdk v1.26.0/go.mod h1:0p8MXpqLeJ0pzcszQQN4F0S5FVjBLgypeGSngLsmirs= -go.opentelemetry.io/otel/sdk/metric v1.26.0 h1:cWSks5tfriHPdWFnl+qpX3P681aAYqlZHcAyHw5aU9Y= -go.opentelemetry.io/otel/sdk/metric v1.26.0/go.mod h1:ClMFFknnThJCksebJwz7KIyEDHO+nTB6gK8obLy8RyE= -go.opentelemetry.io/otel/trace v1.26.0 h1:1ieeAUb4y0TE26jUFrCIXKpTuVK7uJGN9/Z/2LP5sQA= -go.opentelemetry.io/otel/trace v1.26.0/go.mod h1:4iDxvGDQuUkHve82hJJ8UqrwswHYsZuWCBllGV2U2y0= +go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= +go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= +go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= +go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= +go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU= +go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ= +go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= +go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= @@ -885,8 +883,8 @@ golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= -golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= -golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= +golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= +golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -900,8 +898,8 @@ golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE= -golang.org/x/oauth2 v0.19.0 h1:9+E/EZBCbTLNrbN35fHv/a/d/mOBatymz1zbtQrXpIg= -golang.org/x/oauth2 v0.19.0/go.mod h1:vYi7skDa1x015PmRRYZ7+s1cWyPgrPiSYRe4rnsexc8= +golang.org/x/oauth2 v0.26.0 h1:afQXWNNaeC4nvZ0Ed9XvCCzXM6UHJG7iCg0W4fPqSBE= +golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1019,8 +1017,8 @@ golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= -golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4= +golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -1114,8 +1112,8 @@ google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjR google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= google.golang.org/api v0.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8= -google.golang.org/api v0.177.0 h1:8a0p/BbPa65GlqGWtUKxot4p0TV8OGOfyTjtmkXNXmk= -google.golang.org/api v0.177.0/go.mod h1:srbhue4MLjkjbkux5p3dw/ocYOSZTaIEvf7bCOnFQDw= +google.golang.org/api v0.220.0 h1:3oMI4gdBgB72WFVwE1nerDD8W3HUOS4kypK6rRLbGns= +google.golang.org/api v0.220.0/go.mod h1:26ZAlY6aN/8WgpCzjPNy18QpYaz7Zgg1h0qe1GkZEmY= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -1164,10 +1162,11 @@ google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto/googleapis/api v0.0.0-20240509183442-62759503f434 h1:OpXbo8JnN8+jZGPrL4SSfaDjSCjupr8lXyBAbexEm/U= -google.golang.org/genproto/googleapis/api v0.0.0-20240509183442-62759503f434/go.mod h1:FfiGhwUm6CJviekPrc0oJ+7h29e+DmWU6UtjX0ZvI7Y= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= +google.golang.org/genproto v0.0.0-20240123012728-ef4313101c80 h1:KAeGQVN3M9nD0/bQXnr/ClcEMJ968gUXJQ9pwfSynuQ= +google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 h1:CkkIfIt50+lT6NHAVoRYEyAvQGFM7xEwXUUywFvEb3Q= +google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576/go.mod h1:1R3kvZ1dtP3+4p4d3G8uJ8rFk/fWlScl38vanWACI08= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250127172529-29210b9bc287 h1:J1H9f+LEdWAfHcez/4cvaVBox7cOYT+IU6rgqj5x++8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250127172529-29210b9bc287/go.mod h1:8BS3B93F/U1juMFq9+EDk+qOT5CO1R9IzXxG3PTqiRk= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -1188,8 +1187,8 @@ google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAG google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.64.1 h1:LKtvyfbX3UGVPFcGqJ9ItpVWW6oN/2XqTxfAnwRRXiA= -google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvyjeP0= +google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ= +google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -1204,8 +1203,8 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= -google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM= +google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= From 6554026a82bf796161aa0df243da6d5cf1be6b0d Mon Sep 17 00:00:00 2001 From: Maycon Santos Date: Fri, 21 Feb 2025 12:04:26 +0100 Subject: [PATCH 81/92] [client] fix client/Dockerfile to reduce vulnerabilities (#3359) The following vulnerabilities are fixed with an upgrade: - https://snyk.io/vuln/SNYK-ALPINE321-MUSL-8720634 - https://snyk.io/vuln/SNYK-ALPINE321-MUSL-8720634 - https://snyk.io/vuln/SNYK-ALPINE321-OPENSSL-8690014 - https://snyk.io/vuln/SNYK-ALPINE321-OPENSSL-8690014 - https://snyk.io/vuln/SNYK-ALPINE321-OPENSSL-8710358 Co-authored-by: snyk-bot --- client/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/Dockerfile b/client/Dockerfile index 2f5ff14ae..35c1d04c2 100644 --- a/client/Dockerfile +++ b/client/Dockerfile @@ -1,4 +1,4 @@ -FROM alpine:3.21.0 +FROM alpine:3.21.3 RUN apk add --no-cache ca-certificates iptables ip6tables ENV NB_FOREGROUND_MODE=true ENTRYPOINT [ "/usr/local/bin/netbird","up"] From 5134e3a06adf50700165984a6a4a0f67b7789a21 Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Fri, 21 Feb 2025 12:52:04 +0100 Subject: [PATCH 82/92] [client] Add reverse dns zone (#3217) --- client/internal/dns.go | 111 +++++++++++++++++++++++++++++ client/internal/dns/host.go | 8 ++- client/internal/dns/server.go | 10 +-- client/internal/dns/server_test.go | 8 ++- client/internal/engine.go | 11 ++- client/internal/engine_test.go | 15 ++++ 6 files changed, 153 insertions(+), 10 deletions(-) create mode 100644 client/internal/dns.go diff --git a/client/internal/dns.go b/client/internal/dns.go new file mode 100644 index 000000000..8a73f50f2 --- /dev/null +++ b/client/internal/dns.go @@ -0,0 +1,111 @@ +package internal + +import ( + "fmt" + "net" + "slices" + "strings" + + "github.com/miekg/dns" + log "github.com/sirupsen/logrus" + + nbdns "github.com/netbirdio/netbird/dns" +) + +func createPTRRecord(aRecord nbdns.SimpleRecord, ipNet *net.IPNet) (nbdns.SimpleRecord, bool) { + ip := net.ParseIP(aRecord.RData) + if ip == nil || ip.To4() == nil { + return nbdns.SimpleRecord{}, false + } + + if !ipNet.Contains(ip) { + return nbdns.SimpleRecord{}, false + } + + ipOctets := strings.Split(ip.String(), ".") + slices.Reverse(ipOctets) + rdnsName := dns.Fqdn(strings.Join(ipOctets, ".") + ".in-addr.arpa") + + return nbdns.SimpleRecord{ + Name: rdnsName, + Type: int(dns.TypePTR), + Class: aRecord.Class, + TTL: aRecord.TTL, + RData: dns.Fqdn(aRecord.Name), + }, true +} + +// generateReverseZoneName creates the reverse DNS zone name for a given network +func generateReverseZoneName(ipNet *net.IPNet) (string, error) { + networkIP := ipNet.IP.Mask(ipNet.Mask) + maskOnes, _ := ipNet.Mask.Size() + + // round up to nearest byte + octetsToUse := (maskOnes + 7) / 8 + + octets := strings.Split(networkIP.String(), ".") + if octetsToUse > len(octets) { + return "", fmt.Errorf("invalid network mask size for reverse DNS: %d", maskOnes) + } + + reverseOctets := make([]string, octetsToUse) + for i := 0; i < octetsToUse; i++ { + reverseOctets[octetsToUse-1-i] = octets[i] + } + + return dns.Fqdn(strings.Join(reverseOctets, ".") + ".in-addr.arpa"), nil +} + +// zoneExists checks if a zone with the given name already exists in the configuration +func zoneExists(config *nbdns.Config, zoneName string) bool { + for _, zone := range config.CustomZones { + if zone.Domain == zoneName { + log.Debugf("reverse DNS zone %s already exists", zoneName) + return true + } + } + return false +} + +// collectPTRRecords gathers all PTR records for the given network from A records +func collectPTRRecords(config *nbdns.Config, ipNet *net.IPNet) []nbdns.SimpleRecord { + var records []nbdns.SimpleRecord + + for _, zone := range config.CustomZones { + for _, record := range zone.Records { + if record.Type != int(dns.TypeA) { + continue + } + + if ptrRecord, ok := createPTRRecord(record, ipNet); ok { + records = append(records, ptrRecord) + } + } + } + + return records +} + +// addReverseZone adds a reverse DNS zone to the configuration for the given network +func addReverseZone(config *nbdns.Config, ipNet *net.IPNet) { + zoneName, err := generateReverseZoneName(ipNet) + if err != nil { + log.Warn(err) + return + } + + if zoneExists(config, zoneName) { + log.Debugf("reverse DNS zone %s already exists", zoneName) + return + } + + records := collectPTRRecords(config, ipNet) + + reverseZone := nbdns.CustomZone{ + Domain: zoneName, + Records: records, + } + + config.CustomZones = append(config.CustomZones, reverseZone) + log.Debugf("added reverse DNS zone: %s with %d records", zoneName, len(records)) +} diff --git a/client/internal/dns/host.go b/client/internal/dns/host.go index fbe8c4dbb..cfc0cc3c3 100644 --- a/client/internal/dns/host.go +++ b/client/internal/dns/host.go @@ -9,6 +9,11 @@ import ( nbdns "github.com/netbirdio/netbird/dns" ) +const ( + ipv4ReverseZone = ".in-addr.arpa" + ipv6ReverseZone = ".ip6.arpa" +) + type hostManager interface { applyDNSConfig(config HostDNSConfig, stateManager *statemanager.Manager) error restoreHostDNS() error @@ -94,9 +99,10 @@ func dnsConfigToHostDNSConfig(dnsConfig nbdns.Config, ip string, port int) HostD } for _, customZone := range dnsConfig.CustomZones { + matchOnly := strings.HasSuffix(customZone.Domain, ipv4ReverseZone) || strings.HasSuffix(customZone.Domain, ipv6ReverseZone) config.Domains = append(config.Domains, DomainConfig{ Domain: strings.TrimSuffix(customZone.Domain, "."), - MatchOnly: false, + MatchOnly: matchOnly, }) } diff --git a/client/internal/dns/server.go b/client/internal/dns/server.go index d4d68370d..f536a1434 100644 --- a/client/internal/dns/server.go +++ b/client/internal/dns/server.go @@ -395,12 +395,12 @@ func (s *DefaultServer) applyConfiguration(update nbdns.Config) error { localMuxUpdates, localRecordsByDomain, err := s.buildLocalHandlerUpdate(update.CustomZones) if err != nil { - return fmt.Errorf("not applying dns update, error: %v", err) + return fmt.Errorf("local handler updater: %w", err) } upstreamMuxUpdates, err := s.buildUpstreamHandlerUpdate(update.NameServerGroups) if err != nil { - return fmt.Errorf("not applying dns update, error: %v", err) + return fmt.Errorf("upstream handler updater: %w", err) } muxUpdates := append(localMuxUpdates, upstreamMuxUpdates...) //nolint:gocritic @@ -447,7 +447,8 @@ func (s *DefaultServer) buildLocalHandlerUpdate( for _, customZone := range customZones { if len(customZone.Records) == 0 { - return nil, nil, fmt.Errorf("received an empty list of records") + log.Warnf("received a custom zone with empty records, skipping domain: %s", customZone.Domain) + continue } muxUpdates = append(muxUpdates, handlerWrapper{ @@ -460,7 +461,8 @@ func (s *DefaultServer) buildLocalHandlerUpdate( for _, record := range customZone.Records { var class uint16 = dns.ClassINET if record.Class != nbdns.DefaultClass { - return nil, nil, fmt.Errorf("received an invalid class type: %s", record.Class) + log.Warnf("received an invalid class type: %s", record.Class) + continue } key := buildRecordKey(record.Name, class, uint16(record.Type)) diff --git a/client/internal/dns/server_test.go b/client/internal/dns/server_test.go index e9ddd5f59..1354462d9 100644 --- a/client/internal/dns/server_test.go +++ b/client/internal/dns/server_test.go @@ -266,7 +266,7 @@ func TestUpdateDNSServer(t *testing.T) { shouldFail: true, }, { - name: "Invalid Custom Zone Records list Should Fail", + name: "Invalid Custom Zone Records list Should Skip", initLocalMap: make(registrationMap), initUpstreamMap: make(registeredHandlerMap), initSerial: 0, @@ -285,7 +285,11 @@ func TestUpdateDNSServer(t *testing.T) { }, }, }, - shouldFail: true, + expectedUpstreamMap: registeredHandlerMap{generateDummyHandler(".", nameServers).id(): handlerWrapper{ + domain: ".", + handler: dummyHandler, + priority: PriorityDefault, + }}, }, { name: "Empty Config Should Succeed and Clean Maps", diff --git a/client/internal/engine.go b/client/internal/engine.go index 90f70a827..ebb68b98b 100644 --- a/client/internal/engine.go +++ b/client/internal/engine.go @@ -953,7 +953,7 @@ func (e *Engine) updateNetworkMap(networkMap *mgmProto.NetworkMap) error { protoDNSConfig = &mgmProto.DNSConfig{} } - if err := e.dnsServer.UpdateDNSServer(serial, toDNSConfig(protoDNSConfig)); err != nil { + if err := e.dnsServer.UpdateDNSServer(serial, toDNSConfig(protoDNSConfig, e.wgInterface.Address().Network)); err != nil { log.Errorf("failed to update dns server, err: %v", err) } @@ -1022,7 +1022,7 @@ func toRouteDomains(myPubKey string, protoRoutes []*mgmProto.Route) []string { return dnsRoutes } -func toDNSConfig(protoDNSConfig *mgmProto.DNSConfig) nbdns.Config { +func toDNSConfig(protoDNSConfig *mgmProto.DNSConfig, network *net.IPNet) nbdns.Config { dnsUpdate := nbdns.Config{ ServiceEnable: protoDNSConfig.GetServiceEnable(), CustomZones: make([]nbdns.CustomZone, 0), @@ -1062,6 +1062,11 @@ func toDNSConfig(protoDNSConfig *mgmProto.DNSConfig) nbdns.Config { } dnsUpdate.NameServerGroups = append(dnsUpdate.NameServerGroups, dnsNSGroup) } + + if len(dnsUpdate.CustomZones) > 0 { + addReverseZone(&dnsUpdate, network) + } + return dnsUpdate } @@ -1368,7 +1373,7 @@ func (e *Engine) readInitialSettings() ([]*route.Route, *nbdns.Config, error) { return nil, nil, err } routes := toRoutes(netMap.GetRoutes()) - dnsCfg := toDNSConfig(netMap.GetDNSConfig()) + dnsCfg := toDNSConfig(netMap.GetDNSConfig(), e.wgInterface.Address().Network) return routes, &dnsCfg, nil } diff --git a/client/internal/engine_test.go b/client/internal/engine_test.go index 2b1d3b098..599d36eab 100644 --- a/client/internal/engine_test.go +++ b/client/internal/engine_test.go @@ -361,6 +361,15 @@ func TestEngine_UpdateNetworkMap(t *testing.T) { RemovePeerFunc: func(peerKey string) error { return nil }, + AddressFunc: func() iface.WGAddress { + return iface.WGAddress{ + IP: net.ParseIP("10.20.0.1"), + Network: &net.IPNet{ + IP: net.ParseIP("10.20.0.0"), + Mask: net.IPv4Mask(255, 255, 255, 0), + }, + } + }, } engine.wgInterface = wgIface engine.routeManager = routemanager.NewManager(routemanager.ManagerConfig{ @@ -803,6 +812,9 @@ func TestEngine_UpdateNetworkMapWithDNSUpdate(t *testing.T) { }, }, }, + { + Domain: "0.66.100.in-addr.arpa.", + }, }, NameServerGroups: []*mgmtProto.NameServerGroup{ { @@ -832,6 +844,9 @@ func TestEngine_UpdateNetworkMapWithDNSUpdate(t *testing.T) { }, }, }, + { + Domain: "0.66.100.in-addr.arpa.", + }, }, expectedNSGroupsLen: 1, expectedNSGroups: []*nbdns.NameServerGroup{ From f00a997167fd4755673ec3559cf059b19d1b3930 Mon Sep 17 00:00:00 2001 From: Pedro Maia Costa <550684+pnmcosta@users.noreply.github.com> Date: Fri, 21 Feb 2025 14:17:42 +0000 Subject: [PATCH 83/92] [management] fix grpc new account (#3361) --- management/server/grpcserver.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/management/server/grpcserver.go b/management/server/grpcserver.go index 9d1bc1deb..3d170afa4 100644 --- a/management/server/grpcserver.go +++ b/management/server/grpcserver.go @@ -276,11 +276,16 @@ func (s *GRPCServer) validateToken(ctx context.Context, jwtToken string) (string } // we need to call this method because if user is new, we will automatically add it to existing or create a new account - _, _, err = s.accountManager.GetAccountIDFromUserAuth(ctx, userAuth) + accountId, _, err := s.accountManager.GetAccountIDFromUserAuth(ctx, userAuth) if err != nil { return "", status.Errorf(codes.Internal, "unable to fetch account with claims, err: %v", err) } + if userAuth.AccountId != accountId { + log.WithContext(ctx).Debugf("gRPC server sets accountId from ensure, before %s, now %s", userAuth.AccountId, accountId) + userAuth.AccountId = accountId + } + userAuth, err = s.authManager.EnsureUserAccessByJWTGroups(ctx, userAuth, token) if err != nil { return "", status.Error(codes.PermissionDenied, err.Error()) From b307298b2ff38816698db1a925d533e2bca95362 Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Fri, 21 Feb 2025 16:29:21 +0100 Subject: [PATCH 84/92] [client] Add netbird ui improvements (#3222) --- client/cmd/debug.go | 13 +- client/cmd/status.go | 714 +---------------- client/cmd/status_test.go | 596 -------------- client/internal/connect.go | 11 + client/internal/dns/file_unix.go | 6 +- client/internal/dns/host.go | 15 + client/internal/dns/host_android.go | 4 + client/internal/dns/host_darwin.go | 4 + client/internal/dns/host_ios.go | 4 + client/internal/dns/host_windows.go | 4 + client/internal/dns/network_manager_unix.go | 4 + client/internal/dns/resolvconf_unix.go | 6 +- client/internal/dns/server.go | 23 +- client/internal/dns/server_test.go | 14 +- client/internal/dns/systemd_linux.go | 4 + client/internal/dns/upstream.go | 19 +- client/internal/engine.go | 3 + client/internal/routemanager/client.go | 49 +- client/proto/daemon.pb.go | 203 ++--- client/proto/daemon.proto | 1 + client/server/network.go | 25 + .../{cmd/status_event.go => status/event.go} | 12 +- client/status/status.go | 725 ++++++++++++++++++ client/status/status_test.go | 603 +++++++++++++++ client/ui/bundled.go | 12 - client/ui/client_ui.go | 100 ++- client/ui/config/config.go | 46 -- client/ui/debug.go | 50 ++ client/ui/event/event.go | 41 +- client/ui/network.go | 202 ++++- 30 files changed, 1984 insertions(+), 1529 deletions(-) rename client/{cmd/status_event.go => status/event.go} (86%) create mode 100644 client/status/status.go create mode 100644 client/status/status_test.go delete mode 100644 client/ui/bundled.go delete mode 100644 client/ui/config/config.go create mode 100644 client/ui/debug.go diff --git a/client/cmd/debug.go b/client/cmd/debug.go index c7ab87b47..c02f60aed 100644 --- a/client/cmd/debug.go +++ b/client/cmd/debug.go @@ -13,6 +13,7 @@ import ( "github.com/netbirdio/netbird/client/internal" "github.com/netbirdio/netbird/client/proto" "github.com/netbirdio/netbird/client/server" + nbstatus "github.com/netbirdio/netbird/client/status" ) const errCloseConnection = "Failed to close connection: %v" @@ -85,7 +86,7 @@ func debugBundle(cmd *cobra.Command, _ []string) error { client := proto.NewDaemonServiceClient(conn) resp, err := client.DebugBundle(cmd.Context(), &proto.DebugBundleRequest{ Anonymize: anonymizeFlag, - Status: getStatusOutput(cmd), + Status: getStatusOutput(cmd, anonymizeFlag), SystemInfo: debugSystemInfoFlag, }) if err != nil { @@ -196,7 +197,7 @@ func runForDuration(cmd *cobra.Command, args []string) error { time.Sleep(3 * time.Second) headerPostUp := fmt.Sprintf("----- Netbird post-up - Timestamp: %s", time.Now().Format(time.RFC3339)) - statusOutput := fmt.Sprintf("%s\n%s", headerPostUp, getStatusOutput(cmd)) + statusOutput := fmt.Sprintf("%s\n%s", headerPostUp, getStatusOutput(cmd, anonymizeFlag)) if waitErr := waitForDurationOrCancel(cmd.Context(), duration, cmd); waitErr != nil { return waitErr @@ -206,7 +207,7 @@ func runForDuration(cmd *cobra.Command, args []string) error { cmd.Println("Creating debug bundle...") headerPreDown := fmt.Sprintf("----- Netbird pre-down - Timestamp: %s - Duration: %s", time.Now().Format(time.RFC3339), duration) - statusOutput = fmt.Sprintf("%s\n%s\n%s", statusOutput, headerPreDown, getStatusOutput(cmd)) + statusOutput = fmt.Sprintf("%s\n%s\n%s", statusOutput, headerPreDown, getStatusOutput(cmd, anonymizeFlag)) resp, err := client.DebugBundle(cmd.Context(), &proto.DebugBundleRequest{ Anonymize: anonymizeFlag, @@ -271,13 +272,15 @@ func setNetworkMapPersistence(cmd *cobra.Command, args []string) error { return nil } -func getStatusOutput(cmd *cobra.Command) string { +func getStatusOutput(cmd *cobra.Command, anon bool) string { var statusOutputString string statusResp, err := getStatus(cmd.Context()) if err != nil { cmd.PrintErrf("Failed to get status: %v\n", err) } else { - statusOutputString = parseToFullDetailSummary(convertToStatusOutputOverview(statusResp)) + statusOutputString = nbstatus.ParseToFullDetailSummary( + nbstatus.ConvertToStatusOutputOverview(statusResp, anon, "", nil, nil, nil), + ) } return statusOutputString } diff --git a/client/cmd/status.go b/client/cmd/status.go index bf4588ce4..0ddba8b2f 100644 --- a/client/cmd/status.go +++ b/client/cmd/status.go @@ -2,106 +2,20 @@ package cmd import ( "context" - "encoding/json" "fmt" "net" "net/netip" - "os" - "runtime" - "sort" "strings" - "time" "github.com/spf13/cobra" "google.golang.org/grpc/status" - "gopkg.in/yaml.v3" - "github.com/netbirdio/netbird/client/anonymize" "github.com/netbirdio/netbird/client/internal" - "github.com/netbirdio/netbird/client/internal/peer" "github.com/netbirdio/netbird/client/proto" + nbstatus "github.com/netbirdio/netbird/client/status" "github.com/netbirdio/netbird/util" - "github.com/netbirdio/netbird/version" ) -type peerStateDetailOutput struct { - FQDN string `json:"fqdn" yaml:"fqdn"` - IP string `json:"netbirdIp" yaml:"netbirdIp"` - PubKey string `json:"publicKey" yaml:"publicKey"` - Status string `json:"status" yaml:"status"` - LastStatusUpdate time.Time `json:"lastStatusUpdate" yaml:"lastStatusUpdate"` - ConnType string `json:"connectionType" yaml:"connectionType"` - IceCandidateType iceCandidateType `json:"iceCandidateType" yaml:"iceCandidateType"` - IceCandidateEndpoint iceCandidateType `json:"iceCandidateEndpoint" yaml:"iceCandidateEndpoint"` - RelayAddress string `json:"relayAddress" yaml:"relayAddress"` - LastWireguardHandshake time.Time `json:"lastWireguardHandshake" yaml:"lastWireguardHandshake"` - TransferReceived int64 `json:"transferReceived" yaml:"transferReceived"` - TransferSent int64 `json:"transferSent" yaml:"transferSent"` - Latency time.Duration `json:"latency" yaml:"latency"` - RosenpassEnabled bool `json:"quantumResistance" yaml:"quantumResistance"` - Networks []string `json:"networks" yaml:"networks"` -} - -type peersStateOutput struct { - Total int `json:"total" yaml:"total"` - Connected int `json:"connected" yaml:"connected"` - Details []peerStateDetailOutput `json:"details" yaml:"details"` -} - -type signalStateOutput struct { - URL string `json:"url" yaml:"url"` - Connected bool `json:"connected" yaml:"connected"` - Error string `json:"error" yaml:"error"` -} - -type managementStateOutput struct { - URL string `json:"url" yaml:"url"` - Connected bool `json:"connected" yaml:"connected"` - Error string `json:"error" yaml:"error"` -} - -type relayStateOutputDetail struct { - URI string `json:"uri" yaml:"uri"` - Available bool `json:"available" yaml:"available"` - Error string `json:"error" yaml:"error"` -} - -type relayStateOutput struct { - Total int `json:"total" yaml:"total"` - Available int `json:"available" yaml:"available"` - Details []relayStateOutputDetail `json:"details" yaml:"details"` -} - -type iceCandidateType struct { - Local string `json:"local" yaml:"local"` - Remote string `json:"remote" yaml:"remote"` -} - -type nsServerGroupStateOutput struct { - Servers []string `json:"servers" yaml:"servers"` - Domains []string `json:"domains" yaml:"domains"` - Enabled bool `json:"enabled" yaml:"enabled"` - Error string `json:"error" yaml:"error"` -} - -type statusOutputOverview struct { - Peers peersStateOutput `json:"peers" yaml:"peers"` - CliVersion string `json:"cliVersion" yaml:"cliVersion"` - DaemonVersion string `json:"daemonVersion" yaml:"daemonVersion"` - ManagementState managementStateOutput `json:"management" yaml:"management"` - SignalState signalStateOutput `json:"signal" yaml:"signal"` - Relays relayStateOutput `json:"relays" yaml:"relays"` - IP string `json:"netbirdIp" yaml:"netbirdIp"` - PubKey string `json:"publicKey" yaml:"publicKey"` - KernelInterface bool `json:"usesKernelInterface" yaml:"usesKernelInterface"` - FQDN string `json:"fqdn" yaml:"fqdn"` - RosenpassEnabled bool `json:"quantumResistance" yaml:"quantumResistance"` - RosenpassPermissive bool `json:"quantumResistancePermissive" yaml:"quantumResistancePermissive"` - Networks []string `json:"networks" yaml:"networks"` - NSServerGroups []nsServerGroupStateOutput `json:"dnsServers" yaml:"dnsServers"` - Events []systemEventOutput `json:"events" yaml:"events"` -} - var ( detailFlag bool ipv4Flag bool @@ -172,18 +86,17 @@ func statusFunc(cmd *cobra.Command, args []string) error { return nil } - outputInformationHolder := convertToStatusOutputOverview(resp) - + var outputInformationHolder = nbstatus.ConvertToStatusOutputOverview(resp, anonymizeFlag, statusFilter, prefixNamesFilter, prefixNamesFilterMap, ipsFilterMap) var statusOutputString string switch { case detailFlag: - statusOutputString = parseToFullDetailSummary(outputInformationHolder) + statusOutputString = nbstatus.ParseToFullDetailSummary(outputInformationHolder) case jsonFlag: - statusOutputString, err = parseToJSON(outputInformationHolder) + statusOutputString, err = nbstatus.ParseToJSON(outputInformationHolder) case yamlFlag: - statusOutputString, err = parseToYAML(outputInformationHolder) + statusOutputString, err = nbstatus.ParseToYAML(outputInformationHolder) default: - statusOutputString = parseGeneralSummary(outputInformationHolder, false, false, false) + statusOutputString = nbstatus.ParseGeneralSummary(outputInformationHolder, false, false, false) } if err != nil { @@ -213,7 +126,6 @@ func getStatus(ctx context.Context) (*proto.StatusResponse, error) { } func parseFilters() error { - switch strings.ToLower(statusFilter) { case "", "disconnected", "connected": if strings.ToLower(statusFilter) != "" { @@ -250,174 +162,6 @@ func enableDetailFlagWhenFilterFlag() { } } -func convertToStatusOutputOverview(resp *proto.StatusResponse) statusOutputOverview { - pbFullStatus := resp.GetFullStatus() - - managementState := pbFullStatus.GetManagementState() - managementOverview := managementStateOutput{ - URL: managementState.GetURL(), - Connected: managementState.GetConnected(), - Error: managementState.Error, - } - - signalState := pbFullStatus.GetSignalState() - signalOverview := signalStateOutput{ - URL: signalState.GetURL(), - Connected: signalState.GetConnected(), - Error: signalState.Error, - } - - relayOverview := mapRelays(pbFullStatus.GetRelays()) - peersOverview := mapPeers(resp.GetFullStatus().GetPeers()) - - overview := statusOutputOverview{ - Peers: peersOverview, - CliVersion: version.NetbirdVersion(), - DaemonVersion: resp.GetDaemonVersion(), - ManagementState: managementOverview, - SignalState: signalOverview, - Relays: relayOverview, - IP: pbFullStatus.GetLocalPeerState().GetIP(), - PubKey: pbFullStatus.GetLocalPeerState().GetPubKey(), - KernelInterface: pbFullStatus.GetLocalPeerState().GetKernelInterface(), - FQDN: pbFullStatus.GetLocalPeerState().GetFqdn(), - RosenpassEnabled: pbFullStatus.GetLocalPeerState().GetRosenpassEnabled(), - RosenpassPermissive: pbFullStatus.GetLocalPeerState().GetRosenpassPermissive(), - Networks: pbFullStatus.GetLocalPeerState().GetNetworks(), - NSServerGroups: mapNSGroups(pbFullStatus.GetDnsServers()), - Events: mapEvents(pbFullStatus.GetEvents()), - } - - if anonymizeFlag { - anonymizer := anonymize.NewAnonymizer(anonymize.DefaultAddresses()) - anonymizeOverview(anonymizer, &overview) - } - - return overview -} - -func mapRelays(relays []*proto.RelayState) relayStateOutput { - var relayStateDetail []relayStateOutputDetail - - var relaysAvailable int - for _, relay := range relays { - available := relay.GetAvailable() - relayStateDetail = append(relayStateDetail, - relayStateOutputDetail{ - URI: relay.URI, - Available: available, - Error: relay.GetError(), - }, - ) - - if available { - relaysAvailable++ - } - } - - return relayStateOutput{ - Total: len(relays), - Available: relaysAvailable, - Details: relayStateDetail, - } -} - -func mapNSGroups(servers []*proto.NSGroupState) []nsServerGroupStateOutput { - mappedNSGroups := make([]nsServerGroupStateOutput, 0, len(servers)) - for _, pbNsGroupServer := range servers { - mappedNSGroups = append(mappedNSGroups, nsServerGroupStateOutput{ - Servers: pbNsGroupServer.GetServers(), - Domains: pbNsGroupServer.GetDomains(), - Enabled: pbNsGroupServer.GetEnabled(), - Error: pbNsGroupServer.GetError(), - }) - } - return mappedNSGroups -} - -func mapPeers(peers []*proto.PeerState) peersStateOutput { - var peersStateDetail []peerStateDetailOutput - peersConnected := 0 - for _, pbPeerState := range peers { - localICE := "" - remoteICE := "" - localICEEndpoint := "" - remoteICEEndpoint := "" - relayServerAddress := "" - connType := "" - lastHandshake := time.Time{} - transferReceived := int64(0) - transferSent := int64(0) - - isPeerConnected := pbPeerState.ConnStatus == peer.StatusConnected.String() - if skipDetailByFilters(pbPeerState, isPeerConnected) { - continue - } - if isPeerConnected { - peersConnected++ - - localICE = pbPeerState.GetLocalIceCandidateType() - remoteICE = pbPeerState.GetRemoteIceCandidateType() - localICEEndpoint = pbPeerState.GetLocalIceCandidateEndpoint() - remoteICEEndpoint = pbPeerState.GetRemoteIceCandidateEndpoint() - connType = "P2P" - if pbPeerState.Relayed { - connType = "Relayed" - } - relayServerAddress = pbPeerState.GetRelayAddress() - lastHandshake = pbPeerState.GetLastWireguardHandshake().AsTime().Local() - transferReceived = pbPeerState.GetBytesRx() - transferSent = pbPeerState.GetBytesTx() - } - - timeLocal := pbPeerState.GetConnStatusUpdate().AsTime().Local() - peerState := peerStateDetailOutput{ - IP: pbPeerState.GetIP(), - PubKey: pbPeerState.GetPubKey(), - Status: pbPeerState.GetConnStatus(), - LastStatusUpdate: timeLocal, - ConnType: connType, - IceCandidateType: iceCandidateType{ - Local: localICE, - Remote: remoteICE, - }, - IceCandidateEndpoint: iceCandidateType{ - Local: localICEEndpoint, - Remote: remoteICEEndpoint, - }, - RelayAddress: relayServerAddress, - FQDN: pbPeerState.GetFqdn(), - LastWireguardHandshake: lastHandshake, - TransferReceived: transferReceived, - TransferSent: transferSent, - Latency: pbPeerState.GetLatency().AsDuration(), - RosenpassEnabled: pbPeerState.GetRosenpassEnabled(), - Networks: pbPeerState.GetNetworks(), - } - - peersStateDetail = append(peersStateDetail, peerState) - } - - sortPeersByIP(peersStateDetail) - - peersOverview := peersStateOutput{ - Total: len(peersStateDetail), - Connected: peersConnected, - Details: peersStateDetail, - } - return peersOverview -} - -func sortPeersByIP(peersStateDetail []peerStateDetailOutput) { - if len(peersStateDetail) > 0 { - sort.SliceStable(peersStateDetail, func(i, j int) bool { - iAddr, _ := netip.ParseAddr(peersStateDetail[i].IP) - jAddr, _ := netip.ParseAddr(peersStateDetail[j].IP) - return iAddr.Compare(jAddr) == -1 - }) - } -} - func parseInterfaceIP(interfaceIP string) string { ip, _, err := net.ParseCIDR(interfaceIP) if err != nil { @@ -425,449 +169,3 @@ func parseInterfaceIP(interfaceIP string) string { } return fmt.Sprintf("%s\n", ip) } - -func parseToJSON(overview statusOutputOverview) (string, error) { - jsonBytes, err := json.Marshal(overview) - if err != nil { - return "", fmt.Errorf("json marshal failed") - } - return string(jsonBytes), err -} - -func parseToYAML(overview statusOutputOverview) (string, error) { - yamlBytes, err := yaml.Marshal(overview) - if err != nil { - return "", fmt.Errorf("yaml marshal failed") - } - return string(yamlBytes), nil -} - -func parseGeneralSummary(overview statusOutputOverview, showURL bool, showRelays bool, showNameServers bool) string { - var managementConnString string - if overview.ManagementState.Connected { - managementConnString = "Connected" - if showURL { - managementConnString = fmt.Sprintf("%s to %s", managementConnString, overview.ManagementState.URL) - } - } else { - managementConnString = "Disconnected" - if overview.ManagementState.Error != "" { - managementConnString = fmt.Sprintf("%s, reason: %s", managementConnString, overview.ManagementState.Error) - } - } - - var signalConnString string - if overview.SignalState.Connected { - signalConnString = "Connected" - if showURL { - signalConnString = fmt.Sprintf("%s to %s", signalConnString, overview.SignalState.URL) - } - } else { - signalConnString = "Disconnected" - if overview.SignalState.Error != "" { - signalConnString = fmt.Sprintf("%s, reason: %s", signalConnString, overview.SignalState.Error) - } - } - - interfaceTypeString := "Userspace" - interfaceIP := overview.IP - if overview.KernelInterface { - interfaceTypeString = "Kernel" - } else if overview.IP == "" { - interfaceTypeString = "N/A" - interfaceIP = "N/A" - } - - var relaysString string - if showRelays { - for _, relay := range overview.Relays.Details { - available := "Available" - reason := "" - if !relay.Available { - available = "Unavailable" - reason = fmt.Sprintf(", reason: %s", relay.Error) - } - relaysString += fmt.Sprintf("\n [%s] is %s%s", relay.URI, available, reason) - } - } else { - relaysString = fmt.Sprintf("%d/%d Available", overview.Relays.Available, overview.Relays.Total) - } - - networks := "-" - if len(overview.Networks) > 0 { - sort.Strings(overview.Networks) - networks = strings.Join(overview.Networks, ", ") - } - - var dnsServersString string - if showNameServers { - for _, nsServerGroup := range overview.NSServerGroups { - enabled := "Available" - if !nsServerGroup.Enabled { - enabled = "Unavailable" - } - errorString := "" - if nsServerGroup.Error != "" { - errorString = fmt.Sprintf(", reason: %s", nsServerGroup.Error) - errorString = strings.TrimSpace(errorString) - } - - domainsString := strings.Join(nsServerGroup.Domains, ", ") - if domainsString == "" { - domainsString = "." // Show "." for the default zone - } - dnsServersString += fmt.Sprintf( - "\n [%s] for [%s] is %s%s", - strings.Join(nsServerGroup.Servers, ", "), - domainsString, - enabled, - errorString, - ) - } - } else { - dnsServersString = fmt.Sprintf("%d/%d Available", countEnabled(overview.NSServerGroups), len(overview.NSServerGroups)) - } - - rosenpassEnabledStatus := "false" - if overview.RosenpassEnabled { - rosenpassEnabledStatus = "true" - if overview.RosenpassPermissive { - rosenpassEnabledStatus = "true (permissive)" //nolint:gosec - } - } - - peersCountString := fmt.Sprintf("%d/%d Connected", overview.Peers.Connected, overview.Peers.Total) - - goos := runtime.GOOS - goarch := runtime.GOARCH - goarm := "" - if goarch == "arm" { - goarm = fmt.Sprintf(" (ARMv%s)", os.Getenv("GOARM")) - } - - summary := fmt.Sprintf( - "OS: %s\n"+ - "Daemon version: %s\n"+ - "CLI version: %s\n"+ - "Management: %s\n"+ - "Signal: %s\n"+ - "Relays: %s\n"+ - "Nameservers: %s\n"+ - "FQDN: %s\n"+ - "NetBird IP: %s\n"+ - "Interface type: %s\n"+ - "Quantum resistance: %s\n"+ - "Networks: %s\n"+ - "Peers count: %s\n", - fmt.Sprintf("%s/%s%s", goos, goarch, goarm), - overview.DaemonVersion, - version.NetbirdVersion(), - managementConnString, - signalConnString, - relaysString, - dnsServersString, - overview.FQDN, - interfaceIP, - interfaceTypeString, - rosenpassEnabledStatus, - networks, - peersCountString, - ) - return summary -} - -func parseToFullDetailSummary(overview statusOutputOverview) string { - parsedPeersString := parsePeers(overview.Peers, overview.RosenpassEnabled, overview.RosenpassPermissive) - parsedEventsString := parseEvents(overview.Events) - summary := parseGeneralSummary(overview, true, true, true) - - return fmt.Sprintf( - "Peers detail:"+ - "%s\n"+ - "Events:"+ - "%s\n"+ - "%s", - parsedPeersString, - parsedEventsString, - summary, - ) -} - -func parsePeers(peers peersStateOutput, rosenpassEnabled, rosenpassPermissive bool) string { - var ( - peersString = "" - ) - - for _, peerState := range peers.Details { - - localICE := "-" - if peerState.IceCandidateType.Local != "" { - localICE = peerState.IceCandidateType.Local - } - - remoteICE := "-" - if peerState.IceCandidateType.Remote != "" { - remoteICE = peerState.IceCandidateType.Remote - } - - localICEEndpoint := "-" - if peerState.IceCandidateEndpoint.Local != "" { - localICEEndpoint = peerState.IceCandidateEndpoint.Local - } - - remoteICEEndpoint := "-" - if peerState.IceCandidateEndpoint.Remote != "" { - remoteICEEndpoint = peerState.IceCandidateEndpoint.Remote - } - - rosenpassEnabledStatus := "false" - if rosenpassEnabled { - if peerState.RosenpassEnabled { - rosenpassEnabledStatus = "true" - } else { - if rosenpassPermissive { - rosenpassEnabledStatus = "false (remote didn't enable quantum resistance)" - } else { - rosenpassEnabledStatus = "false (connection won't work without a permissive mode)" - } - } - } else { - if peerState.RosenpassEnabled { - rosenpassEnabledStatus = "false (connection might not work without a remote permissive mode)" - } - } - - networks := "-" - if len(peerState.Networks) > 0 { - sort.Strings(peerState.Networks) - networks = strings.Join(peerState.Networks, ", ") - } - - peerString := fmt.Sprintf( - "\n %s:\n"+ - " NetBird IP: %s\n"+ - " Public key: %s\n"+ - " Status: %s\n"+ - " -- detail --\n"+ - " Connection type: %s\n"+ - " ICE candidate (Local/Remote): %s/%s\n"+ - " ICE candidate endpoints (Local/Remote): %s/%s\n"+ - " Relay server address: %s\n"+ - " Last connection update: %s\n"+ - " Last WireGuard handshake: %s\n"+ - " Transfer status (received/sent) %s/%s\n"+ - " Quantum resistance: %s\n"+ - " Networks: %s\n"+ - " Latency: %s\n", - peerState.FQDN, - peerState.IP, - peerState.PubKey, - peerState.Status, - peerState.ConnType, - localICE, - remoteICE, - localICEEndpoint, - remoteICEEndpoint, - peerState.RelayAddress, - timeAgo(peerState.LastStatusUpdate), - timeAgo(peerState.LastWireguardHandshake), - toIEC(peerState.TransferReceived), - toIEC(peerState.TransferSent), - rosenpassEnabledStatus, - networks, - peerState.Latency.String(), - ) - - peersString += peerString - } - return peersString -} - -func skipDetailByFilters(peerState *proto.PeerState, isConnected bool) bool { - statusEval := false - ipEval := false - nameEval := true - - if statusFilter != "" { - lowerStatusFilter := strings.ToLower(statusFilter) - if lowerStatusFilter == "disconnected" && isConnected { - statusEval = true - } else if lowerStatusFilter == "connected" && !isConnected { - statusEval = true - } - } - - if len(ipsFilter) > 0 { - _, ok := ipsFilterMap[peerState.IP] - if !ok { - ipEval = true - } - } - - if len(prefixNamesFilter) > 0 { - for prefixNameFilter := range prefixNamesFilterMap { - if strings.HasPrefix(peerState.Fqdn, prefixNameFilter) { - nameEval = false - break - } - } - } else { - nameEval = false - } - - return statusEval || ipEval || nameEval -} - -func toIEC(b int64) string { - const unit = 1024 - if b < unit { - return fmt.Sprintf("%d B", b) - } - div, exp := int64(unit), 0 - for n := b / unit; n >= unit; n /= unit { - div *= unit - exp++ - } - return fmt.Sprintf("%.1f %ciB", - float64(b)/float64(div), "KMGTPE"[exp]) -} - -func countEnabled(dnsServers []nsServerGroupStateOutput) int { - count := 0 - for _, server := range dnsServers { - if server.Enabled { - count++ - } - } - return count -} - -// timeAgo returns a string representing the duration since the provided time in a human-readable format. -func timeAgo(t time.Time) string { - if t.IsZero() || t.Equal(time.Unix(0, 0)) { - return "-" - } - duration := time.Since(t) - switch { - case duration < time.Second: - return "Now" - case duration < time.Minute: - seconds := int(duration.Seconds()) - if seconds == 1 { - return "1 second ago" - } - return fmt.Sprintf("%d seconds ago", seconds) - case duration < time.Hour: - minutes := int(duration.Minutes()) - seconds := int(duration.Seconds()) % 60 - if minutes == 1 { - if seconds == 1 { - return "1 minute, 1 second ago" - } else if seconds > 0 { - return fmt.Sprintf("1 minute, %d seconds ago", seconds) - } - return "1 minute ago" - } - if seconds > 0 { - return fmt.Sprintf("%d minutes, %d seconds ago", minutes, seconds) - } - return fmt.Sprintf("%d minutes ago", minutes) - case duration < 24*time.Hour: - hours := int(duration.Hours()) - minutes := int(duration.Minutes()) % 60 - if hours == 1 { - if minutes == 1 { - return "1 hour, 1 minute ago" - } else if minutes > 0 { - return fmt.Sprintf("1 hour, %d minutes ago", minutes) - } - return "1 hour ago" - } - if minutes > 0 { - return fmt.Sprintf("%d hours, %d minutes ago", hours, minutes) - } - return fmt.Sprintf("%d hours ago", hours) - } - - days := int(duration.Hours()) / 24 - hours := int(duration.Hours()) % 24 - if days == 1 { - if hours == 1 { - return "1 day, 1 hour ago" - } else if hours > 0 { - return fmt.Sprintf("1 day, %d hours ago", hours) - } - return "1 day ago" - } - if hours > 0 { - return fmt.Sprintf("%d days, %d hours ago", days, hours) - } - return fmt.Sprintf("%d days ago", days) -} - -func anonymizePeerDetail(a *anonymize.Anonymizer, peer *peerStateDetailOutput) { - peer.FQDN = a.AnonymizeDomain(peer.FQDN) - if localIP, port, err := net.SplitHostPort(peer.IceCandidateEndpoint.Local); err == nil { - peer.IceCandidateEndpoint.Local = fmt.Sprintf("%s:%s", a.AnonymizeIPString(localIP), port) - } - if remoteIP, port, err := net.SplitHostPort(peer.IceCandidateEndpoint.Remote); err == nil { - peer.IceCandidateEndpoint.Remote = fmt.Sprintf("%s:%s", a.AnonymizeIPString(remoteIP), port) - } - - peer.RelayAddress = a.AnonymizeURI(peer.RelayAddress) - - for i, route := range peer.Networks { - peer.Networks[i] = a.AnonymizeIPString(route) - } - - for i, route := range peer.Networks { - peer.Networks[i] = a.AnonymizeRoute(route) - } -} - -func anonymizeOverview(a *anonymize.Anonymizer, overview *statusOutputOverview) { - for i, peer := range overview.Peers.Details { - peer := peer - anonymizePeerDetail(a, &peer) - overview.Peers.Details[i] = peer - } - - overview.ManagementState.URL = a.AnonymizeURI(overview.ManagementState.URL) - overview.ManagementState.Error = a.AnonymizeString(overview.ManagementState.Error) - overview.SignalState.URL = a.AnonymizeURI(overview.SignalState.URL) - overview.SignalState.Error = a.AnonymizeString(overview.SignalState.Error) - - overview.IP = a.AnonymizeIPString(overview.IP) - for i, detail := range overview.Relays.Details { - detail.URI = a.AnonymizeURI(detail.URI) - detail.Error = a.AnonymizeString(detail.Error) - overview.Relays.Details[i] = detail - } - - for i, nsGroup := range overview.NSServerGroups { - for j, domain := range nsGroup.Domains { - overview.NSServerGroups[i].Domains[j] = a.AnonymizeDomain(domain) - } - for j, ns := range nsGroup.Servers { - host, port, err := net.SplitHostPort(ns) - if err == nil { - overview.NSServerGroups[i].Servers[j] = fmt.Sprintf("%s:%s", a.AnonymizeIPString(host), port) - } - } - } - - for i, route := range overview.Networks { - overview.Networks[i] = a.AnonymizeRoute(route) - } - - overview.FQDN = a.AnonymizeDomain(overview.FQDN) - - for i, event := range overview.Events { - overview.Events[i].Message = a.AnonymizeString(event.Message) - overview.Events[i].UserMessage = a.AnonymizeString(event.UserMessage) - - for k, v := range event.Metadata { - event.Metadata[k] = a.AnonymizeString(v) - } - } -} diff --git a/client/cmd/status_test.go b/client/cmd/status_test.go index 1e240d192..03608eab0 100644 --- a/client/cmd/status_test.go +++ b/client/cmd/status_test.go @@ -1,579 +1,11 @@ package cmd import ( - "bytes" - "encoding/json" - "fmt" - "runtime" "testing" - "time" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "google.golang.org/protobuf/types/known/durationpb" - "google.golang.org/protobuf/types/known/timestamppb" - - "github.com/netbirdio/netbird/client/proto" - "github.com/netbirdio/netbird/version" ) -func init() { - loc, err := time.LoadLocation("UTC") - if err != nil { - panic(err) - } - - time.Local = loc -} - -var resp = &proto.StatusResponse{ - Status: "Connected", - FullStatus: &proto.FullStatus{ - Peers: []*proto.PeerState{ - { - IP: "192.168.178.101", - PubKey: "Pubkey1", - Fqdn: "peer-1.awesome-domain.com", - ConnStatus: "Connected", - ConnStatusUpdate: timestamppb.New(time.Date(2001, time.Month(1), 1, 1, 1, 1, 0, time.UTC)), - Relayed: false, - LocalIceCandidateType: "", - RemoteIceCandidateType: "", - LocalIceCandidateEndpoint: "", - RemoteIceCandidateEndpoint: "", - LastWireguardHandshake: timestamppb.New(time.Date(2001, time.Month(1), 1, 1, 1, 2, 0, time.UTC)), - BytesRx: 200, - BytesTx: 100, - Networks: []string{ - "10.1.0.0/24", - }, - Latency: durationpb.New(time.Duration(10000000)), - }, - { - IP: "192.168.178.102", - PubKey: "Pubkey2", - Fqdn: "peer-2.awesome-domain.com", - ConnStatus: "Connected", - ConnStatusUpdate: timestamppb.New(time.Date(2002, time.Month(2), 2, 2, 2, 2, 0, time.UTC)), - Relayed: true, - LocalIceCandidateType: "relay", - RemoteIceCandidateType: "prflx", - LocalIceCandidateEndpoint: "10.0.0.1:10001", - RemoteIceCandidateEndpoint: "10.0.10.1:10002", - LastWireguardHandshake: timestamppb.New(time.Date(2002, time.Month(2), 2, 2, 2, 3, 0, time.UTC)), - BytesRx: 2000, - BytesTx: 1000, - Latency: durationpb.New(time.Duration(10000000)), - }, - }, - ManagementState: &proto.ManagementState{ - URL: "my-awesome-management.com:443", - Connected: true, - Error: "", - }, - SignalState: &proto.SignalState{ - URL: "my-awesome-signal.com:443", - Connected: true, - Error: "", - }, - Relays: []*proto.RelayState{ - { - URI: "stun:my-awesome-stun.com:3478", - Available: true, - Error: "", - }, - { - URI: "turns:my-awesome-turn.com:443?transport=tcp", - Available: false, - Error: "context: deadline exceeded", - }, - }, - LocalPeerState: &proto.LocalPeerState{ - IP: "192.168.178.100/16", - PubKey: "Some-Pub-Key", - KernelInterface: true, - Fqdn: "some-localhost.awesome-domain.com", - Networks: []string{ - "10.10.0.0/24", - }, - }, - DnsServers: []*proto.NSGroupState{ - { - Servers: []string{ - "8.8.8.8:53", - }, - Domains: nil, - Enabled: true, - Error: "", - }, - { - Servers: []string{ - "1.1.1.1:53", - "2.2.2.2:53", - }, - Domains: []string{ - "example.com", - "example.net", - }, - Enabled: false, - Error: "timeout", - }, - }, - }, - DaemonVersion: "0.14.1", -} - -var overview = statusOutputOverview{ - Peers: peersStateOutput{ - Total: 2, - Connected: 2, - Details: []peerStateDetailOutput{ - { - IP: "192.168.178.101", - PubKey: "Pubkey1", - FQDN: "peer-1.awesome-domain.com", - Status: "Connected", - LastStatusUpdate: time.Date(2001, 1, 1, 1, 1, 1, 0, time.UTC), - ConnType: "P2P", - IceCandidateType: iceCandidateType{ - Local: "", - Remote: "", - }, - IceCandidateEndpoint: iceCandidateType{ - Local: "", - Remote: "", - }, - LastWireguardHandshake: time.Date(2001, 1, 1, 1, 1, 2, 0, time.UTC), - TransferReceived: 200, - TransferSent: 100, - Networks: []string{ - "10.1.0.0/24", - }, - Latency: time.Duration(10000000), - }, - { - IP: "192.168.178.102", - PubKey: "Pubkey2", - FQDN: "peer-2.awesome-domain.com", - Status: "Connected", - LastStatusUpdate: time.Date(2002, 2, 2, 2, 2, 2, 0, time.UTC), - ConnType: "Relayed", - IceCandidateType: iceCandidateType{ - Local: "relay", - Remote: "prflx", - }, - IceCandidateEndpoint: iceCandidateType{ - Local: "10.0.0.1:10001", - Remote: "10.0.10.1:10002", - }, - LastWireguardHandshake: time.Date(2002, 2, 2, 2, 2, 3, 0, time.UTC), - TransferReceived: 2000, - TransferSent: 1000, - Latency: time.Duration(10000000), - }, - }, - }, - Events: []systemEventOutput{}, - CliVersion: version.NetbirdVersion(), - DaemonVersion: "0.14.1", - ManagementState: managementStateOutput{ - URL: "my-awesome-management.com:443", - Connected: true, - Error: "", - }, - SignalState: signalStateOutput{ - URL: "my-awesome-signal.com:443", - Connected: true, - Error: "", - }, - Relays: relayStateOutput{ - Total: 2, - Available: 1, - Details: []relayStateOutputDetail{ - { - URI: "stun:my-awesome-stun.com:3478", - Available: true, - Error: "", - }, - { - URI: "turns:my-awesome-turn.com:443?transport=tcp", - Available: false, - Error: "context: deadline exceeded", - }, - }, - }, - IP: "192.168.178.100/16", - PubKey: "Some-Pub-Key", - KernelInterface: true, - FQDN: "some-localhost.awesome-domain.com", - NSServerGroups: []nsServerGroupStateOutput{ - { - Servers: []string{ - "8.8.8.8:53", - }, - Domains: nil, - Enabled: true, - Error: "", - }, - { - Servers: []string{ - "1.1.1.1:53", - "2.2.2.2:53", - }, - Domains: []string{ - "example.com", - "example.net", - }, - Enabled: false, - Error: "timeout", - }, - }, - Networks: []string{ - "10.10.0.0/24", - }, -} - -func TestConversionFromFullStatusToOutputOverview(t *testing.T) { - convertedResult := convertToStatusOutputOverview(resp) - - assert.Equal(t, overview, convertedResult) -} - -func TestSortingOfPeers(t *testing.T) { - peers := []peerStateDetailOutput{ - { - IP: "192.168.178.104", - }, - { - IP: "192.168.178.102", - }, - { - IP: "192.168.178.101", - }, - { - IP: "192.168.178.105", - }, - { - IP: "192.168.178.103", - }, - } - - sortPeersByIP(peers) - - assert.Equal(t, peers[3].IP, "192.168.178.104") -} - -func TestParsingToJSON(t *testing.T) { - jsonString, _ := parseToJSON(overview) - - //@formatter:off - expectedJSONString := ` - { - "peers": { - "total": 2, - "connected": 2, - "details": [ - { - "fqdn": "peer-1.awesome-domain.com", - "netbirdIp": "192.168.178.101", - "publicKey": "Pubkey1", - "status": "Connected", - "lastStatusUpdate": "2001-01-01T01:01:01Z", - "connectionType": "P2P", - "iceCandidateType": { - "local": "", - "remote": "" - }, - "iceCandidateEndpoint": { - "local": "", - "remote": "" - }, - "relayAddress": "", - "lastWireguardHandshake": "2001-01-01T01:01:02Z", - "transferReceived": 200, - "transferSent": 100, - "latency": 10000000, - "quantumResistance": false, - "networks": [ - "10.1.0.0/24" - ] - }, - { - "fqdn": "peer-2.awesome-domain.com", - "netbirdIp": "192.168.178.102", - "publicKey": "Pubkey2", - "status": "Connected", - "lastStatusUpdate": "2002-02-02T02:02:02Z", - "connectionType": "Relayed", - "iceCandidateType": { - "local": "relay", - "remote": "prflx" - }, - "iceCandidateEndpoint": { - "local": "10.0.0.1:10001", - "remote": "10.0.10.1:10002" - }, - "relayAddress": "", - "lastWireguardHandshake": "2002-02-02T02:02:03Z", - "transferReceived": 2000, - "transferSent": 1000, - "latency": 10000000, - "quantumResistance": false, - "networks": null - } - ] - }, - "cliVersion": "development", - "daemonVersion": "0.14.1", - "management": { - "url": "my-awesome-management.com:443", - "connected": true, - "error": "" - }, - "signal": { - "url": "my-awesome-signal.com:443", - "connected": true, - "error": "" - }, - "relays": { - "total": 2, - "available": 1, - "details": [ - { - "uri": "stun:my-awesome-stun.com:3478", - "available": true, - "error": "" - }, - { - "uri": "turns:my-awesome-turn.com:443?transport=tcp", - "available": false, - "error": "context: deadline exceeded" - } - ] - }, - "netbirdIp": "192.168.178.100/16", - "publicKey": "Some-Pub-Key", - "usesKernelInterface": true, - "fqdn": "some-localhost.awesome-domain.com", - "quantumResistance": false, - "quantumResistancePermissive": false, - "networks": [ - "10.10.0.0/24" - ], - "dnsServers": [ - { - "servers": [ - "8.8.8.8:53" - ], - "domains": null, - "enabled": true, - "error": "" - }, - { - "servers": [ - "1.1.1.1:53", - "2.2.2.2:53" - ], - "domains": [ - "example.com", - "example.net" - ], - "enabled": false, - "error": "timeout" - } - ], - "events": [] - }` - // @formatter:on - - var expectedJSON bytes.Buffer - require.NoError(t, json.Compact(&expectedJSON, []byte(expectedJSONString))) - - assert.Equal(t, expectedJSON.String(), jsonString) -} - -func TestParsingToYAML(t *testing.T) { - yaml, _ := parseToYAML(overview) - - expectedYAML := - `peers: - total: 2 - connected: 2 - details: - - fqdn: peer-1.awesome-domain.com - netbirdIp: 192.168.178.101 - publicKey: Pubkey1 - status: Connected - lastStatusUpdate: 2001-01-01T01:01:01Z - connectionType: P2P - iceCandidateType: - local: "" - remote: "" - iceCandidateEndpoint: - local: "" - remote: "" - relayAddress: "" - lastWireguardHandshake: 2001-01-01T01:01:02Z - transferReceived: 200 - transferSent: 100 - latency: 10ms - quantumResistance: false - networks: - - 10.1.0.0/24 - - fqdn: peer-2.awesome-domain.com - netbirdIp: 192.168.178.102 - publicKey: Pubkey2 - status: Connected - lastStatusUpdate: 2002-02-02T02:02:02Z - connectionType: Relayed - iceCandidateType: - local: relay - remote: prflx - iceCandidateEndpoint: - local: 10.0.0.1:10001 - remote: 10.0.10.1:10002 - relayAddress: "" - lastWireguardHandshake: 2002-02-02T02:02:03Z - transferReceived: 2000 - transferSent: 1000 - latency: 10ms - quantumResistance: false - networks: [] -cliVersion: development -daemonVersion: 0.14.1 -management: - url: my-awesome-management.com:443 - connected: true - error: "" -signal: - url: my-awesome-signal.com:443 - connected: true - error: "" -relays: - total: 2 - available: 1 - details: - - uri: stun:my-awesome-stun.com:3478 - available: true - error: "" - - uri: turns:my-awesome-turn.com:443?transport=tcp - available: false - error: 'context: deadline exceeded' -netbirdIp: 192.168.178.100/16 -publicKey: Some-Pub-Key -usesKernelInterface: true -fqdn: some-localhost.awesome-domain.com -quantumResistance: false -quantumResistancePermissive: false -networks: - - 10.10.0.0/24 -dnsServers: - - servers: - - 8.8.8.8:53 - domains: [] - enabled: true - error: "" - - servers: - - 1.1.1.1:53 - - 2.2.2.2:53 - domains: - - example.com - - example.net - enabled: false - error: timeout -events: [] -` - - assert.Equal(t, expectedYAML, yaml) -} - -func TestParsingToDetail(t *testing.T) { - // Calculate time ago based on the fixture dates - lastConnectionUpdate1 := timeAgo(overview.Peers.Details[0].LastStatusUpdate) - lastHandshake1 := timeAgo(overview.Peers.Details[0].LastWireguardHandshake) - lastConnectionUpdate2 := timeAgo(overview.Peers.Details[1].LastStatusUpdate) - lastHandshake2 := timeAgo(overview.Peers.Details[1].LastWireguardHandshake) - - detail := parseToFullDetailSummary(overview) - - expectedDetail := fmt.Sprintf( - `Peers detail: - peer-1.awesome-domain.com: - NetBird IP: 192.168.178.101 - Public key: Pubkey1 - Status: Connected - -- detail -- - Connection type: P2P - ICE candidate (Local/Remote): -/- - ICE candidate endpoints (Local/Remote): -/- - Relay server address: - Last connection update: %s - Last WireGuard handshake: %s - Transfer status (received/sent) 200 B/100 B - Quantum resistance: false - Networks: 10.1.0.0/24 - Latency: 10ms - - peer-2.awesome-domain.com: - NetBird IP: 192.168.178.102 - Public key: Pubkey2 - Status: Connected - -- detail -- - Connection type: Relayed - ICE candidate (Local/Remote): relay/prflx - ICE candidate endpoints (Local/Remote): 10.0.0.1:10001/10.0.10.1:10002 - Relay server address: - Last connection update: %s - Last WireGuard handshake: %s - Transfer status (received/sent) 2.0 KiB/1000 B - Quantum resistance: false - Networks: - - Latency: 10ms - -Events: No events recorded -OS: %s/%s -Daemon version: 0.14.1 -CLI version: %s -Management: Connected to my-awesome-management.com:443 -Signal: Connected to my-awesome-signal.com:443 -Relays: - [stun:my-awesome-stun.com:3478] is Available - [turns:my-awesome-turn.com:443?transport=tcp] is Unavailable, reason: context: deadline exceeded -Nameservers: - [8.8.8.8:53] for [.] is Available - [1.1.1.1:53, 2.2.2.2:53] for [example.com, example.net] is Unavailable, reason: timeout -FQDN: some-localhost.awesome-domain.com -NetBird IP: 192.168.178.100/16 -Interface type: Kernel -Quantum resistance: false -Networks: 10.10.0.0/24 -Peers count: 2/2 Connected -`, lastConnectionUpdate1, lastHandshake1, lastConnectionUpdate2, lastHandshake2, runtime.GOOS, runtime.GOARCH, overview.CliVersion) - - assert.Equal(t, expectedDetail, detail) -} - -func TestParsingToShortVersion(t *testing.T) { - shortVersion := parseGeneralSummary(overview, false, false, false) - - expectedString := fmt.Sprintf("OS: %s/%s", runtime.GOOS, runtime.GOARCH) + ` -Daemon version: 0.14.1 -CLI version: development -Management: Connected -Signal: Connected -Relays: 1/2 Available -Nameservers: 1/2 Available -FQDN: some-localhost.awesome-domain.com -NetBird IP: 192.168.178.100/16 -Interface type: Kernel -Quantum resistance: false -Networks: 10.10.0.0/24 -Peers count: 2/2 Connected -` - - assert.Equal(t, expectedString, shortVersion) -} - func TestParsingOfIP(t *testing.T) { InterfaceIP := "192.168.178.123/16" @@ -581,31 +13,3 @@ func TestParsingOfIP(t *testing.T) { assert.Equal(t, "192.168.178.123\n", parsedIP) } - -func TestTimeAgo(t *testing.T) { - now := time.Now() - - cases := []struct { - name string - input time.Time - expected string - }{ - {"Now", now, "Now"}, - {"Seconds ago", now.Add(-10 * time.Second), "10 seconds ago"}, - {"One minute ago", now.Add(-1 * time.Minute), "1 minute ago"}, - {"Minutes and seconds ago", now.Add(-(1*time.Minute + 30*time.Second)), "1 minute, 30 seconds ago"}, - {"One hour ago", now.Add(-1 * time.Hour), "1 hour ago"}, - {"Hours and minutes ago", now.Add(-(2*time.Hour + 15*time.Minute)), "2 hours, 15 minutes ago"}, - {"One day ago", now.Add(-24 * time.Hour), "1 day ago"}, - {"Multiple days ago", now.Add(-(72*time.Hour + 20*time.Minute)), "3 days ago"}, - {"Zero time", time.Time{}, "-"}, - {"Unix zero time", time.Unix(0, 0), "-"}, - } - - for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - result := timeAgo(tc.input) - assert.Equal(t, tc.expected, result, "Failed %s", tc.name) - }) - } -} diff --git a/client/internal/connect.go b/client/internal/connect.go index 26ae3b687..bf513ed39 100644 --- a/client/internal/connect.go +++ b/client/internal/connect.go @@ -23,6 +23,7 @@ import ( "github.com/netbirdio/netbird/client/internal/listener" "github.com/netbirdio/netbird/client/internal/peer" "github.com/netbirdio/netbird/client/internal/stdnet" + cProto "github.com/netbirdio/netbird/client/proto" "github.com/netbirdio/netbird/client/ssh" "github.com/netbirdio/netbird/client/system" mgm "github.com/netbirdio/netbird/management/client" @@ -104,6 +105,16 @@ func (c *ConnectClient) RunOniOS( func (c *ConnectClient) run(mobileDependency MobileDependency, runningChan chan error) error { defer func() { if r := recover(); r != nil { + rec := c.statusRecorder + if rec != nil { + rec.PublishEvent( + cProto.SystemEvent_CRITICAL, cProto.SystemEvent_SYSTEM, + "panic occurred", + "The Netbird service panicked. Please restart the service and submit a bug report with the client logs.", + nil, + ) + } + log.Panicf("Panic occurred: %v, stack trace: %s", r, string(debug.Stack())) } }() diff --git a/client/internal/dns/file_unix.go b/client/internal/dns/file_unix.go index 02ae26e10..1f4ddb67c 100644 --- a/client/internal/dns/file_unix.go +++ b/client/internal/dns/file_unix.go @@ -58,7 +58,7 @@ func (f *fileConfigurator) applyDNSConfig(config HostDNSConfig, stateManager *st return fmt.Errorf("restoring the original resolv.conf file return err: %w", err) } } - return fmt.Errorf("unable to configure DNS for this peer using file manager without a nameserver group with all domains configured") + return ErrRouteAllWithoutNameserverGroup } if !backupFileExist { @@ -121,6 +121,10 @@ func (f *fileConfigurator) restoreHostDNS() error { return f.restore() } +func (f *fileConfigurator) string() string { + return "file" +} + func (f *fileConfigurator) backup() error { stats, err := os.Stat(defaultResolvConfPath) if err != nil { diff --git a/client/internal/dns/host.go b/client/internal/dns/host.go index cfc0cc3c3..25e9ff7e5 100644 --- a/client/internal/dns/host.go +++ b/client/internal/dns/host.go @@ -9,6 +9,8 @@ import ( nbdns "github.com/netbirdio/netbird/dns" ) +var ErrRouteAllWithoutNameserverGroup = fmt.Errorf("unable to configure DNS for this peer using file manager without a nameserver group with all domains configured") + const ( ipv4ReverseZone = ".in-addr.arpa" ipv6ReverseZone = ".ip6.arpa" @@ -18,6 +20,7 @@ type hostManager interface { applyDNSConfig(config HostDNSConfig, stateManager *statemanager.Manager) error restoreHostDNS() error supportCustomPort() bool + string() string } type SystemDNSSettings struct { @@ -44,6 +47,7 @@ type mockHostConfigurator struct { restoreHostDNSFunc func() error supportCustomPortFunc func() bool restoreUncleanShutdownDNSFunc func(*netip.Addr) error + stringFunc func() string } func (m *mockHostConfigurator) applyDNSConfig(config HostDNSConfig, stateManager *statemanager.Manager) error { @@ -67,6 +71,13 @@ func (m *mockHostConfigurator) supportCustomPort() bool { return false } +func (m *mockHostConfigurator) string() string { + if m.stringFunc != nil { + return m.stringFunc() + } + return "mock" +} + func newNoopHostMocker() hostManager { return &mockHostConfigurator{ applyDNSConfigFunc: func(config HostDNSConfig, stateManager *statemanager.Manager) error { return nil }, @@ -122,3 +133,7 @@ func (n noopHostConfigurator) restoreHostDNS() error { func (n noopHostConfigurator) supportCustomPort() bool { return true } + +func (n noopHostConfigurator) string() string { + return "noop" +} diff --git a/client/internal/dns/host_android.go b/client/internal/dns/host_android.go index 5653710d7..dfa3e5712 100644 --- a/client/internal/dns/host_android.go +++ b/client/internal/dns/host_android.go @@ -22,3 +22,7 @@ func (a androidHostManager) restoreHostDNS() error { func (a androidHostManager) supportCustomPort() bool { return false } + +func (a androidHostManager) string() string { + return "none" +} diff --git a/client/internal/dns/host_darwin.go b/client/internal/dns/host_darwin.go index 2f92dd367..f727f68b5 100644 --- a/client/internal/dns/host_darwin.go +++ b/client/internal/dns/host_darwin.go @@ -114,6 +114,10 @@ func (s *systemConfigurator) applyDNSConfig(config HostDNSConfig, stateManager * return nil } +func (s *systemConfigurator) string() string { + return "scutil" +} + func (s *systemConfigurator) restoreHostDNS() error { keys := s.getRemovableKeysWithDefaults() for _, key := range keys { diff --git a/client/internal/dns/host_ios.go b/client/internal/dns/host_ios.go index 4a0acf572..1c0ac63e9 100644 --- a/client/internal/dns/host_ios.go +++ b/client/internal/dns/host_ios.go @@ -38,3 +38,7 @@ func (a iosHostManager) restoreHostDNS() error { func (a iosHostManager) supportCustomPort() bool { return false } + +func (a iosHostManager) string() string { + return "none" +} diff --git a/client/internal/dns/host_windows.go b/client/internal/dns/host_windows.go index 58b0a14de..dceb24420 100644 --- a/client/internal/dns/host_windows.go +++ b/client/internal/dns/host_windows.go @@ -184,6 +184,10 @@ func (r *registryConfigurator) configureDNSPolicy(policyPath string, domains []s return nil } +func (r *registryConfigurator) string() string { + return "registry" +} + func (r *registryConfigurator) updateSearchDomains(domains []string) error { if err := r.setInterfaceRegistryKeyStringValue(interfaceConfigSearchListKey, strings.Join(domains, ",")); err != nil { return fmt.Errorf("update search domains: %w", err) diff --git a/client/internal/dns/network_manager_unix.go b/client/internal/dns/network_manager_unix.go index 63bbead77..10b4e6a6e 100644 --- a/client/internal/dns/network_manager_unix.go +++ b/client/internal/dns/network_manager_unix.go @@ -179,6 +179,10 @@ func (n *networkManagerDbusConfigurator) restoreHostDNS() error { return nil } +func (n *networkManagerDbusConfigurator) string() string { + return "network-manager" +} + func (n *networkManagerDbusConfigurator) getAppliedConnectionSettings() (networkManagerConnSettings, networkManagerConfigVersion, error) { obj, closeConn, err := getDbusObject(networkManagerDest, n.dbusLinkObject) if err != nil { diff --git a/client/internal/dns/resolvconf_unix.go b/client/internal/dns/resolvconf_unix.go index 6b5fdaf86..54c4c75bf 100644 --- a/client/internal/dns/resolvconf_unix.go +++ b/client/internal/dns/resolvconf_unix.go @@ -91,7 +91,7 @@ func (r *resolvconf) applyDNSConfig(config HostDNSConfig, stateManager *stateman if err != nil { log.Errorf("restore host dns: %s", err) } - return fmt.Errorf("unable to configure DNS for this peer using resolvconf manager without a nameserver group with all domains configured") + return ErrRouteAllWithoutNameserverGroup } searchDomainList := searchDomains(config) @@ -139,6 +139,10 @@ func (r *resolvconf) restoreHostDNS() error { return nil } +func (r *resolvconf) string() string { + return fmt.Sprintf("resolvconf (%s)", r.implType) +} + func (r *resolvconf) applyConfig(content bytes.Buffer) error { var cmd *exec.Cmd diff --git a/client/internal/dns/server.go b/client/internal/dns/server.go index f536a1434..bc87012f2 100644 --- a/client/internal/dns/server.go +++ b/client/internal/dns/server.go @@ -2,6 +2,7 @@ package dns import ( "context" + "errors" "fmt" "net/netip" "runtime" @@ -15,6 +16,7 @@ import ( "github.com/netbirdio/netbird/client/internal/listener" "github.com/netbirdio/netbird/client/internal/peer" "github.com/netbirdio/netbird/client/internal/statemanager" + cProto "github.com/netbirdio/netbird/client/proto" nbdns "github.com/netbirdio/netbird/dns" ) @@ -420,6 +422,7 @@ func (s *DefaultServer) applyConfiguration(update nbdns.Config) error { if err = s.hostManager.applyDNSConfig(hostUpdate, s.stateManager); err != nil { log.Error(err) + s.handleErrNoGroupaAll(err) } go func() { @@ -438,10 +441,26 @@ func (s *DefaultServer) applyConfiguration(update nbdns.Config) error { return nil } +func (s *DefaultServer) handleErrNoGroupaAll(err error) { + if !errors.Is(ErrRouteAllWithoutNameserverGroup, err) { + return + } + + if s.statusRecorder == nil { + return + } + + s.statusRecorder.PublishEvent( + cProto.SystemEvent_WARNING, cProto.SystemEvent_DNS, + "The host dns manager does not support match domains", + "The host dns manager does not support match domains without a catch-all nameserver group.", + map[string]string{"manager": s.hostManager.string()}, + ) +} + func (s *DefaultServer) buildLocalHandlerUpdate( customZones []nbdns.CustomZone, ) ([]handlerWrapper, map[string][]nbdns.SimpleRecord, error) { - var muxUpdates []handlerWrapper localRecords := make(map[string][]nbdns.SimpleRecord) @@ -672,6 +691,7 @@ func (s *DefaultServer) upstreamCallbacks( } if err := s.hostManager.applyDNSConfig(s.currentConfig, s.stateManager); err != nil { + s.handleErrNoGroupaAll(err) l.Errorf("Failed to apply nameserver deactivation on the host: %v", err) } @@ -710,6 +730,7 @@ func (s *DefaultServer) upstreamCallbacks( if s.hostManager != nil { if err := s.hostManager.applyDNSConfig(s.currentConfig, s.stateManager); err != nil { + s.handleErrNoGroupaAll(err) l.WithError(err).Error("reactivate temporary disabled nameserver group, DNS update apply") } } diff --git a/client/internal/dns/server_test.go b/client/internal/dns/server_test.go index 1354462d9..84779256f 100644 --- a/client/internal/dns/server_test.go +++ b/client/internal/dns/server_test.go @@ -356,7 +356,7 @@ func TestUpdateDNSServer(t *testing.T) { t.Log(err) } }() - dnsServer, err := NewDefaultServer(context.Background(), wgIface, "", &peer.Status{}, nil, false) + dnsServer, err := NewDefaultServer(context.Background(), wgIface, "", peer.NewRecorder("mgm"), nil, false) if err != nil { t.Fatal(err) } @@ -465,7 +465,7 @@ func TestDNSFakeResolverHandleUpdates(t *testing.T) { return } - dnsServer, err := NewDefaultServer(context.Background(), wgIface, "", &peer.Status{}, nil, false) + dnsServer, err := NewDefaultServer(context.Background(), wgIface, "", peer.NewRecorder("mgm"), nil, false) if err != nil { t.Errorf("create DNS server: %v", err) return @@ -566,7 +566,7 @@ func TestDNSServerStartStop(t *testing.T) { for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { - dnsServer, err := NewDefaultServer(context.Background(), &mocWGIface{}, testCase.addrPort, &peer.Status{}, nil, false) + dnsServer, err := NewDefaultServer(context.Background(), &mocWGIface{}, testCase.addrPort, peer.NewRecorder("mgm"), nil, false) if err != nil { t.Fatalf("%v", err) } @@ -639,7 +639,7 @@ func TestDNSServerUpstreamDeactivateCallback(t *testing.T) { {false, "domain2", false}, }, }, - statusRecorder: &peer.Status{}, + statusRecorder: peer.NewRecorder("mgm"), } var domainsUpdate string @@ -700,7 +700,7 @@ func TestDNSPermanent_updateHostDNS_emptyUpstream(t *testing.T) { var dnsList []string dnsConfig := nbdns.Config{} - dnsServer := NewDefaultServerPermanentUpstream(context.Background(), wgIFace, dnsList, dnsConfig, nil, &peer.Status{}, false) + dnsServer := NewDefaultServerPermanentUpstream(context.Background(), wgIFace, dnsList, dnsConfig, nil, peer.NewRecorder("mgm"), false) err = dnsServer.Initialize() if err != nil { t.Errorf("failed to initialize DNS server: %v", err) @@ -724,7 +724,7 @@ func TestDNSPermanent_updateUpstream(t *testing.T) { } defer wgIFace.Close() dnsConfig := nbdns.Config{} - dnsServer := NewDefaultServerPermanentUpstream(context.Background(), wgIFace, []string{"8.8.8.8"}, dnsConfig, nil, &peer.Status{}, false) + dnsServer := NewDefaultServerPermanentUpstream(context.Background(), wgIFace, []string{"8.8.8.8"}, dnsConfig, nil, peer.NewRecorder("mgm"), false) err = dnsServer.Initialize() if err != nil { t.Errorf("failed to initialize DNS server: %v", err) @@ -816,7 +816,7 @@ func TestDNSPermanent_matchOnly(t *testing.T) { } defer wgIFace.Close() dnsConfig := nbdns.Config{} - dnsServer := NewDefaultServerPermanentUpstream(context.Background(), wgIFace, []string{"8.8.8.8"}, dnsConfig, nil, &peer.Status{}, false) + dnsServer := NewDefaultServerPermanentUpstream(context.Background(), wgIFace, []string{"8.8.8.8"}, dnsConfig, nil, peer.NewRecorder("mgm"), false) err = dnsServer.Initialize() if err != nil { t.Errorf("failed to initialize DNS server: %v", err) diff --git a/client/internal/dns/systemd_linux.go b/client/internal/dns/systemd_linux.go index a031be582..a87cc73e5 100644 --- a/client/internal/dns/systemd_linux.go +++ b/client/internal/dns/systemd_linux.go @@ -154,6 +154,10 @@ func (s *systemdDbusConfigurator) applyDNSConfig(config HostDNSConfig, stateMana return nil } +func (s *systemdDbusConfigurator) string() string { + return "dbus" +} + func (s *systemdDbusConfigurator) setDomainsForInterface(domainsInput []systemdDbusLinkDomainsInput) error { err := s.callLinkMethod(systemdDbusSetDomainsMethodSuffix, domainsInput) if err != nil { diff --git a/client/internal/dns/upstream.go b/client/internal/dns/upstream.go index d269107e3..a22689cf9 100644 --- a/client/internal/dns/upstream.go +++ b/client/internal/dns/upstream.go @@ -183,6 +183,19 @@ func (u *upstreamResolverBase) checkUpstreamFails(err error) { } u.disable(err) + + if u.statusRecorder == nil { + return + } + + u.statusRecorder.PublishEvent( + proto.SystemEvent_WARNING, + proto.SystemEvent_DNS, + "All upstream servers failed (fail count exceeded)", + "Unable to reach one or more DNS servers. This might affect your ability to connect to some services.", + map[string]string{"upstreams": strings.Join(u.upstreamServers, ", ")}, + // TODO add domain meta + ) } // probeAvailability tests all upstream servers simultaneously and @@ -232,10 +245,14 @@ func (u *upstreamResolverBase) probeAvailability() { if !success { u.disable(errors.ErrorOrNil()) + if u.statusRecorder == nil { + return + } + u.statusRecorder.PublishEvent( proto.SystemEvent_WARNING, proto.SystemEvent_DNS, - "All upstream servers failed", + "All upstream servers failed (probe failed)", "Unable to reach one or more DNS servers. This might affect your ability to connect to some services.", map[string]string{"upstreams": strings.Join(u.upstreamServers, ", ")}, ) diff --git a/client/internal/engine.go b/client/internal/engine.go index ebb68b98b..c939240d9 100644 --- a/client/internal/engine.go +++ b/client/internal/engine.go @@ -43,6 +43,7 @@ import ( "github.com/netbirdio/netbird/client/internal/routemanager" "github.com/netbirdio/netbird/client/internal/routemanager/systemops" "github.com/netbirdio/netbird/client/internal/statemanager" + cProto "github.com/netbirdio/netbird/client/proto" "github.com/netbirdio/netbird/management/domain" semaphoregroup "github.com/netbirdio/netbird/util/semaphore-group" @@ -673,6 +674,8 @@ func (e *Engine) handleSync(update *mgmProto.SyncResponse) error { return err } + e.statusRecorder.PublishEvent(cProto.SystemEvent_INFO, cProto.SystemEvent_SYSTEM, "Network map updated", "", nil) + return nil } diff --git a/client/internal/routemanager/client.go b/client/internal/routemanager/client.go index 24a7ef467..2f0b78e7b 100644 --- a/client/internal/routemanager/client.go +++ b/client/internal/routemanager/client.go @@ -306,11 +306,13 @@ func (c *clientNetwork) recalculateRouteAndUpdatePeerAndSystem(rsn reason) error return nil } + var isNew bool if c.currentChosen == nil { // If they were not previously assigned to another peer, add routes to the system first if err := c.handler.AddRoute(c.ctx); err != nil { return fmt.Errorf("add route: %w", err) } + isNew = true } else { // Otherwise, remove the allowed IPs from the previous peer first if err := c.removeRouteFromWireGuardPeer(); err != nil { @@ -324,6 +326,10 @@ func (c *clientNetwork) recalculateRouteAndUpdatePeerAndSystem(rsn reason) error return fmt.Errorf("add allowed IPs for peer %s: %w", c.currentChosen.Peer, err) } + if isNew { + c.connectEvent() + } + err := c.statusRecorder.AddPeerStateRoute(c.currentChosen.Peer, c.handler.String()) if err != nil { return fmt.Errorf("add peer state route: %w", err) @@ -331,6 +337,35 @@ func (c *clientNetwork) recalculateRouteAndUpdatePeerAndSystem(rsn reason) error return nil } +func (c *clientNetwork) connectEvent() { + var defaultRoute bool + for _, r := range c.routes { + if r.Network.Bits() == 0 { + defaultRoute = true + break + } + } + + if !defaultRoute { + return + } + + meta := map[string]string{ + "network": c.handler.String(), + } + if c.currentChosen != nil { + meta["id"] = string(c.currentChosen.NetID) + meta["peer"] = c.currentChosen.Peer + } + c.statusRecorder.PublishEvent( + proto.SystemEvent_INFO, + proto.SystemEvent_NETWORK, + "Default route added", + "Exit node connected.", + meta, + ) +} + func (c *clientNetwork) disconnectEvent(rsn reason) { var defaultRoute bool for _, r := range c.routes { @@ -349,29 +384,27 @@ func (c *clientNetwork) disconnectEvent(rsn reason) { var userMessage string meta := make(map[string]string) + if c.currentChosen != nil { + meta["id"] = string(c.currentChosen.NetID) + meta["peer"] = c.currentChosen.Peer + } + meta["network"] = c.handler.String() switch rsn { case reasonShutdown: severity = proto.SystemEvent_INFO message = "Default route removed" userMessage = "Exit node disconnected." - meta["network"] = c.handler.String() case reasonRouteUpdate: severity = proto.SystemEvent_INFO message = "Default route updated due to configuration change" - meta["network"] = c.handler.String() case reasonPeerUpdate: severity = proto.SystemEvent_WARNING message = "Default route disconnected due to peer unreachability" userMessage = "Exit node connection lost. Your internet access might be affected." - if c.currentChosen != nil { - meta["peer"] = c.currentChosen.Peer - meta["network"] = c.handler.String() - } default: severity = proto.SystemEvent_ERROR - message = "Default route disconnected for unknown reason" + message = "Default route disconnected for unknown reasons" userMessage = "Exit node disconnected for unknown reasons." - meta["network"] = c.handler.String() } c.statusRecorder.PublishEvent( diff --git a/client/proto/daemon.pb.go b/client/proto/daemon.pb.go index 3aa57da8f..55b7aa7e9 100644 --- a/client/proto/daemon.pb.go +++ b/client/proto/daemon.pb.go @@ -146,6 +146,7 @@ const ( SystemEvent_DNS SystemEvent_Category = 1 SystemEvent_AUTHENTICATION SystemEvent_Category = 2 SystemEvent_CONNECTIVITY SystemEvent_Category = 3 + SystemEvent_SYSTEM SystemEvent_Category = 4 ) // Enum value maps for SystemEvent_Category. @@ -155,12 +156,14 @@ var ( 1: "DNS", 2: "AUTHENTICATION", 3: "CONNECTIVITY", + 4: "SYSTEM", } SystemEvent_Category_value = map[string]int32{ "NETWORK": 0, "DNS": 1, "AUTHENTICATION": 2, "CONNECTIVITY": 3, + "SYSTEM": 4, } ) @@ -3675,7 +3678,7 @@ var file_daemon_proto_rawDesc = []byte{ 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x66, 0x69, 0x6e, 0x61, 0x6c, 0x44, 0x69, 0x73, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x12, 0x0a, 0x10, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x22, 0x87, 0x04, 0x0a, 0x0b, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65, 0x6e, + 0x74, 0x22, 0x93, 0x04, 0x0a, 0x0b, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x38, 0x0a, 0x08, 0x73, 0x65, 0x76, 0x65, 0x72, 0x69, 0x74, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1c, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x79, 0x73, @@ -3703,111 +3706,111 @@ var file_daemon_proto_rawDesc = []byte{ 0x69, 0x74, 0x79, 0x12, 0x08, 0x0a, 0x04, 0x49, 0x4e, 0x46, 0x4f, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x57, 0x41, 0x52, 0x4e, 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x02, 0x12, 0x0c, 0x0a, 0x08, 0x43, 0x52, 0x49, 0x54, 0x49, 0x43, 0x41, - 0x4c, 0x10, 0x03, 0x22, 0x46, 0x0a, 0x08, 0x43, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79, 0x12, + 0x4c, 0x10, 0x03, 0x22, 0x52, 0x0a, 0x08, 0x43, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79, 0x12, 0x0b, 0x0a, 0x07, 0x4e, 0x45, 0x54, 0x57, 0x4f, 0x52, 0x4b, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x44, 0x4e, 0x53, 0x10, 0x01, 0x12, 0x12, 0x0a, 0x0e, 0x41, 0x55, 0x54, 0x48, 0x45, 0x4e, 0x54, 0x49, 0x43, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x10, 0x02, 0x12, 0x10, 0x0a, 0x0c, 0x43, 0x4f, 0x4e, - 0x4e, 0x45, 0x43, 0x54, 0x49, 0x56, 0x49, 0x54, 0x59, 0x10, 0x03, 0x22, 0x12, 0x0a, 0x10, 0x47, - 0x65, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, - 0x40, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2b, 0x0a, 0x06, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x01, - 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x79, - 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x06, 0x65, 0x76, 0x65, 0x6e, 0x74, - 0x73, 0x2a, 0x62, 0x0a, 0x08, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x0b, 0x0a, - 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x50, 0x41, - 0x4e, 0x49, 0x43, 0x10, 0x01, 0x12, 0x09, 0x0a, 0x05, 0x46, 0x41, 0x54, 0x41, 0x4c, 0x10, 0x02, - 0x12, 0x09, 0x0a, 0x05, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x03, 0x12, 0x08, 0x0a, 0x04, 0x57, - 0x41, 0x52, 0x4e, 0x10, 0x04, 0x12, 0x08, 0x0a, 0x04, 0x49, 0x4e, 0x46, 0x4f, 0x10, 0x05, 0x12, - 0x09, 0x0a, 0x05, 0x44, 0x45, 0x42, 0x55, 0x47, 0x10, 0x06, 0x12, 0x09, 0x0a, 0x05, 0x54, 0x52, - 0x41, 0x43, 0x45, 0x10, 0x07, 0x32, 0xe7, 0x0a, 0x0a, 0x0d, 0x44, 0x61, 0x65, 0x6d, 0x6f, 0x6e, - 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x36, 0x0a, 0x05, 0x4c, 0x6f, 0x67, 0x69, 0x6e, - 0x12, 0x14, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x15, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, - 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, - 0x4b, 0x0a, 0x0c, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, - 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, - 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x64, - 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, - 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x2d, 0x0a, 0x02, - 0x55, 0x70, 0x12, 0x11, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x55, 0x70, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x55, - 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x39, 0x0a, 0x06, 0x53, - 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x15, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, - 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x64, - 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x33, 0x0a, 0x04, 0x44, 0x6f, 0x77, 0x6e, 0x12, 0x13, - 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x6f, 0x77, - 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x42, 0x0a, 0x09, 0x47, - 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x18, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, - 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x43, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, - 0x4b, 0x0a, 0x0c, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x12, - 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x65, 0x74, - 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x64, - 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, - 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x51, 0x0a, 0x0e, - 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x12, 0x1d, + 0x4e, 0x45, 0x43, 0x54, 0x49, 0x56, 0x49, 0x54, 0x59, 0x10, 0x03, 0x12, 0x0a, 0x0a, 0x06, 0x53, + 0x59, 0x53, 0x54, 0x45, 0x4d, 0x10, 0x04, 0x22, 0x12, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x45, 0x76, + 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x40, 0x0a, 0x11, 0x47, + 0x65, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x2b, 0x0a, 0x06, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x13, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, + 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x06, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x2a, 0x62, 0x0a, + 0x08, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, + 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x50, 0x41, 0x4e, 0x49, 0x43, 0x10, + 0x01, 0x12, 0x09, 0x0a, 0x05, 0x46, 0x41, 0x54, 0x41, 0x4c, 0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, + 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x03, 0x12, 0x08, 0x0a, 0x04, 0x57, 0x41, 0x52, 0x4e, 0x10, + 0x04, 0x12, 0x08, 0x0a, 0x04, 0x49, 0x4e, 0x46, 0x4f, 0x10, 0x05, 0x12, 0x09, 0x0a, 0x05, 0x44, + 0x45, 0x42, 0x55, 0x47, 0x10, 0x06, 0x12, 0x09, 0x0a, 0x05, 0x54, 0x52, 0x41, 0x43, 0x45, 0x10, + 0x07, 0x32, 0xe7, 0x0a, 0x0a, 0x0d, 0x44, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x53, 0x65, 0x72, 0x76, + 0x69, 0x63, 0x65, 0x12, 0x36, 0x0a, 0x05, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x14, 0x2e, 0x64, + 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x15, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x67, 0x69, + 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x4b, 0x0a, 0x0c, 0x57, + 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x1b, 0x2e, 0x64, 0x61, + 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, + 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, + 0x6e, 0x2e, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x2d, 0x0a, 0x02, 0x55, 0x70, 0x12, 0x11, + 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x55, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x12, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x55, 0x70, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x39, 0x0a, 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, + 0x73, 0x12, 0x15, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, + 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, + 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x22, 0x00, 0x12, 0x33, 0x0a, 0x04, 0x44, 0x6f, 0x77, 0x6e, 0x12, 0x13, 0x2e, 0x64, 0x61, 0x65, + 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x14, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x42, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x43, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x12, 0x18, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, + 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, + 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x4b, 0x0a, 0x0c, 0x4c, + 0x69, 0x73, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x12, 0x1b, 0x2e, 0x64, 0x61, + 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, + 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, + 0x6e, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x51, 0x0a, 0x0e, 0x53, 0x65, 0x6c, 0x65, + 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x12, 0x1d, 0x2e, 0x64, 0x61, 0x65, + 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, + 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x64, 0x61, 0x65, 0x6d, + 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, + 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x53, 0x0a, 0x10, 0x44, + 0x65, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x12, + 0x1d, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, + 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, - 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, - 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, - 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, - 0x53, 0x0a, 0x10, 0x44, 0x65, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, - 0x72, 0x6b, 0x73, 0x12, 0x1d, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x6c, - 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x6c, 0x65, - 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x0b, 0x44, 0x65, 0x62, 0x75, 0x67, 0x42, 0x75, 0x6e, - 0x64, 0x6c, 0x65, 0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x62, - 0x75, 0x67, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x62, 0x75, 0x67, 0x42, 0x75, - 0x6e, 0x64, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x48, - 0x0a, 0x0b, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x1a, 0x2e, - 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, - 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, + 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, + 0x12, 0x48, 0x0a, 0x0b, 0x44, 0x65, 0x62, 0x75, 0x67, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x12, + 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x62, 0x75, 0x67, 0x42, 0x75, + 0x6e, 0x64, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, + 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x62, 0x75, 0x67, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x0b, 0x47, 0x65, + 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x0b, 0x53, 0x65, 0x74, 0x4c, - 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, - 0x2e, 0x53, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x74, - 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x22, 0x00, 0x12, 0x45, 0x0a, 0x0a, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, - 0x12, 0x19, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, - 0x61, 0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x64, 0x61, - 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x45, 0x0a, 0x0a, 0x43, 0x6c, 0x65, - 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x19, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, - 0x2e, 0x43, 0x6c, 0x65, 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x43, 0x6c, 0x65, 0x61, - 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, - 0x12, 0x48, 0x0a, 0x0b, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, - 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, - 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, + 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x0b, 0x53, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, + 0x76, 0x65, 0x6c, 0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x74, + 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, + 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x45, + 0x0a, 0x0a, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x12, 0x19, 0x2e, 0x64, + 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, + 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x45, 0x0a, 0x0a, 0x43, 0x6c, 0x65, 0x61, 0x6e, 0x53, 0x74, + 0x61, 0x74, 0x65, 0x12, 0x19, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x43, 0x6c, 0x65, + 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, + 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x43, 0x6c, 0x65, 0x61, 0x6e, 0x53, 0x74, 0x61, + 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x0b, + 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x6f, 0x0a, 0x18, 0x53, 0x65, - 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x50, 0x65, 0x72, 0x73, 0x69, - 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x27, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, - 0x53, 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x50, 0x65, 0x72, - 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x28, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77, - 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x50, 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, - 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x0b, 0x54, - 0x72, 0x61, 0x63, 0x65, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65, - 0x6d, 0x6f, 0x6e, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x65, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, - 0x54, 0x72, 0x61, 0x63, 0x65, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x44, 0x0a, 0x0f, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, - 0x62, 0x65, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x18, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, - 0x6e, 0x2e, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x13, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x79, 0x73, 0x74, - 0x65, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x22, 0x00, 0x30, 0x01, 0x12, 0x42, 0x0a, 0x09, 0x47, - 0x65, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x18, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, - 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x45, - 0x76, 0x65, 0x6e, 0x74, 0x73, 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, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, + 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x6f, 0x0a, 0x18, 0x53, 0x65, 0x74, 0x4e, 0x65, 0x74, + 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x50, 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, + 0x63, 0x65, 0x12, 0x27, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x74, 0x4e, + 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x50, 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, + 0x65, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x28, 0x2e, 0x64, 0x61, + 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, + 0x61, 0x70, 0x50, 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x0b, 0x54, 0x72, 0x61, 0x63, 0x65, + 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, + 0x54, 0x72, 0x61, 0x63, 0x65, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x54, 0x72, 0x61, 0x63, + 0x65, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, + 0x00, 0x12, 0x44, 0x0a, 0x0f, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x45, 0x76, + 0x65, 0x6e, 0x74, 0x73, 0x12, 0x18, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x75, + 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x13, + 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, + 0x65, 0x6e, 0x74, 0x22, 0x00, 0x30, 0x01, 0x12, 0x42, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x45, 0x76, + 0x65, 0x6e, 0x74, 0x73, 0x12, 0x18, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, + 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, + 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, + 0x73, 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 ( diff --git a/client/proto/daemon.proto b/client/proto/daemon.proto index 012b8b4db..b1a6a6614 100644 --- a/client/proto/daemon.proto +++ b/client/proto/daemon.proto @@ -425,6 +425,7 @@ message SystemEvent { DNS = 1; AUTHENTICATION = 2; CONNECTIVITY = 3; + SYSTEM = 4; } string id = 1; diff --git a/client/server/network.go b/client/server/network.go index aaf361524..d310f4da1 100644 --- a/client/server/network.go +++ b/client/server/network.go @@ -6,6 +6,7 @@ import ( "net/netip" "slices" "sort" + "strings" "golang.org/x/exp/maps" @@ -134,6 +135,18 @@ func (s *Server) SelectNetworks(_ context.Context, req *proto.SelectNetworksRequ } routeManager.TriggerSelection(routeManager.GetClientRoutes()) + s.statusRecorder.PublishEvent( + proto.SystemEvent_INFO, + proto.SystemEvent_SYSTEM, + "Network selection changed", + "", + map[string]string{ + "networks": strings.Join(req.GetNetworkIDs(), ", "), + "append": fmt.Sprint(req.GetAppend()), + "all": fmt.Sprint(req.GetAll()), + }, + ) + return &proto.SelectNetworksResponse{}, nil } @@ -164,6 +177,18 @@ func (s *Server) DeselectNetworks(_ context.Context, req *proto.SelectNetworksRe } routeManager.TriggerSelection(routeManager.GetClientRoutes()) + s.statusRecorder.PublishEvent( + proto.SystemEvent_INFO, + proto.SystemEvent_SYSTEM, + "Network deselection changed", + "", + map[string]string{ + "networks": strings.Join(req.GetNetworkIDs(), ", "), + "append": fmt.Sprint(req.GetAppend()), + "all": fmt.Sprint(req.GetAll()), + }, + ) + return &proto.SelectNetworksResponse{}, nil } diff --git a/client/cmd/status_event.go b/client/status/event.go similarity index 86% rename from client/cmd/status_event.go rename to client/status/event.go index 9331570e6..2b65c9fa3 100644 --- a/client/cmd/status_event.go +++ b/client/status/event.go @@ -1,4 +1,4 @@ -package cmd +package status import ( "fmt" @@ -9,7 +9,7 @@ import ( "github.com/netbirdio/netbird/client/proto" ) -type systemEventOutput struct { +type SystemEventOutput struct { ID string `json:"id" yaml:"id"` Severity string `json:"severity" yaml:"severity"` Category string `json:"category" yaml:"category"` @@ -19,10 +19,10 @@ type systemEventOutput struct { Metadata map[string]string `json:"metadata" yaml:"metadata"` } -func mapEvents(protoEvents []*proto.SystemEvent) []systemEventOutput { - events := make([]systemEventOutput, len(protoEvents)) +func mapEvents(protoEvents []*proto.SystemEvent) []SystemEventOutput { + events := make([]SystemEventOutput, len(protoEvents)) for i, event := range protoEvents { - events[i] = systemEventOutput{ + events[i] = SystemEventOutput{ ID: event.GetId(), Severity: event.GetSeverity().String(), Category: event.GetCategory().String(), @@ -35,7 +35,7 @@ func mapEvents(protoEvents []*proto.SystemEvent) []systemEventOutput { return events } -func parseEvents(events []systemEventOutput) string { +func parseEvents(events []SystemEventOutput) string { if len(events) == 0 { return " No events recorded" } diff --git a/client/status/status.go b/client/status/status.go new file mode 100644 index 000000000..2d11ee3ba --- /dev/null +++ b/client/status/status.go @@ -0,0 +1,725 @@ +package status + +import ( + "encoding/json" + "fmt" + "net" + "net/netip" + "os" + "runtime" + "sort" + "strings" + "time" + + "gopkg.in/yaml.v3" + + "github.com/netbirdio/netbird/client/anonymize" + "github.com/netbirdio/netbird/client/internal/peer" + "github.com/netbirdio/netbird/client/proto" + "github.com/netbirdio/netbird/version" +) + +type PeerStateDetailOutput struct { + FQDN string `json:"fqdn" yaml:"fqdn"` + IP string `json:"netbirdIp" yaml:"netbirdIp"` + PubKey string `json:"publicKey" yaml:"publicKey"` + Status string `json:"status" yaml:"status"` + LastStatusUpdate time.Time `json:"lastStatusUpdate" yaml:"lastStatusUpdate"` + ConnType string `json:"connectionType" yaml:"connectionType"` + IceCandidateType IceCandidateType `json:"iceCandidateType" yaml:"iceCandidateType"` + IceCandidateEndpoint IceCandidateType `json:"iceCandidateEndpoint" yaml:"iceCandidateEndpoint"` + RelayAddress string `json:"relayAddress" yaml:"relayAddress"` + LastWireguardHandshake time.Time `json:"lastWireguardHandshake" yaml:"lastWireguardHandshake"` + TransferReceived int64 `json:"transferReceived" yaml:"transferReceived"` + TransferSent int64 `json:"transferSent" yaml:"transferSent"` + Latency time.Duration `json:"latency" yaml:"latency"` + RosenpassEnabled bool `json:"quantumResistance" yaml:"quantumResistance"` + Networks []string `json:"networks" yaml:"networks"` +} + +type PeersStateOutput struct { + Total int `json:"total" yaml:"total"` + Connected int `json:"connected" yaml:"connected"` + Details []PeerStateDetailOutput `json:"details" yaml:"details"` +} + +type SignalStateOutput struct { + URL string `json:"url" yaml:"url"` + Connected bool `json:"connected" yaml:"connected"` + Error string `json:"error" yaml:"error"` +} + +type ManagementStateOutput struct { + URL string `json:"url" yaml:"url"` + Connected bool `json:"connected" yaml:"connected"` + Error string `json:"error" yaml:"error"` +} + +type RelayStateOutputDetail struct { + URI string `json:"uri" yaml:"uri"` + Available bool `json:"available" yaml:"available"` + Error string `json:"error" yaml:"error"` +} + +type RelayStateOutput struct { + Total int `json:"total" yaml:"total"` + Available int `json:"available" yaml:"available"` + Details []RelayStateOutputDetail `json:"details" yaml:"details"` +} + +type IceCandidateType struct { + Local string `json:"local" yaml:"local"` + Remote string `json:"remote" yaml:"remote"` +} + +type NsServerGroupStateOutput struct { + Servers []string `json:"servers" yaml:"servers"` + Domains []string `json:"domains" yaml:"domains"` + Enabled bool `json:"enabled" yaml:"enabled"` + Error string `json:"error" yaml:"error"` +} + +type OutputOverview struct { + Peers PeersStateOutput `json:"peers" yaml:"peers"` + CliVersion string `json:"cliVersion" yaml:"cliVersion"` + DaemonVersion string `json:"daemonVersion" yaml:"daemonVersion"` + ManagementState ManagementStateOutput `json:"management" yaml:"management"` + SignalState SignalStateOutput `json:"signal" yaml:"signal"` + Relays RelayStateOutput `json:"relays" yaml:"relays"` + IP string `json:"netbirdIp" yaml:"netbirdIp"` + PubKey string `json:"publicKey" yaml:"publicKey"` + KernelInterface bool `json:"usesKernelInterface" yaml:"usesKernelInterface"` + FQDN string `json:"fqdn" yaml:"fqdn"` + RosenpassEnabled bool `json:"quantumResistance" yaml:"quantumResistance"` + RosenpassPermissive bool `json:"quantumResistancePermissive" yaml:"quantumResistancePermissive"` + Networks []string `json:"networks" yaml:"networks"` + NSServerGroups []NsServerGroupStateOutput `json:"dnsServers" yaml:"dnsServers"` + Events []SystemEventOutput `json:"events" yaml:"events"` +} + +func ConvertToStatusOutputOverview(resp *proto.StatusResponse, anon bool, statusFilter string, prefixNamesFilter []string, prefixNamesFilterMap map[string]struct{}, ipsFilter map[string]struct{}) OutputOverview { + pbFullStatus := resp.GetFullStatus() + + managementState := pbFullStatus.GetManagementState() + managementOverview := ManagementStateOutput{ + URL: managementState.GetURL(), + Connected: managementState.GetConnected(), + Error: managementState.Error, + } + + signalState := pbFullStatus.GetSignalState() + signalOverview := SignalStateOutput{ + URL: signalState.GetURL(), + Connected: signalState.GetConnected(), + Error: signalState.Error, + } + + relayOverview := mapRelays(pbFullStatus.GetRelays()) + peersOverview := mapPeers(resp.GetFullStatus().GetPeers(), statusFilter, prefixNamesFilter, prefixNamesFilterMap, ipsFilter) + + overview := OutputOverview{ + Peers: peersOverview, + CliVersion: version.NetbirdVersion(), + DaemonVersion: resp.GetDaemonVersion(), + ManagementState: managementOverview, + SignalState: signalOverview, + Relays: relayOverview, + IP: pbFullStatus.GetLocalPeerState().GetIP(), + PubKey: pbFullStatus.GetLocalPeerState().GetPubKey(), + KernelInterface: pbFullStatus.GetLocalPeerState().GetKernelInterface(), + FQDN: pbFullStatus.GetLocalPeerState().GetFqdn(), + RosenpassEnabled: pbFullStatus.GetLocalPeerState().GetRosenpassEnabled(), + RosenpassPermissive: pbFullStatus.GetLocalPeerState().GetRosenpassPermissive(), + Networks: pbFullStatus.GetLocalPeerState().GetNetworks(), + NSServerGroups: mapNSGroups(pbFullStatus.GetDnsServers()), + Events: mapEvents(pbFullStatus.GetEvents()), + } + + if anon { + anonymizer := anonymize.NewAnonymizer(anonymize.DefaultAddresses()) + anonymizeOverview(anonymizer, &overview) + } + + return overview +} + +func mapRelays(relays []*proto.RelayState) RelayStateOutput { + var relayStateDetail []RelayStateOutputDetail + + var relaysAvailable int + for _, relay := range relays { + available := relay.GetAvailable() + relayStateDetail = append(relayStateDetail, + RelayStateOutputDetail{ + URI: relay.URI, + Available: available, + Error: relay.GetError(), + }, + ) + + if available { + relaysAvailable++ + } + } + + return RelayStateOutput{ + Total: len(relays), + Available: relaysAvailable, + Details: relayStateDetail, + } +} + +func mapNSGroups(servers []*proto.NSGroupState) []NsServerGroupStateOutput { + mappedNSGroups := make([]NsServerGroupStateOutput, 0, len(servers)) + for _, pbNsGroupServer := range servers { + mappedNSGroups = append(mappedNSGroups, NsServerGroupStateOutput{ + Servers: pbNsGroupServer.GetServers(), + Domains: pbNsGroupServer.GetDomains(), + Enabled: pbNsGroupServer.GetEnabled(), + Error: pbNsGroupServer.GetError(), + }) + } + return mappedNSGroups +} + +func mapPeers( + peers []*proto.PeerState, + statusFilter string, + prefixNamesFilter []string, + prefixNamesFilterMap map[string]struct{}, + ipsFilter map[string]struct{}, +) PeersStateOutput { + var peersStateDetail []PeerStateDetailOutput + peersConnected := 0 + for _, pbPeerState := range peers { + localICE := "" + remoteICE := "" + localICEEndpoint := "" + remoteICEEndpoint := "" + relayServerAddress := "" + connType := "" + lastHandshake := time.Time{} + transferReceived := int64(0) + transferSent := int64(0) + + isPeerConnected := pbPeerState.ConnStatus == peer.StatusConnected.String() + if skipDetailByFilters(pbPeerState, isPeerConnected, statusFilter, prefixNamesFilter, prefixNamesFilterMap, ipsFilter) { + continue + } + if isPeerConnected { + peersConnected++ + + localICE = pbPeerState.GetLocalIceCandidateType() + remoteICE = pbPeerState.GetRemoteIceCandidateType() + localICEEndpoint = pbPeerState.GetLocalIceCandidateEndpoint() + remoteICEEndpoint = pbPeerState.GetRemoteIceCandidateEndpoint() + connType = "P2P" + if pbPeerState.Relayed { + connType = "Relayed" + } + relayServerAddress = pbPeerState.GetRelayAddress() + lastHandshake = pbPeerState.GetLastWireguardHandshake().AsTime().Local() + transferReceived = pbPeerState.GetBytesRx() + transferSent = pbPeerState.GetBytesTx() + } + + timeLocal := pbPeerState.GetConnStatusUpdate().AsTime().Local() + peerState := PeerStateDetailOutput{ + IP: pbPeerState.GetIP(), + PubKey: pbPeerState.GetPubKey(), + Status: pbPeerState.GetConnStatus(), + LastStatusUpdate: timeLocal, + ConnType: connType, + IceCandidateType: IceCandidateType{ + Local: localICE, + Remote: remoteICE, + }, + IceCandidateEndpoint: IceCandidateType{ + Local: localICEEndpoint, + Remote: remoteICEEndpoint, + }, + RelayAddress: relayServerAddress, + FQDN: pbPeerState.GetFqdn(), + LastWireguardHandshake: lastHandshake, + TransferReceived: transferReceived, + TransferSent: transferSent, + Latency: pbPeerState.GetLatency().AsDuration(), + RosenpassEnabled: pbPeerState.GetRosenpassEnabled(), + Networks: pbPeerState.GetNetworks(), + } + + peersStateDetail = append(peersStateDetail, peerState) + } + + sortPeersByIP(peersStateDetail) + + peersOverview := PeersStateOutput{ + Total: len(peersStateDetail), + Connected: peersConnected, + Details: peersStateDetail, + } + return peersOverview +} + +func sortPeersByIP(peersStateDetail []PeerStateDetailOutput) { + if len(peersStateDetail) > 0 { + sort.SliceStable(peersStateDetail, func(i, j int) bool { + iAddr, _ := netip.ParseAddr(peersStateDetail[i].IP) + jAddr, _ := netip.ParseAddr(peersStateDetail[j].IP) + return iAddr.Compare(jAddr) == -1 + }) + } +} + +func ParseToJSON(overview OutputOverview) (string, error) { + jsonBytes, err := json.Marshal(overview) + if err != nil { + return "", fmt.Errorf("json marshal failed") + } + return string(jsonBytes), err +} + +func ParseToYAML(overview OutputOverview) (string, error) { + yamlBytes, err := yaml.Marshal(overview) + if err != nil { + return "", fmt.Errorf("yaml marshal failed") + } + return string(yamlBytes), nil +} + +func ParseGeneralSummary(overview OutputOverview, showURL bool, showRelays bool, showNameServers bool) string { + var managementConnString string + if overview.ManagementState.Connected { + managementConnString = "Connected" + if showURL { + managementConnString = fmt.Sprintf("%s to %s", managementConnString, overview.ManagementState.URL) + } + } else { + managementConnString = "Disconnected" + if overview.ManagementState.Error != "" { + managementConnString = fmt.Sprintf("%s, reason: %s", managementConnString, overview.ManagementState.Error) + } + } + + var signalConnString string + if overview.SignalState.Connected { + signalConnString = "Connected" + if showURL { + signalConnString = fmt.Sprintf("%s to %s", signalConnString, overview.SignalState.URL) + } + } else { + signalConnString = "Disconnected" + if overview.SignalState.Error != "" { + signalConnString = fmt.Sprintf("%s, reason: %s", signalConnString, overview.SignalState.Error) + } + } + + interfaceTypeString := "Userspace" + interfaceIP := overview.IP + if overview.KernelInterface { + interfaceTypeString = "Kernel" + } else if overview.IP == "" { + interfaceTypeString = "N/A" + interfaceIP = "N/A" + } + + var relaysString string + if showRelays { + for _, relay := range overview.Relays.Details { + available := "Available" + reason := "" + if !relay.Available { + available = "Unavailable" + reason = fmt.Sprintf(", reason: %s", relay.Error) + } + relaysString += fmt.Sprintf("\n [%s] is %s%s", relay.URI, available, reason) + } + } else { + relaysString = fmt.Sprintf("%d/%d Available", overview.Relays.Available, overview.Relays.Total) + } + + networks := "-" + if len(overview.Networks) > 0 { + sort.Strings(overview.Networks) + networks = strings.Join(overview.Networks, ", ") + } + + var dnsServersString string + if showNameServers { + for _, nsServerGroup := range overview.NSServerGroups { + enabled := "Available" + if !nsServerGroup.Enabled { + enabled = "Unavailable" + } + errorString := "" + if nsServerGroup.Error != "" { + errorString = fmt.Sprintf(", reason: %s", nsServerGroup.Error) + errorString = strings.TrimSpace(errorString) + } + + domainsString := strings.Join(nsServerGroup.Domains, ", ") + if domainsString == "" { + domainsString = "." // Show "." for the default zone + } + dnsServersString += fmt.Sprintf( + "\n [%s] for [%s] is %s%s", + strings.Join(nsServerGroup.Servers, ", "), + domainsString, + enabled, + errorString, + ) + } + } else { + dnsServersString = fmt.Sprintf("%d/%d Available", countEnabled(overview.NSServerGroups), len(overview.NSServerGroups)) + } + + rosenpassEnabledStatus := "false" + if overview.RosenpassEnabled { + rosenpassEnabledStatus = "true" + if overview.RosenpassPermissive { + rosenpassEnabledStatus = "true (permissive)" //nolint:gosec + } + } + + peersCountString := fmt.Sprintf("%d/%d Connected", overview.Peers.Connected, overview.Peers.Total) + + goos := runtime.GOOS + goarch := runtime.GOARCH + goarm := "" + if goarch == "arm" { + goarm = fmt.Sprintf(" (ARMv%s)", os.Getenv("GOARM")) + } + + summary := fmt.Sprintf( + "OS: %s\n"+ + "Daemon version: %s\n"+ + "CLI version: %s\n"+ + "Management: %s\n"+ + "Signal: %s\n"+ + "Relays: %s\n"+ + "Nameservers: %s\n"+ + "FQDN: %s\n"+ + "NetBird IP: %s\n"+ + "Interface type: %s\n"+ + "Quantum resistance: %s\n"+ + "Networks: %s\n"+ + "Peers count: %s\n", + fmt.Sprintf("%s/%s%s", goos, goarch, goarm), + overview.DaemonVersion, + version.NetbirdVersion(), + managementConnString, + signalConnString, + relaysString, + dnsServersString, + overview.FQDN, + interfaceIP, + interfaceTypeString, + rosenpassEnabledStatus, + networks, + peersCountString, + ) + return summary +} + +func ParseToFullDetailSummary(overview OutputOverview) string { + parsedPeersString := parsePeers(overview.Peers, overview.RosenpassEnabled, overview.RosenpassPermissive) + parsedEventsString := parseEvents(overview.Events) + summary := ParseGeneralSummary(overview, true, true, true) + + return fmt.Sprintf( + "Peers detail:"+ + "%s\n"+ + "Events:"+ + "%s\n"+ + "%s", + parsedPeersString, + parsedEventsString, + summary, + ) +} + +func parsePeers(peers PeersStateOutput, rosenpassEnabled, rosenpassPermissive bool) string { + var ( + peersString = "" + ) + + for _, peerState := range peers.Details { + + localICE := "-" + if peerState.IceCandidateType.Local != "" { + localICE = peerState.IceCandidateType.Local + } + + remoteICE := "-" + if peerState.IceCandidateType.Remote != "" { + remoteICE = peerState.IceCandidateType.Remote + } + + localICEEndpoint := "-" + if peerState.IceCandidateEndpoint.Local != "" { + localICEEndpoint = peerState.IceCandidateEndpoint.Local + } + + remoteICEEndpoint := "-" + if peerState.IceCandidateEndpoint.Remote != "" { + remoteICEEndpoint = peerState.IceCandidateEndpoint.Remote + } + + rosenpassEnabledStatus := "false" + if rosenpassEnabled { + if peerState.RosenpassEnabled { + rosenpassEnabledStatus = "true" + } else { + if rosenpassPermissive { + rosenpassEnabledStatus = "false (remote didn't enable quantum resistance)" + } else { + rosenpassEnabledStatus = "false (connection won't work without a permissive mode)" + } + } + } else { + if peerState.RosenpassEnabled { + rosenpassEnabledStatus = "false (connection might not work without a remote permissive mode)" + } + } + + networks := "-" + if len(peerState.Networks) > 0 { + sort.Strings(peerState.Networks) + networks = strings.Join(peerState.Networks, ", ") + } + + peerString := fmt.Sprintf( + "\n %s:\n"+ + " NetBird IP: %s\n"+ + " Public key: %s\n"+ + " Status: %s\n"+ + " -- detail --\n"+ + " Connection type: %s\n"+ + " ICE candidate (Local/Remote): %s/%s\n"+ + " ICE candidate endpoints (Local/Remote): %s/%s\n"+ + " Relay server address: %s\n"+ + " Last connection update: %s\n"+ + " Last WireGuard handshake: %s\n"+ + " Transfer status (received/sent) %s/%s\n"+ + " Quantum resistance: %s\n"+ + " Networks: %s\n"+ + " Latency: %s\n", + peerState.FQDN, + peerState.IP, + peerState.PubKey, + peerState.Status, + peerState.ConnType, + localICE, + remoteICE, + localICEEndpoint, + remoteICEEndpoint, + peerState.RelayAddress, + timeAgo(peerState.LastStatusUpdate), + timeAgo(peerState.LastWireguardHandshake), + toIEC(peerState.TransferReceived), + toIEC(peerState.TransferSent), + rosenpassEnabledStatus, + networks, + peerState.Latency.String(), + ) + + peersString += peerString + } + return peersString +} + +func skipDetailByFilters( + peerState *proto.PeerState, + isConnected bool, + statusFilter string, + prefixNamesFilter []string, + prefixNamesFilterMap map[string]struct{}, + ipsFilter map[string]struct{}, +) bool { + statusEval := false + ipEval := false + nameEval := true + + if statusFilter != "" { + lowerStatusFilter := strings.ToLower(statusFilter) + if lowerStatusFilter == "disconnected" && isConnected { + statusEval = true + } else if lowerStatusFilter == "connected" && !isConnected { + statusEval = true + } + } + + if len(ipsFilter) > 0 { + _, ok := ipsFilter[peerState.IP] + if !ok { + ipEval = true + } + } + + if len(prefixNamesFilter) > 0 { + for prefixNameFilter := range prefixNamesFilterMap { + if strings.HasPrefix(peerState.Fqdn, prefixNameFilter) { + nameEval = false + break + } + } + } else { + nameEval = false + } + + return statusEval || ipEval || nameEval +} + +func toIEC(b int64) string { + const unit = 1024 + if b < unit { + return fmt.Sprintf("%d B", b) + } + div, exp := int64(unit), 0 + for n := b / unit; n >= unit; n /= unit { + div *= unit + exp++ + } + return fmt.Sprintf("%.1f %ciB", + float64(b)/float64(div), "KMGTPE"[exp]) +} + +func countEnabled(dnsServers []NsServerGroupStateOutput) int { + count := 0 + for _, server := range dnsServers { + if server.Enabled { + count++ + } + } + return count +} + +// timeAgo returns a string representing the duration since the provided time in a human-readable format. +func timeAgo(t time.Time) string { + if t.IsZero() || t.Equal(time.Unix(0, 0)) { + return "-" + } + duration := time.Since(t) + switch { + case duration < time.Second: + return "Now" + case duration < time.Minute: + seconds := int(duration.Seconds()) + if seconds == 1 { + return "1 second ago" + } + return fmt.Sprintf("%d seconds ago", seconds) + case duration < time.Hour: + minutes := int(duration.Minutes()) + seconds := int(duration.Seconds()) % 60 + if minutes == 1 { + if seconds == 1 { + return "1 minute, 1 second ago" + } else if seconds > 0 { + return fmt.Sprintf("1 minute, %d seconds ago", seconds) + } + return "1 minute ago" + } + if seconds > 0 { + return fmt.Sprintf("%d minutes, %d seconds ago", minutes, seconds) + } + return fmt.Sprintf("%d minutes ago", minutes) + case duration < 24*time.Hour: + hours := int(duration.Hours()) + minutes := int(duration.Minutes()) % 60 + if hours == 1 { + if minutes == 1 { + return "1 hour, 1 minute ago" + } else if minutes > 0 { + return fmt.Sprintf("1 hour, %d minutes ago", minutes) + } + return "1 hour ago" + } + if minutes > 0 { + return fmt.Sprintf("%d hours, %d minutes ago", hours, minutes) + } + return fmt.Sprintf("%d hours ago", hours) + } + + days := int(duration.Hours()) / 24 + hours := int(duration.Hours()) % 24 + if days == 1 { + if hours == 1 { + return "1 day, 1 hour ago" + } else if hours > 0 { + return fmt.Sprintf("1 day, %d hours ago", hours) + } + return "1 day ago" + } + if hours > 0 { + return fmt.Sprintf("%d days, %d hours ago", days, hours) + } + return fmt.Sprintf("%d days ago", days) +} + +func anonymizePeerDetail(a *anonymize.Anonymizer, peer *PeerStateDetailOutput) { + peer.FQDN = a.AnonymizeDomain(peer.FQDN) + if localIP, port, err := net.SplitHostPort(peer.IceCandidateEndpoint.Local); err == nil { + peer.IceCandidateEndpoint.Local = fmt.Sprintf("%s:%s", a.AnonymizeIPString(localIP), port) + } + if remoteIP, port, err := net.SplitHostPort(peer.IceCandidateEndpoint.Remote); err == nil { + peer.IceCandidateEndpoint.Remote = fmt.Sprintf("%s:%s", a.AnonymizeIPString(remoteIP), port) + } + + peer.RelayAddress = a.AnonymizeURI(peer.RelayAddress) + + for i, route := range peer.Networks { + peer.Networks[i] = a.AnonymizeIPString(route) + } + + for i, route := range peer.Networks { + peer.Networks[i] = a.AnonymizeRoute(route) + } +} + +func anonymizeOverview(a *anonymize.Anonymizer, overview *OutputOverview) { + for i, peer := range overview.Peers.Details { + peer := peer + anonymizePeerDetail(a, &peer) + overview.Peers.Details[i] = peer + } + + overview.ManagementState.URL = a.AnonymizeURI(overview.ManagementState.URL) + overview.ManagementState.Error = a.AnonymizeString(overview.ManagementState.Error) + overview.SignalState.URL = a.AnonymizeURI(overview.SignalState.URL) + overview.SignalState.Error = a.AnonymizeString(overview.SignalState.Error) + + overview.IP = a.AnonymizeIPString(overview.IP) + for i, detail := range overview.Relays.Details { + detail.URI = a.AnonymizeURI(detail.URI) + detail.Error = a.AnonymizeString(detail.Error) + overview.Relays.Details[i] = detail + } + + for i, nsGroup := range overview.NSServerGroups { + for j, domain := range nsGroup.Domains { + overview.NSServerGroups[i].Domains[j] = a.AnonymizeDomain(domain) + } + for j, ns := range nsGroup.Servers { + host, port, err := net.SplitHostPort(ns) + if err == nil { + overview.NSServerGroups[i].Servers[j] = fmt.Sprintf("%s:%s", a.AnonymizeIPString(host), port) + } + } + } + + for i, route := range overview.Networks { + overview.Networks[i] = a.AnonymizeRoute(route) + } + + overview.FQDN = a.AnonymizeDomain(overview.FQDN) + + for i, event := range overview.Events { + overview.Events[i].Message = a.AnonymizeString(event.Message) + overview.Events[i].UserMessage = a.AnonymizeString(event.UserMessage) + + for k, v := range event.Metadata { + event.Metadata[k] = a.AnonymizeString(v) + } + } +} diff --git a/client/status/status_test.go b/client/status/status_test.go new file mode 100644 index 000000000..24c4827d3 --- /dev/null +++ b/client/status/status_test.go @@ -0,0 +1,603 @@ +package status + +import ( + "bytes" + "encoding/json" + "fmt" + "runtime" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/durationpb" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/netbirdio/netbird/client/proto" + "github.com/netbirdio/netbird/version" +) + +func init() { + loc, err := time.LoadLocation("UTC") + if err != nil { + panic(err) + } + + time.Local = loc +} + +var resp = &proto.StatusResponse{ + Status: "Connected", + FullStatus: &proto.FullStatus{ + Peers: []*proto.PeerState{ + { + IP: "192.168.178.101", + PubKey: "Pubkey1", + Fqdn: "peer-1.awesome-domain.com", + ConnStatus: "Connected", + ConnStatusUpdate: timestamppb.New(time.Date(2001, time.Month(1), 1, 1, 1, 1, 0, time.UTC)), + Relayed: false, + LocalIceCandidateType: "", + RemoteIceCandidateType: "", + LocalIceCandidateEndpoint: "", + RemoteIceCandidateEndpoint: "", + LastWireguardHandshake: timestamppb.New(time.Date(2001, time.Month(1), 1, 1, 1, 2, 0, time.UTC)), + BytesRx: 200, + BytesTx: 100, + Networks: []string{ + "10.1.0.0/24", + }, + Latency: durationpb.New(time.Duration(10000000)), + }, + { + IP: "192.168.178.102", + PubKey: "Pubkey2", + Fqdn: "peer-2.awesome-domain.com", + ConnStatus: "Connected", + ConnStatusUpdate: timestamppb.New(time.Date(2002, time.Month(2), 2, 2, 2, 2, 0, time.UTC)), + Relayed: true, + LocalIceCandidateType: "relay", + RemoteIceCandidateType: "prflx", + LocalIceCandidateEndpoint: "10.0.0.1:10001", + RemoteIceCandidateEndpoint: "10.0.10.1:10002", + LastWireguardHandshake: timestamppb.New(time.Date(2002, time.Month(2), 2, 2, 2, 3, 0, time.UTC)), + BytesRx: 2000, + BytesTx: 1000, + Latency: durationpb.New(time.Duration(10000000)), + }, + }, + ManagementState: &proto.ManagementState{ + URL: "my-awesome-management.com:443", + Connected: true, + Error: "", + }, + SignalState: &proto.SignalState{ + URL: "my-awesome-signal.com:443", + Connected: true, + Error: "", + }, + Relays: []*proto.RelayState{ + { + URI: "stun:my-awesome-stun.com:3478", + Available: true, + Error: "", + }, + { + URI: "turns:my-awesome-turn.com:443?transport=tcp", + Available: false, + Error: "context: deadline exceeded", + }, + }, + LocalPeerState: &proto.LocalPeerState{ + IP: "192.168.178.100/16", + PubKey: "Some-Pub-Key", + KernelInterface: true, + Fqdn: "some-localhost.awesome-domain.com", + Networks: []string{ + "10.10.0.0/24", + }, + }, + DnsServers: []*proto.NSGroupState{ + { + Servers: []string{ + "8.8.8.8:53", + }, + Domains: nil, + Enabled: true, + Error: "", + }, + { + Servers: []string{ + "1.1.1.1:53", + "2.2.2.2:53", + }, + Domains: []string{ + "example.com", + "example.net", + }, + Enabled: false, + Error: "timeout", + }, + }, + }, + DaemonVersion: "0.14.1", +} + +var overview = OutputOverview{ + Peers: PeersStateOutput{ + Total: 2, + Connected: 2, + Details: []PeerStateDetailOutput{ + { + IP: "192.168.178.101", + PubKey: "Pubkey1", + FQDN: "peer-1.awesome-domain.com", + Status: "Connected", + LastStatusUpdate: time.Date(2001, 1, 1, 1, 1, 1, 0, time.UTC), + ConnType: "P2P", + IceCandidateType: IceCandidateType{ + Local: "", + Remote: "", + }, + IceCandidateEndpoint: IceCandidateType{ + Local: "", + Remote: "", + }, + LastWireguardHandshake: time.Date(2001, 1, 1, 1, 1, 2, 0, time.UTC), + TransferReceived: 200, + TransferSent: 100, + Networks: []string{ + "10.1.0.0/24", + }, + Latency: time.Duration(10000000), + }, + { + IP: "192.168.178.102", + PubKey: "Pubkey2", + FQDN: "peer-2.awesome-domain.com", + Status: "Connected", + LastStatusUpdate: time.Date(2002, 2, 2, 2, 2, 2, 0, time.UTC), + ConnType: "Relayed", + IceCandidateType: IceCandidateType{ + Local: "relay", + Remote: "prflx", + }, + IceCandidateEndpoint: IceCandidateType{ + Local: "10.0.0.1:10001", + Remote: "10.0.10.1:10002", + }, + LastWireguardHandshake: time.Date(2002, 2, 2, 2, 2, 3, 0, time.UTC), + TransferReceived: 2000, + TransferSent: 1000, + Latency: time.Duration(10000000), + }, + }, + }, + Events: []SystemEventOutput{}, + CliVersion: version.NetbirdVersion(), + DaemonVersion: "0.14.1", + ManagementState: ManagementStateOutput{ + URL: "my-awesome-management.com:443", + Connected: true, + Error: "", + }, + SignalState: SignalStateOutput{ + URL: "my-awesome-signal.com:443", + Connected: true, + Error: "", + }, + Relays: RelayStateOutput{ + Total: 2, + Available: 1, + Details: []RelayStateOutputDetail{ + { + URI: "stun:my-awesome-stun.com:3478", + Available: true, + Error: "", + }, + { + URI: "turns:my-awesome-turn.com:443?transport=tcp", + Available: false, + Error: "context: deadline exceeded", + }, + }, + }, + IP: "192.168.178.100/16", + PubKey: "Some-Pub-Key", + KernelInterface: true, + FQDN: "some-localhost.awesome-domain.com", + NSServerGroups: []NsServerGroupStateOutput{ + { + Servers: []string{ + "8.8.8.8:53", + }, + Domains: nil, + Enabled: true, + Error: "", + }, + { + Servers: []string{ + "1.1.1.1:53", + "2.2.2.2:53", + }, + Domains: []string{ + "example.com", + "example.net", + }, + Enabled: false, + Error: "timeout", + }, + }, + Networks: []string{ + "10.10.0.0/24", + }, +} + +func TestConversionFromFullStatusToOutputOverview(t *testing.T) { + convertedResult := ConvertToStatusOutputOverview(resp, false, "", nil, nil, nil) + + assert.Equal(t, overview, convertedResult) +} + +func TestSortingOfPeers(t *testing.T) { + peers := []PeerStateDetailOutput{ + { + IP: "192.168.178.104", + }, + { + IP: "192.168.178.102", + }, + { + IP: "192.168.178.101", + }, + { + IP: "192.168.178.105", + }, + { + IP: "192.168.178.103", + }, + } + + sortPeersByIP(peers) + + assert.Equal(t, peers[3].IP, "192.168.178.104") +} + +func TestParsingToJSON(t *testing.T) { + jsonString, _ := ParseToJSON(overview) + + //@formatter:off + expectedJSONString := ` + { + "peers": { + "total": 2, + "connected": 2, + "details": [ + { + "fqdn": "peer-1.awesome-domain.com", + "netbirdIp": "192.168.178.101", + "publicKey": "Pubkey1", + "status": "Connected", + "lastStatusUpdate": "2001-01-01T01:01:01Z", + "connectionType": "P2P", + "iceCandidateType": { + "local": "", + "remote": "" + }, + "iceCandidateEndpoint": { + "local": "", + "remote": "" + }, + "relayAddress": "", + "lastWireguardHandshake": "2001-01-01T01:01:02Z", + "transferReceived": 200, + "transferSent": 100, + "latency": 10000000, + "quantumResistance": false, + "networks": [ + "10.1.0.0/24" + ] + }, + { + "fqdn": "peer-2.awesome-domain.com", + "netbirdIp": "192.168.178.102", + "publicKey": "Pubkey2", + "status": "Connected", + "lastStatusUpdate": "2002-02-02T02:02:02Z", + "connectionType": "Relayed", + "iceCandidateType": { + "local": "relay", + "remote": "prflx" + }, + "iceCandidateEndpoint": { + "local": "10.0.0.1:10001", + "remote": "10.0.10.1:10002" + }, + "relayAddress": "", + "lastWireguardHandshake": "2002-02-02T02:02:03Z", + "transferReceived": 2000, + "transferSent": 1000, + "latency": 10000000, + "quantumResistance": false, + "networks": null + } + ] + }, + "cliVersion": "development", + "daemonVersion": "0.14.1", + "management": { + "url": "my-awesome-management.com:443", + "connected": true, + "error": "" + }, + "signal": { + "url": "my-awesome-signal.com:443", + "connected": true, + "error": "" + }, + "relays": { + "total": 2, + "available": 1, + "details": [ + { + "uri": "stun:my-awesome-stun.com:3478", + "available": true, + "error": "" + }, + { + "uri": "turns:my-awesome-turn.com:443?transport=tcp", + "available": false, + "error": "context: deadline exceeded" + } + ] + }, + "netbirdIp": "192.168.178.100/16", + "publicKey": "Some-Pub-Key", + "usesKernelInterface": true, + "fqdn": "some-localhost.awesome-domain.com", + "quantumResistance": false, + "quantumResistancePermissive": false, + "networks": [ + "10.10.0.0/24" + ], + "dnsServers": [ + { + "servers": [ + "8.8.8.8:53" + ], + "domains": null, + "enabled": true, + "error": "" + }, + { + "servers": [ + "1.1.1.1:53", + "2.2.2.2:53" + ], + "domains": [ + "example.com", + "example.net" + ], + "enabled": false, + "error": "timeout" + } + ], + "events": [] + }` + // @formatter:on + + var expectedJSON bytes.Buffer + require.NoError(t, json.Compact(&expectedJSON, []byte(expectedJSONString))) + + assert.Equal(t, expectedJSON.String(), jsonString) +} + +func TestParsingToYAML(t *testing.T) { + yaml, _ := ParseToYAML(overview) + + expectedYAML := + `peers: + total: 2 + connected: 2 + details: + - fqdn: peer-1.awesome-domain.com + netbirdIp: 192.168.178.101 + publicKey: Pubkey1 + status: Connected + lastStatusUpdate: 2001-01-01T01:01:01Z + connectionType: P2P + iceCandidateType: + local: "" + remote: "" + iceCandidateEndpoint: + local: "" + remote: "" + relayAddress: "" + lastWireguardHandshake: 2001-01-01T01:01:02Z + transferReceived: 200 + transferSent: 100 + latency: 10ms + quantumResistance: false + networks: + - 10.1.0.0/24 + - fqdn: peer-2.awesome-domain.com + netbirdIp: 192.168.178.102 + publicKey: Pubkey2 + status: Connected + lastStatusUpdate: 2002-02-02T02:02:02Z + connectionType: Relayed + iceCandidateType: + local: relay + remote: prflx + iceCandidateEndpoint: + local: 10.0.0.1:10001 + remote: 10.0.10.1:10002 + relayAddress: "" + lastWireguardHandshake: 2002-02-02T02:02:03Z + transferReceived: 2000 + transferSent: 1000 + latency: 10ms + quantumResistance: false + networks: [] +cliVersion: development +daemonVersion: 0.14.1 +management: + url: my-awesome-management.com:443 + connected: true + error: "" +signal: + url: my-awesome-signal.com:443 + connected: true + error: "" +relays: + total: 2 + available: 1 + details: + - uri: stun:my-awesome-stun.com:3478 + available: true + error: "" + - uri: turns:my-awesome-turn.com:443?transport=tcp + available: false + error: 'context: deadline exceeded' +netbirdIp: 192.168.178.100/16 +publicKey: Some-Pub-Key +usesKernelInterface: true +fqdn: some-localhost.awesome-domain.com +quantumResistance: false +quantumResistancePermissive: false +networks: + - 10.10.0.0/24 +dnsServers: + - servers: + - 8.8.8.8:53 + domains: [] + enabled: true + error: "" + - servers: + - 1.1.1.1:53 + - 2.2.2.2:53 + domains: + - example.com + - example.net + enabled: false + error: timeout +events: [] +` + + assert.Equal(t, expectedYAML, yaml) +} + +func TestParsingToDetail(t *testing.T) { + // Calculate time ago based on the fixture dates + lastConnectionUpdate1 := timeAgo(overview.Peers.Details[0].LastStatusUpdate) + lastHandshake1 := timeAgo(overview.Peers.Details[0].LastWireguardHandshake) + lastConnectionUpdate2 := timeAgo(overview.Peers.Details[1].LastStatusUpdate) + lastHandshake2 := timeAgo(overview.Peers.Details[1].LastWireguardHandshake) + + detail := ParseToFullDetailSummary(overview) + + expectedDetail := fmt.Sprintf( + `Peers detail: + peer-1.awesome-domain.com: + NetBird IP: 192.168.178.101 + Public key: Pubkey1 + Status: Connected + -- detail -- + Connection type: P2P + ICE candidate (Local/Remote): -/- + ICE candidate endpoints (Local/Remote): -/- + Relay server address: + Last connection update: %s + Last WireGuard handshake: %s + Transfer status (received/sent) 200 B/100 B + Quantum resistance: false + Networks: 10.1.0.0/24 + Latency: 10ms + + peer-2.awesome-domain.com: + NetBird IP: 192.168.178.102 + Public key: Pubkey2 + Status: Connected + -- detail -- + Connection type: Relayed + ICE candidate (Local/Remote): relay/prflx + ICE candidate endpoints (Local/Remote): 10.0.0.1:10001/10.0.10.1:10002 + Relay server address: + Last connection update: %s + Last WireGuard handshake: %s + Transfer status (received/sent) 2.0 KiB/1000 B + Quantum resistance: false + Networks: - + Latency: 10ms + +Events: No events recorded +OS: %s/%s +Daemon version: 0.14.1 +CLI version: %s +Management: Connected to my-awesome-management.com:443 +Signal: Connected to my-awesome-signal.com:443 +Relays: + [stun:my-awesome-stun.com:3478] is Available + [turns:my-awesome-turn.com:443?transport=tcp] is Unavailable, reason: context: deadline exceeded +Nameservers: + [8.8.8.8:53] for [.] is Available + [1.1.1.1:53, 2.2.2.2:53] for [example.com, example.net] is Unavailable, reason: timeout +FQDN: some-localhost.awesome-domain.com +NetBird IP: 192.168.178.100/16 +Interface type: Kernel +Quantum resistance: false +Networks: 10.10.0.0/24 +Peers count: 2/2 Connected +`, lastConnectionUpdate1, lastHandshake1, lastConnectionUpdate2, lastHandshake2, runtime.GOOS, runtime.GOARCH, overview.CliVersion) + + assert.Equal(t, expectedDetail, detail) +} + +func TestParsingToShortVersion(t *testing.T) { + shortVersion := ParseGeneralSummary(overview, false, false, false) + + expectedString := fmt.Sprintf("OS: %s/%s", runtime.GOOS, runtime.GOARCH) + ` +Daemon version: 0.14.1 +CLI version: development +Management: Connected +Signal: Connected +Relays: 1/2 Available +Nameservers: 1/2 Available +FQDN: some-localhost.awesome-domain.com +NetBird IP: 192.168.178.100/16 +Interface type: Kernel +Quantum resistance: false +Networks: 10.10.0.0/24 +Peers count: 2/2 Connected +` + + assert.Equal(t, expectedString, shortVersion) +} + +func TestTimeAgo(t *testing.T) { + now := time.Now() + + cases := []struct { + name string + input time.Time + expected string + }{ + {"Now", now, "Now"}, + {"Seconds ago", now.Add(-10 * time.Second), "10 seconds ago"}, + {"One minute ago", now.Add(-1 * time.Minute), "1 minute ago"}, + {"Minutes and seconds ago", now.Add(-(1*time.Minute + 30*time.Second)), "1 minute, 30 seconds ago"}, + {"One hour ago", now.Add(-1 * time.Hour), "1 hour ago"}, + {"Hours and minutes ago", now.Add(-(2*time.Hour + 15*time.Minute)), "2 hours, 15 minutes ago"}, + {"One day ago", now.Add(-24 * time.Hour), "1 day ago"}, + {"Multiple days ago", now.Add(-(72*time.Hour + 20*time.Minute)), "3 days ago"}, + {"Zero time", time.Time{}, "-"}, + {"Unix zero time", time.Unix(0, 0), "-"}, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + result := timeAgo(tc.input) + assert.Equal(t, tc.expected, result, "Failed %s", tc.name) + }) + } +} diff --git a/client/ui/bundled.go b/client/ui/bundled.go deleted file mode 100644 index e2c138b14..000000000 --- a/client/ui/bundled.go +++ /dev/null @@ -1,12 +0,0 @@ -// auto-generated -// Code generated by '$ fyne bundle'. DO NOT EDIT. - -package main - -import "fyne.io/fyne/v2" - -var resourceNetbirdSystemtrayConnectedPng = &fyne.StaticResource{ - StaticName: "netbird-systemtray-connected.png", - StaticContent: []byte( - "\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x01\x00\x00\x00\x01\x00\b\x06\x00\x00\x00\\r\xa8f\x00\x00\x00\xc3zTXtRaw profile type exif\x00\x00x\xdamP\xdb\r\xc3 \f\xfc\xf7\x14\x1d\xc1\xaf\x80\x19\x874\xa9\xd4\r:~\r8Q\x88r\x92χ\x9d\x1cư\xff\xbe\x1fx50)\xe8\x92-\x95\x94СE\vW\x17\x86\x03\xb53\xa1v>@\xc1S\x1dNɞų\x8c\x86\xa5\xf8\xeb\xa8\xd3d\x83T]-\x17#{Gc\x9d\x1bEGf\xbb\x19\xc5E\xd2&b\x17[\x18\x950\x12\x1e\r\n\x83:\x9e\x85\xa9X\xbe>a\xddq\x86\x8d\x80F\x92\xbb\xf7ir?k\xf6\xedm\x8b\x17\x85y\x17\x12t\x16\xd11\x80\xb4P\x90\xdaE\xf5\xf0\xa1\xfc#u-\x92:[L\xe2\vy\xda\xd3\x01\xf8\x03\xda\xd4Y\x17ݮ\xb7\xee\x00\x00\x01\x84iCCPICC profile\x00\x00x\x9c}\x91=H\xc3@\x1c\xc5_S\xa5\"-\x0e\x16\x14\x11\xccP\x9d\xec\xa2\"\xe2T\xabP\x84\n\xa5Vh\xd5\xc1\xe4\xd2/hҐ\xa4\xb88\n\xae\x05\a?\x16\xab\x0e.κ:\xb8\n\x82\xe0\a\x88\xb3\x83\x93\xa2\x8b\x94\xf8\xbf\xa4\xd0\"ƃ\xe3~\xbc\xbb\xf7\xb8{\a\b\x8d\nSͮ\x18\xa0j\x96\x91N\xc4\xc5lnU\f\xbcB\xc0\x00B\x18\xc1\xac\xc4L}.\x95J\xc2s|\xdd\xc3\xc7\u05fb(\xcf\xf2>\xf7\xe7\b)y\x93\x01>\x918\xc6t\xc3\"\xde \x9e\u07b4t\xce\xfb\xc4aV\x92\x14\xe2s\xe2q\x83.H\xfc\xc8u\xd9\xe57\xceE\x87\x05\x9e\x1962\xe9y\xe20\xb1X\xec`\xb9\x83Y\xc9P\x89\xa7\x88#\x8a\xaaQ\xbe\x90uY\xe1\xbc\xc5Y\xad\xd4X\xeb\x9e\xfc\x85\xc1\xbc\xb6\xb2\xccu\x9a\xc3H`\x11KHA\x84\x8c\x1aʨ\xc0B\x94V\x8d\x14\x13iڏ{\xf8\x87\x1c\x7f\x8a\\2\xb9\xca`\xe4X@\x15*$\xc7\x0f\xfe\a\xbf\xbb5\v\x93\x13nR0\x0et\xbf\xd8\xf6\xc7(\x10\xd8\x05\x9au\xdb\xfe>\xb6\xed\xe6\t\xe0\x7f\x06\xae\xb4\xb6\xbf\xda\x00f>I\xaf\xb7\xb5\xc8\x11з\r\\\\\xb75y\x0f\xb8\xdc\x01\x06\x9ftɐ\x1c\xc9OS(\x14\x80\xf73\xfa\xa6\x1c\xd0\x7f\v\xf4\xae\xb9\xbd\xb5\xf6q\xfa\x00d\xa8\xab\xe4\rpp\b\x8c\x15){\xdd\xe3\xdd=\x9d\xbd\xfd{\xa6\xd5\xdf\x0fںr\xd0VwQ\xba\x00\x00\rxiTXtXML:com.adobe.xmp\x00\x00\x00\x00\x00\n\n \n \n \n \n \n \n \n \n \n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\xf0C\xff\xd9\x00\x00\x00\x06bKGD\x00\xff\x00\xff\x00\xff\xa0\xbd\xa7\x93\x00\x00\x00\tpHYs\x00\x00\v\x13\x00\x00\v\x13\x01\x00\x9a\x9c\x18\x00\x00\x00\atIME\a\xe8\x02\x17\r$'\xdd\xf7ȗ\x00\x00\x13;IDATx\xda\xed\x9d]o\x14W\x9a\xc7\xff\xa7\xaamh\xbf\xc46,I`\x99\xa1\xc3\ni\xb5{1\x95O0\xe4\x1b\xc0'X\xf2\t`.W`hp\xa2\xb9\fH{O\xa3\xcc\xc5\xecJ3q\xa4\x1d\xed\xcdJx>Aj/\"EBJګL \xb1\x00g\xf1\v\xb6\xbb\xeb\xec\x85mb\f\xb6\xfb\xa5^Ω\xfa\xfd\xee\x928v\xf7\xa9z\xfe\xcfs\x9e\xa7ο$\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\u0603a\t\xc0g\xd6\x7f\x1f5\x92\x8e\"k\xd4\b\xa4s\xb2jH\x9afez\n\xfe\xdb\b\x00x\x81mF\xd3/CE]\xa3(\x94~c\xa5\x8b;\xc1\x0e\x83E\x7f{\xecF\xfcA\x8d\x95\x00g\xb3\xfb\\tQ\xd2o\xadtq]\xba(I\x81\x95,K345\xa3˒\x84\x00\x80SY~5Х0\xd0o\x13\xabK\x96R>\x9b\xe4o\xd4\x1a\xbd\x1e\xc7\b\x008\x93\xe9\xadtkM\x8a\x02i\xdaZ\x9aS\x99\x12\xea\xf6\xabJ\x80Հ\x02\xf7\xf4W\x13\xe9\xdan\xa6'\xe8sXw\xe9\xf6ؿ\xc6\xed_Z\x01\x00\x05d{\xed\xec\xe9!\xcf\xda\x7f\xbb\xf1\xf7Z/\x80U\x81<\x03\xdf\x12\xf8\xc5\xc5\x7f\xf2K\xe9O\x05\x00d\xfcje\xffx\xecF\xfc\xe1\xfe\x7fM\x05\x00\xd9\x04~3j$5ݲVWX\r\a\xe2?\xdc\x1e\xfb!\x00\x909ks\xd1\xd5Dj\x1a\xcb\x18ω\xe07j\xd5\xf74\xfe\x10\x00Ȅ\x95O\xa3(Ht_R\xc4\xdeҙҿ\xbdw췟\x80\x15\x82\xb4\xb2~\x90\xe8+I\x11\xab\xe1\x0e\xd6\xea\xc1A\xd9\x7f[\x1f\x00\x86\xdc\xeb\xdbP\xf7E\x93\xcf\xc9\xec\xbf\x7f\xec\xc7\x16\x00\xd2\v\xfe\xb9\xe8b\"}axd\xd7\xcd\xf8O\x0e.\xfd\xd9\x02\xc0\xb0\xc1\x7f\xcbJ\x0f\t~G\t4_\xbf\x19\xb7\x8e\xfa1*\x00\xe8\x9b\xd5O\xa2\xfb\x8c\xf7\x1c\xcf\xfe\x81~\xd7\xcb\xcf!\x00\xd03\xb6\x19M\xaf\x87\xfaB\x96\xfd\xbe\xd3\xc1\x7f\xc8؏-\x00\fV\xf27\xa3\xc6z\xa8\x87\xa2\xd9\xe7x\xf4\x1f>\xf6\xa3\x02\x80\x81\x82\xdf\xd6\xf4\x10\a\x1e\x0f\xe2?\xd1\xed\xfa\x8d\u07b2\xff\xb6^\x00\x10\xfc\xa5\xc9\xfeG\x8d\xfdJW\x01\xd8f4\xfd\xf2ؾNt\xe7\xe8\x9b5\f\xb4\xdc\r\xb4\xbc\xfb\xcf\xc77\xb4l\x9a\xf12wѾ=?\xc1\xef\xcf\xf5\xb2\xbd5\xfe\x9c\xac\x00\xd6\x7f\x1f5\xc2D\xd3[\x89\x1a\xd6jZ\x81\xa6\x8d\xd5t`tn\xe7\xcb5$M\xcb\xec\x04{\x867\xa5\x95\x96\x8dѲ\xacvK\xa9ec\xb4\x9cX-Z\xa3ec\xd5\x0e\xa4e\xd5\xd4\xee\xb5\xd9\xe2#ks\x11O\xf6\xf9\x92\xfc\x8dZ\xf5\x1b\xf1\xc7N\n\x80mF\xd3[#jlv\x15)\xd0\xf4\x1e\xfb憌\xa6\xbd\xcf0F\xed\x1d\xb1X\xb6\xd2\xffX\xabvh\xd4>\xdeU\xeckU\xb1v'\xfaLF\xd7\b-On\xc1\x9a>\x18$\x19\x99,\x82<\b\xf4\x1b\xb3\xed\xed\x16Y\xa9Q\xe5\x87E\xac\xb4l\xa4XFq\"-\x86V\xb1\xeb°\xf3\x90O\x93\xb0\xf2\xe6\x1e\xbb=>\x1b\x0ft\xbd\xcc\xc0\x81nuq'\x93Gv\xfb\xf4\x17O\x84\xf5G,\xa9\x9d\x18\xfd5\xb4\x8a\xeb\xb3\xf1\x82\v\x1fju.\xbad\xa4/\xb8<\xfeT\x9f\xf5\x8e>\x1c4\xa1\x98\xa3\x82}-\xd4Ek\xd4\xe0e\f\xb9\xb0 \xa3\xd8Z\xfdu\xac\xab\x85\xbc\xab\x04:\xfe\x1eƿ\xd5ǽ<\xf2ۓ\x00l~\x1aE\x9bV\x17\tv\x87\xaa\x04\xa3\x05c\xf5e\x1e\x15\xc2\xda'\xd1w\\s\xbf\xb2\x7f\xbfc\xbf7~\xc5\xda\\tU\xd2%\xcax/z\t\v\x89\u0557\xe1\x88\x16Ҟ>\xb0\xef\xf70\xfe\al\xfc\xbd\xf6;V>\x8d\"\x93p\xaa\xcb\xc7\xedBb\xf5 \r1\xd89\xd3\xff\x1dK\xeaQ\xf0\x0f8\xf6{\xeb\x16`ǹ\xf5!\xcbZM1X\x9d\x8b\xbe2\xcc\xfb\xbd\xaa\x06\x83\x9a>L\xa3\n|\xd5\x03X\xbf\x13]\xb1F\xf7Y^\uf677҃\xf1\xd9x\xbe\x97\x1f^\xb9\x13]\t\xb8\xee\xbe\t\xc0\xc0c\xbf\x03\x05\x00\x11([\x8d\xa8\xb6\x12͛\x11\xdd;,S\xd0\xf8\xf3\xef\xba\x0e\xdb\xf8\xdb\xcbkǁ\xeb7\xe3\x96U\xefG\t\xc1\xe94ѐ\xd15\xdb\xd1w\xab\x9fD\xf7w^\xb5\xfd\xfa\xde\x7f.\xbaE\xf0{\x16\xffI\xba\xf1i\x0e\xd8\x136\xcd\xf6\xdb\\\xa0d\xd9#It{\xe2fܢ\xf1\xe7\xe5\xf5[\x18\xbb\x11\x7f\x94\xb9\x00 \x02\x15\x10\x82\xe7\x13\xed`z\xe5\"\x8b\xe1\xd1eKa\xec׳\x00 \x02%\xde\x1dlִ\xf5\xf5\xafeF;\nO?Wp\xe2\x05\x8b\xe2z\xf0\xa74\xf6;\xb4\a\xb0\x9f\xf1ٸ\x99H\x0fX\xfer\xd1yt\xe6\x95\x10t\x16Oi\xeb\xeb_+y6\xc9\xc28\\\xb1\xf5c\xf3\x95\x9a\x00H\xd2\xc4l|\x05\x11(\x0fɳI\xd9\xcd\xda\x1b\x15\xc1\xae\x10ؕ:\x8b\xe4\x1e\xf7\xb2\xf2\x9d\xe8\xf94\xe0\xfa\\\xf4\x90w\xbb{^\xfaw\x03u\xbe9\xfb\x86\x00\xbc\x91\x15N\xac(<\xfd\\ft\x8bEs \xfb\xa79\xf6\xeb\xbb\x02\xd8\xe5eW\x97\xed\xf6\x11V\xf05\xfb\xff4ud\xf0oW\t\x13\xda\xfa\xfaW\xea>\x9eaъ\x8e\xff$۱|_~\x00ϛ\xd1\xf4h\xa8\x87<6\xeaa\xf6\xdfi\xfc\xf5}\x83\x8cvT\xbb\xf0\x98j\xa0\x88\xe0Ϩ\xf17P\x05 I3\xcdx9\xe8게\xda\\\x1e\xbf\x184\x9bo\vǯ\xd4\xfd\xfe\xa4l\x97\xd7H\xe4J\x98\xfdCy}_\xd1z3n\x9b\x8e>B\x04<\xca\xfe+\xf5\xa1\xbb\xfcݥ\xa9\x9d\xfe\xc1\b\v\x9a\xc75\x93n\xe7a8;\x90\xa4#\x02~\xd1Y<\x95\xe26\x82\xde@\xf6\xb5\xbf\xdaAM\xad<\xfe\xd4\xc05\x1d\"\xe0\ao\x1b\xfb\r\xbd\x9dx2\xa3-\xaa\x81\xec\xe2?\xc9'\xfbok͐`(\xe2p\x19\xb9YS\xe7љ\xd4\x05\xe0\xd5\xcd3\xdaQx\xf6\xa9\x82\xa9U\x16;\xc5\xec\x9f\xe5\xd8/\xb5\n`\x97\x89\xebql\x03}d%ު\xe3\x18\xdd\xc73\x99\x05\xff+\x81\xf9\xf6=\xb6\x04)R3\xba\x9c\xe7\xdfK\xa5\xad;q=\x8e%}\xcc\xe5s+\xfb\xe7\xf5xo\xf7Ɍ:\x8b\xef2%\x186\xf9\x1b\xb5F\xb7c\xc9/\x01\x90\xa4\xf1\xd9x\xdeXD\xc0\xa5\xec\x9fo\xafa\x82)\xc1\xb0\x84\xf9{q\xa4*\xd9\xf5\x9bqK\xa6\xff\x17\x14B\xda\xc18Y\xc8\xe1\x9e\xed\x9e\xc3iD`\x90\xb5S~\x8d\xbf\xd7[\x0e\x19\xc01\xe2b\xd9\xfa\xfaי\xee\xfd\x8f\xced\x89F.<\x96\xa9op1z\x8b\xc2\\\x1b\x7f\x99U\x00{\xb6\x03M\xac\xc5\n*\xfd{|\xde?\xdb\x0f\x11h\xeb\xd1i%?\x8fsAz\x89\xff\xa4\xb8X\xc9\xf4\xed\xc0T\x02E\x94\xe0g\x8a\x17\x80\xbd\xc5\xc0\xb9%\x85\x18\x8e\x1c\x16\x81\xf1؍\xf8â\xfe|\xa6m\xdb\x1dC\x91{\\\xe5\x9c\x12o\xc6c\xbf\x81>\xd3\xe2)u1\x1b98\xfe\xc3|\xc7~\xb9\n\x80$M\xcc\xc6\xd70\x14\xc9'\xfb\xbb\xea\xea\x83\b\x1c\x10\xfcF\xad\"\x1a\x7f\xb9\n\xc0\x8e\b\xe0*\x941\x9do\xdfw\xbb:A\x04\xf6\x97\xfe\xed\"\xc6~\x85\b\x80$muu\rC\x91lH\x9eMʮ\x8f\xba\xbfE\xf9\xfe\xa4\xec\xfa1.\x98$k\xf5\xa0\xe8쿭C9\x82\xa1HF\xe2Z\xf4د\x1f\xc2D#\xff\xf8\xb7j\x1b\x8c\x148\xf6+\xac\x02\x90\xb6\rE6\xbb\x9c L5\xab:\xd8\xf8;\xfc\x03\a\x95\x7fX\xa8ȱ_\xa1\x02\xb0+\x02\x1c#N\xa9\x8cܬ\xa9\xfbd\xc6\xcb\xcf\xdd\xf9\xf6\xbdJ\x9e\x1d0F\xad\xfa\u0378UY\x01\x90\xf0\x12H3\xfb{+^\xeb\xa3\xea~\xffwջh\xa1[\x0f\xc8\x15&\xc1\x88\xc0\xb0\x01t\xcc\xfb\x97y$\xcf&*u\x94\u0605\xb1\x9f3\x02\xb0W\x04\xf0\x12\xe8\x9fη\uf563\x8ay2S\x8d\x97\x9182\xf6sJ\x00vE\x00C\x91~3\xe7\xa4_\x8d\xbf#\xd8\xfa\xf6\xbd\xd27\x05\xf3\xb4\xf9\xeaO\x97\x1c\x01k\xb1\x1eK\x7f\a\x9f\xf7O\xe5F\x9cx\xa9\x91\v?\x946\xfb\xbb2\xf6s\xae\x02\xd8e\xe2z\x1c\a\x16/\x81#\xb3\xff\xd3\xc9\xd2\x05\xbf$ٕ\xe3\xea.M\x953\xfe\x1d6\xcaqj\x0eS\xbf\x19\xb7p\x15:<\xfb\xfb8\xf6\xeb\xb9\x1f\xf0\xfd\xc9\xd2\xf5\x03\x8cQ\xab>\x1b/ \x00}\x88\x00\xaeB\a\x04H\x05:\xe6\x9d\xc5S\xe5z> t\xdb\x17\xc3ɕ\x1e\xbb\x11\xdf\xc5Pd_\xf6\xffy\xdc\xfb\xb1_\xafUNR\x12\xa1+\xca\xe6\xcb{\x01\x90p\x15z#3~\x7f\xb2:\x95\xceҔ\xff[\x01\xa3\xf6XWw]\xff\x98N\xd7Z\x88\xc06e\x1b\xfbUA\xf0L\xa2ۦ\x19;?\xda6>,\xe6\xca\\t7\x90\xaeV\xb2\xf4/\xe9د\xa7\xed\xf3٧\nO\xfd\xecg\xf6wt\xec\xe7U\x05\xb0K\x95]\x85\xbc;\xed\x97\xf6w\xf7\xb0!hB}\xe4\xcbg\xf5fu\xab\xe8*\xe4\xb2\xcdW>\n\x10xw`\xc8\xc5\xe7\xfdK!\x00R\xf5\\\x85*yZn\x1fɳ\to\x1a\x82VZv}\xec\xe7\xb5\x00\xec\x1a\x8aTA\x04\x92g\x93J~\x1e\x13H\x1d\x7fƂ\xf7|\xca\xfe\x92'M\xc0\xfd\xac7\xa3\x86\xad顬\x1ae\xbd齲\xf9ʁ\x91\v\x8fe&\xd6]\x8e$o\x1a\x7f\xdeV\x00\xbb\x94\xddK\xa0ʍ?_\xab\x00\x97l\xbeJ/\x00e\x16\x01\xbbYSR\xd2C1C\xad\xcb\xcaqw{\x01\x81\xe6]\xb2\xf9\xaa\x84\x00\x94U\x04|\x1d}U\xb9\n0\x81\xbfgW\xbc\xbf\xd3\xca\xe4*T\xf9\xb1\x9f\x87U\x80oc\xbf\xd2\t\xc0\xae\b\x94\xc1U\xa8\xf3\xe8\fQ\xeeS\x15\xe0\xa8\xcdW\xe5\x04@\xda1\x14Q\xb1/Z\x1c\x86*>\xef\xef{\x15\xe0\xaa\xcdW%\x05@\x92\xea\xb3\U000423c6\"\xb6\x1bT\xca\x1d\xb7\x14U\x80Q\xdb\xd7\xc6_i\x05@\xf2\xd3U(\xf9i\x8a\xec\xdfo\x15P\xb0\x89\xa8-\x89}])\xdb\xcd\xf5\x9bq˗c\xc4e\xb7\xf9\xcan\xcb4Q\\\xf27j\x8d\xcf\xc6\xf3\b\x80\xc3\xf8\xe2%@\xe9?\xe0\xba\xfd4Uܸ4,\x8fGE\xa9\aή\x8b\x80]\xa93\xf6\x1bX\x01\x82B\xd6\xce\a\x9b/\x04\xc0\x13\x11\xe8,\x9e\"\x90\x87\xd9\x06,\x8f\xe7\\\xfb\xab\x1d\xd4\xd4*\xd3\x1aV⑳\xf1ٸ隗\x00c\xbf4*\xa8\xe3\xb2\xeb\xc7\xf2\x8b\xff\xa4\\ٿ2\x02 \xb9e(b7k\xec\xfd\xd3\x12Ҽ\x8eL\x97d\xecWY\x01\xd8\x15\x01#-\x14~\xd32\xf6K\xaf\x15\xf0S>\a\xa7j\xc6߇\xcc\x10\x80=\xbc\xec\xear\x91\x86\"v\xb3V\xdaW`\x15\xa3\x00A\xe6O\x06\x1a\xa3\xd6\xe8\xf58F\x00J@ѮB\x94\xfe\x19\xaci\xd6ۀ\xb0\xbc\xd6\xf4\x95@X\xbd\xec\x8f\x00d \x02\x8c\xfd\x1c\xe8\x03\xf4)\x02I\xc5\x1a\x7f\b\xc0\x80\"Ћ\xa1\bo\xf7q\xa1\x0f\xd0\xc76\xc0\xa8\x1d\xd6t\xb7\xaak\x85\x00\xf4\xc1Q\xaeB\xd8|\xb9\xb2\r\xe8\xdd&\xac\x8c6_\xfd`\xb8]\xfagu.\xfa\xcaH\xd1k7]7P盳\b\x80\v7\xf5hG#\xff\xfc\xbf=e\xff\xb1\x1b\xf1\aU^+*\x80\x01x\x9b\xa1\b6_\x0eU\x00\x9b\xb5\x9e\x9a\xb0>\xbeF\x0e\x01p\x80]W\xa1\xdd\x13\x84\xbc\xdd\xc7A\x8e\xd8\x06\x18\xa3V}6^@\x00``\x11\xd8=F\xcc\x13\x7f\xee\x91\x1c5\t\xa8\xe8\xd8\x0f\x01H\x91z3n'\x1b\xb5{\xae\xbc\xae\x1a\xf6l\x03\x0e\xa9\x00\xcan\xf3\x85\x00乀\xc7:Wk\x17~\x90\u0084\xc5pI\x00\xd6\x0e\xa8\x00\x8c\xdac\xdd\xea\x8e\xfd\x10\x80\x14Y\x9b\x8b\xaeʪaF;\x1a\xb9\xf0\x18\x11pI\x00\x0ehȚD\xb7M3^f\x85\x10\x80\xa1XoF\r\x19]{uc\xd574r\xfeG\x16\xc6\x15\xba\xc1\x9b\x0eA%}\xbb\x0f\x02P\x00IM\xb7d\xd5x\xed\xfe\x9aXWxn\x89\xc5q\xa6\x0f\xf0\xfa6\xc0\x84\xfa\x88UA\x00R\xc9\xfe\xc6\xea\xca\xdb\xfe[x\xe2\x05\"\xe0\xe06\xa0J6_\b@\xd6\xd9?\xd4\x17\x87\xfd\xf7\xf0\xc4\v\x85\xa7\x9f\xb3P\x8e\b\x80\x95\x96\x19\xfb!\x00\xa9\xb0r'\xba\xb2\xff1්\xc0\xfb\xcf\x11\x01w*\x80{d\x7f\x04 \x9d\x05\vt\xabןE\x04\nf\xedX%\xde\xee\x83\x00\xe4\xb5\xf7\x9f\x8b\xdeh\xfc!\x02\x0eW\x00ݠ\x926_\xfd\xc0i\xc0^\x83\xbf\x195l\xa8\xef\x06\xfd\xff;\x8b\xa70\n\xc9\xfb\xe6~g\xbd=\xd5\xfa\xaf\x0fX\t*\x80\xa1Ij\xbd\x97\xfeo\xa3vnI\xc1\x89\x17,d\x8e\x84'V\xc8\xfeT\x00\xc5g\xff\xbdl=:û\x02\xf2\xc8lc\x9b\xf3\xef\xfc\xe1?/\xb3\x12T\x00\xc3\xef%kz\x98\xd6\xef\x1a9\xffD\xa6\xbeɢf\x99\xd5F;\xeav&~\xc7J \x00C\xb3r'\xba\xd2o\xe3\xef\xf0\xba4\xd1ȅ\x1f\x10\x81,K\xff\xa9\xf5\xd6\xcc\x1f\xff\xd8f%\x10\x80\xa1K\xff~\xc6~\xfd\x88@\xed\xfc\x13\x99\xd1\x0e\x8b\x9c>\xed\xb0\xb1\xc4\xde\x1f\x01H#P\xf5/\xa9f\xff}ej\xed\xc2\x0f\x88@\xda7\xf4\xf4\x1ag\xfd\xfb\xb9\x0fY\x82\x83\xb3\x7fZ\x8d\xbfC\xfb\v\x9b5u\x1e\x9d\xc1O0\x8d\x9b\x99\xb1\x1f\x15@Z\f;\xf6\xa3\x12(\xe0f\x9e\xd8\xfa\x98U@\x00\x86fu.\xbat\xd0i\xbf\xccD\xe0\xfc\x8f\x18\x8a\fs#\x8fo\xb4&\xff\xed\xbf\x17X\t\x04`\xf8\x804\xfa,\xf7\xbfY\xdf\xc0Uh\b\x01\x1d9\xb9J\xe3\x0f\x01\x18\x9e\xd4\xc7~}\x8a@\r/\x81\xfe\x19\xedܮ\xdf]h\xb3\x10\b\xc0Pd6\xf6\xeb\xe7\x82L\xadb(\xd2\x1f\xedw\xce\xff\x80\xc9'\x020\xff\v\x8d?*\x80\xe1qe\xec\xd7\x0f\xb5\xb3O+\xed%\x10\x9e\xfa?J\x7f*\x80\x94\xb2\xbfcc\xbf\x9e\xe9\x06\xdb\xd6b\xeb\xa3\xd5\xcaV#\xc9\xfc;\xff>\x8f\xcd\x17\x15@\n\xd9\xff\x88\xb7\xfb\xb8\x9d\x06w\\\x85*t\x82Ќv\xd45DZ\xf9B\x00\x86\xa7\u05f7\xfb\xb8.\x02U:F\x8c\xcd\x17\x02\x90ޗv|\xec\xd7OV\xac\x88\b`\xf3\x85\x00\xa4\xb4\xf7\xf7d\xec\x87\b\xfc\x82\x95\xb0\xf9\xca\xea\xfe\xa9T\xf0\xfb\xdc\xf8;*H6k\xda\xfa\xe6\xac\xd4-\x97\xa6\xf3\xbc?\x15@j\xe4e\xf3UT%PFC\x11l\xbe\xa8\x00Ra\xe5\xd3(\n\x12}U\xf6\xefi\u05cfi\xeb\xd1\xe9RT\x02\xc1\xf8F\xeb\x9d\xcf\xff\x82\x00P\x01\xa4\xf0E\xad\xc7c\xbf~\x14\xbd\xbeQ\n/\x013\xdaQwk\x92\xc6\x1f\x02\x90B\xf6/\xd0\xe6\xab\b\xc2\x13/\xfcw\x15\x1a\xed\xdcf\xec\x87\x00\f\x8d\v6_\x85\x89\x80\xbf^\x02\xd8|!\x00)\xed\x89\x03]\xadR\xf6\x7fM\x04<5\x14\t\xde_\xc6\xe6+\xaf\xadVٳ\x7fY\xc7~\xfd\xe0\x93\xb5\x18c?*\x80\xd4(\xf3د\xac\x95\x006_\b@*\xac܉\xae\xe4\xf9v\x1f/D\xc0qW\xa1`|\x83\xe7\xfd\x11\x80\x94\xbeX@\xf6\x7fC\x04\xdcv\x15\xc2\xe6\v\x01H\x87\xb5\xb9\xa8\xb2\x8d\xbf\xa3\xa8\x9d[\x92\x99x\xe9\xde\xde\x1f\x9b\xafbֽl_\xc8G\x9b\xaf\xdcq\xccP\xc4Ԓ\xf6\xd4\x7f\xcc\xd3\xf8\xa3\x02\x18\x1e\x1fm\xbe\xf2\xdf\v\xec\x18\x8a8b-\x16\x9e}J\xe9O\x05\x90R\xf6g\xec\xd73v\xb3\xa6Σ3\xb2\x9bŽ\x1e\x02\x9b/*\x80\x14S\x89\xeesI\xfbP\xff\x82\xbd\x04\xb0\xf9B\x00Rc\xe5Nt\xc5J\x17\xb9\xa4\xfe\x88\x80\x19\xe92\xf6C\x00R\xfa\"\x8c\xfd|\x13\x01\xc6~\b@J{\xff\x92\xd9|\x15&\x02\xe7\x7f\xcc\xcdP\x04\x9b/G\xae\xbb\xf7\xc1ߌ\x1aI\xa8\xaf\x8c4\xcd\xe5L!0s0\x14\xe1\xed>T\x00\xa9\x91\xd4t\x8b\xe0O18\xeb\x1b\x1a9\xffc\xb67\xdd\xd4\x06.?T\x00\xe9d\x7f\xc6~\xd9\xd0}6\xa9\xee\xe2\xa9\xf4o8cZS\x7f\xfa\x13\x02@\x05\x90B\xb9Z\xd3C.a6d\xe1*dF;JFFh\xfc!\x00\xc3S5\x9b\xaf\xc2D \xcdc\xc4\xd8|!\x00\xa9d\xfef4\xcd\xd8/'\x11H\xcfK\x00\x9b/\x04 \x1d^\x86ⴟg\"\x10\x1c\xdf\xc2\xe6\xcbA\xbck\x02\xd2\xf8+\x8eA\xadŰ\xf9\xa2\x02H\rl\xbe\x8a\xad\x04\x061\x14\xc1\xe6\v\x01H\x85չ\xe8\x126_\xc5R;\xb7ԗ\b`\xf3\x85\x00\xa4\xb7_1\xfa\x8cK\xe6\x86\b\xf4\xe4%\x10&\xcb#'W\x19\xfb!\x00\xc3\xc3\xd8\xcf-z1\x141\xf5\xcd{\xf5\xbb\vd\x7f\x97\x93\xaa\x0f\x1f\x12\x9b/G\xe9\x06\xda\xfa\xe6\xecA\x86\"\xed\xe9?\xff\x99\xc6\x1f\x15\xc0\xf0`\xf3\xe5(ar\xe01\xe2Zc\x89ҟ\n \xa5\xec\xcf\xd8\xcfi\xf6[\x8ba\xf3E\x05\x90\xde\xcd\x15\xd2\xf8s>\x8b\xec1\x141a\x82͗G\xd4\\\xfep\xabs\xd1%\x19E\x92\xda\\*\xc7E\xe0XG#\xff\xf0X\x1b\x8b\xa7\xbe\x9c\xf9\xc3<\xd7\v\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00|\xe4\xff\x01\xf6P(\xf3)+S\x1f\x00\x00\x00\x00IEND\xaeB`\x82"), -} diff --git a/client/ui/client_ui.go b/client/ui/client_ui.go index 9ed40b0be..100076806 100644 --- a/client/ui/client_ui.go +++ b/client/ui/client_ui.go @@ -148,22 +148,24 @@ type serviceClient struct { icError []byte // systray menu items - mStatus *systray.MenuItem - mUp *systray.MenuItem - mDown *systray.MenuItem - mAdminPanel *systray.MenuItem - mSettings *systray.MenuItem - mAbout *systray.MenuItem - mVersionUI *systray.MenuItem - mVersionDaemon *systray.MenuItem - mUpdate *systray.MenuItem - mQuit *systray.MenuItem - mRoutes *systray.MenuItem - mAllowSSH *systray.MenuItem - mAutoConnect *systray.MenuItem - mEnableRosenpass *systray.MenuItem - mNotifications *systray.MenuItem - mAdvancedSettings *systray.MenuItem + mStatus *systray.MenuItem + mUp *systray.MenuItem + mDown *systray.MenuItem + mAdminPanel *systray.MenuItem + mSettings *systray.MenuItem + mAbout *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 + mNotifications *systray.MenuItem + mAdvancedSettings *systray.MenuItem + mCreateDebugBundle *systray.MenuItem + mExitNode *systray.MenuItem // application with main windows. app fyne.App @@ -200,6 +202,14 @@ type serviceClient struct { wRoutes fyne.Window eventManager *event.Manager + + exitNodeMu sync.Mutex + mExitNodeItems []menuHandler +} + +type menuHandler struct { + *systray.MenuItem + cancel context.CancelFunc } // newServiceClient instance constructor @@ -473,6 +483,9 @@ func (s *serviceClient) updateStatus() 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 } @@ -498,7 +511,8 @@ func (s *serviceClient) updateStatus() error { s.mStatus.SetTitle("Connected") s.mUp.Disable() s.mDown.Enable() - s.mRoutes.Enable() + s.mNetworks.Enable() + go s.updateExitNodes() systrayIconState = true } else if status.Status != string(internal.StatusConnected) && s.mUp.Disabled() { s.setDisconnectedStatus() @@ -554,7 +568,9 @@ func (s *serviceClient) setDisconnectedStatus() { s.mStatus.SetTitle("Disconnected") s.mDown.Disable() s.mUp.Enable() - s.mRoutes.Disable() + s.mNetworks.Disable() + s.mExitNode.Disable() + go s.updateExitNodes() } func (s *serviceClient) onTrayReady() { @@ -577,10 +593,16 @@ func (s *serviceClient) onTrayReady() { s.mEnableRosenpass = s.mSettings.AddSubMenuItemCheckbox("Enable Quantum-Resistance", "Enable post-quantum security via Rosenpass", false) s.mNotifications = s.mSettings.AddSubMenuItemCheckbox("Notifications", "Enable notifications", true) s.mAdvancedSettings = s.mSettings.AddSubMenuItem("Advanced Settings", "Advanced settings of the application") + s.mCreateDebugBundle = s.mSettings.AddSubMenuItem("Create Debug Bundle", "Create and open debug information bundle") s.loadSettings() - s.mRoutes = systray.AddMenuItem("Networks", "Open the networks management window") - s.mRoutes.Disable() + s.exitNodeMu.Lock() + s.mExitNode = systray.AddMenuItem("Exit Node", "Select exit node for routing traffic") + s.mExitNode.Disable() + s.exitNodeMu.Unlock() + + s.mNetworks = systray.AddMenuItem("Networks", "Open the networks management window") + s.mNetworks.Disable() systray.AddSeparator() s.mAbout = systray.AddMenuItem("About", "About") @@ -599,6 +621,9 @@ func (s *serviceClient) onTrayReady() { systray.AddSeparator() s.mQuit = systray.AddMenuItem("Quit", "Quit the client app") + // update exit node menu in case service is already connected + go s.updateExitNodes() + s.update.SetOnUpdateListener(s.onUpdateAvailable) go func() { s.getSrvConfig() @@ -614,6 +639,12 @@ func (s *serviceClient) onTrayReady() { 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 func() { @@ -628,7 +659,7 @@ func (s *serviceClient) onTrayReady() { defer s.mUp.Enable() err := s.menuUpClick() if err != nil { - s.runSelfCommand("error-msg", err.Error()) + s.app.SendNotification(fyne.NewNotification("Error", "Failed to connect to NetBird service")) return } }() @@ -638,7 +669,7 @@ func (s *serviceClient) onTrayReady() { defer s.mDown.Enable() err := s.menuDownClick() if err != nil { - s.runSelfCommand("error-msg", err.Error()) + s.app.SendNotification(fyne.NewNotification("Error", "Failed to connect to NetBird service")) return } }() @@ -676,6 +707,13 @@ func (s *serviceClient) onTrayReady() { defer s.getSrvConfig() s.runSelfCommand("settings", "true") }() + case <-s.mCreateDebugBundle.ClickedCh: + go func() { + if err := s.createAndOpenDebugBundle(); err != nil { + log.Errorf("Failed to create debug bundle: %v", err) + s.app.SendNotification(fyne.NewNotification("Error", "Failed to create debug bundle")) + } + }() case <-s.mQuit.ClickedCh: systray.Quit() return @@ -684,10 +722,10 @@ func (s *serviceClient) onTrayReady() { if err != nil { log.Errorf("%s", err) } - case <-s.mRoutes.ClickedCh: - s.mRoutes.Disable() + case <-s.mNetworks.ClickedCh: + s.mNetworks.Disable() go func() { - defer s.mRoutes.Enable() + defer s.mNetworks.Enable() s.runSelfCommand("networks", "true") }() case <-s.mNotifications.ClickedCh: @@ -718,7 +756,11 @@ func (s *serviceClient) runSelfCommand(command, arg string) { return } - cmd := exec.Command(proc, fmt.Sprintf("--%s=%s", command, arg)) + cmd := exec.Command(proc, + fmt.Sprintf("--%s=%s", command, arg), + fmt.Sprintf("--daemon-addr=%s", s.addr), + ) + out, err := cmd.CombinedOutput() if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() == 1 { log.Errorf("start %s UI: %v, %s", command, err, string(out)) @@ -737,7 +779,11 @@ func normalizedVersion(version string) string { return versionString } -func (s *serviceClient) onTrayExit() {} +func (s *serviceClient) onTrayExit() { + for _, item := range s.mExitNodeItems { + item.cancel() + } +} // getSrvClient connection to the service. func (s *serviceClient) getSrvClient(timeout time.Duration) (proto.DaemonServiceClient, error) { diff --git a/client/ui/config/config.go b/client/ui/config/config.go deleted file mode 100644 index fc3361b61..000000000 --- a/client/ui/config/config.go +++ /dev/null @@ -1,46 +0,0 @@ -package config - -import ( - "os" - "runtime" -) - -// ClientConfig basic settings for the UI application. -type ClientConfig struct { - configPath string - logFile string - daemonAddr string -} - -// Config object with default settings. -// -// We are creating this package to extract utility functions from the cmd package -// reading and parsing the configurations for the client should be done here -func Config() *ClientConfig { - defaultConfigPath := "/etc/wiretrustee/config.json" - defaultLogFile := "/var/log/wiretrustee/client.log" - if runtime.GOOS == "windows" { - defaultConfigPath = os.Getenv("PROGRAMDATA") + "\\Wiretrustee\\" + "config.json" - defaultLogFile = os.Getenv("PROGRAMDATA") + "\\Wiretrustee\\" + "client.log" - } - - defaultDaemonAddr := "unix:///var/run/wiretrustee.sock" - if runtime.GOOS == "windows" { - defaultDaemonAddr = "tcp://127.0.0.1:41731" - } - return &ClientConfig{ - configPath: defaultConfigPath, - logFile: defaultLogFile, - daemonAddr: defaultDaemonAddr, - } -} - -// DaemonAddr of the gRPC API. -func (c *ClientConfig) DaemonAddr() string { - return c.daemonAddr -} - -// LogFile path. -func (c *ClientConfig) LogFile() string { - return c.logFile -} diff --git a/client/ui/debug.go b/client/ui/debug.go new file mode 100644 index 000000000..845ea284c --- /dev/null +++ b/client/ui/debug.go @@ -0,0 +1,50 @@ +//go:build !(linux && 386) + +package main + +import ( + "fmt" + "path/filepath" + + "fyne.io/fyne/v2" + "github.com/skratchdot/open-golang/open" + + "github.com/netbirdio/netbird/client/proto" + nbstatus "github.com/netbirdio/netbird/client/status" +) + +func (s *serviceClient) createAndOpenDebugBundle() error { + conn, err := s.getSrvClient(failFastTimeout) + if err != nil { + return fmt.Errorf("get client: %v", err) + } + + statusResp, err := conn.Status(s.ctx, &proto.StatusRequest{GetFullPeerStatus: true}) + if err != nil { + return fmt.Errorf("failed to get status: %v", err) + } + + overview := nbstatus.ConvertToStatusOutputOverview(statusResp, true, "", nil, nil, nil) + statusOutput := nbstatus.ParseToFullDetailSummary(overview) + + resp, err := conn.DebugBundle(s.ctx, &proto.DebugBundleRequest{ + Anonymize: true, + Status: statusOutput, + SystemInfo: true, + }) + if err != nil { + return fmt.Errorf("failed to create debug bundle: %v", err) + } + + bundleDir := filepath.Dir(resp.GetPath()) + if err := open.Start(bundleDir); err != nil { + return fmt.Errorf("failed to open debug bundle directory: %v", err) + } + + s.app.SendNotification(fyne.NewNotification( + "Debug Bundle", + fmt.Sprintf("Debug bundle created at %s. Administrator privileges are required to access it.", resp.GetPath()), + )) + + return nil +} diff --git a/client/ui/event/event.go b/client/ui/event/event.go index 7925ee4d3..62a3c7c6a 100644 --- a/client/ui/event/event.go +++ b/client/ui/event/event.go @@ -3,6 +3,7 @@ package event import ( "context" "fmt" + "slices" "strings" "sync" "time" @@ -17,14 +18,17 @@ import ( "github.com/netbirdio/netbird/client/system" ) +type Handler func(*proto.SystemEvent) + type Manager struct { app fyne.App addr string - mu sync.Mutex - ctx context.Context - cancel context.CancelFunc - enabled bool + mu sync.Mutex + ctx context.Context + cancel context.CancelFunc + enabled bool + handlers []Handler } func NewManager(app fyne.App, addr string) *Manager { @@ -100,20 +104,41 @@ func (e *Manager) SetNotificationsEnabled(enabled bool) { func (e *Manager) handleEvent(event *proto.SystemEvent) { e.mu.Lock() enabled := e.enabled + handlers := slices.Clone(e.handlers) e.mu.Unlock() - if !enabled { + // critical events are always shown + if !enabled && event.Severity != proto.SystemEvent_CRITICAL { return } - title := e.getEventTitle(event) - e.app.SendNotification(fyne.NewNotification(title, event.UserMessage)) + if event.UserMessage != "" { + title := e.getEventTitle(event) + body := event.UserMessage + id := event.Metadata["id"] + if id != "" { + body += fmt.Sprintf(" ID: %s", id) + } + e.app.SendNotification(fyne.NewNotification(title, body)) + } + + for _, handler := range handlers { + go handler(event) + } +} + +func (e *Manager) AddHandler(handler Handler) { + e.mu.Lock() + defer e.mu.Unlock() + e.handlers = append(e.handlers, handler) } func (e *Manager) getEventTitle(event *proto.SystemEvent) string { var prefix string switch event.Severity { - case proto.SystemEvent_ERROR, proto.SystemEvent_CRITICAL: + case proto.SystemEvent_CRITICAL: + prefix = "Critical" + case proto.SystemEvent_ERROR: prefix = "Error" case proto.SystemEvent_WARNING: prefix = "Warning" diff --git a/client/ui/network.go b/client/ui/network.go index 852c4765b..750788cf3 100644 --- a/client/ui/network.go +++ b/client/ui/network.go @@ -3,7 +3,9 @@ package main import ( + "context" "fmt" + "runtime" "sort" "strings" "time" @@ -13,6 +15,7 @@ import ( "fyne.io/fyne/v2/dialog" "fyne.io/fyne/v2/layout" "fyne.io/fyne/v2/widget" + "fyne.io/systray" log "github.com/sirupsen/logrus" "github.com/netbirdio/netbird/client/proto" @@ -237,14 +240,14 @@ func (s *serviceClient) selectNetwork(id string, checked bool) { s.showError(fmt.Errorf("failed to select network: %v", err)) return } - log.Infof("Route %s selected", id) + log.Infof("Network '%s' selected", id) } else { if _, err := conn.DeselectNetworks(s.ctx, req); err != nil { log.Errorf("failed to deselect network: %v", err) s.showError(fmt.Errorf("failed to deselect network: %v", err)) return } - log.Infof("Network %s deselected", id) + log.Infof("Network '%s' deselected", id) } } @@ -324,6 +327,201 @@ func (s *serviceClient) updateNetworksBasedOnDisplayTab(tabs *container.AppTabs, s.updateNetworks(grid, f) } +func (s *serviceClient) updateExitNodes() { + conn, err := s.getSrvClient(defaultFailTimeout) + if err != nil { + log.Errorf("get client: %v", err) + return + } + + exitNodes, err := s.getExitNodes(conn) + if err != nil { + log.Errorf("get exit nodes: %v", err) + return + } + + s.exitNodeMu.Lock() + defer s.exitNodeMu.Unlock() + + s.recreateExitNodeMenu(exitNodes) + + if len(s.mExitNodeItems) > 0 { + s.mExitNode.Enable() + } else { + s.mExitNode.Disable() + } + + log.Debugf("Exit nodes updated: %d", len(s.mExitNodeItems)) +} + +func (s *serviceClient) recreateExitNodeMenu(exitNodes []*proto.Network) { + for _, node := range s.mExitNodeItems { + node.cancel() + node.Remove() + } + s.mExitNodeItems = nil + + if runtime.GOOS == "linux" || runtime.GOOS == "freebsd" { + s.mExitNode.Remove() + s.mExitNode = systray.AddMenuItem("Exit Node", "Select exit node for routing traffic") + } + + for _, node := range exitNodes { + menuItem := s.mExitNode.AddSubMenuItemCheckbox( + node.ID, + fmt.Sprintf("Use exit node %s", node.ID), + node.Selected, + ) + + ctx, cancel := context.WithCancel(context.Background()) + s.mExitNodeItems = append(s.mExitNodeItems, menuHandler{ + MenuItem: menuItem, + cancel: cancel, + }) + go s.handleChecked(ctx, node.ID, menuItem) + } + +} + +func (s *serviceClient) getExitNodes(conn proto.DaemonServiceClient) ([]*proto.Network, error) { + ctx, cancel := context.WithTimeout(s.ctx, defaultFailTimeout) + defer cancel() + + resp, err := conn.ListNetworks(ctx, &proto.ListNetworksRequest{}) + if err != nil { + return nil, fmt.Errorf("list networks: %v", err) + } + + var exitNodes []*proto.Network + for _, network := range resp.Routes { + if network.Range == "0.0.0.0/0" { + exitNodes = append(exitNodes, network) + } + } + return exitNodes, nil +} + +func (s *serviceClient) handleChecked(ctx context.Context, id string, item *systray.MenuItem) { + for { + select { + case <-ctx.Done(): + return + case _, ok := <-item.ClickedCh: + if !ok { + return + } + if err := s.toggleExitNode(id, item); err != nil { + log.Errorf("failed to toggle exit node: %v", err) + continue + } + } + } +} + +// Add function to toggle exit node selection +func (s *serviceClient) toggleExitNode(nodeID string, item *systray.MenuItem) error { + conn, err := s.getSrvClient(defaultFailTimeout) + if err != nil { + return fmt.Errorf("get client: %v", err) + } + + log.Infof("Toggling exit node '%s'", nodeID) + + s.exitNodeMu.Lock() + defer s.exitNodeMu.Unlock() + + exitNodes, err := s.getExitNodes(conn) + if err != nil { + return fmt.Errorf("get exit nodes: %v", err) + } + + var exitNode *proto.Network + // find other selected nodes and ours + ids := make([]string, 0, len(exitNodes)) + for _, node := range exitNodes { + if node.ID == nodeID { + // preserve original state + cp := *node //nolint:govet + exitNode = &cp + + // set desired state for recreation + node.Selected = true + continue + } + if node.Selected { + ids = append(ids, node.ID) + + // set desired state for recreation + node.Selected = false + } + } + + if item.Checked() && len(ids) == 0 { + // exit node is the only selected node, deselect it + ids = append(ids, nodeID) + exitNode = nil + } + + // deselect all other selected exit nodes + if err := s.deselectOtherExitNodes(conn, ids, item); err != nil { + return err + } + + if err := s.selectNewExitNode(conn, exitNode, nodeID, item); err != nil { + return err + } + + // linux/bsd doesn't handle Check/Uncheck well, so we recreate the menu + if runtime.GOOS == "linux" || runtime.GOOS == "freebsd" { + s.recreateExitNodeMenu(exitNodes) + } + + return nil +} + +func (s *serviceClient) deselectOtherExitNodes(conn proto.DaemonServiceClient, ids []string, currentItem *systray.MenuItem) error { + // deselect all other selected exit nodes + if len(ids) > 0 { + deselectReq := &proto.SelectNetworksRequest{ + NetworkIDs: ids, + } + if _, err := conn.DeselectNetworks(s.ctx, deselectReq); err != nil { + return fmt.Errorf("deselect networks: %v", err) + } + + log.Infof("Deselected exit nodes: %v", ids) + } + + // uncheck all other exit node menu items + for _, i := range s.mExitNodeItems { + if i.MenuItem == currentItem { + continue + } + i.Uncheck() + log.Infof("Unchecked exit node %v", i) + } + + return nil +} + +func (s *serviceClient) selectNewExitNode(conn proto.DaemonServiceClient, exitNode *proto.Network, nodeID string, item *systray.MenuItem) error { + if exitNode != nil && !exitNode.Selected { + selectReq := &proto.SelectNetworksRequest{ + NetworkIDs: []string{exitNode.ID}, + Append: true, + } + if _, err := conn.SelectNetworks(s.ctx, selectReq); err != nil { + return fmt.Errorf("select network: %v", err) + } + + log.Infof("Selected exit node '%s'", nodeID) + } + + item.Check() + + return nil +} + func getGridAndFilterFromTab(tabs *container.AppTabs, allGrid, overlappingGrid, exitNodesGrid *fyne.Container) (*fyne.Container, filter) { switch tabs.Selected().Text { case overlappingNetworksText: From a74208abac509aed52ea4fe170cb7892ca4a642b Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Fri, 21 Feb 2025 18:51:52 +0100 Subject: [PATCH 85/92] [client] Fix udp forwarder deadline (#3364) --- client/firewall/uspfilter/forwarder/udp.go | 44 ++++++++++------------ 1 file changed, 20 insertions(+), 24 deletions(-) diff --git a/client/firewall/uspfilter/forwarder/udp.go b/client/firewall/uspfilter/forwarder/udp.go index 97e4662fd..c37740587 100644 --- a/client/firewall/uspfilter/forwarder/udp.go +++ b/client/firewall/uspfilter/forwarder/udp.go @@ -245,33 +245,29 @@ func (c *udpPacketConn) copy(ctx context.Context, dst net.Conn, src net.Conn, bu defer bufPool.Put(bufp) buffer := *bufp - if err := src.SetReadDeadline(time.Now().Add(udpTimeout)); err != nil { - return fmt.Errorf("set read deadline: %w", err) - } - if err := src.SetWriteDeadline(time.Now().Add(udpTimeout)); err != nil { - return fmt.Errorf("set write deadline: %w", err) - } - for { - select { - case <-ctx.Done(): + if ctx.Err() != nil { return ctx.Err() - default: - n, err := src.Read(buffer) - if err != nil { - if isTimeout(err) { - continue - } - return fmt.Errorf("read from %s: %w", direction, err) - } - - _, err = dst.Write(buffer[:n]) - if err != nil { - return fmt.Errorf("write to %s: %w", direction, err) - } - - c.updateLastSeen() } + + if err := src.SetDeadline(time.Now().Add(udpTimeout)); err != nil { + return fmt.Errorf("set read deadline: %w", err) + } + + n, err := src.Read(buffer) + if err != nil { + if isTimeout(err) { + continue + } + return fmt.Errorf("read from %s: %w", direction, err) + } + + _, err = dst.Write(buffer[:n]) + if err != nil { + return fmt.Errorf("write to %s: %w", direction, err) + } + + c.updateLastSeen() } } From 73ce746ba7bcfbfe31ad4e2d6710236f1d663e62 Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Fri, 21 Feb 2025 19:07:43 +0100 Subject: [PATCH 86/92] [misc] Rename CI client tests (#3366) --- .github/workflows/golang-test-darwin.yml | 6 ++---- .github/workflows/golang-test-freebsd.yml | 4 ++-- .github/workflows/golang-test-windows.yml | 3 ++- .github/workflows/golangci-lint.yml | 11 +++++++++-- .github/workflows/mobile-build-validation.yml | 4 +++- 5 files changed, 18 insertions(+), 10 deletions(-) diff --git a/.github/workflows/golang-test-darwin.yml b/.github/workflows/golang-test-darwin.yml index 664e8be18..4571ce753 100644 --- a/.github/workflows/golang-test-darwin.yml +++ b/.github/workflows/golang-test-darwin.yml @@ -1,4 +1,4 @@ -name: Test Code Darwin +name: "Darwin" on: push: @@ -12,9 +12,7 @@ concurrency: jobs: test: - strategy: - matrix: - store: ['sqlite'] + name: "Client / Unit" runs-on: macos-latest steps: - name: Install Go diff --git a/.github/workflows/golang-test-freebsd.yml b/.github/workflows/golang-test-freebsd.yml index 0f510cb3a..e1c688b1b 100644 --- a/.github/workflows/golang-test-freebsd.yml +++ b/.github/workflows/golang-test-freebsd.yml @@ -1,5 +1,4 @@ - -name: Test Code FreeBSD +name: "FreeBSD" on: push: @@ -13,6 +12,7 @@ concurrency: jobs: test: + name: "Client / Unit" runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/golang-test-windows.yml b/.github/workflows/golang-test-windows.yml index 782e4c30a..d9ff0a84b 100644 --- a/.github/workflows/golang-test-windows.yml +++ b/.github/workflows/golang-test-windows.yml @@ -1,4 +1,4 @@ -name: Test Code Windows +name: "Windows" on: push: @@ -14,6 +14,7 @@ concurrency: jobs: test: + name: "Client / Unit" runs-on: windows-latest steps: - name: Checkout code diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index 6705a34ec..ca075d30f 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -1,4 +1,4 @@ -name: golangci-lint +name: Lint on: [pull_request] permissions: @@ -27,7 +27,14 @@ jobs: fail-fast: false matrix: os: [macos-latest, windows-latest, ubuntu-latest] - name: lint + include: + - os: macos-latest + display_name: Darwin + - os: windows-latest + display_name: Windows + - os: ubuntu-latest + display_name: Linux + name: ${{ matrix.display_name }} runs-on: ${{ matrix.os }} timeout-minutes: 15 steps: diff --git a/.github/workflows/mobile-build-validation.yml b/.github/workflows/mobile-build-validation.yml index dcf461a34..569956a54 100644 --- a/.github/workflows/mobile-build-validation.yml +++ b/.github/workflows/mobile-build-validation.yml @@ -1,4 +1,4 @@ -name: Mobile build validation +name: Mobile on: push: @@ -12,6 +12,7 @@ concurrency: jobs: android_build: + name: "Android / Build" runs-on: ubuntu-latest steps: - name: Checkout repository @@ -47,6 +48,7 @@ jobs: CGO_ENABLED: 0 ANDROID_NDK_HOME: /usr/local/lib/android/sdk/ndk/23.1.7779620 ios_build: + name: "iOS / Build" runs-on: macos-latest steps: - name: Checkout repository From 73101c897757c9f7ae7ed653f42dd91f0112a20b Mon Sep 17 00:00:00 2001 From: "M. Essam" Date: Fri, 21 Feb 2025 20:39:12 +0200 Subject: [PATCH 87/92] [client] Restart netbird-ui post-install in linux deb&rpm (#2992) --- .goreleaser_ui.yaml | 4 ++++ release_files/ui-post-install.sh | 10 ++++++++++ 2 files changed, 14 insertions(+) create mode 100644 release_files/ui-post-install.sh diff --git a/.goreleaser_ui.yaml b/.goreleaser_ui.yaml index 983aa0e78..1dd649d1b 100644 --- a/.goreleaser_ui.yaml +++ b/.goreleaser_ui.yaml @@ -50,6 +50,8 @@ nfpms: - netbird-ui formats: - deb + scripts: + postinstall: "release_files/ui-post-install.sh" contents: - src: client/ui/netbird.desktop dst: /usr/share/applications/netbird.desktop @@ -67,6 +69,8 @@ nfpms: - netbird-ui formats: - rpm + scripts: + postinstall: "release_files/ui-post-install.sh" contents: - src: client/ui/netbird.desktop dst: /usr/share/applications/netbird.desktop diff --git a/release_files/ui-post-install.sh b/release_files/ui-post-install.sh new file mode 100644 index 000000000..f6e8ddf92 --- /dev/null +++ b/release_files/ui-post-install.sh @@ -0,0 +1,10 @@ +#!/bin/sh + +# Check if netbird-ui is running +if pgrep -x -f /usr/bin/netbird-ui >/dev/null 2>&1; +then + runner=$(ps --no-headers -o '%U' -p $(pgrep -x -f /usr/bin/netbird-ui) | sed 's/^[ \t]*//;s/[ \t]*$//') + # Only re-run if it was already running + pkill -x -f /usr/bin/netbird-ui >/dev/null 2>&1 + su -l - "$runner" -c 'nohup /usr/bin/netbird-ui > /dev/null 2>&1 &' +fi From 9a0354b681fbb8e73be265252b99eb09242d0ab3 Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Fri, 21 Feb 2025 19:44:50 +0100 Subject: [PATCH 88/92] [client] Update local interface addresses when gathering candidates (#3324) --- client/iface/bind/udp_mux.go | 102 +++++++++++++++++------------ client/internal/dns/server_test.go | 4 +- client/internal/stdnet/filter.go | 1 - client/internal/stdnet/stdnet.go | 39 ++++++++++- 4 files changed, 98 insertions(+), 48 deletions(-) diff --git a/client/iface/bind/udp_mux.go b/client/iface/bind/udp_mux.go index 00a91f0ec..4c827de95 100644 --- a/client/iface/bind/udp_mux.go +++ b/client/iface/bind/udp_mux.go @@ -4,6 +4,7 @@ import ( "fmt" "io" "net" + "slices" "strings" "sync" @@ -152,46 +153,7 @@ func NewUDPMuxDefault(params UDPMuxParams) *UDPMuxDefault { params.Logger = logging.NewDefaultLoggerFactory().NewLogger("ice") } - var localAddrsForUnspecified []net.Addr - if addr, ok := params.UDPConn.LocalAddr().(*net.UDPAddr); !ok { - params.Logger.Errorf("LocalAddr is not a net.UDPAddr, got %T", params.UDPConn.LocalAddr()) - } else if ok && addr.IP.IsUnspecified() { - // For unspecified addresses, the correct behavior is to return errListenUnspecified, but - // it will break the applications that are already using unspecified UDP connection - // with UDPMuxDefault, so print a warn log and create a local address list for mux. - params.Logger.Warn("UDPMuxDefault should not listening on unspecified address, use NewMultiUDPMuxFromPort instead") - var networks []ice.NetworkType - switch { - - case addr.IP.To16() != nil: - networks = []ice.NetworkType{ice.NetworkTypeUDP4, ice.NetworkTypeUDP6} - - case addr.IP.To4() != nil: - networks = []ice.NetworkType{ice.NetworkTypeUDP4} - - default: - params.Logger.Errorf("LocalAddr expected IPV4 or IPV6, got %T", params.UDPConn.LocalAddr()) - } - if len(networks) > 0 { - if params.Net == nil { - var err error - if params.Net, err = stdnet.NewNet(); err != nil { - params.Logger.Errorf("failed to get create network: %v", err) - } - } - - ips, err := localInterfaces(params.Net, params.InterfaceFilter, nil, networks, true) - if err == nil { - for _, ip := range ips { - localAddrsForUnspecified = append(localAddrsForUnspecified, &net.UDPAddr{IP: ip, Port: addr.Port}) - } - } else { - params.Logger.Errorf("failed to get local interfaces for unspecified addr: %v", err) - } - } - } - - return &UDPMuxDefault{ + mux := &UDPMuxDefault{ addressMap: map[string][]*udpMuxedConn{}, params: params, connsIPv4: make(map[string]*udpMuxedConn), @@ -203,8 +165,55 @@ func NewUDPMuxDefault(params UDPMuxParams) *UDPMuxDefault { return newBufferHolder(receiveMTU + maxAddrSize) }, }, - localAddrsForUnspecified: localAddrsForUnspecified, } + + mux.updateLocalAddresses() + return mux +} + +func (m *UDPMuxDefault) updateLocalAddresses() { + var localAddrsForUnspecified []net.Addr + if addr, ok := m.params.UDPConn.LocalAddr().(*net.UDPAddr); !ok { + m.params.Logger.Errorf("LocalAddr is not a net.UDPAddr, got %T", m.params.UDPConn.LocalAddr()) + } else if ok && addr.IP.IsUnspecified() { + // For unspecified addresses, the correct behavior is to return errListenUnspecified, but + // it will break the applications that are already using unspecified UDP connection + // with UDPMuxDefault, so print a warn log and create a local address list for mux. + m.params.Logger.Warn("UDPMuxDefault should not listening on unspecified address, use NewMultiUDPMuxFromPort instead") + var networks []ice.NetworkType + switch { + + case addr.IP.To16() != nil: + networks = []ice.NetworkType{ice.NetworkTypeUDP4, ice.NetworkTypeUDP6} + + case addr.IP.To4() != nil: + networks = []ice.NetworkType{ice.NetworkTypeUDP4} + + default: + m.params.Logger.Errorf("LocalAddr expected IPV4 or IPV6, got %T", m.params.UDPConn.LocalAddr()) + } + if len(networks) > 0 { + if m.params.Net == nil { + var err error + if m.params.Net, err = stdnet.NewNet(); err != nil { + m.params.Logger.Errorf("failed to get create network: %v", err) + } + } + + ips, err := localInterfaces(m.params.Net, m.params.InterfaceFilter, nil, networks, true) + if err == nil { + for _, ip := range ips { + localAddrsForUnspecified = append(localAddrsForUnspecified, &net.UDPAddr{IP: ip, Port: addr.Port}) + } + } else { + m.params.Logger.Errorf("failed to get local interfaces for unspecified addr: %v", err) + } + } + } + + m.mu.Lock() + m.localAddrsForUnspecified = localAddrsForUnspecified + m.mu.Unlock() } // LocalAddr returns the listening address of this UDPMuxDefault @@ -214,8 +223,12 @@ func (m *UDPMuxDefault) LocalAddr() net.Addr { // GetListenAddresses returns the list of addresses that this mux is listening on func (m *UDPMuxDefault) GetListenAddresses() []net.Addr { + m.updateLocalAddresses() + + m.mu.Lock() + defer m.mu.Unlock() if len(m.localAddrsForUnspecified) > 0 { - return m.localAddrsForUnspecified + return slices.Clone(m.localAddrsForUnspecified) } return []net.Addr{m.LocalAddr()} @@ -225,7 +238,10 @@ func (m *UDPMuxDefault) GetListenAddresses() []net.Addr { // creates the connection if an existing one can't be found func (m *UDPMuxDefault) GetConn(ufrag string, addr net.Addr) (net.PacketConn, error) { // don't check addr for mux using unspecified address - if len(m.localAddrsForUnspecified) == 0 && m.params.UDPConn.LocalAddr().String() != addr.String() { + m.mu.Lock() + lenLocalAddrs := len(m.localAddrsForUnspecified) + m.mu.Unlock() + if lenLocalAddrs == 0 && m.params.UDPConn.LocalAddr().String() != addr.String() { return nil, fmt.Errorf("invalid address %s", addr.String()) } diff --git a/client/internal/dns/server_test.go b/client/internal/dns/server_test.go index 84779256f..94b87124b 100644 --- a/client/internal/dns/server_test.go +++ b/client/internal/dns/server_test.go @@ -413,7 +413,7 @@ func TestDNSFakeResolverHandleUpdates(t *testing.T) { defer t.Setenv("NB_WG_KERNEL_DISABLED", ov) t.Setenv("NB_WG_KERNEL_DISABLED", "true") - newNet, err := stdnet.NewNet(nil) + newNet, err := stdnet.NewNet([]string{"utun2301"}) if err != nil { t.Errorf("create stdnet: %v", err) return @@ -887,7 +887,7 @@ func createWgInterfaceWithBind(t *testing.T) (*iface.WGIface, error) { defer t.Setenv("NB_WG_KERNEL_DISABLED", ov) t.Setenv("NB_WG_KERNEL_DISABLED", "true") - newNet, err := stdnet.NewNet(nil) + newNet, err := stdnet.NewNet([]string{"utun2301"}) if err != nil { t.Fatalf("create stdnet: %v", err) return nil, err diff --git a/client/internal/stdnet/filter.go b/client/internal/stdnet/filter.go index c04250b2d..e45714001 100644 --- a/client/internal/stdnet/filter.go +++ b/client/internal/stdnet/filter.go @@ -21,7 +21,6 @@ func InterfaceFilter(disallowList []string) func(string) bool { for _, s := range disallowList { if strings.HasPrefix(iFace, s) && runtime.GOOS != "ios" { - log.Tracef("ignoring interface %s - it is not allowed", iFace) return false } } diff --git a/client/internal/stdnet/stdnet.go b/client/internal/stdnet/stdnet.go index 2e87475a5..aa9fdd045 100644 --- a/client/internal/stdnet/stdnet.go +++ b/client/internal/stdnet/stdnet.go @@ -5,11 +5,16 @@ package stdnet import ( "fmt" + "slices" + "sync" + "time" "github.com/pion/transport/v3" "github.com/pion/transport/v3/stdnet" ) +const updateInterval = 30 * time.Second + // Net is an implementation of the net.Net interface // based on functions of the standard net package. type Net struct { @@ -18,6 +23,10 @@ type Net struct { iFaceDiscover iFaceDiscover // interfaceFilter should return true if the given interfaceName is allowed interfaceFilter func(interfaceName string) bool + lastUpdate time.Time + + // mu is shared between interfaces and lastUpdate + mu sync.Mutex } // NewNetWithDiscover creates a new StdNet instance. @@ -43,18 +52,40 @@ func NewNet(disallowList []string) (*Net, error) { // The interfaces are discovered by an external iFaceDiscover function or by a default discoverer if the external one // wasn't specified. func (n *Net) UpdateInterfaces() (err error) { + n.mu.Lock() + defer n.mu.Unlock() + + return n.updateInterfaces() +} + +func (n *Net) updateInterfaces() (err error) { allIfaces, err := n.iFaceDiscover.iFaces() if err != nil { return err } + n.interfaces = n.filterInterfaces(allIfaces) + + n.lastUpdate = time.Now() + return nil } // Interfaces returns a slice of interfaces which are available on the // system func (n *Net) Interfaces() ([]*transport.Interface, error) { - return n.interfaces, nil + n.mu.Lock() + defer n.mu.Unlock() + + if time.Since(n.lastUpdate) < updateInterval { + return slices.Clone(n.interfaces), nil + } + + if err := n.updateInterfaces(); err != nil { + return nil, fmt.Errorf("update interfaces: %w", err) + } + + return slices.Clone(n.interfaces), nil } // InterfaceByIndex returns the interface specified by index. @@ -63,6 +94,8 @@ func (n *Net) Interfaces() ([]*transport.Interface, error) { // sharing the logical data link; for more precision use // InterfaceByName. func (n *Net) InterfaceByIndex(index int) (*transport.Interface, error) { + n.mu.Lock() + defer n.mu.Unlock() for _, ifc := range n.interfaces { if ifc.Index == index { return ifc, nil @@ -74,6 +107,8 @@ func (n *Net) InterfaceByIndex(index int) (*transport.Interface, error) { // InterfaceByName returns the interface specified by name. func (n *Net) InterfaceByName(name string) (*transport.Interface, error) { + n.mu.Lock() + defer n.mu.Unlock() for _, ifc := range n.interfaces { if ifc.Name == name { return ifc, nil @@ -87,7 +122,7 @@ func (n *Net) filterInterfaces(interfaces []*transport.Interface) []*transport.I if n.interfaceFilter == nil { return interfaces } - result := []*transport.Interface{} + var result []*transport.Interface for _, iface := range interfaces { if n.interfaceFilter(iface.Name) { result = append(result, iface) From b64bee35fa4cbe1b3b03620bc5ad84768652f920 Mon Sep 17 00:00:00 2001 From: Pedro Maia Costa <550684+pnmcosta@users.noreply.github.com> Date: Sat, 22 Feb 2025 10:31:39 +0000 Subject: [PATCH 89/92] [management] faster server bootstrap (#3365) Faster server bootstrap by counting accounts rather than fetching all from storage in the account manager instantiation. This change moved the deprecated need to ensure accounts have an All group to tests instead. --- management/server/account.go | 81 ++++------------------- management/server/store/sql_store.go | 20 +++++- management/server/store/sql_store_test.go | 43 +----------- management/server/store/store.go | 29 ++++++++ management/server/testdata/storev1.sql | 1 - management/server/types/account.go | 41 ++++++++++++ 6 files changed, 104 insertions(+), 111 deletions(-) diff --git a/management/server/account.go b/management/server/account.go index 76c984286..332d356e2 100644 --- a/management/server/account.go +++ b/management/server/account.go @@ -246,6 +246,11 @@ func BuildManager( integratedPeerValidator integrated_validator.IntegratedValidator, metrics telemetry.AppMetrics, ) (*DefaultAccountManager, error) { + start := time.Now() + defer func() { + log.WithContext(ctx).Debugf("took %v to instantiate account manager", time.Since(start)) + }() + am := &DefaultAccountManager{ Store: store, geo: geo, @@ -263,39 +268,21 @@ func BuildManager( metrics: metrics, requestBuffer: NewAccountRequestBuffer(ctx, store), } - allAccounts := store.GetAllAccounts(ctx) + accountsCounter, err := store.GetAccountsCounter(ctx) + if err != nil { + log.WithContext(ctx).Error(err) + } + // enable single account mode only if configured by user and number of existing accounts is not grater than 1 - am.singleAccountMode = singleAccountModeDomain != "" && len(allAccounts) <= 1 + am.singleAccountMode = singleAccountModeDomain != "" && accountsCounter <= 1 if am.singleAccountMode { if !isDomainValid(singleAccountModeDomain) { return nil, status.Errorf(status.InvalidArgument, "invalid domain \"%s\" provided for a single account mode. Please review your input for --single-account-mode-domain", singleAccountModeDomain) } am.singleAccountModeDomain = singleAccountModeDomain - log.WithContext(ctx).Infof("single account mode enabled, accounts number %d", len(allAccounts)) + log.WithContext(ctx).Infof("single account mode enabled, accounts number %d", accountsCounter) } else { - log.WithContext(ctx).Infof("single account mode disabled, accounts number %d", len(allAccounts)) - } - - // if account doesn't have a default group - // we create 'all' group and add all peers into it - // also we create default rule with source as destination - for _, account := range allAccounts { - shouldSave := false - - _, err := account.GetGroupAll() - if err != nil { - if err := addAllGroup(account); err != nil { - return nil, err - } - shouldSave = true - } - - if shouldSave { - err = store.SaveAccount(ctx, account) - if err != nil { - return nil, err - } - } + log.WithContext(ctx).Infof("single account mode disabled, accounts number %d", accountsCounter) } goCacheClient := gocache.New(CacheExpirationMax, 30*time.Minute) @@ -1619,46 +1606,6 @@ func (am *DefaultAccountManager) GetAccountSettings(ctx context.Context, account return am.Store.GetAccountSettings(ctx, store.LockingStrengthShare, accountID) } -// addAllGroup to account object if it doesn't exist -func addAllGroup(account *types.Account) error { - if len(account.Groups) == 0 { - allGroup := &types.Group{ - ID: xid.New().String(), - Name: "All", - Issued: types.GroupIssuedAPI, - } - for _, peer := range account.Peers { - allGroup.Peers = append(allGroup.Peers, peer.ID) - } - account.Groups = map[string]*types.Group{allGroup.ID: allGroup} - - id := xid.New().String() - - defaultPolicy := &types.Policy{ - ID: id, - Name: types.DefaultRuleName, - Description: types.DefaultRuleDescription, - Enabled: true, - Rules: []*types.PolicyRule{ - { - ID: id, - Name: types.DefaultRuleName, - Description: types.DefaultRuleDescription, - Enabled: true, - Sources: []string{allGroup.ID}, - Destinations: []string{allGroup.ID}, - Bidirectional: true, - Protocol: types.PolicyRuleProtocolALL, - Action: types.PolicyTrafficActionAccept, - }, - }, - } - - account.Policies = []*types.Policy{defaultPolicy} - } - return nil -} - // newAccountWithId creates a new Account with a default SetupKey (doesn't store in a Store) and provided id func newAccountWithId(ctx context.Context, accountID, userID, domain string) *types.Account { log.WithContext(ctx).Debugf("creating new account") @@ -1703,7 +1650,7 @@ func newAccountWithId(ctx context.Context, accountID, userID, domain string) *ty }, } - if err := addAllGroup(acc); err != nil { + if err := acc.AddAllGroup(); err != nil { log.WithContext(ctx).Errorf("error adding all group to account %s: %v", acc.Id, err) } return acc diff --git a/management/server/store/sql_store.go b/management/server/store/sql_store.go index 947694420..efc2539ff 100644 --- a/management/server/store/sql_store.go +++ b/management/server/store/sql_store.go @@ -15,7 +15,6 @@ import ( "sync" "time" - "github.com/netbirdio/netbird/management/server/util" log "github.com/sirupsen/logrus" "gorm.io/driver/mysql" "gorm.io/driver/postgres" @@ -24,6 +23,8 @@ import ( "gorm.io/gorm/clause" "gorm.io/gorm/logger" + "github.com/netbirdio/netbird/management/server/util" + nbdns "github.com/netbirdio/netbird/dns" "github.com/netbirdio/netbird/management/server/account" resourceTypes "github.com/netbirdio/netbird/management/server/networks/resources/types" @@ -615,6 +616,16 @@ func (s *SqlStore) GetResourceGroups(ctx context.Context, lockStrength LockingSt return groups, nil } +func (s *SqlStore) GetAccountsCounter(ctx context.Context) (int64, error) { + var count int64 + result := s.db.Model(&types.Account{}).Count(&count) + if result.Error != nil { + return 0, fmt.Errorf("failed to get all accounts counter: %w", result.Error) + } + + return count, nil +} + func (s *SqlStore) GetAllAccounts(ctx context.Context) (all []*types.Account) { var accounts []types.Account result := s.db.Find(&accounts) @@ -1035,6 +1046,13 @@ func NewSqliteStoreFromFileStore(ctx context.Context, fileStore *FileStore, data } for _, account := range fileStore.GetAllAccounts(ctx) { + _, err = account.GetGroupAll() + if err != nil { + if err := account.AddAllGroup(); err != nil { + return nil, err + } + } + err := store.SaveAccount(ctx, account) if err != nil { return nil, err diff --git a/management/server/store/sql_store_test.go b/management/server/store/sql_store_test.go index dd240ce6c..5cb092190 100644 --- a/management/server/store/sql_store_test.go +++ b/management/server/store/sql_store_test.go @@ -15,7 +15,6 @@ import ( "time" "github.com/google/uuid" - "github.com/rs/xid" log "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -2045,52 +2044,12 @@ func newAccountWithId(ctx context.Context, accountID, userID, domain string) *ty }, } - if err := addAllGroup(acc); err != nil { + if err := acc.AddAllGroup(); err != nil { log.WithContext(ctx).Errorf("error adding all group to account %s: %v", acc.Id, err) } return acc } -// addAllGroup to account object if it doesn't exist -func addAllGroup(account *types.Account) error { - if len(account.Groups) == 0 { - allGroup := &types.Group{ - ID: xid.New().String(), - Name: "All", - Issued: types.GroupIssuedAPI, - } - for _, peer := range account.Peers { - allGroup.Peers = append(allGroup.Peers, peer.ID) - } - account.Groups = map[string]*types.Group{allGroup.ID: allGroup} - - id := xid.New().String() - - defaultPolicy := &types.Policy{ - ID: id, - Name: types.DefaultRuleName, - Description: types.DefaultRuleDescription, - Enabled: true, - Rules: []*types.PolicyRule{ - { - ID: id, - Name: types.DefaultRuleName, - Description: types.DefaultRuleDescription, - Enabled: true, - Sources: []string{allGroup.ID}, - Destinations: []string{allGroup.ID}, - Bidirectional: true, - Protocol: types.PolicyRuleProtocolALL, - Action: types.PolicyTrafficActionAccept, - }, - }, - } - - account.Policies = []*types.Policy{defaultPolicy} - } - return nil -} - func TestSqlStore_GetAccountNetworks(t *testing.T) { store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/store.sql", t.TempDir()) t.Cleanup(cleanup) diff --git a/management/server/store/store.go b/management/server/store/store.go index e074c4c60..2686c3597 100644 --- a/management/server/store/store.go +++ b/management/server/store/store.go @@ -48,6 +48,7 @@ const ( ) type Store interface { + GetAccountsCounter(ctx context.Context) (int64, error) GetAllAccounts(ctx context.Context) []*types.Account GetAccount(ctx context.Context, accountID string) (*types.Account, error) AccountExists(ctx context.Context, lockStrength LockingStrength, id string) (bool, error) @@ -352,9 +353,37 @@ func NewTestStoreFromSQL(ctx context.Context, filename string, dataDir string) ( return nil, nil, fmt.Errorf("failed to create test store: %v", err) } + err = addAllGroupToAccount(ctx, store) + if err != nil { + return nil, nil, fmt.Errorf("failed to add all group to account: %v", err) + } + return getSqlStoreEngine(ctx, store, kind) } +func addAllGroupToAccount(ctx context.Context, store Store) error { + allAccounts := store.GetAllAccounts(ctx) + for _, account := range allAccounts { + shouldSave := false + + _, err := account.GetGroupAll() + if err != nil { + if err := account.AddAllGroup(); err != nil { + return err + } + shouldSave = true + } + + if shouldSave { + err = store.SaveAccount(ctx, account) + if err != nil { + return err + } + } + } + return nil +} + func getSqlStoreEngine(ctx context.Context, store *SqlStore, kind Engine) (Store, func(), error) { var cleanup func() var err error diff --git a/management/server/testdata/storev1.sql b/management/server/testdata/storev1.sql index cda333d4f..8b09ec2be 100644 --- a/management/server/testdata/storev1.sql +++ b/management/server/testdata/storev1.sql @@ -36,4 +36,3 @@ INSERT INTO peers VALUES('xlx9/9D8+ibnRiIIB8nHGMxGOzxV17r8ShPHgi4aYSM=','auth0|6 INSERT INTO peers VALUES('6kjbmVq1hmucVzvBXo5OucY5OYv+jSsB1jUTLq291Dw=','google-oauth2|103201118415301331038','6kjbmVq1hmucVzvBXo5OucY5OYv+jSsB1jUTLq291Dw=','5AFB60DB-61F2-4251-8E11-494847EE88E9','"100.64.0.2"','braginini','linux','Linux','21.04','x86_64','Ubuntu','','','','',NULL,'','','','{"Cloud":"","Platform":""}',NULL,'braginini','braginini','2021-12-24 16:12:05.994305438+01:00',0,0,0,'','',0,0,NULL,'2024-10-02 17:00:54.228182+02:00',0,'""','','',0); INSERT INTO peers VALUES('Ok+5QMdt/UjoktNOvicGYj+IX2g98p+0N2PJ3vJ45RI=','google-oauth2|103201118415301331038','Ok+5QMdt/UjoktNOvicGYj+IX2g98p+0N2PJ3vJ45RI=','A72E4DC2-00DE-4542-8A24-62945438104E','"100.64.0.1"','braginini','linux','Linux','21.04','x86_64','Ubuntu','','','','',NULL,'','','','{"Cloud":"","Platform":""}',NULL,'braginini','braginini-1','2021-12-24 16:11:27.015739803+01:00',0,0,0,'','',0,0,NULL,'2024-10-02 17:00:54.228182+02:00',1,'""','','',0); INSERT INTO installations VALUES(1,''); - diff --git a/management/server/types/account.go b/management/server/types/account.go index 4c68b9523..c890a7730 100644 --- a/management/server/types/account.go +++ b/management/server/types/account.go @@ -12,6 +12,7 @@ import ( "github.com/hashicorp/go-multierror" "github.com/miekg/dns" + "github.com/rs/xid" log "github.com/sirupsen/logrus" nbdns "github.com/netbirdio/netbird/dns" @@ -1525,3 +1526,43 @@ func getPoliciesSourcePeers(policies []*Policy, groups map[string]*Group) map[st return sourcePeers } + +// AddAllGroup to account object if it doesn't exist +func (a *Account) AddAllGroup() error { + if len(a.Groups) == 0 { + allGroup := &Group{ + ID: xid.New().String(), + Name: "All", + Issued: GroupIssuedAPI, + } + for _, peer := range a.Peers { + allGroup.Peers = append(allGroup.Peers, peer.ID) + } + a.Groups = map[string]*Group{allGroup.ID: allGroup} + + id := xid.New().String() + + defaultPolicy := &Policy{ + ID: id, + Name: DefaultRuleName, + Description: DefaultRuleDescription, + Enabled: true, + Rules: []*PolicyRule{ + { + ID: id, + Name: DefaultRuleName, + Description: DefaultRuleDescription, + Enabled: true, + Sources: []string{allGroup.ID}, + Destinations: []string{allGroup.ID}, + Bidirectional: true, + Protocol: PolicyRuleProtocolALL, + Action: PolicyTrafficActionAccept, + }, + }, + } + + a.Policies = []*Policy{defaultPolicy} + } + return nil +} From 559e6731079ed1c555984389f1a5c6cf485392c8 Mon Sep 17 00:00:00 2001 From: Carlos Hernandez Date: Sat, 22 Feb 2025 04:41:24 -0700 Subject: [PATCH 90/92] [client] fix privacy warning on macOS (#3350) * fix: macos privacy warning Move GetDesktopUIUserAgent to its own package so UI does not have to import client/system package that reaches out to broadcasts address. Thus, fixing the network privacy warnings. --- client/system/info.go | 6 ------ client/ui/client_ui.go | 5 +++-- client/ui/desktop/desktop.go | 8 ++++++++ client/ui/event/event.go | 4 ++-- 4 files changed, 13 insertions(+), 10 deletions(-) create mode 100644 client/ui/desktop/desktop.go diff --git a/client/system/info.go b/client/system/info.go index d83e9509a..2a0343ca6 100644 --- a/client/system/info.go +++ b/client/system/info.go @@ -9,7 +9,6 @@ import ( "google.golang.org/grpc/metadata" "github.com/netbirdio/netbird/management/proto" - "github.com/netbirdio/netbird/version" ) // DeviceNameCtxKey context key for device name @@ -119,11 +118,6 @@ func extractDeviceName(ctx context.Context, defaultName string) string { return v } -// GetDesktopUIUserAgent returns the Desktop ui user agent -func GetDesktopUIUserAgent() string { - return "netbird-desktop-ui/" + version.NetbirdVersion() -} - func networkAddresses() ([]NetworkAddress, error) { interfaces, err := net.Interfaces() if err != nil { diff --git a/client/ui/client_ui.go b/client/ui/client_ui.go index 100076806..bfc4cde16 100644 --- a/client/ui/client_ui.go +++ b/client/ui/client_ui.go @@ -33,7 +33,7 @@ import ( "github.com/netbirdio/netbird/client/internal" "github.com/netbirdio/netbird/client/proto" - "github.com/netbirdio/netbird/client/system" + "github.com/netbirdio/netbird/client/ui/desktop" "github.com/netbirdio/netbird/client/ui/event" "github.com/netbirdio/netbird/util" "github.com/netbirdio/netbird/version" @@ -779,6 +779,7 @@ func normalizedVersion(version string) string { return versionString } +// onTrayExit is called when the tray icon is closed. func (s *serviceClient) onTrayExit() { for _, item := range s.mExitNodeItems { item.cancel() @@ -799,7 +800,7 @@ func (s *serviceClient) getSrvClient(timeout time.Duration) (proto.DaemonService strings.TrimPrefix(s.addr, "tcp://"), grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithBlock(), - grpc.WithUserAgent(system.GetDesktopUIUserAgent()), + grpc.WithUserAgent(desktop.GetUIUserAgent()), ) if err != nil { return nil, fmt.Errorf("dial service: %w", err) diff --git a/client/ui/desktop/desktop.go b/client/ui/desktop/desktop.go new file mode 100644 index 000000000..0c99e2f38 --- /dev/null +++ b/client/ui/desktop/desktop.go @@ -0,0 +1,8 @@ +package desktop + +import "github.com/netbirdio/netbird/version" + +// GetUIUserAgent returns the Desktop ui user agent +func GetUIUserAgent() string { + return "netbird-desktop-ui/" + version.NetbirdVersion() +} diff --git a/client/ui/event/event.go b/client/ui/event/event.go index 62a3c7c6a..4d949416d 100644 --- a/client/ui/event/event.go +++ b/client/ui/event/event.go @@ -15,7 +15,7 @@ import ( "google.golang.org/grpc/credentials/insecure" "github.com/netbirdio/netbird/client/proto" - "github.com/netbirdio/netbird/client/system" + "github.com/netbirdio/netbird/client/ui/desktop" ) type Handler func(*proto.SystemEvent) @@ -167,7 +167,7 @@ func getClient(addr string) (proto.DaemonServiceClient, error) { conn, err := grpc.NewClient( strings.TrimPrefix(addr, "tcp://"), grpc.WithTransportCredentials(insecure.NewCredentials()), - grpc.WithUserAgent(system.GetDesktopUIUserAgent()), + grpc.WithUserAgent(desktop.GetUIUserAgent()), ) if err != nil { return nil, err From cc48594b0b15d3f3961a27729293f72eaa6bcdc7 Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Mon, 24 Feb 2025 01:14:31 +0100 Subject: [PATCH 91/92] [client][ui] Disable notifications by default (#3375) --- client/internal/config.go | 13 ++++++++++--- client/server/server.go | 9 +++++++-- client/ui/client_ui.go | 2 +- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/client/internal/config.go b/client/internal/config.go index b269a3854..b2f96cbdc 100644 --- a/client/internal/config.go +++ b/client/internal/config.go @@ -99,7 +99,7 @@ type Config struct { BlockLANAccess bool - DisableNotifications bool + DisableNotifications *bool DNSLabels domain.List @@ -479,13 +479,20 @@ func (config *Config) apply(input ConfigInput) (updated bool, err error) { updated = true } - if input.DisableNotifications != nil && *input.DisableNotifications != config.DisableNotifications { + if input.DisableNotifications != nil && input.DisableNotifications != config.DisableNotifications { if *input.DisableNotifications { log.Infof("disabling notifications") } else { log.Infof("enabling notifications") } - config.DisableNotifications = *input.DisableNotifications + config.DisableNotifications = input.DisableNotifications + updated = true + } + + if config.DisableNotifications == nil { + disabled := true + config.DisableNotifications = &disabled + log.Infof("setting notifications to disabled by default") updated = true } diff --git a/client/server/server.go b/client/server/server.go index 2efbb94ff..348fb9872 100644 --- a/client/server/server.go +++ b/client/server/server.go @@ -751,6 +751,11 @@ func (s *Server) GetConfig(_ context.Context, _ *proto.GetConfigRequest) (*proto } + disableNotifications := true + if s.config.DisableNotifications != nil { + disableNotifications = *s.config.DisableNotifications + } + return &proto.GetConfigResponse{ ManagementUrl: managementURL, ConfigFile: s.latestConfigInput.ConfigPath, @@ -763,14 +768,14 @@ func (s *Server) GetConfig(_ context.Context, _ *proto.GetConfigRequest) (*proto ServerSSHAllowed: *s.config.ServerSSHAllowed, RosenpassEnabled: s.config.RosenpassEnabled, RosenpassPermissive: s.config.RosenpassPermissive, - DisableNotifications: s.config.DisableNotifications, + DisableNotifications: disableNotifications, }, nil } func (s *Server) onSessionExpire() { if runtime.GOOS != "windows" { isUIActive := internal.CheckUIApp() - if !isUIActive && !s.config.DisableNotifications { + if !isUIActive && s.config.DisableNotifications != nil && !*s.config.DisableNotifications { if err := sendTerminalNotification(); err != nil { log.Errorf("send session expire terminal notification: %v", err) } diff --git a/client/ui/client_ui.go b/client/ui/client_ui.go index bfc4cde16..51eec59a5 100644 --- a/client/ui/client_ui.go +++ b/client/ui/client_ui.go @@ -591,7 +591,7 @@ func (s *serviceClient) onTrayReady() { s.mAllowSSH = s.mSettings.AddSubMenuItemCheckbox("Allow SSH", "Allow SSH connections", false) s.mAutoConnect = s.mSettings.AddSubMenuItemCheckbox("Connect on Startup", "Connect automatically when the service starts", false) s.mEnableRosenpass = s.mSettings.AddSubMenuItemCheckbox("Enable Quantum-Resistance", "Enable post-quantum security via Rosenpass", false) - s.mNotifications = s.mSettings.AddSubMenuItemCheckbox("Notifications", "Enable notifications", true) + s.mNotifications = s.mSettings.AddSubMenuItemCheckbox("Notifications", "Enable notifications", false) s.mAdvancedSettings = s.mSettings.AddSubMenuItem("Advanced Settings", "Advanced settings of the application") s.mCreateDebugBundle = s.mSettings.AddSubMenuItem("Create Debug Bundle", "Create and open debug information bundle") s.loadSettings() From dabdef4d67bee98b7a7c1fc2dc5fb310870ee6d4 Mon Sep 17 00:00:00 2001 From: hakansa <43675540+hakansa@users.noreply.github.com> Date: Mon, 24 Feb 2025 14:53:59 +0300 Subject: [PATCH 92/92] [client] fix extra DNS labels parameter to Register method in client (#3371) [client] fix extra DNS labels parameter to Register method in client (#3371) --- client/internal/engine_test.go | 7 ++++--- client/internal/login.go | 2 +- management/client/client.go | 2 +- management/client/client_test.go | 8 ++++---- management/client/grpc.go | 4 ++-- management/client/mock.go | 6 +++--- 6 files changed, 15 insertions(+), 14 deletions(-) diff --git a/client/internal/engine_test.go b/client/internal/engine_test.go index 599d36eab..02c8edea7 100644 --- a/client/internal/engine_test.go +++ b/client/internal/engine_test.go @@ -22,6 +22,9 @@ import ( "google.golang.org/grpc" "google.golang.org/grpc/keepalive" + wgdevice "golang.zx2c4.com/wireguard/device" + "golang.zx2c4.com/wireguard/tun/netstack" + "github.com/netbirdio/management-integrations/integrations" "github.com/netbirdio/netbird/client/iface" "github.com/netbirdio/netbird/client/iface/bind" @@ -49,8 +52,6 @@ import ( "github.com/netbirdio/netbird/signal/proto" signalServer "github.com/netbirdio/netbird/signal/server" "github.com/netbirdio/netbird/util" - wgdevice "golang.zx2c4.com/wireguard/device" - "golang.zx2c4.com/wireguard/tun/netstack" ) var ( @@ -1256,7 +1257,7 @@ func createEngine(ctx context.Context, cancel context.CancelFunc, setupKey strin } info := system.GetInfo(ctx) - resp, err := mgmtClient.Register(*publicKey, setupKey, "", info, nil) + resp, err := mgmtClient.Register(*publicKey, setupKey, "", info, nil, nil) if err != nil { return nil, err } diff --git a/client/internal/login.go b/client/internal/login.go index 092f2309c..395a17199 100644 --- a/client/internal/login.go +++ b/client/internal/login.go @@ -140,7 +140,7 @@ func registerPeer(ctx context.Context, serverPublicKey wgtypes.Key, client *mgm. config.DisableDNS, config.DisableFirewall, ) - loginResp, err := client.Register(serverPublicKey, validSetupKey.String(), jwtToken, info, pubSSHKey) + loginResp, err := client.Register(serverPublicKey, validSetupKey.String(), jwtToken, info, pubSSHKey, config.DNSLabels) if err != nil { log.Errorf("failed registering peer %v,%s", err, validSetupKey.String()) return nil, err diff --git a/management/client/client.go b/management/client/client.go index e9eeaccc1..950f6137e 100644 --- a/management/client/client.go +++ b/management/client/client.go @@ -15,7 +15,7 @@ type Client interface { io.Closer Sync(ctx context.Context, sysInfo *system.Info, msgHandler func(msg *proto.SyncResponse) error) error GetServerPublicKey() (*wgtypes.Key, error) - Register(serverKey wgtypes.Key, setupKey string, jwtToken string, sysInfo *system.Info, sshKey []byte) (*proto.LoginResponse, error) + Register(serverKey wgtypes.Key, setupKey string, jwtToken string, sysInfo *system.Info, sshKey []byte, dnsLabels domain.List) (*proto.LoginResponse, error) Login(serverKey wgtypes.Key, sysInfo *system.Info, sshKey []byte, dnsLabels domain.List) (*proto.LoginResponse, error) GetDeviceAuthorizationFlow(serverKey wgtypes.Key) (*proto.DeviceAuthorizationFlow, error) GetPKCEAuthorizationFlow(serverKey wgtypes.Key) (*proto.PKCEAuthorizationFlow, error) diff --git a/management/client/client_test.go b/management/client/client_test.go index 2bf802821..21f6b79ad 100644 --- a/management/client/client_test.go +++ b/management/client/client_test.go @@ -205,7 +205,7 @@ func TestClient_LoginRegistered(t *testing.T) { t.Error(err) } info := system.GetInfo(context.TODO()) - resp, err := client.Register(*key, ValidKey, "", info, nil) + resp, err := client.Register(*key, ValidKey, "", info, nil, nil) if err != nil { t.Error(err) } @@ -235,7 +235,7 @@ func TestClient_Sync(t *testing.T) { } info := system.GetInfo(context.TODO()) - _, err = client.Register(*serverKey, ValidKey, "", info, nil) + _, err = client.Register(*serverKey, ValidKey, "", info, nil, nil) if err != nil { t.Error(err) } @@ -251,7 +251,7 @@ func TestClient_Sync(t *testing.T) { } info = system.GetInfo(context.TODO()) - _, err = remoteClient.Register(*serverKey, ValidKey, "", info, nil) + _, err = remoteClient.Register(*serverKey, ValidKey, "", info, nil, nil) if err != nil { t.Fatal(err) } @@ -352,7 +352,7 @@ func Test_SystemMetaDataFromClient(t *testing.T) { } info := system.GetInfo(context.TODO()) - _, err = testClient.Register(*key, ValidKey, "", info, nil) + _, err = testClient.Register(*key, ValidKey, "", info, nil, nil) if err != nil { t.Errorf("error while trying to register client: %v", err) } diff --git a/management/client/grpc.go b/management/client/grpc.go index d02509c27..d3aaffec0 100644 --- a/management/client/grpc.go +++ b/management/client/grpc.go @@ -365,12 +365,12 @@ func (c *GrpcClient) login(serverKey wgtypes.Key, req *proto.LoginRequest) (*pro // Register registers peer on Management Server. It actually calls a Login endpoint with a provided setup key // Takes care of encrypting and decrypting messages. // This method will also collect system info and send it with the request (e.g. hostname, os, etc) -func (c *GrpcClient) Register(serverKey wgtypes.Key, setupKey string, jwtToken string, sysInfo *system.Info, pubSSHKey []byte) (*proto.LoginResponse, error) { +func (c *GrpcClient) Register(serverKey wgtypes.Key, setupKey string, jwtToken string, sysInfo *system.Info, pubSSHKey []byte, dnsLabels domain.List) (*proto.LoginResponse, error) { keys := &proto.PeerKeys{ SshPubKey: pubSSHKey, WgPubKey: []byte(c.key.PublicKey().String()), } - return c.login(serverKey, &proto.LoginRequest{SetupKey: setupKey, Meta: infoToMetaData(sysInfo), JwtToken: jwtToken, PeerKeys: keys}) + return c.login(serverKey, &proto.LoginRequest{SetupKey: setupKey, Meta: infoToMetaData(sysInfo), JwtToken: jwtToken, PeerKeys: keys, DnsLabels: dnsLabels.ToPunycodeList()}) } // Login attempts login to Management Server. Takes care of encrypting and decrypting messages. diff --git a/management/client/mock.go b/management/client/mock.go index 11564093a..9e1786f82 100644 --- a/management/client/mock.go +++ b/management/client/mock.go @@ -14,7 +14,7 @@ type MockClient struct { CloseFunc func() error SyncFunc func(ctx context.Context, sysInfo *system.Info, msgHandler func(msg *proto.SyncResponse) error) error GetServerPublicKeyFunc func() (*wgtypes.Key, error) - RegisterFunc func(serverKey wgtypes.Key, setupKey string, jwtToken string, info *system.Info, sshKey []byte) (*proto.LoginResponse, error) + RegisterFunc func(serverKey wgtypes.Key, setupKey string, jwtToken string, info *system.Info, sshKey []byte, dnsLabels domain.List) (*proto.LoginResponse, error) LoginFunc func(serverKey wgtypes.Key, info *system.Info, sshKey []byte, dnsLabels domain.List) (*proto.LoginResponse, error) GetDeviceAuthorizationFlowFunc func(serverKey wgtypes.Key) (*proto.DeviceAuthorizationFlow, error) GetPKCEAuthorizationFlowFunc func(serverKey wgtypes.Key) (*proto.PKCEAuthorizationFlow, error) @@ -46,11 +46,11 @@ func (m *MockClient) GetServerPublicKey() (*wgtypes.Key, error) { return m.GetServerPublicKeyFunc() } -func (m *MockClient) Register(serverKey wgtypes.Key, setupKey string, jwtToken string, info *system.Info, sshKey []byte) (*proto.LoginResponse, error) { +func (m *MockClient) Register(serverKey wgtypes.Key, setupKey string, jwtToken string, info *system.Info, sshKey []byte, dnsLabels domain.List) (*proto.LoginResponse, error) { if m.RegisterFunc == nil { return nil, nil } - return m.RegisterFunc(serverKey, setupKey, jwtToken, info, sshKey) + return m.RegisterFunc(serverKey, setupKey, jwtToken, info, sshKey, dnsLabels) } func (m *MockClient) Login(serverKey wgtypes.Key, info *system.Info, sshKey []byte, dnsLabels domain.List) (*proto.LoginResponse, error) {