diff --git a/.github/workflows/golang-test-linux.yml b/.github/workflows/golang-test-linux.yml index c30d08eed..120b213e9 100644 --- a/.github/workflows/golang-test-linux.yml +++ b/.github/workflows/golang-test-linux.yml @@ -86,7 +86,10 @@ jobs: run: CGO_ENABLED=0 go test -c -o sharedsock-testing.bin ./sharedsock - name: Generate RouteManager Test bin - run: CGO_ENABLED=1 go test -c -o routemanager-testing.bin -tags netgo -ldflags '-w -extldflags "-static -ldbus-1 -lpcap"' ./client/internal/routemanager/... + run: CGO_ENABLED=0 go test -c -o routemanager-testing.bin ./client/internal/routemanager + + - name: Generate SystemOps Test bin + run: CGO_ENABLED=1 go test -c -o systemops-testing.bin -tags netgo -ldflags '-w -extldflags "-static -ldbus-1 -lpcap"' ./client/internal/routemanager/systemops - name: Generate nftables Manager Test bin run: CGO_ENABLED=0 go test -c -o nftablesmanager-testing.bin ./client/firewall/nftables/... @@ -108,6 +111,9 @@ jobs: - name: Run RouteManager tests in docker run: docker run -t --cap-add=NET_ADMIN --privileged --rm -v $PWD:/ci -w /ci/client/internal/routemanager --entrypoint /busybox/sh gcr.io/distroless/base:debug -c /ci/routemanager-testing.bin -test.timeout 5m -test.parallel 1 + - name: Run SystemOps tests in docker + run: docker run -t --cap-add=NET_ADMIN --privileged --rm -v $PWD:/ci -w /ci/client/internal/routemanager/systemops --entrypoint /busybox/sh gcr.io/distroless/base:debug -c /ci/systemops-testing.bin -test.timeout 5m -test.parallel 1 + - name: Run nftables Manager tests in docker run: docker run -t --cap-add=NET_ADMIN --privileged --rm -v $PWD:/ci -w /ci/client/firewall --entrypoint /busybox/sh gcr.io/distroless/base:debug -c /ci/nftablesmanager-testing.bin -test.timeout 5m -test.parallel 1 diff --git a/client/cmd/root.go b/client/cmd/root.go index 839380712..f0b5d2bdf 100644 --- a/client/cmd/root.go +++ b/client/cmd/root.go @@ -36,6 +36,7 @@ const ( disableAutoConnectFlag = "disable-auto-connect" serverSSHAllowedFlag = "allow-server-ssh" extraIFaceBlackListFlag = "extra-iface-blacklist" + dnsRouteIntervalFlag = "dns-router-interval" ) var ( @@ -68,7 +69,9 @@ var ( autoConnectDisabled bool extraIFaceBlackList []string anonymizeFlag bool - rootCmd = &cobra.Command{ + dnsRouteInterval time.Duration + + rootCmd = &cobra.Command{ Use: "netbird", Short: "", Long: "", diff --git a/client/cmd/route.go b/client/cmd/route.go index d92e079ad..c8881822b 100644 --- a/client/cmd/route.go +++ b/client/cmd/route.go @@ -2,6 +2,7 @@ package cmd import ( "fmt" + "strings" "github.com/spf13/cobra" "google.golang.org/grpc/status" @@ -66,18 +67,60 @@ func routesList(cmd *cobra.Command, _ []string) error { return nil } - cmd.Println("Available Routes:") - for _, route := range resp.Routes { - selectedStatus := "Not Selected" - if route.GetSelected() { - selectedStatus = "Selected" - } - cmd.Printf("\n - ID: %s\n Network: %s\n Status: %s\n", route.GetID(), route.GetNetwork(), selectedStatus) - } + printRoutes(cmd, resp) return nil } +func printRoutes(cmd *cobra.Command, resp *proto.ListRoutesResponse) { + cmd.Println("Available Routes:") + for _, route := range resp.Routes { + printRoute(cmd, route) + } +} + +func printRoute(cmd *cobra.Command, route *proto.Route) { + selectedStatus := getSelectedStatus(route) + domains := route.GetDomains() + + if len(domains) > 0 { + printDomainRoute(cmd, route, domains, selectedStatus) + } else { + printNetworkRoute(cmd, route, selectedStatus) + } +} + +func getSelectedStatus(route *proto.Route) string { + if route.GetSelected() { + return "Selected" + } + return "Not Selected" +} + +func printDomainRoute(cmd *cobra.Command, route *proto.Route, domains []string, selectedStatus string) { + cmd.Printf("\n - ID: %s\n Domains: %s\n Status: %s\n", route.GetID(), strings.Join(domains, ", "), selectedStatus) + resolvedIPs := route.GetResolvedIPs() + + if len(resolvedIPs) > 0 { + printResolvedIPs(cmd, domains, resolvedIPs) + } else { + cmd.Printf(" Resolved IPs: -\n") + } +} + +func printNetworkRoute(cmd *cobra.Command, route *proto.Route, selectedStatus string) { + cmd.Printf("\n - ID: %s\n Network: %s\n Status: %s\n", route.GetID(), route.GetNetwork(), selectedStatus) +} + +func printResolvedIPs(cmd *cobra.Command, domains []string, resolvedIPs map[string]*proto.IPList) { + cmd.Printf(" Resolved IPs:\n") + for _, domain := range domains { + if ipList, exists := resolvedIPs[domain]; exists { + cmd.Printf(" [%s]: %s\n", domain, strings.Join(ipList.GetIps(), ", ")) + } + } +} + func routesSelect(cmd *cobra.Command, args []string) error { conn, err := getClient(cmd) if err != nil { diff --git a/client/cmd/status.go b/client/cmd/status.go index 3dacfbe4f..e6c7b8be8 100644 --- a/client/cmd/status.go +++ b/client/cmd/status.go @@ -807,11 +807,7 @@ func anonymizePeerDetail(a *anonymize.Anonymizer, peer *peerStateDetailOutput) { } for i, route := range peer.Routes { - prefix, err := netip.ParsePrefix(route) - if err == nil { - ip := a.AnonymizeIPString(prefix.Addr().String()) - peer.Routes[i] = fmt.Sprintf("%s/%d", ip, prefix.Bits()) - } + peer.Routes[i] = anonymizeRoute(a, route) } } @@ -847,12 +843,21 @@ func anonymizeOverview(a *anonymize.Anonymizer, overview *statusOutputOverview) } for i, route := range overview.Routes { - prefix, err := netip.ParsePrefix(route) - if err == nil { - ip := a.AnonymizeIPString(prefix.Addr().String()) - overview.Routes[i] = fmt.Sprintf("%s/%d", ip, prefix.Bits()) - } + overview.Routes[i] = anonymizeRoute(a, route) } overview.FQDN = a.AnonymizeDomain(overview.FQDN) } + +func anonymizeRoute(a *anonymize.Anonymizer, route string) string { + prefix, err := netip.ParsePrefix(route) + if err == nil { + ip := a.AnonymizeIPString(prefix.Addr().String()) + return fmt.Sprintf("%s/%d", ip, prefix.Bits()) + } + domains := strings.Split(route, ", ") + for i, domain := range domains { + domains[i] = a.AnonymizeDomain(domain) + } + return strings.Join(domains, ", ") +} diff --git a/client/cmd/up.go b/client/cmd/up.go index a5bbc58be..215635864 100644 --- a/client/cmd/up.go +++ b/client/cmd/up.go @@ -7,11 +7,13 @@ import ( "net/netip" "runtime" "strings" + "time" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "google.golang.org/grpc/codes" gstatus "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/durationpb" "github.com/netbirdio/netbird/client/internal" "github.com/netbirdio/netbird/client/internal/peer" @@ -42,6 +44,7 @@ func init() { upCmd.PersistentFlags().Uint16Var(&wireguardPort, wireguardPortFlag, iface.DefaultWgPort, "Wireguard interface listening port") upCmd.PersistentFlags().BoolVarP(&networkMonitor, networkMonitorFlag, "N", false, "Enable network monitoring") 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") } func upFunc(cmd *cobra.Command, args []string) error { @@ -137,6 +140,10 @@ func runInForegroundMode(ctx context.Context, cmd *cobra.Command) error { } } + if cmd.Flag(dnsRouteIntervalFlag).Changed { + ic.DNSRouteInterval = &dnsRouteInterval + } + config, err := internal.UpdateOrCreateConfig(ic) if err != nil { return fmt.Errorf("get config file: %v", err) @@ -237,6 +244,10 @@ func runInDaemonMode(ctx context.Context, cmd *cobra.Command) error { loginRequest.NetworkMonitor = &networkMonitor } + if cmd.Flag(dnsRouteIntervalFlag).Changed { + loginRequest.DnsRouteInterval = durationpb.New(dnsRouteInterval) + } + var loginErr error var loginResp *proto.LoginResponse diff --git a/client/errors/errors.go b/client/errors/errors.go new file mode 100644 index 000000000..cef999ac8 --- /dev/null +++ b/client/errors/errors.go @@ -0,0 +1,30 @@ +package errors + +import ( + "fmt" + "strings" + + "github.com/hashicorp/go-multierror" +) + +func formatError(es []error) string { + if len(es) == 0 { + return fmt.Sprintf("0 error occurred:\n\t* %s", es[0]) + } + + points := make([]string, len(es)) + for i, err := range es { + points[i] = fmt.Sprintf("* %s", err) + } + + return fmt.Sprintf( + "%d errors occurred:\n\t%s", + len(es), strings.Join(points, "\n\t")) +} + +func FormatErrorOrNil(err *multierror.Error) error { + if err != nil { + err.ErrorFormat = formatError + } + return err.ErrorOrNil() +} diff --git a/client/internal/config.go b/client/internal/config.go index 66721cd21..0b55d5ccb 100644 --- a/client/internal/config.go +++ b/client/internal/config.go @@ -7,12 +7,14 @@ import ( "os" "reflect" "strings" + "time" log "github.com/sirupsen/logrus" "golang.zx2c4.com/wireguard/wgctrl/wgtypes" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" + "github.com/netbirdio/netbird/client/internal/routemanager/dynamic" "github.com/netbirdio/netbird/client/ssh" "github.com/netbirdio/netbird/iface" mgm "github.com/netbirdio/netbird/management/client" @@ -53,6 +55,7 @@ type ConfigInput struct { NetworkMonitor *bool DisableAutoConnect *bool ExtraIFaceBlackList []string + DNSRouteInterval *time.Duration } // Config Configuration type @@ -95,6 +98,9 @@ type Config struct { // DisableAutoConnect determines whether the client should not start with the service // it's set to false by default due to backwards compatibility DisableAutoConnect bool + + // DNSRouteInterval is the interval in which the DNS routes are updated + DNSRouteInterval time.Duration } // ReadConfig read config file and return with Config. If it is not exists create a new with default values @@ -357,6 +363,18 @@ func (config *Config) apply(input ConfigInput) (updated bool, err error) { updated = true } + if input.DNSRouteInterval != nil && *input.DNSRouteInterval != config.DNSRouteInterval { + log.Infof("updating DNS route interval to %s (old value %s)", + input.DNSRouteInterval.String(), config.DNSRouteInterval.String()) + config.DNSRouteInterval = *input.DNSRouteInterval + updated = true + } else if config.DNSRouteInterval == 0 { + config.DNSRouteInterval = dynamic.DefaultInterval + log.Infof("using default DNS route interval %s", config.DNSRouteInterval) + updated = true + + } + return updated, nil } diff --git a/client/internal/connect.go b/client/internal/connect.go index d34d0aab0..eee8e97c5 100644 --- a/client/internal/connect.go +++ b/client/internal/connect.go @@ -252,8 +252,10 @@ func (c *ConnectClient) run( return wrapErr(err) } + checks := loginResp.GetChecks() + c.engineMutex.Lock() - c.engine = NewEngineWithProbes(engineCtx, cancel, signalClient, mgmClient, engineConfig, mobileDependency, c.statusRecorder, mgmProbe, signalProbe, relayProbe, wgProbe) + c.engine = NewEngineWithProbes(engineCtx, cancel, signalClient, mgmClient, engineConfig, mobileDependency, c.statusRecorder, mgmProbe, signalProbe, relayProbe, wgProbe, checks) c.engineMutex.Unlock() err = c.engine.Start() @@ -321,6 +323,7 @@ func createEngineConfig(key wgtypes.Key, config *Config, peerConfig *mgmProto.Pe RosenpassEnabled: config.RosenpassEnabled, RosenpassPermissive: config.RosenpassPermissive, ServerSSHAllowed: util.ReturnBoolWithDefaultTrue(config.ServerSSHAllowed), + DNSRouteInterval: config.DNSRouteInterval, } if config.PreSharedKey != "" { diff --git a/client/internal/dns/consts_freebsd.go b/client/internal/dns/consts_freebsd.go new file mode 100644 index 000000000..958eca8e5 --- /dev/null +++ b/client/internal/dns/consts_freebsd.go @@ -0,0 +1,6 @@ +package dns + +const ( + fileUncleanShutdownResolvConfLocation = "/var/db/netbird/resolv.conf" + fileUncleanShutdownManagerTypeLocation = "/var/db/netbird/manager" +) diff --git a/client/internal/dns/consts_linux.go b/client/internal/dns/consts_linux.go new file mode 100644 index 000000000..32456a50f --- /dev/null +++ b/client/internal/dns/consts_linux.go @@ -0,0 +1,8 @@ +//go:build !android + +package dns + +const ( + fileUncleanShutdownResolvConfLocation = "/var/lib/netbird/resolv.conf" + fileUncleanShutdownManagerTypeLocation = "/var/lib/netbird/manager" +) diff --git a/client/internal/dns/dbus_linux.go b/client/internal/dns/dbus_unix.go similarity index 96% rename from client/internal/dns/dbus_linux.go rename to client/internal/dns/dbus_unix.go index b2604e9fa..ba1c07fae 100644 --- a/client/internal/dns/dbus_linux.go +++ b/client/internal/dns/dbus_unix.go @@ -1,4 +1,4 @@ -//go:build !android +//go:build (linux && !android) || freebsd package dns diff --git a/client/internal/dns/file_parser_linux.go b/client/internal/dns/file_parser_unix.go similarity index 99% rename from client/internal/dns/file_parser_linux.go rename to client/internal/dns/file_parser_unix.go index 02f6d03a5..130c88214 100644 --- a/client/internal/dns/file_parser_linux.go +++ b/client/internal/dns/file_parser_unix.go @@ -1,4 +1,4 @@ -//go:build !android +//go:build (linux && !android) || freebsd package dns diff --git a/client/internal/dns/file_parser_linux_test.go b/client/internal/dns/file_parser_unix_test.go similarity index 99% rename from client/internal/dns/file_parser_linux_test.go rename to client/internal/dns/file_parser_unix_test.go index 4263d4063..1d6e64683 100644 --- a/client/internal/dns/file_parser_linux_test.go +++ b/client/internal/dns/file_parser_unix_test.go @@ -1,4 +1,4 @@ -//go:build !android +//go:build (linux && !android) || freebsd package dns diff --git a/client/internal/dns/file_repair_linux.go b/client/internal/dns/file_repair_unix.go similarity index 98% rename from client/internal/dns/file_repair_linux.go rename to client/internal/dns/file_repair_unix.go index cbdda5e9e..ae2c33b86 100644 --- a/client/internal/dns/file_repair_linux.go +++ b/client/internal/dns/file_repair_unix.go @@ -1,4 +1,4 @@ -//go:build !android +//go:build (linux && !android) || freebsd package dns diff --git a/client/internal/dns/file_repair_linux_test.go b/client/internal/dns/file_repair_unix_test.go similarity index 98% rename from client/internal/dns/file_repair_linux_test.go rename to client/internal/dns/file_repair_unix_test.go index 4e27f46ba..4dba79e99 100644 --- a/client/internal/dns/file_repair_linux_test.go +++ b/client/internal/dns/file_repair_unix_test.go @@ -1,4 +1,4 @@ -//go:build !android +//go:build (linux && !android) || freebsd package dns diff --git a/client/internal/dns/file_linux.go b/client/internal/dns/file_unix.go similarity index 99% rename from client/internal/dns/file_linux.go rename to client/internal/dns/file_unix.go index b9d6d699d..624e089cb 100644 --- a/client/internal/dns/file_linux.go +++ b/client/internal/dns/file_unix.go @@ -1,4 +1,4 @@ -//go:build !android +//go:build (linux && !android) || freebsd package dns diff --git a/client/internal/dns/file_linux_test.go b/client/internal/dns/file_unix_test.go similarity index 98% rename from client/internal/dns/file_linux_test.go rename to client/internal/dns/file_unix_test.go index 902791b36..46726536e 100644 --- a/client/internal/dns/file_linux_test.go +++ b/client/internal/dns/file_unix_test.go @@ -1,4 +1,4 @@ -//go:build !android +//go:build (linux && !android) || freebsd package dns diff --git a/client/internal/dns/host_linux.go b/client/internal/dns/host_unix.go similarity index 85% rename from client/internal/dns/host_linux.go rename to client/internal/dns/host_unix.go index cb246bcfe..72b8f6c6e 100644 --- a/client/internal/dns/host_linux.go +++ b/client/internal/dns/host_unix.go @@ -1,4 +1,4 @@ -//go:build !android +//go:build (linux && !android) || freebsd package dns @@ -108,7 +108,7 @@ func getOSDNSManagerType() (osManagerType, error) { if strings.Contains(text, "NetworkManager") && isDbusListenerRunning(networkManagerDest, networkManagerDbusObjectNode) && isNetworkManagerSupported() { return networkManager, nil } - if strings.Contains(text, "systemd-resolved") && isDbusListenerRunning(systemdResolvedDest, systemdDbusObjectNode) { + if strings.Contains(text, "systemd-resolved") && isSystemdResolvedRunning() { if checkStub() { return systemdManager, nil } else { @@ -116,16 +116,10 @@ func getOSDNSManagerType() (osManagerType, error) { } } if strings.Contains(text, "resolvconf") { - if isDbusListenerRunning(systemdResolvedDest, systemdDbusObjectNode) { - var value string - err = getSystemdDbusProperty(systemdDbusResolvConfModeProperty, &value) - if err == nil { - if value == systemdDbusResolvConfModeForeign { - return systemdManager, nil - } - } - log.Errorf("got an error while checking systemd resolv conf mode, error: %s", err) + if isSystemdResolveConfMode() { + return systemdManager, nil } + return resolvConfManager, nil } } diff --git a/client/internal/dns/network_manager_linux.go b/client/internal/dns/network_manager_unix.go similarity index 99% rename from client/internal/dns/network_manager_linux.go rename to client/internal/dns/network_manager_unix.go index dfd4cf4d3..184047a64 100644 --- a/client/internal/dns/network_manager_linux.go +++ b/client/internal/dns/network_manager_unix.go @@ -1,4 +1,4 @@ -//go:build !android +//go:build (linux && !android) || freebsd package dns diff --git a/client/internal/dns/resolvconf_linux.go b/client/internal/dns/resolvconf_unix.go similarity index 98% rename from client/internal/dns/resolvconf_linux.go rename to client/internal/dns/resolvconf_unix.go index 72db5faf1..0c17626c7 100644 --- a/client/internal/dns/resolvconf_linux.go +++ b/client/internal/dns/resolvconf_unix.go @@ -1,4 +1,4 @@ -//go:build !android +//go:build (linux && !android) || freebsd package dns diff --git a/client/internal/dns/server_test.go b/client/internal/dns/server_test.go index 22966d89c..3709c32ce 100644 --- a/client/internal/dns/server_test.go +++ b/client/internal/dns/server_test.go @@ -39,6 +39,10 @@ func (w *mocWGIface) Address() iface.WGAddress { } } +func (w *mocWGIface) ToInterface() *net.Interface { + panic("implement me") +} + func (w *mocWGIface) GetFilter() iface.PacketFilter { return w.filter } diff --git a/client/internal/dns/server_linux.go b/client/internal/dns/server_unix.go similarity index 76% rename from client/internal/dns/server_linux.go rename to client/internal/dns/server_unix.go index aeb24b511..455425625 100644 --- a/client/internal/dns/server_linux.go +++ b/client/internal/dns/server_unix.go @@ -1,4 +1,4 @@ -//go:build !android +//go:build (linux && !android) || freebsd package dns diff --git a/client/internal/dns/systemd_freebsd.go b/client/internal/dns/systemd_freebsd.go new file mode 100644 index 000000000..0de805337 --- /dev/null +++ b/client/internal/dns/systemd_freebsd.go @@ -0,0 +1,20 @@ +package dns + +import ( + "errors" + "fmt" +) + +var errNotImplemented = errors.New("not implemented") + +func newSystemdDbusConfigurator(wgInterface string) (hostManager, error) { + return nil, fmt.Errorf("systemd dns management: %w on freebsd", errNotImplemented) +} + +func isSystemdResolvedRunning() bool { + return false +} + +func isSystemdResolveConfMode() bool { + return false +} diff --git a/client/internal/dns/systemd_linux.go b/client/internal/dns/systemd_linux.go index 27a93fbe1..e2fa5b71a 100644 --- a/client/internal/dns/systemd_linux.go +++ b/client/internal/dns/systemd_linux.go @@ -242,3 +242,25 @@ func getSystemdDbusProperty(property string, store any) error { return v.Store(store) } + +func isSystemdResolvedRunning() bool { + return isDbusListenerRunning(systemdResolvedDest, systemdDbusObjectNode) +} + +func isSystemdResolveConfMode() bool { + if !isDbusListenerRunning(systemdResolvedDest, systemdDbusObjectNode) { + return false + } + + var value string + if err := getSystemdDbusProperty(systemdDbusResolvConfModeProperty, &value); err != nil { + log.Errorf("got an error while checking systemd resolv conf mode, error: %s", err) + return false + } + + if value == systemdDbusResolvConfModeForeign { + return true + } + + return false +} diff --git a/client/internal/dns/unclean_shutdown_linux.go b/client/internal/dns/unclean_shutdown_unix.go similarity index 94% rename from client/internal/dns/unclean_shutdown_linux.go rename to client/internal/dns/unclean_shutdown_unix.go index afd587720..8a32090c3 100644 --- a/client/internal/dns/unclean_shutdown_linux.go +++ b/client/internal/dns/unclean_shutdown_unix.go @@ -1,4 +1,4 @@ -//go:build !android +//go:build (linux && !android) || freebsd package dns @@ -14,11 +14,6 @@ import ( log "github.com/sirupsen/logrus" ) -const ( - fileUncleanShutdownResolvConfLocation = "/var/lib/netbird/resolv.conf" - fileUncleanShutdownManagerTypeLocation = "/var/lib/netbird/manager" -) - func CheckUncleanShutdown(wgIface string) error { if _, err := os.Stat(fileUncleanShutdownResolvConfLocation); err != nil { if errors.Is(err, fs.ErrNotExist) { diff --git a/client/internal/dns/wgiface.go b/client/internal/dns/wgiface.go index 2c34f1c47..2f08e8d52 100644 --- a/client/internal/dns/wgiface.go +++ b/client/internal/dns/wgiface.go @@ -2,12 +2,17 @@ package dns -import "github.com/netbirdio/netbird/iface" +import ( + "net" + + "github.com/netbirdio/netbird/iface" +) // WGIface defines subset methods of interface required for manager type WGIface interface { Name() string Address() iface.WGAddress + ToInterface() *net.Interface IsUserspaceBind() bool GetFilter() iface.PacketFilter GetDevice() *iface.DeviceWrapper diff --git a/client/internal/engine.go b/client/internal/engine.go index b09235714..fce1e162b 100644 --- a/client/internal/engine.go +++ b/client/internal/engine.go @@ -10,6 +10,7 @@ import ( "net/netip" "reflect" "runtime" + "slices" "strings" "sync" "time" @@ -30,10 +31,12 @@ import ( "github.com/netbirdio/netbird/client/internal/routemanager" "github.com/netbirdio/netbird/client/internal/wgproxy" nbssh "github.com/netbirdio/netbird/client/ssh" + "github.com/netbirdio/netbird/client/system" nbdns "github.com/netbirdio/netbird/dns" "github.com/netbirdio/netbird/iface" "github.com/netbirdio/netbird/iface/bind" mgm "github.com/netbirdio/netbird/management/client" + "github.com/netbirdio/netbird/management/domain" mgmProto "github.com/netbirdio/netbird/management/proto" "github.com/netbirdio/netbird/route" signal "github.com/netbirdio/netbird/signal/client" @@ -89,6 +92,8 @@ type EngineConfig struct { RosenpassPermissive bool ServerSSHAllowed bool + + DNSRouteInterval time.Duration } // Engine is a mechanism responsible for reacting on Signal and Management stream events and managing connections to the remote peers. @@ -154,6 +159,9 @@ type Engine struct { wgProbe *Probe wgConnWorker sync.WaitGroup + + // checks are the client-applied posture checks that need to be evaluated on the client + checks []*mgmProto.Checks } // Peer is an instance of the Connection Peer @@ -171,6 +179,7 @@ func NewEngine( config *EngineConfig, mobileDep MobileDependency, statusRecorder *peer.Status, + checks []*mgmProto.Checks, ) *Engine { return NewEngineWithProbes( clientCtx, @@ -184,6 +193,7 @@ func NewEngine( nil, nil, nil, + checks, ) } @@ -200,6 +210,7 @@ func NewEngineWithProbes( signalProbe *Probe, relayProbe *Probe, wgProbe *Probe, + checks []*mgmProto.Checks, ) *Engine { return &Engine{ @@ -220,6 +231,7 @@ func NewEngineWithProbes( signalProbe: signalProbe, relayProbe: relayProbe, wgProbe: wgProbe, + checks: checks, } } @@ -301,7 +313,7 @@ func (e *Engine) Start() error { } e.dnsServer = dnsServer - e.routeManager = routemanager.NewManager(e.ctx, e.config.WgPrivateKey.PublicKey().String(), e.wgInterface, e.statusRecorder, initialRoutes) + e.routeManager = routemanager.NewManager(e.ctx, e.config.WgPrivateKey.PublicKey().String(), e.config.DNSRouteInterval, e.wgInterface, e.statusRecorder, initialRoutes) beforePeerHook, afterPeerHook, err := e.routeManager.Init() if err != nil { log.Errorf("Failed to initialize route manager: %s", err) @@ -527,6 +539,10 @@ func (e *Engine) handleSync(update *mgmProto.SyncResponse) error { // todo update signal } + if err := e.updateChecksIfNew(update.Checks); err != nil { + return err + } + if update.GetNetworkMap() != nil { // only apply new changes and ignore old ones err := e.updateNetworkMap(update.GetNetworkMap()) @@ -534,7 +550,27 @@ func (e *Engine) handleSync(update *mgmProto.SyncResponse) error { return err } } + return nil +} +// updateChecksIfNew updates checks if there are changes and sync new meta with management +func (e *Engine) updateChecksIfNew(checks []*mgmProto.Checks) error { + // if checks are equal, we skip the update + if isChecksEqual(e.checks, checks) { + return nil + } + e.checks = checks + + info, err := system.GetInfoWithChecks(e.ctx, checks) + if err != nil { + log.Warnf("failed to get system info with checks: %v", err) + info = system.GetInfo(e.ctx) + } + + if err := e.mgmClient.SyncMeta(info); err != nil { + log.Errorf("could not sync meta: error %s", err) + return err + } return nil } @@ -550,8 +586,8 @@ func (e *Engine) updateSSH(sshConf *mgmProto.SSHConfig) error { } else { if sshConf.GetSshEnabled() { - if runtime.GOOS == "windows" { - log.Warnf("running SSH server on Windows is not supported") + if runtime.GOOS == "windows" || runtime.GOOS == "freebsd" { + log.Warnf("running SSH server on %s is not supported", runtime.GOOS) return nil } // start SSH server if it wasn't running @@ -624,7 +660,14 @@ func (e *Engine) updateConfig(conf *mgmProto.PeerConfig) error { // E.g. when a new peer has been registered and we are allowed to connect to it. func (e *Engine) receiveManagementEvents() { go func() { - err := e.mgmClient.Sync(e.ctx, e.handleSync) + info, err := system.GetInfoWithChecks(e.ctx, e.checks) + if err != nil { + log.Warnf("failed to get system info with checks: %v", err) + info = system.GetInfo(e.ctx) + } + + // err = e.mgmClient.Sync(info, e.handleSync) + err = e.mgmClient.Sync(e.ctx, info, e.handleSync) if err != nil { // happens if management is unavailable for a long time. // We want to cancel the operation of the whole client @@ -772,15 +815,24 @@ func (e *Engine) updateNetworkMap(networkMap *mgmProto.NetworkMap) error { func toRoutes(protoRoutes []*mgmProto.Route) []*route.Route { routes := make([]*route.Route, 0) for _, protoRoute := range protoRoutes { - _, prefix, _ := route.ParseNetwork(protoRoute.Network) + var prefix netip.Prefix + if len(protoRoute.Domains) == 0 { + var err error + if prefix, err = netip.ParsePrefix(protoRoute.Network); err != nil { + log.Errorf("Failed to parse prefix %s: %v", protoRoute.Network, err) + continue + } + } convertedRoute := &route.Route{ ID: route.ID(protoRoute.ID), Network: prefix, + Domains: domain.FromPunycodeList(protoRoute.Domains), NetID: route.NetID(protoRoute.NetID), NetworkType: route.NetworkType(protoRoute.NetworkType), Peer: protoRoute.Peer, Metric: int(protoRoute.Metric), Masquerade: protoRoute.Masquerade, + KeepRoute: protoRoute.KeepRoute, } routes = append(routes, convertedRoute) } @@ -1204,7 +1256,8 @@ func (e *Engine) close() { } func (e *Engine) readInitialSettings() ([]*route.Route, *nbdns.Config, error) { - netMap, err := e.mgmClient.GetNetworkMap() + info := system.GetInfo(e.ctx) + netMap, err := e.mgmClient.GetNetworkMap(info) if err != nil { return nil, nil, err } @@ -1430,3 +1483,10 @@ func (e *Engine) startNetworkMonitor() { } }() } + +// isChecksEqual checks if two slices of checks are equal. +func isChecksEqual(checks []*mgmProto.Checks, oChecks []*mgmProto.Checks) bool { + return slices.EqualFunc(checks, oChecks, func(checks, oChecks *mgmProto.Checks) bool { + return slices.Equal(checks.Files, oChecks.Files) + }) +} diff --git a/client/internal/engine_test.go b/client/internal/engine_test.go index 176d65459..28edd5d5a 100644 --- a/client/internal/engine_test.go +++ b/client/internal/engine_test.go @@ -78,7 +78,7 @@ func TestEngine_SSH(t *testing.T) { WgPrivateKey: key, WgPort: 33100, ServerSSHAllowed: true, - }, MobileDependency{}, peer.NewRecorder("https://mgm")) + }, MobileDependency{}, peer.NewRecorder("https://mgm"), nil) engine.dnsServer = &dns.MockServer{ UpdateDNSServerFunc: func(serial uint64, update nbdns.Config) error { return nil }, @@ -212,7 +212,7 @@ func TestEngine_UpdateNetworkMap(t *testing.T) { WgAddr: "100.64.0.1/24", WgPrivateKey: key, WgPort: 33100, - }, MobileDependency{}, peer.NewRecorder("https://mgm")) + }, MobileDependency{}, peer.NewRecorder("https://mgm"), nil) newNet, err := stdnet.NewNet() if err != nil { t.Fatal(err) @@ -221,7 +221,7 @@ func TestEngine_UpdateNetworkMap(t *testing.T) { if err != nil { t.Fatal(err) } - engine.routeManager = routemanager.NewManager(ctx, key.PublicKey().String(), engine.wgInterface, engine.statusRecorder, nil) + engine.routeManager = routemanager.NewManager(ctx, key.PublicKey().String(), time.Minute, engine.wgInterface, engine.statusRecorder, nil) engine.dnsServer = &dns.MockServer{ UpdateDNSServerFunc: func(serial uint64, update nbdns.Config) error { return nil }, } @@ -394,7 +394,7 @@ func TestEngine_Sync(t *testing.T) { // feed updates to Engine via mocked Management client updates := make(chan *mgmtProto.SyncResponse) defer close(updates) - syncFunc := func(ctx context.Context, msgHandler func(msg *mgmtProto.SyncResponse) error) error { + syncFunc := func(ctx context.Context, info *system.Info, msgHandler func(msg *mgmtProto.SyncResponse) error) error { for msg := range updates { err := msgHandler(msg) if err != nil { @@ -409,7 +409,7 @@ func TestEngine_Sync(t *testing.T) { WgAddr: "100.64.0.1/24", WgPrivateKey: key, WgPort: 33100, - }, MobileDependency{}, peer.NewRecorder("https://mgm")) + }, MobileDependency{}, peer.NewRecorder("https://mgm"), nil) engine.ctx = ctx engine.dnsServer = &dns.MockServer{ @@ -568,7 +568,7 @@ func TestEngine_UpdateNetworkMapWithRoutes(t *testing.T) { WgAddr: wgAddr, WgPrivateKey: key, WgPort: 33100, - }, MobileDependency{}, peer.NewRecorder("https://mgm")) + }, MobileDependency{}, peer.NewRecorder("https://mgm"), nil) engine.ctx = ctx newNet, err := stdnet.NewNet() if err != nil { @@ -738,7 +738,7 @@ func TestEngine_UpdateNetworkMapWithDNSUpdate(t *testing.T) { WgAddr: wgAddr, WgPrivateKey: key, WgPort: 33100, - }, MobileDependency{}, peer.NewRecorder("https://mgm")) + }, MobileDependency{}, peer.NewRecorder("https://mgm"), nil) engine.ctx = ctx newNet, err := stdnet.NewNet() @@ -1009,7 +1009,7 @@ func createEngine(ctx context.Context, cancel context.CancelFunc, setupKey strin WgPort: wgPort, } - e, err := NewEngine(ctx, cancel, signalClient, mgmtClient, conf, MobileDependency{}, peer.NewRecorder("https://mgm")), nil + e, err := NewEngine(ctx, cancel, signalClient, mgmtClient, conf, MobileDependency{}, peer.NewRecorder("https://mgm"), nil), nil e.ctx = ctx return e, err } diff --git a/client/internal/networkmonitor/monitor_bsd.go b/client/internal/networkmonitor/monitor_bsd.go index de4209f5d..253fd68ff 100644 --- a/client/internal/networkmonitor/monitor_bsd.go +++ b/client/internal/networkmonitor/monitor_bsd.go @@ -5,8 +5,6 @@ package networkmonitor import ( "context" "fmt" - "net" - "net/netip" "syscall" "unsafe" @@ -14,10 +12,10 @@ import ( "golang.org/x/net/route" "golang.org/x/sys/unix" - "github.com/netbirdio/netbird/client/internal/routemanager" + "github.com/netbirdio/netbird/client/internal/routemanager/systemops" ) -func checkChange(ctx context.Context, nexthopv4 netip.Addr, intfv4 *net.Interface, nexthopv6 netip.Addr, intfv6 *net.Interface, callback func()) error { +func checkChange(ctx context.Context, nexthopv4, nexthopv6 systemops.Nexthop, callback func()) error { fd, err := unix.Socket(syscall.AF_ROUTE, syscall.SOCK_RAW, syscall.AF_UNSPEC) if err != nil { return fmt.Errorf("failed to open routing socket: %v", err) @@ -58,7 +56,7 @@ func checkChange(ctx context.Context, nexthopv4 netip.Addr, intfv4 *net.Interfac if msg.Flags&unix.IFF_UP != 0 { continue } - if (intfv4 == nil || ifinfo.Index != intfv4.Index) && (intfv6 == nil || ifinfo.Index != intfv6.Index) { + if (nexthopv4.Intf == nil || ifinfo.Index != nexthopv4.Intf.Index) && (nexthopv6.Intf == nil || ifinfo.Index != nexthopv6.Intf.Index) { continue } @@ -86,7 +84,7 @@ func checkChange(ctx context.Context, nexthopv4 netip.Addr, intfv4 *net.Interfac log.Infof("Network monitor: default route changed: via %s, interface %s", route.Gw, intf) go callback() case unix.RTM_DELETE: - if intfv4 != nil && route.Gw.Compare(nexthopv4) == 0 || intfv6 != nil && route.Gw.Compare(nexthopv6) == 0 { + if nexthopv4.Intf != nil && route.Gw.Compare(nexthopv4.IP) == 0 || nexthopv6.Intf != nil && route.Gw.Compare(nexthopv6.IP) == 0 { log.Infof("Network monitor: default route removed: via %s, interface %s", route.Gw, intf) go callback() } @@ -114,7 +112,7 @@ func parseInterfaceMessage(buf []byte) (*route.InterfaceMessage, error) { return msg, nil } -func parseRouteMessage(buf []byte) (*routemanager.Route, error) { +func parseRouteMessage(buf []byte) (*systemops.Route, error) { msgs, err := route.ParseRIB(route.RIBTypeRoute, buf) if err != nil { return nil, fmt.Errorf("parse RIB: %v", err) @@ -129,5 +127,5 @@ func parseRouteMessage(buf []byte) (*routemanager.Route, error) { return nil, fmt.Errorf("unexpected RIB message type: %T", msgs[0]) } - return routemanager.MsgToRoute(msg) + return systemops.MsgToRoute(msg) } diff --git a/client/internal/networkmonitor/monitor_generic.go b/client/internal/networkmonitor/monitor_generic.go index 97cfbc2ca..f5cc19473 100644 --- a/client/internal/networkmonitor/monitor_generic.go +++ b/client/internal/networkmonitor/monitor_generic.go @@ -6,14 +6,13 @@ import ( "context" "errors" "fmt" - "net" "net/netip" "runtime/debug" "github.com/cenkalti/backoff/v4" log "github.com/sirupsen/logrus" - "github.com/netbirdio/netbird/client/internal/routemanager" + "github.com/netbirdio/netbird/client/internal/routemanager/systemops" ) // Start begins monitoring network changes. When a change is detected, it calls the callback asynchronously and returns. @@ -29,23 +28,22 @@ func (nw *NetworkMonitor) Start(ctx context.Context, callback func()) (err error nw.wg.Add(1) defer nw.wg.Done() - var nexthop4, nexthop6 netip.Addr - var intf4, intf6 *net.Interface + var nexthop4, nexthop6 systemops.Nexthop operation := func() error { var errv4, errv6 error - nexthop4, intf4, errv4 = routemanager.GetNextHop(netip.IPv4Unspecified()) - nexthop6, intf6, errv6 = routemanager.GetNextHop(netip.IPv6Unspecified()) + nexthop4, errv4 = systemops.GetNextHop(netip.IPv4Unspecified()) + nexthop6, errv6 = systemops.GetNextHop(netip.IPv6Unspecified()) if errv4 != nil && errv6 != nil { return errors.New("failed to get default next hops") } if errv4 == nil { - log.Debugf("Network monitor: IPv4 default route: %s, interface: %s", nexthop4, intf4.Name) + log.Debugf("Network monitor: IPv4 default route: %s, interface: %s", nexthop4.IP, nexthop4.Intf.Name) } if errv6 == nil { - log.Debugf("Network monitor: IPv6 default route: %s, interface: %s", nexthop6, intf6.Name) + log.Debugf("Network monitor: IPv6 default route: %s, interface: %s", nexthop6.IP, nexthop6.Intf.Name) } // continue if either route was found @@ -65,7 +63,7 @@ func (nw *NetworkMonitor) Start(ctx context.Context, callback func()) (err error } }() - if err := checkChange(ctx, nexthop4, intf4, nexthop6, intf6, callback); err != nil { + if err := checkChange(ctx, nexthop4, nexthop6, callback); err != nil { return fmt.Errorf("check change: %w", err) } diff --git a/client/internal/networkmonitor/monitor_linux.go b/client/internal/networkmonitor/monitor_linux.go index 3f93c6ac6..de5e29b38 100644 --- a/client/internal/networkmonitor/monitor_linux.go +++ b/client/internal/networkmonitor/monitor_linux.go @@ -6,16 +6,16 @@ import ( "context" "errors" "fmt" - "net" - "net/netip" "syscall" log "github.com/sirupsen/logrus" "github.com/vishvananda/netlink" + + "github.com/netbirdio/netbird/client/internal/routemanager/systemops" ) -func checkChange(ctx context.Context, nexthopv4 netip.Addr, intfv4 *net.Interface, nexthop6 netip.Addr, intfv6 *net.Interface, callback func()) error { - if intfv4 == nil && intfv6 == nil { +func checkChange(ctx context.Context, nexthopv4, nexthopv6 systemops.Nexthop, callback func()) error { + if nexthopv4.Intf == nil && nexthopv6.Intf == nil { return errors.New("no interfaces available") } @@ -40,7 +40,7 @@ func checkChange(ctx context.Context, nexthopv4 netip.Addr, intfv4 *net.Interfac // handle interface state changes case update := <-linkChan: - if (intfv4 == nil || update.Index != int32(intfv4.Index)) && (intfv6 == nil || update.Index != int32(intfv6.Index)) { + if (nexthopv4.Intf == nil || update.Index != int32(nexthopv4.Intf.Index)) && (nexthopv6.Intf == nil || update.Index != int32(nexthopv6.Intf.Index)) { continue } @@ -70,7 +70,7 @@ func checkChange(ctx context.Context, nexthopv4 netip.Addr, intfv4 *net.Interfac go callback() return nil case syscall.RTM_DELROUTE: - if intfv4 != nil && route.Gw.Equal(nexthopv4.AsSlice()) || intfv6 != nil && route.Gw.Equal(nexthop6.AsSlice()) { + if nexthopv4.Intf != nil && route.Gw.Equal(nexthopv4.IP.AsSlice()) || nexthopv6.Intf != nil && route.Gw.Equal(nexthopv6.IP.AsSlice()) { log.Infof("Network monitor: default route removed: via %s, interface %d", route.Gw, route.LinkIndex) go callback() return nil diff --git a/client/internal/networkmonitor/monitor_windows.go b/client/internal/networkmonitor/monitor_windows.go index b8d9c6de7..19697bcc0 100644 --- a/client/internal/networkmonitor/monitor_windows.go +++ b/client/internal/networkmonitor/monitor_windows.go @@ -9,7 +9,7 @@ import ( log "github.com/sirupsen/logrus" - "github.com/netbirdio/netbird/client/internal/routemanager" + "github.com/netbirdio/netbird/client/internal/routemanager/systemops" ) const ( @@ -25,18 +25,18 @@ const ( const interval = 10 * time.Second -func checkChange(ctx context.Context, nexthopv4 netip.Addr, intfv4 *net.Interface, nexthopv6 netip.Addr, intfv6 *net.Interface, callback func()) error { - var neighborv4, neighborv6 *routemanager.Neighbor +func checkChange(ctx context.Context, nexthopv4, nexthopv6 systemops.Nexthop, callback func()) error { + var neighborv4, neighborv6 *systemops.Neighbor { initialNeighbors, err := getNeighbors() if err != nil { return fmt.Errorf("get neighbors: %w", err) } - if n, ok := initialNeighbors[nexthopv4]; ok { + if n, ok := initialNeighbors[nexthopv4.IP]; ok { neighborv4 = &n } - if n, ok := initialNeighbors[nexthopv6]; ok { + if n, ok := initialNeighbors[nexthopv6.IP]; ok { neighborv6 = &n } } @@ -50,7 +50,7 @@ func checkChange(ctx context.Context, nexthopv4 netip.Addr, intfv4 *net.Interfac case <-ctx.Done(): return ErrStopped case <-ticker.C: - if changed(nexthopv4, intfv4, neighborv4, nexthopv6, intfv6, neighborv6) { + if changed(nexthopv4, neighborv4, nexthopv6, neighborv6) { go callback() return nil } @@ -59,12 +59,10 @@ func checkChange(ctx context.Context, nexthopv4 netip.Addr, intfv4 *net.Interfac } func changed( - nexthopv4 netip.Addr, - intfv4 *net.Interface, - neighborv4 *routemanager.Neighbor, - nexthopv6 netip.Addr, - intfv6 *net.Interface, - neighborv6 *routemanager.Neighbor, + nexthopv4 systemops.Nexthop, + neighborv4 *systemops.Neighbor, + nexthopv6 systemops.Nexthop, + neighborv6 *systemops.Neighbor, ) bool { neighbors, err := getNeighbors() if err != nil { @@ -81,7 +79,7 @@ func changed( return false } - if routeChanged(nexthopv4, intfv4, routes) || routeChanged(nexthopv6, intfv6, routes) { + if routeChanged(nexthopv4, nexthopv4.Intf, routes) || routeChanged(nexthopv6, nexthopv6.Intf, routes) { return true } @@ -89,20 +87,20 @@ func changed( } // routeChanged checks if the default routes still point to our nexthop/interface -func routeChanged(nexthop netip.Addr, intf *net.Interface, routes map[netip.Prefix]routemanager.Route) bool { - if !nexthop.IsValid() { +func routeChanged(nexthop systemops.Nexthop, intf *net.Interface, routes map[netip.Prefix]systemops.Route) bool { + if !nexthop.IP.IsValid() { return false } var unspec netip.Prefix - if nexthop.Is6() { + if nexthop.IP.Is6() { unspec = netip.PrefixFrom(netip.IPv6Unspecified(), 0) } else { unspec = netip.PrefixFrom(netip.IPv4Unspecified(), 0) } if r, ok := routes[unspec]; ok { - if r.Nexthop != nexthop || compareIntf(r.Interface, intf) != 0 { + if r.Nexthop != nexthop.IP || compareIntf(r.Interface, intf) != 0 { intf := "" if r.Interface != nil { intf = r.Interface.Name @@ -119,13 +117,13 @@ func routeChanged(nexthop netip.Addr, intf *net.Interface, routes map[netip.Pref } -func neighborChanged(nexthop netip.Addr, neighbor *routemanager.Neighbor, neighbors map[netip.Addr]routemanager.Neighbor) bool { +func neighborChanged(nexthop systemops.Nexthop, neighbor *systemops.Neighbor, neighbors map[netip.Addr]systemops.Neighbor) bool { if neighbor == nil { return false } // TODO: consider non-local nexthops, e.g. on point-to-point interfaces - if n, ok := neighbors[nexthop]; ok { + if n, ok := neighbors[nexthop.IP]; ok { if n.State != reachable && n.State != permanent { log.Infof("network monitor: neighbor %s (%s) is not reachable: %s", neighbor.IPAddress, neighbor.LinkLayerAddress, stateFromInt(n.State)) return true @@ -150,13 +148,13 @@ func neighborChanged(nexthop netip.Addr, neighbor *routemanager.Neighbor, neighb return false } -func getNeighbors() (map[netip.Addr]routemanager.Neighbor, error) { - entries, err := routemanager.GetNeighbors() +func getNeighbors() (map[netip.Addr]systemops.Neighbor, error) { + entries, err := systemops.GetNeighbors() if err != nil { return nil, fmt.Errorf("get neighbors: %w", err) } - neighbours := make(map[netip.Addr]routemanager.Neighbor, len(entries)) + neighbours := make(map[netip.Addr]systemops.Neighbor, len(entries)) for _, entry := range entries { neighbours[entry.IPAddress] = entry } @@ -164,13 +162,13 @@ func getNeighbors() (map[netip.Addr]routemanager.Neighbor, error) { return neighbours, nil } -func getRoutes() (map[netip.Prefix]routemanager.Route, error) { - entries, err := routemanager.GetRoutes() +func getRoutes() (map[netip.Prefix]systemops.Route, error) { + entries, err := systemops.GetRoutes() if err != nil { return nil, fmt.Errorf("get routes: %w", err) } - routes := make(map[netip.Prefix]routemanager.Route, len(entries)) + routes := make(map[netip.Prefix]systemops.Route, len(entries)) for _, entry := range entries { routes[entry.Destination] = entry } diff --git a/client/internal/peer/status.go b/client/internal/peer/status.go index ddea7d04e..a7cfb95c4 100644 --- a/client/internal/peer/status.go +++ b/client/internal/peer/status.go @@ -2,14 +2,17 @@ package peer import ( "errors" + "net/netip" "sync" "time" + "golang.org/x/exp/maps" "google.golang.org/grpc/codes" gstatus "google.golang.org/grpc/status" "github.com/netbirdio/netbird/client/internal/relay" "github.com/netbirdio/netbird/iface" + "github.com/netbirdio/netbird/management/domain" ) // State contains the latest state of a peer @@ -37,25 +40,25 @@ type State struct { // AddRoute add a single route to routes map func (s *State) AddRoute(network string) { s.Mux.Lock() + defer s.Mux.Unlock() if s.routes == nil { s.routes = make(map[string]struct{}) } s.routes[network] = struct{}{} - s.Mux.Unlock() } // SetRoutes set state routes func (s *State) SetRoutes(routes map[string]struct{}) { s.Mux.Lock() + defer s.Mux.Unlock() s.routes = routes - s.Mux.Unlock() } // DeleteRoute removes a route from the network amp func (s *State) DeleteRoute(network string) { s.Mux.Lock() + defer s.Mux.Unlock() delete(s.routes, network) - s.Mux.Unlock() } // GetRoutes return routes map @@ -117,22 +120,23 @@ type FullStatus struct { // Status holds a state of peers, signal, management connections and relays type Status struct { - mux sync.Mutex - peers map[string]State - changeNotify map[string]chan struct{} - signalState bool - signalError error - managementState bool - managementError error - relayStates []relay.ProbeResult - localPeer LocalPeerState - offlinePeers []State - mgmAddress string - signalAddress string - notifier *notifier - rosenpassEnabled bool - rosenpassPermissive bool - nsGroupStates []NSGroupState + mux sync.Mutex + peers map[string]State + changeNotify map[string]chan struct{} + signalState bool + signalError error + managementState bool + managementError error + relayStates []relay.ProbeResult + localPeer LocalPeerState + offlinePeers []State + mgmAddress string + signalAddress string + notifier *notifier + rosenpassEnabled bool + rosenpassPermissive bool + nsGroupStates []NSGroupState + resolvedDomainsStates map[domain.Domain][]netip.Prefix // To reduce the number of notification invocation this bool will be true when need to call the notification // Some Peer actions mostly used by in a batch when the network map has been synchronized. In these type of events @@ -143,11 +147,12 @@ type Status struct { // NewRecorder returns a new Status instance func NewRecorder(mgmAddress string) *Status { return &Status{ - peers: make(map[string]State), - changeNotify: make(map[string]chan struct{}), - offlinePeers: make([]State, 0), - notifier: newNotifier(), - mgmAddress: mgmAddress, + peers: make(map[string]State), + changeNotify: make(map[string]chan struct{}), + offlinePeers: make([]State, 0), + notifier: newNotifier(), + mgmAddress: mgmAddress, + resolvedDomainsStates: make(map[domain.Domain][]netip.Prefix), } } @@ -188,7 +193,7 @@ func (d *Status) GetPeer(peerPubKey string) (State, error) { state, ok := d.peers[peerPubKey] if !ok { - return State{}, errors.New("peer not found") + return State{}, iface.ErrPeerNotFound } return state, nil } @@ -429,6 +434,18 @@ func (d *Status) UpdateDNSStates(dnsStates []NSGroupState) { d.nsGroupStates = dnsStates } +func (d *Status) UpdateResolvedDomainsStates(domain domain.Domain, prefixes []netip.Prefix) { + d.mux.Lock() + defer d.mux.Unlock() + d.resolvedDomainsStates[domain] = prefixes +} + +func (d *Status) DeleteResolvedDomainsStates(domain domain.Domain) { + d.mux.Lock() + defer d.mux.Unlock() + delete(d.resolvedDomainsStates, domain) +} + func (d *Status) GetRosenpassState() RosenpassState { return RosenpassState{ d.rosenpassEnabled, @@ -493,6 +510,12 @@ func (d *Status) GetDNSStates() []NSGroupState { return d.nsGroupStates } +func (d *Status) GetResolvedDomainsStates() map[domain.Domain][]netip.Prefix { + d.mux.Lock() + defer d.mux.Unlock() + return maps.Clone(d.resolvedDomainsStates) +} + // GetFullStatus gets full status func (d *Status) GetFullStatus() FullStatus { d.mux.Lock() diff --git a/client/internal/routemanager/client.go b/client/internal/routemanager/client.go index e82f4b1da..3c230df21 100644 --- a/client/internal/routemanager/client.go +++ b/client/internal/routemanager/client.go @@ -3,19 +3,20 @@ package routemanager import ( "context" "fmt" - "net" - "net/netip" "time" + "github.com/hashicorp/go-multierror" log "github.com/sirupsen/logrus" + nberrors "github.com/netbirdio/netbird/client/errors" "github.com/netbirdio/netbird/client/internal/peer" + "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/iface" "github.com/netbirdio/netbird/route" ) -const minRangeBits = 7 - type routerPeerStatus struct { connected bool relayed bool @@ -28,33 +29,42 @@ type routesUpdate struct { routes []*route.Route } +// RouteHandler defines the interface for handling routes +type RouteHandler interface { + String() string + AddRoute(ctx context.Context) error + RemoveRoute() error + AddAllowedIPs(peerKey string) error + RemoveAllowedIPs() error +} + type clientNetwork struct { ctx context.Context - stop context.CancelFunc + cancel context.CancelFunc statusRecorder *peer.Status wgInterface *iface.WGIface routes map[route.ID]*route.Route routeUpdate chan routesUpdate peerStateUpdate chan struct{} routePeersNotifiers map[string]chan struct{} - chosenRoute *route.Route - network netip.Prefix + currentChosen *route.Route + handler RouteHandler updateSerial uint64 } -func newClientNetworkWatcher(ctx context.Context, wgInterface *iface.WGIface, statusRecorder *peer.Status, network netip.Prefix) *clientNetwork { +func newClientNetworkWatcher(ctx context.Context, dnsRouteInterval time.Duration, wgInterface *iface.WGIface, statusRecorder *peer.Status, rt *route.Route, routeRefCounter *refcounter.RouteRefCounter, allowedIPsRefCounter *refcounter.AllowedIPsRefCounter) *clientNetwork { ctx, cancel := context.WithCancel(ctx) client := &clientNetwork{ ctx: ctx, - stop: cancel, + cancel: cancel, statusRecorder: statusRecorder, wgInterface: wgInterface, routes: make(map[route.ID]*route.Route), routePeersNotifiers: make(map[string]chan struct{}), routeUpdate: make(chan routesUpdate), peerStateUpdate: make(chan struct{}), - network: network, + handler: handlerFromRoute(rt, routeRefCounter, allowedIPsRefCounter, dnsRouteInterval, statusRecorder), } return client } @@ -86,8 +96,8 @@ func (c *clientNetwork) getRouterPeerStatuses() map[route.ID]routerPeerStatus { // * Metric: Routes with lower metrics (better) are prioritized. // * Non-relayed: Routes without relays are preferred. // * Direct connections: Routes with direct peer connections are favored. -// * Stability: In case of equal scores, the currently active route (if any) is maintained. // * Latency: Routes with lower latency are prioritized. +// * Stability: In case of equal scores, the currently active route (if any) is maintained. // // It returns the ID of the selected optimal route. func (c *clientNetwork) getBestRouteFromStatuses(routePeerStatuses map[route.ID]routerPeerStatus) route.ID { @@ -96,8 +106,8 @@ func (c *clientNetwork) getBestRouteFromStatuses(routePeerStatuses map[route.ID] currScore := float64(0) currID := route.ID("") - if c.chosenRoute != nil { - currID = c.chosenRoute.ID + if c.currentChosen != nil { + currID = c.currentChosen.ID } for _, r := range c.routes { @@ -151,18 +161,18 @@ func (c *clientNetwork) getBestRouteFromStatuses(routePeerStatuses map[route.ID] peers = append(peers, r.Peer) } - log.Warnf("the network %s has not been assigned a routing peer as no peers from the list %s are currently connected", c.network, peers) + log.Warnf("The network [%v] has not been assigned a routing peer as no peers from the list %s are currently connected", c.handler, peers) case chosen != currID: // we compare the current score + 10ms to the chosen score to avoid flapping between routes if currScore != 0 && currScore+0.01 > chosenScore { - log.Debugf("keeping current routing peer because the score difference with latency is less than 0.01(10ms), current: %f, new: %f", currScore, chosenScore) + log.Debugf("Keeping current routing peer because the score difference with latency is less than 0.01(10ms), current: %f, new: %f", currScore, chosenScore) return currID } var p string if rt := c.routes[chosen]; rt != nil { p = rt.Peer } - log.Infof("new chosen route is %s with peer %s with score %f for network %s", chosen, p, chosenScore, c.network) + log.Infof("New chosen route is %s with peer %s with score %f for network [%v]", chosen, p, chosenScore, c.handler) } return chosen @@ -196,98 +206,103 @@ func (c *clientNetwork) startPeersStatusChangeWatcher() { } } -func (c *clientNetwork) removeRouteFromWireguardPeer(peerKey string) error { - state, err := c.statusRecorder.GetPeer(peerKey) - if err != nil { - return fmt.Errorf("get peer state: %v", err) - } +func (c *clientNetwork) removeRouteFromWireguardPeer() error { + c.removeStateRoute() - state.DeleteRoute(c.network.String()) - if err := c.statusRecorder.UpdatePeerState(state); err != nil { - log.Warnf("Failed to update peer state: %v", err) - } - - if state.ConnStatus != peer.StatusConnected { - return nil - } - - err = c.wgInterface.RemoveAllowedIP(peerKey, c.network.String()) - if err != nil { - return fmt.Errorf("remove allowed IP %s removed for peer %s, err: %v", - c.network, c.chosenRoute.Peer, err) + if err := c.handler.RemoveAllowedIPs(); err != nil { + return fmt.Errorf("remove allowed IPs: %w", err) } return nil } func (c *clientNetwork) removeRouteFromPeerAndSystem() error { - if c.chosenRoute != nil { - if err := removeVPNRoute(c.network, c.getAsInterface()); err != nil { - return fmt.Errorf("remove route %s from system, err: %v", c.network, err) - } - - if err := c.removeRouteFromWireguardPeer(c.chosenRoute.Peer); err != nil { - return fmt.Errorf("remove route: %v", err) - } + if c.currentChosen == nil { + return nil } - return nil + + var merr *multierror.Error + + if err := c.removeRouteFromWireguardPeer(); err != nil { + merr = multierror.Append(merr, fmt.Errorf("remove allowed IPs for peer %s: %w", c.currentChosen.Peer, err)) + } + if err := c.handler.RemoveRoute(); err != nil { + merr = multierror.Append(merr, fmt.Errorf("remove route: %w", err)) + } + + return nberrors.FormatErrorOrNil(merr) } func (c *clientNetwork) recalculateRouteAndUpdatePeerAndSystem() error { routerPeerStatuses := c.getRouterPeerStatuses() - chosen := c.getBestRouteFromStatuses(routerPeerStatuses) + newChosenID := c.getBestRouteFromStatuses(routerPeerStatuses) // If no route is chosen, remove the route from the peer and system - if chosen == "" { + if newChosenID == "" { if err := c.removeRouteFromPeerAndSystem(); err != nil { - return fmt.Errorf("remove route from peer and system: %v", err) + return fmt.Errorf("remove route for peer %s: %w", c.currentChosen.Peer, err) } - c.chosenRoute = nil + c.currentChosen = nil return nil } // If the chosen route is the same as the current route, do nothing - if c.chosenRoute != nil && c.chosenRoute.ID == chosen { - if c.chosenRoute.IsEqual(c.routes[chosen]) { - return nil - } + if c.currentChosen != nil && c.currentChosen.ID == newChosenID && + c.currentChosen.IsEqual(c.routes[newChosenID]) { + return nil } - if c.chosenRoute != nil { - // If a previous route exists, remove it from the peer - if err := c.removeRouteFromWireguardPeer(c.chosenRoute.Peer); err != nil { - return fmt.Errorf("remove route from peer: %v", err) + 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) } } else { - // otherwise add the route to the system - if err := addVPNRoute(c.network, c.getAsInterface()); err != nil { - return fmt.Errorf("route %s couldn't be added for peer %s, err: %v", - c.network.String(), c.wgInterface.Address().IP.String(), err) + // Otherwise, remove the allowed IPs from the previous peer first + if err := c.removeRouteFromWireguardPeer(); err != nil { + return fmt.Errorf("remove allowed IPs for peer %s: %w", c.currentChosen.Peer, err) } } - c.chosenRoute = c.routes[chosen] + c.currentChosen = c.routes[newChosenID] - state, err := c.statusRecorder.GetPeer(c.chosenRoute.Peer) - if err != nil { - log.Errorf("Failed to get peer state: %v", err) - } else { - state.AddRoute(c.network.String()) - if err := c.statusRecorder.UpdatePeerState(state); err != nil { - log.Warnf("Failed to update peer state: %v", err) - } + if err := c.handler.AddAllowedIPs(c.currentChosen.Peer); err != nil { + return fmt.Errorf("add allowed IPs for peer %s: %w", c.currentChosen.Peer, err) } - if err := c.wgInterface.AddAllowedIP(c.chosenRoute.Peer, c.network.String()); err != nil { - log.Errorf("couldn't add allowed IP %s added for peer %s, err: %v", - c.network, c.chosenRoute.Peer, err) - } + c.addStateRoute() return nil } +func (c *clientNetwork) addStateRoute() { + state, err := c.statusRecorder.GetPeer(c.currentChosen.Peer) + if err != nil { + log.Errorf("Failed to get peer state: %v", err) + return + } + + state.AddRoute(c.handler.String()) + if err := c.statusRecorder.UpdatePeerState(state); err != nil { + log.Warnf("Failed to update peer state: %v", err) + } +} + +func (c *clientNetwork) removeStateRoute() { + state, err := c.statusRecorder.GetPeer(c.currentChosen.Peer) + if err != nil { + log.Errorf("Failed to get peer state: %v", err) + return + } + + state.DeleteRoute(c.handler.String()) + if err := c.statusRecorder.UpdatePeerState(state); err != nil { + log.Warnf("Failed to update peer state: %v", err) + } +} + func (c *clientNetwork) sendUpdateToClientNetworkWatcher(update routesUpdate) { go func() { c.routeUpdate <- update @@ -318,24 +333,23 @@ func (c *clientNetwork) peersStateAndUpdateWatcher() { for { select { case <-c.ctx.Done(): - log.Debugf("stopping watcher for network %s", c.network) - err := c.removeRouteFromPeerAndSystem() - if err != nil { - log.Errorf("Couldn't remove route from peer and system for network %s: %v", c.network, err) + log.Debugf("Stopping watcher for network [%v]", c.handler) + if err := c.removeRouteFromPeerAndSystem(); err != nil { + log.Errorf("Failed to remove routes for [%v]: %v", c.handler, err) } return case <-c.peerStateUpdate: err := c.recalculateRouteAndUpdatePeerAndSystem() if err != nil { - log.Errorf("Couldn't recalculate route and update peer and system: %v", err) + log.Errorf("Failed to recalculate routes for network [%v]: %v", c.handler, err) } case update := <-c.routeUpdate: if update.updateSerial < c.updateSerial { - log.Warnf("Received a routes update with smaller serial number, ignoring it") + log.Warnf("Received a routes update with smaller serial number (%d -> %d), ignoring it", c.updateSerial, update.updateSerial) continue } - log.Debugf("Received a new client network route update for %s", c.network) + log.Debugf("Received a new client network route update for [%v]", c.handler) c.handleUpdate(update) @@ -343,7 +357,7 @@ func (c *clientNetwork) peersStateAndUpdateWatcher() { err := c.recalculateRouteAndUpdatePeerAndSystem() if err != nil { - log.Errorf("Couldn't recalculate route and update peer and system for network %s: %v", c.network, err) + log.Errorf("Failed to recalculate routes for network [%v]: %v", c.handler, err) } c.startPeersStatusChangeWatcher() @@ -351,14 +365,9 @@ func (c *clientNetwork) peersStateAndUpdateWatcher() { } } -func (c *clientNetwork) getAsInterface() *net.Interface { - intf, err := net.InterfaceByName(c.wgInterface.Name()) - if err != nil { - log.Warnf("Couldn't get interface by name %s: %v", c.wgInterface.Name(), err) - intf = &net.Interface{ - Name: c.wgInterface.Name(), - } +func handlerFromRoute(rt *route.Route, routeRefCounter *refcounter.RouteRefCounter, allowedIPsRefCounter *refcounter.AllowedIPsRefCounter, dnsRouterInteval time.Duration, statusRecorder *peer.Status) RouteHandler { + if rt.IsDynamic() { + return dynamic.NewRoute(rt, routeRefCounter, allowedIPsRefCounter, dnsRouterInteval, statusRecorder) } - - return intf + return static.NewRoute(rt, routeRefCounter, allowedIPsRefCounter) } diff --git a/client/internal/routemanager/client_test.go b/client/internal/routemanager/client_test.go index 9419ea777..0ae10e568 100644 --- a/client/internal/routemanager/client_test.go +++ b/client/internal/routemanager/client_test.go @@ -5,6 +5,7 @@ import ( "testing" "time" + "github.com/netbirdio/netbird/client/internal/routemanager/static" "github.com/netbirdio/netbird/route" ) @@ -340,9 +341,9 @@ func TestGetBestrouteFromStatuses(t *testing.T) { // create new clientNetwork client := &clientNetwork{ - network: netip.MustParsePrefix("192.168.0.0/24"), - routes: tc.existingRoutes, - chosenRoute: currentRoute, + handler: static.NewRoute(&route.Route{Network: netip.MustParsePrefix("192.168.0.0/24")}, nil, nil), + routes: tc.existingRoutes, + currentChosen: currentRoute, } chosenRoute := client.getBestRouteFromStatuses(tc.statuses) diff --git a/client/internal/routemanager/dynamic/route.go b/client/internal/routemanager/dynamic/route.go new file mode 100644 index 000000000..a81a0e0aa --- /dev/null +++ b/client/internal/routemanager/dynamic/route.go @@ -0,0 +1,361 @@ +package dynamic + +import ( + "context" + "fmt" + "net" + "net/netip" + "strings" + "sync" + "time" + + "github.com/hashicorp/go-multierror" + log "github.com/sirupsen/logrus" + + nberrors "github.com/netbirdio/netbird/client/errors" + "github.com/netbirdio/netbird/client/internal/peer" + "github.com/netbirdio/netbird/client/internal/routemanager/refcounter" + "github.com/netbirdio/netbird/client/internal/routemanager/util" + "github.com/netbirdio/netbird/management/domain" + "github.com/netbirdio/netbird/route" +) + +const ( + DefaultInterval = time.Minute + + minInterval = 2 * time.Second + + addAllowedIP = "add allowed IP %s: %w" +) + +type domainMap map[domain.Domain][]netip.Prefix + +type resolveResult struct { + domain domain.Domain + prefix netip.Prefix + err error +} + +type Route struct { + route *route.Route + routeRefCounter *refcounter.RouteRefCounter + allowedIPsRefcounter *refcounter.AllowedIPsRefCounter + interval time.Duration + dynamicDomains domainMap + mu sync.Mutex + currentPeerKey string + cancel context.CancelFunc + statusRecorder *peer.Status +} + +func NewRoute( + rt *route.Route, + routeRefCounter *refcounter.RouteRefCounter, + allowedIPsRefCounter *refcounter.AllowedIPsRefCounter, + interval time.Duration, + statusRecorder *peer.Status, +) *Route { + return &Route{ + route: rt, + routeRefCounter: routeRefCounter, + allowedIPsRefcounter: allowedIPsRefCounter, + interval: interval, + dynamicDomains: domainMap{}, + statusRecorder: statusRecorder, + } +} + +func (r *Route) String() string { + s, err := r.route.Domains.String() + if err != nil { + return r.route.Domains.PunycodeString() + } + return s +} + +func (r *Route) AddRoute(ctx context.Context) error { + r.mu.Lock() + defer r.mu.Unlock() + + if r.cancel != nil { + r.cancel() + } + + ctx, r.cancel = context.WithCancel(ctx) + + go r.startResolver(ctx) + + return nil +} + +// RemoveRoute will stop the dynamic resolver and remove all dynamic routes. +// It doesn't touch allowed IPs, these should be removed separately and before calling this method. +func (r *Route) RemoveRoute() error { + r.mu.Lock() + defer r.mu.Unlock() + + if r.cancel != nil { + r.cancel() + } + + var merr *multierror.Error + for domain, prefixes := range r.dynamicDomains { + for _, prefix := range prefixes { + if _, err := r.routeRefCounter.Decrement(prefix); err != nil { + merr = multierror.Append(merr, fmt.Errorf("remove dynamic route for IP %s: %w", prefix, err)) + } + } + log.Debugf("Removed dynamic route(s) for [%s]: %s", domain.SafeString(), strings.ReplaceAll(fmt.Sprintf("%s", prefixes), " ", ", ")) + + r.statusRecorder.DeleteResolvedDomainsStates(domain) + } + + r.dynamicDomains = domainMap{} + + return nberrors.FormatErrorOrNil(merr) +} + +func (r *Route) AddAllowedIPs(peerKey string) error { + r.mu.Lock() + defer r.mu.Unlock() + + var merr *multierror.Error + for domain, domainPrefixes := range r.dynamicDomains { + for _, prefix := range domainPrefixes { + if err := r.incrementAllowedIP(domain, prefix, peerKey); err != nil { + merr = multierror.Append(merr, fmt.Errorf(addAllowedIP, prefix, err)) + } + } + } + r.currentPeerKey = peerKey + return nberrors.FormatErrorOrNil(merr) +} + +func (r *Route) RemoveAllowedIPs() error { + r.mu.Lock() + defer r.mu.Unlock() + + var merr *multierror.Error + for _, domainPrefixes := range r.dynamicDomains { + for _, prefix := range domainPrefixes { + if _, err := r.allowedIPsRefcounter.Decrement(prefix); err != nil { + merr = multierror.Append(merr, fmt.Errorf("remove allowed IP %s: %w", prefix, err)) + } + } + } + + r.currentPeerKey = "" + return nberrors.FormatErrorOrNil(merr) +} + +func (r *Route) startResolver(ctx context.Context) { + log.Debugf("Starting dynamic route resolver for domains [%v]", r) + + interval := r.interval + if interval < minInterval { + interval = minInterval + log.Warnf("Dynamic route resolver interval %s is too low, setting to minimum value %s", r.interval, minInterval) + } + + ticker := time.NewTicker(interval) + defer ticker.Stop() + + r.update(ctx) + + for { + select { + case <-ctx.Done(): + log.Debugf("Stopping dynamic route resolver for domains [%v]", r) + return + case <-ticker.C: + r.update(ctx) + } + } +} + +func (r *Route) update(ctx context.Context) { + if resolved, err := r.resolveDomains(); err != nil { + log.Errorf("Failed to resolve domains for route [%v]: %v", r, err) + } else if err := r.updateDynamicRoutes(ctx, resolved); err != nil { + log.Errorf("Failed to update dynamic routes for [%v]: %v", r, err) + } +} + +func (r *Route) resolveDomains() (domainMap, error) { + results := make(chan resolveResult) + go r.resolve(results) + + resolved := domainMap{} + var merr *multierror.Error + + for result := range results { + if result.err != nil { + merr = multierror.Append(merr, result.err) + } else { + resolved[result.domain] = append(resolved[result.domain], result.prefix) + } + } + + return resolved, nberrors.FormatErrorOrNil(merr) +} + +func (r *Route) resolve(results chan resolveResult) { + var wg sync.WaitGroup + + for _, d := range r.route.Domains { + wg.Add(1) + go func(domain domain.Domain) { + defer wg.Done() + ips, err := net.LookupIP(string(domain)) + if err != nil { + results <- resolveResult{domain: domain, err: fmt.Errorf("resolve d %s: %w", domain.SafeString(), err)} + return + } + for _, ip := range ips { + prefix, err := util.GetPrefixFromIP(ip) + if err != nil { + results <- resolveResult{domain: domain, err: fmt.Errorf("get prefix from IP %s: %w", ip.String(), err)} + return + } + results <- resolveResult{domain: domain, prefix: prefix} + } + }(d) + } + + wg.Wait() + close(results) +} + +func (r *Route) updateDynamicRoutes(ctx context.Context, newDomains domainMap) error { + r.mu.Lock() + defer r.mu.Unlock() + + if ctx.Err() != nil { + return ctx.Err() + } + + var merr *multierror.Error + + for domain, newPrefixes := range newDomains { + oldPrefixes := r.dynamicDomains[domain] + toAdd, toRemove := determinePrefixChanges(oldPrefixes, newPrefixes) + + addedPrefixes, err := r.addRoutes(domain, toAdd) + if err != nil { + merr = multierror.Append(merr, err) + } else if len(addedPrefixes) > 0 { + log.Debugf("Added dynamic route(s) for [%s]: %s", domain.SafeString(), strings.ReplaceAll(fmt.Sprintf("%s", addedPrefixes), " ", ", ")) + } + + removedPrefixes, err := r.removeRoutes(toRemove) + if err != nil { + merr = multierror.Append(merr, err) + } else if len(removedPrefixes) > 0 { + log.Debugf("Removed dynamic route(s) for [%s]: %s", domain.SafeString(), strings.ReplaceAll(fmt.Sprintf("%s", removedPrefixes), " ", ", ")) + } + + updatedPrefixes := combinePrefixes(oldPrefixes, removedPrefixes, addedPrefixes) + r.dynamicDomains[domain] = updatedPrefixes + + r.statusRecorder.UpdateResolvedDomainsStates(domain, updatedPrefixes) + } + + return nberrors.FormatErrorOrNil(merr) +} + +func (r *Route) addRoutes(domain domain.Domain, prefixes []netip.Prefix) ([]netip.Prefix, error) { + var addedPrefixes []netip.Prefix + var merr *multierror.Error + + for _, prefix := range prefixes { + if _, err := r.routeRefCounter.Increment(prefix, nil); err != nil { + merr = multierror.Append(merr, fmt.Errorf("add dynamic route for IP %s: %w", prefix, err)) + continue + } + if r.currentPeerKey != "" { + if err := r.incrementAllowedIP(domain, prefix, r.currentPeerKey); err != nil { + merr = multierror.Append(merr, fmt.Errorf(addAllowedIP, prefix, err)) + } + } + addedPrefixes = append(addedPrefixes, prefix) + } + + return addedPrefixes, merr.ErrorOrNil() +} + +func (r *Route) removeRoutes(prefixes []netip.Prefix) ([]netip.Prefix, error) { + if r.route.KeepRoute { + return nil, nil + } + + var removedPrefixes []netip.Prefix + var merr *multierror.Error + + for _, prefix := range prefixes { + if _, err := r.routeRefCounter.Decrement(prefix); err != nil { + merr = multierror.Append(merr, fmt.Errorf("remove dynamic route for IP %s: %w", prefix, err)) + } + if r.currentPeerKey != "" { + if _, err := r.allowedIPsRefcounter.Decrement(prefix); err != nil { + merr = multierror.Append(merr, fmt.Errorf("remove allowed IP %s: %w", prefix, err)) + } + } + removedPrefixes = append(removedPrefixes, prefix) + } + + return removedPrefixes, merr.ErrorOrNil() +} + +func (r *Route) incrementAllowedIP(domain domain.Domain, prefix netip.Prefix, peerKey string) error { + if ref, err := r.allowedIPsRefcounter.Increment(prefix, peerKey); err != nil { + return fmt.Errorf(addAllowedIP, prefix, err) + } else if ref.Count > 1 && ref.Out != peerKey { + log.Warnf("IP [%s] for domain [%s] is already routed by peer [%s]. HA routing disabled", + prefix.Addr(), + domain.SafeString(), + ref.Out, + ) + + } + return nil +} + +func determinePrefixChanges(oldPrefixes, newPrefixes []netip.Prefix) (toAdd, toRemove []netip.Prefix) { + prefixSet := make(map[netip.Prefix]bool) + for _, prefix := range oldPrefixes { + prefixSet[prefix] = false + } + for _, prefix := range newPrefixes { + if _, exists := prefixSet[prefix]; exists { + prefixSet[prefix] = true + } else { + toAdd = append(toAdd, prefix) + } + } + for prefix, inUse := range prefixSet { + if !inUse { + toRemove = append(toRemove, prefix) + } + } + return +} + +func combinePrefixes(oldPrefixes, removedPrefixes, addedPrefixes []netip.Prefix) []netip.Prefix { + prefixSet := make(map[netip.Prefix]struct{}) + for _, prefix := range oldPrefixes { + prefixSet[prefix] = struct{}{} + } + for _, prefix := range removedPrefixes { + delete(prefixSet, prefix) + } + for _, prefix := range addedPrefixes { + prefixSet[prefix] = struct{}{} + } + + var combinedPrefixes []netip.Prefix + for prefix := range prefixSet { + combinedPrefixes = append(combinedPrefixes, prefix) + } + + return combinedPrefixes +} diff --git a/client/internal/routemanager/manager.go b/client/internal/routemanager/manager.go index 47549f74d..53943055c 100644 --- a/client/internal/routemanager/manager.go +++ b/client/internal/routemanager/manager.go @@ -2,18 +2,23 @@ package routemanager import ( "context" + "errors" "fmt" "net" "net/netip" "net/url" "runtime" "sync" + "time" log "github.com/sirupsen/logrus" firewall "github.com/netbirdio/netbird/client/firewall/manager" "github.com/netbirdio/netbird/client/internal/listener" "github.com/netbirdio/netbird/client/internal/peer" + "github.com/netbirdio/netbird/client/internal/routemanager/refcounter" + "github.com/netbirdio/netbird/client/internal/routemanager/systemops" + "github.com/netbirdio/netbird/client/internal/routemanager/vars" "github.com/netbirdio/netbird/client/internal/routeselector" "github.com/netbirdio/netbird/iface" "github.com/netbirdio/netbird/route" @@ -21,11 +26,6 @@ import ( "github.com/netbirdio/netbird/version" ) -var defaultv4 = netip.PrefixFrom(netip.IPv4Unspecified(), 0) - -// nolint:unused -var defaultv6 = netip.PrefixFrom(netip.IPv6Unspecified(), 0) - // Manager is a route manager interface type Manager interface { Init() (peer.BeforeAddPeerHookFunc, peer.AfterRemovePeerHookFunc, error) @@ -40,31 +40,71 @@ type Manager interface { // DefaultManager is the default instance of a route manager type DefaultManager struct { - ctx context.Context - stop context.CancelFunc - mux sync.Mutex - clientNetworks map[route.HAUniqueID]*clientNetwork - routeSelector *routeselector.RouteSelector - serverRouter serverRouter - statusRecorder *peer.Status - wgInterface *iface.WGIface - pubKey string - notifier *notifier + ctx context.Context + stop context.CancelFunc + mux sync.Mutex + clientNetworks map[route.HAUniqueID]*clientNetwork + routeSelector *routeselector.RouteSelector + serverRouter serverRouter + sysOps *systemops.SysOps + statusRecorder *peer.Status + wgInterface *iface.WGIface + pubKey string + notifier *notifier + routeRefCounter *refcounter.RouteRefCounter + allowedIPsRefCounter *refcounter.AllowedIPsRefCounter + dnsRouteInterval time.Duration } -func NewManager(ctx context.Context, pubKey string, wgInterface *iface.WGIface, statusRecorder *peer.Status, initialRoutes []*route.Route) *DefaultManager { +func NewManager( + ctx context.Context, + pubKey string, + dnsRouteInterval time.Duration, + wgInterface *iface.WGIface, + statusRecorder *peer.Status, + initialRoutes []*route.Route, +) *DefaultManager { mCTX, cancel := context.WithCancel(ctx) + sysOps := systemops.NewSysOps(wgInterface) + dm := &DefaultManager{ - ctx: mCTX, - stop: cancel, - clientNetworks: make(map[route.HAUniqueID]*clientNetwork), - routeSelector: routeselector.NewRouteSelector(), - statusRecorder: statusRecorder, - wgInterface: wgInterface, - pubKey: pubKey, - notifier: newNotifier(), + ctx: mCTX, + stop: cancel, + dnsRouteInterval: dnsRouteInterval, + clientNetworks: make(map[route.HAUniqueID]*clientNetwork), + routeSelector: routeselector.NewRouteSelector(), + sysOps: sysOps, + statusRecorder: statusRecorder, + wgInterface: wgInterface, + pubKey: pubKey, + notifier: newNotifier(), } + dm.routeRefCounter = refcounter.New( + func(prefix netip.Prefix, _ any) (any, error) { + return nil, sysOps.AddVPNRoute(prefix, wgInterface.ToInterface()) + }, + func(prefix netip.Prefix, _ any) error { + return sysOps.RemoveVPNRoute(prefix, wgInterface.ToInterface()) + }, + ) + + dm.allowedIPsRefCounter = refcounter.New( + func(prefix netip.Prefix, peerKey string) (string, error) { + // save peerKey to use it in the remove function + return peerKey, wgInterface.AddAllowedIP(peerKey, prefix.String()) + }, + func(prefix netip.Prefix, peerKey string) error { + if err := wgInterface.RemoveAllowedIP(peerKey, prefix.String()); err != nil { + if !errors.Is(err, iface.ErrPeerNotFound) && !errors.Is(err, iface.ErrAllowedIPNotFound) { + return err + } + log.Tracef("Remove allowed IPs %s for %s: %v", prefix, peerKey, err) + } + return nil + }, + ) + if runtime.GOOS == "android" { cr := dm.clientRoutes(initialRoutes) dm.notifier.setInitialClientRoutes(cr) @@ -78,7 +118,7 @@ func (m *DefaultManager) Init() (peer.BeforeAddPeerHookFunc, peer.AfterRemovePee return nil, nil, nil } - if err := cleanupRouting(); err != nil { + if err := m.sysOps.CleanupRouting(); err != nil { log.Warnf("Failed cleaning up routing: %v", err) } @@ -86,7 +126,7 @@ func (m *DefaultManager) Init() (peer.BeforeAddPeerHookFunc, peer.AfterRemovePee signalAddress := m.statusRecorder.GetSignalState().URL ips := resolveURLsToIPs([]string{mgmtAddress, signalAddress}) - beforePeerHook, afterPeerHook, err := setupRouting(ips, m.wgInterface) + beforePeerHook, afterPeerHook, err := m.sysOps.SetupRouting(ips) if err != nil { return nil, nil, fmt.Errorf("setup routing: %w", err) } @@ -110,8 +150,19 @@ func (m *DefaultManager) Stop() { m.serverRouter.cleanUp() } + if m.routeRefCounter != nil { + if err := m.routeRefCounter.Flush(); err != nil { + log.Errorf("Error flushing route ref counter: %v", err) + } + } + if m.allowedIPsRefCounter != nil { + if err := m.allowedIPsRefCounter.Flush(); err != nil { + log.Errorf("Error flushing allowed IPs ref counter: %v", err) + } + } + if !nbnet.CustomRoutingDisabled() { - if err := cleanupRouting(); err != nil { + if err := m.sysOps.CleanupRouting(); err != nil { log.Errorf("Error cleaning up routing: %v", err) } else { log.Info("Routing cleanup complete") @@ -185,7 +236,7 @@ func (m *DefaultManager) TriggerSelection(networks route.HAMap) { continue } - clientNetworkWatcher := newClientNetworkWatcher(m.ctx, m.wgInterface, m.statusRecorder, routes[0].Network) + clientNetworkWatcher := newClientNetworkWatcher(m.ctx, m.dnsRouteInterval, m.wgInterface, m.statusRecorder, routes[0], m.routeRefCounter, m.allowedIPsRefCounter) m.clientNetworks[id] = clientNetworkWatcher go clientNetworkWatcher.peersStateAndUpdateWatcher() clientNetworkWatcher.sendUpdateToClientNetworkWatcher(routesUpdate{routes: routes}) @@ -197,7 +248,7 @@ func (m *DefaultManager) stopObsoleteClients(networks route.HAMap) { for id, client := range m.clientNetworks { if _, ok := networks[id]; !ok { log.Debugf("Stopping client network watcher, %s", id) - client.stop() + client.cancel() delete(m.clientNetworks, id) } } @@ -210,7 +261,7 @@ func (m *DefaultManager) updateClientNetworks(updateSerial uint64, networks rout for id, routes := range networks { clientNetworkWatcher, found := m.clientNetworks[id] if !found { - clientNetworkWatcher = newClientNetworkWatcher(m.ctx, m.wgInterface, m.statusRecorder, routes[0].Network) + clientNetworkWatcher = newClientNetworkWatcher(m.ctx, m.dnsRouteInterval, m.wgInterface, m.statusRecorder, routes[0], m.routeRefCounter, m.allowedIPsRefCounter) m.clientNetworks[id] = clientNetworkWatcher go clientNetworkWatcher.peersStateAndUpdateWatcher() } @@ -228,7 +279,7 @@ func (m *DefaultManager) classifyRoutes(newRoutes []*route.Route) (map[route.ID] ownNetworkIDs := make(map[route.HAUniqueID]bool) for _, newRoute := range newRoutes { - haID := route.GetHAUniqueID(newRoute) + haID := newRoute.GetHAUniqueID() if newRoute.Peer == m.pubKey { ownNetworkIDs[haID] = true // only linux is supported for now @@ -241,9 +292,9 @@ func (m *DefaultManager) classifyRoutes(newRoutes []*route.Route) (map[route.ID] } for _, newRoute := range newRoutes { - haID := route.GetHAUniqueID(newRoute) + haID := newRoute.GetHAUniqueID() if !ownNetworkIDs[haID] { - if !isPrefixSupported(newRoute.Network) { + if !isRouteSupported(newRoute) { continue } newClientRoutesIDMap[haID] = append(newClientRoutesIDMap[haID], newRoute) @@ -255,23 +306,23 @@ func (m *DefaultManager) classifyRoutes(newRoutes []*route.Route) (map[route.ID] func (m *DefaultManager) clientRoutes(initialRoutes []*route.Route) []*route.Route { _, crMap := m.classifyRoutes(initialRoutes) - rs := make([]*route.Route, 0) + rs := make([]*route.Route, len(crMap)) for _, routes := range crMap { rs = append(rs, routes...) } return rs } -func isPrefixSupported(prefix netip.Prefix) bool { - if !nbnet.CustomRoutingDisabled() { +func isRouteSupported(route *route.Route) bool { + if !nbnet.CustomRoutingDisabled() || route.IsDynamic() { return true } // If prefix is too small, lets assume it is a possible default prefix which is not yet supported // we skip this prefix management - if prefix.Bits() <= minRangeBits { + if route.Network.Bits() <= vars.MinRangeBits { log.Warnf("This agent version: %s, doesn't support default routes, received %s, skipping this prefix", - version.NetbirdVersion(), prefix) + version.NetbirdVersion(), route.Network) return false } return true diff --git a/client/internal/routemanager/manager_test.go b/client/internal/routemanager/manager_test.go index 7eb8dd002..5d32032a5 100644 --- a/client/internal/routemanager/manager_test.go +++ b/client/internal/routemanager/manager_test.go @@ -416,7 +416,7 @@ func TestManagerUpdateRoutes(t *testing.T) { statusRecorder := peer.NewRecorder("https://mgm") ctx := context.TODO() - routeManager := NewManager(ctx, localPeerKey, wgInterface, statusRecorder, nil) + routeManager := NewManager(ctx, localPeerKey, 0, wgInterface, statusRecorder, nil) _, _, err = routeManager.Init() diff --git a/client/internal/routemanager/refcounter/refcounter.go b/client/internal/routemanager/refcounter/refcounter.go new file mode 100644 index 000000000..f1d696ad9 --- /dev/null +++ b/client/internal/routemanager/refcounter/refcounter.go @@ -0,0 +1,155 @@ +package refcounter + +import ( + "errors" + "fmt" + "net/netip" + "sync" + + "github.com/hashicorp/go-multierror" + log "github.com/sirupsen/logrus" + + nberrors "github.com/netbirdio/netbird/client/errors" +) + +// ErrIgnore can be returned by AddFunc to indicate that the counter not be incremented for the given prefix. +var ErrIgnore = errors.New("ignore") + +type Ref[O any] struct { + Count int + Out O +} + +type AddFunc[I, O any] func(prefix netip.Prefix, in I) (out O, err error) +type RemoveFunc[I, O any] func(prefix netip.Prefix, out O) error + +type Counter[I, O any] struct { + // refCountMap keeps track of the reference Ref for prefixes + refCountMap map[netip.Prefix]Ref[O] + refCountMu sync.Mutex + // idMap keeps track of the prefixes associated with an ID for removal + idMap map[string][]netip.Prefix + idMu sync.Mutex + add AddFunc[I, O] + remove RemoveFunc[I, O] +} + +// New creates a new Counter instance +func New[I, O any](add AddFunc[I, O], remove RemoveFunc[I, O]) *Counter[I, O] { + return &Counter[I, O]{ + refCountMap: map[netip.Prefix]Ref[O]{}, + idMap: map[string][]netip.Prefix{}, + add: add, + remove: remove, + } +} + +// Increment increments the reference count for the given prefix. +// If this is the first reference to the prefix, the AddFunc is called. +func (rm *Counter[I, O]) Increment(prefix netip.Prefix, in I) (Ref[O], error) { + rm.refCountMu.Lock() + defer rm.refCountMu.Unlock() + + ref := rm.refCountMap[prefix] + log.Tracef("Increasing ref count %d for prefix %s with [%v]", ref.Count, prefix, ref.Out) + + // Call AddFunc only if it's a new prefix + if ref.Count == 0 { + log.Tracef("Adding for prefix %s with [%v]", prefix, ref.Out) + out, err := rm.add(prefix, in) + + if errors.Is(err, ErrIgnore) { + return ref, nil + } + if err != nil { + return ref, fmt.Errorf("failed to add for prefix %s: %w", prefix, err) + } + ref.Out = out + } + + ref.Count++ + rm.refCountMap[prefix] = ref + + return ref, nil +} + +// IncrementWithID increments the reference count for the given prefix and groups it under the given ID. +// If this is the first reference to the prefix, the AddFunc is called. +func (rm *Counter[I, O]) IncrementWithID(id string, prefix netip.Prefix, in I) (Ref[O], error) { + rm.idMu.Lock() + defer rm.idMu.Unlock() + + ref, err := rm.Increment(prefix, in) + if err != nil { + return ref, fmt.Errorf("with ID: %w", err) + } + rm.idMap[id] = append(rm.idMap[id], prefix) + + return ref, nil +} + +// Decrement decrements the reference count for the given prefix. +// If the reference count reaches 0, the RemoveFunc is called. +func (rm *Counter[I, O]) Decrement(prefix netip.Prefix) (Ref[O], error) { + rm.refCountMu.Lock() + defer rm.refCountMu.Unlock() + + ref, ok := rm.refCountMap[prefix] + if !ok { + log.Tracef("No reference found for prefix %s", prefix) + return ref, nil + } + + log.Tracef("Decreasing ref count %d for prefix %s with [%v]", ref.Count, prefix, ref.Out) + if ref.Count == 1 { + log.Tracef("Removing for prefix %s with [%v]", prefix, ref.Out) + if err := rm.remove(prefix, ref.Out); err != nil { + return ref, fmt.Errorf("remove for prefix %s: %w", prefix, err) + } + delete(rm.refCountMap, prefix) + } else { + ref.Count-- + rm.refCountMap[prefix] = ref + } + + return ref, nil +} + +// DecrementWithID decrements the reference count for all prefixes associated with the given ID. +// If the reference count reaches 0, the RemoveFunc is called. +func (rm *Counter[I, O]) DecrementWithID(id string) error { + rm.idMu.Lock() + defer rm.idMu.Unlock() + + var merr *multierror.Error + for _, prefix := range rm.idMap[id] { + if _, err := rm.Decrement(prefix); err != nil { + merr = multierror.Append(merr, err) + } + } + delete(rm.idMap, id) + + return nberrors.FormatErrorOrNil(merr) +} + +// Flush removes all references and calls RemoveFunc for each prefix. +func (rm *Counter[I, O]) Flush() error { + rm.refCountMu.Lock() + defer rm.refCountMu.Unlock() + rm.idMu.Lock() + defer rm.idMu.Unlock() + + var merr *multierror.Error + for prefix := range rm.refCountMap { + log.Tracef("Removing for prefix %s", prefix) + ref := rm.refCountMap[prefix] + if err := rm.remove(prefix, ref.Out); err != nil { + merr = multierror.Append(merr, fmt.Errorf("remove for prefix %s: %w", prefix, err)) + } + } + rm.refCountMap = map[netip.Prefix]Ref[O]{} + + rm.idMap = map[string][]netip.Prefix{} + + return nberrors.FormatErrorOrNil(merr) +} diff --git a/client/internal/routemanager/refcounter/types.go b/client/internal/routemanager/refcounter/types.go new file mode 100644 index 000000000..6753b64ef --- /dev/null +++ b/client/internal/routemanager/refcounter/types.go @@ -0,0 +1,7 @@ +package refcounter + +// RouteRefCounter is a Counter for Route, it doesn't take any input on Increment and doesn't use any output on Decrement +type RouteRefCounter = Counter[any, any] + +// AllowedIPsRefCounter is a Counter for AllowedIPs, it takes a peer key on Increment and passes it back to Decrement +type AllowedIPsRefCounter = Counter[string, string] diff --git a/client/internal/routemanager/routemanager.go b/client/internal/routemanager/routemanager.go deleted file mode 100644 index 7715aa819..000000000 --- a/client/internal/routemanager/routemanager.go +++ /dev/null @@ -1,127 +0,0 @@ -//go:build !android && !ios - -package routemanager - -import ( - "errors" - "fmt" - "net" - "net/netip" - "sync" - - "github.com/hashicorp/go-multierror" - log "github.com/sirupsen/logrus" - - nbnet "github.com/netbirdio/netbird/util/net" -) - -type ref struct { - count int - nexthop netip.Addr - intf *net.Interface -} - -type RouteManager struct { - // refCountMap keeps track of the reference ref for prefixes - refCountMap map[netip.Prefix]ref - // prefixMap keeps track of the prefixes associated with a connection ID for removal - prefixMap map[nbnet.ConnectionID][]netip.Prefix - addRoute AddRouteFunc - removeRoute RemoveRouteFunc - mutex sync.Mutex -} - -type AddRouteFunc func(prefix netip.Prefix) (nexthop netip.Addr, intf *net.Interface, err error) -type RemoveRouteFunc func(prefix netip.Prefix, nexthop netip.Addr, intf *net.Interface) error - -func NewRouteManager(addRoute AddRouteFunc, removeRoute RemoveRouteFunc) *RouteManager { - // TODO: read initial routing table into refCountMap - return &RouteManager{ - refCountMap: map[netip.Prefix]ref{}, - prefixMap: map[nbnet.ConnectionID][]netip.Prefix{}, - addRoute: addRoute, - removeRoute: removeRoute, - } -} - -func (rm *RouteManager) AddRouteRef(connID nbnet.ConnectionID, prefix netip.Prefix) error { - rm.mutex.Lock() - defer rm.mutex.Unlock() - - ref := rm.refCountMap[prefix] - log.Debugf("Increasing route ref count %d for prefix %s", ref.count, prefix) - - // Add route to the system, only if it's a new prefix - if ref.count == 0 { - log.Debugf("Adding route for prefix %s", prefix) - nexthop, intf, err := rm.addRoute(prefix) - if errors.Is(err, ErrRouteNotFound) { - return nil - } - if errors.Is(err, ErrRouteNotAllowed) { - log.Debugf("Adding route for prefix %s: %s", prefix, err) - } - if err != nil { - return fmt.Errorf("failed to add route for prefix %s: %w", prefix, err) - } - ref.nexthop = nexthop - ref.intf = intf - } - - ref.count++ - rm.refCountMap[prefix] = ref - rm.prefixMap[connID] = append(rm.prefixMap[connID], prefix) - - return nil -} - -func (rm *RouteManager) RemoveRouteRef(connID nbnet.ConnectionID) error { - rm.mutex.Lock() - defer rm.mutex.Unlock() - - prefixes, ok := rm.prefixMap[connID] - if !ok { - log.Debugf("No prefixes found for connection ID %s", connID) - return nil - } - - var result *multierror.Error - for _, prefix := range prefixes { - ref := rm.refCountMap[prefix] - log.Debugf("Decreasing route ref count %d for prefix %s", ref.count, prefix) - if ref.count == 1 { - log.Debugf("Removing route for prefix %s", prefix) - // TODO: don't fail if the route is not found - if err := rm.removeRoute(prefix, ref.nexthop, ref.intf); err != nil { - result = multierror.Append(result, fmt.Errorf("remove route for prefix %s: %w", prefix, err)) - continue - } - delete(rm.refCountMap, prefix) - } else { - ref.count-- - rm.refCountMap[prefix] = ref - } - } - delete(rm.prefixMap, connID) - - return result.ErrorOrNil() -} - -// Flush removes all references and routes from the system -func (rm *RouteManager) Flush() error { - rm.mutex.Lock() - defer rm.mutex.Unlock() - - var result *multierror.Error - for prefix := range rm.refCountMap { - log.Debugf("Removing route for prefix %s", prefix) - ref := rm.refCountMap[prefix] - if err := rm.removeRoute(prefix, ref.nexthop, ref.intf); err != nil { - result = multierror.Append(result, fmt.Errorf("remove route for prefix %s: %w", prefix, err)) - } - } - rm.refCountMap = map[netip.Prefix]ref{} - rm.prefixMap = map[nbnet.ConnectionID][]netip.Prefix{} - - return result.ErrorOrNil() -} diff --git a/client/internal/routemanager/server_nonandroid.go b/client/internal/routemanager/server_nonandroid.go index 95672e480..24267efdc 100644 --- a/client/internal/routemanager/server_nonandroid.go +++ b/client/internal/routemanager/server_nonandroid.go @@ -5,13 +5,14 @@ package routemanager import ( "context" "fmt" - "net/netip" + "net" "sync" log "github.com/sirupsen/logrus" firewall "github.com/netbirdio/netbird/client/firewall/manager" "github.com/netbirdio/netbird/client/internal/peer" + "github.com/netbirdio/netbird/client/internal/routemanager/systemops" "github.com/netbirdio/netbird/iface" "github.com/netbirdio/netbird/route" ) @@ -70,7 +71,7 @@ func (m *defaultServerRouter) updateRoutes(routesMap map[route.ID]*route.Route) } if len(m.routes) > 0 { - err := enableIPForwarding() + err := systemops.EnableIPForwarding() if err != nil { return err } @@ -88,7 +89,7 @@ func (m *defaultServerRouter) removeFromServerNetwork(route *route.Route) error m.mux.Lock() defer m.mux.Unlock() - routerPair, err := routeToRouterPair(m.wgInterface.Address().Masked().String(), route) + routerPair, err := routeToRouterPair(m.wgInterface.Address().Network, route) if err != nil { return fmt.Errorf("parse prefix: %w", err) } @@ -117,7 +118,7 @@ func (m *defaultServerRouter) addToServerNetwork(route *route.Route) error { m.mux.Lock() defer m.mux.Unlock() - routerPair, err := routeToRouterPair(m.wgInterface.Address().Masked().String(), route) + routerPair, err := routeToRouterPair(m.wgInterface.Address().Network, route) if err != nil { return fmt.Errorf("parse prefix: %w", err) } @@ -133,7 +134,13 @@ func (m *defaultServerRouter) addToServerNetwork(route *route.Route) error { if state.Routes == nil { state.Routes = map[string]struct{}{} } - state.Routes[route.Network.String()] = struct{}{} + + routeStr := route.Network.String() + if route.IsDynamic() { + routeStr = route.Domains.SafeString() + } + state.Routes[routeStr] = struct{}{} + m.statusRecorder.UpdateLocalPeerState(state) return nil @@ -144,7 +151,7 @@ func (m *defaultServerRouter) cleanUp() { m.mux.Lock() defer m.mux.Unlock() for _, r := range m.routes { - routerPair, err := routeToRouterPair(m.wgInterface.Address().Masked().String(), r) + routerPair, err := routeToRouterPair(m.wgInterface.Address().Network, r) if err != nil { log.Errorf("Failed to convert route to router pair: %v", err) continue @@ -162,15 +169,17 @@ func (m *defaultServerRouter) cleanUp() { m.statusRecorder.UpdateLocalPeerState(state) } -func routeToRouterPair(source string, route *route.Route) (firewall.RouterPair, error) { - parsed, err := netip.ParsePrefix(source) - if err != nil { - return firewall.RouterPair{}, err +func routeToRouterPair(source *net.IPNet, route *route.Route) (firewall.RouterPair, error) { + destination := route.Network.Masked().String() + if route.IsDynamic() { + // TODO: add ipv6 + destination = "0.0.0.0/0" } + return firewall.RouterPair{ ID: string(route.ID), - Source: parsed.String(), - Destination: route.Network.Masked().String(), + Source: source.String(), + Destination: destination, Masquerade: route.Masquerade, }, nil } diff --git a/client/internal/routemanager/static/route.go b/client/internal/routemanager/static/route.go new file mode 100644 index 000000000..88cca522a --- /dev/null +++ b/client/internal/routemanager/static/route.go @@ -0,0 +1,57 @@ +package static + +import ( + "context" + "fmt" + + log "github.com/sirupsen/logrus" + + "github.com/netbirdio/netbird/client/internal/routemanager/refcounter" + "github.com/netbirdio/netbird/route" +) + +type Route struct { + route *route.Route + routeRefCounter *refcounter.RouteRefCounter + allowedIPsRefcounter *refcounter.AllowedIPsRefCounter +} + +func NewRoute(rt *route.Route, routeRefCounter *refcounter.RouteRefCounter, allowedIPsRefCounter *refcounter.AllowedIPsRefCounter) *Route { + return &Route{ + route: rt, + routeRefCounter: routeRefCounter, + allowedIPsRefcounter: allowedIPsRefCounter, + } +} + +// Route route methods +func (r *Route) String() string { + return r.route.Network.String() +} + +func (r *Route) AddRoute(context.Context) error { + _, err := r.routeRefCounter.Increment(r.route.Network, nil) + return err +} + +func (r *Route) RemoveRoute() error { + _, err := r.routeRefCounter.Decrement(r.route.Network) + return err +} + +func (r *Route) AddAllowedIPs(peerKey string) error { + if ref, err := r.allowedIPsRefcounter.Increment(r.route.Network, peerKey); err != nil { + return fmt.Errorf("add allowed IP %s: %w", r.route.Network, err) + } else if ref.Count > 1 && ref.Out != peerKey { + log.Warnf("Prefix [%s] is already routed by peer [%s]. HA routing disabled", + r.route.Network, + ref.Out, + ) + } + return nil +} + +func (r *Route) RemoveAllowedIPs() error { + _, err := r.allowedIPsRefcounter.Decrement(r.route.Network) + return err +} diff --git a/client/internal/routemanager/sysctl/sysctl_linux.go b/client/internal/routemanager/sysctl/sysctl_linux.go new file mode 100644 index 000000000..3f2937c89 --- /dev/null +++ b/client/internal/routemanager/sysctl/sysctl_linux.go @@ -0,0 +1,103 @@ +// go:build !android +package sysctl + +import ( + "fmt" + "net" + "os" + "strconv" + "strings" + + "github.com/hashicorp/go-multierror" + log "github.com/sirupsen/logrus" + + nberrors "github.com/netbirdio/netbird/client/errors" + "github.com/netbirdio/netbird/iface" +) + +const ( + rpFilterPath = "net.ipv4.conf.all.rp_filter" + rpFilterInterfacePath = "net.ipv4.conf.%s.rp_filter" + srcValidMarkPath = "net.ipv4.conf.all.src_valid_mark" +) + +// Setup configures sysctl settings for RP filtering and source validation. +func Setup(wgIface *iface.WGIface) (map[string]int, error) { + keys := map[string]int{} + var result *multierror.Error + + oldVal, err := Set(srcValidMarkPath, 1, false) + if err != nil { + result = multierror.Append(result, err) + } else { + keys[srcValidMarkPath] = oldVal + } + + oldVal, err = Set(rpFilterPath, 2, true) + if err != nil { + result = multierror.Append(result, err) + } else { + keys[rpFilterPath] = oldVal + } + + interfaces, err := net.Interfaces() + if err != nil { + result = multierror.Append(result, fmt.Errorf("list interfaces: %w", err)) + } + + for _, intf := range interfaces { + if intf.Name == "lo" || wgIface != nil && intf.Name == wgIface.Name() { + continue + } + + i := fmt.Sprintf(rpFilterInterfacePath, intf.Name) + oldVal, err := Set(i, 2, true) + if err != nil { + result = multierror.Append(result, err) + } else { + keys[i] = oldVal + } + } + + return keys, nberrors.FormatErrorOrNil(result) +} + +// Set sets a sysctl configuration, if onlyIfOne is true it will only set the new value if it's set to 1 +func Set(key string, desiredValue int, onlyIfOne bool) (int, error) { + path := fmt.Sprintf("/proc/sys/%s", strings.ReplaceAll(key, ".", "/")) + currentValue, err := os.ReadFile(path) + if err != nil { + return -1, fmt.Errorf("read sysctl %s: %w", key, err) + } + + currentV, err := strconv.Atoi(strings.TrimSpace(string(currentValue))) + if err != nil && len(currentValue) > 0 { + return -1, fmt.Errorf("convert current desiredValue to int: %w", err) + } + + if currentV == desiredValue || onlyIfOne && currentV != 1 { + return currentV, nil + } + + //nolint:gosec + if err := os.WriteFile(path, []byte(strconv.Itoa(desiredValue)), 0644); err != nil { + return currentV, fmt.Errorf("write sysctl %s: %w", key, err) + } + log.Debugf("Set sysctl %s from %d to %d", key, currentV, desiredValue) + + return currentV, nil +} + +// Cleanup resets sysctl settings to their original values. +func Cleanup(originalSettings map[string]int) error { + var result *multierror.Error + + for key, value := range originalSettings { + _, err := Set(key, value, false) + if err != nil { + result = multierror.Append(result, err) + } + } + + return nberrors.FormatErrorOrNil(result) +} diff --git a/client/internal/routemanager/systemops/routeflags_bsd.go b/client/internal/routemanager/systemops/routeflags_bsd.go new file mode 100644 index 000000000..12f158dcb --- /dev/null +++ b/client/internal/routemanager/systemops/routeflags_bsd.go @@ -0,0 +1,18 @@ +//go:build darwin || dragonfly || netbsd || openbsd + +package systemops + +import "syscall" + +// filterRoutesByFlags - return true if need to ignore such route message because it consists specific flags. +func filterRoutesByFlags(routeMessageFlags int) bool { + if routeMessageFlags&syscall.RTF_UP == 0 { + return true + } + + if routeMessageFlags&(syscall.RTF_REJECT|syscall.RTF_BLACKHOLE|syscall.RTF_WASCLONED) != 0 { + return true + } + + return false +} diff --git a/client/internal/routemanager/systemops/routeflags_freebsd.go b/client/internal/routemanager/systemops/routeflags_freebsd.go new file mode 100644 index 000000000..cb35f521e --- /dev/null +++ b/client/internal/routemanager/systemops/routeflags_freebsd.go @@ -0,0 +1,19 @@ +//go:build: freebsd +package systemops + +import "syscall" + +// filterRoutesByFlags - return true if need to ignore such route message because it consists specific flags. +func filterRoutesByFlags(routeMessageFlags int) bool { + if routeMessageFlags&syscall.RTF_UP == 0 { + return true + } + + // NOTE: syscall.RTF_WASCLONED deprecated in FreeBSD 8.0 (https://www.freebsd.org/releases/8.0R/relnotes-detailed/) + // a concept of cloned route (a route generated by an entry with RTF_CLONING flag) is deprecated. + if routeMessageFlags&(syscall.RTF_REJECT|syscall.RTF_BLACKHOLE) != 0 { + return true + } + + return false +} diff --git a/client/internal/routemanager/systemops/systemops.go b/client/internal/routemanager/systemops/systemops.go new file mode 100644 index 000000000..9ee51538b --- /dev/null +++ b/client/internal/routemanager/systemops/systemops.go @@ -0,0 +1,27 @@ +package systemops + +import ( + "net" + "net/netip" + + "github.com/netbirdio/netbird/client/internal/routemanager/refcounter" + "github.com/netbirdio/netbird/iface" +) + +type Nexthop struct { + IP netip.Addr + Intf *net.Interface +} + +type ExclusionCounter = refcounter.Counter[any, Nexthop] + +type SysOps struct { + refCounter *ExclusionCounter + wgInterface *iface.WGIface +} + +func NewSysOps(wgInterface *iface.WGIface) *SysOps { + return &SysOps{ + wgInterface: wgInterface, + } +} diff --git a/client/internal/routemanager/systemops_bsd.go b/client/internal/routemanager/systemops/systemops_bsd.go similarity index 95% rename from client/internal/routemanager/systemops_bsd.go rename to client/internal/routemanager/systemops/systemops_bsd.go index a3548a1f1..eb7b7a03d 100644 --- a/client/internal/routemanager/systemops_bsd.go +++ b/client/internal/routemanager/systemops/systemops_bsd.go @@ -1,6 +1,6 @@ //go:build darwin || dragonfly || freebsd || netbsd || openbsd -package routemanager +package systemops import ( "errors" @@ -43,8 +43,7 @@ func getRoutesFromTable() ([]netip.Prefix, error) { return nil, fmt.Errorf("unexpected RIB message type: %d", m.Type) } - if m.Flags&syscall.RTF_UP == 0 || - m.Flags&(syscall.RTF_REJECT|syscall.RTF_BLACKHOLE|syscall.RTF_WASCLONED) != 0 { + if filterRoutesByFlags(m.Flags) { continue } @@ -101,6 +100,7 @@ func toNetIP(a route.Addr) netip.Addr { } } +// ones returns the number of leading ones in the mask. func ones(a route.Addr) (int, error) { switch t := a.(type) { case *route.Inet4Addr: @@ -114,6 +114,7 @@ func ones(a route.Addr) (int, error) { } } +// MsgToRoute converts a route message to a Route. func MsgToRoute(msg *route.RouteMessage) (*Route, error) { dstIP, nexthop, dstMask := msg.Addrs[0], msg.Addrs[1], msg.Addrs[2] diff --git a/client/internal/routemanager/systemops_bsd_test.go b/client/internal/routemanager/systemops/systemops_bsd_test.go similarity index 98% rename from client/internal/routemanager/systemops_bsd_test.go rename to client/internal/routemanager/systemops/systemops_bsd_test.go index 81bca504c..2240b053c 100644 --- a/client/internal/routemanager/systemops_bsd_test.go +++ b/client/internal/routemanager/systemops/systemops_bsd_test.go @@ -1,6 +1,6 @@ //go:build darwin || dragonfly || freebsd || netbsd || openbsd -package routemanager +package systemops import ( "testing" diff --git a/client/internal/routemanager/systemops_darwin_test.go b/client/internal/routemanager/systemops/systemops_darwin_test.go similarity index 94% rename from client/internal/routemanager/systemops_darwin_test.go rename to client/internal/routemanager/systemops/systemops_darwin_test.go index c23a7cde3..e42bd343d 100644 --- a/client/internal/routemanager/systemops_darwin_test.go +++ b/client/internal/routemanager/systemops/systemops_darwin_test.go @@ -1,6 +1,6 @@ //go:build !ios -package routemanager +package systemops import ( "fmt" @@ -35,13 +35,15 @@ func TestConcurrentRoutes(t *testing.T) { baseIP := netip.MustParseAddr("192.0.2.0") intf := &net.Interface{Name: "lo0"} + r := NewSysOps(nil) + var wg sync.WaitGroup for i := 0; i < 1024; i++ { wg.Add(1) go func(ip netip.Addr) { defer wg.Done() prefix := netip.PrefixFrom(ip, 32) - if err := addToRouteTable(prefix, netip.Addr{}, intf); err != nil { + if err := r.addToRouteTable(prefix, Nexthop{netip.Addr{}, intf}); err != nil { t.Errorf("Failed to add route for %s: %v", prefix, err) } }(baseIP) @@ -57,7 +59,7 @@ func TestConcurrentRoutes(t *testing.T) { go func(ip netip.Addr) { defer wg.Done() prefix := netip.PrefixFrom(ip, 32) - if err := removeFromRouteTable(prefix, netip.Addr{}, intf); err != nil { + if err := r.removeFromRouteTable(prefix, Nexthop{netip.Addr{}, intf}); err != nil { t.Errorf("Failed to remove route for %s: %v", prefix, err) } }(baseIP) diff --git a/client/internal/routemanager/systemops.go b/client/internal/routemanager/systemops/systemops_generic.go similarity index 56% rename from client/internal/routemanager/systemops.go rename to client/internal/routemanager/systemops/systemops_generic.go index bc506411c..01b9ebda6 100644 --- a/client/internal/routemanager/systemops.go +++ b/client/internal/routemanager/systemops/systemops_generic.go @@ -1,6 +1,6 @@ //go:build !android && !ios -package routemanager +package systemops import ( "context" @@ -15,7 +15,11 @@ import ( "github.com/libp2p/go-netroute" log "github.com/sirupsen/logrus" + nberrors "github.com/netbirdio/netbird/client/errors" "github.com/netbirdio/netbird/client/internal/peer" + "github.com/netbirdio/netbird/client/internal/routemanager/refcounter" + "github.com/netbirdio/netbird/client/internal/routemanager/util" + "github.com/netbirdio/netbird/client/internal/routemanager/vars" "github.com/netbirdio/netbird/iface" nbnet "github.com/netbirdio/netbird/util/net" ) @@ -25,29 +29,75 @@ var splitDefaultv4_2 = netip.PrefixFrom(netip.AddrFrom4([4]byte{128}), 1) var splitDefaultv6_1 = netip.PrefixFrom(netip.IPv6Unspecified(), 1) var splitDefaultv6_2 = netip.PrefixFrom(netip.AddrFrom16([16]byte{0x80}), 1) -var ErrRouteNotFound = errors.New("route not found") -var ErrRouteNotAllowed = errors.New("route not allowed") +func (r *SysOps) setupRefCounter(initAddresses []net.IP) (peer.BeforeAddPeerHookFunc, peer.AfterRemovePeerHookFunc, error) { + initialNextHopV4, err := GetNextHop(netip.IPv4Unspecified()) + if err != nil && !errors.Is(err, vars.ErrRouteNotFound) { + log.Errorf("Unable to get initial v4 default next hop: %v", err) + } + initialNextHopV6, err := GetNextHop(netip.IPv6Unspecified()) + if err != nil && !errors.Is(err, vars.ErrRouteNotFound) { + log.Errorf("Unable to get initial v6 default next hop: %v", err) + } + + refCounter := refcounter.New( + func(prefix netip.Prefix, _ any) (Nexthop, error) { + initialNexthop := initialNextHopV4 + if prefix.Addr().Is6() { + initialNexthop = initialNextHopV6 + } + + nexthop, err := r.addRouteToNonVPNIntf(prefix, r.wgInterface, initialNexthop) + if errors.Is(err, vars.ErrRouteNotAllowed) || errors.Is(err, vars.ErrRouteNotFound) { + log.Tracef("Adding for prefix %s: %v", prefix, err) + // These errors are not critical but also we should not track and try to remove the routes either. + return nexthop, refcounter.ErrIgnore + } + return nexthop, err + }, + r.removeFromRouteTable, + ) + + r.refCounter = refCounter + + return r.setupHooks(initAddresses) +} + +func (r *SysOps) cleanupRefCounter() error { + if r.refCounter == nil { + return nil + } + + // TODO: Remove hooks selectively + nbnet.RemoveDialerHooks() + nbnet.RemoveListenerHooks() + + if err := r.refCounter.Flush(); err != nil { + return fmt.Errorf("flush route manager: %w", err) + } + + return nil +} // TODO: fix: for default our wg address now appears as the default gw -func addRouteForCurrentDefaultGateway(prefix netip.Prefix) error { +func (r *SysOps) addRouteForCurrentDefaultGateway(prefix netip.Prefix) error { addr := netip.IPv4Unspecified() if prefix.Addr().Is6() { addr = netip.IPv6Unspecified() } - defaultGateway, _, err := GetNextHop(addr) - if err != nil && !errors.Is(err, ErrRouteNotFound) { + nexthop, err := GetNextHop(addr) + if err != nil && !errors.Is(err, vars.ErrRouteNotFound) { return fmt.Errorf("get existing route gateway: %s", err) } - if !prefix.Contains(defaultGateway) { - log.Debugf("Skipping adding a new route for gateway %s because it is not in the network %s", defaultGateway, prefix) + if !prefix.Contains(nexthop.IP) { + log.Debugf("Skipping adding a new route for gateway %s because it is not in the network %s", nexthop.IP, prefix) return nil } - gatewayPrefix := netip.PrefixFrom(defaultGateway, 32) - if defaultGateway.Is6() { - gatewayPrefix = netip.PrefixFrom(defaultGateway, 128) + gatewayPrefix := netip.PrefixFrom(nexthop.IP, 32) + if nexthop.IP.Is6() { + gatewayPrefix = netip.PrefixFrom(nexthop.IP, 128) } ok, err := existsInRouteTable(gatewayPrefix) @@ -60,46 +110,264 @@ func addRouteForCurrentDefaultGateway(prefix netip.Prefix) error { return nil } - gatewayHop, intf, err := GetNextHop(defaultGateway) - if err != nil && !errors.Is(err, ErrRouteNotFound) { + nexthop, err = GetNextHop(nexthop.IP) + if err != nil && !errors.Is(err, vars.ErrRouteNotFound) { return fmt.Errorf("unable to get the next hop for the default gateway address. error: %s", err) } - log.Debugf("Adding a new route for gateway %s with next hop %s", gatewayPrefix, gatewayHop) - return addToRouteTable(gatewayPrefix, gatewayHop, intf) + log.Debugf("Adding a new route for gateway %s with next hop %s", gatewayPrefix, nexthop.IP) + return r.addToRouteTable(gatewayPrefix, nexthop) } -func GetNextHop(ip netip.Addr) (netip.Addr, *net.Interface, 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.WGIface, initialNextHop Nexthop) (Nexthop, error) { + addr := prefix.Addr() + switch { + case addr.IsLoopback(), + addr.IsLinkLocalUnicast(), + addr.IsLinkLocalMulticast(), + addr.IsInterfaceLocalMulticast(), + addr.IsUnspecified(), + addr.IsMulticast(): + + return Nexthop{}, vars.ErrRouteNotAllowed + } + + // Determine the exit interface and next hop for the prefix, so we can add a specific route + nexthop, err := GetNextHop(addr) + if err != nil { + return Nexthop{}, fmt.Errorf("get next hop: %w", err) + } + + log.Debugf("Found next hop %s for prefix %s with interface %v", nexthop.IP, prefix, nexthop.IP) + exitNextHop := Nexthop{ + IP: nexthop.IP, + Intf: nexthop.Intf, + } + + vpnAddr, ok := netip.AddrFromSlice(vpnIntf.Address().IP) + if !ok { + return Nexthop{}, fmt.Errorf("failed to convert vpn address to netip.Addr") + } + + // if next hop is the VPN address or the interface is the VPN interface, we should use the initial values + if exitNextHop.IP == vpnAddr || exitNextHop.Intf != nil && exitNextHop.Intf.Name == vpnIntf.Name() { + log.Debugf("Route for prefix %s is pointing to the VPN interface", prefix) + exitNextHop = initialNextHop + } + + log.Debugf("Adding a new route for prefix %s with next hop %s", prefix, exitNextHop.IP) + if err := r.addToRouteTable(prefix, exitNextHop); err != nil { + return Nexthop{}, fmt.Errorf("add route to table: %w", err) + } + + return exitNextHop, nil +} + +// genericAddVPNRoute adds a new route to the vpn interface, it splits the default prefix +// in two /1 prefixes to avoid replacing the existing default route +func (r *SysOps) genericAddVPNRoute(prefix netip.Prefix, intf *net.Interface) error { + nextHop := Nexthop{netip.Addr{}, intf} + + if prefix == vars.Defaultv4 { + if err := r.addToRouteTable(splitDefaultv4_1, nextHop); err != nil { + return err + } + if err := r.addToRouteTable(splitDefaultv4_2, nextHop); err != nil { + if err2 := r.removeFromRouteTable(splitDefaultv4_1, nextHop); err2 != nil { + log.Warnf("Failed to rollback route addition: %s", err2) + } + return err + } + + // TODO: remove once IPv6 is supported on the interface + if err := r.addToRouteTable(splitDefaultv6_1, nextHop); err != nil { + return fmt.Errorf("add unreachable route split 1: %w", err) + } + if err := r.addToRouteTable(splitDefaultv6_2, nextHop); err != nil { + if err2 := r.removeFromRouteTable(splitDefaultv6_1, nextHop); err2 != nil { + log.Warnf("Failed to rollback route addition: %s", err2) + } + return fmt.Errorf("add unreachable route split 2: %w", err) + } + + return nil + } else if prefix == vars.Defaultv6 { + if err := r.addToRouteTable(splitDefaultv6_1, nextHop); err != nil { + return fmt.Errorf("add unreachable route split 1: %w", err) + } + if err := r.addToRouteTable(splitDefaultv6_2, nextHop); err != nil { + if err2 := r.removeFromRouteTable(splitDefaultv6_1, nextHop); err2 != nil { + log.Warnf("Failed to rollback route addition: %s", err2) + } + return fmt.Errorf("add unreachable route split 2: %w", err) + } + + return nil + } + + return r.addNonExistingRoute(prefix, intf) +} + +// addNonExistingRoute adds a new route to the vpn interface if it doesn't exist in the current routing table +func (r *SysOps) addNonExistingRoute(prefix netip.Prefix, intf *net.Interface) error { + ok, err := existsInRouteTable(prefix) + if err != nil { + return fmt.Errorf("exists in route table: %w", err) + } + if ok { + log.Warnf("Skipping adding a new route for network %s because it already exists", prefix) + return nil + } + + ok, err = isSubRange(prefix) + if err != nil { + return fmt.Errorf("sub range: %w", err) + } + + if ok { + if err := r.addRouteForCurrentDefaultGateway(prefix); err != nil { + log.Warnf("Unable to add route for current default gateway route. Will proceed without it. error: %s", err) + } + } + + return r.addToRouteTable(prefix, Nexthop{netip.Addr{}, intf}) +} + +// genericRemoveVPNRoute removes the route from the vpn interface. If a default prefix is given, +// it will remove the split /1 prefixes +func (r *SysOps) genericRemoveVPNRoute(prefix netip.Prefix, intf *net.Interface) error { + nextHop := Nexthop{netip.Addr{}, intf} + + if prefix == vars.Defaultv4 { + var result *multierror.Error + if err := r.removeFromRouteTable(splitDefaultv4_1, nextHop); err != nil { + result = multierror.Append(result, err) + } + if err := r.removeFromRouteTable(splitDefaultv4_2, nextHop); err != nil { + result = multierror.Append(result, err) + } + + // TODO: remove once IPv6 is supported on the interface + if err := r.removeFromRouteTable(splitDefaultv6_1, nextHop); err != nil { + result = multierror.Append(result, err) + } + if err := r.removeFromRouteTable(splitDefaultv6_2, nextHop); err != nil { + result = multierror.Append(result, err) + } + + return nberrors.FormatErrorOrNil(result) + } else if prefix == vars.Defaultv6 { + var result *multierror.Error + if err := r.removeFromRouteTable(splitDefaultv6_1, nextHop); err != nil { + result = multierror.Append(result, err) + } + if err := r.removeFromRouteTable(splitDefaultv6_2, nextHop); err != nil { + result = multierror.Append(result, err) + } + + return nberrors.FormatErrorOrNil(result) + } + + return r.removeFromRouteTable(prefix, nextHop) +} + +func (r *SysOps) setupHooks(initAddresses []net.IP) (peer.BeforeAddPeerHookFunc, peer.AfterRemovePeerHookFunc, error) { + beforeHook := func(connID nbnet.ConnectionID, ip net.IP) error { + prefix, err := util.GetPrefixFromIP(ip) + if err != nil { + return fmt.Errorf("convert ip to prefix: %w", err) + } + + if _, err := r.refCounter.IncrementWithID(string(connID), prefix, nil); err != nil { + return fmt.Errorf("adding route reference: %v", err) + } + + return nil + } + afterHook := func(connID nbnet.ConnectionID) error { + if err := r.refCounter.DecrementWithID(string(connID)); err != nil { + return fmt.Errorf("remove route reference: %w", err) + } + + return nil + } + + for _, ip := range initAddresses { + if err := beforeHook("init", ip); err != nil { + log.Errorf("Failed to add route reference: %v", err) + } + } + + nbnet.AddDialerHook(func(ctx context.Context, connID nbnet.ConnectionID, resolvedIPs []net.IPAddr) error { + if ctx.Err() != nil { + return ctx.Err() + } + + var result *multierror.Error + for _, ip := range resolvedIPs { + result = multierror.Append(result, beforeHook(connID, ip.IP)) + } + return nberrors.FormatErrorOrNil(result) + }) + + nbnet.AddDialerCloseHook(func(connID nbnet.ConnectionID, conn *net.Conn) error { + return afterHook(connID) + }) + + nbnet.AddListenerWriteHook(func(connID nbnet.ConnectionID, ip *net.IPAddr, data []byte) error { + return beforeHook(connID, ip.IP) + }) + + nbnet.AddListenerCloseHook(func(connID nbnet.ConnectionID, conn net.PacketConn) error { + return afterHook(connID) + }) + + return beforeHook, afterHook, nil +} + +func GetNextHop(ip netip.Addr) (Nexthop, error) { r, err := netroute.New() if err != nil { - return netip.Addr{}, nil, fmt.Errorf("new netroute: %w", err) + return Nexthop{}, fmt.Errorf("new netroute: %w", err) } intf, gateway, preferredSrc, err := r.Route(ip.AsSlice()) if err != nil { log.Debugf("Failed to get route for %s: %v", ip, err) - return netip.Addr{}, nil, ErrRouteNotFound + return Nexthop{}, vars.ErrRouteNotFound } log.Debugf("Route for %s: interface %v nexthop %v, preferred source %v", ip, intf, gateway, preferredSrc) if gateway == nil { - if preferredSrc == nil { - return netip.Addr{}, nil, ErrRouteNotFound + if runtime.GOOS == "freebsd" { + return Nexthop{Intf: intf}, nil } - log.Debugf("No next hop found for ip %s, using preferred source %s", ip, preferredSrc) + + if preferredSrc == nil { + return Nexthop{}, vars.ErrRouteNotFound + } + log.Debugf("No next hop found for IP %s, using preferred source %s", ip, preferredSrc) addr, err := ipToAddr(preferredSrc, intf) if err != nil { - return netip.Addr{}, nil, fmt.Errorf("convert preferred source to address: %w", err) + return Nexthop{}, fmt.Errorf("convert preferred source to address: %w", err) } - return addr.Unmap(), intf, nil + return Nexthop{ + IP: addr.Unmap(), + Intf: intf, + }, nil } addr, err := ipToAddr(gateway, intf) if err != nil { - return netip.Addr{}, nil, fmt.Errorf("convert gateway to address: %w", err) + return Nexthop{}, fmt.Errorf("convert gateway to address: %w", err) } - return addr, intf, nil + return Nexthop{ + IP: addr, + Intf: intf, + }, nil } // converts a net.IP to a netip.Addr including the zone based on the passed interface @@ -140,275 +408,9 @@ func isSubRange(prefix netip.Prefix) (bool, error) { return false, fmt.Errorf("get routes from table: %w", err) } for _, tableRoute := range routes { - if tableRoute.Bits() > minRangeBits && tableRoute.Contains(prefix.Addr()) && tableRoute.Bits() < prefix.Bits() { + if tableRoute.Bits() > vars.MinRangeBits && tableRoute.Contains(prefix.Addr()) && tableRoute.Bits() < prefix.Bits() { return true, nil } } return false, nil } - -// 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 addRouteToNonVPNIntf(prefix netip.Prefix, vpnIntf *iface.WGIface, initialNextHop netip.Addr, initialIntf *net.Interface) (netip.Addr, *net.Interface, error) { - addr := prefix.Addr() - switch { - case addr.IsLoopback(), - addr.IsLinkLocalUnicast(), - addr.IsLinkLocalMulticast(), - addr.IsInterfaceLocalMulticast(), - addr.IsUnspecified(), - addr.IsMulticast(): - - return netip.Addr{}, nil, ErrRouteNotAllowed - } - - // Determine the exit interface and next hop for the prefix, so we can add a specific route - nexthop, intf, err := GetNextHop(addr) - if err != nil { - return netip.Addr{}, nil, fmt.Errorf("get next hop: %w", err) - } - - log.Debugf("Found next hop %s for prefix %s with interface %v", nexthop, prefix, intf) - exitNextHop := nexthop - exitIntf := intf - - vpnAddr, ok := netip.AddrFromSlice(vpnIntf.Address().IP) - if !ok { - return netip.Addr{}, nil, fmt.Errorf("failed to convert vpn address to netip.Addr") - } - - // if next hop is the VPN address or the interface is the VPN interface, we should use the initial values - if exitNextHop == vpnAddr || exitIntf != nil && exitIntf.Name == vpnIntf.Name() { - log.Debugf("Route for prefix %s is pointing to the VPN interface", prefix) - exitNextHop = initialNextHop - exitIntf = initialIntf - } - - log.Debugf("Adding a new route for prefix %s with next hop %s", prefix, exitNextHop) - if err := addToRouteTable(prefix, exitNextHop, exitIntf); err != nil { - return netip.Addr{}, nil, fmt.Errorf("add route to table: %w", err) - } - - return exitNextHop, exitIntf, nil -} - -// genericAddVPNRoute adds a new route to the vpn interface, it splits the default prefix -// in two /1 prefixes to avoid replacing the existing default route -func genericAddVPNRoute(prefix netip.Prefix, intf *net.Interface) error { - if prefix == defaultv4 { - if err := addToRouteTable(splitDefaultv4_1, netip.Addr{}, intf); err != nil { - return err - } - if err := addToRouteTable(splitDefaultv4_2, netip.Addr{}, intf); err != nil { - if err2 := removeFromRouteTable(splitDefaultv4_1, netip.Addr{}, intf); err2 != nil { - log.Warnf("Failed to rollback route addition: %s", err2) - } - return err - } - - // TODO: remove once IPv6 is supported on the interface - if err := addToRouteTable(splitDefaultv6_1, netip.Addr{}, intf); err != nil { - return fmt.Errorf("add unreachable route split 1: %w", err) - } - if err := addToRouteTable(splitDefaultv6_2, netip.Addr{}, intf); err != nil { - if err2 := removeFromRouteTable(splitDefaultv6_1, netip.Addr{}, intf); err2 != nil { - log.Warnf("Failed to rollback route addition: %s", err2) - } - return fmt.Errorf("add unreachable route split 2: %w", err) - } - - return nil - } else if prefix == defaultv6 { - if err := addToRouteTable(splitDefaultv6_1, netip.Addr{}, intf); err != nil { - return fmt.Errorf("add unreachable route split 1: %w", err) - } - if err := addToRouteTable(splitDefaultv6_2, netip.Addr{}, intf); err != nil { - if err2 := removeFromRouteTable(splitDefaultv6_1, netip.Addr{}, intf); err2 != nil { - log.Warnf("Failed to rollback route addition: %s", err2) - } - return fmt.Errorf("add unreachable route split 2: %w", err) - } - - return nil - } - - return addNonExistingRoute(prefix, intf) -} - -// addNonExistingRoute adds a new route to the vpn interface if it doesn't exist in the current routing table -func addNonExistingRoute(prefix netip.Prefix, intf *net.Interface) error { - ok, err := existsInRouteTable(prefix) - if err != nil { - return fmt.Errorf("exists in route table: %w", err) - } - if ok { - log.Warnf("Skipping adding a new route for network %s because it already exists", prefix) - return nil - } - - ok, err = isSubRange(prefix) - if err != nil { - return fmt.Errorf("sub range: %w", err) - } - - if ok { - err := addRouteForCurrentDefaultGateway(prefix) - if err != nil { - log.Warnf("Unable to add route for current default gateway route. Will proceed without it. error: %s", err) - } - } - - return addToRouteTable(prefix, netip.Addr{}, intf) -} - -// genericRemoveVPNRoute removes the route from the vpn interface. If a default prefix is given, -// it will remove the split /1 prefixes -func genericRemoveVPNRoute(prefix netip.Prefix, intf *net.Interface) error { - if prefix == defaultv4 { - var result *multierror.Error - if err := removeFromRouteTable(splitDefaultv4_1, netip.Addr{}, intf); err != nil { - result = multierror.Append(result, err) - } - if err := removeFromRouteTable(splitDefaultv4_2, netip.Addr{}, intf); err != nil { - result = multierror.Append(result, err) - } - - // TODO: remove once IPv6 is supported on the interface - if err := removeFromRouteTable(splitDefaultv6_1, netip.Addr{}, intf); err != nil { - result = multierror.Append(result, err) - } - if err := removeFromRouteTable(splitDefaultv6_2, netip.Addr{}, intf); err != nil { - result = multierror.Append(result, err) - } - - return result.ErrorOrNil() - } else if prefix == defaultv6 { - var result *multierror.Error - if err := removeFromRouteTable(splitDefaultv6_1, netip.Addr{}, intf); err != nil { - result = multierror.Append(result, err) - } - if err := removeFromRouteTable(splitDefaultv6_2, netip.Addr{}, intf); err != nil { - result = multierror.Append(result, err) - } - - return result.ErrorOrNil() - } - - return removeFromRouteTable(prefix, netip.Addr{}, intf) -} - -func getPrefixFromIP(ip net.IP) (*netip.Prefix, error) { - addr, ok := netip.AddrFromSlice(ip) - if !ok { - return nil, fmt.Errorf("parse IP address: %s", ip) - } - addr = addr.Unmap() - - var prefixLength int - switch { - case addr.Is4(): - prefixLength = 32 - case addr.Is6(): - prefixLength = 128 - default: - return nil, fmt.Errorf("invalid IP address: %s", addr) - } - - prefix := netip.PrefixFrom(addr, prefixLength) - return &prefix, nil -} - -func setupRoutingWithRouteManager(routeManager **RouteManager, initAddresses []net.IP, wgIface *iface.WGIface) (peer.BeforeAddPeerHookFunc, peer.AfterRemovePeerHookFunc, error) { - initialNextHopV4, initialIntfV4, err := GetNextHop(netip.IPv4Unspecified()) - if err != nil && !errors.Is(err, ErrRouteNotFound) { - log.Errorf("Unable to get initial v4 default next hop: %v", err) - } - initialNextHopV6, initialIntfV6, err := GetNextHop(netip.IPv6Unspecified()) - if err != nil && !errors.Is(err, ErrRouteNotFound) { - log.Errorf("Unable to get initial v6 default next hop: %v", err) - } - - *routeManager = NewRouteManager( - func(prefix netip.Prefix) (netip.Addr, *net.Interface, error) { - addr := prefix.Addr() - nexthop, intf := initialNextHopV4, initialIntfV4 - if addr.Is6() { - nexthop, intf = initialNextHopV6, initialIntfV6 - } - return addRouteToNonVPNIntf(prefix, wgIface, nexthop, intf) - }, - removeFromRouteTable, - ) - - return setupHooks(*routeManager, initAddresses) -} - -func cleanupRoutingWithRouteManager(routeManager *RouteManager) error { - if routeManager == nil { - return nil - } - - // TODO: Remove hooks selectively - nbnet.RemoveDialerHooks() - nbnet.RemoveListenerHooks() - - if err := routeManager.Flush(); err != nil { - return fmt.Errorf("flush route manager: %w", err) - } - - return nil -} - -func setupHooks(routeManager *RouteManager, initAddresses []net.IP) (peer.BeforeAddPeerHookFunc, peer.AfterRemovePeerHookFunc, error) { - beforeHook := func(connID nbnet.ConnectionID, ip net.IP) error { - prefix, err := getPrefixFromIP(ip) - if err != nil { - return fmt.Errorf("convert ip to prefix: %w", err) - } - - if err := routeManager.AddRouteRef(connID, *prefix); err != nil { - return fmt.Errorf("adding route reference: %v", err) - } - - return nil - } - afterHook := func(connID nbnet.ConnectionID) error { - if err := routeManager.RemoveRouteRef(connID); err != nil { - return fmt.Errorf("remove route reference: %w", err) - } - - return nil - } - - for _, ip := range initAddresses { - if err := beforeHook("init", ip); err != nil { - log.Errorf("Failed to add route reference: %v", err) - } - } - - nbnet.AddDialerHook(func(ctx context.Context, connID nbnet.ConnectionID, resolvedIPs []net.IPAddr) error { - if ctx.Err() != nil { - return ctx.Err() - } - - var result *multierror.Error - for _, ip := range resolvedIPs { - result = multierror.Append(result, beforeHook(connID, ip.IP)) - } - return result.ErrorOrNil() - }) - - nbnet.AddDialerCloseHook(func(connID nbnet.ConnectionID, conn *net.Conn) error { - return afterHook(connID) - }) - - nbnet.AddListenerWriteHook(func(connID nbnet.ConnectionID, ip *net.IPAddr, data []byte) error { - return beforeHook(connID, ip.IP) - }) - - nbnet.AddListenerCloseHook(func(connID nbnet.ConnectionID, conn net.PacketConn) error { - return afterHook(connID) - }) - - return beforeHook, afterHook, nil -} diff --git a/client/internal/routemanager/systemops_test.go b/client/internal/routemanager/systemops/systemops_generic_test.go similarity index 76% rename from client/internal/routemanager/systemops_test.go rename to client/internal/routemanager/systemops/systemops_generic_test.go index 8bcf06dce..f7cb17477 100644 --- a/client/internal/routemanager/systemops_test.go +++ b/client/internal/routemanager/systemops/systemops_generic_test.go @@ -1,6 +1,6 @@ //go:build !android && !ios -package routemanager +package systemops import ( "bytes" @@ -63,17 +63,20 @@ func TestAddRemoveRoutes(t *testing.T) { err = wgInterface.Create() require.NoError(t, err, "should create testing wireguard interface") - _, _, err = setupRouting(nil, wgInterface) + + r := NewSysOps(wgInterface) + + _, _, err = r.SetupRouting(nil) require.NoError(t, err) t.Cleanup(func() { - assert.NoError(t, cleanupRouting()) + assert.NoError(t, r.CleanupRouting()) }) index, err := net.InterfaceByName(wgInterface.Name()) require.NoError(t, err, "InterfaceByName should not return err") intf := &net.Interface{Index: index.Index, Name: wgInterface.Name()} - err = addVPNRoute(testCase.prefix, intf) + err = r.AddVPNRoute(testCase.prefix, intf) require.NoError(t, err, "genericAddVPNRoute should not return err") if testCase.shouldRouteToWireguard { @@ -84,19 +87,19 @@ func TestAddRemoveRoutes(t *testing.T) { exists, err := existsInRouteTable(testCase.prefix) require.NoError(t, err, "existsInRouteTable should not return err") if exists && testCase.shouldRouteToWireguard { - err = removeVPNRoute(testCase.prefix, intf) + err = r.RemoveVPNRoute(testCase.prefix, intf) require.NoError(t, err, "genericRemoveVPNRoute should not return err") - prefixGateway, _, err := GetNextHop(testCase.prefix.Addr()) + prefixNexthop, err := GetNextHop(testCase.prefix.Addr()) require.NoError(t, err, "GetNextHop should not return err") - internetGateway, _, err := GetNextHop(netip.MustParseAddr("0.0.0.0")) + internetNexthop, err := GetNextHop(netip.MustParseAddr("0.0.0.0")) require.NoError(t, err) if testCase.shouldBeRemoved { - require.Equal(t, internetGateway, prefixGateway, "route should be pointing to default internet gateway") + require.Equal(t, internetNexthop.IP, prefixNexthop.IP, "route should be pointing to default internet gateway") } else { - require.NotEqual(t, internetGateway, prefixGateway, "route should be pointing to a different gateway than the internet gateway") + require.NotEqual(t, internetNexthop.IP, prefixNexthop.IP, "route should be pointing to a different gateway than the internet gateway") } } }) @@ -104,11 +107,11 @@ func TestAddRemoveRoutes(t *testing.T) { } func TestGetNextHop(t *testing.T) { - gateway, _, err := GetNextHop(netip.MustParseAddr("0.0.0.0")) + nexthop, err := GetNextHop(netip.MustParseAddr("0.0.0.0")) if err != nil { t.Fatal("shouldn't return error when fetching the gateway: ", err) } - if !gateway.IsValid() { + if !nexthop.IP.IsValid() { t.Fatal("should return a gateway") } addresses, err := net.InterfaceAddrs() @@ -130,24 +133,24 @@ func TestGetNextHop(t *testing.T) { } } - localIP, _, err := GetNextHop(testingPrefix.Addr()) + localIP, err := GetNextHop(testingPrefix.Addr()) if err != nil { t.Fatal("shouldn't return error: ", err) } - if !localIP.IsValid() { + if !localIP.IP.IsValid() { t.Fatal("should return a gateway for local network") } - if localIP.String() == gateway.String() { - t.Fatal("local ip should not match with gateway IP") + if localIP.IP.String() == nexthop.IP.String() { + t.Fatal("local IP should not match with gateway IP") } - if localIP.String() != testingIP { - t.Fatalf("local ip should match with testing IP: want %s got %s", testingIP, localIP.String()) + if localIP.IP.String() != testingIP { + t.Fatalf("local IP should match with testing IP: want %s got %s", testingIP, localIP.IP.String()) } } func TestAddExistAndRemoveRoute(t *testing.T) { - defaultGateway, _, err := GetNextHop(netip.MustParseAddr("0.0.0.0")) - t.Log("defaultGateway: ", defaultGateway) + defaultNexthop, err := GetNextHop(netip.MustParseAddr("0.0.0.0")) + t.Log("defaultNexthop: ", defaultNexthop) if err != nil { t.Fatal("shouldn't return error when fetching the gateway: ", err) } @@ -164,7 +167,7 @@ func TestAddExistAndRemoveRoute(t *testing.T) { }, { name: "Should Not Add Route if overlaps with default gateway", - prefix: netip.MustParsePrefix(defaultGateway.String() + "/31"), + prefix: netip.MustParsePrefix(defaultNexthop.IP.String() + "/31"), shouldAddRoute: false, }, { @@ -214,14 +217,16 @@ func TestAddExistAndRemoveRoute(t *testing.T) { require.NoError(t, err, "InterfaceByName should not return err") intf := &net.Interface{Index: index.Index, Name: wgInterface.Name()} + r := NewSysOps(wgInterface) + // Prepare the environment if testCase.preExistingPrefix.IsValid() { - err := addVPNRoute(testCase.preExistingPrefix, intf) + err := r.AddVPNRoute(testCase.preExistingPrefix, intf) require.NoError(t, err, "should not return err when adding pre-existing route") } // Add the route - err = addVPNRoute(testCase.prefix, intf) + err = r.AddVPNRoute(testCase.prefix, intf) require.NoError(t, err, "should not return err when adding route") if testCase.shouldAddRoute { @@ -231,7 +236,7 @@ func TestAddExistAndRemoveRoute(t *testing.T) { require.True(t, ok, "route should exist") // remove route again if added - err = removeVPNRoute(testCase.prefix, intf) + err = r.RemoveVPNRoute(testCase.prefix, intf) require.NoError(t, err, "should not return err") } @@ -343,65 +348,52 @@ func createWGInterface(t *testing.T, interfaceName, ipAddressCIDR string, listen return wgInterface } +func setupRouteAndCleanup(t *testing.T, r *SysOps, prefix netip.Prefix, intf *net.Interface) { + t.Helper() + + err := r.AddVPNRoute(prefix, intf) + require.NoError(t, err, "addVPNRoute should not return err") + t.Cleanup(func() { + err = r.RemoveVPNRoute(prefix, intf) + assert.NoError(t, err, "removeVPNRoute should not return err") + }) +} + func setupTestEnv(t *testing.T) { t.Helper() setupDummyInterfacesAndRoutes(t) - wgIface := createWGInterface(t, expectedVPNint, "100.64.0.1/24", 51820) + wgInterface := createWGInterface(t, expectedVPNint, "100.64.0.1/24", 51820) t.Cleanup(func() { - assert.NoError(t, wgIface.Close()) + assert.NoError(t, wgInterface.Close()) }) - _, _, err := setupRouting(nil, wgIface) + r := NewSysOps(wgInterface) + _, _, err := r.SetupRouting(nil) require.NoError(t, err, "setupRouting should not return err") t.Cleanup(func() { - assert.NoError(t, cleanupRouting()) + assert.NoError(t, r.CleanupRouting()) }) - index, err := net.InterfaceByName(wgIface.Name()) + index, err := net.InterfaceByName(wgInterface.Name()) require.NoError(t, err, "InterfaceByName should not return err") - intf := &net.Interface{Index: index.Index, Name: wgIface.Name()} + intf := &net.Interface{Index: index.Index, Name: wgInterface.Name()} // default route exists in main table and vpn table - err = addVPNRoute(netip.MustParsePrefix("0.0.0.0/0"), intf) - require.NoError(t, err, "addVPNRoute should not return err") - t.Cleanup(func() { - err = removeVPNRoute(netip.MustParsePrefix("0.0.0.0/0"), intf) - assert.NoError(t, err, "removeVPNRoute should not return err") - }) + setupRouteAndCleanup(t, r, netip.MustParsePrefix("0.0.0.0/0"), intf) // 10.0.0.0/8 route exists in main table and vpn table - err = addVPNRoute(netip.MustParsePrefix("10.0.0.0/8"), intf) - require.NoError(t, err, "addVPNRoute should not return err") - t.Cleanup(func() { - err = removeVPNRoute(netip.MustParsePrefix("10.0.0.0/8"), intf) - assert.NoError(t, err, "removeVPNRoute should not return err") - }) + setupRouteAndCleanup(t, r, netip.MustParsePrefix("10.0.0.0/8"), intf) // 10.10.0.0/24 more specific route exists in vpn table - err = addVPNRoute(netip.MustParsePrefix("10.10.0.0/24"), intf) - require.NoError(t, err, "addVPNRoute should not return err") - t.Cleanup(func() { - err = removeVPNRoute(netip.MustParsePrefix("10.10.0.0/24"), intf) - assert.NoError(t, err, "removeVPNRoute should not return err") - }) + setupRouteAndCleanup(t, r, netip.MustParsePrefix("10.10.0.0/24"), intf) // 127.0.10.0/24 more specific route exists in vpn table - err = addVPNRoute(netip.MustParsePrefix("127.0.10.0/24"), intf) - require.NoError(t, err, "addVPNRoute should not return err") - t.Cleanup(func() { - err = removeVPNRoute(netip.MustParsePrefix("127.0.10.0/24"), intf) - assert.NoError(t, err, "removeVPNRoute should not return err") - }) + setupRouteAndCleanup(t, r, netip.MustParsePrefix("127.0.10.0/24"), intf) // unique route in vpn table - err = addVPNRoute(netip.MustParsePrefix("172.16.0.0/12"), intf) - require.NoError(t, err, "addVPNRoute should not return err") - t.Cleanup(func() { - err = removeVPNRoute(netip.MustParsePrefix("172.16.0.0/12"), intf) - assert.NoError(t, err, "removeVPNRoute should not return err") - }) + setupRouteAndCleanup(t, r, netip.MustParsePrefix("172.16.0.0/12"), intf) } func assertWGOutInterface(t *testing.T, prefix netip.Prefix, wgIface *iface.WGIface, invert bool) { @@ -410,11 +402,11 @@ func assertWGOutInterface(t *testing.T, prefix netip.Prefix, wgIface *iface.WGIf return } - prefixGateway, _, err := GetNextHop(prefix.Addr()) + prefixNexthop, err := GetNextHop(prefix.Addr()) require.NoError(t, err, "GetNextHop should not return err") if invert { - assert.NotEqual(t, wgIface.Address().IP.String(), prefixGateway.String(), "route should not point to wireguard interface IP") + assert.NotEqual(t, wgIface.Address().IP.String(), prefixNexthop.IP.String(), "route should not point to wireguard interface IP") } else { - assert.Equal(t, wgIface.Address().IP.String(), prefixGateway.String(), "route should point to wireguard interface IP") + assert.Equal(t, wgIface.Address().IP.String(), prefixNexthop.IP.String(), "route should point to wireguard interface IP") } } diff --git a/client/internal/routemanager/systemops_linux.go b/client/internal/routemanager/systemops/systemops_linux.go similarity index 72% rename from client/internal/routemanager/systemops_linux.go rename to client/internal/routemanager/systemops/systemops_linux.go index ce0c07ce6..bca47d1f9 100644 --- a/client/internal/routemanager/systemops_linux.go +++ b/client/internal/routemanager/systemops/systemops_linux.go @@ -1,6 +1,6 @@ //go:build !android -package routemanager +package systemops import ( "bufio" @@ -9,16 +9,16 @@ import ( "net" "net/netip" "os" - "strconv" - "strings" "syscall" "github.com/hashicorp/go-multierror" log "github.com/sirupsen/logrus" "github.com/vishvananda/netlink" + nberrors "github.com/netbirdio/netbird/client/errors" "github.com/netbirdio/netbird/client/internal/peer" - "github.com/netbirdio/netbird/iface" + "github.com/netbirdio/netbird/client/internal/routemanager/sysctl" + "github.com/netbirdio/netbird/client/internal/routemanager/vars" nbnet "github.com/netbirdio/netbird/util/net" ) @@ -33,16 +33,10 @@ const ( // ipv4ForwardingPath is the path to the file containing the IP forwarding setting. ipv4ForwardingPath = "net.ipv4.ip_forward" - - rpFilterPath = "net.ipv4.conf.all.rp_filter" - rpFilterInterfacePath = "net.ipv4.conf.%s.rp_filter" - srcValidMarkPath = "net.ipv4.conf.all.src_valid_mark" ) var ErrTableIDExists = errors.New("ID exists with different name") -var routeManager = &RouteManager{} - // originalSysctl stores the original sysctl values before they are modified var originalSysctl map[string]int @@ -82,7 +76,7 @@ func getSetupRules() []ruleParams { } } -// setupRouting establishes the routing configuration for the VPN, including essential rules +// SetupRouting establishes the routing configuration for the VPN, including essential rules // to ensure proper traffic flow for management, locally configured routes, and VPN traffic. // // Rule 1 (Main Route Precedence): Safeguards locally installed routes by giving them precedence over @@ -92,17 +86,17 @@ func getSetupRules() []ruleParams { // Rule 2 (VPN Traffic Routing): Directs all remaining traffic to the 'NetbirdVPNTableID' custom routing table. // This table is where a default route or other specific routes received from the management server are configured, // enabling VPN connectivity. -func setupRouting(initAddresses []net.IP, wgIface *iface.WGIface) (_ peer.BeforeAddPeerHookFunc, _ peer.AfterRemovePeerHookFunc, err error) { +func (r *SysOps) SetupRouting(initAddresses []net.IP) (_ peer.BeforeAddPeerHookFunc, _ peer.AfterRemovePeerHookFunc, err error) { if isLegacy() { log.Infof("Using legacy routing setup") - return setupRoutingWithRouteManager(&routeManager, initAddresses, wgIface) + return r.setupRefCounter(initAddresses) } if err = addRoutingTableName(); err != nil { log.Errorf("Error adding routing table name: %v", err) } - originalValues, err := setupSysctl(wgIface) + originalValues, err := sysctl.Setup(r.wgInterface) if err != nil { log.Errorf("Error setting up sysctl: %v", err) sysctlFailed = true @@ -111,7 +105,7 @@ func setupRouting(initAddresses []net.IP, wgIface *iface.WGIface) (_ peer.Before defer func() { if err != nil { - if cleanErr := cleanupRouting(); cleanErr != nil { + if cleanErr := r.CleanupRouting(); cleanErr != nil { log.Errorf("Error cleaning up routing: %v", cleanErr) } } @@ -123,7 +117,7 @@ func setupRouting(initAddresses []net.IP, wgIface *iface.WGIface) (_ peer.Before if errors.Is(err, syscall.EOPNOTSUPP) { log.Warnf("Rule operations are not supported, falling back to the legacy routing setup") setIsLegacy(true) - return setupRoutingWithRouteManager(&routeManager, initAddresses, wgIface) + return r.setupRefCounter(initAddresses) } return nil, nil, fmt.Errorf("%s: %w", rule.description, err) } @@ -132,12 +126,12 @@ func setupRouting(initAddresses []net.IP, wgIface *iface.WGIface) (_ peer.Before return nil, nil, nil } -// cleanupRouting performs a thorough cleanup of the routing configuration established by 'setupRouting'. +// CleanupRouting performs a thorough cleanup of the routing configuration established by 'setupRouting'. // 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 cleanupRouting() error { +func (r *SysOps) CleanupRouting() error { if isLegacy() { - return cleanupRoutingWithRouteManager(routeManager) + return r.cleanupRefCounter() } var result *multierror.Error @@ -156,58 +150,58 @@ func cleanupRouting() error { } } - if err := cleanupSysctl(originalSysctl); err != nil { + if err := sysctl.Cleanup(originalSysctl); err != nil { result = multierror.Append(result, fmt.Errorf("cleanup sysctl: %w", err)) } originalSysctl = nil sysctlFailed = false - return result.ErrorOrNil() + return nberrors.FormatErrorOrNil(result) } -func addToRouteTable(prefix netip.Prefix, nexthop netip.Addr, intf *net.Interface) error { - return addRoute(prefix, nexthop, intf, syscall.RT_TABLE_MAIN) +func (r *SysOps) addToRouteTable(prefix netip.Prefix, nexthop Nexthop) error { + return addRoute(prefix, nexthop, syscall.RT_TABLE_MAIN) } -func removeFromRouteTable(prefix netip.Prefix, nexthop netip.Addr, intf *net.Interface) error { - return removeRoute(prefix, nexthop, intf, syscall.RT_TABLE_MAIN) +func (r *SysOps) removeFromRouteTable(prefix netip.Prefix, nexthop Nexthop) error { + return removeRoute(prefix, nexthop, syscall.RT_TABLE_MAIN) } -func addVPNRoute(prefix netip.Prefix, intf *net.Interface) error { +func (r *SysOps) AddVPNRoute(prefix netip.Prefix, intf *net.Interface) error { if isLegacy() { - return genericAddVPNRoute(prefix, intf) + return r.genericAddVPNRoute(prefix, intf) } - if sysctlFailed && (prefix == defaultv4 || prefix == defaultv6) { + if sysctlFailed && (prefix == vars.Defaultv4 || prefix == vars.Defaultv6) { log.Warnf("Default route is configured but sysctl operations failed, VPN traffic may not be routed correctly, consider using NB_USE_LEGACY_ROUTING=true or setting net.ipv4.conf.*.rp_filter to 2 (loose) or 0 (off)") } // No need to check if routes exist as main table takes precedence over the VPN table via Rule 1 // TODO remove this once we have ipv6 support - if prefix == defaultv4 { - if err := addUnreachableRoute(defaultv6, NetbirdVPNTableID); err != nil { + if prefix == vars.Defaultv4 { + if err := addUnreachableRoute(vars.Defaultv6, NetbirdVPNTableID); err != nil { return fmt.Errorf("add blackhole: %w", err) } } - if err := addRoute(prefix, netip.Addr{}, intf, NetbirdVPNTableID); err != nil { + if err := addRoute(prefix, Nexthop{netip.Addr{}, intf}, NetbirdVPNTableID); err != nil { return fmt.Errorf("add route: %w", err) } return nil } -func removeVPNRoute(prefix netip.Prefix, intf *net.Interface) error { +func (r *SysOps) RemoveVPNRoute(prefix netip.Prefix, intf *net.Interface) error { if isLegacy() { - return genericRemoveVPNRoute(prefix, intf) + return r.genericRemoveVPNRoute(prefix, intf) } // TODO remove this once we have ipv6 support - if prefix == defaultv4 { - if err := removeUnreachableRoute(defaultv6, NetbirdVPNTableID); err != nil { + if prefix == vars.Defaultv4 { + if err := removeUnreachableRoute(vars.Defaultv6, NetbirdVPNTableID); err != nil { return fmt.Errorf("remove unreachable route: %w", err) } } - if err := removeRoute(prefix, netip.Addr{}, intf, NetbirdVPNTableID); err != nil { + if err := removeRoute(prefix, Nexthop{netip.Addr{}, intf}, NetbirdVPNTableID); err != nil { return fmt.Errorf("remove route: %w", err) } return nil @@ -255,7 +249,7 @@ func getRoutes(tableID, family int) ([]netip.Prefix, error) { } // addRoute adds a route to a specific routing table identified by tableID. -func addRoute(prefix netip.Prefix, addr netip.Addr, intf *net.Interface, tableID int) error { +func addRoute(prefix netip.Prefix, nexthop Nexthop, tableID int) error { route := &netlink.Route{ Scope: netlink.SCOPE_UNIVERSE, Table: tableID, @@ -268,7 +262,7 @@ func addRoute(prefix netip.Prefix, addr netip.Addr, intf *net.Interface, tableID } route.Dst = ipNet - if err := addNextHop(addr, intf, route); err != nil { + if err := addNextHop(nexthop, route); err != nil { return fmt.Errorf("add gateway and device: %w", err) } @@ -327,7 +321,7 @@ func removeUnreachableRoute(prefix netip.Prefix, tableID int) error { } // removeRoute removes a route from a specific routing table identified by tableID. -func removeRoute(prefix netip.Prefix, addr netip.Addr, intf *net.Interface, tableID int) error { +func removeRoute(prefix netip.Prefix, nexthop Nexthop, tableID int) error { _, ipNet, err := net.ParseCIDR(prefix.String()) if err != nil { return fmt.Errorf("parse prefix %s: %w", prefix, err) @@ -340,7 +334,7 @@ func removeRoute(prefix netip.Prefix, addr netip.Addr, intf *net.Interface, tabl Dst: ipNet, } - if err := addNextHop(addr, intf, route); err != nil { + if err := addNextHop(nexthop, route); err != nil { return fmt.Errorf("add gateway and device: %w", err) } @@ -373,11 +367,11 @@ func flushRoutes(tableID, family int) error { } } - return result.ErrorOrNil() + return nberrors.FormatErrorOrNil(result) } -func enableIPForwarding() error { - _, err := setSysctl(ipv4ForwardingPath, 1, false) +func EnableIPForwarding() error { + _, err := sysctl.Set(ipv4ForwardingPath, 1, false) return err } @@ -481,19 +475,19 @@ func removeRule(params ruleParams) error { } // addNextHop adds the gateway and device to the route. -func addNextHop(addr netip.Addr, intf *net.Interface, route *netlink.Route) error { - if intf != nil { - route.LinkIndex = intf.Index +func addNextHop(nexthop Nexthop, route *netlink.Route) error { + if nexthop.Intf != nil { + route.LinkIndex = nexthop.Intf.Index } - if addr.IsValid() { - route.Gw = addr.AsSlice() + if nexthop.IP.IsValid() { + route.Gw = nexthop.IP.AsSlice() // if zone is set, it means the gateway is a link-local address, so we set the link index - if addr.Zone() != "" && intf == nil { - link, err := netlink.LinkByName(addr.Zone()) + if nexthop.IP.Zone() != "" && nexthop.Intf == nil { + link, err := netlink.LinkByName(nexthop.IP.Zone()) if err != nil { - return fmt.Errorf("get link by name for zone %s: %w", addr.Zone(), err) + return fmt.Errorf("get link by name for zone %s: %w", nexthop.IP.Zone(), err) } route.LinkIndex = link.Attrs().Index } @@ -508,83 +502,3 @@ func getAddressFamily(prefix netip.Prefix) int { } return netlink.FAMILY_V6 } - -// setupSysctl configures sysctl settings for RP filtering and source validation. -func setupSysctl(wgIface *iface.WGIface) (map[string]int, error) { - keys := map[string]int{} - var result *multierror.Error - - oldVal, err := setSysctl(srcValidMarkPath, 1, false) - if err != nil { - result = multierror.Append(result, err) - } else { - keys[srcValidMarkPath] = oldVal - } - - oldVal, err = setSysctl(rpFilterPath, 2, true) - if err != nil { - result = multierror.Append(result, err) - } else { - keys[rpFilterPath] = oldVal - } - - interfaces, err := net.Interfaces() - if err != nil { - result = multierror.Append(result, fmt.Errorf("list interfaces: %w", err)) - } - - for _, intf := range interfaces { - if intf.Name == "lo" || wgIface != nil && intf.Name == wgIface.Name() { - continue - } - - i := fmt.Sprintf(rpFilterInterfacePath, intf.Name) - oldVal, err := setSysctl(i, 2, true) - if err != nil { - result = multierror.Append(result, err) - } else { - keys[i] = oldVal - } - } - - return keys, result.ErrorOrNil() -} - -// setSysctl sets a sysctl configuration, if onlyIfOne is true it will only set the new value if it's set to 1 -func setSysctl(key string, desiredValue int, onlyIfOne bool) (int, error) { - path := fmt.Sprintf("/proc/sys/%s", strings.ReplaceAll(key, ".", "/")) - currentValue, err := os.ReadFile(path) - if err != nil { - return -1, fmt.Errorf("read sysctl %s: %w", key, err) - } - - currentV, err := strconv.Atoi(strings.TrimSpace(string(currentValue))) - if err != nil && len(currentValue) > 0 { - return -1, fmt.Errorf("convert current desiredValue to int: %w", err) - } - - if currentV == desiredValue || onlyIfOne && currentV != 1 { - return currentV, nil - } - - //nolint:gosec - if err := os.WriteFile(path, []byte(strconv.Itoa(desiredValue)), 0644); err != nil { - return currentV, fmt.Errorf("write sysctl %s: %w", key, err) - } - log.Debugf("Set sysctl %s from %d to %d", key, currentV, desiredValue) - - return currentV, nil -} - -func cleanupSysctl(originalSettings map[string]int) error { - var result *multierror.Error - - for key, value := range originalSettings { - _, err := setSysctl(key, value, false) - if err != nil { - result = multierror.Append(result, err) - } - } - - return result.ErrorOrNil() -} diff --git a/client/internal/routemanager/systemops_linux_test.go b/client/internal/routemanager/systemops/systemops_linux_test.go similarity index 96% rename from client/internal/routemanager/systemops_linux_test.go rename to client/internal/routemanager/systemops/systemops_linux_test.go index 0043c3f4e..8f12740d0 100644 --- a/client/internal/routemanager/systemops_linux_test.go +++ b/client/internal/routemanager/systemops/systemops_linux_test.go @@ -1,6 +1,6 @@ //go:build !android -package routemanager +package systemops import ( "errors" @@ -14,6 +14,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/vishvananda/netlink" + + "github.com/netbirdio/netbird/client/internal/routemanager/vars" ) var expectedVPNint = "wgtest0" @@ -138,7 +140,7 @@ func addDummyRoute(t *testing.T, dstCIDR string, gw net.IP, intf string) { if dstIPNet.String() == "0.0.0.0/0" { var err error originalNexthop, originalLinkIndex, err = fetchOriginalGateway(netlink.FAMILY_V4) - if err != nil && !errors.Is(err, ErrRouteNotFound) { + if err != nil && !errors.Is(err, vars.ErrRouteNotFound) { t.Logf("Failed to fetch original gateway: %v", err) } @@ -193,7 +195,7 @@ func fetchOriginalGateway(family int) (net.IP, int, error) { } } - return nil, 0, ErrRouteNotFound + return nil, 0, vars.ErrRouteNotFound } func setupDummyInterfacesAndRoutes(t *testing.T) { diff --git a/client/internal/routemanager/systemops/systemops_mobile.go b/client/internal/routemanager/systemops/systemops_mobile.go new file mode 100644 index 000000000..1517cf949 --- /dev/null +++ b/client/internal/routemanager/systemops/systemops_mobile.go @@ -0,0 +1,34 @@ +//go:build ios || android + +package systemops + +import ( + "net" + "net/netip" + "runtime" + + log "github.com/sirupsen/logrus" + + "github.com/netbirdio/netbird/client/internal/peer" +) + +func (r *SysOps) SetupRouting([]net.IP) (peer.BeforeAddPeerHookFunc, peer.AfterRemovePeerHookFunc, error) { + return nil, nil, nil +} + +func (r *SysOps) CleanupRouting() error { + return nil +} + +func (r *SysOps) AddVPNRoute(netip.Prefix, *net.Interface) error { + return nil +} + +func (r *SysOps) RemoveVPNRoute(netip.Prefix, *net.Interface) error { + return nil +} + +func EnableIPForwarding() error { + log.Infof("Enable IP forwarding is not implemented on %s", runtime.GOOS) + return nil +} diff --git a/client/internal/routemanager/systemops/systemops_nonlinux.go b/client/internal/routemanager/systemops/systemops_nonlinux.go new file mode 100644 index 000000000..0f70cd78d --- /dev/null +++ b/client/internal/routemanager/systemops/systemops_nonlinux.go @@ -0,0 +1,24 @@ +//go:build !linux && !ios + +package systemops + +import ( + "net" + "net/netip" + "runtime" + + log "github.com/sirupsen/logrus" +) + +func (r *SysOps) AddVPNRoute(prefix netip.Prefix, intf *net.Interface) error { + return r.genericAddVPNRoute(prefix, intf) +} + +func (r *SysOps) RemoveVPNRoute(prefix netip.Prefix, intf *net.Interface) error { + return r.genericRemoveVPNRoute(prefix, intf) +} + +func EnableIPForwarding() error { + log.Infof("Enable IP forwarding is not implemented on %s", runtime.GOOS) + return nil +} diff --git a/client/internal/routemanager/systemops_darwin.go b/client/internal/routemanager/systemops/systemops_unix.go similarity index 58% rename from client/internal/routemanager/systemops_darwin.go rename to client/internal/routemanager/systemops/systemops_unix.go index ee4196a0c..d4d2a31ed 100644 --- a/client/internal/routemanager/systemops_darwin.go +++ b/client/internal/routemanager/systemops/systemops_unix.go @@ -1,6 +1,6 @@ -//go:build darwin && !ios +//go:build (darwin && !ios) || dragonfly || freebsd || netbsd || openbsd -package routemanager +package systemops import ( "fmt" @@ -14,42 +14,40 @@ import ( log "github.com/sirupsen/logrus" "github.com/netbirdio/netbird/client/internal/peer" - "github.com/netbirdio/netbird/iface" ) -var routeManager *RouteManager - -func setupRouting(initAddresses []net.IP, wgIface *iface.WGIface) (peer.BeforeAddPeerHookFunc, peer.AfterRemovePeerHookFunc, error) { - return setupRoutingWithRouteManager(&routeManager, initAddresses, wgIface) +func (r *SysOps) SetupRouting(initAddresses []net.IP) (peer.BeforeAddPeerHookFunc, peer.AfterRemovePeerHookFunc, error) { + return r.setupRefCounter(initAddresses) } -func cleanupRouting() error { - return cleanupRoutingWithRouteManager(routeManager) +func (r *SysOps) CleanupRouting() error { + return r.cleanupRefCounter() } -func addToRouteTable(prefix netip.Prefix, nexthop netip.Addr, intf *net.Interface) error { - return routeCmd("add", prefix, nexthop, intf) +func (r *SysOps) addToRouteTable(prefix netip.Prefix, nexthop Nexthop) error { + return r.routeCmd("add", prefix, nexthop) } -func removeFromRouteTable(prefix netip.Prefix, nexthop netip.Addr, intf *net.Interface) error { - return routeCmd("delete", prefix, nexthop, intf) +func (r *SysOps) removeFromRouteTable(prefix netip.Prefix, nexthop Nexthop) error { + return r.routeCmd("delete", prefix, nexthop) } -func routeCmd(action string, prefix netip.Prefix, nexthop netip.Addr, intf *net.Interface) error { +func (r *SysOps) routeCmd(action string, prefix netip.Prefix, nexthop Nexthop) error { inet := "-inet" - network := prefix.String() - if prefix.IsSingleIP() { - network = prefix.Addr().String() - } if prefix.Addr().Is6() { inet = "-inet6" } + network := prefix.String() + if prefix.IsSingleIP() { + network = prefix.Addr().String() + } + args := []string{"-n", action, inet, network} - if nexthop.IsValid() { - args = append(args, nexthop.Unmap().String()) - } else if intf != nil { - args = append(args, "-interface", intf.Name) + if nexthop.IP.IsValid() { + args = append(args, nexthop.IP.Unmap().String()) + } else if nexthop.Intf != nil { + args = append(args, "-interface", nexthop.Intf.Name) } if err := retryRouteCmd(args); err != nil { diff --git a/client/internal/routemanager/systemops_unix_test.go b/client/internal/routemanager/systemops/systemops_unix_test.go similarity index 99% rename from client/internal/routemanager/systemops_unix_test.go rename to client/internal/routemanager/systemops/systemops_unix_test.go index 561eaeea4..fc964aa62 100644 --- a/client/internal/routemanager/systemops_unix_test.go +++ b/client/internal/routemanager/systemops/systemops_unix_test.go @@ -1,6 +1,6 @@ //go:build (linux && !android) || (darwin && !ios) || freebsd || openbsd || netbsd || dragonfly -package routemanager +package systemops import ( "fmt" diff --git a/client/internal/routemanager/systemops_windows.go b/client/internal/routemanager/systemops/systemops_windows.go similarity index 78% rename from client/internal/routemanager/systemops_windows.go rename to client/internal/routemanager/systemops/systemops_windows.go index 32e94d8da..b869bc85b 100644 --- a/client/internal/routemanager/systemops_windows.go +++ b/client/internal/routemanager/systemops/systemops_windows.go @@ -1,6 +1,6 @@ //go:build windows -package routemanager +package systemops import ( "fmt" @@ -18,7 +18,6 @@ import ( "github.com/netbirdio/netbird/client/firewall/uspfilter" "github.com/netbirdio/netbird/client/internal/peer" - "github.com/netbirdio/netbird/iface" ) type MSFT_NetRoute struct { @@ -57,14 +56,43 @@ var prefixList []netip.Prefix var lastUpdate time.Time var mux = sync.Mutex{} -var routeManager *RouteManager - -func setupRouting(initAddresses []net.IP, wgIface *iface.WGIface) (peer.BeforeAddPeerHookFunc, peer.AfterRemovePeerHookFunc, error) { - return setupRoutingWithRouteManager(&routeManager, initAddresses, wgIface) +func (r *SysOps) SetupRouting(initAddresses []net.IP) (peer.BeforeAddPeerHookFunc, peer.AfterRemovePeerHookFunc, error) { + return r.setupRefCounter(initAddresses) } -func cleanupRouting() error { - return cleanupRoutingWithRouteManager(routeManager) +func (r *SysOps) CleanupRouting() error { + return r.cleanupRefCounter() +} + +func (r *SysOps) addToRouteTable(prefix netip.Prefix, nexthop Nexthop) error { + if nexthop.IP.Zone() != "" && nexthop.Intf == nil { + zone, err := strconv.Atoi(nexthop.IP.Zone()) + if err != nil { + return fmt.Errorf("invalid zone: %w", err) + } + nexthop.Intf = &net.Interface{Index: zone} + nexthop.IP.WithZone("") + } + + return addRouteCmd(prefix, nexthop) +} + +func (r *SysOps) removeFromRouteTable(prefix netip.Prefix, nexthop Nexthop) error { + args := []string{"delete", prefix.String()} + if nexthop.IP.IsValid() { + nexthop.IP.WithZone("") + args = append(args, nexthop.IP.Unmap().String()) + } + + routeCmd := uspfilter.GetSystem32Command("route") + + out, err := exec.Command(routeCmd, args...).CombinedOutput() + log.Tracef("route %s: %s", strings.Join(args, " "), out) + + if err != nil { + return fmt.Errorf("remove route: %w", err) + } + return nil } func getRoutesFromTable() ([]netip.Prefix, error) { @@ -93,7 +121,7 @@ func getRoutesFromTable() ([]netip.Prefix, error) { func GetRoutes() ([]Route, error) { var entries []MSFT_NetRoute - query := `SELECT DestinationPrefix, NextHop, InterfaceIndex, InterfaceAlias, AddressFamily FROM MSFT_NetRoute` + query := `SELECT DestinationPrefix, Nexthop, InterfaceIndex, InterfaceAlias, AddressFamily FROM MSFT_NetRoute` if err := wmi.QueryNamespace(query, &entries, `ROOT\StandardCimv2`); err != nil { return nil, fmt.Errorf("get routes: %w", err) } @@ -157,11 +185,11 @@ func GetNeighbors() ([]Neighbor, error) { return neighbors, nil } -func addRouteCmd(prefix netip.Prefix, nexthop netip.Addr, intf *net.Interface) error { +func addRouteCmd(prefix netip.Prefix, nexthop Nexthop) error { args := []string{"add", prefix.String()} - if nexthop.IsValid() { - args = append(args, nexthop.Unmap().String()) + if nexthop.IP.IsValid() { + args = append(args, nexthop.IP.Unmap().String()) } else { addr := "0.0.0.0" if prefix.Addr().Is6() { @@ -170,8 +198,8 @@ func addRouteCmd(prefix netip.Prefix, nexthop netip.Addr, intf *net.Interface) e args = append(args, addr) } - if intf != nil { - args = append(args, "if", strconv.Itoa(intf.Index)) + if nexthop.Intf != nil { + args = append(args, "if", strconv.Itoa(nexthop.Intf.Index)) } routeCmd := uspfilter.GetSystem32Command("route") @@ -185,37 +213,6 @@ func addRouteCmd(prefix netip.Prefix, nexthop netip.Addr, intf *net.Interface) e return nil } -func addToRouteTable(prefix netip.Prefix, nexthop netip.Addr, intf *net.Interface) error { - if nexthop.Zone() != "" && intf == nil { - zone, err := strconv.Atoi(nexthop.Zone()) - if err != nil { - return fmt.Errorf("invalid zone: %w", err) - } - intf = &net.Interface{Index: zone} - nexthop.WithZone("") - } - - return addRouteCmd(prefix, nexthop, intf) -} - -func removeFromRouteTable(prefix netip.Prefix, nexthop netip.Addr, _ *net.Interface) error { - args := []string{"delete", prefix.String()} - if nexthop.IsValid() { - nexthop.WithZone("") - args = append(args, nexthop.Unmap().String()) - } - - routeCmd := uspfilter.GetSystem32Command("route") - - out, err := exec.Command(routeCmd, args...).CombinedOutput() - log.Tracef("route %s: %s", strings.Join(args, " "), out) - - if err != nil { - return fmt.Errorf("remove route: %w", err) - } - return nil -} - func isCacheDisabled() bool { return os.Getenv("NB_DISABLE_ROUTE_CACHE") == "true" } diff --git a/client/internal/routemanager/systemops_windows_test.go b/client/internal/routemanager/systemops/systemops_windows_test.go similarity index 97% rename from client/internal/routemanager/systemops_windows_test.go rename to client/internal/routemanager/systemops/systemops_windows_test.go index a5e03b8d2..9180ed58c 100644 --- a/client/internal/routemanager/systemops_windows_test.go +++ b/client/internal/routemanager/systemops/systemops_windows_test.go @@ -1,4 +1,4 @@ -package routemanager +package systemops import ( "context" @@ -29,7 +29,7 @@ type FindNetRouteOutput struct { InterfaceIndex int `json:"InterfaceIndex"` InterfaceAlias string `json:"InterfaceAlias"` AddressFamily int `json:"AddressFamily"` - NextHop string `json:"NextHop"` + NextHop string `json:"Nexthop"` DestinationPrefix string `json:"DestinationPrefix"` } @@ -166,7 +166,7 @@ func testRoute(t *testing.T, destination string, dialer dialer) *FindNetRouteOut host, _, err := net.SplitHostPort(destination) require.NoError(t, err) - script := fmt.Sprintf(`Find-NetRoute -RemoteIPAddress "%s" | Select-Object -Property IPAddress, InterfaceIndex, InterfaceAlias, AddressFamily, NextHop, DestinationPrefix | ConvertTo-Json`, host) + script := fmt.Sprintf(`Find-NetRoute -RemoteIPAddress "%s" | Select-Object -Property IPAddress, InterfaceIndex, InterfaceAlias, AddressFamily, Nexthop, DestinationPrefix | ConvertTo-Json`, host) out, err := exec.Command("powershell", "-Command", script).Output() require.NoError(t, err, "Failed to execute Find-NetRoute") @@ -207,7 +207,7 @@ func createAndSetupDummyInterface(t *testing.T, interfaceName, ipAddressCIDR str } func fetchOriginalGateway() (*RouteInfo, error) { - cmd := exec.Command("powershell", "-Command", "Get-NetRoute -DestinationPrefix 0.0.0.0/0 | Select-Object NextHop, RouteMetric, InterfaceAlias | ConvertTo-Json") + cmd := exec.Command("powershell", "-Command", "Get-NetRoute -DestinationPrefix 0.0.0.0/0 | Select-Object Nexthop, RouteMetric, InterfaceAlias | ConvertTo-Json") output, err := cmd.CombinedOutput() if err != nil { return nil, fmt.Errorf("failed to execute Get-NetRoute: %w", err) diff --git a/client/internal/routemanager/systemops_android.go b/client/internal/routemanager/systemops_android.go deleted file mode 100644 index 4d23d3910..000000000 --- a/client/internal/routemanager/systemops_android.go +++ /dev/null @@ -1,33 +0,0 @@ -package routemanager - -import ( - "net" - "net/netip" - "runtime" - - log "github.com/sirupsen/logrus" - - "github.com/netbirdio/netbird/client/internal/peer" - "github.com/netbirdio/netbird/iface" -) - -func setupRouting([]net.IP, *iface.WGIface) (peer.BeforeAddPeerHookFunc, peer.AfterRemovePeerHookFunc, error) { - return nil, nil, nil -} - -func cleanupRouting() error { - return nil -} - -func enableIPForwarding() error { - log.Infof("Enable IP forwarding is not implemented on %s", runtime.GOOS) - return nil -} - -func addVPNRoute(netip.Prefix, *net.Interface) error { - return nil -} - -func removeVPNRoute(netip.Prefix, *net.Interface) error { - return nil -} diff --git a/client/internal/routemanager/systemops_ios.go b/client/internal/routemanager/systemops_ios.go deleted file mode 100644 index 4d23d3910..000000000 --- a/client/internal/routemanager/systemops_ios.go +++ /dev/null @@ -1,33 +0,0 @@ -package routemanager - -import ( - "net" - "net/netip" - "runtime" - - log "github.com/sirupsen/logrus" - - "github.com/netbirdio/netbird/client/internal/peer" - "github.com/netbirdio/netbird/iface" -) - -func setupRouting([]net.IP, *iface.WGIface) (peer.BeforeAddPeerHookFunc, peer.AfterRemovePeerHookFunc, error) { - return nil, nil, nil -} - -func cleanupRouting() error { - return nil -} - -func enableIPForwarding() error { - log.Infof("Enable IP forwarding is not implemented on %s", runtime.GOOS) - return nil -} - -func addVPNRoute(netip.Prefix, *net.Interface) error { - return nil -} - -func removeVPNRoute(netip.Prefix, *net.Interface) error { - return nil -} diff --git a/client/internal/routemanager/systemops_nonlinux.go b/client/internal/routemanager/systemops_nonlinux.go deleted file mode 100644 index 91879790a..000000000 --- a/client/internal/routemanager/systemops_nonlinux.go +++ /dev/null @@ -1,24 +0,0 @@ -//go:build !linux && !ios - -package routemanager - -import ( - "net" - "net/netip" - "runtime" - - log "github.com/sirupsen/logrus" -) - -func enableIPForwarding() error { - log.Infof("Enable IP forwarding is not implemented on %s", runtime.GOOS) - return nil -} - -func addVPNRoute(prefix netip.Prefix, intf *net.Interface) error { - return genericAddVPNRoute(prefix, intf) -} - -func removeVPNRoute(prefix netip.Prefix, intf *net.Interface) error { - return genericRemoveVPNRoute(prefix, intf) -} diff --git a/client/internal/routemanager/util/ip.go b/client/internal/routemanager/util/ip.go new file mode 100644 index 000000000..ac5a48e37 --- /dev/null +++ b/client/internal/routemanager/util/ip.go @@ -0,0 +1,29 @@ +package util + +import ( + "fmt" + "net" + "net/netip" +) + +// GetPrefixFromIP returns a netip.Prefix from a net.IP address. +func GetPrefixFromIP(ip net.IP) (netip.Prefix, error) { + addr, ok := netip.AddrFromSlice(ip) + if !ok { + return netip.Prefix{}, fmt.Errorf("parse IP address: %s", ip) + } + addr = addr.Unmap() + + var prefixLength int + switch { + case addr.Is4(): + prefixLength = 32 + case addr.Is6(): + prefixLength = 128 + default: + return netip.Prefix{}, fmt.Errorf("invalid IP address: %s", addr) + } + + prefix := netip.PrefixFrom(addr, prefixLength) + return prefix, nil +} diff --git a/client/internal/routemanager/vars/vars.go b/client/internal/routemanager/vars/vars.go new file mode 100644 index 000000000..4aa986d2f --- /dev/null +++ b/client/internal/routemanager/vars/vars.go @@ -0,0 +1,16 @@ +package vars + +import ( + "errors" + "net/netip" +) + +const MinRangeBits = 7 + +var ( + ErrRouteNotFound = errors.New("route not found") + ErrRouteNotAllowed = errors.New("route not allowed") + + Defaultv4 = netip.PrefixFrom(netip.IPv4Unspecified(), 0) + Defaultv6 = netip.PrefixFrom(netip.IPv6Unspecified(), 0) +) diff --git a/client/internal/routeselector/routeselector.go b/client/internal/routeselector/routeselector.go index 1c17e8803..00128a27b 100644 --- a/client/internal/routeselector/routeselector.go +++ b/client/internal/routeselector/routeselector.go @@ -3,11 +3,11 @@ package routeselector import ( "fmt" "slices" - "strings" "github.com/hashicorp/go-multierror" "golang.org/x/exp/maps" + "github.com/netbirdio/netbird/client/errors" route "github.com/netbirdio/netbird/route" ) @@ -30,10 +30,10 @@ func (rs *RouteSelector) SelectRoutes(routes []route.NetID, appendRoute bool, al rs.selectedRoutes = map[route.NetID]struct{}{} } - var multiErr *multierror.Error + var err *multierror.Error for _, route := range routes { if !slices.Contains(allRoutes, route) { - multiErr = multierror.Append(multiErr, fmt.Errorf("route '%s' is not available", route)) + err = multierror.Append(err, fmt.Errorf("route '%s' is not available", route)) continue } @@ -41,11 +41,7 @@ func (rs *RouteSelector) SelectRoutes(routes []route.NetID, appendRoute bool, al } rs.selectAll = false - if multiErr != nil { - multiErr.ErrorFormat = formatError - } - - return multiErr.ErrorOrNil() + return errors.FormatErrorOrNil(err) } // SelectAllRoutes sets the selector to select all routes. @@ -65,21 +61,17 @@ func (rs *RouteSelector) DeselectRoutes(routes []route.NetID, allRoutes []route. } } - var multiErr *multierror.Error + var err *multierror.Error for _, route := range routes { if !slices.Contains(allRoutes, route) { - multiErr = multierror.Append(multiErr, fmt.Errorf("route '%s' is not available", route)) + err = multierror.Append(err, fmt.Errorf("route '%s' is not available", route)) continue } delete(rs.selectedRoutes, route) } - if multiErr != nil { - multiErr.ErrorFormat = formatError - } - - return multiErr.ErrorOrNil() + return errors.FormatErrorOrNil(err) } // DeselectAllRoutes deselects all routes, effectively disabling route selection. @@ -111,18 +103,3 @@ func (rs *RouteSelector) FilterSelected(routes route.HAMap) route.HAMap { } return filtered } - -func formatError(es []error) string { - if len(es) == 1 { - return fmt.Sprintf("1 error occurred:\n\t* %s", es[0]) - } - - points := make([]string, len(es)) - for i, err := range es { - points[i] = fmt.Sprintf("* %s", err) - } - - return fmt.Sprintf( - "%d errors occurred:\n\t%s", - len(es), strings.Join(points, "\n\t")) -} diff --git a/client/internal/routeselector/routeselector_test.go b/client/internal/routeselector/routeselector_test.go index fb1e456cd..7df433f92 100644 --- a/client/internal/routeselector/routeselector_test.go +++ b/client/internal/routeselector/routeselector_test.go @@ -261,15 +261,15 @@ func TestRouteSelector_FilterSelected(t *testing.T) { require.NoError(t, err) routes := route.HAMap{ - "route1-10.0.0.0/8": {}, - "route2-192.168.0.0/16": {}, - "route3-172.16.0.0/12": {}, + "route1|10.0.0.0/8": {}, + "route2|192.168.0.0/16": {}, + "route3|172.16.0.0/12": {}, } filtered := rs.FilterSelected(routes) assert.Equal(t, route.HAMap{ - "route1-10.0.0.0/8": {}, - "route2-192.168.0.0/16": {}, + "route1|10.0.0.0/8": {}, + "route2|192.168.0.0/16": {}, }, filtered) } diff --git a/client/proto/daemon.pb.go b/client/proto/daemon.pb.go index 83c8278d5..a624d1336 100644 --- a/client/proto/daemon.pb.go +++ b/client/proto/daemon.pb.go @@ -108,19 +108,20 @@ type LoginRequest struct { // cleanNATExternalIPs clean map list of external IPs. // This is needed because the generated code // omits initialized empty slices due to omitempty tags - CleanNATExternalIPs bool `protobuf:"varint,6,opt,name=cleanNATExternalIPs,proto3" json:"cleanNATExternalIPs,omitempty"` - CustomDNSAddress []byte `protobuf:"bytes,7,opt,name=customDNSAddress,proto3" json:"customDNSAddress,omitempty"` - IsLinuxDesktopClient bool `protobuf:"varint,8,opt,name=isLinuxDesktopClient,proto3" json:"isLinuxDesktopClient,omitempty"` - Hostname string `protobuf:"bytes,9,opt,name=hostname,proto3" json:"hostname,omitempty"` - RosenpassEnabled *bool `protobuf:"varint,10,opt,name=rosenpassEnabled,proto3,oneof" json:"rosenpassEnabled,omitempty"` - InterfaceName *string `protobuf:"bytes,11,opt,name=interfaceName,proto3,oneof" json:"interfaceName,omitempty"` - WireguardPort *int64 `protobuf:"varint,12,opt,name=wireguardPort,proto3,oneof" json:"wireguardPort,omitempty"` - OptionalPreSharedKey *string `protobuf:"bytes,13,opt,name=optionalPreSharedKey,proto3,oneof" json:"optionalPreSharedKey,omitempty"` - DisableAutoConnect *bool `protobuf:"varint,14,opt,name=disableAutoConnect,proto3,oneof" json:"disableAutoConnect,omitempty"` - ServerSSHAllowed *bool `protobuf:"varint,15,opt,name=serverSSHAllowed,proto3,oneof" json:"serverSSHAllowed,omitempty"` - RosenpassPermissive *bool `protobuf:"varint,16,opt,name=rosenpassPermissive,proto3,oneof" json:"rosenpassPermissive,omitempty"` - ExtraIFaceBlacklist []string `protobuf:"bytes,17,rep,name=extraIFaceBlacklist,proto3" json:"extraIFaceBlacklist,omitempty"` - NetworkMonitor *bool `protobuf:"varint,18,opt,name=networkMonitor,proto3,oneof" json:"networkMonitor,omitempty"` + CleanNATExternalIPs bool `protobuf:"varint,6,opt,name=cleanNATExternalIPs,proto3" json:"cleanNATExternalIPs,omitempty"` + CustomDNSAddress []byte `protobuf:"bytes,7,opt,name=customDNSAddress,proto3" json:"customDNSAddress,omitempty"` + IsLinuxDesktopClient bool `protobuf:"varint,8,opt,name=isLinuxDesktopClient,proto3" json:"isLinuxDesktopClient,omitempty"` + Hostname string `protobuf:"bytes,9,opt,name=hostname,proto3" json:"hostname,omitempty"` + RosenpassEnabled *bool `protobuf:"varint,10,opt,name=rosenpassEnabled,proto3,oneof" json:"rosenpassEnabled,omitempty"` + InterfaceName *string `protobuf:"bytes,11,opt,name=interfaceName,proto3,oneof" json:"interfaceName,omitempty"` + WireguardPort *int64 `protobuf:"varint,12,opt,name=wireguardPort,proto3,oneof" json:"wireguardPort,omitempty"` + OptionalPreSharedKey *string `protobuf:"bytes,13,opt,name=optionalPreSharedKey,proto3,oneof" json:"optionalPreSharedKey,omitempty"` + DisableAutoConnect *bool `protobuf:"varint,14,opt,name=disableAutoConnect,proto3,oneof" json:"disableAutoConnect,omitempty"` + ServerSSHAllowed *bool `protobuf:"varint,15,opt,name=serverSSHAllowed,proto3,oneof" json:"serverSSHAllowed,omitempty"` + RosenpassPermissive *bool `protobuf:"varint,16,opt,name=rosenpassPermissive,proto3,oneof" json:"rosenpassPermissive,omitempty"` + ExtraIFaceBlacklist []string `protobuf:"bytes,17,rep,name=extraIFaceBlacklist,proto3" json:"extraIFaceBlacklist,omitempty"` + NetworkMonitor *bool `protobuf:"varint,18,opt,name=networkMonitor,proto3,oneof" json:"networkMonitor,omitempty"` + DnsRouteInterval *duration.Duration `protobuf:"bytes,19,opt,name=dnsRouteInterval,proto3,oneof" json:"dnsRouteInterval,omitempty"` } func (x *LoginRequest) Reset() { @@ -282,6 +283,13 @@ func (x *LoginRequest) GetNetworkMonitor() bool { return false } +func (x *LoginRequest) GetDnsRouteInterval() *duration.Duration { + if x != nil { + return x.DnsRouteInterval + } + return nil +} + type LoginResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -1641,20 +1649,69 @@ func (*SelectRoutesResponse) Descriptor() ([]byte, []int) { return file_daemon_proto_rawDescGZIP(), []int{22} } +type IPList struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Ips []string `protobuf:"bytes,1,rep,name=ips,proto3" json:"ips,omitempty"` +} + +func (x *IPList) Reset() { + *x = IPList{} + if protoimpl.UnsafeEnabled { + mi := &file_daemon_proto_msgTypes[23] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *IPList) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*IPList) ProtoMessage() {} + +func (x *IPList) ProtoReflect() protoreflect.Message { + mi := &file_daemon_proto_msgTypes[23] + 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 IPList.ProtoReflect.Descriptor instead. +func (*IPList) Descriptor() ([]byte, []int) { + return file_daemon_proto_rawDescGZIP(), []int{23} +} + +func (x *IPList) GetIps() []string { + if x != nil { + return x.Ips + } + return nil +} + type Route struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - ID string `protobuf:"bytes,1,opt,name=ID,proto3" json:"ID,omitempty"` - Network string `protobuf:"bytes,2,opt,name=network,proto3" json:"network,omitempty"` - Selected bool `protobuf:"varint,3,opt,name=selected,proto3" json:"selected,omitempty"` + ID string `protobuf:"bytes,1,opt,name=ID,proto3" json:"ID,omitempty"` + Network string `protobuf:"bytes,2,opt,name=network,proto3" json:"network,omitempty"` + Selected bool `protobuf:"varint,3,opt,name=selected,proto3" json:"selected,omitempty"` + Domains []string `protobuf:"bytes,4,rep,name=domains,proto3" json:"domains,omitempty"` + ResolvedIPs map[string]*IPList `protobuf:"bytes,5,rep,name=resolvedIPs,proto3" json:"resolvedIPs,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` } func (x *Route) Reset() { *x = Route{} if protoimpl.UnsafeEnabled { - mi := &file_daemon_proto_msgTypes[23] + mi := &file_daemon_proto_msgTypes[24] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1667,7 +1724,7 @@ func (x *Route) String() string { func (*Route) ProtoMessage() {} func (x *Route) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[23] + mi := &file_daemon_proto_msgTypes[24] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1680,7 +1737,7 @@ func (x *Route) ProtoReflect() protoreflect.Message { // Deprecated: Use Route.ProtoReflect.Descriptor instead. func (*Route) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{23} + return file_daemon_proto_rawDescGZIP(), []int{24} } func (x *Route) GetID() string { @@ -1704,6 +1761,20 @@ func (x *Route) GetSelected() bool { return false } +func (x *Route) GetDomains() []string { + if x != nil { + return x.Domains + } + return nil +} + +func (x *Route) GetResolvedIPs() map[string]*IPList { + if x != nil { + return x.ResolvedIPs + } + return nil +} + type DebugBundleRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -1716,7 +1787,7 @@ type DebugBundleRequest struct { func (x *DebugBundleRequest) Reset() { *x = DebugBundleRequest{} if protoimpl.UnsafeEnabled { - mi := &file_daemon_proto_msgTypes[24] + mi := &file_daemon_proto_msgTypes[25] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1729,7 +1800,7 @@ func (x *DebugBundleRequest) String() string { func (*DebugBundleRequest) ProtoMessage() {} func (x *DebugBundleRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[24] + mi := &file_daemon_proto_msgTypes[25] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1742,7 +1813,7 @@ func (x *DebugBundleRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DebugBundleRequest.ProtoReflect.Descriptor instead. func (*DebugBundleRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{24} + return file_daemon_proto_rawDescGZIP(), []int{25} } func (x *DebugBundleRequest) GetAnonymize() bool { @@ -1770,7 +1841,7 @@ type DebugBundleResponse struct { func (x *DebugBundleResponse) Reset() { *x = DebugBundleResponse{} if protoimpl.UnsafeEnabled { - mi := &file_daemon_proto_msgTypes[25] + mi := &file_daemon_proto_msgTypes[26] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1783,7 +1854,7 @@ func (x *DebugBundleResponse) String() string { func (*DebugBundleResponse) ProtoMessage() {} func (x *DebugBundleResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[25] + mi := &file_daemon_proto_msgTypes[26] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1796,7 +1867,7 @@ func (x *DebugBundleResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use DebugBundleResponse.ProtoReflect.Descriptor instead. func (*DebugBundleResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{25} + return file_daemon_proto_rawDescGZIP(), []int{26} } func (x *DebugBundleResponse) GetPath() string { @@ -1815,7 +1886,7 @@ type GetLogLevelRequest struct { func (x *GetLogLevelRequest) Reset() { *x = GetLogLevelRequest{} if protoimpl.UnsafeEnabled { - mi := &file_daemon_proto_msgTypes[26] + mi := &file_daemon_proto_msgTypes[27] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1828,7 +1899,7 @@ func (x *GetLogLevelRequest) String() string { func (*GetLogLevelRequest) ProtoMessage() {} func (x *GetLogLevelRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[26] + mi := &file_daemon_proto_msgTypes[27] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1841,7 +1912,7 @@ func (x *GetLogLevelRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetLogLevelRequest.ProtoReflect.Descriptor instead. func (*GetLogLevelRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{26} + return file_daemon_proto_rawDescGZIP(), []int{27} } type GetLogLevelResponse struct { @@ -1855,7 +1926,7 @@ type GetLogLevelResponse struct { func (x *GetLogLevelResponse) Reset() { *x = GetLogLevelResponse{} if protoimpl.UnsafeEnabled { - mi := &file_daemon_proto_msgTypes[27] + mi := &file_daemon_proto_msgTypes[28] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1868,7 +1939,7 @@ func (x *GetLogLevelResponse) String() string { func (*GetLogLevelResponse) ProtoMessage() {} func (x *GetLogLevelResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[27] + mi := &file_daemon_proto_msgTypes[28] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1881,7 +1952,7 @@ func (x *GetLogLevelResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetLogLevelResponse.ProtoReflect.Descriptor instead. func (*GetLogLevelResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{27} + return file_daemon_proto_rawDescGZIP(), []int{28} } func (x *GetLogLevelResponse) GetLevel() LogLevel { @@ -1902,7 +1973,7 @@ type SetLogLevelRequest struct { func (x *SetLogLevelRequest) Reset() { *x = SetLogLevelRequest{} if protoimpl.UnsafeEnabled { - mi := &file_daemon_proto_msgTypes[28] + mi := &file_daemon_proto_msgTypes[29] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1915,7 +1986,7 @@ func (x *SetLogLevelRequest) String() string { func (*SetLogLevelRequest) ProtoMessage() {} func (x *SetLogLevelRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[28] + mi := &file_daemon_proto_msgTypes[29] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1928,7 +1999,7 @@ func (x *SetLogLevelRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use SetLogLevelRequest.ProtoReflect.Descriptor instead. func (*SetLogLevelRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{28} + return file_daemon_proto_rawDescGZIP(), []int{29} } func (x *SetLogLevelRequest) GetLevel() LogLevel { @@ -1947,7 +2018,7 @@ type SetLogLevelResponse struct { func (x *SetLogLevelResponse) Reset() { *x = SetLogLevelResponse{} if protoimpl.UnsafeEnabled { - mi := &file_daemon_proto_msgTypes[29] + mi := &file_daemon_proto_msgTypes[30] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1960,7 +2031,7 @@ func (x *SetLogLevelResponse) String() string { func (*SetLogLevelResponse) ProtoMessage() {} func (x *SetLogLevelResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[29] + mi := &file_daemon_proto_msgTypes[30] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1973,7 +2044,7 @@ func (x *SetLogLevelResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use SetLogLevelResponse.ProtoReflect.Descriptor instead. func (*SetLogLevelResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{29} + return file_daemon_proto_rawDescGZIP(), []int{30} } var File_daemon_proto protoreflect.FileDescriptor @@ -1986,7 +2057,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, 0xcf, 0x07, 0x0a, 0x0c, 0x4c, 0x6f, + 0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xb0, 0x08, 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, @@ -2037,263 +2108,281 @@ var file_daemon_proto_rawDesc = []byte{ 0x6b, 0x6c, 0x69, 0x73, 0x74, 0x12, 0x2b, 0x0a, 0x0e, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x18, 0x12, 0x20, 0x01, 0x28, 0x08, 0x48, 0x07, 0x52, 0x0e, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 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, 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, + 0x01, 0x01, 0x12, 0x4a, 0x0a, 0x10, 0x64, 0x6e, 0x73, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x49, 0x6e, + 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x18, 0x13, 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, 0x48, 0x08, 0x52, 0x10, 0x64, 0x6e, 0x73, 0x52, 0x6f, + 0x75, 0x74, 0x65, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 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, 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, 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, 0xb3, 0x01, 0x0a, - 0x11, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x12, 0x24, 0x0a, 0x0d, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, - 0x55, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6d, 0x61, 0x6e, 0x61, 0x67, - 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x55, 0x72, 0x6c, 0x12, 0x1e, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x66, - 0x69, 0x67, 0x46, 0x69, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x63, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x46, 0x69, 0x6c, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6c, 0x6f, 0x67, 0x46, - 0x69, 0x6c, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6c, 0x6f, 0x67, 0x46, 0x69, - 0x6c, 0x65, 0x12, 0x22, 0x0a, 0x0c, 0x70, 0x72, 0x65, 0x53, 0x68, 0x61, 0x72, 0x65, 0x64, 0x4b, - 0x65, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x70, 0x72, 0x65, 0x53, 0x68, 0x61, - 0x72, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x12, 0x1a, 0x0a, 0x08, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x55, - 0x52, 0x4c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x55, - 0x52, 0x4c, 0x22, 0xce, 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, 0x16, 0x0a, 0x06, 0x64, 0x69, - 0x72, 0x65, 0x63, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x64, 0x69, 0x72, 0x65, - 0x63, 0x74, 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, + 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, 0xb3, 0x01, + 0x0a, 0x11, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x24, 0x0a, 0x0d, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, + 0x74, 0x55, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6d, 0x61, 0x6e, 0x61, + 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x55, 0x72, 0x6c, 0x12, 0x1e, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x46, 0x69, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x63, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x46, 0x69, 0x6c, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6c, 0x6f, 0x67, + 0x46, 0x69, 0x6c, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6c, 0x6f, 0x67, 0x46, + 0x69, 0x6c, 0x65, 0x12, 0x22, 0x0a, 0x0c, 0x70, 0x72, 0x65, 0x53, 0x68, 0x61, 0x72, 0x65, 0x64, + 0x4b, 0x65, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x70, 0x72, 0x65, 0x53, 0x68, + 0x61, 0x72, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x12, 0x1a, 0x0a, 0x08, 0x61, 0x64, 0x6d, 0x69, 0x6e, + 0x55, 0x52, 0x4c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x61, 0x64, 0x6d, 0x69, 0x6e, + 0x55, 0x52, 0x4c, 0x22, 0xce, 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, 0x16, 0x0a, 0x06, 0x64, + 0x69, 0x72, 0x65, 0x63, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x64, 0x69, 0x72, + 0x65, 0x63, 0x74, 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, 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, 0x16, 0x0a, 0x06, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, - 0x18, 0x10, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x72, 0x6f, 0x75, 0x74, 0x65, 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, 0x22, 0xec, 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, 0x16, 0x0a, 0x06, 0x72, 0x6f, - 0x75, 0x74, 0x65, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x72, 0x6f, 0x75, 0x74, - 0x65, 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, 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, 0x13, 0x0a, - 0x11, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x22, 0x3b, 0x0a, 0x12, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x25, 0x0a, 0x06, 0x72, 0x6f, 0x75, 0x74, - 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, - 0x6e, 0x2e, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x52, 0x06, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x22, - 0x5b, 0x0a, 0x13, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x49, - 0x44, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x72, 0x6f, 0x75, 0x74, 0x65, 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, 0x16, 0x0a, 0x14, - 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x4d, 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, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, - 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 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, 0x22, 0x4a, 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, 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, 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, 0xb8, 0x06, 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, 0x45, 0x0a, 0x0a, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, - 0x12, 0x19, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x6f, - 0x75, 0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x64, 0x61, - 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x4b, 0x0a, 0x0c, 0x53, 0x65, 0x6c, - 0x65, 0x63, 0x74, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x12, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, - 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, - 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x4d, 0x0a, 0x0e, 0x44, 0x65, 0x73, 0x65, 0x6c, 0x65, - 0x63, 0x74, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x12, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, - 0x6e, 0x2e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, - 0x65, 0x6c, 0x65, 0x63, 0x74, 0x52, 0x6f, 0x75, 0x74, 0x65, 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, 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, 0x16, 0x0a, 0x06, 0x72, 0x6f, 0x75, 0x74, 0x65, + 0x73, 0x18, 0x10, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x72, 0x6f, 0x75, 0x74, 0x65, 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, 0x22, 0xec, 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, 0x16, 0x0a, 0x06, 0x72, + 0x6f, 0x75, 0x74, 0x65, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x72, 0x6f, 0x75, + 0x74, 0x65, 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, 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, 0x13, + 0x0a, 0x11, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x22, 0x3b, 0x0a, 0x12, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x6f, 0x75, 0x74, 0x65, + 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x25, 0x0a, 0x06, 0x72, 0x6f, 0x75, + 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x64, 0x61, 0x65, 0x6d, + 0x6f, 0x6e, 0x2e, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x52, 0x06, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, + 0x22, 0x5b, 0x0a, 0x13, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x6f, 0x75, 0x74, 0x65, + 0x49, 0x44, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x72, 0x6f, 0x75, 0x74, 0x65, + 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, 0x16, 0x0a, + 0x14, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x52, 0x6f, 0x75, 0x74, 0x65, 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, 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, 0x6e, + 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6e, 0x65, + 0x74, 0x77, 0x6f, 0x72, 0x6b, 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, 0x40, 0x0a, 0x0b, 0x72, + 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x64, 0x49, 0x50, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x1e, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x52, 0x6f, 0x75, 0x74, 0x65, 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, 0x4a, 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, 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, 0x22, 0x00, 0x42, 0x08, 0x5a, 0x06, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x33, + 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, 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, 0xb8, 0x06, 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, 0x45, 0x0a, 0x0a, + 0x4c, 0x69, 0x73, 0x74, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x12, 0x19, 0x2e, 0x64, 0x61, 0x65, + 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, + 0x69, 0x73, 0x74, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x22, 0x00, 0x12, 0x4b, 0x0a, 0x0c, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x52, 0x6f, 0x75, + 0x74, 0x65, 0x73, 0x12, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x6c, + 0x65, 0x63, 0x74, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x1c, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, + 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, + 0x12, 0x4d, 0x0a, 0x0e, 0x44, 0x65, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x52, 0x6f, 0x75, 0x74, + 0x65, 0x73, 0x12, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x6c, 0x65, + 0x63, 0x74, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x1c, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x52, + 0x6f, 0x75, 0x74, 0x65, 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, 0x42, 0x08, 0x5a, + 0x06, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -2309,7 +2398,7 @@ func file_daemon_proto_rawDescGZIP() []byte { } var file_daemon_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_daemon_proto_msgTypes = make([]protoimpl.MessageInfo, 30) +var file_daemon_proto_msgTypes = make([]protoimpl.MessageInfo, 32) var file_daemon_proto_goTypes = []interface{}{ (LogLevel)(0), // 0: daemon.LogLevel (*LoginRequest)(nil), // 1: daemon.LoginRequest @@ -2335,59 +2424,64 @@ var file_daemon_proto_goTypes = []interface{}{ (*ListRoutesResponse)(nil), // 21: daemon.ListRoutesResponse (*SelectRoutesRequest)(nil), // 22: daemon.SelectRoutesRequest (*SelectRoutesResponse)(nil), // 23: daemon.SelectRoutesResponse - (*Route)(nil), // 24: daemon.Route - (*DebugBundleRequest)(nil), // 25: daemon.DebugBundleRequest - (*DebugBundleResponse)(nil), // 26: daemon.DebugBundleResponse - (*GetLogLevelRequest)(nil), // 27: daemon.GetLogLevelRequest - (*GetLogLevelResponse)(nil), // 28: daemon.GetLogLevelResponse - (*SetLogLevelRequest)(nil), // 29: daemon.SetLogLevelRequest - (*SetLogLevelResponse)(nil), // 30: daemon.SetLogLevelResponse - (*timestamp.Timestamp)(nil), // 31: google.protobuf.Timestamp - (*duration.Duration)(nil), // 32: google.protobuf.Duration + (*IPList)(nil), // 24: daemon.IPList + (*Route)(nil), // 25: daemon.Route + (*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 + nil, // 32: daemon.Route.ResolvedIPsEntry + (*duration.Duration)(nil), // 33: google.protobuf.Duration + (*timestamp.Timestamp)(nil), // 34: google.protobuf.Timestamp } var file_daemon_proto_depIdxs = []int32{ - 19, // 0: daemon.StatusResponse.fullStatus:type_name -> daemon.FullStatus - 31, // 1: daemon.PeerState.connStatusUpdate:type_name -> google.protobuf.Timestamp - 31, // 2: daemon.PeerState.lastWireguardHandshake:type_name -> google.protobuf.Timestamp - 32, // 3: daemon.PeerState.latency:type_name -> google.protobuf.Duration - 16, // 4: daemon.FullStatus.managementState:type_name -> daemon.ManagementState - 15, // 5: daemon.FullStatus.signalState:type_name -> daemon.SignalState - 14, // 6: daemon.FullStatus.localPeerState:type_name -> daemon.LocalPeerState - 13, // 7: daemon.FullStatus.peers:type_name -> daemon.PeerState - 17, // 8: daemon.FullStatus.relays:type_name -> daemon.RelayState - 18, // 9: daemon.FullStatus.dns_servers:type_name -> daemon.NSGroupState - 24, // 10: daemon.ListRoutesResponse.routes:type_name -> daemon.Route - 0, // 11: daemon.GetLogLevelResponse.level:type_name -> daemon.LogLevel - 0, // 12: daemon.SetLogLevelRequest.level:type_name -> daemon.LogLevel - 1, // 13: daemon.DaemonService.Login:input_type -> daemon.LoginRequest - 3, // 14: daemon.DaemonService.WaitSSOLogin:input_type -> daemon.WaitSSOLoginRequest - 5, // 15: daemon.DaemonService.Up:input_type -> daemon.UpRequest - 7, // 16: daemon.DaemonService.Status:input_type -> daemon.StatusRequest - 9, // 17: daemon.DaemonService.Down:input_type -> daemon.DownRequest - 11, // 18: daemon.DaemonService.GetConfig:input_type -> daemon.GetConfigRequest - 20, // 19: daemon.DaemonService.ListRoutes:input_type -> daemon.ListRoutesRequest - 22, // 20: daemon.DaemonService.SelectRoutes:input_type -> daemon.SelectRoutesRequest - 22, // 21: daemon.DaemonService.DeselectRoutes:input_type -> daemon.SelectRoutesRequest - 25, // 22: daemon.DaemonService.DebugBundle:input_type -> daemon.DebugBundleRequest - 27, // 23: daemon.DaemonService.GetLogLevel:input_type -> daemon.GetLogLevelRequest - 29, // 24: daemon.DaemonService.SetLogLevel:input_type -> daemon.SetLogLevelRequest - 2, // 25: daemon.DaemonService.Login:output_type -> daemon.LoginResponse - 4, // 26: daemon.DaemonService.WaitSSOLogin:output_type -> daemon.WaitSSOLoginResponse - 6, // 27: daemon.DaemonService.Up:output_type -> daemon.UpResponse - 8, // 28: daemon.DaemonService.Status:output_type -> daemon.StatusResponse - 10, // 29: daemon.DaemonService.Down:output_type -> daemon.DownResponse - 12, // 30: daemon.DaemonService.GetConfig:output_type -> daemon.GetConfigResponse - 21, // 31: daemon.DaemonService.ListRoutes:output_type -> daemon.ListRoutesResponse - 23, // 32: daemon.DaemonService.SelectRoutes:output_type -> daemon.SelectRoutesResponse - 23, // 33: daemon.DaemonService.DeselectRoutes:output_type -> daemon.SelectRoutesResponse - 26, // 34: daemon.DaemonService.DebugBundle:output_type -> daemon.DebugBundleResponse - 28, // 35: daemon.DaemonService.GetLogLevel:output_type -> daemon.GetLogLevelResponse - 30, // 36: daemon.DaemonService.SetLogLevel:output_type -> daemon.SetLogLevelResponse - 25, // [25:37] is the sub-list for method output_type - 13, // [13:25] is the sub-list for method input_type - 13, // [13:13] is the sub-list for extension type_name - 13, // [13:13] is the sub-list for extension extendee - 0, // [0:13] is the sub-list for field type_name + 33, // 0: daemon.LoginRequest.dnsRouteInterval:type_name -> google.protobuf.Duration + 19, // 1: daemon.StatusResponse.fullStatus:type_name -> daemon.FullStatus + 34, // 2: daemon.PeerState.connStatusUpdate:type_name -> google.protobuf.Timestamp + 34, // 3: daemon.PeerState.lastWireguardHandshake:type_name -> google.protobuf.Timestamp + 33, // 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.ListRoutesResponse.routes:type_name -> daemon.Route + 32, // 12: daemon.Route.resolvedIPs:type_name -> daemon.Route.ResolvedIPsEntry + 0, // 13: daemon.GetLogLevelResponse.level:type_name -> daemon.LogLevel + 0, // 14: daemon.SetLogLevelRequest.level:type_name -> daemon.LogLevel + 24, // 15: daemon.Route.ResolvedIPsEntry.value:type_name -> daemon.IPList + 1, // 16: daemon.DaemonService.Login:input_type -> daemon.LoginRequest + 3, // 17: daemon.DaemonService.WaitSSOLogin:input_type -> daemon.WaitSSOLoginRequest + 5, // 18: daemon.DaemonService.Up:input_type -> daemon.UpRequest + 7, // 19: daemon.DaemonService.Status:input_type -> daemon.StatusRequest + 9, // 20: daemon.DaemonService.Down:input_type -> daemon.DownRequest + 11, // 21: daemon.DaemonService.GetConfig:input_type -> daemon.GetConfigRequest + 20, // 22: daemon.DaemonService.ListRoutes:input_type -> daemon.ListRoutesRequest + 22, // 23: daemon.DaemonService.SelectRoutes:input_type -> daemon.SelectRoutesRequest + 22, // 24: daemon.DaemonService.DeselectRoutes:input_type -> daemon.SelectRoutesRequest + 26, // 25: daemon.DaemonService.DebugBundle:input_type -> daemon.DebugBundleRequest + 28, // 26: daemon.DaemonService.GetLogLevel:input_type -> daemon.GetLogLevelRequest + 30, // 27: daemon.DaemonService.SetLogLevel:input_type -> daemon.SetLogLevelRequest + 2, // 28: daemon.DaemonService.Login:output_type -> daemon.LoginResponse + 4, // 29: daemon.DaemonService.WaitSSOLogin:output_type -> daemon.WaitSSOLoginResponse + 6, // 30: daemon.DaemonService.Up:output_type -> daemon.UpResponse + 8, // 31: daemon.DaemonService.Status:output_type -> daemon.StatusResponse + 10, // 32: daemon.DaemonService.Down:output_type -> daemon.DownResponse + 12, // 33: daemon.DaemonService.GetConfig:output_type -> daemon.GetConfigResponse + 21, // 34: daemon.DaemonService.ListRoutes:output_type -> daemon.ListRoutesResponse + 23, // 35: daemon.DaemonService.SelectRoutes:output_type -> daemon.SelectRoutesResponse + 23, // 36: daemon.DaemonService.DeselectRoutes:output_type -> daemon.SelectRoutesResponse + 27, // 37: daemon.DaemonService.DebugBundle:output_type -> daemon.DebugBundleResponse + 29, // 38: daemon.DaemonService.GetLogLevel:output_type -> daemon.GetLogLevelResponse + 31, // 39: daemon.DaemonService.SetLogLevel:output_type -> daemon.SetLogLevelResponse + 28, // [28:40] is the sub-list for method output_type + 16, // [16:28] is the sub-list for method input_type + 16, // [16:16] is the sub-list for extension type_name + 16, // [16:16] is the sub-list for extension extendee + 0, // [0:16] is the sub-list for field type_name } func init() { file_daemon_proto_init() } @@ -2673,7 +2767,7 @@ func file_daemon_proto_init() { } } file_daemon_proto_msgTypes[23].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Route); i { + switch v := v.(*IPList); i { case 0: return &v.state case 1: @@ -2685,7 +2779,7 @@ func file_daemon_proto_init() { } } file_daemon_proto_msgTypes[24].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DebugBundleRequest); i { + switch v := v.(*Route); i { case 0: return &v.state case 1: @@ -2697,7 +2791,7 @@ func file_daemon_proto_init() { } } file_daemon_proto_msgTypes[25].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DebugBundleResponse); i { + switch v := v.(*DebugBundleRequest); i { case 0: return &v.state case 1: @@ -2709,7 +2803,7 @@ func file_daemon_proto_init() { } } file_daemon_proto_msgTypes[26].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GetLogLevelRequest); i { + switch v := v.(*DebugBundleResponse); i { case 0: return &v.state case 1: @@ -2721,7 +2815,7 @@ func file_daemon_proto_init() { } } file_daemon_proto_msgTypes[27].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GetLogLevelResponse); i { + switch v := v.(*GetLogLevelRequest); i { case 0: return &v.state case 1: @@ -2733,7 +2827,7 @@ func file_daemon_proto_init() { } } file_daemon_proto_msgTypes[28].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*SetLogLevelRequest); i { + switch v := v.(*GetLogLevelResponse); i { case 0: return &v.state case 1: @@ -2745,6 +2839,18 @@ func file_daemon_proto_init() { } } file_daemon_proto_msgTypes[29].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SetLogLevelRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_daemon_proto_msgTypes[30].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*SetLogLevelResponse); i { case 0: return &v.state @@ -2764,7 +2870,7 @@ func file_daemon_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_daemon_proto_rawDesc, NumEnums: 1, - NumMessages: 30, + NumMessages: 32, NumExtensions: 0, NumServices: 1, }, diff --git a/client/proto/daemon.proto b/client/proto/daemon.proto index e39b08bc3..eb3efbb29 100644 --- a/client/proto/daemon.proto +++ b/client/proto/daemon.proto @@ -92,6 +92,8 @@ message LoginRequest { repeated string extraIFaceBlacklist = 17; optional bool networkMonitor = 18; + + optional google.protobuf.Duration dnsRouteInterval = 19; } message LoginResponse { @@ -233,10 +235,17 @@ message SelectRoutesRequest { message SelectRoutesResponse { } +message IPList { + repeated string ips = 1; +} + + message Route { string ID = 1; string network = 2; bool selected = 3; + repeated string domains = 4; + map resolvedIPs = 5; } message DebugBundleRequest { diff --git a/client/server/route.go b/client/server/route.go index 4c63cea93..d70e0dca3 100644 --- a/client/server/route.go +++ b/client/server/route.go @@ -9,17 +9,19 @@ import ( "golang.org/x/exp/maps" "github.com/netbirdio/netbird/client/proto" + "github.com/netbirdio/netbird/management/domain" "github.com/netbirdio/netbird/route" ) type selectRoute struct { NetID route.NetID Network netip.Prefix + Domains domain.List Selected bool } // ListRoutes returns a list of all available routes. -func (s *Server) ListRoutes(ctx context.Context, req *proto.ListRoutesRequest) (*proto.ListRoutesResponse, error) { +func (s *Server) ListRoutes(context.Context, *proto.ListRoutesRequest) (*proto.ListRoutesResponse, error) { s.mutex.Lock() defer s.mutex.Unlock() @@ -43,6 +45,7 @@ func (s *Server) ListRoutes(ctx context.Context, req *proto.ListRoutesRequest) ( route := &selectRoute{ NetID: id, Network: rt[0].Network, + Domains: rt[0].Domains, Selected: routeSelector.IsSelected(id), } routes = append(routes, route) @@ -63,13 +66,29 @@ func (s *Server) ListRoutes(ctx context.Context, req *proto.ListRoutesRequest) ( return iPrefix < jPrefix }) + resolvedDomains := s.statusRecorder.GetResolvedDomainsStates() var pbRoutes []*proto.Route for _, route := range routes { - pbRoutes = append(pbRoutes, &proto.Route{ - ID: string(route.NetID), - Network: route.Network.String(), - Selected: route.Selected, - }) + pbRoute := &proto.Route{ + ID: string(route.NetID), + Network: route.Network.String(), + Domains: route.Domains.ToSafeStringList(), + ResolvedIPs: map[string]*proto.IPList{}, + Selected: route.Selected, + } + + for _, domain := range route.Domains { + if prefixes, exists := resolvedDomains[domain]; exists { + var ipStrings []string + for _, prefix := range prefixes { + ipStrings = append(ipStrings, prefix.Addr().String()) + } + pbRoute.ResolvedIPs[string(domain)] = &proto.IPList{ + Ips: ipStrings, + } + } + } + pbRoutes = append(pbRoutes, pbRoute) } return &proto.ListRoutesResponse{ diff --git a/client/server/server.go b/client/server/server.go index a59cffd14..482d02efc 100644 --- a/client/server/server.go +++ b/client/server/server.go @@ -365,6 +365,12 @@ func (s *Server) Login(callerCtx context.Context, msg *proto.LoginRequest) (*pro s.latestConfigInput.ExtraIFaceBlackList = msg.ExtraIFaceBlacklist } + if msg.DnsRouteInterval != nil { + duration := msg.DnsRouteInterval.AsDuration() + inputConfig.DNSRouteInterval = &duration + s.latestConfigInput.DNSRouteInterval = &duration + } + s.mutex.Unlock() if msg.OptionalPreSharedKey != nil { diff --git a/client/ssh/window_freebsd.go b/client/ssh/window_freebsd.go new file mode 100644 index 000000000..ef4848341 --- /dev/null +++ b/client/ssh/window_freebsd.go @@ -0,0 +1,10 @@ +//go:build freebsd + +package ssh + +import ( + "os" +) + +func setWinSize(file *os.File, width, height int) { +} diff --git a/client/system/info.go b/client/system/info.go index e2e057206..4b0970ceb 100644 --- a/client/system/info.go +++ b/client/system/info.go @@ -8,6 +8,7 @@ import ( "google.golang.org/grpc/metadata" + "github.com/netbirdio/netbird/management/proto" "github.com/netbirdio/netbird/version" ) @@ -33,6 +34,12 @@ type Environment struct { Platform string } +type File struct { + Path string + Exist bool + ProcessIsRunning bool +} + // Info is an object that contains machine information // Most of the code is taken from https://github.com/matishsiao/goInfo type Info struct { @@ -51,6 +58,7 @@ type Info struct { SystemProductName string SystemManufacturer string Environment Environment + Files []File } // extractUserAgent extracts Netbird's agent (client) name and version from the outgoing context @@ -132,3 +140,21 @@ func isDuplicated(addresses []NetworkAddress, addr NetworkAddress) bool { } return false } + +// GetInfoWithChecks retrieves and parses the system information with applied checks. +func GetInfoWithChecks(ctx context.Context, checks []*proto.Checks) (*Info, error) { + processCheckPaths := make([]string, 0) + for _, check := range checks { + processCheckPaths = append(processCheckPaths, check.GetFiles()...) + } + + files, err := checkFileAndProcess(processCheckPaths) + if err != nil { + return nil, err + } + + info := GetInfo(ctx) + info.Files = files + + return info, nil +} diff --git a/client/system/info_android.go b/client/system/info_android.go index 7f5dd371b..65657f4be 100644 --- a/client/system/info_android.go +++ b/client/system/info_android.go @@ -44,6 +44,11 @@ func GetInfo(ctx context.Context) *Info { return gio } +// checkFileAndProcess checks if the file path exists and if a process is running at that path. +func checkFileAndProcess(paths []string) ([]File, error) { + return []File{}, nil +} + func uname() []string { res := run("/system/bin/uname", "-a") return strings.Split(res, " ") diff --git a/client/system/info_freebsd.go b/client/system/info_freebsd.go index b44fdee7c..454e58a0b 100644 --- a/client/system/info_freebsd.go +++ b/client/system/info_freebsd.go @@ -1,15 +1,18 @@ +//go:build freebsd + package system import ( "bytes" "context" - "fmt" "os" "os/exec" "runtime" "strings" "time" + log "github.com/sirupsen/logrus" + "github.com/netbirdio/netbird/client/system/detect_cloud" "github.com/netbirdio/netbird/client/system/detect_platform" "github.com/netbirdio/netbird/version" @@ -22,8 +25,8 @@ func GetInfo(ctx context.Context) *Info { out = _getInfo() time.Sleep(500 * time.Millisecond) } - osStr := strings.Replace(out, "\n", "", -1) - osStr = strings.Replace(osStr, "\r\n", "", -1) + osStr := strings.ReplaceAll(out, "\n", "") + osStr = strings.ReplaceAll(osStr, "\r\n", "") osInfo := strings.Split(osStr, " ") env := Environment{ @@ -31,14 +34,23 @@ func GetInfo(ctx context.Context) *Info { Platform: detect_platform.Detect(ctx), } - gio := &Info{Kernel: osInfo[0], Platform: runtime.GOARCH, OS: osInfo[2], GoOS: runtime.GOOS, CPUs: runtime.NumCPU(), KernelVersion: osInfo[1], Environment: env} + osName, osVersion := readOsReleaseFile() systemHostname, _ := os.Hostname() - gio.Hostname = extractDeviceName(ctx, systemHostname) - gio.WiretrusteeVersion = version.NetbirdVersion() - gio.UIVersion = extractUserAgent(ctx) - return gio + 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, + } } func _getInfo() string { @@ -50,7 +62,8 @@ func _getInfo() string { cmd.Stderr = &stderr err := cmd.Run() if err != nil { - fmt.Println("getInfo:", err) + log.Warnf("getInfo: %s", err) } + return out.String() } diff --git a/client/system/info_ios.go b/client/system/info_ios.go index e1c291ef5..3dbf50e1e 100644 --- a/client/system/info_ios.go +++ b/client/system/info_ios.go @@ -25,6 +25,11 @@ func GetInfo(ctx context.Context) *Info { return gio } +// checkFileAndProcess checks if the file path exists and if a process is running at that path. +func checkFileAndProcess(paths []string) ([]File, error) { + return []File{}, nil +} + // extractOsVersion extracts operating system version from context or returns the default func extractOsVersion(ctx context.Context, defaultName string) string { v, ok := ctx.Value(OsVersionCtxKey).(string) diff --git a/client/system/info_linux.go b/client/system/info_linux.go index 652bc1115..1c5405d4d 100644 --- a/client/system/info_linux.go +++ b/client/system/info_linux.go @@ -28,28 +28,11 @@ func GetInfo(ctx context.Context) *Info { time.Sleep(500 * time.Millisecond) } - releaseInfo := _getReleaseInfo() - for strings.Contains(info, "broken pipe") { - releaseInfo = _getReleaseInfo() - time.Sleep(500 * time.Millisecond) - } - - osRelease := strings.Split(releaseInfo, "\n") - var osName string - var osVer string - for _, s := range osRelease { - if strings.HasPrefix(s, "NAME=") { - osName = strings.Split(s, "=")[1] - osName = strings.ReplaceAll(osName, "\"", "") - } else if strings.HasPrefix(s, "VERSION_ID=") { - osVer = strings.Split(s, "=")[1] - osVer = strings.ReplaceAll(osVer, "\"", "") - } - } - osStr := strings.ReplaceAll(info, "\n", "") osStr = strings.ReplaceAll(osStr, "\r\n", "") osInfo := strings.Split(osStr, " ") + + osName, osVersion := readOsReleaseFile() if osName == "" { osName = osInfo[3] } @@ -72,7 +55,7 @@ func GetInfo(ctx context.Context) *Info { Kernel: osInfo[0], Platform: osInfo[2], OS: osName, - OSVersion: osVer, + OSVersion: osVersion, Hostname: extractDeviceName(ctx, systemHostname), GoOS: runtime.GOOS, CPUs: runtime.NumCPU(), @@ -103,20 +86,6 @@ func _getInfo() string { return out.String() } -func _getReleaseInfo() string { - cmd := exec.Command("cat", "/etc/os-release") - cmd.Stdin = strings.NewReader("some") - var out bytes.Buffer - var stderr bytes.Buffer - cmd.Stdout = &out - cmd.Stderr = &stderr - err := cmd.Run() - if err != nil { - log.Warnf("geucwReleaseInfo: %s", err) - } - return out.String() -} - func sysInfo() (serialNumber string, productName string, manufacturer string) { var si sysinfo.SysInfo si.GetSysInfo() diff --git a/client/system/osrelease_unix.go b/client/system/osrelease_unix.go new file mode 100644 index 000000000..851633248 --- /dev/null +++ b/client/system/osrelease_unix.go @@ -0,0 +1,38 @@ +//go:build (linux && !android) || freebsd + +package system + +import ( + "bufio" + "os" + "strings" + + log "github.com/sirupsen/logrus" +) + +func readOsReleaseFile() (osName string, osVer string) { + file, err := os.Open("/etc/os-release") + if err != nil { + log.Warnf("failed to open file /etc/os-release: %s", err) + return "", "" + } + defer file.Close() + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := scanner.Text() + if strings.HasPrefix(line, "NAME=") { + osName = strings.ReplaceAll(strings.Split(line, "=")[1], "\"", "") + continue + } + if strings.HasPrefix(line, "VERSION_ID=") { + osVer = strings.ReplaceAll(strings.Split(line, "=")[1], "\"", "") + continue + } + + if osName != "" && osVer != "" { + break + } + } + return +} diff --git a/client/system/process.go b/client/system/process.go new file mode 100644 index 000000000..b44e66ec6 --- /dev/null +++ b/client/system/process.go @@ -0,0 +1,58 @@ +//go:build windows || (linux && !android) || (darwin && !ios) + +package system + +import ( + "os" + "slices" + + "github.com/shirou/gopsutil/v3/process" +) + +// getRunningProcesses returns a list of running process paths. +func getRunningProcesses() ([]string, error) { + processes, err := process.Processes() + if err != nil { + return nil, err + } + + processMap := make(map[string]bool) + for _, p := range processes { + path, _ := p.Exe() + if path != "" { + processMap[path] = true + } + } + + uniqueProcesses := make([]string, 0, len(processMap)) + for p := range processMap { + uniqueProcesses = append(uniqueProcesses, p) + } + + return uniqueProcesses, nil +} + +// checkFileAndProcess checks if the file path exists and if a process is running at that path. +func checkFileAndProcess(paths []string) ([]File, error) { + files := make([]File, len(paths)) + if len(paths) == 0 { + return files, nil + } + + runningProcesses, err := getRunningProcesses() + if err != nil { + return nil, err + } + + for i, path := range paths { + file := File{Path: path} + + _, err := os.Stat(path) + file.Exist = !os.IsNotExist(err) + + file.ProcessIsRunning = slices.Contains(runningProcesses, path) + files[i] = file + } + + return files, nil +} diff --git a/client/ui/client_ui.go b/client/ui/client_ui.go index 7b1e0320a..75f158601 100644 --- a/client/ui/client_ui.go +++ b/client/ui/client_ui.go @@ -1,4 +1,4 @@ -//go:build !(linux && 386) +//go:build !(linux && 386) && !freebsd package main diff --git a/client/ui/route.go b/client/ui/route.go index 0ac58e5d5..cb4399254 100644 --- a/client/ui/route.go +++ b/client/ui/route.go @@ -20,7 +20,7 @@ import ( func (s *serviceClient) showRoutesUI() { s.wRoutes = s.app.NewWindow("NetBird Routes") - grid := container.New(layout.NewGridLayout(2)) + grid := container.New(layout.NewGridLayout(3)) go s.updateRoutes(grid) routeCheckContainer := container.NewVBox() routeCheckContainer.Add(grid) @@ -61,14 +61,16 @@ func (s *serviceClient) updateRoutes(grid *fyne.Container) { grid.Objects = nil idHeader := widget.NewLabelWithStyle(" ID", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}) - networkHeader := widget.NewLabelWithStyle("Network", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}) + networkHeader := widget.NewLabelWithStyle("Network/Domains", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}) + resolvedIPsHeader := widget.NewLabelWithStyle("Resolved IPs", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}) grid.Add(idHeader) grid.Add(networkHeader) + grid.Add(resolvedIPsHeader) for _, route := range routes { r := route - checkBox := widget.NewCheck(r.ID, func(checked bool) { + checkBox := widget.NewCheck(r.GetID(), func(checked bool) { s.selectRoute(r.ID, checked) }) checkBox.Checked = route.Selected @@ -76,10 +78,31 @@ func (s *serviceClient) updateRoutes(grid *fyne.Container) { checkBox.Refresh() grid.Add(checkBox) - grid.Add(widget.NewLabel(r.Network)) + network := r.GetNetwork() + domains := r.GetDomains() + if len(domains) > 0 { + network = strings.Join(domains, ", ") + } + grid.Add(widget.NewLabel(network)) + + if len(domains) > 0 { + var resolvedIPsList []string + for _, domain := range r.GetDomains() { + if ipList, exists := r.GetResolvedIPs()[domain]; exists { + resolvedIPsList = append(resolvedIPsList, fmt.Sprintf("%s: %s", domain, strings.Join(ipList.GetIps(), ", "))) + } + } + // TODO: limit width + resolvedIPsLabel := widget.NewLabel(strings.Join(resolvedIPsList, ", ")) + grid.Add(resolvedIPsLabel) + } else { + grid.Add(widget.NewLabel("")) + + } } s.wRoutes.Content().Refresh() + grid.Refresh() } func (s *serviceClient) fetchRoutes() ([]*proto.Route, error) { diff --git a/go.mod b/go.mod index a300eb1e5..3753fe651 100644 --- a/go.mod +++ b/go.mod @@ -68,6 +68,7 @@ require ( github.com/pion/turn/v3 v3.0.1 github.com/prometheus/client_golang v1.19.1 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 github.com/stretchr/testify v1.9.0 github.com/testcontainers/testcontainers-go v0.31.0 @@ -176,7 +177,6 @@ require ( github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/common v0.53.0 // indirect github.com/prometheus/procfs v0.15.0 // indirect - github.com/shirou/gopsutil/v3 v3.24.4 // indirect github.com/shoenig/go-m1cpu v0.1.6 // indirect github.com/spf13/cast v1.5.0 // indirect github.com/srwiley/oksvg v0.0.0-20200311192757-870daf9aa564 // indirect diff --git a/iface/address.go b/iface/address.go index 2920d009f..5ff4fbc06 100644 --- a/iface/address.go +++ b/iface/address.go @@ -23,24 +23,6 @@ func parseWGAddress(address string) (WGAddress, error) { }, nil } -// Masked returns the WGAddress with the IP address part masked according to its network mask. -func (addr WGAddress) Masked() WGAddress { - ip := addr.IP.To4() - if ip == nil { - ip = addr.IP.To16() - } - - maskedIP := make(net.IP, len(ip)) - for i := range ip { - maskedIP[i] = ip[i] & addr.Network.Mask[i] - } - - return WGAddress{ - IP: maskedIP, - Network: addr.Network, - } -} - func (addr WGAddress) String() string { maskSize, _ := addr.Network.Mask.Size() return fmt.Sprintf("%s/%d", addr.IP.String(), maskSize) diff --git a/iface/freebsd/errors.go b/iface/freebsd/errors.go new file mode 100644 index 000000000..e2c6a2aa9 --- /dev/null +++ b/iface/freebsd/errors.go @@ -0,0 +1,8 @@ +package freebsd + +import "errors" + +var ( + ErrDoesNotExist = errors.New("does not exist") + ErrNameDoesNotMatch = errors.New("name does not match") +) diff --git a/iface/freebsd/iface.go b/iface/freebsd/iface.go new file mode 100644 index 000000000..d32fa6436 --- /dev/null +++ b/iface/freebsd/iface.go @@ -0,0 +1,108 @@ +package freebsd + +import ( + "bufio" + "fmt" + "strconv" + "strings" +) + +type iface struct { + Name string + MTU int + Group string + IPAddrs []string +} + +func parseError(output []byte) error { + // TODO: implement without allocations + lines := string(output) + + if strings.Contains(lines, "does not exist") { + return ErrDoesNotExist + } + + return nil +} + +func parseIfconfigOutput(output []byte) (*iface, error) { + // TODO: implement without allocations + lines := string(output) + + scanner := bufio.NewScanner(strings.NewReader(lines)) + + var name, mtu, group string + var ips []string + + for scanner.Scan() { + line := scanner.Text() + + // If line contains ": flags", it's a line with interface information + if strings.Contains(line, ": flags") { + parts := strings.Fields(line) + if len(parts) < 4 { + return nil, fmt.Errorf("failed to parse line: %s", line) + } + name = strings.TrimSuffix(parts[0], ":") + if strings.Contains(line, "mtu") { + mtuIndex := 0 + for i, part := range parts { + if part == "mtu" { + mtuIndex = i + break + } + } + mtu = parts[mtuIndex+1] + } + } + + // If line contains "groups:", it's a line with interface group + if strings.Contains(line, "groups:") { + parts := strings.Fields(line) + if len(parts) < 2 { + return nil, fmt.Errorf("failed to parse line: %s", line) + } + group = parts[1] + } + + // If line contains "inet ", it's a line with IP address + if strings.Contains(line, "inet ") { + parts := strings.Fields(line) + if len(parts) < 2 { + return nil, fmt.Errorf("failed to parse line: %s", line) + } + ips = append(ips, parts[1]) + } + } + + if name == "" { + return nil, fmt.Errorf("interface name not found in ifconfig output") + } + + mtuInt, err := strconv.Atoi(mtu) + if err != nil { + return nil, fmt.Errorf("failed to parse MTU: %w", err) + } + + return &iface{ + Name: name, + MTU: mtuInt, + Group: group, + IPAddrs: ips, + }, nil +} + +func parseIFName(output []byte) (string, error) { + // TODO: implement without allocations + lines := strings.Split(string(output), "\n") + if len(lines) == 0 || lines[0] == "" { + return "", fmt.Errorf("no output returned") + } + + fields := strings.Fields(lines[0]) + if len(fields) > 1 { + return "", fmt.Errorf("invalid output") + } + + return fields[0], nil +} diff --git a/iface/freebsd/iface_internal_test.go b/iface/freebsd/iface_internal_test.go new file mode 100644 index 000000000..f933ae634 --- /dev/null +++ b/iface/freebsd/iface_internal_test.go @@ -0,0 +1,76 @@ +package freebsd + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestParseIfconfigOutput(t *testing.T) { + testOutput := `wg1: flags=8080 metric 0 mtu 1420 + options=80000 + groups: wg + nd6 options=109` + + expected := &iface{ + Name: "wg1", + MTU: 1420, + Group: "wg", + } + + result, err := parseIfconfigOutput(([]byte)(testOutput)) + if err != nil { + t.Errorf("Error parsing ifconfig output: %v", err) + return + } + + assert.Equal(t, expected.Name, result.Name, "Name should match") + assert.Equal(t, expected.MTU, result.MTU, "MTU should match") + assert.Equal(t, expected.Group, result.Group, "Group should match") +} + +func TestParseIFName(t *testing.T) { + tests := []struct { + name string + output string + expected string + expectedErr error + }{ + { + name: "ValidOutput", + output: "eth0\n", + expected: "eth0", + }, + { + name: "ValidOutputOneLine", + output: "eth0", + expected: "eth0", + }, + { + name: "EmptyOutput", + output: "", + expectedErr: fmt.Errorf("no output returned"), + }, + { + name: "InvalidOutput", + output: "This is an invalid output\n", + expectedErr: fmt.Errorf("invalid output"), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + result, err := parseIFName(([]byte)(test.output)) + + assert.Equal(t, test.expected, result, "Interface names should match") + + if test.expectedErr != nil { + assert.NotNil(t, err, "Error should not be nil") + assert.EqualError(t, err, test.expectedErr.Error(), "Error messages should match") + } else { + assert.Nil(t, err, "Error should be nil") + } + }) + } +} diff --git a/iface/freebsd/link.go b/iface/freebsd/link.go new file mode 100644 index 000000000..b7924f04b --- /dev/null +++ b/iface/freebsd/link.go @@ -0,0 +1,239 @@ +package freebsd + +import ( + "bytes" + "errors" + "fmt" + "os/exec" + "strconv" + + log "github.com/sirupsen/logrus" +) + +const wgIFGroup = "wg" + +// Link represents a network interface. +type Link struct { + name string +} + +func NewLink(name string) *Link { + return &Link{ + name: name, + } +} + +// LinkByName retrieves a network interface by its name. +func LinkByName(name string) (*Link, error) { + out, err := exec.Command("ifconfig", name).CombinedOutput() + if err != nil { + if pErr := parseError(out); pErr != nil { + return nil, pErr + } + + log.Debugf("ifconfig out: %s", out) + + return nil, fmt.Errorf("command run: %w", err) + } + + i, err := parseIfconfigOutput(out) + if err != nil { + return nil, fmt.Errorf("parse ifconfig output: %w", err) + } + + if i.Name != name { + return nil, ErrNameDoesNotMatch + } + + return &Link{name: i.Name}, nil +} + +// Recreate - create new interface, remove current before create if it exists +func (l *Link) Recreate() error { + ok, err := l.isExist() + if err != nil { + return fmt.Errorf("is exist: %w", err) + } + + if ok { + if err := l.del(l.name); err != nil { + return fmt.Errorf("del: %w", err) + } + } + + return l.Add() +} + +// Add creates a new network interface. +func (l *Link) Add() error { + parsedName, err := l.create(wgIFGroup) + if err != nil { + return fmt.Errorf("create link: %w", err) + } + + if parsedName == l.name { + return nil + } + + parsedName, err = l.rename(parsedName, l.name) + if err != nil { + errDel := l.del(parsedName) + if errDel != nil { + return fmt.Errorf("del on rename link: %w: %w", err, errDel) + } + + return fmt.Errorf("rename link: %w", err) + } + + return nil +} + +// Del removes an existing network interface. +func (l *Link) Del() error { + return l.del(l.name) +} + +// SetMTU sets the MTU of the network interface. +func (l *Link) SetMTU(mtu int) error { + return l.setMTU(mtu) +} + +// AssignAddr assigns an IP address and netmask to the network interface. +func (l *Link) AssignAddr(ip, netmask string) error { + return l.setAddr(ip, netmask) +} + +func (l *Link) Up() error { + return l.up(l.name) +} + +func (l *Link) Down() error { + return l.down(l.name) +} + +func (l *Link) isExist() (bool, error) { + _, err := LinkByName(l.name) + if errors.Is(err, ErrDoesNotExist) { + return false, nil + } + + if err != nil { + return false, fmt.Errorf("link by name: %w", err) + } + + return true, nil +} + +func (l *Link) create(groupName string) (string, error) { + cmd := exec.Command("ifconfig", groupName, "create") + + output, err := cmd.CombinedOutput() + if err != nil { + log.Debugf("ifconfig out: %s", output) + + return "", fmt.Errorf("create %s interface: %w", groupName, err) + } + + interfaceName, err := parseIFName(output) + if err != nil { + return "", fmt.Errorf("parse interface name: %w", err) + } + + return interfaceName, nil +} + +func (l *Link) rename(oldName, newName string) (string, error) { + cmd := exec.Command("ifconfig", oldName, "name", newName) + + output, err := cmd.CombinedOutput() + if err != nil { + log.Debugf("ifconfig out: %s", output) + + return "", fmt.Errorf("change name %q -> %q: %w", oldName, newName, err) + } + + interfaceName, err := parseIFName(output) + if err != nil { + return "", fmt.Errorf("parse new name: %w", err) + } + + return interfaceName, nil +} + +func (l *Link) del(name string) error { + var stderr bytes.Buffer + + cmd := exec.Command("ifconfig", name, "destroy") + cmd.Stderr = &stderr + + err := cmd.Run() + if err != nil { + log.Debugf("ifconfig out: %s", stderr.String()) + + return fmt.Errorf("destroy %s interface: %w", name, err) + } + + return nil +} + +func (l *Link) setMTU(mtu int) error { + var stderr bytes.Buffer + + cmd := exec.Command("ifconfig", l.name, "mtu", strconv.Itoa(mtu)) + cmd.Stderr = &stderr + + if err := cmd.Run(); err != nil { + log.Debugf("ifconfig out: %s", stderr.String()) + + return fmt.Errorf("set interface mtu: %w", err) + } + + return nil +} + +func (l *Link) setAddr(ip, netmask string) error { + var stderr bytes.Buffer + + cmd := exec.Command("ifconfig", l.name, "inet", ip, "netmask", netmask) + cmd.Stderr = &stderr + + if err := cmd.Run(); err != nil { + log.Debugf("ifconfig out: %s", stderr.String()) + + return fmt.Errorf("set interface addr: %w", err) + } + + return nil +} + +func (l *Link) up(name string) error { + var stderr bytes.Buffer + + cmd := exec.Command("ifconfig", name, "up") + cmd.Stderr = &stderr + + err := cmd.Run() + if err != nil { + log.Debugf("ifconfig out: %s", stderr.String()) + + return fmt.Errorf("up %s interface: %w", name, err) + } + + return nil +} + +func (l *Link) down(name string) error { + var stderr bytes.Buffer + + cmd := exec.Command("ifconfig", name, "down") + cmd.Stderr = &stderr + + err := cmd.Run() + if err != nil { + log.Debugf("ifconfig out: %s", stderr.String()) + + return fmt.Errorf("down %s interface: %w", name, err) + } + + return nil +} diff --git a/iface/iface.go b/iface/iface.go index 3ae40ad4c..928077a3d 100644 --- a/iface/iface.go +++ b/iface/iface.go @@ -48,6 +48,19 @@ func (w *WGIface) Address() WGAddress { return w.tun.WgAddress() } +// ToInterface returns the net.Interface for the Wireguard interface +func (r *WGIface) ToInterface() *net.Interface { + name := r.tun.DeviceName() + intf, err := net.InterfaceByName(name) + if err != nil { + log.Warnf("Failed to get interface by name %s: %v", name, err) + intf = &net.Interface{ + Name: name, + } + } + return intf +} + // Up configures a Wireguard interface // The interface must exist before calling this method (e.g. call interface.Create() before) func (w *WGIface) Up() (*bind.UniversalUDPMuxDefault, error) { @@ -94,7 +107,7 @@ func (w *WGIface) AddAllowedIP(peerKey string, allowedIP string) error { w.mu.Lock() defer w.mu.Unlock() - log.Debugf("adding allowed IP to interface %s and peer %s: allowed IP %s ", w.tun.DeviceName(), peerKey, allowedIP) + log.Debugf("Adding allowed IP to interface %s and peer %s: allowed IP %s ", w.tun.DeviceName(), peerKey, allowedIP) return w.configurer.addAllowedIP(peerKey, allowedIP) } @@ -103,7 +116,7 @@ func (w *WGIface) RemoveAllowedIP(peerKey string, allowedIP string) error { w.mu.Lock() defer w.mu.Unlock() - log.Debugf("removing allowed IP from interface %s and peer %s: allowed IP %s ", w.tun.DeviceName(), peerKey, allowedIP) + log.Debugf("Removing allowed IP from interface %s and peer %s: allowed IP %s ", w.tun.DeviceName(), peerKey, allowedIP) return w.configurer.removeAllowedIP(peerKey, allowedIP) } diff --git a/iface/iface_create.go b/iface/iface_create.go index 86c3f320f..cfc555f2e 100644 --- a/iface/iface_create.go +++ b/iface/iface_create.go @@ -1,5 +1,4 @@ //go:build !android -// +build !android package iface diff --git a/iface/iface_darwin.go b/iface/iface_darwin.go index 4d62c6af6..d68f562cd 100644 --- a/iface/iface_darwin.go +++ b/iface/iface_darwin.go @@ -1,5 +1,4 @@ //go:build !ios -// +build !ios package iface diff --git a/iface/iface_ios.go b/iface/iface_ios.go index b22e1a6a4..39032e6bd 100644 --- a/iface/iface_ios.go +++ b/iface/iface_ios.go @@ -1,5 +1,4 @@ //go:build ios -// +build ios package iface diff --git a/iface/iface_linux.go b/iface/iface_unix.go similarity index 89% rename from iface/iface_linux.go rename to iface/iface_unix.go index 62ae0f0de..b378abef3 100644 --- a/iface/iface_linux.go +++ b/iface/iface_unix.go @@ -1,10 +1,10 @@ -//go:build !android -// +build !android +//go:build (linux && !android) || freebsd package iface import ( "fmt" + "runtime" "github.com/pion/transport/v3" @@ -43,5 +43,5 @@ func NewWGIFace(iFaceName string, address string, wgPort int, wgPrivKey string, // CreateOnAndroid this function make sense on mobile only func (w *WGIface) CreateOnAndroid([]string, string, []string) error { - return fmt.Errorf("this function has not implemented on this platform") + return fmt.Errorf("CreateOnAndroid function has not implemented on %s platform", runtime.GOOS) } diff --git a/iface/module.go b/iface/module.go index 7f337201d..ca70cf3c7 100644 --- a/iface/module.go +++ b/iface/module.go @@ -1,5 +1,4 @@ -//go:build !linux || android -// +build !linux android +//go:build (!linux && !freebsd) || android package iface diff --git a/iface/module_freebsd.go b/iface/module_freebsd.go new file mode 100644 index 000000000..00ad882c2 --- /dev/null +++ b/iface/module_freebsd.go @@ -0,0 +1,18 @@ +package iface + +// WireGuardModuleIsLoaded check if kernel support wireguard +func WireGuardModuleIsLoaded() bool { + // Despite the fact FreeBSD natively support Wireguard (https://github.com/WireGuard/wireguard-freebsd) + // we are currently do not use it, since it is required to add wireguard kernel support to + // - https://github.com/netbirdio/netbird/tree/main/sharedsock + // - https://github.com/mdlayher/socket + // TODO: implement kernel space + return false +} + +// tunModuleIsLoaded check if tun module exist, if is not attempt to load it +func tunModuleIsLoaded() bool { + // Assume tun supported by freebsd kernel by default + // TODO: implement check for module loaded in kernel or build-it + return true +} diff --git a/iface/name.go b/iface/name.go index 05d0299d3..706cb65ad 100644 --- a/iface/name.go +++ b/iface/name.go @@ -1,5 +1,4 @@ -//go:build linux || windows -// +build linux windows +//go:build linux || windows || freebsd package iface diff --git a/iface/name_darwin.go b/iface/name_darwin.go index c80f790f5..a4016ce15 100644 --- a/iface/name_darwin.go +++ b/iface/name_darwin.go @@ -1,5 +1,4 @@ //go:build darwin -// +build darwin package iface diff --git a/iface/tun_kernel_linux.go b/iface/tun_kernel_unix.go similarity index 64% rename from iface/tun_kernel_linux.go rename to iface/tun_kernel_unix.go index 12adcdf73..db47b68cf 100644 --- a/iface/tun_kernel_linux.go +++ b/iface/tun_kernel_unix.go @@ -1,4 +1,4 @@ -//go:build linux && !android +//go:build (linux && !android) || freebsd package iface @@ -6,11 +6,9 @@ import ( "context" "fmt" "net" - "os" "github.com/pion/transport/v3" log "github.com/sirupsen/logrus" - "github.com/vishvananda/netlink" "github.com/netbirdio/netbird/iface/bind" "github.com/netbirdio/netbird/sharedsock" @@ -32,6 +30,8 @@ type tunKernelDevice struct { } func newTunDevice(name string, address WGAddress, wgPort int, key string, mtu int, transportNet transport.Net) wgTunDevice { + checkUser() + ctx, cancel := context.WithCancel(context.Background()) return &tunKernelDevice{ ctx: ctx, @@ -48,53 +48,29 @@ func newTunDevice(name string, address WGAddress, wgPort int, key string, mtu in func (t *tunKernelDevice) Create() (wgConfigurer, error) { link := newWGLink(t.name) - // check if interface exists - l, err := netlink.LinkByName(t.name) - if err != nil { - switch err.(type) { - case netlink.LinkNotFoundError: - break - default: - return nil, err - } - } - - // remove if interface exists - if l != nil { - err = netlink.LinkDel(link) - if err != nil { - return nil, err - } - } - - log.Debugf("adding device: %s", t.name) - err = netlink.LinkAdd(link) - if os.IsExist(err) { - log.Infof("interface %s already exists. Will reuse.", t.name) - } else if err != nil { - return nil, err + if err := link.recreate(); err != nil { + return nil, fmt.Errorf("recreate: %w", err) } t.link = link - err = t.assignAddr() - if err != nil { - return nil, err + if err := t.assignAddr(); err != nil { + return nil, fmt.Errorf("assign addr: %w", err) } - // todo do a discovery + // TODO: do a MTU discovery log.Debugf("setting MTU: %d interface: %s", t.mtu, t.name) - err = netlink.LinkSetMTU(link, t.mtu) - if err != nil { - log.Errorf("error setting MTU on interface: %s", t.name) - return nil, err + + if err := link.setMTU(t.mtu); err != nil { + return nil, fmt.Errorf("set mtu: %w", err) } configurer := newWGConfigurer(t.name) - err = configurer.configureInterface(t.key, t.wgPort) - if err != nil { + + if err := configurer.configureInterface(t.key, t.wgPort); err != nil { return nil, err } + return configurer, nil } @@ -108,9 +84,10 @@ func (t *tunKernelDevice) Up() (*bind.UniversalUDPMuxDefault, error) { } log.Debugf("bringing up interface: %s", t.name) - err := netlink.LinkSetUp(t.link) - if err != nil { + + if err := t.link.up(); err != nil { log.Errorf("error bringing up interface: %s", t.name) + return nil, err } @@ -178,32 +155,5 @@ func (t *tunKernelDevice) Wrapper() *DeviceWrapper { // assignAddr Adds IP address to the tunnel interface func (t *tunKernelDevice) assignAddr() error { - link := newWGLink(t.name) - - //delete existing addresses - list, err := netlink.AddrList(link, 0) - if err != nil { - return err - } - if len(list) > 0 { - for _, a := range list { - addr := a - err = netlink.AddrDel(link, &addr) - if err != nil { - return err - } - } - } - - log.Debugf("adding address %s to interface: %s", t.address.String(), t.name) - addr, _ := netlink.ParseAddr(t.address.String()) - err = netlink.AddrAdd(link, addr) - if os.IsExist(err) { - log.Infof("interface %s already has the address: %s", t.name, t.address.String()) - } else if err != nil { - return err - } - // On linux, the link must be brought up - err = netlink.LinkSetUp(link) - return err + return t.link.assignAddr(t.address) } diff --git a/iface/tun_link_freebsd.go b/iface/tun_link_freebsd.go new file mode 100644 index 000000000..be7921fdb --- /dev/null +++ b/iface/tun_link_freebsd.go @@ -0,0 +1,80 @@ +package iface + +import ( + "fmt" + + "github.com/netbirdio/netbird/iface/freebsd" + log "github.com/sirupsen/logrus" +) + +type wgLink struct { + name string + link *freebsd.Link +} + +func newWGLink(name string) *wgLink { + link := freebsd.NewLink(name) + + return &wgLink{ + name: name, + link: link, + } +} + +// Type returns the interface type +func (l *wgLink) Type() string { + return "wireguard" +} + +// Close deletes the link interface +func (l *wgLink) Close() error { + return l.link.Del() +} + +func (l *wgLink) recreate() error { + if err := l.link.Recreate(); err != nil { + return fmt.Errorf("recreate: %w", err) + } + + return nil +} + +func (l *wgLink) setMTU(mtu int) error { + if err := l.link.SetMTU(mtu); err != nil { + return fmt.Errorf("set mtu: %w", err) + } + + return nil +} + +func (l *wgLink) up() error { + if err := l.link.Up(); err != nil { + return fmt.Errorf("up: %w", err) + } + + return nil +} + +func (l *wgLink) assignAddr(address WGAddress) error { + link, err := freebsd.LinkByName(l.name) + if err != nil { + return fmt.Errorf("link by name: %w", err) + } + + ip := address.IP.String() + mask := "0x" + address.Network.Mask.String() + + log.Infof("assign addr %s mask %s to %s interface", ip, mask, l.name) + + err = link.AssignAddr(ip, mask) + if err != nil { + return fmt.Errorf("assign addr: %w", err) + } + + err = link.Up() + if err != nil { + return fmt.Errorf("up: %w", err) + } + + return nil +} diff --git a/iface/tun_link_linux.go b/iface/tun_link_linux.go index ab28b7e38..3ce644e84 100644 --- a/iface/tun_link_linux.go +++ b/iface/tun_link_linux.go @@ -2,7 +2,13 @@ package iface -import "github.com/vishvananda/netlink" +import ( + "fmt" + "os" + + log "github.com/sirupsen/logrus" + "github.com/vishvananda/netlink" +) type wgLink struct { attrs *netlink.LinkAttrs @@ -31,3 +37,97 @@ func (l *wgLink) Type() string { func (l *wgLink) Close() error { return netlink.LinkDel(l) } + +func (l *wgLink) recreate() error { + name := l.attrs.Name + + // check if interface exists + link, err := netlink.LinkByName(name) + if err != nil { + switch err.(type) { + case netlink.LinkNotFoundError: + break + default: + return fmt.Errorf("link by name: %w", err) + } + } + + // remove if interface exists + if link != nil { + err = netlink.LinkDel(l) + if err != nil { + return err + } + } + + log.Debugf("adding device: %s", name) + err = netlink.LinkAdd(l) + if os.IsExist(err) { + log.Infof("interface %s already exists. Will reuse.", name) + } else if err != nil { + return fmt.Errorf("link add: %w", err) + } + + return nil +} + +func (l *wgLink) setMTU(mtu int) error { + if err := netlink.LinkSetMTU(l, mtu); err != nil { + log.Errorf("error setting MTU on interface: %s", l.attrs.Name) + + return fmt.Errorf("link set mtu: %w", err) + } + + return nil +} + +func (l *wgLink) up() error { + if err := netlink.LinkSetUp(l); err != nil { + log.Errorf("error bringing up interface: %s", l.attrs.Name) + return fmt.Errorf("link setup: %w", err) + } + + return nil +} + +func (l *wgLink) assignAddr(address WGAddress) error { + //delete existing addresses + list, err := netlink.AddrList(l, 0) + if err != nil { + return fmt.Errorf("list addr: %w", err) + } + + if len(list) > 0 { + for _, a := range list { + addr := a + err = netlink.AddrDel(l, &addr) + if err != nil { + return fmt.Errorf("del addr: %w", err) + } + } + } + + name := l.attrs.Name + addrStr := address.String() + + log.Debugf("adding address %s to interface: %s", addrStr, name) + + addr, err := netlink.ParseAddr(addrStr) + if err != nil { + return fmt.Errorf("parse addr: %w", err) + } + + err = netlink.AddrAdd(l, addr) + if os.IsExist(err) { + log.Infof("interface %s already has the address: %s", name, addrStr) + } else if err != nil { + return fmt.Errorf("add addr: %w", err) + } + + // On linux, the link must be brought up + if err := netlink.LinkSetUp(l); err != nil { + return fmt.Errorf("link setup: %w", err) + } + + return nil +} diff --git a/iface/tun_usp_linux.go b/iface/tun_usp_unix.go similarity index 78% rename from iface/tun_usp_linux.go rename to iface/tun_usp_unix.go index 9f0210228..2e4be5280 100644 --- a/iface/tun_usp_linux.go +++ b/iface/tun_usp_unix.go @@ -1,14 +1,14 @@ -//go:build linux && !android +//go:build (linux && !android) || freebsd package iface import ( "fmt" "os" + "runtime" "github.com/pion/transport/v3" log "github.com/sirupsen/logrus" - "github.com/vishvananda/netlink" "golang.zx2c4.com/wireguard/device" "golang.zx2c4.com/wireguard/tun" @@ -31,6 +31,9 @@ type tunUSPDevice struct { func newTunUSPDevice(name string, address WGAddress, port int, key string, mtu int, transportNet transport.Net) wgTunDevice { log.Infof("using userspace bind mode") + + checkUser() + return &tunUSPDevice{ name: name, address: address, @@ -129,30 +132,14 @@ func (t *tunUSPDevice) Wrapper() *DeviceWrapper { func (t *tunUSPDevice) assignAddr() error { link := newWGLink(t.name) - //delete existing addresses - list, err := netlink.AddrList(link, 0) - if err != nil { - return err - } - if len(list) > 0 { - for _, a := range list { - addr := a - err = netlink.AddrDel(link, &addr) - if err != nil { - return err - } + 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") } } - - log.Debugf("adding address %s to interface: %s", t.address.String(), t.name) - addr, _ := netlink.ParseAddr(t.address.String()) - err = netlink.AddrAdd(link, addr) - if os.IsExist(err) { - log.Infof("interface %s already has the address: %s", t.name, t.address.String()) - } else if err != nil { - return err - } - // On linux, the link must be brought up - err = netlink.LinkSetUp(link) - return err } diff --git a/iface/wg_configurer.go b/iface/wg_configurer.go index 91c57eb9c..dd38ba075 100644 --- a/iface/wg_configurer.go +++ b/iface/wg_configurer.go @@ -1,12 +1,15 @@ package iface import ( + "errors" "net" "time" "golang.zx2c4.com/wireguard/wgctrl/wgtypes" ) +var ErrPeerNotFound = errors.New("peer not found") + type wgConfigurer interface { configureInterface(privateKey string, port int) error updatePeer(peerKey string, allowedIps string, keepAlive time.Duration, endpoint *net.UDPAddr, preSharedKey *wgtypes.Key) error diff --git a/iface/wg_configurer_kernel.go b/iface/wg_configurer_kernel_unix.go similarity index 90% rename from iface/wg_configurer_kernel.go rename to iface/wg_configurer_kernel_unix.go index 67bfb716d..48ea70b7b 100644 --- a/iface/wg_configurer_kernel.go +++ b/iface/wg_configurer_kernel_unix.go @@ -1,4 +1,4 @@ -//go:build linux && !android +//go:build (linux && !android) || freebsd package iface @@ -125,17 +125,17 @@ func (c *wgKernelConfigurer) addAllowedIP(peerKey string, allowedIP string) erro func (c *wgKernelConfigurer) removeAllowedIP(peerKey string, allowedIP string) error { _, ipNet, err := net.ParseCIDR(allowedIP) if err != nil { - return err + return fmt.Errorf("parse allowed IP: %w", err) } peerKeyParsed, err := wgtypes.ParseKey(peerKey) if err != nil { - return err + return fmt.Errorf("parse peer key: %w", err) } existingPeer, err := c.getPeer(c.deviceName, peerKey) if err != nil { - return err + return fmt.Errorf("get peer: %w", err) } newAllowedIPs := existingPeer.AllowedIPs @@ -159,7 +159,7 @@ func (c *wgKernelConfigurer) removeAllowedIP(peerKey string, allowedIP string) e } err = c.configure(config) if err != nil { - return fmt.Errorf(`received error "%w" while removing allowed IP from peer on interface %s with settings: allowed ips %s`, err, c.deviceName, allowedIP) + return fmt.Errorf("remove allowed IP %s on interface %s: %w", allowedIP, c.deviceName, err) } return nil } @@ -167,25 +167,25 @@ func (c *wgKernelConfigurer) removeAllowedIP(peerKey string, allowedIP string) e func (c *wgKernelConfigurer) getPeer(ifaceName, peerPubKey string) (wgtypes.Peer, error) { wg, err := wgctrl.New() if err != nil { - return wgtypes.Peer{}, err + return wgtypes.Peer{}, fmt.Errorf("wgctl: %w", err) } defer func() { err = wg.Close() if err != nil { - log.Errorf("got error while closing wgctl: %v", err) + log.Errorf("Got error while closing wgctl: %v", err) } }() wgDevice, err := wg.Device(ifaceName) if err != nil { - return wgtypes.Peer{}, err + return wgtypes.Peer{}, fmt.Errorf("get device %s: %w", ifaceName, err) } for _, peer := range wgDevice.Peers { if peer.PublicKey.String() == peerPubKey { return peer, nil } } - return wgtypes.Peer{}, fmt.Errorf("peer not found") + return wgtypes.Peer{}, ErrPeerNotFound } func (c *wgKernelConfigurer) configure(config wgtypes.Config) error { @@ -200,7 +200,6 @@ func (c *wgKernelConfigurer) configure(config wgtypes.Config) error { if err != nil { return err } - log.Tracef("got Wireguard device %s", c.deviceName) return wg.ConfigureDevice(c.deviceName, config) } diff --git a/iface/wg_configurer_usp.go b/iface/wg_configurer_usp.go index 0c1b6e85a..04a29a60b 100644 --- a/iface/wg_configurer_usp.go +++ b/iface/wg_configurer_usp.go @@ -17,6 +17,8 @@ import ( nbnet "github.com/netbirdio/netbird/util/net" ) +var ErrAllowedIPNotFound = fmt.Errorf("allowed IP not found") + type wgUSPConfigurer struct { device *device.Device deviceName string @@ -173,7 +175,7 @@ func (c *wgUSPConfigurer) removeAllowedIP(peerKey string, ip string) error { } if !removedAllowedIP { - return fmt.Errorf("allowedIP not found") + return ErrAllowedIPNotFound } config := wgtypes.Config{ Peers: []wgtypes.PeerConfig{peer}, @@ -301,7 +303,7 @@ func findPeerInfo(ipcInput string, peerKey string, searchConfigKeys []string) (m } } if !foundPeer { - return nil, fmt.Errorf("peer not found: %s", peerKey) + return nil, fmt.Errorf("%w: %s", ErrPeerNotFound, peerKey) } return configFound, nil diff --git a/management/client/client.go b/management/client/client.go index 928092a40..e79884292 100644 --- a/management/client/client.go +++ b/management/client/client.go @@ -12,12 +12,13 @@ import ( type Client interface { io.Closer - Sync(ctx context.Context, msgHandler func(msg *proto.SyncResponse) error) error + 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) GetDeviceAuthorizationFlow(serverKey wgtypes.Key) (*proto.DeviceAuthorizationFlow, error) GetPKCEAuthorizationFlow(serverKey wgtypes.Key) (*proto.PKCEAuthorizationFlow, error) - GetNetworkMap() (*proto.NetworkMap, error) + GetNetworkMap(sysInfo *system.Info) (*proto.NetworkMap, error) IsHealthy() bool + SyncMeta(sysInfo *system.Info) error } diff --git a/management/client/client_test.go b/management/client/client_test.go index 32ad8fce4..001a89b73 100644 --- a/management/client/client_test.go +++ b/management/client/client_test.go @@ -257,7 +257,7 @@ func TestClient_Sync(t *testing.T) { ch := make(chan *mgmtProto.SyncResponse, 1) go func() { - err = client.Sync(context.Background(), func(msg *mgmtProto.SyncResponse) error { + err = client.Sync(context.Background(), info, func(msg *mgmtProto.SyncResponse) error { ch <- msg return nil }) diff --git a/management/client/grpc.go b/management/client/grpc.go index df687a160..a8f4a91c7 100644 --- a/management/client/grpc.go +++ b/management/client/grpc.go @@ -29,6 +29,11 @@ import ( const ConnectTimeout = 10 * time.Second +const ( + errMsgMgmtPublicKey = "failed getting Management Service public key: %s" + errMsgNoMgmtConnection = "no connection to management" +) + // ConnStateNotifier is a wrapper interface of the status recorders type ConnStateNotifier interface { MarkManagementDisconnected(error) @@ -113,13 +118,11 @@ func (c *GrpcClient) ready() bool { // Sync wraps the real client's Sync endpoint call and takes care of retries and encryption/decryption of messages // Blocking request. The result will be sent via msgHandler callback function -func (c *GrpcClient) Sync(ctx context.Context, msgHandler func(msg *proto.SyncResponse) error) error { - backOff := defaultBackoff(ctx) - +func (c *GrpcClient) Sync(ctx context.Context, sysInfo *system.Info, msgHandler func(msg *proto.SyncResponse) error) error { operation := func() error { log.Debugf("management connection state %v", c.conn.GetState()) - connState := c.conn.GetState() + if connState == connectivity.Shutdown { return backoff.Permanent(fmt.Errorf("connection to management has been shut down")) } else if !(connState == connectivity.Ready || connState == connectivity.Idle) { @@ -129,55 +132,60 @@ func (c *GrpcClient) Sync(ctx context.Context, msgHandler func(msg *proto.SyncRe serverPubKey, err := c.GetServerPublicKey() if err != nil { - log.Debugf("failed getting Management Service public key: %s", err) + log.Debugf(errMsgMgmtPublicKey, err) return err } - ctx, cancelStream := context.WithCancel(ctx) - defer cancelStream() - stream, err := c.connectToStream(ctx, *serverPubKey) - if err != nil { - log.Debugf("failed to open Management Service stream: %s", err) - if s, ok := gstatus.FromError(err); ok && s.Code() == codes.PermissionDenied { - return backoff.Permanent(err) // unrecoverable error, propagate to the upper layer - } - return err - } - - log.Infof("connected to the Management Service stream") - c.notifyConnected() - // blocking until error - err = c.receiveEvents(stream, *serverPubKey, msgHandler) - if err != nil { - s, _ := gstatus.FromError(err) - switch s.Code() { - case codes.PermissionDenied: - return backoff.Permanent(err) // unrecoverable error, propagate to the upper layer - case codes.Canceled: - log.Debugf("management connection context has been canceled, this usually indicates shutdown") - return nil - default: - backOff.Reset() // reset backoff counter after successful connection - c.notifyDisconnected(err) - log.Warnf("disconnected from the Management service but will retry silently. Reason: %v", err) - return err - } - } - - return nil + return c.handleStream(ctx, *serverPubKey, sysInfo, msgHandler) } - err := backoff.Retry(operation, backOff) + err := backoff.Retry(operation, defaultBackoff(ctx)) if err != nil { log.Warnf("exiting the Management service connection retry loop due to the unrecoverable error: %s", err) + } + + return err +} + +func (c *GrpcClient) handleStream(ctx context.Context, serverPubKey wgtypes.Key, sysInfo *system.Info, + msgHandler func(msg *proto.SyncResponse) error) error { + ctx, cancelStream := context.WithCancel(ctx) + defer cancelStream() + + stream, err := c.connectToStream(ctx, serverPubKey, sysInfo) + if err != nil { + log.Debugf("failed to open Management Service stream: %s", err) + if s, ok := gstatus.FromError(err); ok && s.Code() == codes.PermissionDenied { + return backoff.Permanent(err) // unrecoverable error, propagate to the upper layer + } return err } + log.Infof("connected to the Management Service stream") + c.notifyConnected() + + // blocking until error + err = c.receiveEvents(stream, serverPubKey, msgHandler) + if err != nil { + s, _ := gstatus.FromError(err) + switch s.Code() { + case codes.PermissionDenied: + return backoff.Permanent(err) // unrecoverable error, propagate to the upper layer + case codes.Canceled: + log.Debugf("management connection context has been canceled, this usually indicates shutdown") + return nil + default: + c.notifyDisconnected(err) + log.Warnf("disconnected from the Management service but will retry silently. Reason: %v", err) + return err + } + } + return nil } // GetNetworkMap return with the network map -func (c *GrpcClient) GetNetworkMap() (*proto.NetworkMap, error) { +func (c *GrpcClient) GetNetworkMap(sysInfo *system.Info) (*proto.NetworkMap, error) { serverPubKey, err := c.GetServerPublicKey() if err != nil { log.Debugf("failed getting Management Service public key: %s", err) @@ -186,7 +194,7 @@ func (c *GrpcClient) GetNetworkMap() (*proto.NetworkMap, error) { ctx, cancelStream := context.WithCancel(c.ctx) defer cancelStream() - stream, err := c.connectToStream(ctx, *serverPubKey) + stream, err := c.connectToStream(ctx, *serverPubKey, sysInfo) if err != nil { log.Debugf("failed to open Management Service stream: %s", err) return nil, err @@ -219,8 +227,8 @@ func (c *GrpcClient) GetNetworkMap() (*proto.NetworkMap, error) { return decryptedResp.GetNetworkMap(), nil } -func (c *GrpcClient) connectToStream(ctx context.Context, serverPubKey wgtypes.Key) (proto.ManagementService_SyncClient, error) { - req := &proto.SyncRequest{} +func (c *GrpcClient) connectToStream(ctx context.Context, serverPubKey wgtypes.Key, sysInfo *system.Info) (proto.ManagementService_SyncClient, error) { + req := &proto.SyncRequest{Meta: infoToMetaData(sysInfo)} myPrivateKey := c.key myPublicKey := myPrivateKey.PublicKey() @@ -269,7 +277,7 @@ func (c *GrpcClient) receiveEvents(stream proto.ManagementService_SyncClient, se // GetServerPublicKey returns server's WireGuard public key (used later for encrypting messages sent to the server) func (c *GrpcClient) GetServerPublicKey() (*wgtypes.Key, error) { if !c.ready() { - return nil, fmt.Errorf("no connection to management") + return nil, fmt.Errorf(errMsgNoMgmtConnection) } mgmCtx, cancel := context.WithTimeout(c.ctx, 5*time.Second) @@ -316,7 +324,7 @@ func (c *GrpcClient) IsHealthy() bool { func (c *GrpcClient) login(serverKey wgtypes.Key, req *proto.LoginRequest) (*proto.LoginResponse, error) { if !c.ready() { - return nil, fmt.Errorf("no connection to management") + return nil, fmt.Errorf(errMsgNoMgmtConnection) } loginReq, err := encryption.EncryptMessage(serverKey, c.key, req) if err != nil { @@ -431,6 +439,35 @@ func (c *GrpcClient) GetPKCEAuthorizationFlow(serverKey wgtypes.Key) (*proto.PKC return flowInfoResp, nil } +// SyncMeta sends updated system metadata to the Management Service. +// It should be used if there is changes on peer posture check after initial sync. +func (c *GrpcClient) SyncMeta(sysInfo *system.Info) error { + if !c.ready() { + return fmt.Errorf(errMsgNoMgmtConnection) + } + + serverPubKey, err := c.GetServerPublicKey() + if err != nil { + log.Debugf(errMsgMgmtPublicKey, err) + return err + } + + syncMetaReq, err := encryption.EncryptMessage(*serverPubKey, c.key, &proto.SyncMetaRequest{Meta: infoToMetaData(sysInfo)}) + if err != nil { + log.Errorf("failed to encrypt message: %s", err) + return err + } + + mgmCtx, cancel := context.WithTimeout(c.ctx, ConnectTimeout) + defer cancel() + + _, err = c.realClient.SyncMeta(mgmCtx, &proto.EncryptedMessage{ + WgPubKey: c.key.PublicKey().String(), + Body: syncMetaReq, + }) + return err +} + func (c *GrpcClient) notifyDisconnected(err error) { c.connStateCallbackLock.RLock() defer c.connStateCallbackLock.RUnlock() @@ -464,6 +501,15 @@ func infoToMetaData(info *system.Info) *proto.PeerSystemMeta { }) } + files := make([]*proto.File, 0, len(info.Files)) + for _, file := range info.Files { + files = append(files, &proto.File{ + Path: file.Path, + Exist: file.Exist, + ProcessIsRunning: file.ProcessIsRunning, + }) + } + return &proto.PeerSystemMeta{ Hostname: info.Hostname, GoOS: info.GoOS, @@ -483,5 +529,6 @@ func infoToMetaData(info *system.Info) *proto.PeerSystemMeta { Cloud: info.Environment.Cloud, Platform: info.Environment.Platform, }, + Files: files, } } diff --git a/management/client/mock.go b/management/client/mock.go index 02a5ade3a..73a7ac38f 100644 --- a/management/client/mock.go +++ b/management/client/mock.go @@ -11,12 +11,13 @@ import ( type MockClient struct { CloseFunc func() error - SyncFunc func(ctx context.Context, msgHandler func(msg *proto.SyncResponse) error) 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) LoginFunc func(serverKey wgtypes.Key, info *system.Info, sshKey []byte) (*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 } func (m *MockClient) IsHealthy() bool { @@ -30,11 +31,11 @@ func (m *MockClient) Close() error { return m.CloseFunc() } -func (m *MockClient) Sync(ctx context.Context, msgHandler func(msg *proto.SyncResponse) error) error { +func (m *MockClient) Sync(ctx context.Context, sysInfo *system.Info, msgHandler func(msg *proto.SyncResponse) error) error { if m.SyncFunc == nil { return nil } - return m.SyncFunc(ctx, msgHandler) + return m.SyncFunc(ctx, sysInfo, msgHandler) } func (m *MockClient) GetServerPublicKey() (*wgtypes.Key, error) { @@ -73,6 +74,13 @@ func (m *MockClient) GetPKCEAuthorizationFlow(serverKey wgtypes.Key) (*proto.PKC } // GetNetworkMap mock implementation of GetNetworkMap from mgm.Client interface -func (m *MockClient) GetNetworkMap() (*proto.NetworkMap, error) { +func (m *MockClient) GetNetworkMap(_ *system.Info) (*proto.NetworkMap, error) { return nil, nil } + +func (m *MockClient) SyncMeta(sysInfo *system.Info) error { + if m.SyncMetaFunc == nil { + return nil + } + return m.SyncMetaFunc(sysInfo) +} diff --git a/management/domain/domain.go b/management/domain/domain.go new file mode 100644 index 000000000..e7e6b050a --- /dev/null +++ b/management/domain/domain.go @@ -0,0 +1,34 @@ +package domain + +import ( + "golang.org/x/net/idna" +) + +type Domain string + +// String converts the Domain to a non-punycode string. +func (d Domain) String() (string, error) { + unicode, err := idna.ToUnicode(string(d)) + if err != nil { + return "", err + } + return unicode, nil +} + +// SafeString converts the Domain to a non-punycode string, falling back to the original string if conversion fails. +func (d Domain) SafeString() string { + str, err := d.String() + if err != nil { + str = string(d) + } + return str +} + +// FromString creates a Domain from a string, converting it to punycode. +func FromString(s string) (Domain, error) { + ascii, err := idna.ToASCII(s) + if err != nil { + return "", err + } + return Domain(ascii), nil +} diff --git a/management/domain/list.go b/management/domain/list.go new file mode 100644 index 000000000..413a23442 --- /dev/null +++ b/management/domain/list.go @@ -0,0 +1,83 @@ +package domain + +import "strings" + +type List []Domain + +// ToStringList converts a List to a slice of string. +func (d List) ToStringList() ([]string, error) { + var list []string + for _, domain := range d { + s, err := domain.String() + if err != nil { + return nil, err + } + list = append(list, s) + } + return list, nil +} + +// ToPunycodeList converts the List to a slice of Punycode-encoded domain strings. +func (d List) ToPunycodeList() []string { + var list []string + for _, domain := range d { + list = append(list, string(domain)) + } + return list +} + +// ToSafeStringList converts the List to a slice of non-punycode strings. +// If a domain cannot be converted, the original string is used. +func (d List) ToSafeStringList() []string { + var list []string + for _, domain := range d { + list = append(list, domain.SafeString()) + } + return list +} + +// String converts List to a comma-separated string. +func (d List) String() (string, error) { + list, err := d.ToStringList() + if err != nil { + return "", err + } + return strings.Join(list, ", "), nil +} + +// SafeString converts List to a comma-separated non-punycode string. +// If a domain cannot be converted, the original string is used. +func (d List) SafeString() string { + str, err := d.String() + if err != nil { + return strings.Join(d.ToPunycodeList(), ", ") + } + return str +} + +// PunycodeString converts the List to a comma-separated string of Punycode-encoded domains. +func (d List) PunycodeString() string { + return strings.Join(d.ToPunycodeList(), ", ") +} + +// FromStringList creates a DomainList from a slice of string. +func FromStringList(s []string) (List, error) { + var dl List + for _, domain := range s { + d, err := FromString(domain) + if err != nil { + return nil, err + } + dl = append(dl, d) + } + return dl, nil +} + +// FromPunycodeList creates a List from a slice of Punycode-encoded domain strings. +func FromPunycodeList(s []string) List { + var dl List + for _, domain := range s { + dl = append(dl, Domain(domain)) + } + return dl +} diff --git a/management/proto/management.pb.go b/management/proto/management.pb.go index 18077ea89..ecf738ea5 100644 --- a/management/proto/management.pb.go +++ b/management/proto/management.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.26.0 -// protoc v4.24.3 +// protoc v4.23.4 // source: management.proto package proto @@ -73,7 +73,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{11, 0} + return file_management_proto_rawDescGZIP(), []int{13, 0} } type DeviceAuthorizationFlowProvider int32 @@ -116,7 +116,7 @@ func (x DeviceAuthorizationFlowProvider) Number() protoreflect.EnumNumber { // Deprecated: Use DeviceAuthorizationFlowProvider.Descriptor instead. func (DeviceAuthorizationFlowProvider) EnumDescriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{18, 0} + return file_management_proto_rawDescGZIP(), []int{20, 0} } type FirewallRuleDirection int32 @@ -162,7 +162,7 @@ func (x FirewallRuleDirection) Number() protoreflect.EnumNumber { // Deprecated: Use FirewallRuleDirection.Descriptor instead. func (FirewallRuleDirection) EnumDescriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{28, 0} + return file_management_proto_rawDescGZIP(), []int{30, 0} } type FirewallRuleAction int32 @@ -208,7 +208,7 @@ func (x FirewallRuleAction) Number() protoreflect.EnumNumber { // Deprecated: Use FirewallRuleAction.Descriptor instead. func (FirewallRuleAction) EnumDescriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{28, 1} + return file_management_proto_rawDescGZIP(), []int{30, 1} } type FirewallRuleProtocol int32 @@ -263,7 +263,7 @@ func (x FirewallRuleProtocol) Number() protoreflect.EnumNumber { // Deprecated: Use FirewallRuleProtocol.Descriptor instead. func (FirewallRuleProtocol) EnumDescriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{28, 2} + return file_management_proto_rawDescGZIP(), []int{30, 2} } type EncryptedMessage struct { @@ -336,6 +336,9 @@ type SyncRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields + + // Meta data of the peer + Meta *PeerSystemMeta `protobuf:"bytes,1,opt,name=meta,proto3" json:"meta,omitempty"` } func (x *SyncRequest) Reset() { @@ -370,6 +373,13 @@ func (*SyncRequest) Descriptor() ([]byte, []int) { return file_management_proto_rawDescGZIP(), []int{1} } +func (x *SyncRequest) GetMeta() *PeerSystemMeta { + if x != nil { + return x.Meta + } + 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) type SyncResponse struct { state protoimpl.MessageState @@ -386,6 +396,8 @@ type SyncResponse struct { // Deprecated. Use NetworkMap.remotePeersIsEmpty RemotePeersIsEmpty bool `protobuf:"varint,4,opt,name=remotePeersIsEmpty,proto3" json:"remotePeersIsEmpty,omitempty"` NetworkMap *NetworkMap `protobuf:"bytes,5,opt,name=NetworkMap,proto3" json:"NetworkMap,omitempty"` + // Posture checks to be evaluated by client + Checks []*Checks `protobuf:"bytes,6,rep,name=Checks,proto3" json:"Checks,omitempty"` } func (x *SyncResponse) Reset() { @@ -455,6 +467,61 @@ func (x *SyncResponse) GetNetworkMap() *NetworkMap { return nil } +func (x *SyncResponse) GetChecks() []*Checks { + if x != nil { + return x.Checks + } + return nil +} + +type SyncMetaRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Meta data of the peer + Meta *PeerSystemMeta `protobuf:"bytes,1,opt,name=meta,proto3" json:"meta,omitempty"` +} + +func (x *SyncMetaRequest) Reset() { + *x = SyncMetaRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_management_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SyncMetaRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SyncMetaRequest) ProtoMessage() {} + +func (x *SyncMetaRequest) ProtoReflect() protoreflect.Message { + mi := &file_management_proto_msgTypes[3] + 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 SyncMetaRequest.ProtoReflect.Descriptor instead. +func (*SyncMetaRequest) Descriptor() ([]byte, []int) { + return file_management_proto_rawDescGZIP(), []int{3} +} + +func (x *SyncMetaRequest) GetMeta() *PeerSystemMeta { + if x != nil { + return x.Meta + } + return nil +} + type LoginRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -473,7 +540,7 @@ type LoginRequest struct { func (x *LoginRequest) Reset() { *x = LoginRequest{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[3] + mi := &file_management_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -486,7 +553,7 @@ func (x *LoginRequest) String() string { func (*LoginRequest) ProtoMessage() {} func (x *LoginRequest) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[3] + mi := &file_management_proto_msgTypes[4] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -499,7 +566,7 @@ func (x *LoginRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use LoginRequest.ProtoReflect.Descriptor instead. func (*LoginRequest) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{3} + return file_management_proto_rawDescGZIP(), []int{4} } func (x *LoginRequest) GetSetupKey() string { @@ -546,7 +613,7 @@ type PeerKeys struct { func (x *PeerKeys) Reset() { *x = PeerKeys{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[4] + mi := &file_management_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -559,7 +626,7 @@ func (x *PeerKeys) String() string { func (*PeerKeys) ProtoMessage() {} func (x *PeerKeys) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[4] + mi := &file_management_proto_msgTypes[5] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -572,7 +639,7 @@ func (x *PeerKeys) ProtoReflect() protoreflect.Message { // Deprecated: Use PeerKeys.ProtoReflect.Descriptor instead. func (*PeerKeys) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{4} + return file_management_proto_rawDescGZIP(), []int{5} } func (x *PeerKeys) GetSshPubKey() []byte { @@ -604,7 +671,7 @@ type Environment struct { func (x *Environment) Reset() { *x = Environment{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[5] + mi := &file_management_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -617,7 +684,7 @@ func (x *Environment) String() string { func (*Environment) ProtoMessage() {} func (x *Environment) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[5] + mi := &file_management_proto_msgTypes[6] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -630,7 +697,7 @@ func (x *Environment) ProtoReflect() protoreflect.Message { // Deprecated: Use Environment.ProtoReflect.Descriptor instead. func (*Environment) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{5} + return file_management_proto_rawDescGZIP(), []int{6} } func (x *Environment) GetCloud() string { @@ -647,6 +714,73 @@ func (x *Environment) GetPlatform() string { return "" } +// File represents a file on the system. +type File struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // path is the path to the file. + Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"` + // exist indicate whether the file exists. + Exist bool `protobuf:"varint,2,opt,name=exist,proto3" json:"exist,omitempty"` + // processIsRunning indicates whether the file is a running process or not. + ProcessIsRunning bool `protobuf:"varint,3,opt,name=processIsRunning,proto3" json:"processIsRunning,omitempty"` +} + +func (x *File) Reset() { + *x = File{} + if protoimpl.UnsafeEnabled { + mi := &file_management_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *File) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*File) ProtoMessage() {} + +func (x *File) ProtoReflect() protoreflect.Message { + mi := &file_management_proto_msgTypes[7] + 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 File.ProtoReflect.Descriptor instead. +func (*File) Descriptor() ([]byte, []int) { + return file_management_proto_rawDescGZIP(), []int{7} +} + +func (x *File) GetPath() string { + if x != nil { + return x.Path + } + return "" +} + +func (x *File) GetExist() bool { + if x != nil { + return x.Exist + } + return false +} + +func (x *File) GetProcessIsRunning() bool { + if x != nil { + return x.ProcessIsRunning + } + return false +} + // PeerSystemMeta is machine meta data like OS and version. type PeerSystemMeta struct { state protoimpl.MessageState @@ -668,12 +802,13 @@ type PeerSystemMeta struct { 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"` } func (x *PeerSystemMeta) Reset() { *x = PeerSystemMeta{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[6] + mi := &file_management_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -686,7 +821,7 @@ func (x *PeerSystemMeta) String() string { func (*PeerSystemMeta) ProtoMessage() {} func (x *PeerSystemMeta) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[6] + mi := &file_management_proto_msgTypes[8] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -699,7 +834,7 @@ func (x *PeerSystemMeta) ProtoReflect() protoreflect.Message { // Deprecated: Use PeerSystemMeta.ProtoReflect.Descriptor instead. func (*PeerSystemMeta) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{6} + return file_management_proto_rawDescGZIP(), []int{8} } func (x *PeerSystemMeta) GetHostname() string { @@ -807,6 +942,13 @@ func (x *PeerSystemMeta) GetEnvironment() *Environment { return nil } +func (x *PeerSystemMeta) GetFiles() []*File { + if x != nil { + return x.Files + } + return nil +} + type LoginResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -816,12 +958,14 @@ type LoginResponse struct { WiretrusteeConfig *WiretrusteeConfig `protobuf:"bytes,1,opt,name=wiretrusteeConfig,proto3" json:"wiretrusteeConfig,omitempty"` // Peer local config PeerConfig *PeerConfig `protobuf:"bytes,2,opt,name=peerConfig,proto3" json:"peerConfig,omitempty"` + // Posture checks to be evaluated by client + Checks []*Checks `protobuf:"bytes,3,rep,name=Checks,proto3" json:"Checks,omitempty"` } func (x *LoginResponse) Reset() { *x = LoginResponse{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[7] + mi := &file_management_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -834,7 +978,7 @@ func (x *LoginResponse) String() string { func (*LoginResponse) ProtoMessage() {} func (x *LoginResponse) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[7] + mi := &file_management_proto_msgTypes[9] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -847,7 +991,7 @@ func (x *LoginResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use LoginResponse.ProtoReflect.Descriptor instead. func (*LoginResponse) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{7} + return file_management_proto_rawDescGZIP(), []int{9} } func (x *LoginResponse) GetWiretrusteeConfig() *WiretrusteeConfig { @@ -864,6 +1008,13 @@ func (x *LoginResponse) GetPeerConfig() *PeerConfig { return nil } +func (x *LoginResponse) GetChecks() []*Checks { + if x != nil { + return x.Checks + } + return nil +} + type ServerKeyResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -880,7 +1031,7 @@ type ServerKeyResponse struct { func (x *ServerKeyResponse) Reset() { *x = ServerKeyResponse{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[8] + mi := &file_management_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -893,7 +1044,7 @@ func (x *ServerKeyResponse) String() string { func (*ServerKeyResponse) ProtoMessage() {} func (x *ServerKeyResponse) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[8] + mi := &file_management_proto_msgTypes[10] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -906,7 +1057,7 @@ func (x *ServerKeyResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ServerKeyResponse.ProtoReflect.Descriptor instead. func (*ServerKeyResponse) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{8} + return file_management_proto_rawDescGZIP(), []int{10} } func (x *ServerKeyResponse) GetKey() string { @@ -939,7 +1090,7 @@ type Empty struct { func (x *Empty) Reset() { *x = Empty{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[9] + mi := &file_management_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -952,7 +1103,7 @@ func (x *Empty) String() string { func (*Empty) ProtoMessage() {} func (x *Empty) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[9] + mi := &file_management_proto_msgTypes[11] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -965,7 +1116,7 @@ func (x *Empty) ProtoReflect() protoreflect.Message { // Deprecated: Use Empty.ProtoReflect.Descriptor instead. func (*Empty) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{9} + return file_management_proto_rawDescGZIP(), []int{11} } // WiretrusteeConfig is a common configuration of any Wiretrustee peer. It contains STUN, TURN, Signal and Management servers configurations @@ -985,7 +1136,7 @@ type WiretrusteeConfig struct { func (x *WiretrusteeConfig) Reset() { *x = WiretrusteeConfig{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[10] + mi := &file_management_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -998,7 +1149,7 @@ func (x *WiretrusteeConfig) String() string { func (*WiretrusteeConfig) ProtoMessage() {} func (x *WiretrusteeConfig) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[10] + mi := &file_management_proto_msgTypes[12] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1011,7 +1162,7 @@ func (x *WiretrusteeConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use WiretrusteeConfig.ProtoReflect.Descriptor instead. func (*WiretrusteeConfig) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{10} + return file_management_proto_rawDescGZIP(), []int{12} } func (x *WiretrusteeConfig) GetStuns() []*HostConfig { @@ -1049,7 +1200,7 @@ type HostConfig struct { func (x *HostConfig) Reset() { *x = HostConfig{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[11] + mi := &file_management_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1062,7 +1213,7 @@ func (x *HostConfig) String() string { func (*HostConfig) ProtoMessage() {} func (x *HostConfig) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[11] + mi := &file_management_proto_msgTypes[13] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1075,7 +1226,7 @@ func (x *HostConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use HostConfig.ProtoReflect.Descriptor instead. func (*HostConfig) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{11} + return file_management_proto_rawDescGZIP(), []int{13} } func (x *HostConfig) GetUri() string { @@ -1107,7 +1258,7 @@ type ProtectedHostConfig struct { func (x *ProtectedHostConfig) Reset() { *x = ProtectedHostConfig{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[12] + mi := &file_management_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1120,7 +1271,7 @@ func (x *ProtectedHostConfig) String() string { func (*ProtectedHostConfig) ProtoMessage() {} func (x *ProtectedHostConfig) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[12] + mi := &file_management_proto_msgTypes[14] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1133,7 +1284,7 @@ func (x *ProtectedHostConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use ProtectedHostConfig.ProtoReflect.Descriptor instead. func (*ProtectedHostConfig) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{12} + return file_management_proto_rawDescGZIP(), []int{14} } func (x *ProtectedHostConfig) GetHostConfig() *HostConfig { @@ -1177,7 +1328,7 @@ type PeerConfig struct { func (x *PeerConfig) Reset() { *x = PeerConfig{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[13] + mi := &file_management_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1190,7 +1341,7 @@ func (x *PeerConfig) String() string { func (*PeerConfig) ProtoMessage() {} func (x *PeerConfig) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[13] + mi := &file_management_proto_msgTypes[15] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1203,7 +1354,7 @@ func (x *PeerConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use PeerConfig.ProtoReflect.Descriptor instead. func (*PeerConfig) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{13} + return file_management_proto_rawDescGZIP(), []int{15} } func (x *PeerConfig) GetAddress() string { @@ -1265,7 +1416,7 @@ type NetworkMap struct { func (x *NetworkMap) Reset() { *x = NetworkMap{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[14] + mi := &file_management_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1278,7 +1429,7 @@ func (x *NetworkMap) String() string { func (*NetworkMap) ProtoMessage() {} func (x *NetworkMap) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[14] + mi := &file_management_proto_msgTypes[16] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1291,7 +1442,7 @@ func (x *NetworkMap) ProtoReflect() protoreflect.Message { // Deprecated: Use NetworkMap.ProtoReflect.Descriptor instead. func (*NetworkMap) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{14} + return file_management_proto_rawDescGZIP(), []int{16} } func (x *NetworkMap) GetSerial() uint64 { @@ -1377,7 +1528,7 @@ type RemotePeerConfig struct { func (x *RemotePeerConfig) Reset() { *x = RemotePeerConfig{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[15] + mi := &file_management_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1390,7 +1541,7 @@ func (x *RemotePeerConfig) String() string { func (*RemotePeerConfig) ProtoMessage() {} func (x *RemotePeerConfig) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[15] + mi := &file_management_proto_msgTypes[17] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1403,7 +1554,7 @@ func (x *RemotePeerConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use RemotePeerConfig.ProtoReflect.Descriptor instead. func (*RemotePeerConfig) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{15} + return file_management_proto_rawDescGZIP(), []int{17} } func (x *RemotePeerConfig) GetWgPubKey() string { @@ -1450,7 +1601,7 @@ type SSHConfig struct { func (x *SSHConfig) Reset() { *x = SSHConfig{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[16] + mi := &file_management_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1463,7 +1614,7 @@ func (x *SSHConfig) String() string { func (*SSHConfig) ProtoMessage() {} func (x *SSHConfig) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[16] + mi := &file_management_proto_msgTypes[18] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1476,7 +1627,7 @@ func (x *SSHConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use SSHConfig.ProtoReflect.Descriptor instead. func (*SSHConfig) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{16} + return file_management_proto_rawDescGZIP(), []int{18} } func (x *SSHConfig) GetSshEnabled() bool { @@ -1503,7 +1654,7 @@ type DeviceAuthorizationFlowRequest struct { func (x *DeviceAuthorizationFlowRequest) Reset() { *x = DeviceAuthorizationFlowRequest{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[17] + mi := &file_management_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1516,7 +1667,7 @@ func (x *DeviceAuthorizationFlowRequest) String() string { func (*DeviceAuthorizationFlowRequest) ProtoMessage() {} func (x *DeviceAuthorizationFlowRequest) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[17] + mi := &file_management_proto_msgTypes[19] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1529,7 +1680,7 @@ func (x *DeviceAuthorizationFlowRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DeviceAuthorizationFlowRequest.ProtoReflect.Descriptor instead. func (*DeviceAuthorizationFlowRequest) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{17} + return file_management_proto_rawDescGZIP(), []int{19} } // DeviceAuthorizationFlow represents Device Authorization Flow information @@ -1548,7 +1699,7 @@ type DeviceAuthorizationFlow struct { func (x *DeviceAuthorizationFlow) Reset() { *x = DeviceAuthorizationFlow{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[18] + mi := &file_management_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1561,7 +1712,7 @@ func (x *DeviceAuthorizationFlow) String() string { func (*DeviceAuthorizationFlow) ProtoMessage() {} func (x *DeviceAuthorizationFlow) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[18] + mi := &file_management_proto_msgTypes[20] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1574,7 +1725,7 @@ func (x *DeviceAuthorizationFlow) ProtoReflect() protoreflect.Message { // Deprecated: Use DeviceAuthorizationFlow.ProtoReflect.Descriptor instead. func (*DeviceAuthorizationFlow) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{18} + return file_management_proto_rawDescGZIP(), []int{20} } func (x *DeviceAuthorizationFlow) GetProvider() DeviceAuthorizationFlowProvider { @@ -1601,7 +1752,7 @@ type PKCEAuthorizationFlowRequest struct { func (x *PKCEAuthorizationFlowRequest) Reset() { *x = PKCEAuthorizationFlowRequest{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[19] + mi := &file_management_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1614,7 +1765,7 @@ func (x *PKCEAuthorizationFlowRequest) String() string { func (*PKCEAuthorizationFlowRequest) ProtoMessage() {} func (x *PKCEAuthorizationFlowRequest) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[19] + mi := &file_management_proto_msgTypes[21] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1627,7 +1778,7 @@ func (x *PKCEAuthorizationFlowRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use PKCEAuthorizationFlowRequest.ProtoReflect.Descriptor instead. func (*PKCEAuthorizationFlowRequest) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{19} + return file_management_proto_rawDescGZIP(), []int{21} } // PKCEAuthorizationFlow represents Authorization Code Flow information @@ -1644,7 +1795,7 @@ type PKCEAuthorizationFlow struct { func (x *PKCEAuthorizationFlow) Reset() { *x = PKCEAuthorizationFlow{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[20] + mi := &file_management_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1657,7 +1808,7 @@ func (x *PKCEAuthorizationFlow) String() string { func (*PKCEAuthorizationFlow) ProtoMessage() {} func (x *PKCEAuthorizationFlow) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[20] + mi := &file_management_proto_msgTypes[22] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1670,7 +1821,7 @@ func (x *PKCEAuthorizationFlow) ProtoReflect() protoreflect.Message { // Deprecated: Use PKCEAuthorizationFlow.ProtoReflect.Descriptor instead. func (*PKCEAuthorizationFlow) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{20} + return file_management_proto_rawDescGZIP(), []int{22} } func (x *PKCEAuthorizationFlow) GetProviderConfig() *ProviderConfig { @@ -1712,7 +1863,7 @@ type ProviderConfig struct { func (x *ProviderConfig) Reset() { *x = ProviderConfig{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[21] + mi := &file_management_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1725,7 +1876,7 @@ func (x *ProviderConfig) String() string { func (*ProviderConfig) ProtoMessage() {} func (x *ProviderConfig) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[21] + mi := &file_management_proto_msgTypes[23] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1738,7 +1889,7 @@ func (x *ProviderConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use ProviderConfig.ProtoReflect.Descriptor instead. func (*ProviderConfig) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{21} + return file_management_proto_rawDescGZIP(), []int{23} } func (x *ProviderConfig) GetClientID() string { @@ -1817,19 +1968,21 @@ type Route struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - ID string `protobuf:"bytes,1,opt,name=ID,proto3" json:"ID,omitempty"` - Network string `protobuf:"bytes,2,opt,name=Network,proto3" json:"Network,omitempty"` - NetworkType int64 `protobuf:"varint,3,opt,name=NetworkType,proto3" json:"NetworkType,omitempty"` - Peer string `protobuf:"bytes,4,opt,name=Peer,proto3" json:"Peer,omitempty"` - Metric int64 `protobuf:"varint,5,opt,name=Metric,proto3" json:"Metric,omitempty"` - Masquerade bool `protobuf:"varint,6,opt,name=Masquerade,proto3" json:"Masquerade,omitempty"` - NetID string `protobuf:"bytes,7,opt,name=NetID,proto3" json:"NetID,omitempty"` + ID string `protobuf:"bytes,1,opt,name=ID,proto3" json:"ID,omitempty"` + Network string `protobuf:"bytes,2,opt,name=Network,proto3" json:"Network,omitempty"` + NetworkType int64 `protobuf:"varint,3,opt,name=NetworkType,proto3" json:"NetworkType,omitempty"` + Peer string `protobuf:"bytes,4,opt,name=Peer,proto3" json:"Peer,omitempty"` + Metric int64 `protobuf:"varint,5,opt,name=Metric,proto3" json:"Metric,omitempty"` + Masquerade bool `protobuf:"varint,6,opt,name=Masquerade,proto3" json:"Masquerade,omitempty"` + NetID string `protobuf:"bytes,7,opt,name=NetID,proto3" json:"NetID,omitempty"` + Domains []string `protobuf:"bytes,8,rep,name=Domains,proto3" json:"Domains,omitempty"` + KeepRoute bool `protobuf:"varint,9,opt,name=keepRoute,proto3" json:"keepRoute,omitempty"` } func (x *Route) Reset() { *x = Route{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[22] + mi := &file_management_proto_msgTypes[24] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1842,7 +1995,7 @@ func (x *Route) String() string { func (*Route) ProtoMessage() {} func (x *Route) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[22] + mi := &file_management_proto_msgTypes[24] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1855,7 +2008,7 @@ func (x *Route) ProtoReflect() protoreflect.Message { // Deprecated: Use Route.ProtoReflect.Descriptor instead. func (*Route) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{22} + return file_management_proto_rawDescGZIP(), []int{24} } func (x *Route) GetID() string { @@ -1907,6 +2060,20 @@ func (x *Route) GetNetID() string { return "" } +func (x *Route) GetDomains() []string { + if x != nil { + return x.Domains + } + return nil +} + +func (x *Route) GetKeepRoute() bool { + if x != nil { + return x.KeepRoute + } + return false +} + // DNSConfig represents a dns.Update type DNSConfig struct { state protoimpl.MessageState @@ -1921,7 +2088,7 @@ type DNSConfig struct { func (x *DNSConfig) Reset() { *x = DNSConfig{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[23] + mi := &file_management_proto_msgTypes[25] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1934,7 +2101,7 @@ func (x *DNSConfig) String() string { func (*DNSConfig) ProtoMessage() {} func (x *DNSConfig) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[23] + mi := &file_management_proto_msgTypes[25] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1947,7 +2114,7 @@ func (x *DNSConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use DNSConfig.ProtoReflect.Descriptor instead. func (*DNSConfig) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{23} + return file_management_proto_rawDescGZIP(), []int{25} } func (x *DNSConfig) GetServiceEnable() bool { @@ -1984,7 +2151,7 @@ type CustomZone struct { func (x *CustomZone) Reset() { *x = CustomZone{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[24] + mi := &file_management_proto_msgTypes[26] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1997,7 +2164,7 @@ func (x *CustomZone) String() string { func (*CustomZone) ProtoMessage() {} func (x *CustomZone) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[24] + mi := &file_management_proto_msgTypes[26] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2010,7 +2177,7 @@ func (x *CustomZone) ProtoReflect() protoreflect.Message { // Deprecated: Use CustomZone.ProtoReflect.Descriptor instead. func (*CustomZone) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{24} + return file_management_proto_rawDescGZIP(), []int{26} } func (x *CustomZone) GetDomain() string { @@ -2043,7 +2210,7 @@ type SimpleRecord struct { func (x *SimpleRecord) Reset() { *x = SimpleRecord{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[25] + mi := &file_management_proto_msgTypes[27] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2056,7 +2223,7 @@ func (x *SimpleRecord) String() string { func (*SimpleRecord) ProtoMessage() {} func (x *SimpleRecord) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[25] + mi := &file_management_proto_msgTypes[27] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2069,7 +2236,7 @@ func (x *SimpleRecord) ProtoReflect() protoreflect.Message { // Deprecated: Use SimpleRecord.ProtoReflect.Descriptor instead. func (*SimpleRecord) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{25} + return file_management_proto_rawDescGZIP(), []int{27} } func (x *SimpleRecord) GetName() string { @@ -2122,7 +2289,7 @@ type NameServerGroup struct { func (x *NameServerGroup) Reset() { *x = NameServerGroup{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[26] + mi := &file_management_proto_msgTypes[28] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2135,7 +2302,7 @@ func (x *NameServerGroup) String() string { func (*NameServerGroup) ProtoMessage() {} func (x *NameServerGroup) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[26] + mi := &file_management_proto_msgTypes[28] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2148,7 +2315,7 @@ func (x *NameServerGroup) ProtoReflect() protoreflect.Message { // Deprecated: Use NameServerGroup.ProtoReflect.Descriptor instead. func (*NameServerGroup) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{26} + return file_management_proto_rawDescGZIP(), []int{28} } func (x *NameServerGroup) GetNameServers() []*NameServer { @@ -2193,7 +2360,7 @@ type NameServer struct { func (x *NameServer) Reset() { *x = NameServer{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[27] + mi := &file_management_proto_msgTypes[29] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2206,7 +2373,7 @@ func (x *NameServer) String() string { func (*NameServer) ProtoMessage() {} func (x *NameServer) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[27] + mi := &file_management_proto_msgTypes[29] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2219,7 +2386,7 @@ func (x *NameServer) ProtoReflect() protoreflect.Message { // Deprecated: Use NameServer.ProtoReflect.Descriptor instead. func (*NameServer) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{27} + return file_management_proto_rawDescGZIP(), []int{29} } func (x *NameServer) GetIP() string { @@ -2259,7 +2426,7 @@ type FirewallRule struct { func (x *FirewallRule) Reset() { *x = FirewallRule{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[28] + mi := &file_management_proto_msgTypes[30] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2272,7 +2439,7 @@ func (x *FirewallRule) String() string { func (*FirewallRule) ProtoMessage() {} func (x *FirewallRule) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[28] + mi := &file_management_proto_msgTypes[30] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2285,7 +2452,7 @@ func (x *FirewallRule) ProtoReflect() protoreflect.Message { // Deprecated: Use FirewallRule.ProtoReflect.Descriptor instead. func (*FirewallRule) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{28} + return file_management_proto_rawDescGZIP(), []int{30} } func (x *FirewallRule) GetPeerIP() string { @@ -2335,7 +2502,7 @@ type NetworkAddress struct { func (x *NetworkAddress) Reset() { *x = NetworkAddress{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[29] + mi := &file_management_proto_msgTypes[31] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2348,7 +2515,7 @@ func (x *NetworkAddress) String() string { func (*NetworkAddress) ProtoMessage() {} func (x *NetworkAddress) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[29] + mi := &file_management_proto_msgTypes[31] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2361,7 +2528,7 @@ func (x *NetworkAddress) ProtoReflect() protoreflect.Message { // Deprecated: Use NetworkAddress.ProtoReflect.Descriptor instead. func (*NetworkAddress) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{29} + return file_management_proto_rawDescGZIP(), []int{31} } func (x *NetworkAddress) GetNetIP() string { @@ -2378,6 +2545,53 @@ func (x *NetworkAddress) GetMac() string { return "" } +type Checks struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Files []string `protobuf:"bytes,1,rep,name=Files,proto3" json:"Files,omitempty"` +} + +func (x *Checks) Reset() { + *x = Checks{} + if protoimpl.UnsafeEnabled { + mi := &file_management_proto_msgTypes[32] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Checks) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Checks) ProtoMessage() {} + +func (x *Checks) ProtoReflect() protoreflect.Message { + mi := &file_management_proto_msgTypes[32] + 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 Checks.ProtoReflect.Descriptor instead. +func (*Checks) Descriptor() ([]byte, []int) { + return file_management_proto_rawDescGZIP(), []int{32} +} + +func (x *Checks) GetFiles() []string { + if x != nil { + return x.Files + } + return nil +} + var File_management_proto protoreflect.FileDescriptor var file_management_proto_rawDesc = []byte{ @@ -2390,8 +2604,11 @@ var file_management_proto_rawDesc = []byte{ 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x77, 0x67, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x12, 0x12, 0x0a, 0x04, 0x62, 0x6f, 0x64, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x62, 0x6f, 0x64, 0x79, 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, 0x0d, 0x0a, - 0x0b, 0x53, 0x79, 0x6e, 0x63, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xbb, 0x02, 0x0a, + 0x20, 0x01, 0x28, 0x05, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x3d, 0x0a, + 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, @@ -2411,314 +2628,341 @@ var file_management_proto_rawDesc = []byte{ 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, 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, 0xa9, 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, 0x22, 0x94, 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, 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, 0xa8, 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, 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, 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, 0x81, 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, + 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, 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, 0xa8, 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, 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, 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, 0x81, 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, 0x22, 0xe2, 0x03, + 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, 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, 0xe2, 0x03, 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, 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, 0xb5, 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, 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, 0xf0, 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, 0x40, 0x0a, 0x09, - 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, - 0x22, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x46, 0x69, 0x72, - 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x2e, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, - 0x69, 0x6f, 0x6e, 0x52, 0x09, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x37, - 0x0a, 0x06, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1f, - 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x46, 0x69, 0x72, 0x65, - 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x2e, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, - 0x06, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x3d, 0x0a, 0x08, 0x50, 0x72, 0x6f, 0x74, 0x6f, - 0x63, 0x6f, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x21, 0x2e, 0x6d, 0x61, 0x6e, 0x61, - 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, - 0x75, 0x6c, 0x65, 0x2e, 0x70, 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, 0x1c, 0x0a, 0x09, 0x64, 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, 0x22, 0x1e, 0x0a, 0x06, 0x61, 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, 0x22, 0x3c, 0x0a, 0x08, 0x70, 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, 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, - 0x32, 0xd1, 0x03, 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, + 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, 0xf0, 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, 0x40, 0x0a, 0x09, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, + 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x22, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, + 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, + 0x6c, 0x65, 0x2e, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x09, 0x44, 0x69, + 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x37, 0x0a, 0x06, 0x41, 0x63, 0x74, 0x69, 0x6f, + 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1f, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, + 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, + 0x65, 0x2e, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, + 0x12, 0x3d, 0x0a, 0x08, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x04, 0x20, 0x01, + 0x28, 0x0e, 0x32, 0x21, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, + 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x2e, 0x70, 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, 0x1c, 0x0a, 0x09, 0x64, 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, 0x22, 0x1e, 0x0a, 0x06, 0x61, 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, 0x22, 0x3c, 0x0a, 0x08, 0x70, 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, 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, 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, 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, 0x42, 0x08, 0x5a, 0x06, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 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 ( @@ -2734,7 +2978,7 @@ func file_management_proto_rawDescGZIP() []byte { } var file_management_proto_enumTypes = make([]protoimpl.EnumInfo, 5) -var file_management_proto_msgTypes = make([]protoimpl.MessageInfo, 30) +var file_management_proto_msgTypes = make([]protoimpl.MessageInfo, 33) var file_management_proto_goTypes = []interface{}{ (HostConfig_Protocol)(0), // 0: management.HostConfig.Protocol (DeviceAuthorizationFlowProvider)(0), // 1: management.DeviceAuthorizationFlow.provider @@ -2744,87 +2988,97 @@ var file_management_proto_goTypes = []interface{}{ (*EncryptedMessage)(nil), // 5: management.EncryptedMessage (*SyncRequest)(nil), // 6: management.SyncRequest (*SyncResponse)(nil), // 7: management.SyncResponse - (*LoginRequest)(nil), // 8: management.LoginRequest - (*PeerKeys)(nil), // 9: management.PeerKeys - (*Environment)(nil), // 10: management.Environment - (*PeerSystemMeta)(nil), // 11: management.PeerSystemMeta - (*LoginResponse)(nil), // 12: management.LoginResponse - (*ServerKeyResponse)(nil), // 13: management.ServerKeyResponse - (*Empty)(nil), // 14: management.Empty - (*WiretrusteeConfig)(nil), // 15: management.WiretrusteeConfig - (*HostConfig)(nil), // 16: management.HostConfig - (*ProtectedHostConfig)(nil), // 17: management.ProtectedHostConfig - (*PeerConfig)(nil), // 18: management.PeerConfig - (*NetworkMap)(nil), // 19: management.NetworkMap - (*RemotePeerConfig)(nil), // 20: management.RemotePeerConfig - (*SSHConfig)(nil), // 21: management.SSHConfig - (*DeviceAuthorizationFlowRequest)(nil), // 22: management.DeviceAuthorizationFlowRequest - (*DeviceAuthorizationFlow)(nil), // 23: management.DeviceAuthorizationFlow - (*PKCEAuthorizationFlowRequest)(nil), // 24: management.PKCEAuthorizationFlowRequest - (*PKCEAuthorizationFlow)(nil), // 25: management.PKCEAuthorizationFlow - (*ProviderConfig)(nil), // 26: management.ProviderConfig - (*Route)(nil), // 27: management.Route - (*DNSConfig)(nil), // 28: management.DNSConfig - (*CustomZone)(nil), // 29: management.CustomZone - (*SimpleRecord)(nil), // 30: management.SimpleRecord - (*NameServerGroup)(nil), // 31: management.NameServerGroup - (*NameServer)(nil), // 32: management.NameServer - (*FirewallRule)(nil), // 33: management.FirewallRule - (*NetworkAddress)(nil), // 34: management.NetworkAddress - (*timestamppb.Timestamp)(nil), // 35: google.protobuf.Timestamp + (*SyncMetaRequest)(nil), // 8: management.SyncMetaRequest + (*LoginRequest)(nil), // 9: management.LoginRequest + (*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 + (*ProtectedHostConfig)(nil), // 19: management.ProtectedHostConfig + (*PeerConfig)(nil), // 20: management.PeerConfig + (*NetworkMap)(nil), // 21: management.NetworkMap + (*RemotePeerConfig)(nil), // 22: management.RemotePeerConfig + (*SSHConfig)(nil), // 23: management.SSHConfig + (*DeviceAuthorizationFlowRequest)(nil), // 24: management.DeviceAuthorizationFlowRequest + (*DeviceAuthorizationFlow)(nil), // 25: management.DeviceAuthorizationFlow + (*PKCEAuthorizationFlowRequest)(nil), // 26: management.PKCEAuthorizationFlowRequest + (*PKCEAuthorizationFlow)(nil), // 27: management.PKCEAuthorizationFlow + (*ProviderConfig)(nil), // 28: management.ProviderConfig + (*Route)(nil), // 29: management.Route + (*DNSConfig)(nil), // 30: management.DNSConfig + (*CustomZone)(nil), // 31: management.CustomZone + (*SimpleRecord)(nil), // 32: management.SimpleRecord + (*NameServerGroup)(nil), // 33: management.NameServerGroup + (*NameServer)(nil), // 34: management.NameServer + (*FirewallRule)(nil), // 35: management.FirewallRule + (*NetworkAddress)(nil), // 36: management.NetworkAddress + (*Checks)(nil), // 37: management.Checks + (*timestamppb.Timestamp)(nil), // 38: google.protobuf.Timestamp } var file_management_proto_depIdxs = []int32{ - 15, // 0: management.SyncResponse.wiretrusteeConfig:type_name -> management.WiretrusteeConfig - 18, // 1: management.SyncResponse.peerConfig:type_name -> management.PeerConfig - 20, // 2: management.SyncResponse.remotePeers:type_name -> management.RemotePeerConfig - 19, // 3: management.SyncResponse.NetworkMap:type_name -> management.NetworkMap - 11, // 4: management.LoginRequest.meta:type_name -> management.PeerSystemMeta - 9, // 5: management.LoginRequest.peerKeys:type_name -> management.PeerKeys - 34, // 6: management.PeerSystemMeta.networkAddresses:type_name -> management.NetworkAddress - 10, // 7: management.PeerSystemMeta.environment:type_name -> management.Environment - 15, // 8: management.LoginResponse.wiretrusteeConfig:type_name -> management.WiretrusteeConfig - 18, // 9: management.LoginResponse.peerConfig:type_name -> management.PeerConfig - 35, // 10: management.ServerKeyResponse.expiresAt:type_name -> google.protobuf.Timestamp - 16, // 11: management.WiretrusteeConfig.stuns:type_name -> management.HostConfig - 17, // 12: management.WiretrusteeConfig.turns:type_name -> management.ProtectedHostConfig - 16, // 13: management.WiretrusteeConfig.signal:type_name -> management.HostConfig - 0, // 14: management.HostConfig.protocol:type_name -> management.HostConfig.Protocol - 16, // 15: management.ProtectedHostConfig.hostConfig:type_name -> management.HostConfig - 21, // 16: management.PeerConfig.sshConfig:type_name -> management.SSHConfig - 18, // 17: management.NetworkMap.peerConfig:type_name -> management.PeerConfig - 20, // 18: management.NetworkMap.remotePeers:type_name -> management.RemotePeerConfig - 27, // 19: management.NetworkMap.Routes:type_name -> management.Route - 28, // 20: management.NetworkMap.DNSConfig:type_name -> management.DNSConfig - 20, // 21: management.NetworkMap.offlinePeers:type_name -> management.RemotePeerConfig - 33, // 22: management.NetworkMap.FirewallRules:type_name -> management.FirewallRule - 21, // 23: management.RemotePeerConfig.sshConfig:type_name -> management.SSHConfig - 1, // 24: management.DeviceAuthorizationFlow.Provider:type_name -> management.DeviceAuthorizationFlow.provider - 26, // 25: management.DeviceAuthorizationFlow.ProviderConfig:type_name -> management.ProviderConfig - 26, // 26: management.PKCEAuthorizationFlow.ProviderConfig:type_name -> management.ProviderConfig - 31, // 27: management.DNSConfig.NameServerGroups:type_name -> management.NameServerGroup - 29, // 28: management.DNSConfig.CustomZones:type_name -> management.CustomZone - 30, // 29: management.CustomZone.Records:type_name -> management.SimpleRecord - 32, // 30: management.NameServerGroup.NameServers:type_name -> management.NameServer - 2, // 31: management.FirewallRule.Direction:type_name -> management.FirewallRule.direction - 3, // 32: management.FirewallRule.Action:type_name -> management.FirewallRule.action - 4, // 33: management.FirewallRule.Protocol:type_name -> management.FirewallRule.protocol - 5, // 34: management.ManagementService.Login:input_type -> management.EncryptedMessage - 5, // 35: management.ManagementService.Sync:input_type -> management.EncryptedMessage - 14, // 36: management.ManagementService.GetServerKey:input_type -> management.Empty - 14, // 37: management.ManagementService.isHealthy:input_type -> management.Empty - 5, // 38: management.ManagementService.GetDeviceAuthorizationFlow:input_type -> management.EncryptedMessage - 5, // 39: management.ManagementService.GetPKCEAuthorizationFlow:input_type -> management.EncryptedMessage - 5, // 40: management.ManagementService.Login:output_type -> management.EncryptedMessage - 5, // 41: management.ManagementService.Sync:output_type -> management.EncryptedMessage - 13, // 42: management.ManagementService.GetServerKey:output_type -> management.ServerKeyResponse - 14, // 43: management.ManagementService.isHealthy:output_type -> management.Empty - 5, // 44: management.ManagementService.GetDeviceAuthorizationFlow:output_type -> management.EncryptedMessage - 5, // 45: management.ManagementService.GetPKCEAuthorizationFlow:output_type -> management.EncryptedMessage - 40, // [40:46] is the sub-list for method output_type - 34, // [34:40] is the sub-list for method input_type - 34, // [34:34] is the sub-list for extension type_name - 34, // [34:34] is the sub-list for extension extendee - 0, // [0:34] is the sub-list for field type_name + 13, // 0: management.SyncRequest.meta:type_name -> management.PeerSystemMeta + 17, // 1: management.SyncResponse.wiretrusteeConfig:type_name -> management.WiretrusteeConfig + 20, // 2: management.SyncResponse.peerConfig:type_name -> management.PeerConfig + 22, // 3: management.SyncResponse.remotePeers:type_name -> management.RemotePeerConfig + 21, // 4: management.SyncResponse.NetworkMap:type_name -> management.NetworkMap + 37, // 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 + 10, // 8: management.LoginRequest.peerKeys:type_name -> management.PeerKeys + 36, // 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 + 20, // 13: management.LoginResponse.peerConfig:type_name -> management.PeerConfig + 37, // 14: management.LoginResponse.Checks:type_name -> management.Checks + 38, // 15: management.ServerKeyResponse.expiresAt:type_name -> google.protobuf.Timestamp + 18, // 16: management.WiretrusteeConfig.stuns:type_name -> management.HostConfig + 19, // 17: management.WiretrusteeConfig.turns:type_name -> management.ProtectedHostConfig + 18, // 18: management.WiretrusteeConfig.signal:type_name -> management.HostConfig + 0, // 19: management.HostConfig.protocol:type_name -> management.HostConfig.Protocol + 18, // 20: management.ProtectedHostConfig.hostConfig:type_name -> management.HostConfig + 23, // 21: management.PeerConfig.sshConfig:type_name -> management.SSHConfig + 20, // 22: management.NetworkMap.peerConfig:type_name -> management.PeerConfig + 22, // 23: management.NetworkMap.remotePeers:type_name -> management.RemotePeerConfig + 29, // 24: management.NetworkMap.Routes:type_name -> management.Route + 30, // 25: management.NetworkMap.DNSConfig:type_name -> management.DNSConfig + 22, // 26: management.NetworkMap.offlinePeers:type_name -> management.RemotePeerConfig + 35, // 27: management.NetworkMap.FirewallRules:type_name -> management.FirewallRule + 23, // 28: management.RemotePeerConfig.sshConfig:type_name -> management.SSHConfig + 1, // 29: management.DeviceAuthorizationFlow.Provider:type_name -> management.DeviceAuthorizationFlow.provider + 28, // 30: management.DeviceAuthorizationFlow.ProviderConfig:type_name -> management.ProviderConfig + 28, // 31: management.PKCEAuthorizationFlow.ProviderConfig:type_name -> management.ProviderConfig + 33, // 32: management.DNSConfig.NameServerGroups:type_name -> management.NameServerGroup + 31, // 33: management.DNSConfig.CustomZones:type_name -> management.CustomZone + 32, // 34: management.CustomZone.Records:type_name -> management.SimpleRecord + 34, // 35: management.NameServerGroup.NameServers:type_name -> management.NameServer + 2, // 36: management.FirewallRule.Direction:type_name -> management.FirewallRule.direction + 3, // 37: management.FirewallRule.Action:type_name -> management.FirewallRule.action + 4, // 38: management.FirewallRule.Protocol:type_name -> management.FirewallRule.protocol + 5, // 39: management.ManagementService.Login:input_type -> management.EncryptedMessage + 5, // 40: management.ManagementService.Sync:input_type -> management.EncryptedMessage + 16, // 41: management.ManagementService.GetServerKey:input_type -> management.Empty + 16, // 42: management.ManagementService.isHealthy:input_type -> management.Empty + 5, // 43: management.ManagementService.GetDeviceAuthorizationFlow:input_type -> management.EncryptedMessage + 5, // 44: management.ManagementService.GetPKCEAuthorizationFlow:input_type -> management.EncryptedMessage + 5, // 45: management.ManagementService.SyncMeta:input_type -> management.EncryptedMessage + 5, // 46: management.ManagementService.Login:output_type -> management.EncryptedMessage + 5, // 47: management.ManagementService.Sync:output_type -> management.EncryptedMessage + 15, // 48: management.ManagementService.GetServerKey:output_type -> management.ServerKeyResponse + 16, // 49: management.ManagementService.isHealthy:output_type -> management.Empty + 5, // 50: management.ManagementService.GetDeviceAuthorizationFlow:output_type -> management.EncryptedMessage + 5, // 51: management.ManagementService.GetPKCEAuthorizationFlow:output_type -> management.EncryptedMessage + 16, // 52: management.ManagementService.SyncMeta:output_type -> management.Empty + 46, // [46:53] is the sub-list for method output_type + 39, // [39:46] is the sub-list for method input_type + 39, // [39:39] is the sub-list for extension type_name + 39, // [39:39] is the sub-list for extension extendee + 0, // [0:39] is the sub-list for field type_name } func init() { file_management_proto_init() } @@ -2870,7 +3124,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*LoginRequest); i { + switch v := v.(*SyncMetaRequest); i { case 0: return &v.state case 1: @@ -2882,7 +3136,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*PeerKeys); i { + switch v := v.(*LoginRequest); i { case 0: return &v.state case 1: @@ -2894,7 +3148,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Environment); i { + switch v := v.(*PeerKeys); i { case 0: return &v.state case 1: @@ -2906,7 +3160,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*PeerSystemMeta); i { + switch v := v.(*Environment); i { case 0: return &v.state case 1: @@ -2918,7 +3172,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*LoginResponse); i { + switch v := v.(*File); i { case 0: return &v.state case 1: @@ -2930,7 +3184,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ServerKeyResponse); i { + switch v := v.(*PeerSystemMeta); i { case 0: return &v.state case 1: @@ -2942,7 +3196,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Empty); i { + switch v := v.(*LoginResponse); i { case 0: return &v.state case 1: @@ -2954,7 +3208,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*WiretrusteeConfig); i { + switch v := v.(*ServerKeyResponse); i { case 0: return &v.state case 1: @@ -2966,7 +3220,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[11].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*HostConfig); i { + switch v := v.(*Empty); i { case 0: return &v.state case 1: @@ -2978,7 +3232,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[12].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ProtectedHostConfig); i { + switch v := v.(*WiretrusteeConfig); i { case 0: return &v.state case 1: @@ -2990,7 +3244,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[13].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*PeerConfig); i { + switch v := v.(*HostConfig); i { case 0: return &v.state case 1: @@ -3002,7 +3256,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[14].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*NetworkMap); i { + switch v := v.(*ProtectedHostConfig); i { case 0: return &v.state case 1: @@ -3014,7 +3268,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[15].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*RemotePeerConfig); i { + switch v := v.(*PeerConfig); i { case 0: return &v.state case 1: @@ -3026,7 +3280,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[16].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*SSHConfig); i { + switch v := v.(*NetworkMap); i { case 0: return &v.state case 1: @@ -3038,7 +3292,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[17].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DeviceAuthorizationFlowRequest); i { + switch v := v.(*RemotePeerConfig); i { case 0: return &v.state case 1: @@ -3050,7 +3304,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[18].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DeviceAuthorizationFlow); i { + switch v := v.(*SSHConfig); i { case 0: return &v.state case 1: @@ -3062,7 +3316,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[19].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*PKCEAuthorizationFlowRequest); i { + switch v := v.(*DeviceAuthorizationFlowRequest); i { case 0: return &v.state case 1: @@ -3074,7 +3328,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[20].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*PKCEAuthorizationFlow); i { + switch v := v.(*DeviceAuthorizationFlow); i { case 0: return &v.state case 1: @@ -3086,7 +3340,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[21].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ProviderConfig); i { + switch v := v.(*PKCEAuthorizationFlowRequest); i { case 0: return &v.state case 1: @@ -3098,7 +3352,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[22].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Route); i { + switch v := v.(*PKCEAuthorizationFlow); i { case 0: return &v.state case 1: @@ -3110,7 +3364,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[23].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DNSConfig); i { + switch v := v.(*ProviderConfig); i { case 0: return &v.state case 1: @@ -3122,7 +3376,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[24].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*CustomZone); i { + switch v := v.(*Route); i { case 0: return &v.state case 1: @@ -3134,7 +3388,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[25].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*SimpleRecord); i { + switch v := v.(*DNSConfig); i { case 0: return &v.state case 1: @@ -3146,7 +3400,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[26].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*NameServerGroup); i { + switch v := v.(*CustomZone); i { case 0: return &v.state case 1: @@ -3158,7 +3412,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[27].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*NameServer); i { + switch v := v.(*SimpleRecord); i { case 0: return &v.state case 1: @@ -3170,7 +3424,7 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[28].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*FirewallRule); i { + switch v := v.(*NameServerGroup); i { case 0: return &v.state case 1: @@ -3182,6 +3436,30 @@ func file_management_proto_init() { } } file_management_proto_msgTypes[29].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*NameServer); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_management_proto_msgTypes[30].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*FirewallRule); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_management_proto_msgTypes[31].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*NetworkAddress); i { case 0: return &v.state @@ -3193,6 +3471,18 @@ func file_management_proto_init() { return nil } } + file_management_proto_msgTypes[32].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Checks); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } } type x struct{} out := protoimpl.TypeBuilder{ @@ -3200,7 +3490,7 @@ func file_management_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_management_proto_rawDesc, NumEnums: 5, - NumMessages: 30, + NumMessages: 33, NumExtensions: 0, NumServices: 1, }, diff --git a/management/proto/management.proto b/management/proto/management.proto index 2cc0efa22..06b243773 100644 --- a/management/proto/management.proto +++ b/management/proto/management.proto @@ -38,6 +38,12 @@ service ManagementService { // EncryptedMessage of the request has a body of PKCEAuthorizationFlowRequest. // EncryptedMessage of the response has a body of PKCEAuthorizationFlow. rpc GetPKCEAuthorizationFlow(EncryptedMessage) returns (EncryptedMessage) {} + + // SyncMeta is used to sync metadata of the peer. + // After sync the peer if there is a change in peer posture check which needs to be evaluated by the client, + // sync meta will evaluate the checks and update the peer meta with the result. + // EncryptedMessage of the request has a body of Empty. + rpc SyncMeta(EncryptedMessage) returns (Empty) {} } message EncryptedMessage { @@ -50,7 +56,10 @@ message EncryptedMessage { int32 version = 3; } -message SyncRequest {} +message SyncRequest { + // Meta data of the peer + 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) message SyncResponse { @@ -69,6 +78,14 @@ message SyncResponse { bool remotePeersIsEmpty = 4; NetworkMap NetworkMap = 5; + + // Posture checks to be evaluated by client + repeated Checks Checks = 6; +} + +message SyncMetaRequest { + // Meta data of the peer + PeerSystemMeta meta = 1; } message LoginRequest { @@ -82,6 +99,7 @@ message LoginRequest { PeerKeys peerKeys = 4; } + // 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. message PeerKeys { @@ -100,6 +118,16 @@ message Environment { string platform = 2; } +// File represents a file on the system. +message File { + // path is the path to the file. + string path = 1; + // exist indicate whether the file exists. + bool exist = 2; + // processIsRunning indicates whether the file is a running process or not. + bool processIsRunning = 3; +} + // PeerSystemMeta is machine meta data like OS and version. message PeerSystemMeta { string hostname = 1; @@ -117,6 +145,7 @@ message PeerSystemMeta { string sysProductName = 13; string sysManufacturer = 14; Environment environment = 15; + repeated File files = 16; } message LoginResponse { @@ -124,6 +153,8 @@ message LoginResponse { WiretrusteeConfig wiretrusteeConfig = 1; // Peer local config PeerConfig peerConfig = 2; + // Posture checks to be evaluated by client + repeated Checks Checks = 3; } message ServerKeyResponse { @@ -303,6 +334,8 @@ message Route { int64 Metric = 5; bool Masquerade = 6; string NetID = 7; + repeated string Domains = 8; + bool keepRoute = 9; } // DNSConfig represents a dns.Update @@ -371,3 +404,7 @@ message NetworkAddress { string netIP = 1; string mac = 2; } + +message Checks { + repeated string Files= 1; +} diff --git a/management/proto/management_grpc.pb.go b/management/proto/management_grpc.pb.go index 5e2bcd225..badf242f5 100644 --- a/management/proto/management_grpc.pb.go +++ b/management/proto/management_grpc.pb.go @@ -43,6 +43,11 @@ type ManagementServiceClient interface { // EncryptedMessage of the request has a body of PKCEAuthorizationFlowRequest. // EncryptedMessage of the response has a body of PKCEAuthorizationFlow. GetPKCEAuthorizationFlow(ctx context.Context, in *EncryptedMessage, opts ...grpc.CallOption) (*EncryptedMessage, error) + // SyncMeta is used to sync metadata of the peer. + // After sync the peer if there is a change in peer posture check which needs to be evaluated by the client, + // sync meta will evaluate the checks and update the peer meta with the result. + // EncryptedMessage of the request has a body of Empty. + SyncMeta(ctx context.Context, in *EncryptedMessage, opts ...grpc.CallOption) (*Empty, error) } type managementServiceClient struct { @@ -130,6 +135,15 @@ func (c *managementServiceClient) GetPKCEAuthorizationFlow(ctx context.Context, return out, nil } +func (c *managementServiceClient) SyncMeta(ctx context.Context, in *EncryptedMessage, opts ...grpc.CallOption) (*Empty, error) { + out := new(Empty) + err := c.cc.Invoke(ctx, "/management.ManagementService/SyncMeta", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + // ManagementServiceServer is the server API for ManagementService service. // All implementations must embed UnimplementedManagementServiceServer // for forward compatibility @@ -159,6 +173,11 @@ type ManagementServiceServer interface { // EncryptedMessage of the request has a body of PKCEAuthorizationFlowRequest. // EncryptedMessage of the response has a body of PKCEAuthorizationFlow. GetPKCEAuthorizationFlow(context.Context, *EncryptedMessage) (*EncryptedMessage, error) + // SyncMeta is used to sync metadata of the peer. + // After sync the peer if there is a change in peer posture check which needs to be evaluated by the client, + // sync meta will evaluate the checks and update the peer meta with the result. + // EncryptedMessage of the request has a body of Empty. + SyncMeta(context.Context, *EncryptedMessage) (*Empty, error) mustEmbedUnimplementedManagementServiceServer() } @@ -184,6 +203,9 @@ func (UnimplementedManagementServiceServer) GetDeviceAuthorizationFlow(context.C func (UnimplementedManagementServiceServer) GetPKCEAuthorizationFlow(context.Context, *EncryptedMessage) (*EncryptedMessage, error) { return nil, status.Errorf(codes.Unimplemented, "method GetPKCEAuthorizationFlow not implemented") } +func (UnimplementedManagementServiceServer) SyncMeta(context.Context, *EncryptedMessage) (*Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method SyncMeta not implemented") +} func (UnimplementedManagementServiceServer) mustEmbedUnimplementedManagementServiceServer() {} // UnsafeManagementServiceServer may be embedded to opt out of forward compatibility for this service. @@ -308,6 +330,24 @@ func _ManagementService_GetPKCEAuthorizationFlow_Handler(srv interface{}, ctx co return interceptor(ctx, in, info, handler) } +func _ManagementService_SyncMeta_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(EncryptedMessage) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ManagementServiceServer).SyncMeta(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/management.ManagementService/SyncMeta", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ManagementServiceServer).SyncMeta(ctx, req.(*EncryptedMessage)) + } + return interceptor(ctx, in, info, handler) +} + // ManagementService_ServiceDesc is the grpc.ServiceDesc for ManagementService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -335,6 +375,10 @@ var ManagementService_ServiceDesc = grpc.ServiceDesc{ MethodName: "GetPKCEAuthorizationFlow", Handler: _ManagementService_GetPKCEAuthorizationFlow_Handler, }, + { + MethodName: "SyncMeta", + Handler: _ManagementService_SyncMeta_Handler, + }, }, Streams: []grpc.StreamDesc{ { diff --git a/management/server/account.go b/management/server/account.go index cb32378ed..99ed654a2 100644 --- a/management/server/account.go +++ b/management/server/account.go @@ -20,6 +20,7 @@ import ( cacheStore "github.com/eko/gocache/v3/store" "github.com/netbirdio/netbird/base62" nbdns "github.com/netbirdio/netbird/dns" + "github.com/netbirdio/netbird/management/domain" "github.com/netbirdio/netbird/management/server/account" "github.com/netbirdio/netbird/management/server/activity" "github.com/netbirdio/netbird/management/server/geolocation" @@ -102,7 +103,7 @@ type AccountManager interface { DeletePolicy(accountID, policyID, userID string) error ListPolicies(accountID, userID string) ([]*Policy, error) GetRoute(accountID string, routeID route.ID, userID string) (*route.Route, error) - CreateRoute(accountID, prefix, peerID string, peerGroupIDs []string, description string, netID route.NetID, masquerade bool, metric int, groups []string, enabled bool, userID string) (*route.Route, error) + CreateRoute(accountID string, prefix netip.Prefix, networkType route.NetworkType, domains domain.List, peerID string, peerGroupIDs []string, description string, netID route.NetID, masquerade bool, metric int, groups []string, enabled bool, userID string, keepRoute bool) (*route.Route, error) SaveRoute(accountID, userID string, route *route.Route) error DeleteRoute(accountID string, routeID route.ID, userID string) error ListRoutes(accountID, userID string) ([]*route.Route, error) @@ -117,6 +118,7 @@ type AccountManager interface { GetDNSSettings(accountID string, userID string) (*DNSSettings, error) SaveDNSSettings(accountID string, userID string, dnsSettingsToSave *DNSSettings) error GetPeer(accountID, peerID, userID string) (*nbpeer.Peer, error) + GetPeerAppliedPostureChecks(peerKey string) ([]posture.Checks, error) UpdateAccountSettings(accountID, userID string, newSettings *Settings) (*Account, error) LoginPeer(login PeerLogin) (*nbpeer.Peer, *NetworkMap, error) // used by peer gRPC API SyncPeer(sync PeerSync, account *Account) (*nbpeer.Peer, *NetworkMap, error) // used by peer gRPC API @@ -131,8 +133,9 @@ type AccountManager interface { UpdateIntegratedValidatorGroups(accountID string, userID string, groups []string) error GroupValidation(accountId string, groups []string) (bool, error) GetValidatedPeers(account *Account) (map[string]struct{}, error) - SyncAndMarkPeer(peerPubKey string, realIP net.IP) (*nbpeer.Peer, *NetworkMap, error) + SyncAndMarkPeer(peerPubKey string, meta nbpeer.PeerSystemMeta, realIP net.IP) (*nbpeer.Peer, *NetworkMap, error) CancelPeerRoutines(peer *nbpeer.Peer) error + SyncPeerMeta(peerPubKey string, meta nbpeer.PeerSystemMeta) error FindExistingPostureCheck(accountID string, checks *posture.ChecksDefinition) (*posture.Checks, error) } @@ -275,7 +278,7 @@ func (a *Account) getRoutesToSync(peerID string, aclPeers []*nbpeer.Peer) []*rou routes, peerDisabledRoutes := a.getRoutingPeerRoutes(peerID) peerRoutesMembership := make(lookupMap) for _, r := range append(routes, peerDisabledRoutes...) { - peerRoutesMembership[string(route.GetHAUniqueID(r))] = struct{}{} + peerRoutesMembership[string(r.GetHAUniqueID())] = struct{}{} } groupListMap := a.getPeerGroups(peerID) @@ -293,7 +296,7 @@ func (a *Account) getRoutesToSync(peerID string, aclPeers []*nbpeer.Peer) []*rou func (a *Account) filterRoutesFromPeersOfSameHAGroup(routes []*route.Route, peerMemberships lookupMap) []*route.Route { var filteredRoutes []*route.Route for _, r := range routes { - _, found := peerMemberships[string(route.GetHAUniqueID(r))] + _, found := peerMemberships[string(r.GetHAUniqueID())] if !found { filteredRoutes = append(filteredRoutes, r) } @@ -376,11 +379,13 @@ func (a *Account) getRoutingPeerRoutes(peerID string) (enabledRoutes []*route.Ro return enabledRoutes, disabledRoutes } -// GetRoutesByPrefix return list of routes by account and route prefix -func (a *Account) GetRoutesByPrefix(prefix netip.Prefix) []*route.Route { +// GetRoutesByPrefixOrDomains return list of routes by account and route prefix +func (a *Account) GetRoutesByPrefixOrDomains(prefix netip.Prefix, domains domain.List) []*route.Route { var routes []*route.Route for _, r := range a.Routes { - if r.Network.String() == prefix.String() { + if r.IsDynamic() && r.Domains.PunycodeString() == domains.PunycodeString() { + routes = append(routes, r) + } else if r.Network.String() == prefix.String() { routes = append(routes, r) } } @@ -1850,7 +1855,7 @@ func (am *DefaultAccountManager) getAccountWithAuthorizationClaims(claims jwtcla } } -func (am *DefaultAccountManager) SyncAndMarkPeer(peerPubKey string, realIP net.IP) (*nbpeer.Peer, *NetworkMap, error) { +func (am *DefaultAccountManager) SyncAndMarkPeer(peerPubKey string, meta nbpeer.PeerSystemMeta, realIP net.IP) (*nbpeer.Peer, *NetworkMap, error) { accountID, err := am.Store.GetAccountIDByPeerPubKey(peerPubKey) if err != nil { if errStatus, ok := status.FromError(err); ok && errStatus.Type() == status.NotFound { @@ -1867,7 +1872,7 @@ func (am *DefaultAccountManager) SyncAndMarkPeer(peerPubKey string, realIP net.I return nil, nil, err } - peer, netMap, err := am.SyncPeer(PeerSync{WireGuardPubKey: peerPubKey}, account) + peer, netMap, err := am.SyncPeer(PeerSync{WireGuardPubKey: peerPubKey, Meta: meta}, account) if err != nil { return nil, nil, err } @@ -1906,6 +1911,27 @@ func (am *DefaultAccountManager) CancelPeerRoutines(peer *nbpeer.Peer) error { } +func (am *DefaultAccountManager) SyncPeerMeta(peerPubKey string, meta nbpeer.PeerSystemMeta) error { + accountID, err := am.Store.GetAccountIDByPeerPubKey(peerPubKey) + if err != nil { + return err + } + + unlock := am.Store.AcquireAccountReadLock(accountID) + defer unlock() + + account, err := am.Store.GetAccount(accountID) + if err != nil { + return err + } + + _, _, err = am.SyncPeer(PeerSync{WireGuardPubKey: peerPubKey, Meta: meta, UpdateAccountPeers: true}, account) + if err != nil { + return mapError(err) + } + return nil +} + // GetAllConnectedPeers returns connected peers based on peersUpdateManager.GetAllConnectedPeers() func (am *DefaultAccountManager) GetAllConnectedPeers() (map[string]struct{}, error) { return am.peersUpdateManager.GetAllConnectedPeers(), nil diff --git a/management/server/account_test.go b/management/server/account_test.go index a5ba8fdcf..476a4f823 100644 --- a/management/server/account_test.go +++ b/management/server/account_test.go @@ -1435,7 +1435,7 @@ func TestFileStore_GetRoutesByPrefix(t *testing.T) { }, } - routes := account.GetRoutesByPrefix(prefix) + routes := account.GetRoutesByPrefixOrDomains(prefix, nil) assert.Len(t, routes, 2) routeIDs := make(map[route.ID]struct{}, 2) diff --git a/management/server/grpcserver.go b/management/server/grpcserver.go index 32989df7d..5501c1925 100644 --- a/management/server/grpcserver.go +++ b/management/server/grpcserver.go @@ -11,6 +11,7 @@ import ( pb "github.com/golang/protobuf/proto" // nolint "github.com/golang/protobuf/ptypes/timestamp" "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/realip" + "github.com/netbirdio/netbird/management/server/posture" log "github.com/sirupsen/logrus" "golang.zx2c4.com/wireguard/wgctrl/wgtypes" "google.golang.org/grpc/codes" @@ -134,7 +135,11 @@ func (s *GRPCServer) Sync(req *proto.EncryptedMessage, srv proto.ManagementServi return err } - peer, netMap, err := s.accountManager.SyncAndMarkPeer(peerKey.String(), realIP) + if syncReq.GetMeta() == nil { + log.Tracef("peer system meta has to be provided on sync. Peer %s, remote addr %s", peerKey.String(), realIP) + } + + peer, netMap, err := s.accountManager.SyncAndMarkPeer(peerKey.String(), extractPeerMeta(syncReq.GetMeta()), realIP) if err != nil { return mapError(err) } @@ -157,12 +162,15 @@ func (s *GRPCServer) Sync(req *proto.EncryptedMessage, srv proto.ManagementServi s.appMetrics.GRPCMetrics().CountSyncRequestDuration(time.Since(reqStart)) } - // keep a connection to the peer and send updates when available + return s.handleUpdates(peerKey, peer, updates, srv) +} + +// handleUpdates sends updates to the connected peer until the updates channel is closed. +func (s *GRPCServer) handleUpdates(peerKey wgtypes.Key, peer *nbpeer.Peer, updates chan *UpdateMessage, srv proto.ManagementService_SyncServer) error { for { select { // condition when there are some updates case update, open := <-updates: - if s.appMetrics != nil { s.appMetrics.GRPCMetrics().UpdateChannelQueueLength(len(updates) + 1) } @@ -174,21 +182,10 @@ func (s *GRPCServer) Sync(req *proto.EncryptedMessage, srv proto.ManagementServi } log.Debugf("received an update for peer %s", peerKey.String()) - encryptedResp, err := encryption.EncryptMessage(peerKey, s.wgKey, update.Update) - if err != nil { - s.cancelPeerRoutines(peer) - return status.Errorf(codes.Internal, "failed processing update message") + if err := s.sendUpdate(peerKey, peer, update, srv); err != nil { + return err } - err = srv.SendMsg(&proto.EncryptedMessage{ - WgPubKey: s.wgKey.PublicKey().String(), - Body: encryptedResp, - }) - if err != nil { - s.cancelPeerRoutines(peer) - return status.Errorf(codes.Internal, "failed sending update message") - } - log.Debugf("sent an update to peer %s", peerKey.String()) // condition when client <-> server connection has been terminated case <-srv.Context().Done(): // happens when connection drops, e.g. client disconnects @@ -199,6 +196,26 @@ func (s *GRPCServer) Sync(req *proto.EncryptedMessage, srv proto.ManagementServi } } +// sendUpdate encrypts the update message using the peer key and the server's wireguard key, +// then sends the encrypted message to the connected peer via the sync server. +func (s *GRPCServer) sendUpdate(peerKey wgtypes.Key, peer *nbpeer.Peer, update *UpdateMessage, srv proto.ManagementService_SyncServer) error { + encryptedResp, err := encryption.EncryptMessage(peerKey, s.wgKey, update.Update) + if err != nil { + s.cancelPeerRoutines(peer) + return status.Errorf(codes.Internal, "failed processing update message") + } + err = srv.SendMsg(&proto.EncryptedMessage{ + WgPubKey: s.wgKey.PublicKey().String(), + Body: encryptedResp, + }) + if err != nil { + s.cancelPeerRoutines(peer) + return status.Errorf(codes.Internal, "failed sending update message") + } + log.Debugf("sent an update to peer %s", peerKey.String()) + return nil +} + func (s *GRPCServer) cancelPeerRoutines(peer *nbpeer.Peer) { s.peersUpdateManager.CloseChannel(peer.ID) s.turnCredentialsManager.CancelRefresh(peer.ID) @@ -250,14 +267,18 @@ func mapError(err error) error { return status.Errorf(codes.Internal, "failed handling request") } -func extractPeerMeta(loginReq *proto.LoginRequest) nbpeer.PeerSystemMeta { - osVersion := loginReq.GetMeta().GetOSVersion() - if osVersion == "" { - osVersion = loginReq.GetMeta().GetCore() +func extractPeerMeta(meta *proto.PeerSystemMeta) nbpeer.PeerSystemMeta { + if meta == nil { + return nbpeer.PeerSystemMeta{} } - networkAddresses := make([]nbpeer.NetworkAddress, 0, len(loginReq.GetMeta().GetNetworkAddresses())) - for _, addr := range loginReq.GetMeta().GetNetworkAddresses() { + osVersion := meta.GetOSVersion() + if osVersion == "" { + osVersion = meta.GetCore() + } + + networkAddresses := make([]nbpeer.NetworkAddress, 0, len(meta.GetNetworkAddresses())) + for _, addr := range meta.GetNetworkAddresses() { netAddr, err := netip.ParsePrefix(addr.GetNetIP()) if err != nil { log.Warnf("failed to parse netip address, %s: %v", addr.GetNetIP(), err) @@ -269,24 +290,34 @@ func extractPeerMeta(loginReq *proto.LoginRequest) nbpeer.PeerSystemMeta { }) } + files := make([]nbpeer.File, 0, len(meta.GetFiles())) + for _, file := range meta.GetFiles() { + files = append(files, nbpeer.File{ + Path: file.GetPath(), + Exist: file.GetExist(), + ProcessIsRunning: file.GetProcessIsRunning(), + }) + } + return nbpeer.PeerSystemMeta{ - Hostname: loginReq.GetMeta().GetHostname(), - GoOS: loginReq.GetMeta().GetGoOS(), - Kernel: loginReq.GetMeta().GetKernel(), - Platform: loginReq.GetMeta().GetPlatform(), - OS: loginReq.GetMeta().GetOS(), + Hostname: meta.GetHostname(), + GoOS: meta.GetGoOS(), + Kernel: meta.GetKernel(), + Platform: meta.GetPlatform(), + OS: meta.GetOS(), OSVersion: osVersion, - WtVersion: loginReq.GetMeta().GetWiretrusteeVersion(), - UIVersion: loginReq.GetMeta().GetUiVersion(), - KernelVersion: loginReq.GetMeta().GetKernelVersion(), + WtVersion: meta.GetWiretrusteeVersion(), + UIVersion: meta.GetUiVersion(), + KernelVersion: meta.GetKernelVersion(), NetworkAddresses: networkAddresses, - SystemSerialNumber: loginReq.GetMeta().GetSysSerialNumber(), - SystemProductName: loginReq.GetMeta().GetSysProductName(), - SystemManufacturer: loginReq.GetMeta().GetSysManufacturer(), + SystemSerialNumber: meta.GetSysSerialNumber(), + SystemProductName: meta.GetSysProductName(), + SystemManufacturer: meta.GetSysManufacturer(), Environment: nbpeer.Environment{ - Cloud: loginReq.GetMeta().GetEnvironment().GetCloud(), - Platform: loginReq.GetMeta().GetEnvironment().GetPlatform(), + Cloud: meta.GetEnvironment().GetCloud(), + Platform: meta.GetEnvironment().GetPlatform(), }, + Files: files, } } @@ -335,24 +366,11 @@ func (s *GRPCServer) Login(ctx context.Context, req *proto.EncryptedMessage) (*p return nil, msg } - userID := "" - // JWT token is not always provided, it is fine for userID to be empty cuz it might be that peer is already registered, - // or it uses a setup key to register. - - if loginReq.GetJwtToken() != "" { - for i := 0; i < 3; i++ { - userID, err = s.validateToken(loginReq.GetJwtToken()) - if err == nil { - break - } - log.Warnf("failed validating JWT token sent from peer %s with error %v. "+ - "Trying again as it may be due to the IdP cache issue", peerKey, err) - time.Sleep(200 * time.Millisecond) - } - if err != nil { - return nil, err - } + userID, err := s.processJwtToken(loginReq, peerKey) + if err != nil { + return nil, err } + var sshKey []byte if loginReq.GetPeerKeys() != nil { sshKey = loginReq.GetPeerKeys().GetSshPubKey() @@ -361,12 +379,11 @@ func (s *GRPCServer) Login(ctx context.Context, req *proto.EncryptedMessage) (*p peer, netMap, err := s.accountManager.LoginPeer(PeerLogin{ WireGuardPubKey: peerKey.String(), SSHKey: string(sshKey), - Meta: extractPeerMeta(loginReq), + Meta: extractPeerMeta(loginReq.GetMeta()), UserID: userID, SetupKey: loginReq.GetSetupKey(), ConnectionIP: realIP, }) - if err != nil { log.Warnf("failed logging in peer %s: %s", peerKey, err) return nil, mapError(err) @@ -381,6 +398,7 @@ func (s *GRPCServer) Login(ctx context.Context, req *proto.EncryptedMessage) (*p loginResp := &proto.LoginResponse{ WiretrusteeConfig: toWiretrusteeConfig(s.config, nil), PeerConfig: toPeerConfig(peer, netMap.Network, s.accountManager.GetDNSDomain()), + Checks: toProtocolChecks(s.accountManager, peerKey.String()), } encryptedResp, err := encryption.EncryptMessage(peerKey, s.wgKey, loginResp) if err != nil { @@ -394,6 +412,31 @@ func (s *GRPCServer) Login(ctx context.Context, req *proto.EncryptedMessage) (*p }, nil } +// processJwtToken validates the existence of a JWT token in the login request, and returns the corresponding user ID if +// the token is valid. +// +// The user ID can be empty if the token is not provided, which is acceptable if the peer is already +// registered or if it uses a setup key to register. +func (s *GRPCServer) processJwtToken(loginReq *proto.LoginRequest, peerKey wgtypes.Key) (string, error) { + userID := "" + if loginReq.GetJwtToken() != "" { + var err error + for i := 0; i < 3; i++ { + userID, err = s.validateToken(loginReq.GetJwtToken()) + if err == nil { + break + } + log.Warnf("failed validating JWT token sent from peer %s with error %v. "+ + "Trying again as it may be due to the IdP cache issue", peerKey.String(), err) + time.Sleep(200 * time.Millisecond) + } + if err != nil { + return "", err + } + } + return userID, nil +} + func ToResponseProto(configProto Protocol) proto.HostConfig_Protocol { switch configProto { case UDP: @@ -477,7 +520,7 @@ func toRemotePeerConfig(peers []*nbpeer.Peer, dnsName string) []*proto.RemotePee return remotePeers } -func toSyncResponse(config *Config, peer *nbpeer.Peer, turnCredentials *TURNCredentials, networkMap *NetworkMap, dnsName string) *proto.SyncResponse { +func toSyncResponse(accountManager AccountManager, config *Config, peer *nbpeer.Peer, turnCredentials *TURNCredentials, networkMap *NetworkMap, dnsName string) *proto.SyncResponse { wtConfig := toWiretrusteeConfig(config, turnCredentials) pConfig := toPeerConfig(peer, networkMap.Network, dnsName) @@ -508,6 +551,7 @@ func toSyncResponse(config *Config, peer *nbpeer.Peer, turnCredentials *TURNCred FirewallRules: firewallRules, FirewallRulesIsEmpty: len(firewallRules) == 0, }, + Checks: toProtocolChecks(accountManager, peer.Key), } } @@ -526,7 +570,7 @@ func (s *GRPCServer) sendInitialSync(peerKey wgtypes.Key, peer *nbpeer.Peer, net } else { turnCredentials = nil } - plainResp := toSyncResponse(s.config, peer, turnCredentials, networkMap, s.accountManager.GetDNSDomain()) + plainResp := toSyncResponse(s.accountManager, s.config, peer, turnCredentials, networkMap, s.accountManager.GetDNSDomain()) encryptedResp, err := encryption.EncryptMessage(peerKey, s.wgKey, plainResp) if err != nil { @@ -643,3 +687,67 @@ func (s *GRPCServer) GetPKCEAuthorizationFlow(_ context.Context, req *proto.Encr Body: encryptedResp, }, nil } + +// SyncMeta endpoint is used to synchronize peer's system metadata and notifies the connected, +// peer's under the same account of any updates. +func (s *GRPCServer) SyncMeta(ctx context.Context, req *proto.EncryptedMessage) (*proto.Empty, error) { + realIP := getRealIP(ctx) + log.Debugf("Sync meta request from peer [%s] [%s]", req.WgPubKey, realIP.String()) + + syncMetaReq := &proto.SyncMetaRequest{} + peerKey, err := s.parseRequest(req, syncMetaReq) + if err != nil { + return nil, err + } + + if syncMetaReq.GetMeta() == nil { + msg := status.Errorf(codes.FailedPrecondition, + "peer system meta has to be provided on sync. Peer %s, remote addr %s", peerKey.String(), realIP) + log.Warn(msg) + return nil, msg + } + + err = s.accountManager.SyncPeerMeta(peerKey.String(), extractPeerMeta(syncMetaReq.GetMeta())) + if err != nil { + return nil, mapError(err) + } + + return &proto.Empty{}, nil +} + +// toProtocolChecks returns posture checks for the peer that needs to be evaluated on the client side. +func toProtocolChecks(accountManager AccountManager, peerKey string) []*proto.Checks { + postureChecks, err := accountManager.GetPeerAppliedPostureChecks(peerKey) + if err != nil { + log.Errorf("failed getting peer's: %s posture checks: %v", peerKey, err) + return nil + } + + protoChecks := make([]*proto.Checks, 0) + for _, postureCheck := range postureChecks { + protoChecks = append(protoChecks, toProtocolCheck(postureCheck)) + } + + return protoChecks +} + +// toProtocolCheck converts a posture.Checks to a proto.Checks. +func toProtocolCheck(postureCheck posture.Checks) *proto.Checks { + protoCheck := &proto.Checks{} + + if check := postureCheck.Checks.ProcessCheck; check != nil { + for _, process := range check.Processes { + if process.LinuxPath != "" { + protoCheck.Files = append(protoCheck.Files, process.LinuxPath) + } + if process.MacPath != "" { + protoCheck.Files = append(protoCheck.Files, process.MacPath) + } + if process.WindowsPath != "" { + protoCheck.Files = append(protoCheck.Files, process.WindowsPath) + } + } + } + + return protoCheck +} diff --git a/management/server/http/api/openapi.yml b/management/server/http/api/openapi.yml index aeaef6f64..30cb19c0c 100644 --- a/management/server/http/api/openapi.yml +++ b/management/server/http/api/openapi.yml @@ -817,6 +817,8 @@ components: $ref: '#/components/schemas/GeoLocationCheck' peer_network_range_check: $ref: '#/components/schemas/PeerNetworkRangeCheck' + process_check: + $ref: '#/components/schemas/ProcessCheck' NBVersionCheck: description: Posture check for the version of NetBird type: object @@ -905,6 +907,32 @@ components: required: - ranges - action + ProcessCheck: + description: Posture Check for binaries exist and are running in the peer’s system + type: object + properties: + processes: + type: array + items: + $ref: '#/components/schemas/Process' + required: + - processes + Process: + description: Describes the operational activity within a peer's system. + type: object + properties: + linux_path: + description: Path to the process executable file in a Linux operating system + type: string + example: "/usr/local/bin/netbird" + mac_path: + description: Path to the process executable file in a Mac operating system + type: string + example: "/Applications/NetBird.app/Contents/MacOS/netbird" + windows_path: + description: Path to the process executable file in a Windows operating system + type: string + example: "C:\ProgramData\NetBird\netbird.exe" Location: description: Describe geographical location information type: object @@ -995,9 +1023,17 @@ components: type: string example: chacbco6lnnbn6cg5s91 network: - description: Network range in CIDR format + description: Network range in CIDR format, Conflicts with domains type: string example: 10.64.0.0/24 + domains: + description: Domain list to be dynamically resolved. Conflicts with network + type: array + items: + type: string + minLength: 1 + maxLength: 255 + example: "example.com" metric: description: Route metric number. Lowest number has higher priority type: integer @@ -1014,6 +1050,10 @@ components: items: type: string example: "chacdk86lnnboviihd70" + keep_route: + description: Indicate if the route should be kept after a domain doesn't resolve that IP anymore + type: boolean + example: true required: - id - description @@ -1022,10 +1062,13 @@ components: # Only one property has to be set #- peer #- peer_groups - - network + # Only one property has to be set + #- network + #- domains - metric - masquerade - groups + - keep_route Route: allOf: - type: object @@ -1035,7 +1078,7 @@ components: type: string example: chacdk86lnnboviihd7g network_type: - description: Network type indicating if it is IPv4 or IPv6 + description: Network type indicating if it is a domain route or a IPv4/IPv6 route type: string example: IPv4 required: diff --git a/management/server/http/api/types.gen.go b/management/server/http/api/types.gen.go index e378213a1..f731356ee 100644 --- a/management/server/http/api/types.gen.go +++ b/management/server/http/api/types.gen.go @@ -225,6 +225,9 @@ type Checks struct { // PeerNetworkRangeCheck Posture check for allow or deny access based on peer local network addresses PeerNetworkRangeCheck *PeerNetworkRangeCheck `json:"peer_network_range_check,omitempty"` + + // ProcessCheck Posture Check for binaries exist and are running in the peer’s system + ProcessCheck *ProcessCheck `json:"process_check,omitempty"` } // City Describe city geographical location information @@ -949,11 +952,31 @@ type PostureCheckUpdate struct { Name string `json:"name"` } +// Process Describes the operational activity within a peer's system. +type Process struct { + // LinuxPath Path to the process executable file in a Linux operating system + LinuxPath *string `json:"linux_path,omitempty"` + + // MacPath Path to the process executable file in a Mac operating system + MacPath *string `json:"mac_path,omitempty"` + + // WindowsPath Path to the process executable file in a Windows operating system + WindowsPath *string `json:"windows_path,omitempty"` +} + +// ProcessCheck Posture Check for binaries exist and are running in the peer’s system +type ProcessCheck struct { + Processes []Process `json:"processes"` +} + // Route defines model for Route. type Route struct { // Description Route description Description string `json:"description"` + // Domains Domain list to be dynamically resolved. Conflicts with network + Domains *[]string `json:"domains,omitempty"` + // Enabled Route status Enabled bool `json:"enabled"` @@ -963,19 +986,22 @@ type Route struct { // Id Route Id Id string `json:"id"` + // KeepRoute Indicate if the route should be kept after a domain doesn't resolve that IP anymore + KeepRoute bool `json:"keep_route"` + // Masquerade Indicate if peer should masquerade traffic to this route's prefix Masquerade bool `json:"masquerade"` // Metric Route metric number. Lowest number has higher priority Metric int `json:"metric"` - // Network Network range in CIDR format - Network string `json:"network"` + // Network Network range in CIDR format, Conflicts with domains + Network *string `json:"network,omitempty"` // NetworkId Route network identifier, to group HA routes NetworkId string `json:"network_id"` - // NetworkType Network type indicating if it is IPv4 or IPv6 + // NetworkType Network type indicating if it is a domain route or a IPv4/IPv6 route NetworkType string `json:"network_type"` // Peer Peer Identifier associated with route. This property can not be set together with `peer_groups` @@ -990,20 +1016,26 @@ type RouteRequest struct { // Description Route description Description string `json:"description"` + // Domains Domain list to be dynamically resolved. Conflicts with network + Domains *[]string `json:"domains,omitempty"` + // Enabled Route status Enabled bool `json:"enabled"` // Groups Group IDs containing routing peers Groups []string `json:"groups"` + // KeepRoute Indicate if the route should be kept after a domain doesn't resolve that IP anymore + KeepRoute bool `json:"keep_route"` + // Masquerade Indicate if peer should masquerade traffic to this route's prefix Masquerade bool `json:"masquerade"` // Metric Route metric number. Lowest number has higher priority Metric int `json:"metric"` - // Network Network range in CIDR format - Network string `json:"network"` + // Network Network range in CIDR format, Conflicts with domains + Network *string `json:"network,omitempty"` // NetworkId Route network identifier, to group HA routes NetworkId string `json:"network_id"` diff --git a/management/server/http/geolocations_handler.go b/management/server/http/geolocations_handler.go index 070aa6350..cf961267b 100644 --- a/management/server/http/geolocations_handler.go +++ b/management/server/http/geolocations_handler.go @@ -2,6 +2,7 @@ package http import ( "net/http" + "regexp" "github.com/gorilla/mux" @@ -13,6 +14,10 @@ import ( "github.com/netbirdio/netbird/management/server/status" ) +var ( + countryCodeRegex = regexp.MustCompile("^[a-zA-Z]{2}$") +) + // GeolocationsHandler is a handler that returns locations. type GeolocationsHandler struct { accountManager server.AccountManager @@ -73,8 +78,8 @@ func (l *GeolocationsHandler) GetCitiesByCountry(w http.ResponseWriter, r *http. } if l.geolocationManager == nil { - // TODO: update error message to include geo db self hosted doc link when ready - util.WriteError(status.Errorf(status.PreconditionFailed, "Geo location database is not initialized"), w) + util.WriteError(status.Errorf(status.PreconditionFailed, "Geo location database is not initialized. "+ + "Check the self-hosted Geo database documentation at https://docs.netbird.io/selfhosted/geo-support"), w) return } diff --git a/management/server/http/pat_handler_test.go b/management/server/http/pat_handler_test.go index 45fda0a55..5058b4110 100644 --- a/management/server/http/pat_handler_test.go +++ b/management/server/http/pat_handler_test.go @@ -27,12 +27,12 @@ const ( notFoundUserID = "notFoundUserID" existingTokenID = "existingTokenID" notFoundTokenID = "notFoundTokenID" - domain = "hotmail.com" + testDomain = "hotmail.com" ) var testAccount = &server.Account{ Id: existingAccountID, - Domain: domain, + Domain: testDomain, Users: map[string]*server.User{ existingUserID: { Id: existingUserID, @@ -117,7 +117,7 @@ func initPATTestData() *PATHandler { jwtclaims.WithFromRequestContext(func(r *http.Request) jwtclaims.AuthorizationClaims { return jwtclaims.AuthorizationClaims{ UserId: existingUserID, - Domain: domain, + Domain: testDomain, AccountId: testNSGroupAccountID, } }), diff --git a/management/server/http/posture_checks_handler.go b/management/server/http/posture_checks_handler.go index f256d9ee0..9051e8d18 100644 --- a/management/server/http/posture_checks_handler.go +++ b/management/server/http/posture_checks_handler.go @@ -3,8 +3,6 @@ package http import ( "encoding/json" "net/http" - "regexp" - "slices" "github.com/gorilla/mux" @@ -17,10 +15,6 @@ import ( "github.com/netbirdio/netbird/management/server/status" ) -var ( - countryCodeRegex = regexp.MustCompile("^[a-zA-Z]{2}$") -) - // PostureChecksHandler is a handler that returns posture checks of the account. type PostureChecksHandler struct { accountManager server.AccountManager @@ -163,23 +157,20 @@ func (p *PostureChecksHandler) savePostureChecks( user *server.User, postureChecksID string, ) { + var ( + err error + req api.PostureCheckUpdate + ) - var req api.PostureCheckUpdate - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + if err = json.NewDecoder(r.Body).Decode(&req); err != nil { util.WriteErrorResponse("couldn't parse JSON request", http.StatusBadRequest, w) return } - err := validatePostureChecksUpdate(req) - if err != nil { - util.WriteErrorResponse(err.Error(), http.StatusBadRequest, w) - return - } - if geoLocationCheck := req.Checks.GeoLocationCheck; geoLocationCheck != nil { if p.geolocationManager == nil { - // TODO: update error message to include geo db self hosted doc link when ready - util.WriteError(status.Errorf(status.PreconditionFailed, "Geo location database is not initialized"), w) + util.WriteError(status.Errorf(status.PreconditionFailed, "Geo location database is not initialized. "+ + "Check the self-hosted Geo database documentation at https://docs.netbird.io/selfhosted/geo-support"), w) return } } @@ -197,69 +188,3 @@ func (p *PostureChecksHandler) savePostureChecks( util.WriteJSONObject(w, postureChecks.ToAPIResponse()) } - -func validatePostureChecksUpdate(req api.PostureCheckUpdate) error { - if req.Name == "" { - return status.Errorf(status.InvalidArgument, "posture checks name shouldn't be empty") - } - - if req.Checks == nil || (req.Checks.NbVersionCheck == nil && req.Checks.OsVersionCheck == nil && - req.Checks.GeoLocationCheck == nil && req.Checks.PeerNetworkRangeCheck == nil) { - return status.Errorf(status.InvalidArgument, "posture checks shouldn't be empty") - } - - if req.Checks.NbVersionCheck != nil && req.Checks.NbVersionCheck.MinVersion == "" { - return status.Errorf(status.InvalidArgument, "minimum version for NetBird's version check shouldn't be empty") - } - - if osVersionCheck := req.Checks.OsVersionCheck; osVersionCheck != nil { - emptyOS := osVersionCheck.Android == nil && osVersionCheck.Darwin == nil && osVersionCheck.Ios == nil && - osVersionCheck.Linux == nil && osVersionCheck.Windows == nil - emptyMinVersion := osVersionCheck.Android != nil && osVersionCheck.Android.MinVersion == "" || - osVersionCheck.Darwin != nil && osVersionCheck.Darwin.MinVersion == "" || - osVersionCheck.Ios != nil && osVersionCheck.Ios.MinVersion == "" || - osVersionCheck.Linux != nil && osVersionCheck.Linux.MinKernelVersion == "" || - osVersionCheck.Windows != nil && osVersionCheck.Windows.MinKernelVersion == "" - if emptyOS || emptyMinVersion { - return status.Errorf(status.InvalidArgument, - "minimum version for at least one OS in the OS version check shouldn't be empty") - } - } - - if geoLocationCheck := req.Checks.GeoLocationCheck; geoLocationCheck != nil { - if geoLocationCheck.Action == "" { - return status.Errorf(status.InvalidArgument, "action for geolocation check shouldn't be empty") - } - allowedActions := []api.GeoLocationCheckAction{api.GeoLocationCheckActionAllow, api.GeoLocationCheckActionDeny} - if !slices.Contains(allowedActions, geoLocationCheck.Action) { - return status.Errorf(status.InvalidArgument, "action for geolocation check is not valid value") - } - if len(geoLocationCheck.Locations) == 0 { - return status.Errorf(status.InvalidArgument, "locations for geolocation check shouldn't be empty") - } - for _, loc := range geoLocationCheck.Locations { - if loc.CountryCode == "" { - return status.Errorf(status.InvalidArgument, "country code for geolocation check shouldn't be empty") - } - if !countryCodeRegex.MatchString(loc.CountryCode) { - return status.Errorf(status.InvalidArgument, "country code must be 2 letters (ISO 3166-1 alpha-2 format)") - } - } - } - - if peerNetworkRangeCheck := req.Checks.PeerNetworkRangeCheck; peerNetworkRangeCheck != nil { - if peerNetworkRangeCheck.Action == "" { - return status.Errorf(status.InvalidArgument, "action for peer network range check shouldn't be empty") - } - - allowedActions := []api.PeerNetworkRangeCheckAction{api.PeerNetworkRangeCheckActionAllow, api.PeerNetworkRangeCheckActionDeny} - if !slices.Contains(allowedActions, peerNetworkRangeCheck.Action) { - return status.Errorf(status.InvalidArgument, "action for peer network range check is not valid value") - } - if len(peerNetworkRangeCheck.Ranges) == 0 { - return status.Errorf(status.InvalidArgument, "network ranges for peer network range check shouldn't be empty") - } - } - - return nil -} diff --git a/management/server/http/posture_checks_handler_test.go b/management/server/http/posture_checks_handler_test.go index 70e803214..f0b503f1a 100644 --- a/management/server/http/posture_checks_handler_test.go +++ b/management/server/http/posture_checks_handler_test.go @@ -43,6 +43,11 @@ func initPostureChecksTestData(postureChecks ...*posture.Checks) *PostureChecksH SavePostureChecksFunc: func(accountID, userID string, postureChecks *posture.Checks) error { postureChecks.ID = "postureCheck" testPostureChecks[postureChecks.ID] = postureChecks + + if err := postureChecks.Validate(); err != nil { + return status.Errorf(status.InvalidArgument, err.Error()) + } + return nil }, DeletePostureChecksFunc: func(accountID, postureChecksID, userID string) error { @@ -433,6 +438,45 @@ func TestPostureCheckUpdate(t *testing.T) { handler.geolocationManager = nil }, }, + { + name: "Create Posture Checks Process Check", + requestType: http.MethodPost, + requestPath: "/api/posture-checks", + requestBody: bytes.NewBuffer( + []byte(`{ + "name": "default", + "description": "default", + "checks": { + "process_check": { + "processes": [ + { + "linux_path": "/usr/local/bin/netbird", + "mac_path": "/Applications/NetBird.app/Contents/MacOS/netbird", + "windows_path": "C:\\ProgramData\\NetBird\\netbird.exe" + } + ] + } + } + }`)), + expectedStatus: http.StatusOK, + expectedBody: true, + expectedPostureCheck: &api.PostureCheck{ + Id: "postureCheck", + Name: "default", + Description: str("default"), + Checks: api.Checks{ + ProcessCheck: &api.ProcessCheck{ + Processes: []api.Process{ + { + LinuxPath: str("/usr/local/bin/netbird"), + MacPath: str("/Applications/NetBird.app/Contents/MacOS/netbird"), + WindowsPath: str("C:\\ProgramData\\NetBird\\netbird.exe"), + }, + }, + }, + }, + }, + }, { name: "Create Posture Checks Invalid Check", requestType: http.MethodPost, @@ -446,7 +490,7 @@ func TestPostureCheckUpdate(t *testing.T) { } } }`)), - expectedStatus: http.StatusBadRequest, + expectedStatus: http.StatusUnprocessableEntity, expectedBody: false, }, { @@ -461,7 +505,7 @@ func TestPostureCheckUpdate(t *testing.T) { } } }`)), - expectedStatus: http.StatusBadRequest, + expectedStatus: http.StatusUnprocessableEntity, expectedBody: false, }, { @@ -475,7 +519,7 @@ func TestPostureCheckUpdate(t *testing.T) { "nb_version_check": {} } }`)), - expectedStatus: http.StatusBadRequest, + expectedStatus: http.StatusUnprocessableEntity, expectedBody: false, }, { @@ -489,7 +533,7 @@ func TestPostureCheckUpdate(t *testing.T) { "geo_location_check": {} } }`)), - expectedStatus: http.StatusBadRequest, + expectedStatus: http.StatusUnprocessableEntity, expectedBody: false, }, { @@ -663,11 +707,8 @@ func TestPostureCheckUpdate(t *testing.T) { } } }`)), - expectedStatus: http.StatusBadRequest, + expectedStatus: http.StatusUnprocessableEntity, expectedBody: false, - setupHandlerFunc: func(handler *PostureChecksHandler) { - handler.geolocationManager = nil - }, }, { name: "Update Posture Checks Invalid Check", @@ -682,7 +723,7 @@ func TestPostureCheckUpdate(t *testing.T) { } } }`)), - expectedStatus: http.StatusBadRequest, + expectedStatus: http.StatusUnprocessableEntity, expectedBody: false, }, { @@ -697,7 +738,7 @@ func TestPostureCheckUpdate(t *testing.T) { } } }`)), - expectedStatus: http.StatusBadRequest, + expectedStatus: http.StatusUnprocessableEntity, expectedBody: false, }, { @@ -711,7 +752,7 @@ func TestPostureCheckUpdate(t *testing.T) { "nb_version_check": {} } }`)), - expectedStatus: http.StatusBadRequest, + expectedStatus: http.StatusUnprocessableEntity, expectedBody: false, }, { @@ -841,100 +882,3 @@ func TestPostureCheckUpdate(t *testing.T) { }) } } - -func TestPostureCheck_validatePostureChecksUpdate(t *testing.T) { - // empty name - err := validatePostureChecksUpdate(api.PostureCheckUpdate{}) - assert.Error(t, err) - - // empty checks - err = validatePostureChecksUpdate(api.PostureCheckUpdate{Name: "Default"}) - assert.Error(t, err) - err = validatePostureChecksUpdate(api.PostureCheckUpdate{Name: "Default", Checks: &api.Checks{}}) - assert.Error(t, err) - - // not valid NbVersionCheck - nbVersionCheck := api.NBVersionCheck{} - err = validatePostureChecksUpdate(api.PostureCheckUpdate{Name: "Default", Checks: &api.Checks{NbVersionCheck: &nbVersionCheck}}) - assert.Error(t, err) - - // valid NbVersionCheck - nbVersionCheck = api.NBVersionCheck{MinVersion: "1.0"} - err = validatePostureChecksUpdate(api.PostureCheckUpdate{Name: "Default", Checks: &api.Checks{NbVersionCheck: &nbVersionCheck}}) - assert.NoError(t, err) - - // not valid OsVersionCheck - osVersionCheck := api.OSVersionCheck{} - err = validatePostureChecksUpdate(api.PostureCheckUpdate{Name: "Default", Checks: &api.Checks{OsVersionCheck: &osVersionCheck}}) - assert.Error(t, err) - - // not valid OsVersionCheck - osVersionCheck = api.OSVersionCheck{Linux: &api.MinKernelVersionCheck{}} - err = validatePostureChecksUpdate(api.PostureCheckUpdate{Name: "Default", Checks: &api.Checks{OsVersionCheck: &osVersionCheck}}) - assert.Error(t, err) - - // not valid OsVersionCheck - osVersionCheck = api.OSVersionCheck{Linux: &api.MinKernelVersionCheck{}, Darwin: &api.MinVersionCheck{MinVersion: "14.2"}} - err = validatePostureChecksUpdate(api.PostureCheckUpdate{Name: "Default", Checks: &api.Checks{OsVersionCheck: &osVersionCheck}}) - assert.Error(t, err) - - // valid OsVersionCheck - osVersionCheck = api.OSVersionCheck{Linux: &api.MinKernelVersionCheck{MinKernelVersion: "6.0"}} - err = validatePostureChecksUpdate(api.PostureCheckUpdate{Name: "Default", Checks: &api.Checks{OsVersionCheck: &osVersionCheck}}) - assert.NoError(t, err) - - // valid OsVersionCheck - osVersionCheck = api.OSVersionCheck{ - Linux: &api.MinKernelVersionCheck{MinKernelVersion: "6.0"}, - Darwin: &api.MinVersionCheck{MinVersion: "14.2"}, - } - err = validatePostureChecksUpdate(api.PostureCheckUpdate{Name: "Default", Checks: &api.Checks{OsVersionCheck: &osVersionCheck}}) - assert.NoError(t, err) - - // valid peer network range check - peerNetworkRangeCheck := api.PeerNetworkRangeCheck{ - Action: api.PeerNetworkRangeCheckActionAllow, - Ranges: []string{ - "192.168.1.0/24", "10.0.0.0/8", - }, - } - err = validatePostureChecksUpdate( - api.PostureCheckUpdate{ - Name: "Default", - Checks: &api.Checks{ - PeerNetworkRangeCheck: &peerNetworkRangeCheck, - }, - }, - ) - assert.NoError(t, err) - - // invalid peer network range check - peerNetworkRangeCheck = api.PeerNetworkRangeCheck{ - Action: api.PeerNetworkRangeCheckActionDeny, - Ranges: []string{}, - } - err = validatePostureChecksUpdate( - api.PostureCheckUpdate{ - Name: "Default", - Checks: &api.Checks{ - PeerNetworkRangeCheck: &peerNetworkRangeCheck, - }, - }, - ) - assert.Error(t, err) - - // invalid peer network range check - peerNetworkRangeCheck = api.PeerNetworkRangeCheck{ - Action: "unknownAction", - Ranges: []string{}, - } - err = validatePostureChecksUpdate( - api.PostureCheckUpdate{ - Name: "Default", - Checks: &api.Checks{ - PeerNetworkRangeCheck: &peerNetworkRangeCheck, - }, - }, - ) - assert.Error(t, err) -} diff --git a/management/server/http/routes_handler.go b/management/server/http/routes_handler.go index f755e7a16..a48c6d61d 100644 --- a/management/server/http/routes_handler.go +++ b/management/server/http/routes_handler.go @@ -2,11 +2,16 @@ package http import ( "encoding/json" + "fmt" "net/http" + "net/netip" + "regexp" + "strings" "unicode/utf8" "github.com/gorilla/mux" + "github.com/netbirdio/netbird/management/domain" "github.com/netbirdio/netbird/management/server" "github.com/netbirdio/netbird/management/server/http/api" "github.com/netbirdio/netbird/management/server/http/util" @@ -15,6 +20,9 @@ import ( "github.com/netbirdio/netbird/route" ) +const maxDomains = 32 +const failedToConvertRoute = "failed to convert route to response: %v" + // RoutesHandler is the routes handler of the account type RoutesHandler struct { accountManager server.AccountManager @@ -48,7 +56,12 @@ func (h *RoutesHandler) GetAllRoutes(w http.ResponseWriter, r *http.Request) { } apiRoutes := make([]*api.Route, 0) for _, r := range routes { - apiRoutes = append(apiRoutes, toRouteResponse(r)) + route, err := toRouteResponse(r) + if err != nil { + util.WriteError(status.Errorf(status.Internal, failedToConvertRoute, err), w) + return + } + apiRoutes = append(apiRoutes, route) } util.WriteJSONObject(w, apiRoutes) @@ -70,16 +83,28 @@ func (h *RoutesHandler) CreateRoute(w http.ResponseWriter, r *http.Request) { return } - _, newPrefix, err := route.ParseNetwork(req.Network) - if err != nil { + if err := h.validateRoute(req); err != nil { util.WriteError(err, w) return } - if utf8.RuneCountInString(req.NetworkId) > route.MaxNetIDChar || req.NetworkId == "" { - util.WriteError(status.Errorf(status.InvalidArgument, "identifier should be between 1 and %d", - route.MaxNetIDChar), w) - return + var domains domain.List + var networkType route.NetworkType + var newPrefix netip.Prefix + if req.Domains != nil { + d, err := validateDomains(*req.Domains) + if err != nil { + util.WriteError(status.Errorf(status.InvalidArgument, "invalid domains: %v", err), w) + return + } + domains = d + networkType = route.DomainNetwork + } else if req.Network != nil { + networkType, newPrefix, err = route.ParseNetwork(*req.Network) + if err != nil { + util.WriteError(err, w) + return + } } peerId := "" @@ -87,36 +112,57 @@ func (h *RoutesHandler) CreateRoute(w http.ResponseWriter, r *http.Request) { peerId = *req.Peer } - peerGroupIds := []string{} + var peerGroupIds []string if req.PeerGroups != nil { peerGroupIds = *req.PeerGroups } - if (peerId != "" && len(peerGroupIds) > 0) || (peerId == "" && len(peerGroupIds) == 0) { - util.WriteError(status.Errorf(status.InvalidArgument, "only one peer or peer_groups should be provided"), w) - return - } - - // do not allow non Linux peers + // Do not allow non-Linux peers if peer := account.GetPeer(peerId); peer != nil { if peer.Meta.GoOS != "linux" { - util.WriteError(status.Errorf(status.InvalidArgument, "non-linux peers are non supported as network routes"), w) + util.WriteError(status.Errorf(status.InvalidArgument, "non-linux peers are not supported as network routes"), w) return } } - newRoute, err := h.accountManager.CreateRoute( - account.Id, newPrefix.String(), peerId, peerGroupIds, - req.Description, route.NetID(req.NetworkId), req.Masquerade, req.Metric, req.Groups, req.Enabled, user.Id, - ) + newRoute, err := h.accountManager.CreateRoute(account.Id, newPrefix, networkType, domains, peerId, peerGroupIds, req.Description, route.NetID(req.NetworkId), req.Masquerade, req.Metric, req.Groups, req.Enabled, user.Id, req.KeepRoute) if err != nil { util.WriteError(err, w) return } - resp := toRouteResponse(newRoute) + routes, err := toRouteResponse(newRoute) + if err != nil { + util.WriteError(status.Errorf(status.Internal, failedToConvertRoute, err), w) + return + } - util.WriteJSONObject(w, &resp) + util.WriteJSONObject(w, routes) +} + +func (h *RoutesHandler) validateRoute(req api.PostApiRoutesJSONRequestBody) error { + if req.Network != nil && req.Domains != nil { + return status.Errorf(status.InvalidArgument, "only one of 'network' or 'domains' should be provided") + } + + if req.Network == nil && req.Domains == nil { + return status.Errorf(status.InvalidArgument, "either 'network' or 'domains' should be provided") + } + + if req.Peer == nil && req.PeerGroups == nil { + return status.Errorf(status.InvalidArgument, "either 'peer' or 'peers_group' should be provided") + } + + if req.Peer != nil && req.PeerGroups != nil { + return status.Errorf(status.InvalidArgument, "only one of 'peer' or 'peer_groups' should be provided") + } + + if utf8.RuneCountInString(req.NetworkId) > route.MaxNetIDChar || req.NetworkId == "" { + return status.Errorf(status.InvalidArgument, "identifier should be between 1 and %d characters", + route.MaxNetIDChar) + } + + return nil } // UpdateRoute handles update to a route identified by a given ID @@ -148,26 +194,8 @@ func (h *RoutesHandler) UpdateRoute(w http.ResponseWriter, r *http.Request) { return } - prefixType, newPrefix, err := route.ParseNetwork(req.Network) - if err != nil { - util.WriteError(status.Errorf(status.InvalidArgument, "couldn't parse update prefix %s for route ID %s", - req.Network, routeID), w) - return - } - - if utf8.RuneCountInString(req.NetworkId) > route.MaxNetIDChar || req.NetworkId == "" { - util.WriteError(status.Errorf(status.InvalidArgument, - "identifier should be between 1 and %d", route.MaxNetIDChar), w) - return - } - - if req.Peer != nil && req.PeerGroups != nil { - util.WriteError(status.Errorf(status.InvalidArgument, "only peer or peers_group should be provided"), w) - return - } - - if req.Peer == nil && req.PeerGroups == nil { - util.WriteError(status.Errorf(status.InvalidArgument, "either peer or peers_group should be provided"), w) + if err := h.validateRoute(req); err != nil { + util.WriteError(err, w) return } @@ -186,14 +214,29 @@ func (h *RoutesHandler) UpdateRoute(w http.ResponseWriter, r *http.Request) { newRoute := &route.Route{ ID: route.ID(routeID), - Network: newPrefix, NetID: route.NetID(req.NetworkId), - NetworkType: prefixType, Masquerade: req.Masquerade, Metric: req.Metric, Description: req.Description, Enabled: req.Enabled, Groups: req.Groups, + KeepRoute: req.KeepRoute, + } + + if req.Domains != nil { + d, err := validateDomains(*req.Domains) + if err != nil { + util.WriteError(status.Errorf(status.InvalidArgument, "invalid domains: %v", err), w) + return + } + newRoute.Domains = d + newRoute.NetworkType = route.DomainNetwork + } else if req.Network != nil { + newRoute.NetworkType, newRoute.Network, err = route.ParseNetwork(*req.Network) + if err != nil { + util.WriteError(err, w) + return + } } if req.Peer != nil { @@ -210,9 +253,13 @@ func (h *RoutesHandler) UpdateRoute(w http.ResponseWriter, r *http.Request) { return } - resp := toRouteResponse(newRoute) + routes, err := toRouteResponse(newRoute) + if err != nil { + util.WriteError(status.Errorf(status.Internal, failedToConvertRoute, err), w) + return + } - util.WriteJSONObject(w, &resp) + util.WriteJSONObject(w, routes) } // DeleteRoute handles route deletion request @@ -260,25 +307,69 @@ func (h *RoutesHandler) GetRoute(w http.ResponseWriter, r *http.Request) { return } - util.WriteJSONObject(w, toRouteResponse(foundRoute)) + routes, err := toRouteResponse(foundRoute) + if err != nil { + util.WriteError(status.Errorf(status.Internal, failedToConvertRoute, err), w) + return + } + + util.WriteJSONObject(w, routes) } -func toRouteResponse(serverRoute *route.Route) *api.Route { +func toRouteResponse(serverRoute *route.Route) (*api.Route, error) { + domains, err := serverRoute.Domains.ToStringList() + if err != nil { + return nil, err + } + network := serverRoute.Network.String() route := &api.Route{ Id: string(serverRoute.ID), Description: serverRoute.Description, NetworkId: string(serverRoute.NetID), Enabled: serverRoute.Enabled, Peer: &serverRoute.Peer, - Network: serverRoute.Network.String(), + Network: &network, + Domains: &domains, NetworkType: serverRoute.NetworkType.String(), Masquerade: serverRoute.Masquerade, Metric: serverRoute.Metric, Groups: serverRoute.Groups, + KeepRoute: serverRoute.KeepRoute, } if len(serverRoute.PeerGroups) > 0 { route.PeerGroups = &serverRoute.PeerGroups } - return route + 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/routes_handler_test.go b/management/server/http/routes_handler_test.go index 1c8288d5f..261d0c231 100644 --- a/management/server/http/routes_handler_test.go +++ b/management/server/http/routes_handler_test.go @@ -10,6 +10,8 @@ import ( "net/netip" "testing" + "github.com/stretchr/testify/require" + "github.com/netbirdio/netbird/management/server/http/api" nbpeer "github.com/netbirdio/netbird/management/server/peer" "github.com/netbirdio/netbird/management/server/status" @@ -18,6 +20,7 @@ import ( "github.com/gorilla/mux" "github.com/magiconair/properties/assert" + "github.com/netbirdio/netbird/management/domain" "github.com/netbirdio/netbird/management/server" "github.com/netbirdio/netbird/management/server/jwtclaims" "github.com/netbirdio/netbird/management/server/mock_server" @@ -26,6 +29,7 @@ import ( const ( existingRouteID = "existingRouteID" existingRouteID2 = "existingRouteID2" // for peer_groups test + existingRouteID3 = "existingRouteID3" // for domains test notFoundRouteID = "notFoundRouteID" existingPeerIP1 = "100.64.0.100" existingPeerIP2 = "100.64.0.101" @@ -35,6 +39,7 @@ const ( testAccountID = "test_id" existingGroupID = "testGroup" notFoundGroupID = "nonExistingGroup" + existingDomain = "example.com" ) var emptyString = "" @@ -46,6 +51,8 @@ var baseExistingRoute = &route.Route{ Description: "base route", NetID: "awesomeNet", Network: netip.MustParsePrefix("192.168.0.0/24"), + Domains: domain.List{}, + KeepRoute: false, NetworkType: route.IPv4Network, Metric: 9999, Masquerade: false, @@ -90,28 +97,33 @@ func initRoutesTestData() *RoutesHandler { route := baseExistingRoute.Copy() route.PeerGroups = []string{existingGroupID} return route, nil + } else if routeID == existingRouteID3 { + route := baseExistingRoute.Copy() + route.Domains = domain.List{existingDomain} + return route, nil } return nil, status.Errorf(status.NotFound, "route with ID %s not found", routeID) }, - CreateRouteFunc: func(accountID, network, peerID string, peerGroups []string, description string, netID route.NetID, masquerade bool, metric int, groups []string, enabled bool, _ string) (*route.Route, error) { + CreateRouteFunc: func(accountID string, prefix netip.Prefix, networkType route.NetworkType, domains domain.List, peerID string, peerGroups []string, description string, netID route.NetID, masquerade bool, metric int, groups []string, enabled bool, _ string, keepRoute bool) (*route.Route, error) { if peerID == notFoundPeerID { return nil, status.Errorf(status.InvalidArgument, "peer with ID %s not found", peerID) } if len(peerGroups) > 0 && peerGroups[0] == notFoundGroupID { return nil, status.Errorf(status.InvalidArgument, "peer groups with ID %s not found", peerGroups[0]) } - networkType, p, _ := route.ParseNetwork(network) return &route.Route{ ID: existingRouteID, NetID: netID, Peer: peerID, PeerGroups: peerGroups, - Network: p, + Network: prefix, + Domains: domains, NetworkType: networkType, Description: description, Masquerade: masquerade, Enabled: enabled, Groups: groups, + KeepRoute: keepRoute, }, nil }, SaveRouteFunc: func(_, _ string, r *route.Route) error { @@ -146,6 +158,9 @@ func TestRoutesHandlers(t *testing.T) { baseExistingRouteWithPeerGroups := baseExistingRoute.Copy() baseExistingRouteWithPeerGroups.PeerGroups = []string{existingGroupID} + baseExistingRouteWithDomains := baseExistingRoute.Copy() + baseExistingRouteWithDomains.Domains = domain.List{existingDomain} + tt := []struct { name string expectedStatus int @@ -161,7 +176,7 @@ func TestRoutesHandlers(t *testing.T) { requestPath: "/api/routes/" + existingRouteID, expectedStatus: http.StatusOK, expectedBody: true, - expectedRoute: toRouteResponse(baseExistingRoute), + expectedRoute: toApiRoute(t, baseExistingRoute), }, { name: "Get Not Existing Route", @@ -175,7 +190,15 @@ func TestRoutesHandlers(t *testing.T) { requestPath: "/api/routes/" + existingRouteID2, expectedStatus: http.StatusOK, expectedBody: true, - expectedRoute: toRouteResponse(baseExistingRouteWithPeerGroups), + expectedRoute: toApiRoute(t, baseExistingRouteWithPeerGroups), + }, + { + name: "Get Existing Route with Domains", + requestType: http.MethodGet, + requestPath: "/api/routes/" + existingRouteID3, + expectedStatus: http.StatusOK, + expectedBody: true, + expectedRoute: toApiRoute(t, baseExistingRouteWithDomains), }, { name: "Delete Existing Route", @@ -191,18 +214,18 @@ func TestRoutesHandlers(t *testing.T) { expectedStatus: http.StatusNotFound, }, { - name: "POST OK", + name: "Network POST OK", requestType: http.MethodPost, requestPath: "/api/routes", requestBody: bytes.NewBuffer( - []byte(fmt.Sprintf("{\"Description\":\"Post\",\"Network\":\"192.168.0.0/16\",\"network_id\":\"awesomeNet\",\"Peer\":\"%s\",\"groups\":[\"%s\"]}", existingPeerID, existingGroupID))), + []byte(fmt.Sprintf(`{"Description":"Post","Network":"192.168.0.0/16","network_id":"awesomeNet","Peer":"%s","groups":["%s"]}`, existingPeerID, existingGroupID))), expectedStatus: http.StatusOK, expectedBody: true, expectedRoute: &api.Route{ Id: existingRouteID, Description: "Post", NetworkId: "awesomeNet", - Network: "192.168.0.0/16", + Network: toPtr("192.168.0.0/16"), Peer: &existingPeerID, NetworkType: route.IPv4NetworkString, Masquerade: false, @@ -210,6 +233,28 @@ func TestRoutesHandlers(t *testing.T) { Groups: []string{existingGroupID}, }, }, + { + name: "Domains POST OK", + requestType: http.MethodPost, + requestPath: "/api/routes", + requestBody: bytes.NewBuffer( + []byte(fmt.Sprintf(`{"description":"Post","domains":["example.com"],"network_id":"domainNet","peer":"%s","groups":["%s"],"keep_route":true}`, existingPeerID, existingGroupID))), + expectedStatus: http.StatusOK, + expectedBody: true, + expectedRoute: &api.Route{ + Id: existingRouteID, + Description: "Post", + NetworkId: "domainNet", + Network: toPtr("invalid Prefix"), + KeepRoute: true, + Domains: &[]string{existingDomain}, + Peer: &existingPeerID, + NetworkType: route.DomainNetworkString, + Masquerade: false, + Enabled: false, + Groups: []string{existingGroupID}, + }, + }, { name: "POST Non Linux Peer", requestType: http.MethodPost, @@ -242,6 +287,32 @@ func TestRoutesHandlers(t *testing.T) { expectedStatus: http.StatusUnprocessableEntity, expectedBody: false, }, + { + name: "POST Invalid Domains", + requestType: http.MethodPost, + requestPath: "/api/routes", + requestBody: bytes.NewBufferString(fmt.Sprintf(`{"Description":"Post","domains":["-example.com"],"network_id":"awesomeNet","Peer":"%s","groups":["%s"]}`, existingPeerID, existingGroupID)), + expectedStatus: http.StatusUnprocessableEntity, + expectedBody: false, + }, + { + name: "POST UnprocessableEntity when both network and domains are provided", + requestType: http.MethodPost, + requestPath: "/api/routes", + requestBody: bytes.NewBuffer( + []byte(fmt.Sprintf(`{"Description":"Post","Network":"192.168.0.0/16","domains":["example.com"],"network_id":"awesomeNet","peer":"%s","peer_groups":["%s"],"groups":["%s"]}`, existingPeerID, existingGroupID, existingGroupID))), + expectedStatus: http.StatusUnprocessableEntity, + expectedBody: false, + }, + { + name: "POST UnprocessableEntity when no network and domains are provided", + requestType: http.MethodPost, + requestPath: "/api/routes", + requestBody: bytes.NewBuffer( + []byte(fmt.Sprintf(`{"Description":"Post","network_id":"awesomeNet","groups":["%s"]}`, existingPeerID))), + expectedStatus: http.StatusUnprocessableEntity, + expectedBody: false, + }, { name: "POST UnprocessableEntity when both peer and peer_groups are provided", requestType: http.MethodPost, @@ -261,7 +332,7 @@ func TestRoutesHandlers(t *testing.T) { expectedBody: false, }, { - name: "PUT OK", + name: "Network PUT OK", requestType: http.MethodPut, requestPath: "/api/routes/" + existingRouteID, requestBody: bytes.NewBufferString(fmt.Sprintf("{\"Description\":\"Post\",\"Network\":\"192.168.0.0/16\",\"network_id\":\"awesomeNet\",\"Peer\":\"%s\",\"groups\":[\"%s\"]}", existingPeerID, existingGroupID)), @@ -271,7 +342,7 @@ func TestRoutesHandlers(t *testing.T) { Id: existingRouteID, Description: "Post", NetworkId: "awesomeNet", - Network: "192.168.0.0/16", + Network: toPtr("192.168.0.0/16"), Peer: &existingPeerID, NetworkType: route.IPv4NetworkString, Masquerade: false, @@ -279,6 +350,27 @@ func TestRoutesHandlers(t *testing.T) { Groups: []string{existingGroupID}, }, }, + { + name: "Domains PUT OK", + requestType: http.MethodPut, + requestPath: "/api/routes/" + existingRouteID, + requestBody: bytes.NewBufferString(fmt.Sprintf(`{"Description":"Post","domains":["example.com"],"network_id":"awesomeNet","Peer":"%s","groups":["%s"],"keep_route":true}`, existingPeerID, existingGroupID)), + expectedStatus: http.StatusOK, + expectedBody: true, + expectedRoute: &api.Route{ + Id: existingRouteID, + Description: "Post", + NetworkId: "awesomeNet", + Network: toPtr("invalid Prefix"), + Domains: &[]string{existingDomain}, + Peer: &existingPeerID, + NetworkType: route.DomainNetworkString, + Masquerade: false, + Enabled: false, + Groups: []string{existingGroupID}, + KeepRoute: true, + }, + }, { name: "PUT OK when peer_groups provided", requestType: http.MethodPut, @@ -290,7 +382,7 @@ func TestRoutesHandlers(t *testing.T) { Id: existingRouteID, Description: "Post", NetworkId: "awesomeNet", - Network: "192.168.0.0/16", + Network: toPtr("192.168.0.0/16"), Peer: &emptyString, PeerGroups: &[]string{existingGroupID}, NetworkType: route.IPv4NetworkString, @@ -339,6 +431,33 @@ func TestRoutesHandlers(t *testing.T) { expectedStatus: http.StatusUnprocessableEntity, expectedBody: false, }, + { + name: "PUT Invalid Domains", + requestType: http.MethodPut, + requestPath: "/api/routes/" + existingRouteID, + requestBody: bytes.NewBuffer( + []byte(fmt.Sprintf(`{"Description":"Post","domains":["-example.com"],"network_id":"awesomeNet","peer":"%s","peer_groups":["%s"],"groups":["%s"]}`, existingPeerID, existingGroupID, existingGroupID))), + expectedStatus: http.StatusUnprocessableEntity, + expectedBody: false, + }, + { + name: "PUT UnprocessableEntity when both network and domains are provided", + requestType: http.MethodPut, + requestPath: "/api/routes/" + existingRouteID, + requestBody: bytes.NewBuffer( + []byte(fmt.Sprintf(`{"Description":"Post","Network":"192.168.0.0/16","domains":["example.com"],"network_id":"awesomeNet","peer":"%s","peer_groups":["%s"],"groups":["%s"]}`, existingPeerID, existingGroupID, existingGroupID))), + expectedStatus: http.StatusUnprocessableEntity, + expectedBody: false, + }, + { + name: "PUT UnprocessableEntity when no network and domains are provided", + requestType: http.MethodPut, + requestPath: "/api/routes/" + existingRouteID, + requestBody: bytes.NewBuffer( + []byte(fmt.Sprintf(`{"Description":"Post","network_id":"awesomeNet","peer":"%s","peer_groups":["%s"],"groups":["%s"]}`, existingPeerID, existingGroupID, existingGroupID))), + expectedStatus: http.StatusUnprocessableEntity, + expectedBody: false, + }, { name: "PUT UnprocessableEntity when both peer and peer_groups are provided", requestType: http.MethodPut, @@ -399,3 +518,85 @@ 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, + }, + } + + 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() + + apiRoute, err := toRouteResponse(r) + // json flattens pointer to nil slices to null + if apiRoute.Domains != nil && *apiRoute.Domains == nil { + apiRoute.Domains = nil + } + require.NoError(t, err, "Failed to convert route") + return apiRoute +} + +func toPtr[T any](v T) *T { + return &v +} diff --git a/management/server/http/users_handler_test.go b/management/server/http/users_handler_test.go index 91f19d8d8..8a78188be 100644 --- a/management/server/http/users_handler_test.go +++ b/management/server/http/users_handler_test.go @@ -27,7 +27,7 @@ const ( var usersTestAccount = &server.Account{ Id: existingAccountID, - Domain: domain, + Domain: testDomain, Users: map[string]*server.User{ existingUserID: { Id: existingUserID, @@ -127,7 +127,7 @@ func initUsersTestData() *UsersHandler { jwtclaims.WithFromRequestContext(func(r *http.Request) jwtclaims.AuthorizationClaims { return jwtclaims.AuthorizationClaims{ UserId: existingUserID, - Domain: domain, + Domain: testDomain, AccountId: existingAccountID, } }), diff --git a/management/server/management_proto_test.go b/management/server/management_proto_test.go index c2672b1e9..e49ea5338 100644 --- a/management/server/management_proto_test.go +++ b/management/server/management_proto_test.go @@ -134,7 +134,8 @@ func Test_SyncProtocol(t *testing.T) { // take the first registered peer as a base for the test. Total four. key := *peers[0] - message, err := encryption.EncryptMessage(*serverKey, key, &mgmtProto.SyncRequest{}) + syncReq := &mgmtProto.SyncRequest{Meta: &mgmtProto.PeerSystemMeta{}} + message, err := encryption.EncryptMessage(*serverKey, key, syncReq) if err != nil { t.Fatal(err) return diff --git a/management/server/management_test.go b/management/server/management_test.go index 564afaf55..4e997d4d9 100644 --- a/management/server/management_test.go +++ b/management/server/management_test.go @@ -93,7 +93,8 @@ var _ = Describe("Management service", func() { key, _ := wgtypes.GenerateKey() loginPeerWithValidSetupKey(serverPubKey, key, client) - encryptedBytes, err := encryption.EncryptMessage(serverPubKey, key, &mgmtProto.SyncRequest{}) + 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{ @@ -143,7 +144,7 @@ var _ = Describe("Management service", func() { loginPeerWithValidSetupKey(serverPubKey, key1, client) loginPeerWithValidSetupKey(serverPubKey, key2, client) - messageBytes, err := pb.Marshal(&mgmtProto.SyncRequest{}) + messageBytes, err := pb.Marshal(&mgmtProto.SyncRequest{Meta: &mgmtProto.PeerSystemMeta{}}) Expect(err).NotTo(HaveOccurred()) encryptedBytes, err := encryption.Encrypt(messageBytes, serverPubKey, key) Expect(err).NotTo(HaveOccurred()) @@ -176,7 +177,7 @@ var _ = Describe("Management service", func() { key, _ := wgtypes.GenerateKey() loginPeerWithValidSetupKey(serverPubKey, key, client) - messageBytes, err := pb.Marshal(&mgmtProto.SyncRequest{}) + messageBytes, err := pb.Marshal(&mgmtProto.SyncRequest{Meta: &mgmtProto.PeerSystemMeta{}}) Expect(err).NotTo(HaveOccurred()) encryptedBytes, err := encryption.Encrypt(messageBytes, serverPubKey, key) Expect(err).NotTo(HaveOccurred()) @@ -329,7 +330,7 @@ var _ = Describe("Management service", func() { var clients []mgmtProto.ManagementService_SyncClient for _, peer := range peers { - messageBytes, err := pb.Marshal(&mgmtProto.SyncRequest{}) + messageBytes, err := pb.Marshal(&mgmtProto.SyncRequest{Meta: &mgmtProto.PeerSystemMeta{}}) Expect(err).NotTo(HaveOccurred()) encryptedBytes, err := encryption.Encrypt(messageBytes, serverPubKey, peer) Expect(err).NotTo(HaveOccurred()) @@ -394,7 +395,8 @@ var _ = Describe("Management service", func() { defer GinkgoRecover() key, _ := wgtypes.GenerateKey() loginPeerWithValidSetupKey(serverPubKey, key, client) - encryptedBytes, err := encryption.EncryptMessage(serverPubKey, key, &mgmtProto.SyncRequest{}) + syncReq := &mgmtProto.SyncRequest{Meta: &mgmtProto.PeerSystemMeta{}} + encryptedBytes, err := encryption.EncryptMessage(serverPubKey, key, syncReq) Expect(err).NotTo(HaveOccurred()) // open stream diff --git a/management/server/mock_server/account_mock.go b/management/server/mock_server/account_mock.go index 765cd8483..a915dca64 100644 --- a/management/server/mock_server/account_mock.go +++ b/management/server/mock_server/account_mock.go @@ -2,12 +2,14 @@ package mock_server import ( "net" + "net/netip" "time" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" nbdns "github.com/netbirdio/netbird/dns" + "github.com/netbirdio/netbird/management/domain" "github.com/netbirdio/netbird/management/server" "github.com/netbirdio/netbird/management/server/activity" "github.com/netbirdio/netbird/management/server/group" @@ -28,7 +30,7 @@ type MockAccountManager struct { ListUsersFunc func(accountID string) ([]*server.User, error) GetPeersFunc func(accountID, userID string) ([]*nbpeer.Peer, error) MarkPeerConnectedFunc func(peerKey string, connected bool, realIP net.IP) error - SyncAndMarkPeerFunc func(peerPubKey string, realIP net.IP) (*nbpeer.Peer, *server.NetworkMap, error) + SyncAndMarkPeerFunc func(peerPubKey string, meta nbpeer.PeerSystemMeta, realIP net.IP) (*nbpeer.Peer, *server.NetworkMap, error) DeletePeerFunc func(accountID, peerKey, userID string) error GetNetworkMapFunc func(peerKey string) (*server.NetworkMap, error) GetPeerNetworkFunc func(peerKey string) (*server.Network, error) @@ -52,7 +54,7 @@ type MockAccountManager struct { UpdatePeerMetaFunc func(peerID string, meta nbpeer.PeerSystemMeta) error UpdatePeerSSHKeyFunc func(peerID string, sshKey string) error UpdatePeerFunc func(accountID, userID string, peer *nbpeer.Peer) (*nbpeer.Peer, error) - CreateRouteFunc func(accountID, prefix, peer string, peerGroups []string, description string, netID route.NetID, masquerade bool, metric int, groups []string, enabled bool, userID string) (*route.Route, error) + CreateRouteFunc func(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 []string, enabled bool, userID string, keepRoute bool) (*route.Route, error) GetRouteFunc func(accountID string, routeID route.ID, userID string) (*route.Route, error) SaveRouteFunc func(accountID string, userID string, route *route.Route) error DeleteRouteFunc func(accountID string, routeID route.ID, userID string) error @@ -81,6 +83,7 @@ type MockAccountManager struct { GetDNSSettingsFunc func(accountID, userID string) (*server.DNSSettings, error) SaveDNSSettingsFunc func(accountID, userID string, dnsSettingsToSave *server.DNSSettings) error GetPeerFunc func(accountID, peerID, userID string) (*nbpeer.Peer, error) + GetPeerAppliedPostureChecksFunc func(peerKey string) ([]posture.Checks, error) UpdateAccountSettingsFunc func(accountID, userID string, newSettings *server.Settings) (*server.Account, error) LoginPeerFunc func(login server.PeerLogin) (*nbpeer.Peer, *server.NetworkMap, error) SyncPeerFunc func(sync server.PeerSync, account *server.Account) (*nbpeer.Peer, *server.NetworkMap, error) @@ -95,12 +98,13 @@ type MockAccountManager struct { GetIdpManagerFunc func() idp.Manager UpdateIntegratedValidatorGroupsFunc func(accountID string, userID string, groups []string) error GroupValidationFunc func(accountId string, groups []string) (bool, error) + SyncPeerMetaFunc func(peerPubKey string, meta nbpeer.PeerSystemMeta) error FindExistingPostureCheckFunc func(accountID string, checks *posture.ChecksDefinition) (*posture.Checks, error) } -func (am *MockAccountManager) SyncAndMarkPeer(peerPubKey string, realIP net.IP) (*nbpeer.Peer, *server.NetworkMap, error) { +func (am *MockAccountManager) SyncAndMarkPeer(peerPubKey string, meta nbpeer.PeerSystemMeta, realIP net.IP) (*nbpeer.Peer, *server.NetworkMap, error) { if am.SyncAndMarkPeerFunc != nil { - return am.SyncAndMarkPeerFunc(peerPubKey, realIP) + return am.SyncAndMarkPeerFunc(peerPubKey, meta, realIP) } return nil, nil, status.Errorf(codes.Unimplemented, "method MarkPeerConnected is not implemented") } @@ -413,9 +417,9 @@ func (am *MockAccountManager) UpdatePeer(accountID, userID string, peer *nbpeer. } // CreateRoute mock implementation of CreateRoute from server.AccountManager interface -func (am *MockAccountManager) CreateRoute(accountID, prefix, peerID string, peerGroupIDs []string, description string, netID route.NetID, masquerade bool, metric int, groups []string, enabled bool, userID string) (*route.Route, error) { +func (am *MockAccountManager) CreateRoute(accountID string, prefix netip.Prefix, networkType route.NetworkType, domains domain.List, peerID string, peerGroupIDs []string, description string, netID route.NetID, masquerade bool, metric int, groups []string, enabled bool, userID string, keepRoute bool) (*route.Route, error) { if am.CreateRouteFunc != nil { - return am.CreateRouteFunc(accountID, prefix, peerID, peerGroupIDs, description, netID, masquerade, metric, groups, enabled, userID) + return am.CreateRouteFunc(accountID, prefix, networkType, domains, peerID, peerGroupIDs, description, netID, masquerade, metric, groups, enabled, userID, keepRoute) } return nil, status.Errorf(codes.Unimplemented, "method CreateRoute is not implemented") } @@ -623,6 +627,14 @@ func (am *MockAccountManager) GetPeer(accountID, peerID, userID string) (*nbpeer return nil, status.Errorf(codes.Unimplemented, "method GetPeer is not implemented") } +// GetPeerAppliedPostureChecks mocks GetPeerAppliedPostureChecks of the AccountManager interface +func (am *MockAccountManager) GetPeerAppliedPostureChecks(peerKey string) ([]posture.Checks, error) { + if am.GetPeerAppliedPostureChecksFunc != nil { + return am.GetPeerAppliedPostureChecksFunc(peerKey) + } + return nil, status.Errorf(codes.Unimplemented, "method GetPeerAppliedPostureChecks is not implemented") +} + // UpdateAccountSettings mocks UpdateAccountSettings of the AccountManager interface func (am *MockAccountManager) UpdateAccountSettings(accountID, userID string, newSettings *server.Settings) (*server.Account, error) { if am.UpdateAccountSettingsFunc != nil { @@ -736,6 +748,14 @@ func (am *MockAccountManager) GroupValidation(accountId string, groups []string) return false, status.Errorf(codes.Unimplemented, "method GroupValidation is not implemented") } +// SyncPeerMeta mocks SyncPeerMeta of the AccountManager interface +func (am *MockAccountManager) SyncPeerMeta(peerPubKey string, meta nbpeer.PeerSystemMeta) error { + if am.SyncPeerMetaFunc != nil { + return am.SyncPeerMetaFunc(peerPubKey, meta) + } + return status.Errorf(codes.Unimplemented, "method SyncPeerMeta is not implemented") +} + // FindExistingPostureCheck mocks FindExistingPostureCheck of the AccountManager interface func (am *MockAccountManager) FindExistingPostureCheck(accountID string, checks *posture.ChecksDefinition) (*posture.Checks, error) { if am.FindExistingPostureCheckFunc != nil { diff --git a/management/server/mock_server/management_server_mock.go b/management/server/mock_server/management_server_mock.go index 29544b53f..d79fbd4e9 100644 --- a/management/server/mock_server/management_server_mock.go +++ b/management/server/mock_server/management_server_mock.go @@ -3,9 +3,10 @@ package mock_server import ( "context" - "github.com/netbirdio/netbird/management/proto" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" + + "github.com/netbirdio/netbird/management/proto" ) type ManagementServiceServerMock struct { @@ -17,6 +18,7 @@ type ManagementServiceServerMock struct { IsHealthyFunc func(context.Context, *proto.Empty) (*proto.Empty, error) GetDeviceAuthorizationFlowFunc func(ctx context.Context, req *proto.EncryptedMessage) (*proto.EncryptedMessage, error) GetPKCEAuthorizationFlowFunc func(ctx context.Context, req *proto.EncryptedMessage) (*proto.EncryptedMessage, error) + SyncMetaFunc func(ctx context.Context, req *proto.EncryptedMessage) (*proto.Empty, error) } func (m ManagementServiceServerMock) Login(ctx context.Context, req *proto.EncryptedMessage) (*proto.EncryptedMessage, error) { @@ -60,3 +62,10 @@ func (m ManagementServiceServerMock) GetPKCEAuthorizationFlow(ctx context.Contex } return nil, status.Errorf(codes.Unimplemented, "method GetPKCEAuthorizationFlow not implemented") } + +func (m ManagementServiceServerMock) SyncMeta(ctx context.Context, req *proto.EncryptedMessage) (*proto.Empty, error) { + if m.SyncMetaFunc != nil { + return m.SyncMetaFunc(ctx, req) + } + return nil, status.Errorf(codes.Unimplemented, "method SyncMeta not implemented") +} diff --git a/management/server/peer.go b/management/server/peer.go index 13ac3801d..6987cff3e 100644 --- a/management/server/peer.go +++ b/management/server/peer.go @@ -19,6 +19,11 @@ import ( type PeerSync struct { // WireGuardPubKey is a peers WireGuard public key WireGuardPubKey string + // Meta is the system information passed by peer, must be always present + Meta nbpeer.PeerSystemMeta + // UpdateAccountPeers indicate updating account peers, + // which occurs when the peer's metadata is updated + UpdateAccountPeers bool } // PeerLogin used as a data object between the gRPC API and AccountManager on Login request. @@ -528,6 +533,18 @@ func (am *DefaultAccountManager) SyncPeer(sync PeerSync, account *Account) (*nbp return nil, nil, status.Errorf(status.PermissionDenied, "peer login has expired, please log in once more") } + peer, updated := updatePeerMeta(peer, sync.Meta, account) + if updated { + err = am.Store.SaveAccount(account) + if err != nil { + return nil, nil, err + } + + if sync.UpdateAccountPeers { + am.updateAccountPeers(account) + } + } + peerNotValid, isStatusChanged, err := am.integratedPeerValidator.IsNotValidPeer(account.Id, peer, account.GetPeerGroupsList(peer.ID), account.Settings.Extra) if err != nil { return nil, nil, err @@ -900,7 +917,7 @@ func (am *DefaultAccountManager) updateAccountPeers(account *Account) { continue } remotePeerNetworkMap := account.GetPeerNetworkMap(peer.ID, am.dnsDomain, approvedPeersMap) - update := toSyncResponse(nil, peer, nil, remotePeerNetworkMap, am.GetDNSDomain()) + update := toSyncResponse(am, nil, peer, nil, remotePeerNetworkMap, am.GetDNSDomain()) am.peersUpdateManager.SendUpdate(peer.ID, &UpdateMessage{Update: update}) } } diff --git a/management/server/peer/peer.go b/management/server/peer/peer.go index f71f629f6..4f808a79e 100644 --- a/management/server/peer/peer.go +++ b/management/server/peer/peer.go @@ -4,6 +4,7 @@ import ( "fmt" "net" "net/netip" + "slices" "time" ) @@ -79,6 +80,13 @@ type Environment struct { Platform string } +// File is a file on the system. +type File struct { + Path string + Exist bool + ProcessIsRunning bool +} + // PeerSystemMeta is a metadata of a Peer machine system type PeerSystemMeta struct { //nolint:revive Hostname string @@ -96,24 +104,22 @@ type PeerSystemMeta struct { //nolint:revive SystemProductName string SystemManufacturer string Environment Environment `gorm:"serializer:json"` + Files []File `gorm:"serializer:json"` } func (p PeerSystemMeta) isEqual(other PeerSystemMeta) bool { - if len(p.NetworkAddresses) != len(other.NetworkAddresses) { + equalNetworkAddresses := slices.EqualFunc(p.NetworkAddresses, other.NetworkAddresses, func(addr NetworkAddress, oAddr NetworkAddress) bool { + return addr.Mac == oAddr.Mac && addr.NetIP == oAddr.NetIP + }) + if !equalNetworkAddresses { return false } - for _, addr := range p.NetworkAddresses { - var found bool - for _, oAddr := range other.NetworkAddresses { - if addr.Mac == oAddr.Mac && addr.NetIP == oAddr.NetIP { - found = true - continue - } - } - if !found { - return false - } + equalFiles := slices.EqualFunc(p.Files, other.Files, func(file File, oFile File) bool { + return file.Path == oFile.Path && file.Exist == oFile.Exist && file.ProcessIsRunning == oFile.ProcessIsRunning + }) + if !equalFiles { + return false } return p.Hostname == other.Hostname && @@ -133,6 +139,26 @@ func (p PeerSystemMeta) isEqual(other PeerSystemMeta) bool { p.Environment.Platform == other.Environment.Platform } +func (p PeerSystemMeta) isEmpty() bool { + return p.Hostname == "" && + p.GoOS == "" && + p.Kernel == "" && + p.Core == "" && + p.Platform == "" && + p.OS == "" && + p.OSVersion == "" && + p.WtVersion == "" && + p.UIVersion == "" && + p.KernelVersion == "" && + len(p.NetworkAddresses) == 0 && + p.SystemSerialNumber == "" && + p.SystemProductName == "" && + p.SystemManufacturer == "" && + p.Environment.Cloud == "" && + p.Environment.Platform == "" && + len(p.Files) == 0 +} + // AddedWithSSOLogin indicates whether this peer has been added with an SSO login by a user. func (p *Peer) AddedWithSSOLogin() bool { return p.UserID != "" @@ -168,6 +194,10 @@ func (p *Peer) Copy() *Peer { // UpdateMetaIfNew updates peer's system metadata if new information is provided // returns true if meta was updated, false otherwise func (p *Peer) UpdateMetaIfNew(meta PeerSystemMeta) bool { + if meta.isEmpty() { + return false + } + // Avoid overwriting UIVersion if the update was triggered sole by the CLI client if meta.UIVersion == "" { meta.UIVersion = p.Meta.UIVersion diff --git a/management/server/posture/checks.go b/management/server/posture/checks.go index 23b0e8379..3df5beacf 100644 --- a/management/server/posture/checks.go +++ b/management/server/posture/checks.go @@ -1,8 +1,9 @@ package posture import ( - "fmt" + "errors" "net/netip" + "regexp" "github.com/hashicorp/go-version" "github.com/rs/xid" @@ -17,15 +18,21 @@ const ( OSVersionCheckName = "OSVersionCheck" GeoLocationCheckName = "GeoLocationCheck" PeerNetworkRangeCheckName = "PeerNetworkRangeCheck" + ProcessCheckName = "ProcessCheck" CheckActionAllow string = "allow" CheckActionDeny string = "deny" ) +var ( + countryCodeRegex = regexp.MustCompile("^[a-zA-Z]{2}$") +) + // Check represents an interface for performing a check on a peer. type Check interface { - Check(peer nbpeer.Peer) (bool, error) Name() string + Check(peer nbpeer.Peer) (bool, error) + Validate() error } type Checks struct { @@ -51,6 +58,7 @@ type ChecksDefinition struct { OSVersionCheck *OSVersionCheck `json:",omitempty"` GeoLocationCheck *GeoLocationCheck `json:",omitempty"` PeerNetworkRangeCheck *PeerNetworkRangeCheck `json:",omitempty"` + ProcessCheck *ProcessCheck `json:",omitempty"` } // Copy returns a copy of a checks definition. @@ -96,6 +104,13 @@ func (cd ChecksDefinition) Copy() ChecksDefinition { } copy(cdCopy.PeerNetworkRangeCheck.Ranges, peerNetRangeCheck.Ranges) } + if cd.ProcessCheck != nil { + processCheck := cd.ProcessCheck + cdCopy.ProcessCheck = &ProcessCheck{ + Processes: make([]Process, len(processCheck.Processes)), + } + copy(cdCopy.ProcessCheck.Processes, processCheck.Processes) + } return cdCopy } @@ -136,6 +151,9 @@ func (pc *Checks) GetChecks() []Check { if pc.Checks.PeerNetworkRangeCheck != nil { checks = append(checks, pc.Checks.PeerNetworkRangeCheck) } + if pc.Checks.ProcessCheck != nil { + checks = append(checks, pc.Checks.ProcessCheck) + } return checks } @@ -191,6 +209,10 @@ func buildPostureCheck(postureChecksID string, name string, description string, } } + if processCheck := checks.ProcessCheck; processCheck != nil { + postureChecks.Checks.ProcessCheck = toProcessCheck(processCheck) + } + return &postureChecks, nil } @@ -221,6 +243,10 @@ func (pc *Checks) ToAPIResponse() *api.PostureCheck { checks.PeerNetworkRangeCheck = toPeerNetworkRangeCheckResponse(pc.Checks.PeerNetworkRangeCheck) } + if pc.Checks.ProcessCheck != nil { + checks.ProcessCheck = toProcessCheckResponse(pc.Checks.ProcessCheck) + } + return &api.PostureCheck{ Id: pc.ID, Name: pc.Name, @@ -229,44 +255,20 @@ func (pc *Checks) ToAPIResponse() *api.PostureCheck { } } +// Validate checks the validity of a posture checks. func (pc *Checks) Validate() error { - if check := pc.Checks.NBVersionCheck; check != nil { - if !isVersionValid(check.MinVersion) { - return fmt.Errorf("%s version: %s is not valid", check.Name(), check.MinVersion) - } + if pc.Name == "" { + return errors.New("posture checks name shouldn't be empty") } - if osCheck := pc.Checks.OSVersionCheck; osCheck != nil { - if osCheck.Android != nil { - if !isVersionValid(osCheck.Android.MinVersion) { - return fmt.Errorf("%s android version: %s is not valid", osCheck.Name(), osCheck.Android.MinVersion) - } - } + checks := pc.GetChecks() + if len(checks) == 0 { + return errors.New("posture checks shouldn't be empty") + } - if osCheck.Ios != nil { - if !isVersionValid(osCheck.Ios.MinVersion) { - return fmt.Errorf("%s ios version: %s is not valid", osCheck.Name(), osCheck.Ios.MinVersion) - } - } - - if osCheck.Darwin != nil { - if !isVersionValid(osCheck.Darwin.MinVersion) { - return fmt.Errorf("%s darwin version: %s is not valid", osCheck.Name(), osCheck.Darwin.MinVersion) - } - } - - if osCheck.Linux != nil { - if !isVersionValid(osCheck.Linux.MinKernelVersion) { - return fmt.Errorf("%s linux kernel version: %s is not valid", osCheck.Name(), - osCheck.Linux.MinKernelVersion) - } - } - - if osCheck.Windows != nil { - if !isVersionValid(osCheck.Windows.MinKernelVersion) { - return fmt.Errorf("%s windows kernel version: %s is not valid", osCheck.Name(), - osCheck.Windows.MinKernelVersion) - } + for _, check := range checks { + if err := check.Validate(); err != nil { + return err } } @@ -352,3 +354,40 @@ func toPeerNetworkRangeCheck(check *api.PeerNetworkRangeCheck) (*PeerNetworkRang Action: string(check.Action), }, nil } + +func toProcessCheckResponse(check *ProcessCheck) *api.ProcessCheck { + processes := make([]api.Process, 0, len(check.Processes)) + for i := range check.Processes { + processes = append(processes, api.Process{ + LinuxPath: &check.Processes[i].LinuxPath, + MacPath: &check.Processes[i].MacPath, + WindowsPath: &check.Processes[i].WindowsPath, + }) + } + + return &api.ProcessCheck{ + Processes: processes, + } +} + +func toProcessCheck(check *api.ProcessCheck) *ProcessCheck { + processes := make([]Process, 0, len(check.Processes)) + for _, process := range check.Processes { + var p Process + if process.LinuxPath != nil { + p.LinuxPath = *process.LinuxPath + } + if process.MacPath != nil { + p.MacPath = *process.MacPath + } + if process.WindowsPath != nil { + p.WindowsPath = *process.WindowsPath + } + + processes = append(processes, p) + } + + return &ProcessCheck{ + Processes: processes, + } +} diff --git a/management/server/posture/checks_test.go b/management/server/posture/checks_test.go index d36d4f50c..16268b72d 100644 --- a/management/server/posture/checks_test.go +++ b/management/server/posture/checks_test.go @@ -150,9 +150,23 @@ func TestChecks_Validate(t *testing.T) { checks Checks expectedError bool }{ + { + name: "Empty name", + checks: Checks{}, + expectedError: true, + }, + { + name: "Empty checks", + checks: Checks{ + Name: "Default", + Checks: ChecksDefinition{}, + }, + expectedError: true, + }, { name: "Valid checks version", checks: Checks{ + Name: "default", Checks: ChecksDefinition{ NBVersionCheck: &NBVersionCheck{ MinVersion: "0.25.0", @@ -261,6 +275,14 @@ func TestChecks_Copy(t *testing.T) { }, Action: CheckActionDeny, }, + ProcessCheck: &ProcessCheck{ + Processes: []Process{ + { + MacPath: "/Applications/NetBird.app/Contents/MacOS/netbird", + WindowsPath: "C:\\ProgramData\\NetBird\\netbird.exe", + }, + }, + }, }, } checkCopy := check.Copy() diff --git a/management/server/posture/geo_location.go b/management/server/posture/geo_location.go index 856913a7a..b51f80519 100644 --- a/management/server/posture/geo_location.go +++ b/management/server/posture/geo_location.go @@ -2,6 +2,7 @@ package posture import ( "fmt" + "slices" nbpeer "github.com/netbirdio/netbird/management/server/peer" ) @@ -60,3 +61,28 @@ func (g *GeoLocationCheck) Check(peer nbpeer.Peer) (bool, error) { func (g *GeoLocationCheck) Name() string { return GeoLocationCheckName } + +func (g *GeoLocationCheck) Validate() error { + if g.Action == "" { + return fmt.Errorf("%s action shouldn't be empty", g.Name()) + } + + allowedActions := []string{CheckActionAllow, CheckActionDeny} + if !slices.Contains(allowedActions, g.Action) { + return fmt.Errorf("%s action is not valid", g.Name()) + } + + if len(g.Locations) == 0 { + return fmt.Errorf("%s locations shouldn't be empty", g.Name()) + } + + for _, loc := range g.Locations { + if loc.CountryCode == "" { + return fmt.Errorf("%s country code shouldn't be empty", g.Name()) + } + if !countryCodeRegex.MatchString(loc.CountryCode) { + return fmt.Errorf("%s country code must be 2 letters (ISO 3166-1 alpha-2 format)", g.Name()) + } + } + return nil +} diff --git a/management/server/posture/geo_location_test.go b/management/server/posture/geo_location_test.go index 267bbe0f2..a92732c53 100644 --- a/management/server/posture/geo_location_test.go +++ b/management/server/posture/geo_location_test.go @@ -236,3 +236,81 @@ func TestGeoLocationCheck_Check(t *testing.T) { }) } } + +func TestGeoLocationCheck_Validate(t *testing.T) { + testCases := []struct { + name string + check GeoLocationCheck + expectedError bool + }{ + { + name: "Valid location list", + check: GeoLocationCheck{ + Action: CheckActionAllow, + Locations: []Location{ + { + CountryCode: "DE", + CityName: "Berlin", + }, + }, + }, + expectedError: false, + }, + { + name: "Invalid empty location list", + check: GeoLocationCheck{ + Action: CheckActionDeny, + Locations: []Location{}, + }, + expectedError: true, + }, + { + name: "Invalid empty country name", + check: GeoLocationCheck{ + Action: CheckActionDeny, + Locations: []Location{ + { + CityName: "Los Angeles", + }, + }, + }, + expectedError: true, + }, + { + name: "Invalid check action", + check: GeoLocationCheck{ + Action: "unknownAction", + Locations: []Location{ + { + CountryCode: "DE", + CityName: "Berlin", + }, + }, + }, + expectedError: true, + }, + { + name: "Invalid country code", + check: GeoLocationCheck{ + Action: CheckActionAllow, + Locations: []Location{ + { + CountryCode: "USA", + }, + }, + }, + expectedError: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := tc.check.Validate() + if tc.expectedError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/management/server/posture/nb_version.go b/management/server/posture/nb_version.go index 0645b8f73..62a6e268c 100644 --- a/management/server/posture/nb_version.go +++ b/management/server/posture/nb_version.go @@ -1,6 +1,8 @@ package posture import ( + "fmt" + "github.com/hashicorp/go-version" log "github.com/sirupsen/logrus" @@ -37,3 +39,13 @@ func (n *NBVersionCheck) Check(peer nbpeer.Peer) (bool, error) { func (n *NBVersionCheck) Name() string { return NBVersionCheckName } + +func (n *NBVersionCheck) Validate() error { + if n.MinVersion == "" { + return fmt.Errorf("%s minimum version shouldn't be empty", n.Name()) + } + if !isVersionValid(n.MinVersion) { + return fmt.Errorf("%s version: %s is not valid", n.Name(), n.MinVersion) + } + return nil +} diff --git a/management/server/posture/nb_version_test.go b/management/server/posture/nb_version_test.go index de51c2283..fbe24aa16 100644 --- a/management/server/posture/nb_version_test.go +++ b/management/server/posture/nb_version_test.go @@ -108,3 +108,33 @@ func TestNBVersionCheck_Check(t *testing.T) { }) } } + +func TestNBVersionCheck_Validate(t *testing.T) { + testCases := []struct { + name string + check NBVersionCheck + expectedError bool + }{ + { + name: "Valid NBVersionCheck", + check: NBVersionCheck{MinVersion: "1.0"}, + expectedError: false, + }, + { + name: "Invalid NBVersionCheck", + check: NBVersionCheck{}, + expectedError: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := tc.check.Validate() + if tc.expectedError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/management/server/posture/network.go b/management/server/posture/network.go index 9bf969f4c..ed90ea91b 100644 --- a/management/server/posture/network.go +++ b/management/server/posture/network.go @@ -6,6 +6,7 @@ import ( "slices" nbpeer "github.com/netbirdio/netbird/management/server/peer" + "github.com/netbirdio/netbird/management/server/status" ) type PeerNetworkRangeCheck struct { @@ -52,3 +53,19 @@ func (p *PeerNetworkRangeCheck) Check(peer nbpeer.Peer) (bool, error) { func (p *PeerNetworkRangeCheck) Name() string { return PeerNetworkRangeCheckName } + +func (p *PeerNetworkRangeCheck) Validate() error { + if p.Action == "" { + return status.Errorf(status.InvalidArgument, "action for peer network range check shouldn't be empty") + } + + allowedActions := []string{CheckActionAllow, CheckActionDeny} + if !slices.Contains(allowedActions, p.Action) { + return fmt.Errorf("%s action is not valid", p.Name()) + } + + if len(p.Ranges) == 0 { + return fmt.Errorf("%s network ranges shouldn't be empty", p.Name()) + } + return nil +} diff --git a/management/server/posture/network_test.go b/management/server/posture/network_test.go index 36ead4660..6242ece99 100644 --- a/management/server/posture/network_test.go +++ b/management/server/posture/network_test.go @@ -147,3 +147,52 @@ func TestPeerNetworkRangeCheck_Check(t *testing.T) { }) } } + +func TestNetworkCheck_Validate(t *testing.T) { + testCases := []struct { + name string + check PeerNetworkRangeCheck + expectedError bool + }{ + { + name: "Valid network range", + check: PeerNetworkRangeCheck{ + Action: CheckActionAllow, + Ranges: []netip.Prefix{ + netip.MustParsePrefix("192.168.1.0/24"), + netip.MustParsePrefix("10.0.0.0/8"), + }, + }, + expectedError: false, + }, + { + name: "Invalid empty network range", + check: PeerNetworkRangeCheck{ + Action: CheckActionDeny, + Ranges: []netip.Prefix{}, + }, + expectedError: true, + }, + { + name: "Invalid check action", + check: PeerNetworkRangeCheck{ + Action: "unknownAction", + Ranges: []netip.Prefix{ + netip.MustParsePrefix("10.0.0.0/8"), + }, + }, + expectedError: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := tc.check.Validate() + if tc.expectedError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/management/server/posture/os_version.go b/management/server/posture/os_version.go index 4c311f01b..e6f8ec367 100644 --- a/management/server/posture/os_version.go +++ b/management/server/posture/os_version.go @@ -1,11 +1,13 @@ package posture import ( + "fmt" "strings" "github.com/hashicorp/go-version" - nbpeer "github.com/netbirdio/netbird/management/server/peer" log "github.com/sirupsen/logrus" + + nbpeer "github.com/netbirdio/netbird/management/server/peer" ) type MinVersionCheck struct { @@ -48,6 +50,35 @@ func (c *OSVersionCheck) Name() string { return OSVersionCheckName } +func (c *OSVersionCheck) Validate() error { + if c.Android == nil && c.Darwin == nil && c.Ios == nil && c.Linux == nil && c.Windows == nil { + return fmt.Errorf("%s at least one OS version check is required", c.Name()) + } + + if c.Android != nil && !isVersionValid(c.Android.MinVersion) { + return fmt.Errorf("%s android version: %s is not valid", c.Name(), c.Android.MinVersion) + } + + if c.Ios != nil && !isVersionValid(c.Ios.MinVersion) { + return fmt.Errorf("%s ios version: %s is not valid", c.Name(), c.Ios.MinVersion) + } + + if c.Darwin != nil && !isVersionValid(c.Darwin.MinVersion) { + return fmt.Errorf("%s darwin version: %s is not valid", c.Name(), c.Darwin.MinVersion) + } + + if c.Linux != nil && !isVersionValid(c.Linux.MinKernelVersion) { + return fmt.Errorf("%s linux kernel version: %s is not valid", c.Name(), + c.Linux.MinKernelVersion) + } + + if c.Windows != nil && !isVersionValid(c.Windows.MinKernelVersion) { + return fmt.Errorf("%s windows kernel version: %s is not valid", c.Name(), + c.Windows.MinKernelVersion) + } + return nil +} + func checkMinVersion(peerGoOS, peerVersion string, check *MinVersionCheck) (bool, error) { if check == nil { log.Debugf("peer %s OS is not allowed in the check", peerGoOS) diff --git a/management/server/posture/os_version_test.go b/management/server/posture/os_version_test.go index 32bf52660..845e703cf 100644 --- a/management/server/posture/os_version_test.go +++ b/management/server/posture/os_version_test.go @@ -150,3 +150,79 @@ func TestOSVersionCheck_Check(t *testing.T) { }) } } + +func TestOSVersionCheck_Validate(t *testing.T) { + testCases := []struct { + name string + check OSVersionCheck + expectedError bool + }{ + { + name: "Valid linux kernel version", + check: OSVersionCheck{ + Linux: &MinKernelVersionCheck{MinKernelVersion: "6.0"}, + }, + expectedError: false, + }, + { + name: "Valid linux and darwin version", + check: OSVersionCheck{ + Linux: &MinKernelVersionCheck{MinKernelVersion: "6.0"}, + Darwin: &MinVersionCheck{MinVersion: "14.2"}, + }, + expectedError: false, + }, + { + name: "Invalid empty check", + check: OSVersionCheck{}, + expectedError: true, + }, + { + name: "Invalid empty linux kernel version", + check: OSVersionCheck{ + Linux: &MinKernelVersionCheck{}, + }, + expectedError: true, + }, + { + name: "Invalid empty linux kernel version with correct darwin version", + check: OSVersionCheck{ + Linux: &MinKernelVersionCheck{}, + Darwin: &MinVersionCheck{MinVersion: "14.2"}, + }, + expectedError: true, + }, + { + name: "Valid windows kernel version", + check: OSVersionCheck{ + Windows: &MinKernelVersionCheck{MinKernelVersion: "10.0"}, + }, + expectedError: false, + }, + { + name: "Valid ios minimum version", + check: OSVersionCheck{ + Ios: &MinVersionCheck{MinVersion: "13.0"}, + }, + expectedError: false, + }, + { + name: "Invalid empty window version with valid ios minimum version", + check: OSVersionCheck{ + Windows: &MinKernelVersionCheck{}, + Ios: &MinVersionCheck{MinVersion: "13.0"}, + }, + expectedError: true, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := tc.check.Validate() + if tc.expectedError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/management/server/posture/process.go b/management/server/posture/process.go new file mode 100644 index 000000000..cd3b23fcf --- /dev/null +++ b/management/server/posture/process.go @@ -0,0 +1,79 @@ +package posture + +import ( + "fmt" + "slices" + + nbpeer "github.com/netbirdio/netbird/management/server/peer" +) + +type Process struct { + LinuxPath string + MacPath string + WindowsPath string +} + +type ProcessCheck struct { + Processes []Process +} + +var _ Check = (*ProcessCheck)(nil) + +func (p *ProcessCheck) Check(peer nbpeer.Peer) (bool, error) { + peerActiveProcesses := extractPeerActiveProcesses(peer.Meta.Files) + + var pathSelector func(Process) string + switch peer.Meta.GoOS { + case "linux": + pathSelector = func(process Process) string { return process.LinuxPath } + case "darwin": + pathSelector = func(process Process) string { return process.MacPath } + case "windows": + pathSelector = func(process Process) string { return process.WindowsPath } + default: + return false, fmt.Errorf("unsupported peer's operating system: %s", peer.Meta.GoOS) + } + + return p.areAllProcessesRunning(peerActiveProcesses, pathSelector), nil +} + +func (p *ProcessCheck) Name() string { + return ProcessCheckName +} + +func (p *ProcessCheck) Validate() error { + if len(p.Processes) == 0 { + return fmt.Errorf("%s processes shouldn't be empty", p.Name()) + } + + for _, process := range p.Processes { + if process.LinuxPath == "" && process.MacPath == "" && process.WindowsPath == "" { + return fmt.Errorf("%s path shouldn't be empty", p.Name()) + } + } + return nil +} + +// areAllProcessesRunning checks if all processes specified in ProcessCheck are running. +// It uses the provided pathSelector to get the appropriate process path for the peer's OS. +// It returns true if all processes are running, otherwise false. +func (p *ProcessCheck) areAllProcessesRunning(activeProcesses []string, pathSelector func(Process) string) bool { + for _, process := range p.Processes { + path := pathSelector(process) + if path == "" || !slices.Contains(activeProcesses, path) { + return false + } + } + return true +} + +// extractPeerActiveProcesses extracts the paths of running processes from the peer meta. +func extractPeerActiveProcesses(files []nbpeer.File) []string { + activeProcesses := make([]string, 0, len(files)) + for _, file := range files { + if file.ProcessIsRunning { + activeProcesses = append(activeProcesses, file.Path) + } + } + return activeProcesses +} diff --git a/management/server/posture/process_test.go b/management/server/posture/process_test.go new file mode 100644 index 000000000..0bfaf4cb9 --- /dev/null +++ b/management/server/posture/process_test.go @@ -0,0 +1,318 @@ +package posture + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/netbirdio/netbird/management/server/peer" +) + +func TestProcessCheck_Check(t *testing.T) { + tests := []struct { + name string + input peer.Peer + check ProcessCheck + wantErr bool + isValid bool + }{ + { + name: "darwin with matching running processes", + input: peer.Peer{ + Meta: peer.PeerSystemMeta{ + GoOS: "darwin", + Files: []peer.File{ + {Path: "/Applications/process1.app", ProcessIsRunning: true}, + {Path: "/Applications/process2.app", ProcessIsRunning: true}, + }, + }, + }, + check: ProcessCheck{ + Processes: []Process{ + {MacPath: "/Applications/process1.app"}, + {MacPath: "/Applications/process2.app"}, + }, + }, + wantErr: false, + isValid: true, + }, + { + name: "darwin with windows process paths", + input: peer.Peer{ + Meta: peer.PeerSystemMeta{ + GoOS: "darwin", + Files: []peer.File{ + {Path: "/Applications/process1.app", ProcessIsRunning: true}, + {Path: "/Applications/process2.app", ProcessIsRunning: true}, + }, + }, + }, + check: ProcessCheck{ + Processes: []Process{ + {WindowsPath: "C:\\Program Files\\process1.exe"}, + {WindowsPath: "C:\\Program Files\\process2.exe"}, + }, + }, + wantErr: false, + isValid: false, + }, + { + name: "linux with matching running processes", + input: peer.Peer{ + Meta: peer.PeerSystemMeta{ + GoOS: "linux", + Files: []peer.File{ + {Path: "/usr/bin/process1", ProcessIsRunning: true}, + {Path: "/usr/bin/process2", ProcessIsRunning: true}, + }, + }, + }, + check: ProcessCheck{ + Processes: []Process{ + {LinuxPath: "/usr/bin/process1"}, + {LinuxPath: "/usr/bin/process2"}, + }, + }, + wantErr: false, + isValid: true, + }, + { + name: "linux with matching no running processes", + input: peer.Peer{ + Meta: peer.PeerSystemMeta{ + GoOS: "linux", + Files: []peer.File{ + {Path: "/usr/bin/process1", ProcessIsRunning: true}, + {Path: "/usr/bin/process2", ProcessIsRunning: false}, + }, + }, + }, + check: ProcessCheck{ + Processes: []Process{ + {LinuxPath: "/usr/bin/process1"}, + {LinuxPath: "/usr/bin/process2"}, + }, + }, + wantErr: false, + isValid: false, + }, + { + name: "linux with windows process paths", + input: peer.Peer{ + Meta: peer.PeerSystemMeta{ + GoOS: "linux", + Files: []peer.File{ + {Path: "/usr/bin/process1", ProcessIsRunning: true}, + {Path: "/usr/bin/process2"}, + }, + }, + }, + check: ProcessCheck{ + Processes: []Process{ + {WindowsPath: "C:\\Program Files\\process1.exe"}, + {WindowsPath: "C:\\Program Files\\process2.exe"}, + }, + }, + wantErr: false, + isValid: false, + }, + { + name: "linux with non-matching processes", + input: peer.Peer{ + Meta: peer.PeerSystemMeta{ + GoOS: "linux", + Files: []peer.File{ + {Path: "/usr/bin/process3"}, + {Path: "/usr/bin/process4"}, + }, + }, + }, + check: ProcessCheck{ + Processes: []Process{ + {LinuxPath: "/usr/bin/process1"}, + {LinuxPath: "/usr/bin/process2"}, + }, + }, + wantErr: false, + isValid: false, + }, + { + name: "windows with matching running processes", + input: peer.Peer{ + Meta: peer.PeerSystemMeta{ + GoOS: "windows", + Files: []peer.File{ + {Path: "C:\\Program Files\\process1.exe", ProcessIsRunning: true}, + {Path: "C:\\Program Files\\process1.exe", ProcessIsRunning: true}, + }, + }, + }, + check: ProcessCheck{ + Processes: []Process{ + {WindowsPath: "C:\\Program Files\\process1.exe"}, + {WindowsPath: "C:\\Program Files\\process1.exe"}, + }, + }, + wantErr: false, + isValid: true, + }, + { + name: "windows with darwin process paths", + input: peer.Peer{ + Meta: peer.PeerSystemMeta{ + GoOS: "windows", + Files: []peer.File{ + {Path: "C:\\Program Files\\process1.exe"}, + {Path: "C:\\Program Files\\process1.exe"}, + }, + }, + }, + check: ProcessCheck{ + Processes: []Process{ + {MacPath: "/Applications/process1.app"}, + {LinuxPath: "/Applications/process2.app"}, + }, + }, + wantErr: false, + isValid: false, + }, + { + name: "windows with non-matching processes", + input: peer.Peer{ + Meta: peer.PeerSystemMeta{ + GoOS: "windows", + Files: []peer.File{ + {Path: "C:\\Program Files\\process3.exe"}, + {Path: "C:\\Program Files\\process4.exe"}, + }, + }, + }, + check: ProcessCheck{ + Processes: []Process{ + {WindowsPath: "C:\\Program Files\\process1.exe"}, + {WindowsPath: "C:\\Program Files\\process2.exe"}, + }, + }, + wantErr: false, + isValid: false, + }, + { + name: "unsupported ios operating system", + input: peer.Peer{ + Meta: peer.PeerSystemMeta{ + GoOS: "ios", + }, + }, + check: ProcessCheck{ + Processes: []Process{ + {WindowsPath: "C:\\Program Files\\process1.exe"}, + {MacPath: "/Applications/process2.app"}, + }, + }, + wantErr: true, + isValid: false, + }, + { + name: "unsupported android operating system", + input: peer.Peer{ + Meta: peer.PeerSystemMeta{ + GoOS: "android", + }, + }, + check: ProcessCheck{ + Processes: []Process{ + {WindowsPath: "C:\\Program Files\\process1.exe"}, + {MacPath: "/Applications/process2.app"}, + {LinuxPath: "/usr/bin/process2"}, + }, + }, + wantErr: true, + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + isValid, err := tt.check.Check(tt.input) + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + assert.Equal(t, tt.isValid, isValid) + }) + } +} + +func TestProcessCheck_Validate(t *testing.T) { + testCases := []struct { + name string + check ProcessCheck + expectedError bool + }{ + { + name: "Valid linux, mac and windows processes", + check: ProcessCheck{ + Processes: []Process{ + { + LinuxPath: "/usr/local/bin/netbird", + MacPath: "/usr/local/bin/netbird", + WindowsPath: "C:\\ProgramData\\NetBird\\netbird.exe", + }, + }, + }, + expectedError: false, + }, + { + name: "Valid linux process", + check: ProcessCheck{ + Processes: []Process{ + { + LinuxPath: "/usr/local/bin/netbird", + }, + }, + }, + expectedError: false, + }, + { + name: "Valid mac process", + check: ProcessCheck{ + Processes: []Process{ + { + MacPath: "/Applications/NetBird.app/Contents/MacOS/netbird", + }, + }, + }, + expectedError: false, + }, + { + name: "Valid windows process", + check: ProcessCheck{ + Processes: []Process{ + { + WindowsPath: "C:\\ProgramData\\NetBird\\netbird.exe", + }, + }, + }, + expectedError: false, + }, + { + name: "Invalid empty processes", + check: ProcessCheck{ + Processes: []Process{}, + }, + expectedError: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := tc.check.Validate() + if tc.expectedError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/management/server/posture_checks.go b/management/server/posture_checks.go index fb904c10f..873f8da59 100644 --- a/management/server/posture_checks.go +++ b/management/server/posture_checks.go @@ -1,9 +1,17 @@ package server import ( + "slices" + "github.com/netbirdio/netbird/management/server/activity" + nbpeer "github.com/netbirdio/netbird/management/server/peer" "github.com/netbirdio/netbird/management/server/posture" "github.com/netbirdio/netbird/management/server/status" + log "github.com/sirupsen/logrus" +) + +const ( + errMsgPostureAdminOnly = "only users with admin power are allowed to view posture checks" ) func (am *DefaultAccountManager) GetPostureChecks(accountID, postureChecksID, userID string) (*posture.Checks, error) { @@ -21,7 +29,7 @@ func (am *DefaultAccountManager) GetPostureChecks(accountID, postureChecksID, us } if !user.HasAdminPower() { - return nil, status.Errorf(status.PermissionDenied, "only users with admin power are allowed to view posture checks") + return nil, status.Errorf(status.PermissionDenied, errMsgPostureAdminOnly) } for _, postureChecks := range account.PostureChecks { @@ -48,11 +56,11 @@ func (am *DefaultAccountManager) SavePostureChecks(accountID, userID string, pos } if !user.HasAdminPower() { - return status.Errorf(status.PermissionDenied, "only users with admin power are allowed to view posture checks") + return status.Errorf(status.PermissionDenied, errMsgPostureAdminOnly) } if err := postureChecks.Validate(); err != nil { - return status.Errorf(status.BadRequest, err.Error()) + return status.Errorf(status.InvalidArgument, err.Error()) } exists, uniqName := am.savePostureChecks(account, postureChecks) @@ -95,7 +103,7 @@ func (am *DefaultAccountManager) DeletePostureChecks(accountID, postureChecksID, } if !user.HasAdminPower() { - return status.Errorf(status.PermissionDenied, "only users with admin power are allowed to view posture checks") + return status.Errorf(status.PermissionDenied, errMsgPostureAdminOnly) } postureChecks, err := am.deletePostureChecks(account, postureChecksID) @@ -127,7 +135,7 @@ func (am *DefaultAccountManager) ListPostureChecks(accountID, userID string) ([] } if !user.HasAdminPower() { - return nil, status.Errorf(status.PermissionDenied, "only users with admin power are allowed to view posture checks") + return nil, status.Errorf(status.PermissionDenied, errMsgPostureAdminOnly) } return account.PostureChecks, nil @@ -176,3 +184,74 @@ func (am *DefaultAccountManager) deletePostureChecks(account *Account, postureCh return postureChecks, nil } + +// GetPeerAppliedPostureChecks returns posture checks that are applied to the peer. +func (am *DefaultAccountManager) GetPeerAppliedPostureChecks(peerKey string) ([]posture.Checks, error) { + account, err := am.Store.GetAccountByPeerPubKey(peerKey) + if err != nil { + log.Errorf("failed while getting peer %s: %v", peerKey, err) + return nil, err + } + + peer, err := account.FindPeerByPubKey(peerKey) + if err != nil { + return nil, status.Errorf(status.NotFound, "peer is not registered") + } + if peer == nil { + return nil, nil + } + + peerPostureChecks := am.collectPeerPostureChecks(account, peer) + + postureChecksList := make([]posture.Checks, 0, len(peerPostureChecks)) + for _, check := range peerPostureChecks { + postureChecksList = append(postureChecksList, check) + } + + return postureChecksList, nil +} + +// collectPeerPostureChecks collects the posture checks applied for a given peer. +func (am *DefaultAccountManager) collectPeerPostureChecks(account *Account, peer *nbpeer.Peer) map[string]posture.Checks { + peerPostureChecks := make(map[string]posture.Checks) + + for _, policy := range account.Policies { + if !policy.Enabled { + continue + } + + if isPeerInPolicySourceGroups(peer.ID, account, policy) { + addPolicyPostureChecks(account, policy, peerPostureChecks) + } + } + + return peerPostureChecks +} + +// isPeerInPolicySourceGroups checks if a peer is present in any of the policy rule source groups. +func isPeerInPolicySourceGroups(peerID string, account *Account, policy *Policy) bool { + for _, rule := range policy.Rules { + if !rule.Enabled { + continue + } + + for _, sourceGroup := range rule.Sources { + group, ok := account.Groups[sourceGroup] + if ok && slices.Contains(group.Peers, peerID) { + return true + } + } + } + + return false +} + +func addPolicyPostureChecks(account *Account, policy *Policy, peerPostureChecks map[string]posture.Checks) { + for _, sourcePostureCheckID := range policy.SourcePostureChecks { + for _, postureCheck := range account.PostureChecks { + if postureCheck.ID == sourcePostureCheckID { + peerPostureChecks[sourcePostureCheckID] = *postureCheck + } + } + } +} diff --git a/management/server/route.go b/management/server/route.go index 2de813d48..8741cc47d 100644 --- a/management/server/route.go +++ b/management/server/route.go @@ -6,6 +6,7 @@ import ( "github.com/rs/xid" + "github.com/netbirdio/netbird/management/domain" "github.com/netbirdio/netbird/management/proto" "github.com/netbirdio/netbird/management/server/activity" "github.com/netbirdio/netbird/management/server/status" @@ -39,10 +40,10 @@ func (am *DefaultAccountManager) GetRoute(accountID string, routeID route.ID, us return nil, status.Errorf(status.NotFound, "route with ID %s not found", routeID) } -// checkRoutePrefixExistsForPeers checks if a route with a given prefix exists for a single peer or multiple peer groups. -func (am *DefaultAccountManager) checkRoutePrefixExistsForPeers(account *Account, peerID string, routeID route.ID, peerGroupIDs []string, prefix netip.Prefix) error { +// checkRoutePrefixOrDomainsExistForPeers checks if a route with a given prefix exists for a single peer or multiple peer groups. +func (am *DefaultAccountManager) checkRoutePrefixOrDomainsExistForPeers(account *Account, peerID string, routeID route.ID, peerGroupIDs []string, prefix netip.Prefix, domains domain.List) error { // routes can have both peer and peer_groups - routesWithPrefix := account.GetRoutesByPrefix(prefix) + routesWithPrefix := account.GetRoutesByPrefixOrDomains(prefix, domains) // lets remember all the peers and the peer groups from routesWithPrefix seenPeers := make(map[string]bool) @@ -114,7 +115,7 @@ func (am *DefaultAccountManager) checkRoutePrefixExistsForPeers(account *Account } // CreateRoute creates and saves a new route -func (am *DefaultAccountManager) CreateRoute(accountID, network, peerID string, peerGroupIDs []string, description string, netID route.NetID, masquerade bool, metric int, groups []string, enabled bool, userID string) (*route.Route, error) { +func (am *DefaultAccountManager) CreateRoute(accountID string, prefix netip.Prefix, networkType route.NetworkType, domains domain.List, peerID string, peerGroupIDs []string, description string, netID route.NetID, masquerade bool, metric int, groups []string, enabled bool, userID string, keepRoute bool) (*route.Route, error) { unlock := am.Store.AcquireAccountWriteLock(accountID) defer unlock() @@ -123,6 +124,18 @@ func (am *DefaultAccountManager) CreateRoute(accountID, network, peerID string, return nil, err } + if len(domains) > 0 && prefix.IsValid() { + return nil, status.Errorf(status.InvalidArgument, "domains and network should not be provided at the same time") + } + + if len(domains) == 0 && !prefix.IsValid() { + return nil, status.Errorf(status.InvalidArgument, "invalid Prefix") + } + + if len(domains) > 0 { + prefix = getPlaceholderIP() + } + if peerID != "" && len(peerGroupIDs) != 0 { return nil, status.Errorf( status.InvalidArgument, @@ -133,11 +146,6 @@ func (am *DefaultAccountManager) CreateRoute(accountID, network, peerID string, var newRoute route.Route newRoute.ID = route.ID(xid.New().String()) - prefixType, newPrefix, err := route.ParseNetwork(network) - if err != nil { - return nil, status.Errorf(status.InvalidArgument, "failed to parse IP %s", network) - } - if len(peerGroupIDs) > 0 { err = validateGroups(peerGroupIDs, account.Groups) if err != nil { @@ -145,7 +153,7 @@ func (am *DefaultAccountManager) CreateRoute(accountID, network, peerID string, } } - err = am.checkRoutePrefixExistsForPeers(account, peerID, newRoute.ID, peerGroupIDs, newPrefix) + err = am.checkRoutePrefixOrDomainsExistForPeers(account, peerID, newRoute.ID, peerGroupIDs, prefix, domains) if err != nil { return nil, err } @@ -165,14 +173,16 @@ func (am *DefaultAccountManager) CreateRoute(accountID, network, peerID string, newRoute.Peer = peerID newRoute.PeerGroups = peerGroupIDs - newRoute.Network = newPrefix - newRoute.NetworkType = prefixType + newRoute.Network = prefix + newRoute.Domains = domains + newRoute.NetworkType = networkType newRoute.Description = description newRoute.NetID = netID newRoute.Masquerade = masquerade newRoute.Metric = metric newRoute.Enabled = enabled newRoute.Groups = groups + newRoute.KeepRoute = keepRoute if account.Routes == nil { account.Routes = make(map[route.ID]*route.Route) @@ -201,10 +211,6 @@ func (am *DefaultAccountManager) SaveRoute(accountID, userID string, routeToSave return status.Errorf(status.InvalidArgument, "route provided is nil") } - if !routeToSave.Network.IsValid() { - return status.Errorf(status.InvalidArgument, "invalid Prefix %s", routeToSave.Network.String()) - } - if routeToSave.Metric < route.MinMetric || routeToSave.Metric > route.MaxMetric { return status.Errorf(status.InvalidArgument, "metric should be between %d and %d", route.MinMetric, route.MaxMetric) } @@ -218,6 +224,18 @@ func (am *DefaultAccountManager) SaveRoute(accountID, userID string, routeToSave return err } + if len(routeToSave.Domains) > 0 && routeToSave.Network.IsValid() { + return status.Errorf(status.InvalidArgument, "domains and network should not be provided at the same time") + } + + if len(routeToSave.Domains) == 0 && !routeToSave.Network.IsValid() { + return status.Errorf(status.InvalidArgument, "invalid Prefix") + } + + if len(routeToSave.Domains) > 0 { + routeToSave.Network = getPlaceholderIP() + } + if routeToSave.Peer != "" && len(routeToSave.PeerGroups) != 0 { return status.Errorf(status.InvalidArgument, "peer with ID and peer groups should not be provided at the same time") } @@ -229,7 +247,7 @@ func (am *DefaultAccountManager) SaveRoute(accountID, userID string, routeToSave } } - err = am.checkRoutePrefixExistsForPeers(account, routeToSave.Peer, routeToSave.ID, routeToSave.Copy().PeerGroups, routeToSave.Network) + err = am.checkRoutePrefixOrDomainsExistForPeers(account, routeToSave.Peer, routeToSave.ID, routeToSave.Copy().PeerGroups, routeToSave.Network, routeToSave.Domains) if err != nil { return err } @@ -313,10 +331,12 @@ func toProtocolRoute(route *route.Route) *proto.Route { ID: string(route.ID), NetID: string(route.NetID), Network: route.Network.String(), + Domains: route.Domains.ToPunycodeList(), NetworkType: int64(route.NetworkType), Peer: route.Peer, Metric: int64(route.Metric), Masquerade: route.Masquerade, + KeepRoute: route.KeepRoute, } } @@ -327,3 +347,9 @@ func toProtocolRoutes(routes []*route.Route) []*proto.Route { } return protoRoutes } + +// getPlaceholderIP returns a placeholder IP address for the route if domains are used +func getPlaceholderIP() netip.Prefix { + // Using an IP from the documentation range to minimize impact in case older clients try to set a route + return netip.PrefixFrom(netip.AddrFrom4([4]byte{192, 0, 2, 0}), 32) +} diff --git a/management/server/route_test.go b/management/server/route_test.go index d28b40d48..4fd1d7357 100644 --- a/management/server/route_test.go +++ b/management/server/route_test.go @@ -8,6 +8,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/netbirdio/netbird/management/domain" "github.com/netbirdio/netbird/management/server/activity" nbgroup "github.com/netbirdio/netbird/management/server/group" nbpeer "github.com/netbirdio/netbird/management/server/peer" @@ -33,13 +34,18 @@ const ( routeGroupHA2 = "routeGroupHA2" routeInvalidGroup1 = "routeInvalidGroup1" userID = "testingUser" - existingNetwork = "10.10.10.0/24" existingRouteID = "random-id" ) +var existingNetwork = netip.MustParsePrefix("10.10.10.0/24") +var existingDomains = domain.List{"example.com"} + func TestCreateRoute(t *testing.T) { type input struct { - network string + network netip.Prefix + domains domain.List + keepRoute bool + networkType route.NetworkType netID route.NetID peerKey string peerGroupIDs []string @@ -59,9 +65,10 @@ func TestCreateRoute(t *testing.T) { expectedRoute *route.Route }{ { - name: "Happy Path", + name: "Happy Path Network", inputArgs: input{ - network: "192.168.0.0/16", + network: netip.MustParsePrefix("192.168.0.0/16"), + networkType: route.IPv4Network, netID: "happy", peerKey: peer1ID, description: "super", @@ -84,10 +91,41 @@ func TestCreateRoute(t *testing.T) { Groups: []string{routeGroup1}, }, }, + { + name: "Happy Path Domains", + inputArgs: input{ + domains: domain.List{"domain1", "domain2"}, + keepRoute: true, + networkType: route.DomainNetwork, + netID: "happy", + peerKey: peer1ID, + description: "super", + masquerade: false, + metric: 9999, + enabled: true, + groups: []string{routeGroup1}, + }, + errFunc: require.NoError, + shouldCreate: true, + expectedRoute: &route.Route{ + Network: netip.MustParsePrefix("192.0.2.0/32"), + Domains: domain.List{"domain1", "domain2"}, + NetworkType: route.DomainNetwork, + NetID: "happy", + Peer: peer1ID, + Description: "super", + Masquerade: false, + Metric: 9999, + Enabled: true, + Groups: []string{routeGroup1}, + KeepRoute: true, + }, + }, { name: "Happy Path Peer Groups", inputArgs: input{ - network: "192.168.0.0/16", + network: netip.MustParsePrefix("192.168.0.0/16"), + networkType: route.IPv4Network, netID: "happy", peerGroupIDs: []string{routeGroupHA1, routeGroupHA2}, description: "super", @@ -111,9 +149,10 @@ func TestCreateRoute(t *testing.T) { }, }, { - name: "Both peer and peer_groups Provided Should Fail", + name: "Both network and domains provided should fail", inputArgs: input{ - network: "192.168.0.0/16", + network: netip.MustParsePrefix("192.168.0.0/16"), + domains: domain.List{"domain1", "domain2"}, netID: "happy", peerKey: peer1ID, peerGroupIDs: []string{routeGroupHA1}, @@ -127,16 +166,18 @@ func TestCreateRoute(t *testing.T) { shouldCreate: false, }, { - name: "Bad Prefix Should Fail", + name: "Both peer and peer_groups Provided Should Fail", inputArgs: input{ - network: "192.168.0.0/34", - netID: "happy", - peerKey: peer1ID, - description: "super", - masquerade: false, - metric: 9999, - enabled: true, - groups: []string{routeGroup1}, + network: netip.MustParsePrefix("192.168.0.0/16"), + networkType: route.IPv4Network, + netID: "happy", + peerKey: peer1ID, + peerGroupIDs: []string{routeGroupHA1}, + description: "super", + masquerade: false, + metric: 9999, + enabled: true, + groups: []string{routeGroup1}, }, errFunc: require.Error, shouldCreate: false, @@ -144,7 +185,8 @@ func TestCreateRoute(t *testing.T) { { name: "Bad Peer Should Fail", inputArgs: input{ - network: "192.168.0.0/16", + network: netip.MustParsePrefix("192.168.0.0/16"), + networkType: route.IPv4Network, netID: "happy", peerKey: "notExistingPeer", description: "super", @@ -157,9 +199,10 @@ func TestCreateRoute(t *testing.T) { shouldCreate: false, }, { - name: "Bad Peer already has this route", + name: "Bad Peer already has this network route", inputArgs: input{ network: existingNetwork, + networkType: route.IPv4Network, netID: "bad", peerKey: peer5ID, description: "super", @@ -173,9 +216,44 @@ func TestCreateRoute(t *testing.T) { shouldCreate: false, }, { - name: "Bad Peers Group already has this route", + name: "Bad Peer already has this domains route", + inputArgs: input{ + domains: existingDomains, + networkType: route.DomainNetwork, + netID: "bad", + peerKey: peer5ID, + description: "super", + masquerade: false, + metric: 9999, + enabled: true, + groups: []string{routeGroup1}, + }, + createInitRoute: true, + errFunc: require.Error, + shouldCreate: false, + }, + { + name: "Bad Peers Group already has this network route", inputArgs: input{ network: existingNetwork, + networkType: route.IPv4Network, + netID: "bad", + peerGroupIDs: []string{routeGroup1, routeGroup3}, + description: "super", + masquerade: false, + metric: 9999, + enabled: true, + groups: []string{routeGroup1}, + }, + createInitRoute: true, + errFunc: require.Error, + shouldCreate: false, + }, + { + name: "Bad Peers Group already has this domains route", + inputArgs: input{ + domains: existingDomains, + networkType: route.DomainNetwork, netID: "bad", peerGroupIDs: []string{routeGroup1, routeGroup3}, description: "super", @@ -191,7 +269,8 @@ func TestCreateRoute(t *testing.T) { { name: "Empty Peer Should Create", inputArgs: input{ - network: "192.168.0.0/16", + network: netip.MustParsePrefix("192.168.0.0/16"), + networkType: route.IPv4Network, netID: "happy", peerKey: "", description: "super", @@ -217,7 +296,8 @@ func TestCreateRoute(t *testing.T) { { name: "Large Metric Should Fail", inputArgs: input{ - network: "192.168.0.0/16", + network: netip.MustParsePrefix("192.168.0.0/16"), + networkType: route.IPv4Network, peerKey: peer1ID, netID: "happy", description: "super", @@ -232,7 +312,8 @@ func TestCreateRoute(t *testing.T) { { name: "Small Metric Should Fail", inputArgs: input{ - network: "192.168.0.0/16", + network: netip.MustParsePrefix("192.168.0.0/16"), + networkType: route.IPv4Network, netID: "happy", peerKey: peer1ID, description: "super", @@ -247,7 +328,8 @@ func TestCreateRoute(t *testing.T) { { name: "Large NetID Should Fail", inputArgs: input{ - network: "192.168.0.0/16", + network: netip.MustParsePrefix("192.168.0.0/16"), + networkType: route.IPv4Network, peerKey: peer1ID, netID: "12345678901234567890qwertyuiopqwertyuiop1", description: "super", @@ -262,7 +344,8 @@ func TestCreateRoute(t *testing.T) { { name: "Small NetID Should Fail", inputArgs: input{ - network: "192.168.0.0/16", + network: netip.MustParsePrefix("192.168.0.0/16"), + networkType: route.IPv4Network, netID: "", peerKey: peer1ID, description: "", @@ -277,7 +360,8 @@ func TestCreateRoute(t *testing.T) { { name: "Empty Group List Should Fail", inputArgs: input{ - network: "192.168.0.0/16", + network: netip.MustParsePrefix("192.168.0.0/16"), + networkType: route.IPv4Network, netID: "NewId", peerKey: peer1ID, description: "", @@ -292,7 +376,8 @@ func TestCreateRoute(t *testing.T) { { name: "Empty Group ID string Should Fail", inputArgs: input{ - network: "192.168.0.0/16", + network: netip.MustParsePrefix("192.168.0.0/16"), + networkType: route.IPv4Network, netID: "NewId", peerKey: peer1ID, description: "", @@ -307,7 +392,8 @@ func TestCreateRoute(t *testing.T) { { name: "Invalid Group Should Fail", inputArgs: input{ - network: "192.168.0.0/16", + network: netip.MustParsePrefix("192.168.0.0/16"), + networkType: route.IPv4Network, netID: "NewId", peerKey: peer1ID, description: "", @@ -334,29 +420,14 @@ func TestCreateRoute(t *testing.T) { if testCase.createInitRoute { groupAll, errInit := account.GetGroupAll() - if errInit != nil { - t.Errorf("failed to get group all: %s", errInit) - } - _, errInit = am.CreateRoute(account.Id, existingNetwork, "", []string{routeGroup3, routeGroup4}, - "", existingRouteID, false, 1000, []string{groupAll.ID}, true, userID) - if errInit != nil { - t.Errorf("failed to create init route: %s", errInit) - } + require.NoError(t, errInit) + _, errInit = am.CreateRoute(account.Id, existingNetwork, 1, nil, "", []string{routeGroup3, routeGroup4}, "", existingRouteID, false, 1000, []string{groupAll.ID}, true, userID, false) + require.NoError(t, errInit) + _, errInit = am.CreateRoute(account.Id, netip.Prefix{}, 3, existingDomains, "", []string{routeGroup3, routeGroup4}, "", existingRouteID, false, 1000, []string{groupAll.ID}, true, userID, false) + require.NoError(t, errInit) } - outRoute, err := am.CreateRoute( - account.Id, - testCase.inputArgs.network, - testCase.inputArgs.peerKey, - testCase.inputArgs.peerGroupIDs, - testCase.inputArgs.description, - testCase.inputArgs.netID, - testCase.inputArgs.masquerade, - testCase.inputArgs.metric, - testCase.inputArgs.groups, - testCase.inputArgs.enabled, - userID, - ) + outRoute, err := am.CreateRoute(account.Id, testCase.inputArgs.network, testCase.inputArgs.networkType, testCase.inputArgs.domains, testCase.inputArgs.peerKey, testCase.inputArgs.peerGroupIDs, testCase.inputArgs.description, testCase.inputArgs.netID, testCase.inputArgs.masquerade, testCase.inputArgs.metric, testCase.inputArgs.groups, testCase.inputArgs.enabled, userID, testCase.inputArgs.keepRoute) testCase.errFunc(t, err) @@ -379,8 +450,13 @@ func TestSaveRoute(t *testing.T) { validUsedPeer := peer5ID invalidPeer := "nonExisting" validPrefix := netip.MustParsePrefix("192.168.0.0/24") + placeholderPrefix := netip.MustParsePrefix("192.0.2.0/32") invalidPrefix, _ := netip.ParsePrefix("192.168.0.0/34") validMetric := 1000 + trueKeepRoute := true + falseKeepRoute := false + ipv4networkType := route.IPv4Network + domainNetworkType := route.DomainNetwork invalidMetric := 99999 validNetID := route.NetID("12345678901234567890qw") invalidNetID := route.NetID("12345678901234567890qwertyuiopqwertyuiop1") @@ -395,6 +471,9 @@ func TestSaveRoute(t *testing.T) { newPeerGroups []string newMetric *int newPrefix *netip.Prefix + newDomains domain.List + newNetworkType *route.NetworkType + newKeepRoute *bool newGroups []string skipCopying bool shouldCreate bool @@ -402,7 +481,7 @@ func TestSaveRoute(t *testing.T) { expectedRoute *route.Route }{ { - name: "Happy Path", + name: "Happy Path Network", existingRoute: &route.Route{ ID: "testingRoute", Network: netip.MustParsePrefix("192.168.0.0/16"), @@ -434,6 +513,45 @@ func TestSaveRoute(t *testing.T) { Groups: []string{routeGroup2}, }, }, + { + name: "Happy Path Domains", + existingRoute: &route.Route{ + ID: "testingRoute", + Network: netip.Prefix{}, + Domains: domain.List{"example.com"}, + KeepRoute: false, + NetID: validNetID, + NetworkType: route.DomainNetwork, + Peer: peer1ID, + Description: "super", + Masquerade: false, + Metric: 9999, + Enabled: true, + Groups: []string{routeGroup1}, + }, + newPeer: &validPeer, + newMetric: &validMetric, + newPrefix: &netip.Prefix{}, + newDomains: domain.List{"example.com", "example2.com"}, + newKeepRoute: &trueKeepRoute, + newGroups: []string{routeGroup1}, + errFunc: require.NoError, + shouldCreate: true, + expectedRoute: &route.Route{ + ID: "testingRoute", + Network: placeholderPrefix, + Domains: domain.List{"example.com", "example2.com"}, + KeepRoute: true, + NetID: validNetID, + NetworkType: route.DomainNetwork, + Peer: validPeer, + Description: "super", + Masquerade: false, + Metric: validMetric, + Enabled: true, + Groups: []string{routeGroup1}, + }, + }, { name: "Happy Path Peer Groups", existingRoute: &route.Route{ @@ -466,6 +584,23 @@ func TestSaveRoute(t *testing.T) { Groups: []string{routeGroup2}, }, }, + { + name: "Both network and domains provided should fail", + existingRoute: &route.Route{ + ID: "testingRoute", + Network: netip.MustParsePrefix("192.168.0.0/16"), + NetID: validNetID, + NetworkType: route.IPv4Network, + Description: "super", + Masquerade: false, + Metric: 9999, + Enabled: true, + Groups: []string{routeGroup1}, + }, + newPrefix: &validPrefix, + newDomains: domain.List{"example.com"}, + errFunc: require.Error, + }, { name: "Both peer and peers_roup Provided Should Fail", existingRoute: &route.Route{ @@ -623,7 +758,7 @@ func TestSaveRoute(t *testing.T) { name: "Allow to modify existing route with new peer", existingRoute: &route.Route{ ID: "testingRoute", - Network: netip.MustParsePrefix(existingNetwork), + Network: existingNetwork, NetID: validNetID, NetworkType: route.IPv4Network, Peer: peer1ID, @@ -638,7 +773,7 @@ func TestSaveRoute(t *testing.T) { shouldCreate: true, expectedRoute: &route.Route{ ID: "testingRoute", - Network: netip.MustParsePrefix(existingNetwork), + Network: existingNetwork, NetID: validNetID, NetworkType: route.IPv4Network, Peer: validPeer, @@ -654,7 +789,7 @@ func TestSaveRoute(t *testing.T) { name: "Do not allow to modify existing route with a peer from another route", existingRoute: &route.Route{ ID: "testingRoute", - Network: netip.MustParsePrefix(existingNetwork), + Network: existingNetwork, NetID: validNetID, NetworkType: route.IPv4Network, Peer: peer1ID, @@ -672,7 +807,7 @@ func TestSaveRoute(t *testing.T) { name: "Do not allow to modify existing route with a peers group from another route", existingRoute: &route.Route{ ID: "testingRoute", - Network: netip.MustParsePrefix(existingNetwork), + Network: existingNetwork, NetID: validNetID, NetworkType: route.IPv4Network, PeerGroups: []string{routeGroup3}, @@ -686,6 +821,80 @@ func TestSaveRoute(t *testing.T) { newPeerGroups: []string{routeGroup4}, errFunc: require.Error, }, + { + name: "Allow switching from network route to domains route", + existingRoute: &route.Route{ + ID: "testingRoute", + Network: validPrefix, + Domains: nil, + KeepRoute: false, + NetID: validNetID, + NetworkType: route.IPv4Network, + Peer: peer1ID, + Description: "super", + Masquerade: false, + Metric: 9999, + Enabled: true, + Groups: []string{routeGroup1}, + }, + newPrefix: &netip.Prefix{}, + newDomains: domain.List{"example.com"}, + newNetworkType: &domainNetworkType, + newKeepRoute: &trueKeepRoute, + errFunc: require.NoError, + shouldCreate: true, + expectedRoute: &route.Route{ + ID: "testingRoute", + Network: placeholderPrefix, + NetworkType: route.DomainNetwork, + Domains: domain.List{"example.com"}, + KeepRoute: true, + NetID: validNetID, + Peer: peer1ID, + Description: "super", + Masquerade: false, + Metric: 9999, + Enabled: true, + Groups: []string{routeGroup1}, + }, + }, + { + name: "Allow switching from domains route to network route", + existingRoute: &route.Route{ + ID: "testingRoute", + Network: placeholderPrefix, + Domains: domain.List{"example.com"}, + KeepRoute: true, + NetID: validNetID, + NetworkType: route.DomainNetwork, + Peer: peer1ID, + Description: "super", + Masquerade: false, + Metric: 9999, + Enabled: true, + Groups: []string{routeGroup1}, + }, + newPrefix: &validPrefix, + newDomains: nil, + newKeepRoute: &falseKeepRoute, + newNetworkType: &ipv4networkType, + errFunc: require.NoError, + shouldCreate: true, + expectedRoute: &route.Route{ + ID: "testingRoute", + Network: validPrefix, + NetworkType: route.IPv4Network, + KeepRoute: false, + Domains: nil, + NetID: validNetID, + Peer: peer1ID, + Description: "super", + Masquerade: false, + Metric: 9999, + Enabled: true, + Groups: []string{routeGroup1}, + }, + }, } for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { @@ -702,7 +911,7 @@ func TestSaveRoute(t *testing.T) { if testCase.createInitRoute { account.Routes["initRoute"] = &route.Route{ ID: "initRoute", - Network: netip.MustParsePrefix(existingNetwork), + Network: existingNetwork, NetID: existingRouteID, NetworkType: route.IPv4Network, PeerGroups: []string{routeGroup4}, @@ -739,6 +948,16 @@ func TestSaveRoute(t *testing.T) { routeToSave.Network = *testCase.newPrefix } + routeToSave.Domains = testCase.newDomains + + if testCase.newNetworkType != nil { + routeToSave.NetworkType = *testCase.newNetworkType + } + + if testCase.newKeepRoute != nil { + routeToSave.KeepRoute = *testCase.newKeepRoute + } + if testCase.newGroups != nil { routeToSave.Groups = testCase.newGroups } @@ -771,6 +990,8 @@ func TestDeleteRoute(t *testing.T) { testingRoute := &route.Route{ ID: "testingRoute", Network: netip.MustParsePrefix("192.168.0.0/16"), + Domains: domain.List{"domain1", "domain2"}, + KeepRoute: true, NetworkType: route.IPv4Network, Peer: peer1Key, Description: "super", @@ -839,9 +1060,7 @@ func TestGetNetworkMap_RouteSyncPeerGroups(t *testing.T) { require.NoError(t, err) require.Len(t, newAccountRoutes.Routes, 0, "new accounts should have no routes") - newRoute, err := am.CreateRoute( - account.Id, baseRoute.Network.String(), baseRoute.Peer, baseRoute.PeerGroups, baseRoute.Description, - baseRoute.NetID, baseRoute.Masquerade, baseRoute.Metric, baseRoute.Groups, baseRoute.Enabled, userID) + newRoute, err := am.CreateRoute(account.Id, baseRoute.Network, baseRoute.NetworkType, baseRoute.Domains, baseRoute.Peer, baseRoute.PeerGroups, baseRoute.Description, baseRoute.NetID, baseRoute.Masquerade, baseRoute.Metric, baseRoute.Groups, baseRoute.Enabled, userID, baseRoute.KeepRoute) require.NoError(t, err) require.Equal(t, newRoute.Enabled, true) @@ -932,9 +1151,7 @@ func TestGetNetworkMap_RouteSync(t *testing.T) { require.NoError(t, err) require.Len(t, newAccountRoutes.Routes, 0, "new accounts should have no routes") - createdRoute, err := am.CreateRoute(account.Id, baseRoute.Network.String(), peer1ID, []string{}, - baseRoute.Description, baseRoute.NetID, baseRoute.Masquerade, baseRoute.Metric, baseRoute.Groups, false, - userID) + createdRoute, err := am.CreateRoute(account.Id, baseRoute.Network, baseRoute.NetworkType, baseRoute.Domains, peer1ID, []string{}, baseRoute.Description, baseRoute.NetID, baseRoute.Masquerade, baseRoute.Metric, baseRoute.Groups, false, userID, baseRoute.KeepRoute) require.NoError(t, err) noDisabledRoutes, err := am.GetNetworkMap(peer1ID) diff --git a/route/hauniqueid.go b/route/hauniqueid.go index 6f74563e2..4d952beba 100644 --- a/route/hauniqueid.go +++ b/route/hauniqueid.go @@ -2,12 +2,9 @@ package route import "strings" -type HAUniqueID string +const haSeparator = "|" -// GetHAUniqueID returns a highly available route ID by combining Network ID and Network range address -func GetHAUniqueID(input *Route) HAUniqueID { - return HAUniqueID(string(input.NetID) + "-" + input.Network.String()) -} +type HAUniqueID string func (id HAUniqueID) String() string { return string(id) @@ -15,7 +12,7 @@ func (id HAUniqueID) String() string { // NetID returns the Network ID from the HAUniqueID func (id HAUniqueID) NetID() NetID { - if i := strings.LastIndex(string(id), "-"); i != -1 { + if i := strings.LastIndex(string(id), haSeparator); i != -1 { return NetID(id[:i]) } return NetID(id) diff --git a/route/route.go b/route/route.go index 50c53cbe6..eb6c36bd8 100644 --- a/route/route.go +++ b/route/route.go @@ -1,8 +1,13 @@ package route import ( + "fmt" "net/netip" + "slices" + log "github.com/sirupsen/logrus" + + "github.com/netbirdio/netbird/management/domain" "github.com/netbirdio/netbird/management/server/status" ) @@ -25,6 +30,8 @@ const ( IPv4NetworkString = "IPv4" // IPv6NetworkString IPv6 network type string IPv6NetworkString = "IPv6" + // DomainNetworkString domain network type string + DomainNetworkString = "Domain" ) const ( @@ -34,6 +41,8 @@ const ( IPv4Network // IPv6Network IPv6 network type IPv6Network + // DomainNetwork domain network type + DomainNetwork ) type ID string @@ -52,6 +61,8 @@ func (p NetworkType) String() string { return IPv4NetworkString case IPv6Network: return IPv6NetworkString + case DomainNetwork: + return DomainNetworkString default: return InvalidNetworkString } @@ -64,6 +75,8 @@ func ToPrefixType(prefix string) NetworkType { return IPv4Network case IPv6NetworkString: return IPv6Network + case DomainNetworkString: + return DomainNetwork default: return InvalidNetwork } @@ -73,8 +86,11 @@ func ToPrefixType(prefix string) NetworkType { type Route struct { ID ID `gorm:"primaryKey"` // AccountID is a reference to Account that this object belongs - AccountID string `gorm:"index"` + AccountID string `gorm:"index"` + // Network and Domains are mutually exclusive Network netip.Prefix `gorm:"serializer:json"` + Domains domain.List `gorm:"serializer:json"` + KeepRoute bool NetID NetID Description string Peer string @@ -88,7 +104,7 @@ type Route struct { // EventMeta returns activity event meta related to the route func (r *Route) EventMeta() map[string]any { - return map[string]any{"name": r.NetID, "network_range": r.Network.String(), "peer_id": r.Peer, "peer_groups": r.PeerGroups} + return map[string]any{"name": r.NetID, "network_range": r.Network.String(), "domains": r.Domains.SafeString(), "peer_id": r.Peer, "peer_groups": r.PeerGroups} } // Copy copies a route object @@ -98,16 +114,16 @@ func (r *Route) Copy() *Route { Description: r.Description, NetID: r.NetID, Network: r.Network, + Domains: slices.Clone(r.Domains), + KeepRoute: r.KeepRoute, NetworkType: r.NetworkType, Peer: r.Peer, - PeerGroups: make([]string, len(r.PeerGroups)), + PeerGroups: slices.Clone(r.PeerGroups), Metric: r.Metric, Masquerade: r.Masquerade, Enabled: r.Enabled, - Groups: make([]string, len(r.Groups)), + Groups: slices.Clone(r.Groups), } - copy(route.Groups, r.Groups) - copy(route.PeerGroups, r.PeerGroups) return route } @@ -123,13 +139,32 @@ func (r *Route) IsEqual(other *Route) bool { other.Description == r.Description && other.NetID == r.NetID && other.Network == r.Network && + slices.Equal(r.Domains, other.Domains) && + other.KeepRoute == r.KeepRoute && other.NetworkType == r.NetworkType && other.Peer == r.Peer && other.Metric == r.Metric && other.Masquerade == r.Masquerade && other.Enabled == r.Enabled && - compareList(r.Groups, other.Groups) && - compareList(r.PeerGroups, other.PeerGroups) + slices.Equal(r.Groups, other.Groups) && + slices.Equal(r.PeerGroups, other.PeerGroups) +} + +// IsDynamic returns if the route is dynamic, i.e. has domains +func (r *Route) IsDynamic() bool { + return r.NetworkType == DomainNetwork +} + +func (r *Route) GetHAUniqueID() HAUniqueID { + if r.IsDynamic() { + domains, err := r.Domains.String() + if err != nil { + log.Errorf("Failed to convert domains to string: %v", err) + domains = r.Domains.PunycodeString() + } + return HAUniqueID(fmt.Sprintf("%s%s%s", r.NetID, haSeparator, domains)) + } + return HAUniqueID(fmt.Sprintf("%s%s%s", r.NetID, haSeparator, r.Network.String())) } // ParseNetwork Parses a network prefix string and returns a netip.Prefix object and if is invalid, IPv4 or IPv6 @@ -151,23 +186,3 @@ func ParseNetwork(networkString string) (NetworkType, netip.Prefix, error) { return IPv4Network, masked, nil } - -func compareList(list, other []string) bool { - if len(list) != len(other) { - return false - } - for _, id := range list { - match := false - for _, otherID := range other { - if id == otherID { - match = true - break - } - } - if !match { - return false - } - } - - return true -} diff --git a/util/membership_unix.go b/util/membership_unix.go index 82237461c..a9e55af84 100644 --- a/util/membership_unix.go +++ b/util/membership_unix.go @@ -1,4 +1,4 @@ -//go:build linux || darwin +//go:build linux || darwin || freebsd package util diff --git a/version/url_freebsd.go b/version/url_freebsd.go new file mode 100644 index 000000000..c8193e30c --- /dev/null +++ b/version/url_freebsd.go @@ -0,0 +1,6 @@ +package version + +// DownloadUrl return with the proper download link +func DownloadUrl() string { + return downloadURL +}