diff --git a/client/cmd/root.go b/client/cmd/root.go index 9c4ad99de..ca143ffc2 100644 --- a/client/cmd/root.go +++ b/client/cmd/root.go @@ -119,6 +119,7 @@ func init() { rootCmd.PersistentFlags().StringVarP(&setupKey, "setup-key", "k", "", "Setup key obtained from the Management Service Dashboard (used to register peer)") rootCmd.PersistentFlags().StringVar(&preSharedKey, preSharedKeyFlag, "", "Sets Wireguard PreSharedKey property. If set, then only peers that have the same key can communicate.") rootCmd.PersistentFlags().StringVarP(&hostName, "hostname", "n", "", "Sets a custom hostname for the device") + rootCmd.AddCommand(serviceCmd) rootCmd.AddCommand(upCmd) rootCmd.AddCommand(downCmd) @@ -126,8 +127,14 @@ func init() { rootCmd.AddCommand(loginCmd) rootCmd.AddCommand(versionCmd) rootCmd.AddCommand(sshCmd) + rootCmd.AddCommand(routesCmd) + serviceCmd.AddCommand(runCmd, startCmd, stopCmd, restartCmd) // service control commands are subcommands of service serviceCmd.AddCommand(installCmd, uninstallCmd) // service installer commands are subcommands of service + + routesCmd.AddCommand(routesListCmd) + routesCmd.AddCommand(routesSelectCmd, routesDeselectCmd) + upCmd.PersistentFlags().StringSliceVar(&natExternalIPs, externalIPMapFlag, nil, `Sets external IPs maps between local addresses and interfaces.`+ `You can specify a comma-separated list with a single IP and IP/IP or IP/Interface Name. `+ diff --git a/client/cmd/route.go b/client/cmd/route.go new file mode 100644 index 000000000..3d5d4b247 --- /dev/null +++ b/client/cmd/route.go @@ -0,0 +1,144 @@ +package cmd + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + "google.golang.org/grpc" + "google.golang.org/grpc/status" + + "github.com/netbirdio/netbird/client/proto" +) + +var appendFlag bool + +var routesCmd = &cobra.Command{ + Use: "routes", + Short: "Manage network routes", + Long: `Commands to list, select, or deselect network routes.`, +} + +var routesListCmd = &cobra.Command{ + Use: "list", + Aliases: []string{"ls"}, + Short: "List routes", + Example: " netbird routes list", + Long: "List all available network routes.", + RunE: routesList, +} + +var routesSelectCmd = &cobra.Command{ + Use: "select route...|all", + Short: "Select routes", + Long: "Select a list of routes by identifiers or 'all' to clear all selections and to accept all (including new) routes.\nDefault mode is replace, use -a to append to already selected routes.", + Example: " netbird routes select all\n netbird routes select route1 route2\n netbird routes select -a route3", + Args: cobra.MinimumNArgs(1), + RunE: routesSelect, +} + +var routesDeselectCmd = &cobra.Command{ + Use: "deselect route...|all", + Short: "Deselect routes", + Long: "Deselect previously selected routes by identifiers or 'all' to disable accepting any routes.", + Example: " netbird routes deselect all\n netbird routes deselect route1 route2", + Args: cobra.MinimumNArgs(1), + RunE: routesDeselect, +} + +func init() { + routesSelectCmd.PersistentFlags().BoolVarP(&appendFlag, "append", "a", false, "Append to current route selection instead of replacing") +} + +func routesList(cmd *cobra.Command, _ []string) error { + conn, err := getClient(cmd.Context()) + if err != nil { + return err + } + defer conn.Close() + + client := proto.NewDaemonServiceClient(conn) + resp, err := client.ListRoutes(cmd.Context(), &proto.ListRoutesRequest{}) + if err != nil { + return fmt.Errorf("failed to list routes: %v", status.Convert(err).Message()) + } + + if len(resp.Routes) == 0 { + cmd.Println("No routes available.") + return nil + } + + cmd.Println("Available Routes:") + for _, route := range resp.Routes { + selectedStatus := "Not Selected" + if route.GetSelected() { + selectedStatus = "Selected" + } + cmd.Printf("\n - ID: %s\n Network: %s\n Status: %s\n", route.GetID(), route.GetNetwork(), selectedStatus) + } + + return nil +} + +func routesSelect(cmd *cobra.Command, args []string) error { + conn, err := getClient(cmd.Context()) + if err != nil { + return err + } + defer conn.Close() + + client := proto.NewDaemonServiceClient(conn) + req := &proto.SelectRoutesRequest{ + RouteIDs: args, + } + + if len(args) == 1 && args[0] == "all" { + req.All = true + } else if appendFlag { + req.Append = true + } + + if _, err := client.SelectRoutes(cmd.Context(), req); err != nil { + return fmt.Errorf("failed to select routes: %v", status.Convert(err).Message()) + } + + cmd.Println("Routes selected successfully.") + + return nil +} + +func routesDeselect(cmd *cobra.Command, args []string) error { + conn, err := getClient(cmd.Context()) + if err != nil { + return err + } + defer conn.Close() + + client := proto.NewDaemonServiceClient(conn) + req := &proto.SelectRoutesRequest{ + RouteIDs: args, + } + + if len(args) == 1 && args[0] == "all" { + req.All = true + } + + if _, err := client.DeselectRoutes(cmd.Context(), req); err != nil { + return fmt.Errorf("failed to deselect routes: %v", status.Convert(err).Message()) + } + + cmd.Println("Routes deselected successfully.") + + return nil +} + +func getClient(ctx context.Context) (*grpc.ClientConn, error) { + conn, err := DialClientGRPCServer(ctx, daemonAddr) + if err != nil { + return nil, fmt.Errorf("failed to connect to daemon error: %v\n"+ + "If the daemon is not running please run: "+ + "\nnetbird service install \nnetbird service start\n", err) + } + + return conn, nil +} diff --git a/client/cmd/ssh.go b/client/cmd/ssh.go index dd9407738..81e6c255a 100644 --- a/client/cmd/ssh.go +++ b/client/cmd/ssh.go @@ -24,7 +24,7 @@ var ( ) var sshCmd = &cobra.Command{ - Use: "ssh", + Use: "ssh [user@]host", Args: func(cmd *cobra.Command, args []string) error { if len(args) < 1 { return errors.New("requires a host argument") @@ -94,7 +94,7 @@ func runSSH(ctx context.Context, addr string, pemKey []byte, cmd *cobra.Command) if err != nil { cmd.Printf("Error: %v\n", err) cmd.Printf("Couldn't connect. Please check the connection status or if the ssh server is enabled on the other peer" + - "You can verify the connection by running:\n\n" + + "\nYou can verify the connection by running:\n\n" + " netbird status\n\n") return err } diff --git a/client/internal/connect.go b/client/internal/connect.go index 6b888c9cc..c238fa31c 100644 --- a/client/internal/connect.go +++ b/client/internal/connect.go @@ -31,7 +31,7 @@ import ( // RunClient with main logic. func RunClient(ctx context.Context, config *Config, statusRecorder *peer.Status) error { - return runClient(ctx, config, statusRecorder, MobileDependency{}, nil, nil, nil, nil) + return runClient(ctx, config, statusRecorder, MobileDependency{}, nil, nil, nil, nil, nil) } // RunClientWithProbes runs the client's main logic with probes attached @@ -43,8 +43,9 @@ func RunClientWithProbes( signalProbe *Probe, relayProbe *Probe, wgProbe *Probe, + engineChan chan<- *Engine, ) error { - return runClient(ctx, config, statusRecorder, MobileDependency{}, mgmProbe, signalProbe, relayProbe, wgProbe) + return runClient(ctx, config, statusRecorder, MobileDependency{}, mgmProbe, signalProbe, relayProbe, wgProbe, engineChan) } // RunClientMobile with main logic on mobile system @@ -66,7 +67,7 @@ func RunClientMobile( HostDNSAddresses: dnsAddresses, DnsReadyListener: dnsReadyListener, } - return runClient(ctx, config, statusRecorder, mobileDependency, nil, nil, nil, nil) + return runClient(ctx, config, statusRecorder, mobileDependency, nil, nil, nil, nil, nil) } func RunClientiOS( @@ -82,7 +83,7 @@ func RunClientiOS( NetworkChangeListener: networkChangeListener, DnsManager: dnsManager, } - return runClient(ctx, config, statusRecorder, mobileDependency, nil, nil, nil, nil) + return runClient(ctx, config, statusRecorder, mobileDependency, nil, nil, nil, nil, nil) } func runClient( @@ -94,6 +95,7 @@ func runClient( signalProbe *Probe, relayProbe *Probe, wgProbe *Probe, + engineChan chan<- *Engine, ) error { defer func() { if r := recover(); r != nil { @@ -243,6 +245,9 @@ func runClient( log.Errorf("error while starting Netbird Connection Engine: %s", err) return wrapErr(err) } + if engineChan != nil { + engineChan <- engine + } log.Print("Netbird engine started, my IP is: ", peerConfig.Address) state.Set(StatusConnected) @@ -252,6 +257,10 @@ func runClient( backOff.Reset() + if engineChan != nil { + engineChan <- nil + } + err = engine.Stop() if err != nil { log.Errorf("failed stopping engine %v", err) diff --git a/client/internal/engine.go b/client/internal/engine.go index ba7074672..28e1f1b55 100644 --- a/client/internal/engine.go +++ b/client/internal/engine.go @@ -111,6 +111,9 @@ type Engine struct { // TURNs is a list of STUN servers used by ICE TURNs []*stun.URI + // clientRoutes is the most recent list of clientRoutes received from the Management Service + clientRoutes map[string][]*route.Route + cancel context.CancelFunc ctx context.Context @@ -216,6 +219,8 @@ func (e *Engine) Stop() error { return err } + e.clientRoutes = nil + // very ugly but we want to remove peers from the WireGuard interface first before removing interface. // Removing peers happens in the conn.CLose() asynchronously time.Sleep(500 * time.Millisecond) @@ -695,11 +700,14 @@ func (e *Engine) updateNetworkMap(networkMap *mgmProto.NetworkMap) error { if protoRoutes == nil { protoRoutes = []*mgmProto.Route{} } - err := e.routeManager.UpdateRoutes(serial, toRoutes(protoRoutes)) + + _, clientRoutes, err := e.routeManager.UpdateRoutes(serial, toRoutes(protoRoutes)) if err != nil { - log.Errorf("failed to update routes, err: %v", err) + log.Errorf("failed to update clientRoutes, err: %v", err) } + e.clientRoutes = clientRoutes + protoDNSConfig := networkMap.GetDNSConfig() if protoDNSConfig == nil { protoDNSConfig = &mgmProto.DNSConfig{} @@ -1229,6 +1237,28 @@ func (e *Engine) newDnsServer() ([]*route.Route, dns.Server, error) { } } +// GetClientRoutes returns the current routes from the route map +func (e *Engine) GetClientRoutes() map[string][]*route.Route { + return e.clientRoutes +} + +// GetClientRoutesWithNetID returns the current routes from the route map, but the keys consist of the network ID only +func (e *Engine) GetClientRoutesWithNetID() map[string][]*route.Route { + routes := make(map[string][]*route.Route, len(e.clientRoutes)) + for id, v := range e.clientRoutes { + if i := strings.LastIndex(id, "-"); i != -1 { + id = id[:i] + } + routes[id] = v + } + return routes +} + +// GetRouteManager returns the route manager +func (e *Engine) GetRouteManager() routemanager.Manager { + return e.routeManager +} + func findIPFromInterfaceName(ifaceName string) (net.IP, error) { iface, err := net.InterfaceByName(ifaceName) if err != nil { diff --git a/client/internal/engine_test.go b/client/internal/engine_test.go index 309b2e7c6..f487cc71e 100644 --- a/client/internal/engine_test.go +++ b/client/internal/engine_test.go @@ -22,6 +22,7 @@ import ( "google.golang.org/grpc/keepalive" "github.com/netbirdio/management-integrations/integrations" + "github.com/netbirdio/netbird/client/internal/dns" "github.com/netbirdio/netbird/client/internal/peer" "github.com/netbirdio/netbird/client/internal/routemanager" @@ -577,10 +578,10 @@ func TestEngine_UpdateNetworkMapWithRoutes(t *testing.T) { }{} mockRouteManager := &routemanager.MockManager{ - UpdateRoutesFunc: func(updateSerial uint64, newRoutes []*route.Route) error { + UpdateRoutesFunc: func(updateSerial uint64, newRoutes []*route.Route) (map[string]*route.Route, map[string][]*route.Route, error) { input.inputSerial = updateSerial input.inputRoutes = newRoutes - return testCase.inputErr + return nil, nil, testCase.inputErr }, } @@ -597,8 +598,8 @@ func TestEngine_UpdateNetworkMapWithRoutes(t *testing.T) { err = engine.updateNetworkMap(testCase.networkMap) assert.NoError(t, err, "shouldn't return error") assert.Equal(t, testCase.expectedSerial, input.inputSerial, "serial should match") - assert.Len(t, input.inputRoutes, testCase.expectedLen, "routes len should match") - assert.Equal(t, testCase.expectedRoutes, input.inputRoutes, "routes should match") + assert.Len(t, input.inputRoutes, testCase.expectedLen, "clientRoutes len should match") + assert.Equal(t, testCase.expectedRoutes, input.inputRoutes, "clientRoutes should match") }) } } @@ -742,8 +743,8 @@ func TestEngine_UpdateNetworkMapWithDNSUpdate(t *testing.T) { assert.NoError(t, err, "shouldn't return error") mockRouteManager := &routemanager.MockManager{ - UpdateRoutesFunc: func(updateSerial uint64, newRoutes []*route.Route) error { - return nil + UpdateRoutesFunc: func(updateSerial uint64, newRoutes []*route.Route) (map[string]*route.Route, map[string][]*route.Route, error) { + return nil, nil, nil }, } diff --git a/client/internal/routemanager/manager.go b/client/internal/routemanager/manager.go index 0dfc0f7e0..57007c4a3 100644 --- a/client/internal/routemanager/manager.go +++ b/client/internal/routemanager/manager.go @@ -14,6 +14,7 @@ import ( 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/routeselector" "github.com/netbirdio/netbird/iface" "github.com/netbirdio/netbird/route" nbnet "github.com/netbirdio/netbird/util/net" @@ -28,7 +29,9 @@ var defaultv6 = netip.PrefixFrom(netip.IPv6Unspecified(), 0) // Manager is a route manager interface type Manager interface { Init() (peer.BeforeAddPeerHookFunc, peer.AfterRemovePeerHookFunc, error) - UpdateRoutes(updateSerial uint64, newRoutes []*route.Route) error + UpdateRoutes(updateSerial uint64, newRoutes []*route.Route) (map[string]*route.Route, map[string][]*route.Route, error) + TriggerSelection(map[string][]*route.Route) + GetRouteSelector() *routeselector.RouteSelector SetRouteChangeListener(listener listener.NetworkChangeListener) InitialRouteRange() []string EnableServerRouter(firewall firewall.Manager) error @@ -41,6 +44,7 @@ type DefaultManager struct { stop context.CancelFunc mux sync.Mutex clientNetworks map[string]*clientNetwork + routeSelector *routeselector.RouteSelector serverRouter serverRouter statusRecorder *peer.Status wgInterface *iface.WGIface @@ -54,6 +58,7 @@ func NewManager(ctx context.Context, pubKey string, wgInterface *iface.WGIface, ctx: mCTX, stop: cancel, clientNetworks: make(map[string]*clientNetwork), + routeSelector: routeselector.NewRouteSelector(), statusRecorder: statusRecorder, wgInterface: wgInterface, pubKey: pubKey, @@ -117,28 +122,29 @@ func (m *DefaultManager) Stop() { } // UpdateRoutes compares received routes with existing routes and removes, updates or adds them to the client and server maps -func (m *DefaultManager) UpdateRoutes(updateSerial uint64, newRoutes []*route.Route) error { +func (m *DefaultManager) UpdateRoutes(updateSerial uint64, newRoutes []*route.Route) (map[string]*route.Route, map[string][]*route.Route, error) { select { case <-m.ctx.Done(): log.Infof("not updating routes as context is closed") - return m.ctx.Err() + return nil, nil, m.ctx.Err() default: m.mux.Lock() defer m.mux.Unlock() - newServerRoutesMap, newClientRoutesIDMap := m.classifiesRoutes(newRoutes) + newServerRoutesMap, newClientRoutesIDMap := m.classifyRoutes(newRoutes) - m.updateClientNetworks(updateSerial, newClientRoutesIDMap) - m.notifier.onNewRoutes(newClientRoutesIDMap) + filteredClientRoutes := m.routeSelector.FilterSelected(newClientRoutesIDMap) + m.updateClientNetworks(updateSerial, filteredClientRoutes) + m.notifier.onNewRoutes(filteredClientRoutes) if m.serverRouter != nil { err := m.serverRouter.updateRoutes(newServerRoutesMap) if err != nil { - return fmt.Errorf("update routes: %w", err) + return nil, nil, fmt.Errorf("update routes: %w", err) } } - return nil + return newServerRoutesMap, newClientRoutesIDMap, nil } } @@ -152,16 +158,51 @@ func (m *DefaultManager) InitialRouteRange() []string { return m.notifier.initialRouteRanges() } -func (m *DefaultManager) updateClientNetworks(updateSerial uint64, networks map[string][]*route.Route) { - // removing routes that do not exist as per the update from the Management service. +// GetRouteSelector returns the route selector +func (m *DefaultManager) GetRouteSelector() *routeselector.RouteSelector { + return m.routeSelector +} + +// GetClientRoutes returns the client routes +func (m *DefaultManager) GetClientRoutes() map[string]*clientNetwork { + return m.clientNetworks +} + +// TriggerSelection triggers the selection of routes, stopping deselected watchers and starting newly selected ones +func (m *DefaultManager) TriggerSelection(networks map[string][]*route.Route) { + m.mux.Lock() + defer m.mux.Unlock() + + networks = m.routeSelector.FilterSelected(networks) + m.stopObsoleteClients(networks) + + for id, routes := range networks { + if _, found := m.clientNetworks[id]; found { + // don't touch existing client network watchers + continue + } + + clientNetworkWatcher := newClientNetworkWatcher(m.ctx, m.wgInterface, m.statusRecorder, routes[0].Network) + m.clientNetworks[id] = clientNetworkWatcher + go clientNetworkWatcher.peersStateAndUpdateWatcher() + clientNetworkWatcher.sendUpdateToClientNetworkWatcher(routesUpdate{routes: routes}) + } +} + +// stopObsoleteClients stops the client network watcher for the networks that are not in the new list +func (m *DefaultManager) stopObsoleteClients(networks map[string][]*route.Route) { for id, client := range m.clientNetworks { - _, found := networks[id] - if !found { - log.Debugf("stopping client network watcher, %s", id) + if _, ok := networks[id]; !ok { + log.Debugf("Stopping client network watcher, %s", id) client.stop() delete(m.clientNetworks, id) } } +} + +func (m *DefaultManager) updateClientNetworks(updateSerial uint64, networks map[string][]*route.Route) { + // removing routes that do not exist as per the update from the Management service. + m.stopObsoleteClients(networks) for id, routes := range networks { clientNetworkWatcher, found := m.clientNetworks[id] @@ -178,7 +219,7 @@ func (m *DefaultManager) updateClientNetworks(updateSerial uint64, networks map[ } } -func (m *DefaultManager) classifiesRoutes(newRoutes []*route.Route) (map[string]*route.Route, map[string][]*route.Route) { +func (m *DefaultManager) classifyRoutes(newRoutes []*route.Route) (map[string]*route.Route, map[string][]*route.Route) { newClientRoutesIDMap := make(map[string][]*route.Route) newServerRoutesMap := make(map[string]*route.Route) ownNetworkIDs := make(map[string]bool) @@ -210,7 +251,7 @@ func (m *DefaultManager) classifiesRoutes(newRoutes []*route.Route) (map[string] } func (m *DefaultManager) clientRoutes(initialRoutes []*route.Route) []*route.Route { - _, crMap := m.classifiesRoutes(initialRoutes) + _, crMap := m.classifyRoutes(initialRoutes) rs := make([]*route.Route, 0) for _, routes := range crMap { rs = append(rs, routes...) diff --git a/client/internal/routemanager/manager_test.go b/client/internal/routemanager/manager_test.go index 03e77e09b..7eb8dd002 100644 --- a/client/internal/routemanager/manager_test.go +++ b/client/internal/routemanager/manager_test.go @@ -428,11 +428,11 @@ func TestManagerUpdateRoutes(t *testing.T) { } if len(testCase.inputInitRoutes) > 0 { - err = routeManager.UpdateRoutes(testCase.inputSerial, testCase.inputRoutes) + _, _, err = routeManager.UpdateRoutes(testCase.inputSerial, testCase.inputRoutes) require.NoError(t, err, "should update routes with init routes") } - err = routeManager.UpdateRoutes(testCase.inputSerial+uint64(len(testCase.inputInitRoutes)), testCase.inputRoutes) + _, _, err = routeManager.UpdateRoutes(testCase.inputSerial+uint64(len(testCase.inputInitRoutes)), testCase.inputRoutes) require.NoError(t, err, "should update routes") expectedWatchers := testCase.clientNetworkWatchersExpected diff --git a/client/internal/routemanager/mock.go b/client/internal/routemanager/mock.go index dd2c28e59..b3464018e 100644 --- a/client/internal/routemanager/mock.go +++ b/client/internal/routemanager/mock.go @@ -7,14 +7,17 @@ import ( 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/routeselector" "github.com/netbirdio/netbird/iface" "github.com/netbirdio/netbird/route" ) // MockManager is the mock instance of a route manager type MockManager struct { - UpdateRoutesFunc func(updateSerial uint64, newRoutes []*route.Route) error - StopFunc func() + UpdateRoutesFunc func(updateSerial uint64, newRoutes []*route.Route) (map[string]*route.Route, map[string][]*route.Route, error) + TriggerSelectionFunc func(map[string][]*route.Route) + GetRouteSelectorFunc func() *routeselector.RouteSelector + StopFunc func() } func (m *MockManager) Init() (peer.BeforeAddPeerHookFunc, peer.AfterRemovePeerHookFunc, error) { @@ -27,11 +30,25 @@ func (m *MockManager) InitialRouteRange() []string { } // UpdateRoutes mock implementation of UpdateRoutes from Manager interface -func (m *MockManager) UpdateRoutes(updateSerial uint64, newRoutes []*route.Route) error { +func (m *MockManager) UpdateRoutes(updateSerial uint64, newRoutes []*route.Route) (map[string]*route.Route, map[string][]*route.Route, error) { if m.UpdateRoutesFunc != nil { return m.UpdateRoutesFunc(updateSerial, newRoutes) } - return fmt.Errorf("method UpdateRoutes is not implemented") + return nil, nil, fmt.Errorf("method UpdateRoutes is not implemented") +} + +func (m *MockManager) TriggerSelection(networks map[string][]*route.Route) { + if m.TriggerSelectionFunc != nil { + m.TriggerSelectionFunc(networks) + } +} + +// GetRouteSelector mock implementation of GetRouteSelector from Manager interface +func (m *MockManager) GetRouteSelector() *routeselector.RouteSelector { + if m.GetRouteSelectorFunc != nil { + return m.GetRouteSelectorFunc() + } + return nil } // Start mock implementation of Start from Manager interface diff --git a/client/internal/routemanager/systemops_linux.go b/client/internal/routemanager/systemops_linux.go index d1302b39c..7c77c9fbb 100644 --- a/client/internal/routemanager/systemops_linux.go +++ b/client/internal/routemanager/systemops_linux.go @@ -304,7 +304,10 @@ func removeUnreachableRoute(prefix netip.Prefix, tableID int) error { Dst: ipNet, } - if err := netlink.RouteDel(route); err != nil && !errors.Is(err, syscall.ESRCH) && !errors.Is(err, syscall.EAFNOSUPPORT) { + if err := netlink.RouteDel(route); err != nil && + !errors.Is(err, syscall.ESRCH) && + !errors.Is(err, syscall.ENOENT) && + !errors.Is(err, syscall.EAFNOSUPPORT) { return fmt.Errorf("netlink remove unreachable route: %w", err) } diff --git a/client/internal/routeselector/routeselector.go b/client/internal/routeselector/routeselector.go new file mode 100644 index 000000000..7bd93b46e --- /dev/null +++ b/client/internal/routeselector/routeselector.go @@ -0,0 +1,132 @@ +package routeselector + +import ( + "fmt" + "slices" + "strings" + + "github.com/hashicorp/go-multierror" + "golang.org/x/exp/maps" + + route "github.com/netbirdio/netbird/route" +) + +type RouteSelector struct { + selectedRoutes map[string]struct{} + selectAll bool +} + +func NewRouteSelector() *RouteSelector { + return &RouteSelector{ + selectedRoutes: map[string]struct{}{}, + // default selects all routes + selectAll: true, + } +} + +// SelectRoutes updates the selected routes based on the provided route IDs. +func (rs *RouteSelector) SelectRoutes(routes []string, appendRoute bool, allRoutes []string) error { + if !appendRoute { + rs.selectedRoutes = map[string]struct{}{} + } + + var multiErr *multierror.Error + for _, route := range routes { + if !slices.Contains(allRoutes, route) { + multiErr = multierror.Append(multiErr, fmt.Errorf("route '%s' is not available", route)) + continue + } + + rs.selectedRoutes[route] = struct{}{} + } + rs.selectAll = false + + if multiErr != nil { + multiErr.ErrorFormat = formatError + } + + return multiErr.ErrorOrNil() +} + +// SelectAllRoutes sets the selector to select all routes. +func (rs *RouteSelector) SelectAllRoutes() { + rs.selectAll = true + rs.selectedRoutes = map[string]struct{}{} +} + +// DeselectRoutes removes specific routes from the selection. +// If the selector is in "select all" mode, it will transition to "select specific" mode. +func (rs *RouteSelector) DeselectRoutes(routes []string, allRoutes []string) error { + if rs.selectAll { + rs.selectAll = false + rs.selectedRoutes = map[string]struct{}{} + for _, route := range allRoutes { + rs.selectedRoutes[route] = struct{}{} + } + } + + var multiErr *multierror.Error + + for _, route := range routes { + if !slices.Contains(allRoutes, route) { + multiErr = multierror.Append(multiErr, fmt.Errorf("route '%s' is not available", route)) + continue + } + delete(rs.selectedRoutes, route) + } + + if multiErr != nil { + multiErr.ErrorFormat = formatError + } + + return multiErr.ErrorOrNil() +} + +// DeselectAllRoutes deselects all routes, effectively disabling route selection. +func (rs *RouteSelector) DeselectAllRoutes() { + rs.selectAll = false + rs.selectedRoutes = map[string]struct{}{} +} + +// IsSelected checks if a specific route is selected. +func (rs *RouteSelector) IsSelected(routeID string) bool { + if rs.selectAll { + return true + } + _, selected := rs.selectedRoutes[routeID] + return selected +} + +// FilterSelected removes unselected routes from the provided map. +func (rs *RouteSelector) FilterSelected(routes map[string][]*route.Route) map[string][]*route.Route { + if rs.selectAll { + return maps.Clone(routes) + } + + filtered := map[string][]*route.Route{} + for id, rt := range routes { + netID := id + if i := strings.LastIndex(id, "-"); i != -1 { + netID = id[:i] + } + if rs.IsSelected(netID) { + filtered[id] = rt + } + } + 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 new file mode 100644 index 000000000..b3d0547b5 --- /dev/null +++ b/client/internal/routeselector/routeselector_test.go @@ -0,0 +1,275 @@ +package routeselector_test + +import ( + "slices" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/netbirdio/netbird/client/internal/routeselector" + "github.com/netbirdio/netbird/route" +) + +func TestRouteSelector_SelectRoutes(t *testing.T) { + allRoutes := []string{"route1", "route2", "route3"} + + tests := []struct { + name string + initialSelected []string + + selectRoutes []string + append bool + + wantSelected []string + wantError bool + }{ + { + name: "Select specific routes, initial all selected", + selectRoutes: []string{"route1", "route2"}, + wantSelected: []string{"route1", "route2"}, + }, + { + name: "Select specific routes, initial all deselected", + initialSelected: []string{}, + selectRoutes: []string{"route1", "route2"}, + wantSelected: []string{"route1", "route2"}, + }, + { + name: "Select specific routes with initial selection", + initialSelected: []string{"route1"}, + selectRoutes: []string{"route2", "route3"}, + wantSelected: []string{"route2", "route3"}, + }, + { + name: "Select non-existing route", + selectRoutes: []string{"route1", "route4"}, + wantSelected: []string{"route1"}, + wantError: true, + }, + { + name: "Append route with initial selection", + initialSelected: []string{"route1"}, + selectRoutes: []string{"route2"}, + append: true, + wantSelected: []string{"route1", "route2"}, + }, + { + name: "Append route without initial selection", + selectRoutes: []string{"route2"}, + append: true, + wantSelected: []string{"route2"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rs := routeselector.NewRouteSelector() + + if tt.initialSelected != nil { + err := rs.SelectRoutes(tt.initialSelected, false, allRoutes) + require.NoError(t, err) + } + + err := rs.SelectRoutes(tt.selectRoutes, tt.append, allRoutes) + if tt.wantError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + for _, id := range allRoutes { + assert.Equal(t, rs.IsSelected(id), slices.Contains(tt.wantSelected, id)) + } + }) + } +} + +func TestRouteSelector_SelectAllRoutes(t *testing.T) { + allRoutes := []string{"route1", "route2", "route3"} + + tests := []struct { + name string + initialSelected []string + + wantSelected []string + }{ + { + name: "Initial all selected", + wantSelected: []string{"route1", "route2", "route3"}, + }, + { + name: "Initial all deselected", + initialSelected: []string{}, + wantSelected: []string{"route1", "route2", "route3"}, + }, + { + name: "Initial some selected", + initialSelected: []string{"route1"}, + wantSelected: []string{"route1", "route2", "route3"}, + }, + { + name: "Initial all selected", + initialSelected: []string{"route1", "route2", "route3"}, + wantSelected: []string{"route1", "route2", "route3"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rs := routeselector.NewRouteSelector() + + if tt.initialSelected != nil { + err := rs.SelectRoutes(tt.initialSelected, false, allRoutes) + require.NoError(t, err) + } + + rs.SelectAllRoutes() + + for _, id := range allRoutes { + assert.Equal(t, rs.IsSelected(id), slices.Contains(tt.wantSelected, id)) + } + }) + } +} + +func TestRouteSelector_DeselectRoutes(t *testing.T) { + allRoutes := []string{"route1", "route2", "route3"} + + tests := []struct { + name string + initialSelected []string + + deselectRoutes []string + + wantSelected []string + wantError bool + }{ + { + name: "Deselect specific routes, initial all selected", + deselectRoutes: []string{"route1", "route2"}, + wantSelected: []string{"route3"}, + }, + { + name: "Deselect specific routes, initial all deselected", + initialSelected: []string{}, + deselectRoutes: []string{"route1", "route2"}, + wantSelected: []string{}, + }, + { + name: "Deselect specific routes with initial selection", + initialSelected: []string{"route1", "route2"}, + deselectRoutes: []string{"route1", "route3"}, + wantSelected: []string{"route2"}, + }, + { + name: "Deselect non-existing route", + initialSelected: []string{"route1", "route2"}, + deselectRoutes: []string{"route1", "route4"}, + wantSelected: []string{"route2"}, + wantError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rs := routeselector.NewRouteSelector() + + if tt.initialSelected != nil { + err := rs.SelectRoutes(tt.initialSelected, false, allRoutes) + require.NoError(t, err) + } + + err := rs.DeselectRoutes(tt.deselectRoutes, allRoutes) + if tt.wantError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + for _, id := range allRoutes { + assert.Equal(t, rs.IsSelected(id), slices.Contains(tt.wantSelected, id)) + } + }) + } +} + +func TestRouteSelector_DeselectAll(t *testing.T) { + allRoutes := []string{"route1", "route2", "route3"} + + tests := []struct { + name string + initialSelected []string + + wantSelected []string + }{ + { + name: "Initial all selected", + wantSelected: []string{}, + }, + { + name: "Initial all deselected", + initialSelected: []string{}, + wantSelected: []string{}, + }, + { + name: "Initial some selected", + initialSelected: []string{"route1", "route2"}, + wantSelected: []string{}, + }, + { + name: "Initial all selected", + initialSelected: []string{"route1", "route2", "route3"}, + wantSelected: []string{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rs := routeselector.NewRouteSelector() + + if tt.initialSelected != nil { + err := rs.SelectRoutes(tt.initialSelected, false, allRoutes) + require.NoError(t, err) + } + + rs.DeselectAllRoutes() + + for _, id := range allRoutes { + assert.Equal(t, rs.IsSelected(id), slices.Contains(tt.wantSelected, id)) + } + }) + } +} + +func TestRouteSelector_IsSelected(t *testing.T) { + rs := routeselector.NewRouteSelector() + + err := rs.SelectRoutes([]string{"route1", "route2"}, false, []string{"route1", "route2", "route3"}) + require.NoError(t, err) + + assert.True(t, rs.IsSelected("route1")) + assert.True(t, rs.IsSelected("route2")) + assert.False(t, rs.IsSelected("route3")) + assert.False(t, rs.IsSelected("route4")) +} + +func TestRouteSelector_FilterSelected(t *testing.T) { + rs := routeselector.NewRouteSelector() + + err := rs.SelectRoutes([]string{"route1", "route2"}, false, []string{"route1", "route2", "route3"}) + require.NoError(t, err) + + routes := map[string][]*route.Route{ + "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, map[string][]*route.Route{ + "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 4b8502268..fbb754fc6 100644 --- a/client/proto/daemon.pb.go +++ b/client/proto/daemon.pb.go @@ -1,17 +1,17 @@ // 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: daemon.proto package proto import ( + _ "github.com/golang/protobuf/protoc-gen-go/descriptor" + duration "github.com/golang/protobuf/ptypes/duration" + timestamp "github.com/golang/protobuf/ptypes/timestamp" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" - _ "google.golang.org/protobuf/types/descriptorpb" - durationpb "google.golang.org/protobuf/types/known/durationpb" - timestamppb "google.golang.org/protobuf/types/known/timestamppb" reflect "reflect" sync "sync" ) @@ -766,23 +766,23 @@ type PeerState struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - IP string `protobuf:"bytes,1,opt,name=IP,proto3" json:"IP,omitempty"` - PubKey string `protobuf:"bytes,2,opt,name=pubKey,proto3" json:"pubKey,omitempty"` - ConnStatus string `protobuf:"bytes,3,opt,name=connStatus,proto3" json:"connStatus,omitempty"` - ConnStatusUpdate *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=connStatusUpdate,proto3" json:"connStatusUpdate,omitempty"` - Relayed bool `protobuf:"varint,5,opt,name=relayed,proto3" json:"relayed,omitempty"` - Direct bool `protobuf:"varint,6,opt,name=direct,proto3" json:"direct,omitempty"` - LocalIceCandidateType string `protobuf:"bytes,7,opt,name=localIceCandidateType,proto3" json:"localIceCandidateType,omitempty"` - RemoteIceCandidateType string `protobuf:"bytes,8,opt,name=remoteIceCandidateType,proto3" json:"remoteIceCandidateType,omitempty"` - Fqdn string `protobuf:"bytes,9,opt,name=fqdn,proto3" json:"fqdn,omitempty"` - LocalIceCandidateEndpoint string `protobuf:"bytes,10,opt,name=localIceCandidateEndpoint,proto3" json:"localIceCandidateEndpoint,omitempty"` - RemoteIceCandidateEndpoint string `protobuf:"bytes,11,opt,name=remoteIceCandidateEndpoint,proto3" json:"remoteIceCandidateEndpoint,omitempty"` - LastWireguardHandshake *timestamppb.Timestamp `protobuf:"bytes,12,opt,name=lastWireguardHandshake,proto3" json:"lastWireguardHandshake,omitempty"` - BytesRx int64 `protobuf:"varint,13,opt,name=bytesRx,proto3" json:"bytesRx,omitempty"` - BytesTx int64 `protobuf:"varint,14,opt,name=bytesTx,proto3" json:"bytesTx,omitempty"` - RosenpassEnabled bool `protobuf:"varint,15,opt,name=rosenpassEnabled,proto3" json:"rosenpassEnabled,omitempty"` - Routes []string `protobuf:"bytes,16,rep,name=routes,proto3" json:"routes,omitempty"` - Latency *durationpb.Duration `protobuf:"bytes,17,opt,name=latency,proto3" json:"latency,omitempty"` + IP string `protobuf:"bytes,1,opt,name=IP,proto3" json:"IP,omitempty"` + PubKey string `protobuf:"bytes,2,opt,name=pubKey,proto3" json:"pubKey,omitempty"` + ConnStatus string `protobuf:"bytes,3,opt,name=connStatus,proto3" json:"connStatus,omitempty"` + ConnStatusUpdate *timestamp.Timestamp `protobuf:"bytes,4,opt,name=connStatusUpdate,proto3" json:"connStatusUpdate,omitempty"` + Relayed bool `protobuf:"varint,5,opt,name=relayed,proto3" json:"relayed,omitempty"` + Direct bool `protobuf:"varint,6,opt,name=direct,proto3" json:"direct,omitempty"` + LocalIceCandidateType string `protobuf:"bytes,7,opt,name=localIceCandidateType,proto3" json:"localIceCandidateType,omitempty"` + RemoteIceCandidateType string `protobuf:"bytes,8,opt,name=remoteIceCandidateType,proto3" json:"remoteIceCandidateType,omitempty"` + Fqdn string `protobuf:"bytes,9,opt,name=fqdn,proto3" json:"fqdn,omitempty"` + LocalIceCandidateEndpoint string `protobuf:"bytes,10,opt,name=localIceCandidateEndpoint,proto3" json:"localIceCandidateEndpoint,omitempty"` + RemoteIceCandidateEndpoint string `protobuf:"bytes,11,opt,name=remoteIceCandidateEndpoint,proto3" json:"remoteIceCandidateEndpoint,omitempty"` + LastWireguardHandshake *timestamp.Timestamp `protobuf:"bytes,12,opt,name=lastWireguardHandshake,proto3" json:"lastWireguardHandshake,omitempty"` + BytesRx int64 `protobuf:"varint,13,opt,name=bytesRx,proto3" json:"bytesRx,omitempty"` + BytesTx int64 `protobuf:"varint,14,opt,name=bytesTx,proto3" json:"bytesTx,omitempty"` + RosenpassEnabled bool `protobuf:"varint,15,opt,name=rosenpassEnabled,proto3" json:"rosenpassEnabled,omitempty"` + Routes []string `protobuf:"bytes,16,rep,name=routes,proto3" json:"routes,omitempty"` + Latency *duration.Duration `protobuf:"bytes,17,opt,name=latency,proto3" json:"latency,omitempty"` } func (x *PeerState) Reset() { @@ -838,7 +838,7 @@ func (x *PeerState) GetConnStatus() string { return "" } -func (x *PeerState) GetConnStatusUpdate() *timestamppb.Timestamp { +func (x *PeerState) GetConnStatusUpdate() *timestamp.Timestamp { if x != nil { return x.ConnStatusUpdate } @@ -894,7 +894,7 @@ func (x *PeerState) GetRemoteIceCandidateEndpoint() string { return "" } -func (x *PeerState) GetLastWireguardHandshake() *timestamppb.Timestamp { +func (x *PeerState) GetLastWireguardHandshake() *timestamp.Timestamp { if x != nil { return x.LastWireguardHandshake } @@ -929,7 +929,7 @@ func (x *PeerState) GetRoutes() []string { return nil } -func (x *PeerState) GetLatency() *durationpb.Duration { +func (x *PeerState) GetLatency() *duration.Duration { if x != nil { return x.Latency } @@ -1383,6 +1383,255 @@ func (x *FullStatus) GetDnsServers() []*NSGroupState { return nil } +type ListRoutesRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *ListRoutesRequest) Reset() { + *x = ListRoutesRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_daemon_proto_msgTypes[19] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ListRoutesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListRoutesRequest) ProtoMessage() {} + +func (x *ListRoutesRequest) ProtoReflect() protoreflect.Message { + mi := &file_daemon_proto_msgTypes[19] + 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 ListRoutesRequest.ProtoReflect.Descriptor instead. +func (*ListRoutesRequest) Descriptor() ([]byte, []int) { + return file_daemon_proto_rawDescGZIP(), []int{19} +} + +type ListRoutesResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Routes []*Route `protobuf:"bytes,1,rep,name=routes,proto3" json:"routes,omitempty"` +} + +func (x *ListRoutesResponse) Reset() { + *x = ListRoutesResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_daemon_proto_msgTypes[20] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ListRoutesResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListRoutesResponse) ProtoMessage() {} + +func (x *ListRoutesResponse) ProtoReflect() protoreflect.Message { + mi := &file_daemon_proto_msgTypes[20] + 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 ListRoutesResponse.ProtoReflect.Descriptor instead. +func (*ListRoutesResponse) Descriptor() ([]byte, []int) { + return file_daemon_proto_rawDescGZIP(), []int{20} +} + +func (x *ListRoutesResponse) GetRoutes() []*Route { + if x != nil { + return x.Routes + } + return nil +} + +type SelectRoutesRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + RouteIDs []string `protobuf:"bytes,1,rep,name=routeIDs,proto3" json:"routeIDs,omitempty"` + Append bool `protobuf:"varint,2,opt,name=append,proto3" json:"append,omitempty"` + All bool `protobuf:"varint,3,opt,name=all,proto3" json:"all,omitempty"` +} + +func (x *SelectRoutesRequest) Reset() { + *x = SelectRoutesRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_daemon_proto_msgTypes[21] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SelectRoutesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SelectRoutesRequest) ProtoMessage() {} + +func (x *SelectRoutesRequest) ProtoReflect() protoreflect.Message { + mi := &file_daemon_proto_msgTypes[21] + 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 SelectRoutesRequest.ProtoReflect.Descriptor instead. +func (*SelectRoutesRequest) Descriptor() ([]byte, []int) { + return file_daemon_proto_rawDescGZIP(), []int{21} +} + +func (x *SelectRoutesRequest) GetRouteIDs() []string { + if x != nil { + return x.RouteIDs + } + return nil +} + +func (x *SelectRoutesRequest) GetAppend() bool { + if x != nil { + return x.Append + } + return false +} + +func (x *SelectRoutesRequest) GetAll() bool { + if x != nil { + return x.All + } + return false +} + +type SelectRoutesResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *SelectRoutesResponse) Reset() { + *x = SelectRoutesResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_daemon_proto_msgTypes[22] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SelectRoutesResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SelectRoutesResponse) ProtoMessage() {} + +func (x *SelectRoutesResponse) ProtoReflect() protoreflect.Message { + mi := &file_daemon_proto_msgTypes[22] + 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 SelectRoutesResponse.ProtoReflect.Descriptor instead. +func (*SelectRoutesResponse) Descriptor() ([]byte, []int) { + return file_daemon_proto_rawDescGZIP(), []int{22} +} + +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"` +} + +func (x *Route) Reset() { + *x = Route{} + if protoimpl.UnsafeEnabled { + mi := &file_daemon_proto_msgTypes[23] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Route) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Route) ProtoMessage() {} + +func (x *Route) 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 Route.ProtoReflect.Descriptor instead. +func (*Route) Descriptor() ([]byte, []int) { + return file_daemon_proto_rawDescGZIP(), []int{23} +} + +func (x *Route) GetID() string { + if x != nil { + return x.ID + } + return "" +} + +func (x *Route) GetNetwork() string { + if x != nil { + return x.Network + } + return "" +} + +func (x *Route) GetSelected() bool { + if x != nil { + return x.Selected + } + return false +} + var File_daemon_proto protoreflect.FileDescriptor var file_daemon_proto_rawDesc = []byte{ @@ -1601,32 +1850,64 @@ var file_daemon_proto_rawDesc = []byte{ 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, 0x32, 0xf7, 0x02, - 0x0a, 0x0d, 0x44, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, - 0x36, 0x0a, 0x05, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x14, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, - 0x6e, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x15, - 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 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, 0x42, 0x08, 0x5a, 0x06, 0x2f, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 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, 0x32, 0xda, 0x04, 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, + 0x42, 0x08, 0x5a, 0x06, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x33, } var ( @@ -1641,58 +1922,70 @@ func file_daemon_proto_rawDescGZIP() []byte { return file_daemon_proto_rawDescData } -var file_daemon_proto_msgTypes = make([]protoimpl.MessageInfo, 19) +var file_daemon_proto_msgTypes = make([]protoimpl.MessageInfo, 24) var file_daemon_proto_goTypes = []interface{}{ - (*LoginRequest)(nil), // 0: daemon.LoginRequest - (*LoginResponse)(nil), // 1: daemon.LoginResponse - (*WaitSSOLoginRequest)(nil), // 2: daemon.WaitSSOLoginRequest - (*WaitSSOLoginResponse)(nil), // 3: daemon.WaitSSOLoginResponse - (*UpRequest)(nil), // 4: daemon.UpRequest - (*UpResponse)(nil), // 5: daemon.UpResponse - (*StatusRequest)(nil), // 6: daemon.StatusRequest - (*StatusResponse)(nil), // 7: daemon.StatusResponse - (*DownRequest)(nil), // 8: daemon.DownRequest - (*DownResponse)(nil), // 9: daemon.DownResponse - (*GetConfigRequest)(nil), // 10: daemon.GetConfigRequest - (*GetConfigResponse)(nil), // 11: daemon.GetConfigResponse - (*PeerState)(nil), // 12: daemon.PeerState - (*LocalPeerState)(nil), // 13: daemon.LocalPeerState - (*SignalState)(nil), // 14: daemon.SignalState - (*ManagementState)(nil), // 15: daemon.ManagementState - (*RelayState)(nil), // 16: daemon.RelayState - (*NSGroupState)(nil), // 17: daemon.NSGroupState - (*FullStatus)(nil), // 18: daemon.FullStatus - (*timestamppb.Timestamp)(nil), // 19: google.protobuf.Timestamp - (*durationpb.Duration)(nil), // 20: google.protobuf.Duration + (*LoginRequest)(nil), // 0: daemon.LoginRequest + (*LoginResponse)(nil), // 1: daemon.LoginResponse + (*WaitSSOLoginRequest)(nil), // 2: daemon.WaitSSOLoginRequest + (*WaitSSOLoginResponse)(nil), // 3: daemon.WaitSSOLoginResponse + (*UpRequest)(nil), // 4: daemon.UpRequest + (*UpResponse)(nil), // 5: daemon.UpResponse + (*StatusRequest)(nil), // 6: daemon.StatusRequest + (*StatusResponse)(nil), // 7: daemon.StatusResponse + (*DownRequest)(nil), // 8: daemon.DownRequest + (*DownResponse)(nil), // 9: daemon.DownResponse + (*GetConfigRequest)(nil), // 10: daemon.GetConfigRequest + (*GetConfigResponse)(nil), // 11: daemon.GetConfigResponse + (*PeerState)(nil), // 12: daemon.PeerState + (*LocalPeerState)(nil), // 13: daemon.LocalPeerState + (*SignalState)(nil), // 14: daemon.SignalState + (*ManagementState)(nil), // 15: daemon.ManagementState + (*RelayState)(nil), // 16: daemon.RelayState + (*NSGroupState)(nil), // 17: daemon.NSGroupState + (*FullStatus)(nil), // 18: daemon.FullStatus + (*ListRoutesRequest)(nil), // 19: daemon.ListRoutesRequest + (*ListRoutesResponse)(nil), // 20: daemon.ListRoutesResponse + (*SelectRoutesRequest)(nil), // 21: daemon.SelectRoutesRequest + (*SelectRoutesResponse)(nil), // 22: daemon.SelectRoutesResponse + (*Route)(nil), // 23: daemon.Route + (*timestamp.Timestamp)(nil), // 24: google.protobuf.Timestamp + (*duration.Duration)(nil), // 25: google.protobuf.Duration } var file_daemon_proto_depIdxs = []int32{ 18, // 0: daemon.StatusResponse.fullStatus:type_name -> daemon.FullStatus - 19, // 1: daemon.PeerState.connStatusUpdate:type_name -> google.protobuf.Timestamp - 19, // 2: daemon.PeerState.lastWireguardHandshake:type_name -> google.protobuf.Timestamp - 20, // 3: daemon.PeerState.latency:type_name -> google.protobuf.Duration + 24, // 1: daemon.PeerState.connStatusUpdate:type_name -> google.protobuf.Timestamp + 24, // 2: daemon.PeerState.lastWireguardHandshake:type_name -> google.protobuf.Timestamp + 25, // 3: daemon.PeerState.latency:type_name -> google.protobuf.Duration 15, // 4: daemon.FullStatus.managementState:type_name -> daemon.ManagementState 14, // 5: daemon.FullStatus.signalState:type_name -> daemon.SignalState 13, // 6: daemon.FullStatus.localPeerState:type_name -> daemon.LocalPeerState 12, // 7: daemon.FullStatus.peers:type_name -> daemon.PeerState 16, // 8: daemon.FullStatus.relays:type_name -> daemon.RelayState 17, // 9: daemon.FullStatus.dns_servers:type_name -> daemon.NSGroupState - 0, // 10: daemon.DaemonService.Login:input_type -> daemon.LoginRequest - 2, // 11: daemon.DaemonService.WaitSSOLogin:input_type -> daemon.WaitSSOLoginRequest - 4, // 12: daemon.DaemonService.Up:input_type -> daemon.UpRequest - 6, // 13: daemon.DaemonService.Status:input_type -> daemon.StatusRequest - 8, // 14: daemon.DaemonService.Down:input_type -> daemon.DownRequest - 10, // 15: daemon.DaemonService.GetConfig:input_type -> daemon.GetConfigRequest - 1, // 16: daemon.DaemonService.Login:output_type -> daemon.LoginResponse - 3, // 17: daemon.DaemonService.WaitSSOLogin:output_type -> daemon.WaitSSOLoginResponse - 5, // 18: daemon.DaemonService.Up:output_type -> daemon.UpResponse - 7, // 19: daemon.DaemonService.Status:output_type -> daemon.StatusResponse - 9, // 20: daemon.DaemonService.Down:output_type -> daemon.DownResponse - 11, // 21: daemon.DaemonService.GetConfig:output_type -> daemon.GetConfigResponse - 16, // [16:22] is the sub-list for method output_type - 10, // [10:16] is the sub-list for method input_type - 10, // [10:10] is the sub-list for extension type_name - 10, // [10:10] is the sub-list for extension extendee - 0, // [0:10] is the sub-list for field type_name + 23, // 10: daemon.ListRoutesResponse.routes:type_name -> daemon.Route + 0, // 11: daemon.DaemonService.Login:input_type -> daemon.LoginRequest + 2, // 12: daemon.DaemonService.WaitSSOLogin:input_type -> daemon.WaitSSOLoginRequest + 4, // 13: daemon.DaemonService.Up:input_type -> daemon.UpRequest + 6, // 14: daemon.DaemonService.Status:input_type -> daemon.StatusRequest + 8, // 15: daemon.DaemonService.Down:input_type -> daemon.DownRequest + 10, // 16: daemon.DaemonService.GetConfig:input_type -> daemon.GetConfigRequest + 19, // 17: daemon.DaemonService.ListRoutes:input_type -> daemon.ListRoutesRequest + 21, // 18: daemon.DaemonService.SelectRoutes:input_type -> daemon.SelectRoutesRequest + 21, // 19: daemon.DaemonService.DeselectRoutes:input_type -> daemon.SelectRoutesRequest + 1, // 20: daemon.DaemonService.Login:output_type -> daemon.LoginResponse + 3, // 21: daemon.DaemonService.WaitSSOLogin:output_type -> daemon.WaitSSOLoginResponse + 5, // 22: daemon.DaemonService.Up:output_type -> daemon.UpResponse + 7, // 23: daemon.DaemonService.Status:output_type -> daemon.StatusResponse + 9, // 24: daemon.DaemonService.Down:output_type -> daemon.DownResponse + 11, // 25: daemon.DaemonService.GetConfig:output_type -> daemon.GetConfigResponse + 20, // 26: daemon.DaemonService.ListRoutes:output_type -> daemon.ListRoutesResponse + 22, // 27: daemon.DaemonService.SelectRoutes:output_type -> daemon.SelectRoutesResponse + 22, // 28: daemon.DaemonService.DeselectRoutes:output_type -> daemon.SelectRoutesResponse + 20, // [20:29] is the sub-list for method output_type + 11, // [11:20] is the sub-list for method input_type + 11, // [11:11] is the sub-list for extension type_name + 11, // [11:11] is the sub-list for extension extendee + 0, // [0:11] is the sub-list for field type_name } func init() { file_daemon_proto_init() } @@ -1929,6 +2222,66 @@ func file_daemon_proto_init() { return nil } } + file_daemon_proto_msgTypes[19].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ListRoutesRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_daemon_proto_msgTypes[20].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ListRoutesResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_daemon_proto_msgTypes[21].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SelectRoutesRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_daemon_proto_msgTypes[22].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SelectRoutesResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_daemon_proto_msgTypes[23].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Route); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } } file_daemon_proto_msgTypes[0].OneofWrappers = []interface{}{} type x struct{} @@ -1937,7 +2290,7 @@ func file_daemon_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_daemon_proto_rawDesc, NumEnums: 0, - NumMessages: 19, + NumMessages: 24, NumExtensions: 0, NumServices: 1, }, diff --git a/client/proto/daemon.proto b/client/proto/daemon.proto index 5f8878a11..31ef4abc7 100644 --- a/client/proto/daemon.proto +++ b/client/proto/daemon.proto @@ -27,6 +27,15 @@ service DaemonService { // GetConfig of the daemon. rpc GetConfig(GetConfigRequest) returns (GetConfigResponse) {} + + // List available network routes + rpc ListRoutes(ListRoutesRequest) returns (ListRoutesResponse) {} + + // Select specific routes + rpc SelectRoutes(SelectRoutesRequest) returns (SelectRoutesResponse) {} + + // Deselect specific routes + rpc DeselectRoutes(SelectRoutesRequest) returns (SelectRoutesResponse) {} }; message LoginRequest { @@ -195,4 +204,26 @@ message FullStatus { repeated PeerState peers = 4; repeated RelayState relays = 5; repeated NSGroupState dns_servers = 6; +} + +message ListRoutesRequest { +} + +message ListRoutesResponse { + repeated Route routes = 1; +} + +message SelectRoutesRequest { + repeated string routeIDs = 1; + bool append = 2; + bool all = 3; +} + +message SelectRoutesResponse { +} + +message Route { + string ID = 1; + string network = 2; + bool selected = 3; } \ No newline at end of file diff --git a/client/proto/daemon_grpc.pb.go b/client/proto/daemon_grpc.pb.go index 0b339fab2..d149ee6cd 100644 --- a/client/proto/daemon_grpc.pb.go +++ b/client/proto/daemon_grpc.pb.go @@ -31,6 +31,12 @@ type DaemonServiceClient interface { Down(ctx context.Context, in *DownRequest, opts ...grpc.CallOption) (*DownResponse, error) // GetConfig of the daemon. GetConfig(ctx context.Context, in *GetConfigRequest, opts ...grpc.CallOption) (*GetConfigResponse, error) + // List available network routes + ListRoutes(ctx context.Context, in *ListRoutesRequest, opts ...grpc.CallOption) (*ListRoutesResponse, error) + // Select specific routes + SelectRoutes(ctx context.Context, in *SelectRoutesRequest, opts ...grpc.CallOption) (*SelectRoutesResponse, error) + // Deselect specific routes + DeselectRoutes(ctx context.Context, in *SelectRoutesRequest, opts ...grpc.CallOption) (*SelectRoutesResponse, error) } type daemonServiceClient struct { @@ -95,6 +101,33 @@ func (c *daemonServiceClient) GetConfig(ctx context.Context, in *GetConfigReques return out, nil } +func (c *daemonServiceClient) ListRoutes(ctx context.Context, in *ListRoutesRequest, opts ...grpc.CallOption) (*ListRoutesResponse, error) { + out := new(ListRoutesResponse) + err := c.cc.Invoke(ctx, "/daemon.DaemonService/ListRoutes", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *daemonServiceClient) SelectRoutes(ctx context.Context, in *SelectRoutesRequest, opts ...grpc.CallOption) (*SelectRoutesResponse, error) { + out := new(SelectRoutesResponse) + err := c.cc.Invoke(ctx, "/daemon.DaemonService/SelectRoutes", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *daemonServiceClient) DeselectRoutes(ctx context.Context, in *SelectRoutesRequest, opts ...grpc.CallOption) (*SelectRoutesResponse, error) { + out := new(SelectRoutesResponse) + err := c.cc.Invoke(ctx, "/daemon.DaemonService/DeselectRoutes", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + // DaemonServiceServer is the server API for DaemonService service. // All implementations must embed UnimplementedDaemonServiceServer // for forward compatibility @@ -112,6 +145,12 @@ type DaemonServiceServer interface { Down(context.Context, *DownRequest) (*DownResponse, error) // GetConfig of the daemon. GetConfig(context.Context, *GetConfigRequest) (*GetConfigResponse, error) + // List available network routes + ListRoutes(context.Context, *ListRoutesRequest) (*ListRoutesResponse, error) + // Select specific routes + SelectRoutes(context.Context, *SelectRoutesRequest) (*SelectRoutesResponse, error) + // Deselect specific routes + DeselectRoutes(context.Context, *SelectRoutesRequest) (*SelectRoutesResponse, error) mustEmbedUnimplementedDaemonServiceServer() } @@ -137,6 +176,15 @@ func (UnimplementedDaemonServiceServer) Down(context.Context, *DownRequest) (*Do func (UnimplementedDaemonServiceServer) GetConfig(context.Context, *GetConfigRequest) (*GetConfigResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method GetConfig not implemented") } +func (UnimplementedDaemonServiceServer) ListRoutes(context.Context, *ListRoutesRequest) (*ListRoutesResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method ListRoutes not implemented") +} +func (UnimplementedDaemonServiceServer) SelectRoutes(context.Context, *SelectRoutesRequest) (*SelectRoutesResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method SelectRoutes not implemented") +} +func (UnimplementedDaemonServiceServer) DeselectRoutes(context.Context, *SelectRoutesRequest) (*SelectRoutesResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method DeselectRoutes not implemented") +} func (UnimplementedDaemonServiceServer) mustEmbedUnimplementedDaemonServiceServer() {} // UnsafeDaemonServiceServer may be embedded to opt out of forward compatibility for this service. @@ -258,6 +306,60 @@ func _DaemonService_GetConfig_Handler(srv interface{}, ctx context.Context, dec return interceptor(ctx, in, info, handler) } +func _DaemonService_ListRoutes_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListRoutesRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DaemonServiceServer).ListRoutes(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/daemon.DaemonService/ListRoutes", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DaemonServiceServer).ListRoutes(ctx, req.(*ListRoutesRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _DaemonService_SelectRoutes_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SelectRoutesRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DaemonServiceServer).SelectRoutes(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/daemon.DaemonService/SelectRoutes", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DaemonServiceServer).SelectRoutes(ctx, req.(*SelectRoutesRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _DaemonService_DeselectRoutes_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SelectRoutesRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DaemonServiceServer).DeselectRoutes(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/daemon.DaemonService/DeselectRoutes", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DaemonServiceServer).DeselectRoutes(ctx, req.(*SelectRoutesRequest)) + } + return interceptor(ctx, in, info, handler) +} + // DaemonService_ServiceDesc is the grpc.ServiceDesc for DaemonService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -289,6 +391,18 @@ var DaemonService_ServiceDesc = grpc.ServiceDesc{ MethodName: "GetConfig", Handler: _DaemonService_GetConfig_Handler, }, + { + MethodName: "ListRoutes", + Handler: _DaemonService_ListRoutes_Handler, + }, + { + MethodName: "SelectRoutes", + Handler: _DaemonService_SelectRoutes_Handler, + }, + { + MethodName: "DeselectRoutes", + Handler: _DaemonService_DeselectRoutes_Handler, + }, }, Streams: []grpc.StreamDesc{}, Metadata: "daemon.proto", diff --git a/client/server/route.go b/client/server/route.go new file mode 100644 index 000000000..4aa37dbb7 --- /dev/null +++ b/client/server/route.go @@ -0,0 +1,100 @@ +package server + +import ( + "context" + "fmt" + "sort" + + "golang.org/x/exp/maps" + + "github.com/netbirdio/netbird/client/proto" + "github.com/netbirdio/netbird/route" +) + +// ListRoutes returns a list of all available routes. +func (s *Server) ListRoutes(ctx context.Context, req *proto.ListRoutesRequest) (*proto.ListRoutesResponse, error) { + s.mutex.Lock() + defer s.mutex.Unlock() + + if s.engine == nil { + return nil, fmt.Errorf("not connected") + } + + routesMap := s.engine.GetClientRoutesWithNetID() + routeSelector := s.engine.GetRouteManager().GetRouteSelector() + + var routes []*route.Route + for id, rt := range routesMap { + if len(rt) == 0 { + continue + } + rt[0].ID = id + routes = append(routes, rt[0]) + } + + sort.Slice(routes, func(i, j int) bool { + iPrefix := routes[i].Network.Bits() + jPrefix := routes[j].Network.Bits() + + if iPrefix == jPrefix { + iAddr := routes[i].Network.Addr() + jAddr := routes[j].Network.Addr() + if iAddr == jAddr { + return routes[i].ID < routes[j].ID + } + return iAddr.String() < jAddr.String() + } + return iPrefix < jPrefix + }) + + var pbRoutes []*proto.Route + for _, route := range routes { + pbRoutes = append(pbRoutes, &proto.Route{ + ID: route.ID, + Network: route.Network.String(), + Selected: routeSelector.IsSelected(route.ID), + }) + } + + return &proto.ListRoutesResponse{ + Routes: pbRoutes, + }, nil +} + +// SelectRoutes selects specific routes based on the client request. +func (s *Server) SelectRoutes(_ context.Context, req *proto.SelectRoutesRequest) (*proto.SelectRoutesResponse, error) { + s.mutex.Lock() + defer s.mutex.Unlock() + + routeManager := s.engine.GetRouteManager() + routeSelector := routeManager.GetRouteSelector() + if req.GetAll() { + routeSelector.SelectAllRoutes() + } else { + if err := routeSelector.SelectRoutes(req.GetRouteIDs(), req.GetAppend(), maps.Keys(s.engine.GetClientRoutesWithNetID())); err != nil { + return nil, fmt.Errorf("select routes: %w", err) + } + } + routeManager.TriggerSelection(s.engine.GetClientRoutes()) + + return &proto.SelectRoutesResponse{}, nil +} + +// DeselectRoutes deselects specific routes based on the client request. +func (s *Server) DeselectRoutes(_ context.Context, req *proto.SelectRoutesRequest) (*proto.SelectRoutesResponse, error) { + s.mutex.Lock() + defer s.mutex.Unlock() + + routeManager := s.engine.GetRouteManager() + routeSelector := routeManager.GetRouteSelector() + if req.GetAll() { + routeSelector.DeselectAllRoutes() + } else { + if err := routeSelector.DeselectRoutes(req.GetRouteIDs(), maps.Keys(s.engine.GetClientRoutesWithNetID())); err != nil { + return nil, fmt.Errorf("deselect routes: %w", err) + } + } + routeManager.TriggerSelection(s.engine.GetClientRoutes()) + + return &proto.SelectRoutesResponse{}, nil +} diff --git a/client/server/server.go b/client/server/server.go index d33bb5155..e0e9504fa 100644 --- a/client/server/server.go +++ b/client/server/server.go @@ -15,15 +15,15 @@ import ( "google.golang.org/protobuf/types/known/durationpb" - "github.com/netbirdio/netbird/client/internal/auth" - "github.com/netbirdio/netbird/client/system" - log "github.com/sirupsen/logrus" "google.golang.org/grpc/codes" "google.golang.org/grpc/metadata" gstatus "google.golang.org/grpc/status" "google.golang.org/protobuf/types/known/timestamppb" + "github.com/netbirdio/netbird/client/internal/auth" + "github.com/netbirdio/netbird/client/system" + "github.com/netbirdio/netbird/client/internal" "github.com/netbirdio/netbird/client/internal/peer" "github.com/netbirdio/netbird/client/proto" @@ -57,6 +57,8 @@ type Server struct { config *internal.Config proto.UnimplementedDaemonServiceServer + engine *internal.Engine + statusRecorder *peer.Status sessionWatcher *internal.SessionWatcher @@ -141,8 +143,11 @@ func (s *Server) Start() error { s.sessionWatcher.SetOnExpireListener(s.onSessionExpire) } + engineChan := make(chan *internal.Engine, 1) + go s.watchEngine(ctx, engineChan) + if !config.DisableAutoConnect { - go s.connectWithRetryRuns(ctx, config, s.statusRecorder, s.mgmProbe, s.signalProbe, s.relayProbe, s.wgProbe) + go s.connectWithRetryRuns(ctx, config, s.statusRecorder, s.mgmProbe, s.signalProbe, s.relayProbe, s.wgProbe, engineChan) } return nil @@ -153,6 +158,7 @@ func (s *Server) Start() error { // we cancel retry if the client receive a stop or down command, or if disable auto connect is configured. func (s *Server) connectWithRetryRuns(ctx context.Context, config *internal.Config, statusRecorder *peer.Status, mgmProbe *internal.Probe, signalProbe *internal.Probe, relayProbe *internal.Probe, wgProbe *internal.Probe, + engineChan chan<- *internal.Engine, ) { backOff := getConnectWithBackoff(ctx) retryStarted := false @@ -182,7 +188,7 @@ func (s *Server) connectWithRetryRuns(ctx context.Context, config *internal.Conf runOperation := func() error { log.Tracef("running client connection") - err := internal.RunClientWithProbes(ctx, config, statusRecorder, mgmProbe, signalProbe, relayProbe, wgProbe) + err := internal.RunClientWithProbes(ctx, config, statusRecorder, mgmProbe, signalProbe, relayProbe, wgProbe, engineChan) if err != nil { log.Debugf("run client connection exited with error: %v. Will retry in the background", err) } @@ -562,7 +568,10 @@ func (s *Server) Up(callerCtx context.Context, _ *proto.UpRequest) (*proto.UpRes s.statusRecorder.UpdateManagementAddress(s.config.ManagementURL.String()) s.statusRecorder.UpdateRosenpass(s.config.RosenpassEnabled, s.config.RosenpassPermissive) - go s.connectWithRetryRuns(ctx, s.config, s.statusRecorder, s.mgmProbe, s.signalProbe, s.relayProbe, s.wgProbe) + engineChan := make(chan *internal.Engine, 1) + go s.watchEngine(ctx, engineChan) + + go s.connectWithRetryRuns(ctx, s.config, s.statusRecorder, s.mgmProbe, s.signalProbe, s.relayProbe, s.wgProbe, engineChan) return &proto.UpResponse{}, nil } @@ -579,6 +588,8 @@ func (s *Server) Down(_ context.Context, _ *proto.DownRequest) (*proto.DownRespo state := internal.CtxGetState(s.rootCtx) state.Set(internal.StatusIdle) + s.engine = nil + return &proto.DownResponse{}, nil } @@ -661,7 +672,6 @@ func (s *Server) GetConfig(_ context.Context, _ *proto.GetConfigRequest) (*proto PreSharedKey: preSharedKey, }, nil } - func (s *Server) onSessionExpire() { if runtime.GOOS != "windows" { isUIActive := internal.CheckUIApp() @@ -673,6 +683,22 @@ func (s *Server) onSessionExpire() { } } +// watchEngine watches the engine channel and updates the engine state +func (s *Server) watchEngine(ctx context.Context, engineChan chan *internal.Engine) { + log.Tracef("Started watching engine") + for { + select { + case <-ctx.Done(): + s.engine = nil + log.Tracef("Stopped watching engine") + return + case engine := <-engineChan: + log.Tracef("Received engine from watcher") + s.engine = engine + } + } +} + func toProtoFullStatus(fullStatus peer.FullStatus) *proto.FullStatus { pbFullStatus := proto.FullStatus{ ManagementState: &proto.ManagementState{}, diff --git a/client/server/server_test.go b/client/server/server_test.go index 4e4a09145..8082e6bba 100644 --- a/client/server/server_test.go +++ b/client/server/server_test.go @@ -2,11 +2,12 @@ package server import ( "context" - "github.com/netbirdio/management-integrations/integrations" "net" "testing" "time" + "github.com/netbirdio/management-integrations/integrations" + log "github.com/sirupsen/logrus" "google.golang.org/grpc" "google.golang.org/grpc/keepalive" @@ -69,7 +70,7 @@ func TestConnectWithRetryRuns(t *testing.T) { t.Setenv(maxRetryTimeVar, "5s") t.Setenv(retryMultiplierVar, "1") - s.connectWithRetryRuns(ctx, config, s.statusRecorder, s.mgmProbe, s.signalProbe, s.relayProbe, s.wgProbe) + s.connectWithRetryRuns(ctx, config, s.statusRecorder, s.mgmProbe, s.signalProbe, s.relayProbe, s.wgProbe, nil) if counter < 3 { t.Fatalf("expected counter > 2, got %d", counter) } diff --git a/client/ui/client_ui.go b/client/ui/client_ui.go index aec2c8fac..0f16369a5 100644 --- a/client/ui/client_ui.go +++ b/client/ui/client_ui.go @@ -1,7 +1,5 @@ //go:build !(linux && 386) -// +build !linux !386 -// skipping linux 32 bits build and tests package main import ( @@ -58,6 +56,8 @@ func main() { var showSettings bool flag.BoolVar(&showSettings, "settings", false, "run settings windows") + var showRoutes bool + flag.BoolVar(&showRoutes, "routes", false, "run routes windows") var errorMSG string flag.StringVar(&errorMSG, "error-msg", "", "displays a error message window") @@ -71,8 +71,8 @@ func main() { return } - client := newServiceClient(daemonAddr, a, showSettings) - if showSettings { + client := newServiceClient(daemonAddr, a, showSettings, showRoutes) + if showSettings || showRoutes { a.Run() } else { if err := checkPIDFile(); err != nil { @@ -135,6 +135,7 @@ type serviceClient struct { mVersionDaemon *systray.MenuItem mUpdate *systray.MenuItem mQuit *systray.MenuItem + mRoutes *systray.MenuItem // application with main windows. app fyne.App @@ -159,12 +160,15 @@ type serviceClient struct { daemonVersion string updateIndicationLock sync.Mutex isUpdateIconActive bool + + showRoutes bool + wRoutes fyne.Window } // newServiceClient instance constructor // // This constructor also builds the UI elements for the settings window. -func newServiceClient(addr string, a fyne.App, showSettings bool) *serviceClient { +func newServiceClient(addr string, a fyne.App, showSettings bool, showRoutes bool) *serviceClient { s := &serviceClient{ ctx: context.Background(), addr: addr, @@ -172,6 +176,7 @@ func newServiceClient(addr string, a fyne.App, showSettings bool) *serviceClient sendNotification: false, showSettings: showSettings, + showRoutes: showRoutes, update: version.NewUpdate(), } @@ -191,14 +196,16 @@ func newServiceClient(addr string, a fyne.App, showSettings bool) *serviceClient } if showSettings { - s.showUIElements() + s.showSettingsUI() return s + } else if showRoutes { + s.showRoutesUI() } return s } -func (s *serviceClient) showUIElements() { +func (s *serviceClient) showSettingsUI() { // add settings window UI elements. s.wSettings = s.app.NewWindow("NetBird Settings") s.iMngURL = widget.NewEntry() @@ -416,6 +423,7 @@ func (s *serviceClient) updateStatus() error { s.mStatus.SetTitle("Connected") s.mUp.Disable() s.mDown.Enable() + s.mRoutes.Enable() systrayIconState = true } else if status.Status != string(internal.StatusConnected) && s.mUp.Disabled() { s.connected = false @@ -428,6 +436,7 @@ func (s *serviceClient) updateStatus() error { s.mStatus.SetTitle("Disconnected") s.mDown.Disable() s.mUp.Enable() + s.mRoutes.Disable() systrayIconState = false } @@ -483,9 +492,11 @@ func (s *serviceClient) onTrayReady() { s.mUp = systray.AddMenuItem("Connect", "Connect") s.mDown = systray.AddMenuItem("Disconnect", "Disconnect") s.mDown.Disable() - s.mAdminPanel = systray.AddMenuItem("Admin Panel", "Wiretrustee Admin Panel") + s.mAdminPanel = systray.AddMenuItem("Admin Panel", "Netbird Admin Panel") systray.AddSeparator() s.mSettings = systray.AddMenuItem("Settings", "Settings of the application") + s.mRoutes = systray.AddMenuItem("Network Routes", "Open the routes management window") + s.mRoutes.Disable() systray.AddSeparator() s.mAbout = systray.AddMenuItem("About", "About") @@ -557,6 +568,12 @@ func (s *serviceClient) onTrayReady() { if err != nil { log.Errorf("%s", err) } + case <-s.mRoutes.ClickedCh: + s.mRoutes.Disable() + go func() { + defer s.mRoutes.Enable() + s.runSelfCommand("routes", "true") + }() } if err != nil { log.Errorf("process connection: %v", err) diff --git a/client/ui/route.go b/client/ui/route.go new file mode 100644 index 000000000..0ac58e5d5 --- /dev/null +++ b/client/ui/route.go @@ -0,0 +1,203 @@ +//go:build !(linux && 386) + +package main + +import ( + "fmt" + "strings" + "time" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/dialog" + "fyne.io/fyne/v2/layout" + "fyne.io/fyne/v2/widget" + log "github.com/sirupsen/logrus" + + "github.com/netbirdio/netbird/client/proto" +) + +func (s *serviceClient) showRoutesUI() { + s.wRoutes = s.app.NewWindow("NetBird Routes") + + grid := container.New(layout.NewGridLayout(2)) + go s.updateRoutes(grid) + routeCheckContainer := container.NewVBox() + routeCheckContainer.Add(grid) + scrollContainer := container.NewVScroll(routeCheckContainer) + scrollContainer.SetMinSize(fyne.NewSize(200, 300)) + + buttonBox := container.NewHBox( + layout.NewSpacer(), + widget.NewButton("Refresh", func() { + s.updateRoutes(grid) + }), + widget.NewButton("Select all", func() { + s.selectAllRoutes() + s.updateRoutes(grid) + }), + widget.NewButton("Deselect All", func() { + s.deselectAllRoutes() + s.updateRoutes(grid) + }), + layout.NewSpacer(), + ) + + content := container.NewBorder(nil, buttonBox, nil, nil, scrollContainer) + + s.wRoutes.SetContent(content) + s.wRoutes.Show() + + s.startAutoRefresh(5*time.Second, grid) +} + +func (s *serviceClient) updateRoutes(grid *fyne.Container) { + routes, err := s.fetchRoutes() + if err != nil { + log.Errorf("get client: %v", err) + s.showError(fmt.Errorf("get client: %v", err)) + return + } + + grid.Objects = nil + idHeader := widget.NewLabelWithStyle(" ID", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}) + networkHeader := widget.NewLabelWithStyle("Network", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}) + + grid.Add(idHeader) + grid.Add(networkHeader) + for _, route := range routes { + r := route + + checkBox := widget.NewCheck(r.ID, func(checked bool) { + s.selectRoute(r.ID, checked) + }) + checkBox.Checked = route.Selected + checkBox.Resize(fyne.NewSize(20, 20)) + checkBox.Refresh() + + grid.Add(checkBox) + grid.Add(widget.NewLabel(r.Network)) + } + + s.wRoutes.Content().Refresh() +} + +func (s *serviceClient) fetchRoutes() ([]*proto.Route, error) { + conn, err := s.getSrvClient(defaultFailTimeout) + if err != nil { + return nil, fmt.Errorf("get client: %v", err) + } + + resp, err := conn.ListRoutes(s.ctx, &proto.ListRoutesRequest{}) + if err != nil { + return nil, fmt.Errorf("failed to list routes: %v", err) + } + + return resp.Routes, nil +} + +func (s *serviceClient) selectRoute(id string, checked bool) { + conn, err := s.getSrvClient(defaultFailTimeout) + if err != nil { + log.Errorf("get client: %v", err) + s.showError(fmt.Errorf("get client: %v", err)) + return + } + + req := &proto.SelectRoutesRequest{ + RouteIDs: []string{id}, + Append: checked, + } + + if checked { + if _, err := conn.SelectRoutes(s.ctx, req); err != nil { + log.Errorf("failed to select route: %v", err) + s.showError(fmt.Errorf("failed to select route: %v", err)) + return + } + log.Infof("Route %s selected", id) + } else { + if _, err := conn.DeselectRoutes(s.ctx, req); err != nil { + log.Errorf("failed to deselect route: %v", err) + s.showError(fmt.Errorf("failed to deselect route: %v", err)) + return + } + log.Infof("Route %s deselected", id) + } +} + +func (s *serviceClient) selectAllRoutes() { + conn, err := s.getSrvClient(defaultFailTimeout) + if err != nil { + log.Errorf("get client: %v", err) + return + } + + req := &proto.SelectRoutesRequest{ + All: true, + } + if _, err := conn.SelectRoutes(s.ctx, req); err != nil { + log.Errorf("failed to select all routes: %v", err) + s.showError(fmt.Errorf("failed to select all routes: %v", err)) + return + } + + log.Debug("All routes selected") +} + +func (s *serviceClient) deselectAllRoutes() { + conn, err := s.getSrvClient(defaultFailTimeout) + if err != nil { + log.Errorf("get client: %v", err) + return + } + + req := &proto.SelectRoutesRequest{ + All: true, + } + if _, err := conn.DeselectRoutes(s.ctx, req); err != nil { + log.Errorf("failed to deselect all routes: %v", err) + s.showError(fmt.Errorf("failed to deselect all routes: %v", err)) + return + } + + log.Debug("All routes deselected") +} + +func (s *serviceClient) showError(err error) { + wrappedMessage := wrapText(err.Error(), 50) + + dialog.ShowError(fmt.Errorf("%s", wrappedMessage), s.wRoutes) +} + +func (s *serviceClient) startAutoRefresh(interval time.Duration, grid *fyne.Container) { + ticker := time.NewTicker(interval) + go func() { + for range ticker.C { + s.updateRoutes(grid) + } + }() + + s.wRoutes.SetOnClosed(func() { + ticker.Stop() + }) +} + +// wrapText inserts newlines into the text to ensure that each line is +// no longer than 'lineLength' runes. +func wrapText(text string, lineLength int) string { + var sb strings.Builder + var currentLineLength int + + for _, runeValue := range text { + sb.WriteRune(runeValue) + currentLineLength++ + + if currentLineLength >= lineLength || runeValue == '\n' { + sb.WriteRune('\n') + currentLineLength = 0 + } + } + + return sb.String() +}