From 6a228efbcc5efed86ab7a924965eb406016c7fd9 Mon Sep 17 00:00:00 2001 From: Arsolitt Date: Fri, 13 Feb 2026 16:25:55 +0300 Subject: [PATCH 1/6] [client] Add disable_default_route field to SetConfigRequest Add optional bool disable_default_route field (tag 35) to the SetConfigRequest protobuf message and wire it through the daemon server handler to ConfigInput. --- client/proto/daemon.pb.go | 16 +++++++++++++--- client/proto/daemon.proto | 2 ++ client/server/server.go | 1 + 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/client/proto/daemon.pb.go b/client/proto/daemon.pb.go index 3879beba306..0ab4b48f2fb 100644 --- a/client/proto/daemon.pb.go +++ b/client/proto/daemon.pb.go @@ -4144,6 +4144,7 @@ type SetConfigRequest struct { EnableSSHRemotePortForwarding *bool `protobuf:"varint,32,opt,name=enableSSHRemotePortForwarding,proto3,oneof" json:"enableSSHRemotePortForwarding,omitempty"` DisableSSHAuth *bool `protobuf:"varint,33,opt,name=disableSSHAuth,proto3,oneof" json:"disableSSHAuth,omitempty"` SshJWTCacheTTL *int32 `protobuf:"varint,34,opt,name=sshJWTCacheTTL,proto3,oneof" json:"sshJWTCacheTTL,omitempty"` + DisableDefaultRoute *bool `protobuf:"varint,35,opt,name=disable_default_route,json=disableDefaultRoute,proto3,oneof" json:"disable_default_route,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -4416,6 +4417,13 @@ func (x *SetConfigRequest) GetSshJWTCacheTTL() int32 { return 0 } +func (x *SetConfigRequest) GetDisableDefaultRoute() bool { + if x != nil && x.DisableDefaultRoute != nil { + return *x.DisableDefaultRoute + } + return false +} + type SetConfigResponse struct { state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields @@ -6287,7 +6295,7 @@ const file_daemon_proto_rawDesc = "" + "\busername\x18\x02 \x01(\tH\x01R\busername\x88\x01\x01B\x0e\n" + "\f_profileNameB\v\n" + "\t_username\"\x17\n" + - "\x15SwitchProfileResponse\"\xdf\x10\n" + + "\x15SwitchProfileResponse\"\xb2\x11\n" + "\x10SetConfigRequest\x12\x1a\n" + "\busername\x18\x01 \x01(\tR\busername\x12 \n" + "\vprofileName\x18\x02 \x01(\tR\vprofileName\x12$\n" + @@ -6326,7 +6334,8 @@ const file_daemon_proto_rawDesc = "" + "\x1cenableSSHLocalPortForwarding\x18\x1f \x01(\bH\x14R\x1cenableSSHLocalPortForwarding\x88\x01\x01\x12I\n" + "\x1denableSSHRemotePortForwarding\x18 \x01(\bH\x15R\x1denableSSHRemotePortForwarding\x88\x01\x01\x12+\n" + "\x0edisableSSHAuth\x18! \x01(\bH\x16R\x0edisableSSHAuth\x88\x01\x01\x12+\n" + - "\x0esshJWTCacheTTL\x18\" \x01(\x05H\x17R\x0esshJWTCacheTTL\x88\x01\x01B\x13\n" + + "\x0esshJWTCacheTTL\x18\" \x01(\x05H\x17R\x0esshJWTCacheTTL\x88\x01\x01\x127\n" + + "\x15disable_default_route\x18# \x01(\bH\x18R\x13disableDefaultRoute\x88\x01\x01B\x13\n" + "\x11_rosenpassEnabledB\x10\n" + "\x0e_interfaceNameB\x10\n" + "\x0e_wireguardPortB\x17\n" + @@ -6350,7 +6359,8 @@ const file_daemon_proto_rawDesc = "" + "\x1d_enableSSHLocalPortForwardingB \n" + "\x1e_enableSSHRemotePortForwardingB\x11\n" + "\x0f_disableSSHAuthB\x11\n" + - "\x0f_sshJWTCacheTTL\"\x13\n" + + "\x0f_sshJWTCacheTTLB\x18\n" + + "\x16_disable_default_route\"\x13\n" + "\x11SetConfigResponse\"Q\n" + "\x11AddProfileRequest\x12\x1a\n" + "\busername\x18\x01 \x01(\tR\busername\x12 \n" + diff --git a/client/proto/daemon.proto b/client/proto/daemon.proto index 4dc41d401b6..35866b3c208 100644 --- a/client/proto/daemon.proto +++ b/client/proto/daemon.proto @@ -673,6 +673,8 @@ message SetConfigRequest { optional bool enableSSHRemotePortForwarding = 32; optional bool disableSSHAuth = 33; optional int32 sshJWTCacheTTL = 34; + + optional bool disable_default_route = 35; } message SetConfigResponse{} diff --git a/client/server/server.go b/client/server/server.go index 0466630c5fc..963de355bcf 100644 --- a/client/server/server.go +++ b/client/server/server.go @@ -360,6 +360,7 @@ func (s *Server) SetConfig(callerCtx context.Context, msg *proto.SetConfigReques config.NetworkMonitor = msg.NetworkMonitor config.DisableClientRoutes = msg.DisableClientRoutes config.DisableServerRoutes = msg.DisableServerRoutes + config.DisableDefaultRoute = msg.DisableDefaultRoute config.DisableDNS = msg.DisableDns config.DisableFirewall = msg.DisableFirewall config.BlockLANAccess = msg.BlockLanAccess From 962ab2d734d897a2453d6c2d40676c9dd3444f56 Mon Sep 17 00:00:00 2001 From: Arsolitt Date: Fri, 13 Feb 2026 16:32:09 +0300 Subject: [PATCH 2/6] [client] Add DisableDefaultRoute to config layer Add DisableDefaultRoute field to ConfigInput, Config, EngineConfig structs and wire it through the config update/create logic and engine config mapping. --- client/internal/connect.go | 1 + client/internal/engine.go | 1 + client/internal/profilemanager/config.go | 12 ++++++++++++ 3 files changed, 14 insertions(+) diff --git a/client/internal/connect.go b/client/internal/connect.go index 17fc20c427a..774febfbaed 100644 --- a/client/internal/connect.go +++ b/client/internal/connect.go @@ -500,6 +500,7 @@ func createEngineConfig(key wgtypes.Key, config *profilemanager.Config, peerConf DisableClientRoutes: config.DisableClientRoutes, DisableServerRoutes: config.DisableServerRoutes || config.BlockInbound, + DisableDefaultRoute: config.DisableDefaultRoute, DisableDNS: config.DisableDNS, DisableFirewall: config.DisableFirewall, BlockLANAccess: config.BlockLANAccess, diff --git a/client/internal/engine.go b/client/internal/engine.go index b0ae841f8c0..bbcef8f9b82 100644 --- a/client/internal/engine.go +++ b/client/internal/engine.go @@ -126,6 +126,7 @@ type EngineConfig struct { DisableClientRoutes bool DisableServerRoutes bool + DisableDefaultRoute bool DisableDNS bool DisableFirewall bool BlockLANAccess bool diff --git a/client/internal/profilemanager/config.go b/client/internal/profilemanager/config.go index 8f3ff8b1131..31f70c06e39 100644 --- a/client/internal/profilemanager/config.go +++ b/client/internal/profilemanager/config.go @@ -73,6 +73,7 @@ type ConfigInput struct { DisableClientRoutes *bool DisableServerRoutes *bool + DisableDefaultRoute *bool DisableDNS *bool DisableFirewall *bool BlockLANAccess *bool @@ -111,6 +112,7 @@ type Config struct { DisableClientRoutes bool DisableServerRoutes bool + DisableDefaultRoute bool DisableDNS bool DisableFirewall bool BlockLANAccess bool @@ -484,6 +486,16 @@ func (config *Config) apply(input ConfigInput) (updated bool, err error) { updated = true } + if input.DisableDefaultRoute != nil && *input.DisableDefaultRoute != config.DisableDefaultRoute { + if *input.DisableDefaultRoute { + log.Infof("disabling default route") + } else { + log.Infof("enabling default route") + } + config.DisableDefaultRoute = *input.DisableDefaultRoute + updated = true + } + if input.DisableDNS != nil && *input.DisableDNS != config.DisableDNS { if *input.DisableDNS { log.Infof("disabling DNS configuration") From dff61f192057a0c790d57d70675b5423dec086da Mon Sep 17 00:00:00 2001 From: Arsolitt Date: Fri, 13 Feb 2026 16:36:33 +0300 Subject: [PATCH 3/6] [client] Add --disable-default-route CLI flag and route filtering Register the --disable-default-route flag and implement filtering in the route ref counter to skip adding default route (0.0.0.0/0) to the system routing table while preserving WireGuard allowed IPs. --- client/cmd/system.go | 17 +++++++++++------ client/cmd/up.go | 7 +++++++ client/internal/engine.go | 1 + client/internal/routemanager/manager.go | 10 ++++++++++ 4 files changed, 29 insertions(+), 6 deletions(-) diff --git a/client/cmd/system.go b/client/cmd/system.go index f63432401d3..eb23692c934 100644 --- a/client/cmd/system.go +++ b/client/cmd/system.go @@ -2,17 +2,19 @@ package cmd // Flag constants for system configuration const ( - disableClientRoutesFlag = "disable-client-routes" - disableServerRoutesFlag = "disable-server-routes" - disableDNSFlag = "disable-dns" - disableFirewallFlag = "disable-firewall" - blockLANAccessFlag = "block-lan-access" - blockInboundFlag = "block-inbound" + disableClientRoutesFlag = "disable-client-routes" + disableServerRoutesFlag = "disable-server-routes" + disableDefaultRouteFlag = "disable-default-route" + disableDNSFlag = "disable-dns" + disableFirewallFlag = "disable-firewall" + blockLANAccessFlag = "block-lan-access" + blockInboundFlag = "block-inbound" ) var ( disableClientRoutes bool disableServerRoutes bool + disableDefaultRoute bool disableDNS bool disableFirewall bool blockLANAccess bool @@ -27,6 +29,9 @@ func init() { upCmd.PersistentFlags().BoolVar(&disableServerRoutes, disableServerRoutesFlag, false, "Disable server routes. If enabled, the client won't act as a router for server routes received from the management service.") + upCmd.PersistentFlags().BoolVar(&disableDefaultRoute, disableDefaultRouteFlag, false, + "Disable adding default route (0.0.0.0/0) to the system routing table while keeping it in WireGuard allowed IPs.") + upCmd.PersistentFlags().BoolVar(&disableDNS, disableDNSFlag, false, "Disable DNS. If enabled, the client won't configure DNS settings.") diff --git a/client/cmd/up.go b/client/cmd/up.go index 9559287d5e1..b5be33a6551 100644 --- a/client/cmd/up.go +++ b/client/cmd/up.go @@ -414,6 +414,10 @@ func setupSetConfigReq(customDNSAddressConverted []byte, cmd *cobra.Command, pro req.DisableServerRoutes = &disableServerRoutes } + if cmd.Flag(disableDefaultRouteFlag).Changed { + req.DisableDefaultRoute = &disableDefaultRoute + } + if cmd.Flag(disableDNSFlag).Changed { req.DisableDns = &disableDNS } @@ -532,6 +536,9 @@ func setupConfig(customDNSAddressConverted []byte, cmd *cobra.Command, configFil if cmd.Flag(disableServerRoutesFlag).Changed { ic.DisableServerRoutes = &disableServerRoutes } + if cmd.Flag(disableDefaultRouteFlag).Changed { + ic.DisableDefaultRoute = &disableDefaultRoute + } if cmd.Flag(disableDNSFlag).Changed { ic.DisableDNS = &disableDNS } diff --git a/client/internal/engine.go b/client/internal/engine.go index bbcef8f9b82..76040ea741f 100644 --- a/client/internal/engine.go +++ b/client/internal/engine.go @@ -481,6 +481,7 @@ func (e *Engine) Start(netbirdConfig *mgmProto.NetbirdConfig, mgmtURL *url.URL) PeerStore: e.peerStore, DisableClientRoutes: e.config.DisableClientRoutes, DisableServerRoutes: e.config.DisableServerRoutes, + DisableDefaultRoute: e.config.DisableDefaultRoute, }) if err := e.routeManager.Init(); err != nil { log.Errorf("Failed to initialize route manager: %s", err) diff --git a/client/internal/routemanager/manager.go b/client/internal/routemanager/manager.go index 9afe2049d00..1015062c958 100644 --- a/client/internal/routemanager/manager.go +++ b/client/internal/routemanager/manager.go @@ -74,6 +74,7 @@ type ManagerConfig struct { PeerStore *peerstore.Store DisableClientRoutes bool DisableServerRoutes bool + DisableDefaultRoute bool } // DefaultManager is the default instance of a route manager @@ -103,6 +104,7 @@ type DefaultManager struct { useNewDNSRoute bool disableClientRoutes bool disableServerRoutes bool + disableDefaultRoute bool activeRoutes map[route.HAUniqueID]client.RouteHandler fakeIPManager *fakeip.Manager dnsForwarderPort atomic.Uint32 @@ -133,6 +135,7 @@ func NewManager(config ManagerConfig) *DefaultManager { peerStore: config.PeerStore, disableClientRoutes: config.DisableClientRoutes, disableServerRoutes: config.DisableServerRoutes, + disableDefaultRoute: config.DisableDefaultRoute, activeRoutes: make(map[route.HAUniqueID]client.RouteHandler), } dm.dnsForwarderPort.Store(uint32(nbdns.ForwarderClientPort)) @@ -184,9 +187,16 @@ func (m *DefaultManager) setupRefCounters(useNoop bool) { m.routeRefCounter = refcounter.New( func(prefix netip.Prefix, _ struct{}) (struct{}, error) { + if m.disableDefaultRoute && (prefix == vars.Defaultv4 || prefix == vars.Defaultv6) { + log.Infof("Default route %s is disabled by user, skipping system route", prefix) + return struct{}{}, refcounter.ErrIgnore + } return struct{}{}, m.sysOps.AddVPNRoute(prefix, toInterface()) }, func(prefix netip.Prefix, _ struct{}) error { + if m.disableDefaultRoute && (prefix == vars.Defaultv4 || prefix == vars.Defaultv6) { + return nil + } return m.sysOps.RemoveVPNRoute(prefix, toInterface()) }, ) From 2e27f087994c09a97d1962eb9f9ce8060bffc991 Mon Sep 17 00:00:00 2001 From: Arsolitt Date: Fri, 13 Feb 2026 16:39:06 +0300 Subject: [PATCH 4/6] [client] Add tests for DisableDefaultRoute flag Update setconfig_test.go to cover the new DisableDefaultRoute field in SetConfigRequest (AllFieldsSaved, verifyAllFieldsCovered, and CLIFlags mapping). Add a test case to TestManagerUpdateRoutes verifying that the default route watcher is still created when the flag is set. --- client/internal/routemanager/manager_test.go | 30 +++++++++++++++++--- client/server/setconfig_test.go | 5 ++++ 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/client/internal/routemanager/manager_test.go b/client/internal/routemanager/manager_test.go index 3697545ae96..391dc15aa2b 100644 --- a/client/internal/routemanager/manager_test.go +++ b/client/internal/routemanager/manager_test.go @@ -32,6 +32,7 @@ func TestManagerUpdateRoutes(t *testing.T) { inputRoutes []*route.Route inputSerial uint64 removeSrvRouter bool + disableDefaultRoute bool serverRoutesExpected int clientNetworkWatchersExpected int clientNetworkWatchersExpectedAllowed int @@ -205,6 +206,26 @@ func TestManagerUpdateRoutes(t *testing.T) { clientNetworkWatchersExpected: 0, clientNetworkWatchersExpectedAllowed: 1, }, + { + name: "Default Route With DisableDefaultRoute Still Creates Watcher", + disableDefaultRoute: true, + inputRoutes: []*route.Route{ + { + ID: "a", + NetID: "routeA", + Peer: remotePeerKey1, + Network: netip.MustParsePrefix("0.0.0.0/0"), + NetworkType: route.IPv4Network, + Metric: 9999, + Masquerade: false, + Enabled: true, + SkipAutoApply: false, + }, + }, + inputSerial: 1, + clientNetworkWatchersExpected: 0, + clientNetworkWatchersExpectedAllowed: 1, + }, { name: "Remove 1 Client Route", inputInitRoutes: []*route.Route{ @@ -425,10 +446,11 @@ func TestManagerUpdateRoutes(t *testing.T) { statusRecorder := peer.NewRecorder("https://mgm") ctx := context.TODO() routeManager := NewManager(ManagerConfig{ - Context: ctx, - PublicKey: localPeerKey, - WGInterface: wgInterface, - StatusRecorder: statusRecorder, + Context: ctx, + PublicKey: localPeerKey, + WGInterface: wgInterface, + StatusRecorder: statusRecorder, + DisableDefaultRoute: testCase.disableDefaultRoute, }) err = routeManager.Init() diff --git a/client/server/setconfig_test.go b/client/server/setconfig_test.go index 8e360175d45..b073c745a88 100644 --- a/client/server/setconfig_test.go +++ b/client/server/setconfig_test.go @@ -65,6 +65,7 @@ func TestSetConfig_AllFieldsSaved(t *testing.T) { networkMonitor := true disableClientRoutes := true disableServerRoutes := true + disableDefaultRoute := true disableDNS := true disableFirewall := true blockLANAccess := true @@ -89,6 +90,7 @@ func TestSetConfig_AllFieldsSaved(t *testing.T) { NetworkMonitor: &networkMonitor, DisableClientRoutes: &disableClientRoutes, DisableServerRoutes: &disableServerRoutes, + DisableDefaultRoute: &disableDefaultRoute, DisableDns: &disableDNS, DisableFirewall: &disableFirewall, BlockLanAccess: &blockLANAccess, @@ -133,6 +135,7 @@ func TestSetConfig_AllFieldsSaved(t *testing.T) { require.Equal(t, networkMonitor, *cfg.NetworkMonitor) require.Equal(t, disableClientRoutes, cfg.DisableClientRoutes) require.Equal(t, disableServerRoutes, cfg.DisableServerRoutes) + require.Equal(t, disableDefaultRoute, cfg.DisableDefaultRoute) require.Equal(t, disableDNS, cfg.DisableDNS) require.Equal(t, disableFirewall, cfg.DisableFirewall) require.Equal(t, blockLANAccess, cfg.BlockLANAccess) @@ -183,6 +186,7 @@ func verifyAllFieldsCovered(t *testing.T, req *proto.SetConfigRequest) { "NetworkMonitor": true, "DisableClientRoutes": true, "DisableServerRoutes": true, + "DisableDefaultRoute": true, "DisableDns": true, "DisableFirewall": true, "BlockLanAccess": true, @@ -243,6 +247,7 @@ func TestCLIFlags_MappedToSetConfig(t *testing.T) { "network-monitor": "NetworkMonitor", "disable-client-routes": "DisableClientRoutes", "disable-server-routes": "DisableServerRoutes", + "disable-default-route": "DisableDefaultRoute", "disable-dns": "DisableDns", "disable-firewall": "DisableFirewall", "block-lan-access": "BlockLanAccess", From 2dcb1cb239e6f1085abb37393bd84b8dadd55ac9 Mon Sep 17 00:00:00 2001 From: Arsolitt Date: Fri, 13 Feb 2026 17:34:40 +0300 Subject: [PATCH 5/6] [client] Add disable_default_route to LoginRequest and GetConfigResponse The field was missing from LoginRequest (used during setup key login) and GetConfigResponse (used by UI and status queries), causing the flag to not persist in daemon mode. --- client/cmd/up.go | 3 +++ client/proto/daemon.pb.go | 29 ++++++++++++++++++++++++----- client/proto/daemon.proto | 4 ++++ client/server/server.go | 2 ++ 4 files changed, 33 insertions(+), 5 deletions(-) diff --git a/client/cmd/up.go b/client/cmd/up.go index b5be33a6551..077853ca62a 100644 --- a/client/cmd/up.go +++ b/client/cmd/up.go @@ -653,6 +653,9 @@ func setupLoginRequest(providedSetupKey string, customDNSAddressConverted []byte if cmd.Flag(disableServerRoutesFlag).Changed { loginRequest.DisableServerRoutes = &disableServerRoutes } + if cmd.Flag(disableDefaultRouteFlag).Changed { + loginRequest.DisableDefaultRoute = &disableDefaultRoute + } if cmd.Flag(disableDNSFlag).Changed { loginRequest.DisableDns = &disableDNS } diff --git a/client/proto/daemon.pb.go b/client/proto/daemon.pb.go index 0ab4b48f2fb..ec5825f46e2 100644 --- a/client/proto/daemon.pb.go +++ b/client/proto/daemon.pb.go @@ -469,6 +469,7 @@ type LoginRequest struct { EnableSSHRemotePortForwarding *bool `protobuf:"varint,37,opt,name=enableSSHRemotePortForwarding,proto3,oneof" json:"enableSSHRemotePortForwarding,omitempty"` DisableSSHAuth *bool `protobuf:"varint,38,opt,name=disableSSHAuth,proto3,oneof" json:"disableSSHAuth,omitempty"` SshJWTCacheTTL *int32 `protobuf:"varint,39,opt,name=sshJWTCacheTTL,proto3,oneof" json:"sshJWTCacheTTL,omitempty"` + DisableDefaultRoute *bool `protobuf:"varint,40,opt,name=disable_default_route,json=disableDefaultRoute,proto3,oneof" json:"disable_default_route,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -777,6 +778,13 @@ func (x *LoginRequest) GetSshJWTCacheTTL() int32 { return 0 } +func (x *LoginRequest) GetDisableDefaultRoute() bool { + if x != nil && x.DisableDefaultRoute != nil { + return *x.DisableDefaultRoute + } + return false +} + type LoginResponse struct { state protoimpl.MessageState `protogen:"open.v1"` NeedsSSOLogin bool `protobuf:"varint,1,opt,name=needsSSOLogin,proto3" json:"needsSSOLogin,omitempty"` @@ -1317,6 +1325,7 @@ type GetConfigResponse struct { EnableSSHRemotePortForwarding bool `protobuf:"varint,23,opt,name=enableSSHRemotePortForwarding,proto3" json:"enableSSHRemotePortForwarding,omitempty"` DisableSSHAuth bool `protobuf:"varint,25,opt,name=disableSSHAuth,proto3" json:"disableSSHAuth,omitempty"` SshJWTCacheTTL int32 `protobuf:"varint,26,opt,name=sshJWTCacheTTL,proto3" json:"sshJWTCacheTTL,omitempty"` + DisableDefaultRoute bool `protobuf:"varint,27,opt,name=disable_default_route,json=disableDefaultRoute,proto3" json:"disable_default_route,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -1533,6 +1542,13 @@ func (x *GetConfigResponse) GetSshJWTCacheTTL() int32 { return 0 } +func (x *GetConfigResponse) GetDisableDefaultRoute() bool { + if x != nil { + return x.DisableDefaultRoute + } + return false +} + // PeerState contains the latest state of a peer type PeerState struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -5943,7 +5959,7 @@ const file_daemon_proto_rawDesc = "" + "\x05SLEEP\x10\x01\x12\n" + "\n" + "\x06WAKEUP\x10\x02\"\x15\n" + - "\x13OSLifecycleResponse\"\xb6\x12\n" + + "\x13OSLifecycleResponse\"\x89\x13\n" + "\fLoginRequest\x12\x1a\n" + "\bsetupKey\x18\x01 \x01(\tR\bsetupKey\x12&\n" + "\fpreSharedKey\x18\x02 \x01(\tB\x02\x18\x01R\fpreSharedKey\x12$\n" + @@ -5987,7 +6003,8 @@ const file_daemon_proto_rawDesc = "" + "\x1cenableSSHLocalPortForwarding\x18$ \x01(\bH\x17R\x1cenableSSHLocalPortForwarding\x88\x01\x01\x12I\n" + "\x1denableSSHRemotePortForwarding\x18% \x01(\bH\x18R\x1denableSSHRemotePortForwarding\x88\x01\x01\x12+\n" + "\x0edisableSSHAuth\x18& \x01(\bH\x19R\x0edisableSSHAuth\x88\x01\x01\x12+\n" + - "\x0esshJWTCacheTTL\x18' \x01(\x05H\x1aR\x0esshJWTCacheTTL\x88\x01\x01B\x13\n" + + "\x0esshJWTCacheTTL\x18' \x01(\x05H\x1aR\x0esshJWTCacheTTL\x88\x01\x01\x127\n" + + "\x15disable_default_route\x18( \x01(\bH\x1bR\x13disableDefaultRoute\x88\x01\x01B\x13\n" + "\x11_rosenpassEnabledB\x10\n" + "\x0e_interfaceNameB\x10\n" + "\x0e_wireguardPortB\x17\n" + @@ -6014,7 +6031,8 @@ const file_daemon_proto_rawDesc = "" + "\x1d_enableSSHLocalPortForwardingB \n" + "\x1e_enableSSHRemotePortForwardingB\x11\n" + "\x0f_disableSSHAuthB\x11\n" + - "\x0f_sshJWTCacheTTL\"\xb5\x01\n" + + "\x0f_sshJWTCacheTTLB\x18\n" + + "\x16_disable_default_route\"\xb5\x01\n" + "\rLoginResponse\x12$\n" + "\rneedsSSOLogin\x18\x01 \x01(\bR\rneedsSSOLogin\x12\x1a\n" + "\buserCode\x18\x02 \x01(\tR\buserCode\x12(\n" + @@ -6051,7 +6069,7 @@ const file_daemon_proto_rawDesc = "" + "\fDownResponse\"P\n" + "\x10GetConfigRequest\x12 \n" + "\vprofileName\x18\x01 \x01(\tR\vprofileName\x12\x1a\n" + - "\busername\x18\x02 \x01(\tR\busername\"\xdb\b\n" + + "\busername\x18\x02 \x01(\tR\busername\"\x8f\t\n" + "\x11GetConfigResponse\x12$\n" + "\rmanagementUrl\x18\x01 \x01(\tR\rmanagementUrl\x12\x1e\n" + "\n" + @@ -6082,7 +6100,8 @@ const file_daemon_proto_rawDesc = "" + "\x1cenableSSHLocalPortForwarding\x18\x16 \x01(\bR\x1cenableSSHLocalPortForwarding\x12D\n" + "\x1denableSSHRemotePortForwarding\x18\x17 \x01(\bR\x1denableSSHRemotePortForwarding\x12&\n" + "\x0edisableSSHAuth\x18\x19 \x01(\bR\x0edisableSSHAuth\x12&\n" + - "\x0esshJWTCacheTTL\x18\x1a \x01(\x05R\x0esshJWTCacheTTL\"\xfe\x05\n" + + "\x0esshJWTCacheTTL\x18\x1a \x01(\x05R\x0esshJWTCacheTTL\x122\n" + + "\x15disable_default_route\x18\x1b \x01(\bR\x13disableDefaultRoute\"\xfe\x05\n" + "\tPeerState\x12\x0e\n" + "\x02IP\x18\x01 \x01(\tR\x02IP\x12\x16\n" + "\x06pubKey\x18\x02 \x01(\tR\x06pubKey\x12\x1e\n" + diff --git a/client/proto/daemon.proto b/client/proto/daemon.proto index 35866b3c208..140578db03c 100644 --- a/client/proto/daemon.proto +++ b/client/proto/daemon.proto @@ -205,6 +205,8 @@ message LoginRequest { optional bool enableSSHRemotePortForwarding = 37; optional bool disableSSHAuth = 38; optional int32 sshJWTCacheTTL = 39; + + optional bool disable_default_route = 40; } message LoginResponse { @@ -312,6 +314,8 @@ message GetConfigResponse { bool disableSSHAuth = 25; int32 sshJWTCacheTTL = 26; + + bool disable_default_route = 27; } // PeerState contains the latest state of a peer diff --git a/client/server/server.go b/client/server/server.go index 963de355bcf..aeacf863cdd 100644 --- a/client/server/server.go +++ b/client/server/server.go @@ -1446,6 +1446,7 @@ func (s *Server) GetConfig(ctx context.Context, req *proto.GetConfigRequest) (*p disableDNS := cfg.DisableDNS disableClientRoutes := cfg.DisableClientRoutes disableServerRoutes := cfg.DisableServerRoutes + disableDefaultRoute := cfg.DisableDefaultRoute blockLANAccess := cfg.BlockLANAccess enableSSHRoot := false @@ -1496,6 +1497,7 @@ func (s *Server) GetConfig(ctx context.Context, req *proto.GetConfigRequest) (*p DisableDns: disableDNS, DisableClientRoutes: disableClientRoutes, DisableServerRoutes: disableServerRoutes, + DisableDefaultRoute: disableDefaultRoute, BlockLanAccess: blockLANAccess, EnableSSHRoot: enableSSHRoot, EnableSSHSFTP: enableSSHSFTP, From 48fc631122d75c9db515c624b8c22d573c0bfc2c Mon Sep 17 00:00:00 2001 From: Arsolitt Date: Fri, 13 Feb 2026 18:06:02 +0300 Subject: [PATCH 6/6] [client] Improve DisableDefaultRoute test to verify route-skipping Add TestDisableDefaultRouteSkipsSystemRoute that directly asserts the route ref counter behavior: default route (0.0.0.0/0) is not tracked in the ref counter (ErrIgnore path) while non-default routes are tracked normally. Also update the table-driven test case to include a non-default route alongside the default one to verify selective filtering. --- client/internal/routemanager/manager_test.go | 66 +++++++++++++++++++- 1 file changed, 64 insertions(+), 2 deletions(-) diff --git a/client/internal/routemanager/manager_test.go b/client/internal/routemanager/manager_test.go index 391dc15aa2b..a62ac736774 100644 --- a/client/internal/routemanager/manager_test.go +++ b/client/internal/routemanager/manager_test.go @@ -6,6 +6,7 @@ import ( "net/netip" "testing" + "github.com/netbirdio/netbird/client/internal/routemanager/vars" "github.com/netbirdio/netbird/client/internal/stdnet" "golang.zx2c4.com/wireguard/wgctrl/wgtypes" @@ -221,10 +222,20 @@ func TestManagerUpdateRoutes(t *testing.T) { Enabled: true, SkipAutoApply: false, }, + { + ID: "b", + NetID: "routeB", + Peer: remotePeerKey1, + Network: netip.MustParsePrefix("10.0.0.0/8"), + NetworkType: route.IPv4Network, + Metric: 9999, + Masquerade: false, + Enabled: true, + }, }, inputSerial: 1, - clientNetworkWatchersExpected: 0, - clientNetworkWatchersExpectedAllowed: 1, + clientNetworkWatchersExpected: 1, + clientNetworkWatchersExpectedAllowed: 2, }, { name: "Remove 1 Client Route", @@ -484,3 +495,54 @@ func TestManagerUpdateRoutes(t *testing.T) { }) } } + +func TestDisableDefaultRouteSkipsSystemRoute(t *testing.T) { + peerPrivateKey, _ := wgtypes.GeneratePrivateKey() + newNet, err := stdnet.NewNet(context.Background(), nil) + require.NoError(t, err) + + opts := iface.WGIFaceOpts{ + IFaceName: "utun4399", + Address: "100.65.65.2/24", + WGPort: 33100, + WGPrivKey: peerPrivateKey.String(), + MTU: iface.DefaultMTU, + TransportNet: newNet, + } + wgInterface, err := iface.NewWGIFace(opts) + require.NoError(t, err) + defer wgInterface.Close() + + require.NoError(t, wgInterface.Create()) + + statusRecorder := peer.NewRecorder("https://mgm") + + mgr := NewManager(ManagerConfig{ + Context: context.TODO(), + PublicKey: localPeerKey, + WGInterface: wgInterface, + StatusRecorder: statusRecorder, + DisableDefaultRoute: true, + }) + require.NoError(t, mgr.Init()) + defer mgr.Stop(nil) + + // Increment the route ref counter for the default v4 prefix. + // With DisableDefaultRoute=true, this should be ignored (ErrIgnore path). + _, err = mgr.routeRefCounter.Increment(vars.Defaultv4, struct{}{}) + require.NoError(t, err, "increment should not return error for ignored default route") + + // The key should NOT be tracked in the ref counter because ErrIgnore skips it. + _, ok := mgr.routeRefCounter.Get(vars.Defaultv4) + require.False(t, ok, "default v4 route should not be in route ref counter when DisableDefaultRoute is true") + + // A non-default prefix should still be tracked normally. + normalPrefix := netip.MustParsePrefix("10.0.0.0/8") + _, err = mgr.routeRefCounter.Increment(normalPrefix, struct{}{}) + // AddVPNRoute may fail without proper routing setup, but the ref counter should still track it. + // We only care that non-default routes are NOT ignored. + ref, ok := mgr.routeRefCounter.Get(normalPrefix) + if ok { + require.Equal(t, 1, ref.Count, "non-default route should have ref count 1") + } +}