Skip to content

[client] Ability to disable default route with Exit Node#5309

Open
Arsolitt wants to merge 31 commits intonetbirdio:mainfrom
Arsolitt:feature/disable-default-route
Open

[client] Ability to disable default route with Exit Node#5309
Arsolitt wants to merge 31 commits intonetbirdio:mainfrom
Arsolitt:feature/disable-default-route

Conversation

@Arsolitt
Copy link
Copy Markdown

@Arsolitt Arsolitt commented Feb 13, 2026

Describe your changes

This PR introduces the ability to prevent the installation of the default route (0.0.0.0/0) into the system routing table while preserving the WireGuard AllowedIPs configuration by passing
--disable-default-route CLI arg.

This allows enabling an Exit Node on the peer without automatically routing all host traffic through the tunnel, enabling custom routing management outside netbird.

Issue ticket number and link

Stack

Checklist

  • Is it a bug fix
  • Is a typo/documentation fix
  • Is a feature enhancement
  • It is a refactor
  • Created tests that fail without the change (if possible)

By submitting this pull request, you confirm that you have read and agree to the terms of the Contributor License Agreement.

Documentation

Select exactly one:

  • I added/updated documentation for this change
  • Documentation is not needed for this change (explain why)

Docs PR URL (required if "docs added" is checked)

Paste the PR link from https://github.com/netbirdio/docs here:

netbirdio/docs#602

Summary by CodeRabbit

  • New Features

    • New configuration option and CLI flag to disable installation of the system default route; the choice is persisted, surfaced via the daemon API, and, when enabled, prevents the client from adding the 0.0.0.0/0 default route.
  • Tests

    • Added and updated tests covering the disable-default-route behavior across configuration, startup, and route-management scenarios.

@CLAassistant
Copy link
Copy Markdown

CLAassistant commented Feb 13, 2026

CLA assistant check
All committers have signed the CLA.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 13, 2026

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a new disable-default-route flag and boolean that flow CLI → proto/server → profile config → engine → route manager; when enabled, default IPv4/IPv6 system routes are skipped during add/remove operations. (50 words)

Changes

DisableDefaultRoute propagation

Layer / File(s) Summary
Proto / Server
client/proto/daemon.proto, client/server/server.go
Add disable_default_route fields to LoginRequest, SetConfigRequest, GetConfigResponse; server maps request field into internal config and includes it in GetConfig responses.
CLI
client/cmd/system.go, client/cmd/up.go
Register disable-default-route persistent flag and populate SetConfigRequest, ConfigInput, and LoginRequest when the flag is changed.
Profile / Config
client/internal/profilemanager/config.go, client/internal/connect.go
Expose DisableDefaultRoute in ConfigInput/Config; apply() logs and updates persisted value; propagate flag into EngineConfig.
Engine / Route Manager
client/internal/engine.go, client/internal/routemanager/manager.go
Add DisableDefaultRoute to EngineConfig/ManagerConfig; manager stores flag and skips adding/removing default IPv4/IPv6 system routes when set.
Tests
client/server/setconfig_test.go, client/internal/routemanager/manager_test.go
Wire new field into tests; update reflection coverage; add cases verifying default-route skipping behavior and watcher counts.

Sequence Diagram(s)

sequenceDiagram
    participant CLI as CLI
    participant Server as Server (daemon)
    participant Profile as ProfileManager
    participant Engine as Engine
    participant RouteMgr as RouteManager

    CLI->>Server: Send SetConfigRequest (disable_default_route)
    Server->>Profile: apply SetConfig (config.DisableDefaultRoute = msg.DisableDefaultRoute)
    Profile->>Engine: create EngineConfig (propagate DisableDefaultRoute)
    Engine->>RouteMgr: NewManager / ManagerConfig (DisableDefaultRoute)
    RouteMgr->>RouteMgr: setupRefCounters: if DisableDefaultRoute -> skip default routes
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested reviewers

  • pappz
  • bcmmbaga

Poem

🐇
A tiny flag hopped through every stack,
From CLI to daemon, never looking back,
When 0.0.0.0 wants to quietly hide,
The rabbit skips the route with nimble pride,
Tunnels hum softly as hops decide.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 36.36% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title '[client] Ability to disable default route with Exit Node' clearly and specifically describes the main feature being added: a new capability to disable the default route in the client when using an Exit Node.
Description check ✅ Passed The description covers the main changes (adding --disable-default-route CLI flag), its purpose (preventing default route installation while preserving WireGuard config), and correctly marks the PR as a feature enhancement with tests and documentation updates.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@client/proto/daemon.proto`:
- Around line 673-674: The proto adds the new field disable_default_route but it
was not added to the LoginRequest and GetConfigResponse messages nor propagated
in setupLoginRequest, so the value won't persist or be queryable; add an
optional bool disable_default_route field to both LoginRequest and
GetConfigResponse (choosing a unique tag number consistent with nearby fields),
update setupLoginRequest in client/cmd/up.go to copy the local
disable_default_route setting into the LoginRequest like the other disable_*
flags, regenerate protobuf stubs, and ensure server-side code that builds
GetConfigResponse populates disable_default_route from the server config/state.
🧹 Nitpick comments (1)
client/internal/routemanager/manager_test.go (1)

209-228: Test validates watcher creation but not route-skipping behavior.

The test only asserts require.Len(t, routeManager.clientNetworks, expectedWatchers), confirming a watcher is created. It does not verify the core feature: when disableDefaultRoute is true, the default route is excluded from the system routing table via the route ref counter's ErrIgnore path (see manager.go lines 190-193).

Add assertions that verify the default route is skipped from system route addition, or document that this behavior is covered by integration tests.

Comment thread client/proto/daemon.proto Outdated
@sonarqubecloud
Copy link
Copy Markdown

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@client/cmd/system.go`:
- Around line 32-34: Update the flag help text for
upCmd.PersistentFlags().BoolVar that uses disableDefaultRoute and
disableDefaultRouteFlag to explicitly mention both IPv4 and IPv6 default routes
(e.g. "Disable adding the IPv4 and IPv6 default routes (0.0.0.0/0 and ::/0) to
the system routing table while keeping them in WireGuard allowed IPs.") so users
understand the flag affects both families.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 60a55a1 and 48fc631.

⛔ Files ignored due to path filters (1)
  • client/proto/daemon.pb.go is excluded by !**/*.pb.go
📒 Files selected for processing (10)
  • client/cmd/system.go
  • client/cmd/up.go
  • client/internal/connect.go
  • client/internal/engine.go
  • client/internal/profilemanager/config.go
  • client/internal/routemanager/manager.go
  • client/internal/routemanager/manager_test.go
  • client/proto/daemon.proto
  • client/server/server.go
  • client/server/setconfig_test.go
🚧 Files skipped from review as they are similar to previous changes (3)
  • client/cmd/up.go
  • client/proto/daemon.proto
  • client/internal/engine.go

Comment thread client/cmd/system.go
@Arsolitt Arsolitt force-pushed the feature/disable-default-route branch from 48fc631 to 3220165 Compare May 7, 2026 08:22
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
client/internal/routemanager/manager_test.go (1)

210-239: 💤 Low value

clientNetworkWatchersExpected: 1 is dead in this test case.

Because clientNetworkWatchersExpectedAllowed is non-zero (2), the test-runner logic at line 487 unconditionally overrides clientNetworkWatchersExpected, making the 1 value unobserved and the intent of the field ambiguous for this case.

Consider either removing the unused field value or documenting why both differ, to keep the test data self-explanatory.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@client/internal/routemanager/manager_test.go` around lines 210 - 239, The
test case sets clientNetworkWatchersExpected to 1 but the test-runner logic
overrides it when clientNetworkWatchersExpectedAllowed is non-zero, making that
field dead; update the test case in manager_test.go by either removing the
unused clientNetworkWatchersExpected entry, setting
clientNetworkWatchersExpected to match clientNetworkWatchersExpectedAllowed, or
add a one-line comment next to the fields explaining why they differ so the
intent is explicit (refer to the test case struct literal and the test-runner
logic that uses clientNetworkWatchersExpectedAllowed).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@client/internal/routemanager/manager_test.go`:
- Around line 540-547: The test currently conditionally checks the Get result so
a false negative can silently pass; change the assertion to unconditionally
require that mgr.routeRefCounter.Get(normalPrefix) returns ok==true and then
assert ref.Count == 1 (and also ensure the Increment call's err is
checked/handled) so the test fails if the non-default prefix is not tracked;
refer to routeRefCounter.Increment, normalPrefix, routeRefCounter.Get and the
require.Equal assertion for where to update the test.

---

Nitpick comments:
In `@client/internal/routemanager/manager_test.go`:
- Around line 210-239: The test case sets clientNetworkWatchersExpected to 1 but
the test-runner logic overrides it when clientNetworkWatchersExpectedAllowed is
non-zero, making that field dead; update the test case in manager_test.go by
either removing the unused clientNetworkWatchersExpected entry, setting
clientNetworkWatchersExpected to match clientNetworkWatchersExpectedAllowed, or
add a one-line comment next to the fields explaining why they differ so the
intent is explicit (refer to the test case struct literal and the test-runner
logic that uses clientNetworkWatchersExpectedAllowed).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 38323ef8-b8e1-495c-9725-70bd65528721

📥 Commits

Reviewing files that changed from the base of the PR and between 48fc631 and 3220165.

⛔ Files ignored due to path filters (2)
  • client/proto/daemon.pb.go is excluded by !**/*.pb.go
  • client/proto/daemon_grpc.pb.go is excluded by !**/*.pb.go
📒 Files selected for processing (10)
  • client/cmd/system.go
  • client/cmd/up.go
  • client/internal/connect.go
  • client/internal/engine.go
  • client/internal/profilemanager/config.go
  • client/internal/routemanager/manager.go
  • client/internal/routemanager/manager_test.go
  • client/proto/daemon.proto
  • client/server/server.go
  • client/server/setconfig_test.go
🚧 Files skipped from review as they are similar to previous changes (6)
  • client/internal/engine.go
  • client/cmd/up.go
  • client/internal/profilemanager/config.go
  • client/internal/connect.go
  • client/internal/routemanager/manager.go
  • client/server/server.go

Comment thread client/internal/routemanager/manager_test.go
@mlsmaycon
Copy link
Copy Markdown
Collaborator

@Arsolitt the intent is clear, but out of curiosity, can you share a bit more around the use case you have now?

@Arsolitt
Copy link
Copy Markdown
Author

Arsolitt commented May 7, 2026

@Arsolitt the intent is clear, but out of curiosity, can you share a bit more around the use case you have now?

hi, i use sing-box for more flexible routing, it can send traffic to any network interface on host

sing-box TUN inbound pushes everything into sing-box tun interface, and from there it routes some of the traffic through an exit node in netbird based on my own rules.

as a workaround i can either run an infinite loop in a bash script that keeps deleting default route through netbird, or maintain my own builds for all platforms

Arsolitt added 17 commits May 7, 2026 13:19
Add optional bool disable_default_route field (tag 35) to the
SetConfigRequest protobuf message and wire it through the daemon
server handler to ConfigInput.
Add DisableDefaultRoute field to ConfigInput, Config, EngineConfig
structs and wire it through the config update/create logic and
engine config mapping.
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.
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.
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.
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.
Signed-off-by: Arsolitt <arsolitt@gmail.com>
Signed-off-by: Arsolitt <arsolitt@gmail.com>
The Login handler does not read Disable* fields from LoginRequest -
config is applied via SetConfig instead. Remove the dead field to
avoid confusion.

Signed-off-by: Arsolitt <arsolitt@gmail.com>
Make non-default route assertion unconditional to prevent silently
skipping verification when AddVPNRoute fails due to missing
network interface.

Signed-off-by: Arsolitt <arsolitt@gmail.com>
When ErrIgnore is returned by the add function, the refcounter never
stores the key, so decrement early-returns before calling remove.

Signed-off-by: Arsolitt <arsolitt@gmail.com>
Signed-off-by: Arsolitt <arsolitt@gmail.com>
…efcounter

Use the manager's real routeRefCounter (created by setupRefCounters) instead
of replacing it with a test double. This ensures the test actually validates
the production wiring in manager.go: if the disableDefaultRoute guard were
removed, the default routes would no longer return ErrIgnore and the
refcounter presence assertions would catch the regression.

Also extend coverage to IPv6 default route (::/0) alongside IPv4 (0.0.0.0/0),
matching the dual-check in production code.

Signed-off-by: Arsolitt <arsolitt@gmail.com>
Signed-off-by: Arsolitt <arsolitt@gmail.com>
…ilable

Signed-off-by: Arsolitt <arsolitt@gmail.com>
…te config

Signed-off-by: Arsolitt <arsolitt@gmail.com>
Arsolitt added 11 commits May 7, 2026 13:20
Signed-off-by: Arsolitt <arsolitt@gmail.com>
Move noop check before real counter creation so the disableDefaultRoute
closure is only allocated when it will actually be used.

Signed-off-by: Arsolitt <arsolitt@gmail.com>
Replace WireGuard-dependent test with spy-based unit test that
verifies the refcounter filtering logic for both IPv4 and IPv6
default routes without requiring elevated privileges.

Signed-off-by: Arsolitt <arsolitt@gmail.com>
Signed-off-by: Arsolitt <arsolitt@gmail.com>
Signed-off-by: Arsolitt <arsolitt@gmail.com>
Signed-off-by: Arsolitt <arsolitt@gmail.com>
…oute

Signed-off-by: Arsolitt <arsolitt@gmail.com>
TestDisableDefaultRouteWatcherCreation verifies that with DisableDefaultRoute=true,
UpdateRoutes creates watchers in clientNetworks for all routes (default + normal)
while the default route is excluded from the system routing table via ErrIgnore.

Uses noop ref-counters and constructs DefaultManager fields directly (same package),
so the test runs without a real WireGuard interface and is safe in unprivileged CI.

Signed-off-by: Arsolitt <arsolitt@gmail.com>
Signed-off-by: Arsolitt <arsolitt@gmail.com>
…dateRoutes

The behavior is fully covered by dedicated tests that run without
elevated privileges: TestDisableDefaultRouteSkipsSystemRoute and
TestDisableDefaultRouteWatcherCreation.

Signed-off-by: Arsolitt <arsolitt@gmail.com>
@Arsolitt Arsolitt force-pushed the feature/disable-default-route branch from 626d6a7 to abf0baf Compare May 7, 2026 10:51
Signed-off-by: Arsolitt <arsolitt@gmail.com>
@Arsolitt Arsolitt force-pushed the feature/disable-default-route branch from abf0baf to 6bba1b1 Compare May 7, 2026 12:24
Arsolitt added 2 commits May 7, 2026 16:58
Split the monolithic apply method into focused helper methods grouped
by concern: network, routes, server, interface, and credentials.
No behavior changes.

Signed-off-by: Arsolitt <arsolitt@gmail.com>
Extract repeated field-update patterns into generic helpers to bring
all methods under the SonarQube complexity limit of 20.

Signed-off-by: Arsolitt <arsolitt@gmail.com>
@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud Bot commented May 7, 2026

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants