Route mode
Route mode sends one application’s traffic through an existing tunnel interface. It uses a firewall mark and a dedicated routing table; it does not create the tunnel, configure the interface, or manage WireGuard, OpenVPN, or any other VPN.
sudo netscope --uid "$(id -u)" add routed --exe /usr/bin/curl --route tun0
The interface must already exist and be up. If it does not, the daemon fails to apply the profile and the CLI exits 1.
Options
| Option | Meaning |
|---|---|
--route <iface> | Interface for the default route, for example tun0 or wg0. Required unless --link is given. |
--link <spec> | Ordered failover link, repeatable: <iface>[@<ipv4>][#<ipv6>]. Conflicts with --route and the standalone gateway flags. |
--gateway <ipv4> | IPv4 next hop. Optional when the interface is point-to-point. |
--gateway6 <ipv6> | IPv6 next hop. Only valid with --allow-ipv6. |
--include-lan | Route LAN destinations through the tunnel instead of bypassing them. |
--allow-ipv6 | Carry IPv6 through the profile. Without it, all IPv6 from the profile is dropped. |
--udp | Mark only IPv4 UDP. TCP keeps its normal route, so a TCP proxy can own TCP on the same process. |
--except-port <port> | UDP destination port that stays direct. Repeatable, up to 32; requires --udp. |
--except-sport <port> | UDP source port pinned direct. Repeatable, up to 64; requires --udp. |
--except <cidr> | Destination that stays direct. Repeatable, up to 30. The daemon installs an ip rule to main at a lower priority than the mark rule. Loopback is always kept direct the same way and does not count against the 30. |
--gateway and --gateway6 are mutually exclusive with the wrong family: --gateway6 requires --allow-ipv6, and --gateway is IPv4 only.
What gets routed
The nftables chain for a route profile:
- accepts loopback (
127.0.0.0/8,::1/128); - unless
--include-lan, accepts the LAN and link-local ranges listed below; - unless
--allow-ipv6, drops all IPv6; - with
--udp, accepts the--except-portdestinations and the--except-sportsources, then sets the mark on IPv4 UDP only; without it, sets the mark on everything left.
--except-sport exists for a route that opens over a live flow. A UDP socket keeps its source port for its whole life, and the mark applies to every packet it sends, so marking one mid-session moves the flow to the tunnel and changes the source address the peer sees. Pinning the source port leaves that socket on its normal route; sockets created afterwards are marked and routed as usual. The daemon never marks a pinned socket, so the pin survives later attaches.
The mark is then matched by an ip rule:
fwmark 0x4e53 table 20000 pref 1000
The table contains two defaults:
| Route | Metric | Effect |
|---|---|---|
default dev <iface> [via <gateway>] | 100 | Used while the interface has a usable route. |
blackhole default | 1000 | Used when the real route is gone, so traffic fails instead of falling back to the main table. |
With ordered links the tunnel default is moved between links, or removed entirely while every link is down; the blackhole stays installed either way.
The mark and table values come from the profile index. Table 20000 and mark 0x4e53 are the first profile; the next profile uses 20001 and 0x4e54, up to 32 profiles.
Each --except adds a rule just above the mark rule:
fwmark 0x4e53 to 203.0.113.0/24 table main pref 999
A packet from the profile to an excepted destination matches that rule first and uses the main table; everything else reaches the mark rule and uses the profile table. Exceptions are removed with the profile.
UDP-only routes
--udp narrows the mark rule to IPv4 UDP. TCP from the same process is not
marked and keeps the host’s normal route, which is what lets a TCP proxy and a
UDP route share one application. Excluded UDP ports return with accept
before the mark, so DNS (53) or a game’s own local port can stay direct:
sudo netscope --uid "$(id -u)" open --consumer exitlink --name overwatch \
--process Overwatch.exe --route tun0 --udp \
--except-port 53 --except-port 29523
Late attach still covers sockets that existed before the profile: the daemon sets the same mark on the process’s open descriptors, and only UDP packets are matched by the narrowed rule.
LAN bypass
By default — include_lan = false — route mode bypasses the tunnel for local networks. This keeps printers, game consoles, local services, and router administration reachable while the app’s internet traffic is routed.
The bypass list is fixed:
| Family | Bypassed destinations |
|---|---|
| IPv4 | 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16, 224.0.0.0/4 |
| IPv6 | fc00::/7, fe80::/10, ff00::/8 |
--include-lan (include_lan = true) removes this bypass, so traffic to those ranges is marked and routed through the tunnel. Use it when the tunnel carries the LAN (for example a remote-access VPN that reaches a home network) and the application must use it.
Ranges outside this list, such as CGNAT 100.64.0.0/10, are not bypassed. They follow the profile’s default route.
IPv6
The default is conservative: allow_ipv6: false drops every IPv6 packet from the profile, including link-local traffic that would otherwise bypass the tunnel. This avoids a dual-stack leak when the tunnel carries only IPv4.
To carry IPv6:
sudo netscope --uid "$(id -u)" add routed6 \
--exe /usr/bin/curl \
--route tun0 \
--allow-ipv6 \
--gateway6 2001:db8::1
With --allow-ipv6, the daemon also installs an IPv6 policy rule and an IPv6 default route in the same table. If the IPv6 route cannot be installed, the add fails and the profile is not left applied. If the interface later loses that route, the IPv6 blackhole matches and the traffic fails instead of using the main table. --gateway6 is optional for point-to-point interfaces.
Ordered links and failover
--link replaces --route with an ordered list of interfaces, and a gateway
belongs inside the link spec: --link wlan0@192.168.1.1 is the canonical form.
The legacy --route wlan0 --gateway 192.168.1.1 (with --gateway6 for the
IPv6 half) is still accepted and maps to a single-link profile with the same
fields.
The profile gets one default route at a time, owned by the first link that is
healthy. That single winner is used for both families. A link is probed over
IPv4 when it has an IPv4 source address, and otherwise over IPv6 when the
profile allows IPv6, so a link can be the healthy winner while its other
family has no path. With --allow-ipv6, a winner with no usable IPv6 path
has no IPv6 tunnel default, the IPv6 blackhole matches, and IPv6 from the
profile fails instead of moving to a lower-priority link that could carry it.
A link switch deletes the previous winner’s IPv6 default as well, so the
stale route cannot leak IPv6 past a v6-incapable winner.
sudo netscope --uid "$(id -u)" open --consumer exitlink --name overwatch \
--process Overwatch.exe \
--link enp7s0f4u1 --link wwan0@10.64.0.1
Each link is probed every probe_interval_ms (2000 by default). A probe
binds to that interface, clears any inherited firewall mark, and opens a TCP
connection to probe_target with a one-second timeout; an interface with no
global source address is probed with ICMP through ping -I instead. When the
link spec carries a gateway, the probe first installs a host route for the
probe target (/32 for IPv4, /128 for IPv6) through that gateway on the
link’s interface. Without it, SO_BINDTODEVICE makes the off-link target
resolve as on-link, ARP fails, and a healthy link reads as down. Probe failure
is a health signal, not a daemon error.
| State | Rule |
|---|---|
| Failure | Three consecutive failed probes take a link from up to down. |
| Recovery | Two consecutive successful probes take it from down back to up. |
| Winner | The first link in --link order whose phase is up. |
| Failback | A recovered higher-priority link takes over only after its second success, so a flapping link does not bounce the route. |
| All down | No tunnel default is installed; the profile’s blackhole rejects its traffic. |
The daemon applies the winner’s default route immediately on a change. Its
health reconciler (exists_for/ensure_for) follows the current winner as
well, so a switched default is not reset to the first configured link on the
next tick, and a profile whose links are all down keeps its rules and blackhole.
Status shows the probe state for every link:
NAME MODE ENABLED TARGET ATTACHED CONSUMERS POLICIES PIDS
overwatch route yes name:Overwatch.exe late exitlink exitlink:route 6120
link enp7s0f4u1 10.0.0.1 up 41ms -
link wwan0 10.64.0.1 up 88ms active
status --json carries the same data in the profile’s links array. The
winner is the entry with active: true; healthy mirrors the phase.
This is not a kill switch for the whole machine. It protects traffic that enters the profile — the selected executable, process tree, or Wine launch. A game started outside the scoped path has no mark, no route, and no blackhole, so it is unaffected by failover.
Fail-closed while active, fail-open on removal
The blackhole protects traffic while the profile is active and the tunnel goes down: the real default route disappears, the blackhole at metric 1000 matches, and the packet is rejected.
It is not a system-wide kill switch, and it is not designed to survive the daemon being removed:
netscope disablestops enforcement for that profile.netscope removedeletes the nftables table, the policy rule, and the routes. Marked traffic then falls through to the main table.- A clean
systemctl stop netscopedremoves the daemon’s host state, and manual removal of the table has the same result: the guarantee ends and normal routing resumes. - A reboot clears the rules.
- If the daemon is killed without cleanup, the installed rules can remain. A restart reconciles and re-applies them for enabled profiles; to remove them, stop the daemon and run
sudo netscope cleanup --all. Until they are removed, the blackhole still applies.
In short: daemon death and table removal are fail-open once the rules are gone. The blackhole is a property of the installed rules, not of the daemon’s liveness.
Treat route mode as leak protection for the selected application while netscope is running, not as a firewall that prevents all leaks on the machine. If you need a hard kill switch, enforce it in the tunnel configuration or with a separate firewall policy.
DNS
netscope does not configure DNS. The application’s DNS queries are routed like its other UDP traffic through the tunnel. If the tunnel does not carry DNS or does not push a resolver, name resolution fails even though the interface is up. Configure DNS in the tunnel client or use a resolver reachable through the tunnel.
Queries sent to a local stub resolver are delivered over loopback and are therefore accepted before the mark. The stub’s own upstream queries belong to the stub process and are routed by that process, not by this profile.
Existing connections
A socket that existed before the process was attached is marked with the
profile’s firewall mark. Changing SO_MARK resets the kernel’s cached
destination, so packets sent after the attach use the profile’s routing table.
The connection is not re-established: TCP state, sequence numbers, and the
peer stay as they were, and only the path changes. If a descriptor could not be
marked, the daemon logs mark profile=... error=...; restart the application
to cover that socket.
Interaction with filter mode
A shared profile can carry one route policy and several filter policies. Each filter runs before the route mark. A destination dropped by any filter is never routed; an accepted destination is marked and follows this route. A second consumer asking for a route on a profile that already has one gets an error; route ownership moves only when the owning consumer replaces its own policy. See Shared scopes.
Inspect the routing state
ip rule show
ip route show table 20000
nft list table inet netscope_routed
Mark and table are allocated by profile index: the first profile uses mark 0x4e53 and table 20000, and each additional profile takes the next free index. netscope status --json reports each profile’s mark, table, and nft_table; ip rule show maps a mark to its table.
Removing a route profile
sudo netscope remove routed
The daemon deletes the IPv4 (and, when enabled, IPv6) policy rule, flushes the table, and removes the nftables table. The application keeps running and returns to the main routing table. Restart it if you need its existing connections to follow the new path.