From ee79f6cd0c8ab22cdd9d739710017d813c8b5f81 Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Tue, 4 Jun 2024 13:41:35 +0200 Subject: [PATCH] Add DNS routes (#1943) --- .github/workflows/golang-test-linux.yml | 8 +- client/cmd/root.go | 5 +- client/cmd/route.go | 55 +- client/cmd/status.go | 25 +- client/cmd/up.go | 11 + client/errors/errors.go | 30 + client/internal/config.go | 18 + client/internal/connect.go | 1 + client/internal/dns/server_test.go | 4 + client/internal/dns/wgiface.go | 7 +- client/internal/engine.go | 16 +- client/internal/engine_test.go | 2 +- 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 | 174 ++-- client/internal/routemanager/client_test.go | 7 +- client/internal/routemanager/dynamic/route.go | 362 ++++++++ client/internal/routemanager/manager.go | 123 ++- 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 | 47 + .../{ => systemops}/routeflags_bsd.go | 2 +- .../{ => systemops}/routeflags_freebsd.go | 2 +- .../routemanager/{ => systemops}/systemops.go | 208 ++--- .../{ => systemops}/systemops_android.go | 12 +- .../{ => systemops}/systemops_bsd.go | 4 +- .../{ => systemops}/systemops_bsd_test.go | 2 +- .../{ => systemops}/systemops_darwin_test.go | 6 +- .../{ => systemops}/systemops_ios.go | 12 +- .../{ => systemops}/systemops_linux.go | 73 +- .../{ => systemops}/systemops_linux_test.go | 8 +- .../{ => systemops}/systemops_nonlinux.go | 8 +- .../{ => systemops}/systemops_test.go | 76 +- .../{ => systemops}/systemops_unix.go | 30 +- .../{ => systemops}/systemops_unix_test.go | 2 +- .../{ => systemops}/systemops_windows.go | 44 +- .../{ => systemops}/systemops_windows_test.go | 8 +- 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 | 806 ++++++++++-------- client/proto/daemon.proto | 9 + client/server/route.go | 31 +- client/server/server.go | 6 + client/ui/route.go | 31 +- iface/address.go | 18 - iface/iface.go | 17 +- iface/wg_configurer.go | 3 + iface/wg_configurer_kernel_unix.go | 17 +- iface/wg_configurer_usp.go | 6 +- management/domain/domain.go | 34 + management/domain/list.go | 83 ++ management/proto/management.pb.go | 239 +++--- management/proto/management.proto | 2 + management/server/account.go | 15 +- management/server/account_test.go | 2 +- management/server/http/api/openapi.yml | 21 +- management/server/http/api/types.gen.go | 22 +- management/server/http/pat_handler_test.go | 6 +- 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/mock_server/account_mock.go | 8 +- management/server/route.go | 60 +- management/server/route_test.go | 335 ++++++-- route/hauniqueid.go | 9 +- route/route.go | 71 +- 74 files changed, 2911 insertions(+), 1327 deletions(-) create mode 100644 client/errors/errors.go 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 rename client/internal/routemanager/{ => systemops}/routeflags_bsd.go (95%) rename client/internal/routemanager/{ => systemops}/routeflags_freebsd.go (96%) rename client/internal/routemanager/{ => systemops}/systemops.go (62%) rename client/internal/routemanager/{ => systemops}/systemops_android.go (58%) rename client/internal/routemanager/{ => systemops}/systemops_bsd.go (96%) 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}/systemops_ios.go (58%) rename client/internal/routemanager/{ => systemops}/systemops_linux.go (88%) rename client/internal/routemanager/{ => systemops}/systemops_linux_test.go (96%) rename client/internal/routemanager/{ => systemops}/systemops_nonlinux.go (61%) rename client/internal/routemanager/{ => systemops}/systemops_test.go (82%) rename client/internal/routemanager/{ => systemops}/systemops_unix.go (64%) rename client/internal/routemanager/{ => systemops}/systemops_unix_test.go (99%) rename client/internal/routemanager/{ => systemops}/systemops_windows.go (80%) rename client/internal/routemanager/{ => systemops}/systemops_windows_test.go (97%) create mode 100644 client/internal/routemanager/util/ip.go create mode 100644 client/internal/routemanager/vars/vars.go create mode 100644 management/domain/domain.go create mode 100644 management/domain/list.go diff --git a/.github/workflows/golang-test-linux.yml b/.github/workflows/golang-test-linux.yml index 4259f1b3e12..8966904c4b6 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 1eca27d8c3e..d49609d5de5 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 95cedb8baa2..533f10d6bf5 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,16 +67,58 @@ func routesList(cmd *cobra.Command, _ []string) error { return nil } + printRoutes(cmd, resp) + + return nil +} + +func printRoutes(cmd *cobra.Command, resp *proto.ListRoutesResponse) { 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) + printRoute(cmd, route) } +} - return nil +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 { diff --git a/client/cmd/status.go b/client/cmd/status.go index 3dacfbe4ff0..e6c7b8be8ec 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 a5bbc58bee3..2156358643f 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 00000000000..cef999ac872 --- /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 66721cd21f8..0b55d5ccbfd 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 d34d0aab006..0eaba3112cd 100644 --- a/client/internal/connect.go +++ b/client/internal/connect.go @@ -321,6 +321,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/server_test.go b/client/internal/dns/server_test.go index 22966d89ca2..3709c32ce48 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/wgiface.go b/client/internal/dns/wgiface.go index 2c34f1c47d9..2f08e8d52b8 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 669fdaaff29..67e1fba2c7e 100644 --- a/client/internal/engine.go +++ b/client/internal/engine.go @@ -33,6 +33,7 @@ import ( "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" @@ -88,6 +89,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. @@ -297,7 +300,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) @@ -766,15 +769,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) } diff --git a/client/internal/engine_test.go b/client/internal/engine_test.go index f5a98cb7fff..bd132758009 100644 --- a/client/internal/engine_test.go +++ b/client/internal/engine_test.go @@ -220,7 +220,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 }, } diff --git a/client/internal/networkmonitor/monitor_bsd.go b/client/internal/networkmonitor/monitor_bsd.go index de4209f5d48..253fd68ff3c 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 97cfbc2ca83..f5cc19473a5 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 3f93c6ac6f9..de5e29b3830 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 b8d9c6de77d..19697bcc07c 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 ddea7d04e16..a7cfb95c4c7 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 e82f4b1dac3..bc3a6ef0c24 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 @@ -197,95 +207,109 @@ func (c *clientNetwork) startPeersStatusChangeWatcher() { } func (c *clientNetwork) removeRouteFromWireguardPeer(peerKey string) error { + c.removeStateRoute() + state, err := c.statusRecorder.GetPeer(peerKey) if err != nil { - return fmt.Errorf("get peer state: %v", err) - } - - state.DeleteRoute(c.network.String()) - if err := c.statusRecorder.UpdatePeerState(state); err != nil { - log.Warnf("Failed to update peer state: %v", err) + return fmt.Errorf("get peer state: %w", 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 c.currentChosen == nil { + return nil + } - if err := c.removeRouteFromWireguardPeer(c.chosenRoute.Peer); err != nil { - return fmt.Errorf("remove route: %v", err) - } + var merr *multierror.Error + + if err := c.removeRouteFromWireguardPeer(c.currentChosen.Peer); err != nil { + merr = multierror.Append(merr, fmt.Errorf("remove allowed IPs for peer %s: %w", c.currentChosen.Peer, err)) } - return nil + 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(c.currentChosen.Peer); 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] + + if err := c.handler.AddAllowedIPs(c.currentChosen.Peer); err != nil { + return fmt.Errorf("add allowed IPs for peer %s: %w", c.currentChosen.Peer, err) + } + + c.addStateRoute() + + return nil +} - state, err := c.statusRecorder.GetPeer(c.chosenRoute.Peer) +func (c *clientNetwork) addStateRoute() { + state, err := c.statusRecorder.GetPeer(c.currentChosen.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) - } + return } - 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) + state.AddRoute(c.handler.String()) + if err := c.statusRecorder.UpdatePeerState(state); err != nil { + log.Warnf("Failed to update peer state: %v", err) } +} - return nil +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) { @@ -318,24 +342,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 +366,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 +374,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 9419ea777fe..0ae10e568d7 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 00000000000..ff52e98a078 --- /dev/null +++ b/client/internal/routemanager/dynamic/route.go @@ -0,0 +1,362 @@ +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] was already resolved for a different domain routed by peer [%s]. Routing for this IP will be done by peer [%s], HA routing disabled", + prefix.Addr(), + domain.SafeString(), + ref.Out, + 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 47549f74d77..4293d5e85b8 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,67 @@ 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 + 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) 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(), + statusRecorder: statusRecorder, + wgInterface: wgInterface, + pubKey: pubKey, + notifier: newNotifier(), } + dm.routeRefCounter = refcounter.New( + func(prefix netip.Prefix, _ any) (any, error) { + return nil, systemops.AddVPNRoute(prefix, wgInterface.ToInterface()) + }, + func(prefix netip.Prefix, _ any) error { + return systemops.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 +114,7 @@ func (m *DefaultManager) Init() (peer.BeforeAddPeerHookFunc, peer.AfterRemovePee return nil, nil, nil } - if err := cleanupRouting(); err != nil { + if err := systemops.CleanupRouting(); err != nil { log.Warnf("Failed cleaning up routing: %v", err) } @@ -86,7 +122,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 := systemops.SetupRouting(ips, m.wgInterface) if err != nil { return nil, nil, fmt.Errorf("setup routing: %w", err) } @@ -110,8 +146,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 := systemops.CleanupRouting(); err != nil { log.Errorf("Error cleaning up routing: %v", err) } else { log.Info("Routing cleanup complete") @@ -185,7 +232,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 +244,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 +257,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 +275,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 +288,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 +302,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 7eb8dd00210..5d32032a5c3 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 00000000000..02d9cb09b7f --- /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", ref.Count, prefix) + + // Call AddFunc only if it's a new prefix + if ref.Count == 0 { + log.Tracef("Adding for prefix %s", prefix) + 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", ref.Count, prefix) + if ref.Count == 1 { + log.Tracef("Removing for prefix %s", prefix) + 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 00000000000..6753b64efe0 --- /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 7715aa8194d..00000000000 --- 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 95672e4805c..24267efdc63 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 00000000000..c4d7e6c6c6c --- /dev/null +++ b/client/internal/routemanager/static/route.go @@ -0,0 +1,47 @@ +package static + +import ( + "context" + + "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 { + _, err := r.allowedIPsRefcounter.Increment(r.route.Network, peerKey) + return err +} + +func (r *Route) RemoveAllowedIPs() error { + _, err := r.allowedIPsRefcounter.Decrement(r.route.Network) + return err +} diff --git a/client/internal/routemanager/routeflags_bsd.go b/client/internal/routemanager/systemops/routeflags_bsd.go similarity index 95% rename from client/internal/routemanager/routeflags_bsd.go rename to client/internal/routemanager/systemops/routeflags_bsd.go index b39079e61b9..12f158dcba6 100644 --- a/client/internal/routemanager/routeflags_bsd.go +++ b/client/internal/routemanager/systemops/routeflags_bsd.go @@ -1,6 +1,6 @@ //go:build darwin || dragonfly || netbsd || openbsd -package routemanager +package systemops import "syscall" diff --git a/client/internal/routemanager/routeflags_freebsd.go b/client/internal/routemanager/systemops/routeflags_freebsd.go similarity index 96% rename from client/internal/routemanager/routeflags_freebsd.go rename to client/internal/routemanager/systemops/routeflags_freebsd.go index 259253a765f..cb35f521e80 100644 --- a/client/internal/routemanager/routeflags_freebsd.go +++ b/client/internal/routemanager/systemops/routeflags_freebsd.go @@ -1,5 +1,5 @@ //go:build: freebsd -package routemanager +package systemops import "syscall" diff --git a/client/internal/routemanager/systemops.go b/client/internal/routemanager/systemops/systemops.go similarity index 62% rename from client/internal/routemanager/systemops.go rename to client/internal/routemanager/systemops/systemops.go index f349402d15c..2baccb64b6e 100644 --- a/client/internal/routemanager/systemops.go +++ b/client/internal/routemanager/systemops/systemops.go @@ -1,6 +1,6 @@ //go:build !android && !ios -package routemanager +package systemops import ( "context" @@ -15,19 +15,27 @@ 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" ) +type Nexthop struct { + IP netip.Addr + Intf *net.Interface +} + +type ExclusionCounter = refcounter.Counter[any, Nexthop] + var splitDefaultv4_1 = netip.PrefixFrom(netip.IPv4Unspecified(), 1) 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") - // TODO: fix: for default our wg address now appears as the default gw func addRouteForCurrentDefaultGateway(prefix netip.Prefix) error { addr := netip.IPv4Unspecified() @@ -35,19 +43,19 @@ func addRouteForCurrentDefaultGateway(prefix netip.Prefix) error { 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,50 +68,56 @@ 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 addToRouteTable(gatewayPrefix, nexthop) } -func GetNextHop(ip netip.Addr) (netip.Addr, *net.Interface, error) { +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 runtime.GOOS == "freebsd" { - return netip.Addr{}, intf, nil + return Nexthop{Intf: intf}, nil } if preferredSrc == nil { - return netip.Addr{}, nil, ErrRouteNotFound + return Nexthop{}, vars.ErrRouteNotFound } - log.Debugf("No next hop found for ip %s, using preferred source %s", ip, preferredSrc) + 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 @@ -144,7 +158,7 @@ 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 } } @@ -153,7 +167,7 @@ func isSubRange(prefix netip.Prefix) (bool, 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 addRouteToNonVPNIntf(prefix netip.Prefix, vpnIntf *iface.WGIface, initialNextHop netip.Addr, initialIntf *net.Interface) (netip.Addr, *net.Interface, error) { +func addRouteToNonVPNIntf(prefix netip.Prefix, vpnIntf *iface.WGIface, initialNextHop Nexthop) (Nexthop, error) { addr := prefix.Addr() switch { case addr.IsLoopback(), @@ -163,71 +177,74 @@ func addRouteToNonVPNIntf(prefix netip.Prefix, vpnIntf *iface.WGIface, initialNe addr.IsUnspecified(), addr.IsMulticast(): - return netip.Addr{}, nil, ErrRouteNotAllowed + return Nexthop{}, vars.ErrRouteNotAllowed } // Determine the exit interface and next hop for the prefix, so we can add a specific route - nexthop, intf, err := GetNextHop(addr) + nexthop, err := GetNextHop(addr) if err != nil { - return netip.Addr{}, nil, fmt.Errorf("get next hop: %w", err) + return Nexthop{}, 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 + 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 netip.Addr{}, nil, fmt.Errorf("failed to convert vpn address to netip.Addr") + 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 == vpnAddr || exitIntf != nil && exitIntf.Name == vpnIntf.Name() { + 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 - 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) + log.Debugf("Adding a new route for prefix %s with next hop %s", prefix, exitNextHop.IP) + if err := addToRouteTable(prefix, exitNextHop); err != nil { + return Nexthop{}, fmt.Errorf("add route to table: %w", err) } - return exitNextHop, exitIntf, nil + 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 genericAddVPNRoute(prefix netip.Prefix, intf *net.Interface) error { - if prefix == defaultv4 { - if err := addToRouteTable(splitDefaultv4_1, netip.Addr{}, intf); err != nil { + nextHop := Nexthop{netip.Addr{}, intf} + + if prefix == vars.Defaultv4 { + if err := addToRouteTable(splitDefaultv4_1, nextHop); err != nil { return err } - if err := addToRouteTable(splitDefaultv4_2, netip.Addr{}, intf); err != nil { - if err2 := removeFromRouteTable(splitDefaultv4_1, netip.Addr{}, intf); err2 != nil { + if err := addToRouteTable(splitDefaultv4_2, nextHop); err != nil { + if err2 := 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 := addToRouteTable(splitDefaultv6_1, netip.Addr{}, intf); err != nil { + if err := addToRouteTable(splitDefaultv6_1, nextHop); 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 { + if err := addToRouteTable(splitDefaultv6_2, nextHop); err != nil { + if err2 := 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 == defaultv6 { - if err := addToRouteTable(splitDefaultv6_1, netip.Addr{}, intf); err != nil { + } else if prefix == vars.Defaultv6 { + if err := addToRouteTable(splitDefaultv6_1, nextHop); 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 { + if err := addToRouteTable(splitDefaultv6_2, nextHop); err != nil { + if err2 := 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) @@ -262,92 +279,79 @@ func addNonExistingRoute(prefix netip.Prefix, intf *net.Interface) error { } } - return addToRouteTable(prefix, netip.Addr{}, intf) + return 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 genericRemoveVPNRoute(prefix netip.Prefix, intf *net.Interface) error { - if prefix == defaultv4 { + nextHop := Nexthop{netip.Addr{}, intf} + + if prefix == vars.Defaultv4 { var result *multierror.Error - if err := removeFromRouteTable(splitDefaultv4_1, netip.Addr{}, intf); err != nil { + if err := removeFromRouteTable(splitDefaultv4_1, nextHop); err != nil { result = multierror.Append(result, err) } - if err := removeFromRouteTable(splitDefaultv4_2, netip.Addr{}, intf); err != nil { + if err := removeFromRouteTable(splitDefaultv4_2, nextHop); 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 { + if err := removeFromRouteTable(splitDefaultv6_1, nextHop); err != nil { result = multierror.Append(result, err) } - if err := removeFromRouteTable(splitDefaultv6_2, netip.Addr{}, intf); err != nil { + if err := removeFromRouteTable(splitDefaultv6_2, nextHop); err != nil { result = multierror.Append(result, err) } return result.ErrorOrNil() - } else if prefix == defaultv6 { + } else if prefix == vars.Defaultv6 { var result *multierror.Error - if err := removeFromRouteTable(splitDefaultv6_1, netip.Addr{}, intf); err != nil { + if err := removeFromRouteTable(splitDefaultv6_1, nextHop); err != nil { result = multierror.Append(result, err) } - if err := removeFromRouteTable(splitDefaultv6_2, netip.Addr{}, intf); err != nil { + if err := removeFromRouteTable(splitDefaultv6_2, nextHop); 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) + return nberrors.FormatErrorOrNil(result) } - prefix := netip.PrefixFrom(addr, prefixLength) - return &prefix, nil + return removeFromRouteTable(prefix, nextHop) } -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) { +func setupRoutingWithRefCounter(refCounter **ExclusionCounter, initAddresses []net.IP, wgIface *iface.WGIface) (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, initialIntfV6, err := GetNextHop(netip.IPv6Unspecified()) - if err != nil && !errors.Is(err, ErrRouteNotFound) { + 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) } - *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 + *refCounter = refcounter.New( + func(prefix netip.Prefix, _ any) (Nexthop, error) { + initialNexthop := initialNextHopV4 + if prefix.Addr().Is6() { + initialNexthop = initialNextHopV6 } - return addRouteToNonVPNIntf(prefix, wgIface, nexthop, intf) + + nexthop, err := addRouteToNonVPNIntf(prefix, wgIface, 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 }, removeFromRouteTable, ) - return setupHooks(*routeManager, initAddresses) + return setupHooks(*refCounter, initAddresses) } -func cleanupRoutingWithRouteManager(routeManager *RouteManager) error { +func cleanupRoutingWithRefCounter(routeManager *ExclusionCounter) error { if routeManager == nil { return nil } @@ -363,21 +367,21 @@ func cleanupRoutingWithRouteManager(routeManager *RouteManager) error { return nil } -func setupHooks(routeManager *RouteManager, initAddresses []net.IP) (peer.BeforeAddPeerHookFunc, peer.AfterRemovePeerHookFunc, error) { +func setupHooks(routeManager *ExclusionCounter, initAddresses []net.IP) (peer.BeforeAddPeerHookFunc, peer.AfterRemovePeerHookFunc, error) { beforeHook := func(connID nbnet.ConnectionID, ip net.IP) error { - prefix, err := getPrefixFromIP(ip) + prefix, err := util.GetPrefixFromIP(ip) if err != nil { return fmt.Errorf("convert ip to prefix: %w", err) } - if err := routeManager.AddRouteRef(connID, *prefix); err != nil { + if _, err := routeManager.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 := routeManager.RemoveRouteRef(connID); err != nil { + if err := routeManager.DecrementWithID(string(connID)); err != nil { return fmt.Errorf("remove route reference: %w", err) } @@ -399,7 +403,7 @@ func setupHooks(routeManager *RouteManager, initAddresses []net.IP) (peer.Before for _, ip := range resolvedIPs { result = multierror.Append(result, beforeHook(connID, ip.IP)) } - return result.ErrorOrNil() + return nberrors.FormatErrorOrNil(result) }) nbnet.AddDialerCloseHook(func(connID nbnet.ConnectionID, conn *net.Conn) error { diff --git a/client/internal/routemanager/systemops_android.go b/client/internal/routemanager/systemops/systemops_android.go similarity index 58% rename from client/internal/routemanager/systemops_android.go rename to client/internal/routemanager/systemops/systemops_android.go index 4d23d39100e..d312e5e4938 100644 --- a/client/internal/routemanager/systemops_android.go +++ b/client/internal/routemanager/systemops/systemops_android.go @@ -1,4 +1,4 @@ -package routemanager +package systemops import ( "net" @@ -11,23 +11,23 @@ import ( "github.com/netbirdio/netbird/iface" ) -func setupRouting([]net.IP, *iface.WGIface) (peer.BeforeAddPeerHookFunc, peer.AfterRemovePeerHookFunc, error) { +func SetupRouting([]net.IP, *iface.WGIface) (peer.BeforeAddPeerHookFunc, peer.AfterRemovePeerHookFunc, error) { return nil, nil, nil } -func cleanupRouting() error { +func CleanupRouting() error { return nil } -func enableIPForwarding() error { +func EnableIPForwarding() error { log.Infof("Enable IP forwarding is not implemented on %s", runtime.GOOS) return nil } -func addVPNRoute(netip.Prefix, *net.Interface) error { +func AddVPNRoute(netip.Prefix, *net.Interface) error { return nil } -func removeVPNRoute(netip.Prefix, *net.Interface) error { +func RemoveVPNRoute(netip.Prefix, *net.Interface) error { return nil } diff --git a/client/internal/routemanager/systemops_bsd.go b/client/internal/routemanager/systemops/systemops_bsd.go similarity index 96% rename from client/internal/routemanager/systemops_bsd.go rename to client/internal/routemanager/systemops/systemops_bsd.go index 45dbe525636..eb7b7a03dcf 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" @@ -100,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: @@ -113,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 81bca504cf0..2240b053c8f 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 c23a7cde3fa..59fd9b74a5b 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" @@ -41,7 +41,7 @@ func TestConcurrentRoutes(t *testing.T) { go func(ip netip.Addr) { defer wg.Done() prefix := netip.PrefixFrom(ip, 32) - if err := addToRouteTable(prefix, netip.Addr{}, intf); err != nil { + if err := addToRouteTable(prefix, Nexthop{netip.Addr{}, intf}); err != nil { t.Errorf("Failed to add route for %s: %v", prefix, err) } }(baseIP) @@ -57,7 +57,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 := 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_ios.go b/client/internal/routemanager/systemops/systemops_ios.go similarity index 58% rename from client/internal/routemanager/systemops_ios.go rename to client/internal/routemanager/systemops/systemops_ios.go index 4d23d39100e..d312e5e4938 100644 --- a/client/internal/routemanager/systemops_ios.go +++ b/client/internal/routemanager/systemops/systemops_ios.go @@ -1,4 +1,4 @@ -package routemanager +package systemops import ( "net" @@ -11,23 +11,23 @@ import ( "github.com/netbirdio/netbird/iface" ) -func setupRouting([]net.IP, *iface.WGIface) (peer.BeforeAddPeerHookFunc, peer.AfterRemovePeerHookFunc, error) { +func SetupRouting([]net.IP, *iface.WGIface) (peer.BeforeAddPeerHookFunc, peer.AfterRemovePeerHookFunc, error) { return nil, nil, nil } -func cleanupRouting() error { +func CleanupRouting() error { return nil } -func enableIPForwarding() error { +func EnableIPForwarding() error { log.Infof("Enable IP forwarding is not implemented on %s", runtime.GOOS) return nil } -func addVPNRoute(netip.Prefix, *net.Interface) error { +func AddVPNRoute(netip.Prefix, *net.Interface) error { return nil } -func removeVPNRoute(netip.Prefix, *net.Interface) error { +func RemoveVPNRoute(netip.Prefix, *net.Interface) error { return nil } diff --git a/client/internal/routemanager/systemops_linux.go b/client/internal/routemanager/systemops/systemops_linux.go similarity index 88% rename from client/internal/routemanager/systemops_linux.go rename to client/internal/routemanager/systemops/systemops_linux.go index ce0c07ce69e..3ef99d35736 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" @@ -18,6 +18,7 @@ import ( "github.com/vishvananda/netlink" "github.com/netbirdio/netbird/client/internal/peer" + "github.com/netbirdio/netbird/client/internal/routemanager/vars" "github.com/netbirdio/netbird/iface" nbnet "github.com/netbirdio/netbird/util/net" ) @@ -41,7 +42,7 @@ const ( var ErrTableIDExists = errors.New("ID exists with different name") -var routeManager = &RouteManager{} +var refCounter *ExclusionCounter // originalSysctl stores the original sysctl values before they are modified var originalSysctl map[string]int @@ -82,7 +83,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,10 +93,10 @@ 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 SetupRouting(initAddresses []net.IP, wgIface *iface.WGIface) (_ peer.BeforeAddPeerHookFunc, _ peer.AfterRemovePeerHookFunc, err error) { if isLegacy() { log.Infof("Using legacy routing setup") - return setupRoutingWithRouteManager(&routeManager, initAddresses, wgIface) + return setupRoutingWithRefCounter(&refCounter, initAddresses, wgIface) } if err = addRoutingTableName(); err != nil { @@ -111,7 +112,7 @@ func setupRouting(initAddresses []net.IP, wgIface *iface.WGIface) (_ peer.Before defer func() { if err != nil { - if cleanErr := cleanupRouting(); cleanErr != nil { + if cleanErr := CleanupRouting(); cleanErr != nil { log.Errorf("Error cleaning up routing: %v", cleanErr) } } @@ -123,7 +124,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 setupRoutingWithRefCounter(&refCounter, initAddresses, wgIface) } return nil, nil, fmt.Errorf("%s: %w", rule.description, err) } @@ -132,12 +133,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 CleanupRouting() error { if isLegacy() { - return cleanupRoutingWithRouteManager(routeManager) + return cleanupRoutingWithRefCounter(refCounter) } var result *multierror.Error @@ -165,49 +166,49 @@ func cleanupRouting() error { return result.ErrorOrNil() } -func addToRouteTable(prefix netip.Prefix, nexthop netip.Addr, intf *net.Interface) error { - return addRoute(prefix, nexthop, intf, syscall.RT_TABLE_MAIN) +func 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 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 AddVPNRoute(prefix netip.Prefix, intf *net.Interface) error { if isLegacy() { return 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 RemoveVPNRoute(prefix netip.Prefix, intf *net.Interface) error { if isLegacy() { return 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 +256,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 +269,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 +328,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 +341,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) } @@ -376,7 +377,7 @@ func flushRoutes(tableID, family int) error { return result.ErrorOrNil() } -func enableIPForwarding() error { +func EnableIPForwarding() error { _, err := setSysctl(ipv4ForwardingPath, 1, false) return err } @@ -481,19 +482,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 } 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 0043c3f4e94..8f12740d077 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_nonlinux.go b/client/internal/routemanager/systemops/systemops_nonlinux.go similarity index 61% rename from client/internal/routemanager/systemops_nonlinux.go rename to client/internal/routemanager/systemops/systemops_nonlinux.go index 91879790a1f..735238abc81 100644 --- a/client/internal/routemanager/systemops_nonlinux.go +++ b/client/internal/routemanager/systemops/systemops_nonlinux.go @@ -1,6 +1,6 @@ //go:build !linux && !ios -package routemanager +package systemops import ( "net" @@ -10,15 +10,15 @@ import ( log "github.com/sirupsen/logrus" ) -func enableIPForwarding() error { +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 { +func AddVPNRoute(prefix netip.Prefix, intf *net.Interface) error { return genericAddVPNRoute(prefix, intf) } -func removeVPNRoute(prefix netip.Prefix, intf *net.Interface) error { +func RemoveVPNRoute(prefix netip.Prefix, intf *net.Interface) error { return genericRemoveVPNRoute(prefix, intf) } diff --git a/client/internal/routemanager/systemops_test.go b/client/internal/routemanager/systemops/systemops_test.go similarity index 82% rename from client/internal/routemanager/systemops_test.go rename to client/internal/routemanager/systemops/systemops_test.go index 8bcf06dcefe..ce113402e64 100644 --- a/client/internal/routemanager/systemops_test.go +++ b/client/internal/routemanager/systemops/systemops_test.go @@ -1,6 +1,6 @@ //go:build !android && !ios -package routemanager +package systemops import ( "bytes" @@ -63,17 +63,17 @@ func TestAddRemoveRoutes(t *testing.T) { err = wgInterface.Create() require.NoError(t, err, "should create testing wireguard interface") - _, _, err = setupRouting(nil, wgInterface) + _, _, err = SetupRouting(nil, wgInterface) require.NoError(t, err) t.Cleanup(func() { - assert.NoError(t, cleanupRouting()) + assert.NoError(t, 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 = AddVPNRoute(testCase.prefix, intf) require.NoError(t, err, "genericAddVPNRoute should not return err") if testCase.shouldRouteToWireguard { @@ -84,19 +84,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 = 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 +104,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 +130,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 +164,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, }, { @@ -216,12 +216,12 @@ func TestAddExistAndRemoveRoute(t *testing.T) { // Prepare the environment if testCase.preExistingPrefix.IsValid() { - err := addVPNRoute(testCase.preExistingPrefix, intf) + err := 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 = AddVPNRoute(testCase.prefix, intf) require.NoError(t, err, "should not return err when adding route") if testCase.shouldAddRoute { @@ -231,7 +231,7 @@ func TestAddExistAndRemoveRoute(t *testing.T) { require.True(t, ok, "route should exist") // remove route again if added - err = removeVPNRoute(testCase.prefix, intf) + err = RemoveVPNRoute(testCase.prefix, intf) require.NoError(t, err, "should not return err") } @@ -353,10 +353,10 @@ func setupTestEnv(t *testing.T) { assert.NoError(t, wgIface.Close()) }) - _, _, err := setupRouting(nil, wgIface) + _, _, err := SetupRouting(nil, wgIface) require.NoError(t, err, "setupRouting should not return err") t.Cleanup(func() { - assert.NoError(t, cleanupRouting()) + assert.NoError(t, CleanupRouting()) }) index, err := net.InterfaceByName(wgIface.Name()) @@ -364,42 +364,42 @@ func setupTestEnv(t *testing.T) { intf := &net.Interface{Index: index.Index, Name: wgIface.Name()} // default route exists in main table and vpn table - err = addVPNRoute(netip.MustParsePrefix("0.0.0.0/0"), intf) + 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) + err = RemoveVPNRoute(netip.MustParsePrefix("0.0.0.0/0"), intf) assert.NoError(t, err, "removeVPNRoute should not return err") }) // 10.0.0.0/8 route exists in main table and vpn table - err = addVPNRoute(netip.MustParsePrefix("10.0.0.0/8"), intf) + 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) + err = RemoveVPNRoute(netip.MustParsePrefix("10.0.0.0/8"), intf) assert.NoError(t, err, "removeVPNRoute should not return err") }) // 10.10.0.0/24 more specific route exists in vpn table - err = addVPNRoute(netip.MustParsePrefix("10.10.0.0/24"), intf) + 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) + err = RemoveVPNRoute(netip.MustParsePrefix("10.10.0.0/24"), intf) assert.NoError(t, err, "removeVPNRoute should not return err") }) // 127.0.10.0/24 more specific route exists in vpn table - err = addVPNRoute(netip.MustParsePrefix("127.0.10.0/24"), intf) + 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) + err = RemoveVPNRoute(netip.MustParsePrefix("127.0.10.0/24"), intf) assert.NoError(t, err, "removeVPNRoute should not return err") }) // unique route in vpn table - err = addVPNRoute(netip.MustParsePrefix("172.16.0.0/12"), intf) + 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) + err = RemoveVPNRoute(netip.MustParsePrefix("172.16.0.0/12"), intf) assert.NoError(t, err, "removeVPNRoute should not return err") }) } @@ -410,11 +410,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_unix.go b/client/internal/routemanager/systemops/systemops_unix.go similarity index 64% rename from client/internal/routemanager/systemops_unix.go rename to client/internal/routemanager/systemops/systemops_unix.go index e54924d65f9..7b6b60a0ccb 100644 --- a/client/internal/routemanager/systemops_unix.go +++ b/client/internal/routemanager/systemops/systemops_unix.go @@ -1,6 +1,6 @@ //go:build (darwin && !ios) || dragonfly || freebsd || netbsd || openbsd -package routemanager +package systemops import ( "fmt" @@ -17,25 +17,25 @@ import ( "github.com/netbirdio/netbird/iface" ) -var routeManager *RouteManager +var refCounter *ExclusionCounter -func setupRouting(initAddresses []net.IP, wgIface *iface.WGIface) (peer.BeforeAddPeerHookFunc, peer.AfterRemovePeerHookFunc, error) { - return setupRoutingWithRouteManager(&routeManager, initAddresses, wgIface) +func SetupRouting(initAddresses []net.IP, wgIface *iface.WGIface) (peer.BeforeAddPeerHookFunc, peer.AfterRemovePeerHookFunc, error) { + return setupRoutingWithRefCounter(&refCounter, initAddresses, wgIface) } -func cleanupRouting() error { - return cleanupRoutingWithRouteManager(routeManager) +func CleanupRouting() error { + return cleanupRoutingWithRefCounter(refCounter) } -func addToRouteTable(prefix netip.Prefix, nexthop netip.Addr, intf *net.Interface) error { - return routeCmd("add", prefix, nexthop, intf) +func addToRouteTable(prefix netip.Prefix, nexthop Nexthop) error { + return routeCmd("add", prefix, nexthop) } -func removeFromRouteTable(prefix netip.Prefix, nexthop netip.Addr, intf *net.Interface) error { - return routeCmd("delete", prefix, nexthop, intf) +func removeFromRouteTable(prefix netip.Prefix, nexthop Nexthop) error { + return routeCmd("delete", prefix, nexthop) } -func routeCmd(action string, prefix netip.Prefix, nexthop netip.Addr, intf *net.Interface) error { +func routeCmd(action string, prefix netip.Prefix, nexthop Nexthop) error { inet := "-inet" network := prefix.String() if prefix.IsSingleIP() { @@ -46,10 +46,10 @@ func routeCmd(action string, prefix netip.Prefix, nexthop netip.Addr, intf *net. } 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 561eaeea4b2..fc964aa6222 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 80% rename from client/internal/routemanager/systemops_windows.go rename to client/internal/routemanager/systemops/systemops_windows.go index 32e94d8da40..7ca29a74504 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" @@ -57,14 +57,14 @@ var prefixList []netip.Prefix var lastUpdate time.Time var mux = sync.Mutex{} -var routeManager *RouteManager +var refCounter *ExclusionCounter -func setupRouting(initAddresses []net.IP, wgIface *iface.WGIface) (peer.BeforeAddPeerHookFunc, peer.AfterRemovePeerHookFunc, error) { - return setupRoutingWithRouteManager(&routeManager, initAddresses, wgIface) +func SetupRouting(initAddresses []net.IP, wgIface *iface.WGIface) (peer.BeforeAddPeerHookFunc, peer.AfterRemovePeerHookFunc, error) { + return setupRoutingWithRefCounter(&refCounter, initAddresses, wgIface) } -func cleanupRouting() error { - return cleanupRoutingWithRouteManager(routeManager) +func CleanupRouting() error { + return cleanupRoutingWithRefCounter(refCounter) } func getRoutesFromTable() ([]netip.Prefix, error) { @@ -93,7 +93,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 +157,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 +170,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,24 +185,24 @@ 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()) +func 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) } - intf = &net.Interface{Index: zone} - nexthop.WithZone("") + nexthop.Intf = &net.Interface{Index: zone} + nexthop.IP.WithZone("") } - return addRouteCmd(prefix, nexthop, intf) + return addRouteCmd(prefix, nexthop) } -func removeFromRouteTable(prefix netip.Prefix, nexthop netip.Addr, _ *net.Interface) error { +func removeFromRouteTable(prefix netip.Prefix, nexthop Nexthop) error { args := []string{"delete", prefix.String()} - if nexthop.IsValid() { - nexthop.WithZone("") - args = append(args, nexthop.Unmap().String()) + if nexthop.IP.IsValid() { + nexthop.IP.WithZone("") + args = append(args, nexthop.IP.Unmap().String()) } routeCmd := uspfilter.GetSystem32Command("route") 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 a5e03b8d2ce..9180ed58c4a 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/util/ip.go b/client/internal/routemanager/util/ip.go new file mode 100644 index 00000000000..ac5a48e3769 --- /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 00000000000..4aa986d2fa5 --- /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 1c17e880393..00128a27b03 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 fb1e456cd00..7df433f9264 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 83c8278d56e..a624d1336b8 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, - 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, + 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, 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, + 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, 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, 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, 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, + 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, 0x42, 0x08, 0x5a, 0x06, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x33, + 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 e39b08bc375..eb3efbb291d 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 4c63cea93a5..d70e0dca391 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 a59cffd14e2..482d02efc2a 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/ui/route.go b/client/ui/route.go index 0ac58e5d5b0..cb43992540b 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/iface/address.go b/iface/address.go index 2920d009fa1..5ff4fbc0645 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/iface.go b/iface/iface.go index 3ae40ad4c0e..928077a3db4 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/wg_configurer.go b/iface/wg_configurer.go index 91c57eb9c55..dd38ba0757a 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_unix.go b/iface/wg_configurer_kernel_unix.go index f2d6001cadc..48ea70b7ba6 100644 --- a/iface/wg_configurer_kernel_unix.go +++ b/iface/wg_configurer_kernel_unix.go @@ -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 0c1b6e85a8c..04a29a60b19 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/domain/domain.go b/management/domain/domain.go new file mode 100644 index 00000000000..e7e6b050ae3 --- /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 00000000000..413a23442d2 --- /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 18077ea892f..d8bb2ad4a1e 100644 --- a/management/proto/management.pb.go +++ b/management/proto/management.pb.go @@ -1,15 +1,15 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.26.0 -// protoc v4.24.3 +// protoc v3.12.4 // source: management.proto package proto import ( + timestamp "github.com/golang/protobuf/ptypes/timestamp" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" - timestamppb "google.golang.org/protobuf/types/known/timestamppb" reflect "reflect" sync "sync" ) @@ -872,7 +872,7 @@ type ServerKeyResponse struct { // Server's Wireguard public key Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` // Key expiration timestamp after which the key should be fetched again by the client - ExpiresAt *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=expiresAt,proto3" json:"expiresAt,omitempty"` + ExpiresAt *timestamp.Timestamp `protobuf:"bytes,2,opt,name=expiresAt,proto3" json:"expiresAt,omitempty"` // Version of the Wiretrustee Management Service protocol Version int32 `protobuf:"varint,3,opt,name=version,proto3" json:"version,omitempty"` } @@ -916,7 +916,7 @@ func (x *ServerKeyResponse) GetKey() string { return "" } -func (x *ServerKeyResponse) GetExpiresAt() *timestamppb.Timestamp { +func (x *ServerKeyResponse) GetExpiresAt() *timestamp.Timestamp { if x != nil { return x.ExpiresAt } @@ -1817,13 +1817,15 @@ 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() { @@ -1907,6 +1909,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 @@ -2609,7 +2625,7 @@ var file_management_proto_rawDesc = []byte{ 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, + 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, @@ -2620,105 +2636,108 @@ var file_management_proto_rawDesc = []byte{ 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, + 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, 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, 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, + 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, 0x22, 0x00, 0x42, 0x08, 0x5a, 0x06, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 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, } var ( @@ -2771,7 +2790,7 @@ var file_management_proto_goTypes = []interface{}{ (*NameServer)(nil), // 32: management.NameServer (*FirewallRule)(nil), // 33: management.FirewallRule (*NetworkAddress)(nil), // 34: management.NetworkAddress - (*timestamppb.Timestamp)(nil), // 35: google.protobuf.Timestamp + (*timestamp.Timestamp)(nil), // 35: google.protobuf.Timestamp } var file_management_proto_depIdxs = []int32{ 15, // 0: management.SyncResponse.wiretrusteeConfig:type_name -> management.WiretrusteeConfig diff --git a/management/proto/management.proto b/management/proto/management.proto index 2cc0efa22ba..49bdd67528a 100644 --- a/management/proto/management.proto +++ b/management/proto/management.proto @@ -303,6 +303,8 @@ message Route { int64 Metric = 5; bool Masquerade = 6; string NetID = 7; + repeated string Domains = 8; + bool keepRoute = 9; } // DNSConfig represents a dns.Update diff --git a/management/server/account.go b/management/server/account.go index 984139a12d6..313093195ea 100644 --- a/management/server/account.go +++ b/management/server/account.go @@ -23,6 +23,7 @@ import ( "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" @@ -101,7 +102,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) @@ -274,7 +275,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) @@ -292,7 +293,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) } @@ -375,11 +376,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) } } diff --git a/management/server/account_test.go b/management/server/account_test.go index 38c9fabbc2f..0daff4e1d19 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/http/api/openapi.yml b/management/server/http/api/openapi.yml index aeaef6f6412..e8a98aa7039 100644 --- a/management/server/http/api/openapi.yml +++ b/management/server/http/api/openapi.yml @@ -995,9 +995,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 +1022,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 +1034,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 +1050,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 e378213a116..9bd12446756 100644 --- a/management/server/http/api/types.gen.go +++ b/management/server/http/api/types.gen.go @@ -954,6 +954,9 @@ 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 +966,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 +996,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/pat_handler_test.go b/management/server/http/pat_handler_test.go index 45fda0a5581..5058b4110ac 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/routes_handler.go b/management/server/http/routes_handler.go index f755e7a16a2..a48c6d61d9c 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, 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") + } - util.WriteJSONObject(w, &resp) + 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 1c8288d5f7f..261d0c2310c 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 91f19d8d89e..8a78188be92 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/mock_server/account_mock.go b/management/server/mock_server/account_mock.go index 765cd8483cb..c08c2b787a1 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" @@ -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 @@ -413,9 +415,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") } diff --git a/management/server/route.go b/management/server/route.go index 2de813d48ba..8741cc47dec 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 d28b40d48ed..4fd1d73572d 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", @@ -110,10 +148,28 @@ func TestCreateRoute(t *testing.T) { Groups: []string{routeGroup1, routeGroup2}, }, }, + { + name: "Both network and domains provided should fail", + inputArgs: input{ + network: netip.MustParsePrefix("192.168.0.0/16"), + domains: domain.List{"domain1", "domain2"}, + netID: "happy", + peerKey: peer1ID, + peerGroupIDs: []string{routeGroupHA1}, + description: "super", + masquerade: false, + metric: 9999, + enabled: true, + groups: []string{routeGroup1}, + }, + errFunc: require.Error, + shouldCreate: false, + }, { name: "Both peer and peer_groups Provided 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, peerGroupIDs: []string{routeGroupHA1}, @@ -127,11 +183,12 @@ func TestCreateRoute(t *testing.T) { shouldCreate: false, }, { - name: "Bad Prefix Should Fail", + name: "Bad Peer Should Fail", inputArgs: input{ - network: "192.168.0.0/34", + network: netip.MustParsePrefix("192.168.0.0/16"), + networkType: route.IPv4Network, netID: "happy", - peerKey: peer1ID, + peerKey: "notExistingPeer", description: "super", masquerade: false, metric: 9999, @@ -142,24 +199,27 @@ func TestCreateRoute(t *testing.T) { shouldCreate: false, }, { - name: "Bad Peer Should Fail", + name: "Bad Peer already has this network route", inputArgs: input{ - network: "192.168.0.0/16", - netID: "happy", - peerKey: "notExistingPeer", + network: existingNetwork, + networkType: route.IPv4Network, + netID: "bad", + peerKey: peer5ID, description: "super", masquerade: false, metric: 9999, enabled: true, groups: []string{routeGroup1}, }, - errFunc: require.Error, - shouldCreate: false, + createInitRoute: true, + errFunc: require.Error, + shouldCreate: false, }, { - name: "Bad Peer already has this route", + name: "Bad Peer already has this domains route", inputArgs: input{ - network: existingNetwork, + domains: existingDomains, + networkType: route.DomainNetwork, netID: "bad", peerKey: peer5ID, description: "super", @@ -173,9 +233,27 @@ func TestCreateRoute(t *testing.T) { shouldCreate: false, }, { - name: "Bad Peers Group already has this route", + 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 6f74563e261..4d952beba14 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 50c53cbe6da..eb6c36bd8bc 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 -}