Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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

OptionMeaning
--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-lanRoute LAN destinations through the tunnel instead of bypassing them.
--allow-ipv6Carry IPv6 through the profile. Without it, all IPv6 from the profile is dropped.
--udpMark 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:

  1. accepts loopback (127.0.0.0/8, ::1/128);
  2. unless --include-lan, accepts the LAN and link-local ranges listed below;
  3. unless --allow-ipv6, drops all IPv6;
  4. with --udp, accepts the --except-port destinations and the --except-sport sources, 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:

RouteMetricEffect
default dev <iface> [via <gateway>]100Used while the interface has a usable route.
blackhole default1000Used 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:

FamilyBypassed destinations
IPv410.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16, 224.0.0.0/4
IPv6fc00::/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.

--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.

StateRule
FailureThree consecutive failed probes take a link from up to down.
RecoveryTwo consecutive successful probes take it from down back to up.
WinnerThe first link in --link order whose phase is up.
FailbackA recovered higher-priority link takes over only after its second success, so a flapping link does not bounce the route.
All downNo 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 disable stops enforcement for that profile.
  • netscope remove deletes the nftables table, the policy rule, and the routes. Marked traffic then falls through to the main table.
  • A clean systemctl stop netscoped removes 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.