Let the agent run unprivileged against a prepared TUN interface
Creating a network interface needs CAP_NET_ADMIN, but that is a one-time setup step rather than something the agent must hold for its whole life. SystemTunFactory now attaches to an interface that already exists and only creates one when it does not. A persistent interface created by root and owned by the user therefore lets the agent run with no privileges and no capabilities at all. When attaching, nothing is reconfigured, since doing so would need exactly the privileges we are avoiding. New `tsunagi tun-setup` prints the three commands to run once as root, resolving the derived interface name and overlay address for the network. This also fixes a real gap: the overlay address was passed to the factory and thrown away, so an interface the agent created had no address and could never have received anything. The `tun` crate sets addresses through an IPv4-only ioctl and cannot assign an IPv6 one at all, so the agent now verifies the address is present via /proc/net/if_inet6 and refuses with the exact command to run instead of coming up broken. Doing it in-process would mean speaking netlink, which is not implemented and is recorded as such. Not verified on this machine: no sudo is available here, so the privileged setup and the attach path were not executed end to end. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+15
-3
@@ -22,7 +22,13 @@ system can hand us IP packets, and even that is behind a trait
|
||||
| | needs privileges | what it proves |
|
||||
|---|---|---|
|
||||
| `MemoryTunFactory` | no | handshake, encryption, routing, address ownership |
|
||||
| `SystemTunFactory` | `CAP_NET_ADMIN` | traffic actually reaches the OS |
|
||||
| `SystemTunFactory`, attaching | none, if the interface was prepared | traffic actually reaches the OS |
|
||||
| `SystemTunFactory`, creating | `CAP_NET_ADMIN` | the same, at the cost of a capability |
|
||||
|
||||
`SystemTunFactory` attaches to an interface that already exists and only
|
||||
creates one when it does not. A persistent interface created by root and owned
|
||||
by the user lets the agent run with no privileges at all; see *Running
|
||||
unprivileged* in [../README.md](../README.md#running-unprivileged).
|
||||
|
||||
[boringtun]: https://docs.rs/boringtun
|
||||
[`TunFactory`]: https://docs.rs/tsunagi
|
||||
@@ -184,5 +190,11 @@ async fn main() -> Result<()> {
|
||||
* **Userspace costs CPU.** Kernel WireGuard is faster. A kernel backend could
|
||||
return behind the same boundary, but it would give up transport-provided NAT
|
||||
traversal unless paired with a local proxy.
|
||||
* **The system interface path is barely exercised by the default suite**,
|
||||
because it needs privileges. Everything else about the data plane is.
|
||||
* **The agent cannot assign the overlay address itself.** The `tun` crate sets
|
||||
addresses through an IPv4-only ioctl, so the IPv6 overlay address must come
|
||||
from `ip -6 address add` or an equivalent. The agent verifies the address is
|
||||
present, via `/proc/net/if_inet6`, and refuses with the exact command rather
|
||||
than running an interface that could never receive anything. Doing it
|
||||
in-process would mean speaking netlink, which is not implemented.
|
||||
* **The system interface path is not exercised by the default suite**, because
|
||||
it needs privileges. Everything else about the data plane is.
|
||||
|
||||
Reference in New Issue
Block a user