From 4fec709bb1bf015289af829cb0f291365c0c1bce Mon Sep 17 00:00:00 2001 From: Maycon Santos Date: Thu, 13 Jun 2024 13:24:24 +0200 Subject: [PATCH] Release 0.28.0 (#2092) * compile client under freebsd (#1620) Compile netbird client under freebsd and now support netstack and userspace modes. Refactoring linux specific code to share same code with FreeBSD, move to *_unix.go files. Not implemented yet: Kernel mode not supported DNS probably does not work yet Routing also probably does not work yet SSH support did not tested yet Lack of test environment for freebsd (dedicated VM for github runners under FreeBSD required) Lack of tests for freebsd specific code info reporting need to review and also implement, for example OS reported as GENERIC instead of FreeBSD (lack of FreeBSD icon in management interface) Lack of proper client setup under FreeBSD Lack of FreeBSD port/package * Add DNS routes (#1943) Given domains are resolved periodically and resolved IPs are replaced with the new ones. Unless the flag keep_route is set to true, then only new ones are added. This option is helpful if there are long-running connections that might still point to old IP addresses from changed DNS records. * Add process posture check (#1693) Introduces a process posture check to validate the existence and active status of specific binaries on peer systems. The check ensures that files are present at specified paths, and that corresponding processes are running. This check supports Linux, Windows, and macOS systems. Co-authored-by: Evgenii Co-authored-by: Pascal Fischer Co-authored-by: Zoltan Papp Co-authored-by: Viktor Liu <17948409+lixmal@users.noreply.github.com> Co-authored-by: Bethuel Mmbaga --- .github/workflows/golang-test-linux.yml | 8 +- client/cmd/root.go | 5 +- client/cmd/route.go | 59 +- client/cmd/status.go | 25 +- client/cmd/up.go | 11 + client/errors/errors.go | 30 + client/internal/config.go | 18 + client/internal/connect.go | 5 +- client/internal/dns/consts_freebsd.go | 6 + client/internal/dns/consts_linux.go | 8 + .../dns/{dbus_linux.go => dbus_unix.go} | 2 +- ...le_parser_linux.go => file_parser_unix.go} | 2 +- ...linux_test.go => file_parser_unix_test.go} | 2 +- ...le_repair_linux.go => file_repair_unix.go} | 2 +- ...linux_test.go => file_repair_unix_test.go} | 2 +- .../dns/{file_linux.go => file_unix.go} | 2 +- .../{file_linux_test.go => file_unix_test.go} | 2 +- .../dns/{host_linux.go => host_unix.go} | 16 +- ...nager_linux.go => network_manager_unix.go} | 2 +- ...resolvconf_linux.go => resolvconf_unix.go} | 2 +- client/internal/dns/server_test.go | 4 + .../dns/{server_linux.go => server_unix.go} | 2 +- client/internal/dns/systemd_freebsd.go | 20 + client/internal/dns/systemd_linux.go | 22 + ...down_linux.go => unclean_shutdown_unix.go} | 7 +- client/internal/dns/wgiface.go | 7 +- client/internal/engine.go | 72 +- client/internal/engine_test.go | 16 +- client/internal/networkmonitor/monitor_bsd.go | 14 +- .../networkmonitor/monitor_generic.go | 16 +- .../internal/networkmonitor/monitor_linux.go | 12 +- .../networkmonitor/monitor_windows.go | 48 +- client/internal/peer/status.go | 73 +- client/internal/routemanager/client.go | 187 +-- client/internal/routemanager/client_test.go | 7 +- client/internal/routemanager/dynamic/route.go | 361 +++++ client/internal/routemanager/manager.go | 127 +- client/internal/routemanager/manager_test.go | 2 +- .../routemanager/refcounter/refcounter.go | 155 ++ .../internal/routemanager/refcounter/types.go | 7 + client/internal/routemanager/routemanager.go | 127 -- .../routemanager/server_nonandroid.go | 33 +- client/internal/routemanager/static/route.go | 57 + .../routemanager/sysctl/sysctl_linux.go | 103 ++ .../routemanager/systemops/routeflags_bsd.go | 18 + .../systemops/routeflags_freebsd.go | 19 + .../routemanager/systemops/systemops.go | 27 + .../{ => systemops}/systemops_bsd.go | 7 +- .../{ => systemops}/systemops_bsd_test.go | 2 +- .../{ => systemops}/systemops_darwin_test.go | 8 +- .../systemops_generic.go} | 586 ++++---- .../systemops_generic_test.go} | 116 +- .../{ => systemops}/systemops_linux.go | 176 +-- .../{ => systemops}/systemops_linux_test.go | 8 +- .../systemops/systemops_mobile.go | 34 + .../systemops/systemops_nonlinux.go | 24 + .../systemops_unix.go} | 42 +- .../{ => systemops}/systemops_unix_test.go | 2 +- .../{ => systemops}/systemops_windows.go | 87 +- .../{ => systemops}/systemops_windows_test.go | 8 +- .../routemanager/systemops_android.go | 33 - client/internal/routemanager/systemops_ios.go | 33 - .../routemanager/systemops_nonlinux.go | 24 - client/internal/routemanager/util/ip.go | 29 + client/internal/routemanager/vars/vars.go | 16 + .../internal/routeselector/routeselector.go | 37 +- .../routeselector/routeselector_test.go | 10 +- client/proto/daemon.pb.go | 808 +++++----- client/proto/daemon.proto | 9 + client/server/route.go | 31 +- client/server/server.go | 6 + client/ssh/window_freebsd.go | 10 + client/system/info.go | 26 + client/system/info_android.go | 5 + client/system/info_freebsd.go | 31 +- client/system/info_ios.go | 5 + client/system/info_linux.go | 37 +- client/system/osrelease_unix.go | 38 + client/system/process.go | 58 + client/ui/client_ui.go | 2 +- client/ui/route.go | 31 +- go.mod | 2 +- iface/address.go | 18 - iface/freebsd/errors.go | 8 + iface/freebsd/iface.go | 108 ++ iface/freebsd/iface_internal_test.go | 76 + iface/freebsd/link.go | 239 +++ iface/iface.go | 17 +- iface/iface_create.go | 1 - iface/iface_darwin.go | 1 - iface/iface_ios.go | 1 - iface/{iface_linux.go => iface_unix.go} | 6 +- iface/module.go | 3 +- iface/module_freebsd.go | 18 + iface/name.go | 3 +- iface/name_darwin.go | 1 - ...tun_kernel_linux.go => tun_kernel_unix.go} | 86 +- iface/tun_link_freebsd.go | 80 + iface/tun_link_linux.go | 102 +- iface/{tun_usp_linux.go => tun_usp_unix.go} | 39 +- iface/wg_configurer.go | 3 + ...kernel.go => wg_configurer_kernel_unix.go} | 19 +- iface/wg_configurer_usp.go | 6 +- management/client/client.go | 5 +- management/client/client_test.go | 2 +- management/client/grpc.go | 135 +- management/client/mock.go | 16 +- management/domain/domain.go | 34 + management/domain/list.go | 83 ++ management/proto/management.pb.go | 1300 ++++++++++------- management/proto/management.proto | 39 +- management/proto/management_grpc.pb.go | 44 + management/server/account.go | 44 +- management/server/account_test.go | 2 +- management/server/grpcserver.go | 220 ++- management/server/http/api/openapi.yml | 49 +- management/server/http/api/types.gen.go | 42 +- .../server/http/geolocations_handler.go | 9 +- management/server/http/pat_handler_test.go | 6 +- .../server/http/posture_checks_handler.go | 89 +- .../http/posture_checks_handler_test.go | 160 +- management/server/http/routes_handler.go | 189 ++- management/server/http/routes_handler_test.go | 223 ++- management/server/http/users_handler_test.go | 4 +- management/server/management_proto_test.go | 3 +- management/server/management_test.go | 12 +- management/server/mock_server/account_mock.go | 32 +- .../mock_server/management_server_mock.go | 11 +- management/server/peer.go | 19 +- management/server/peer/peer.go | 54 +- management/server/posture/checks.go | 111 +- management/server/posture/checks_test.go | 22 + management/server/posture/geo_location.go | 26 + .../server/posture/geo_location_test.go | 78 + management/server/posture/nb_version.go | 12 + management/server/posture/nb_version_test.go | 30 + management/server/posture/network.go | 17 + management/server/posture/network_test.go | 49 + management/server/posture/os_version.go | 33 +- management/server/posture/os_version_test.go | 76 + management/server/posture/process.go | 79 + management/server/posture/process_test.go | 318 ++++ management/server/posture_checks.go | 89 +- management/server/route.go | 60 +- management/server/route_test.go | 337 ++++- route/hauniqueid.go | 9 +- route/route.go | 71 +- util/membership_unix.go | 2 +- version/url_freebsd.go | 6 + 149 files changed, 6509 insertions(+), 2710 deletions(-) create mode 100644 client/errors/errors.go create mode 100644 client/internal/dns/consts_freebsd.go create mode 100644 client/internal/dns/consts_linux.go rename client/internal/dns/{dbus_linux.go => dbus_unix.go} (96%) rename client/internal/dns/{file_parser_linux.go => file_parser_unix.go} (99%) rename client/internal/dns/{file_parser_linux_test.go => file_parser_unix_test.go} (99%) rename client/internal/dns/{file_repair_linux.go => file_repair_unix.go} (98%) rename client/internal/dns/{file_repair_linux_test.go => file_repair_unix_test.go} (98%) rename client/internal/dns/{file_linux.go => file_unix.go} (99%) rename client/internal/dns/{file_linux_test.go => file_unix_test.go} (98%) rename client/internal/dns/{host_linux.go => host_unix.go} (85%) rename client/internal/dns/{network_manager_linux.go => network_manager_unix.go} (99%) rename client/internal/dns/{resolvconf_linux.go => resolvconf_unix.go} (98%) rename client/internal/dns/{server_linux.go => server_unix.go} (76%) create mode 100644 client/internal/dns/systemd_freebsd.go rename client/internal/dns/{unclean_shutdown_linux.go => unclean_shutdown_unix.go} (94%) create mode 100644 client/internal/routemanager/dynamic/route.go create mode 100644 client/internal/routemanager/refcounter/refcounter.go create mode 100644 client/internal/routemanager/refcounter/types.go delete mode 100644 client/internal/routemanager/routemanager.go create mode 100644 client/internal/routemanager/static/route.go create mode 100644 client/internal/routemanager/sysctl/sysctl_linux.go create mode 100644 client/internal/routemanager/systemops/routeflags_bsd.go create mode 100644 client/internal/routemanager/systemops/routeflags_freebsd.go create mode 100644 client/internal/routemanager/systemops/systemops.go rename client/internal/routemanager/{ => systemops}/systemops_bsd.go (95%) rename client/internal/routemanager/{ => systemops}/systemops_bsd_test.go (98%) rename client/internal/routemanager/{ => systemops}/systemops_darwin_test.go (94%) rename client/internal/routemanager/{systemops.go => systemops/systemops_generic.go} (56%) rename client/internal/routemanager/{systemops_test.go => systemops/systemops_generic_test.go} (76%) rename client/internal/routemanager/{ => systemops}/systemops_linux.go (72%) rename client/internal/routemanager/{ => systemops}/systemops_linux_test.go (96%) create mode 100644 client/internal/routemanager/systemops/systemops_mobile.go create mode 100644 client/internal/routemanager/systemops/systemops_nonlinux.go rename client/internal/routemanager/{systemops_darwin.go => systemops/systemops_unix.go} (58%) rename client/internal/routemanager/{ => systemops}/systemops_unix_test.go (99%) rename client/internal/routemanager/{ => systemops}/systemops_windows.go (78%) rename client/internal/routemanager/{ => systemops}/systemops_windows_test.go (97%) delete mode 100644 client/internal/routemanager/systemops_android.go delete mode 100644 client/internal/routemanager/systemops_ios.go delete mode 100644 client/internal/routemanager/systemops_nonlinux.go create mode 100644 client/internal/routemanager/util/ip.go create mode 100644 client/internal/routemanager/vars/vars.go create mode 100644 client/ssh/window_freebsd.go create mode 100644 client/system/osrelease_unix.go create mode 100644 client/system/process.go create mode 100644 iface/freebsd/errors.go create mode 100644 iface/freebsd/iface.go create mode 100644 iface/freebsd/iface_internal_test.go create mode 100644 iface/freebsd/link.go rename iface/{iface_linux.go => iface_unix.go} (89%) create mode 100644 iface/module_freebsd.go rename iface/{tun_kernel_linux.go => tun_kernel_unix.go} (64%) create mode 100644 iface/tun_link_freebsd.go rename iface/{tun_usp_linux.go => tun_usp_unix.go} (78%) rename iface/{wg_configurer_kernel.go => wg_configurer_kernel_unix.go} (90%) create mode 100644 management/domain/domain.go create mode 100644 management/domain/list.go create mode 100644 management/server/posture/process.go create mode 100644 management/server/posture/process_test.go create mode 100644 version/url_freebsd.go 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 +}