Security
netscope is alpha software and has not received an independent security audit. Treat it as a convenience tool for scoping one application’s traffic, not as a hardened security boundary against a hostile local user or a compromised kernel.
Reporting a vulnerability
Report privately through GitHub Security Advisories. Do not open a public issue for a vulnerability.
Include the netscope version, distribution, kernel, steps to reproduce, and whether the daemon runs under systemd or manually. A reviewed support bundle helps; see Support bundle. We aim to acknowledge a report within seven days and to allow a fix and release before public disclosure.
Trust boundary
netscoped runs as root. It must, because it creates cgroups, opens a fanotify permission group, and invokes nft and ip. The privileged surface is deliberately small:
| Component | Privilege | Exposure |
|---|---|---|
netscoped | root | Unix socket, fanotify, child nft/ip processes |
netscope CLI | user | Unix socket |
| Kernel | — | cgroup v2, fanotify, nftables, policy routing |
The daemon opens one Unix socket at /run/netscope/control.sock. It does not open a TCP or UDP listener, does not serve HTTP, and has no remote UI, telemetry, or analytics.
The socket is owned by root:netscope with mode 0660. Only root and members of the netscope group can connect. Joining that group is equivalent to trusting the user to manage profiles for their own UID; do not add untrusted accounts.
Authentication and authorization
There are no passwords or tokens. The daemon uses the peer credentials of the Unix socket connection:
- A member of the
netscopegroup can add and manage profiles owned by their own UID. - Root can manage any profile and can pass
--uidto create one for another user.--uidis refused for non-root callers. - A profile records its owner UID. A non-root user cannot remove, enable, or disable another user’s profile.
- The CLI does not send a claimed UID that the daemon blindly trusts; ownership comes from the socket peer, except for the root-only
--uidpath.
Input validation
All requests are JSON lines with a 256 KiB request limit and a 256 KiB response limit. The daemon rejects malformed JSON, oversized requests, and unknown operations. The CLI checks the status response’s API version and refuses a daemon with a different protocol version; the current version is 2.
Before any host change, the daemon validates:
- Profile IDs: 1–32 characters,
[a-z0-9][a-z0-9-]*. The ID is used in cgroup and nftables table names, and-is mapped to_for nftables. - Executable paths: canonicalized. The daemon records the device and inode and matches on that identity, re-resolving it when the file at the recorded path changes.
- Windows PE paths:
--winetakes an absolute path. The daemon canonicalizes it, requires a regular file, and requires the first two bytes to beMZbefore recording its device and inode. No ELF check is applied. - Process names:
--processtakes a name, not a path; it is rejected if empty, longer than 64 printable characters, or contains a path separator. The daemon refusesnetscope,netscoped, and the sharedwineserveras name targets. - Steam app IDs: numeric, within the profile target type.
- CIDRs: parsed as IPv4 or IPv6 networks. Invalid input is rejected, not passed through.
- Interfaces and gateways: passed as separate arguments to
ip; no shell is involved. - Numeric values: profile count is bounded at 32, and index, mark, table, and priority allocation is bounded.
- Shared scopes: every consumer must resolve to the profile’s owner UID, and a non-root caller cannot name another UID as
target_uid. One route policy is allowed per profile; a second consumer’s route request is refused. Filter policies compose, so a consumer cannot remove another consumer’s filter by replacing its own policy.
The daemon never invokes a shell, never evaluates a profile-controlled string as shell code, and never writes attacker-controlled text into an nftables rule without parsing or a fixed grammar.
Root daemon risks
A root daemon is a real risk if it has a bug. Reduce exposure:
- Keep the
netscopegroup small. Membership is a privilege. - Keep
/etc/netscope/config.tomland/var/lib/netscope/state.jsonroot-owned and not writable by group members. - Do not expose
/run/netscope/control.sockthrough a bind mount, a container, or a network filesystem. - Run the shipped systemd unit, which supervises and restarts the daemon. It sets
NoNewPrivileges=yes,ProtectSystem=strict,ProtectHome=read-only,PrivateTmp=yes,RestrictNamespaces=yes, and a bound set of system calls.ProtectControlGroups=noandProtectKernelModules=noare deliberate: the daemon must write the cgroup hierarchy, andnft_socketmay autoload on the first cgroup rule. - Review the unit’s sandboxing before changing it. It grants
CAP_SYS_ADMIN,CAP_NET_ADMIN, andCAP_SYS_PTRACE; removing one breaks attachment or rule installation. - Treat a kernel or nftables vulnerability as outside netscope’s boundary; netscope is a consumer of those interfaces.
What netscope does not protect
netscope scopes traffic from processes it successfully attaches to a profile cgroup. It does not protect:
- Sockets that cannot be marked. A socket opened before attachment is marked with the profile mark and matched by
socket mark. Marking requires the root daemon to reach the target’s descriptor table (pidfd_getfd,CAP_SYS_PTRACE); the nftables and routing rules needCAP_NET_ADMIN. If a descriptor cannot be reached, the daemon logsmark profile=... error=...and that socket keeps its original route. A--processtarget for an already-running app attaches late, so this is the case to watch for name targets; a new Wine/Proton launch is caught by the loader hold before its first socket. Restart the app if the log reports a failure. - Mark collisions. The nftables rules match any socket carrying the profile’s mark, not only sockets the daemon marked. A process with
CAP_NET_ADMINcan set the same mark on its own socket and become subject to the profile. The mark base (0x4e53) is intended to be private to netscope; another tool that uses those values can collide. - Name collisions. A
--processtarget matches every process with thatcommor argv basename, including another user’s process if it runs under the same UID. Both are attached. Use--exeor--winewhen the identity must be exact. - Non-Wine processes that open a selected PE. A
--wineprofile marks the file, so the daemon sees every open of it, but it attaches only a Wine process whose command line names the selected PE. Any other process that opens the same file — a file manager, an indexer, a scanner — is allowed and left unscoped.wineserveris never attached; it may open the PE for a loader, but it is shared across Wine applications. Children the attached process starts stay in the same cgroup. - Processes that escape attachment. A process that was never matched by the exec hook or the poller is unscoped. Privileged processes can move themselves out of the cgroup; netscope does not fight that.
- Kernel-generated traffic with no local socket. Forwarded packets, some ICMP errors, and other locally generated traffic that is not attributed to a socket in the cgroup are not matched.
- Traffic forwarded for another host. A router or NAT workload forwarding packets on behalf of another machine is out of scope.
- DNS decisions. netscope matches destinations, not names. A name that resolves to an unexpected address is filtered or routed by address.
- The tunnel itself. Route mode does not create, authenticate, or encrypt a tunnel. It sends marked traffic to an interface you configured.
- A determined local attacker with root. Root can change cgroups, nftables, and routes. netscope is not a defense against root.
Fail-open behavior
Route mode fails closed only while its rules are installed and the tunnel is down. Removing the profile, a clean daemon stop, a reboot, or manual removal of the table is fail-open: marked traffic falls through to the main routing table. See Route mode.
Filter mode is fail-open in the same operational sense: if the profile or daemon is removed, packets are no longer dropped.
Supply chain and build
- The workspace is dual-licensed MIT OR Apache-2.0.
- No dependencies are vendored; builds use crates.io through Cargo.
- Release tarballs should ship with a SHA-256 file; verify it before installing.
- CI runs formatting, Clippy, tests, a release build, and an mdBook build.
- No external assets, fonts, analytics, or tracking scripts are loaded by these docs or the binaries.