Repository navigation
tunless 0.2.0
The first release that is not a preview. What changed is not the feature list —
it is that a defect class this project was built to avoid was found on a live
machine, understood, and closed, and the platform it affected now has evidence
behind it rather than only tests.
tunless catches connections at the socket layer and hands them to a SOCKS5
proxy you already run, so unmodified applications go through your proxy without
a TUN device, a fake-IP pool, or a rewritten routing table. The kernel still
knows the hostname and the calling process at that layer, so nothing has to be
reconstructed or faked, and if tunless stops, connections go out the normal
way.
Platform maturity
| Platform | Ships as | Because |
|---|---|---|
| Linux | Generally available | eBPF capture for TCP and UDP over both address families, on the host and inside container namespaces, from a reproducible embedded BPF object, with dated throughput, footprint, filter, and recovery evidence, plus a netfilter fallback for kernels the eBPF backend cannot run on |
| macOS | Beta | The resolver-lifetime defect below was found and fixed on a live host, but the fix itself has not been qualified on one: a locally signed system extension cannot activate while SIP is on, so installing it requires the notarized build path. Clean-machine qualification of the exact candidate is open |
| Windows | Source only, not a shippable artifact | The WFP backend is implemented, but WDK build, Driver Verifier, runtime, UDP, fuzzing, and Microsoft attestation gates are all unmet, and loading a driver on Windows 10 or later needs a signature this project does not hold |
No Windows binary or driver is attached to this release. The Windows source
is a design to read and build on, not a download.
The defect this release exists for
On macOS, a captured UDP flow used to be closed whenever the transport under it
failed — a watchdog pause, a SOCKS5 handshake that timed out against a busy
mixed port, an upstream that dropped its UDP association, or two minutes of an
idle one.
macOS treats a datagram flow the provider closed as final. It does not
re-capture the socket above it and does not hand it back to the kernel, so
every later send on that socket fails locally. mDNSResponder holds one
resolver socket per delegated client and never replaces it. One closed flow
therefore ended name resolution for one application, for as long as the
daemon ran, while every other program on the host resolved normally — which
reads like a problem with the site, not with DNS.
Measured on a live host: a single watchdog pause at 16:47 produced 6,307
sending ... failed: [22: Invalid argument] messages over the following five
hours, with one application's lookups timing out at thirty seconds throughout.
The association is now the disposable half. It is retired and rebuilt
underneath a flow that stays open; while it cannot carry them, datagrams go out
directly, exactly as they would if tunless were not installed; and the
application's socket is never touched.
Also in this release
- Flow attribution: what produced a flow, not just where it was going.
- Kubernetes capture scope: pick a node's pod hierarchy without capturing
the agent's own traffic, with the remaining gap reported rather than worked
around. - A netfilter fallback backend for hosts the eBPF backend cannot run on —
an older kernel, a distribution that disables unprivileged BPF, or an
operator who will grantNET_ADMINand notCAP_BPF. It gives up fail-open
and cannot attribute processes, and says so rather than silently matching
nothing.autonever selects it. - DNS probes that read what the upstream actually sent. Both probes decoded
a relayed answer at a fixed ten-byte offset, which is the SOCKS5 UDP header
length only for an IPv4 source; an upstream answering from an IPv6 address
had a good answer scored as no answer, which told operators UDP relaying was
broken and dropped the watchdog to TCP-only probing for the session. - A UDP association that survives a duplicate answer. A resolver answering
the same query twice produced a datagram the flow correctly refused, and the
association used to end over it, discarding every other query in flight on
that socket. - DNS stays watched with
--disable-dns-override. That flag preserves each
application's resolver; it never stopped capture relaying the query, but it
did switch off the preflight proof, the post-start verification, and the
runtime watchdog. doctorchecks what the selected backend actually needs instead of
reporting every non-eBPF backend as privilege-free.
The complete list is in CHANGELOG.md.
Upgrading from 0.1.0
Linux: replace the binary and restart the unit. Nothing in the datapath's
configuration changed.
macOS: the extension must actually be replaced, which is worth checking rather
than assuming. CFBundleVersion was 14 for two different binaries in 0.1.0,
and macOS keys system-extension replacement on that number, so an install could
leave the earlier code running while the launcher reported the newer version.
This release is build 15, the number now lives in macos/project.yml where the
build reads it, and the release checklist gates on it. After installing, read
the running build back:
systemextensionsctl list | grep tunlessKnown limitations
- The macOS fix has not been qualified on a live host. It is covered by
unit tests that stand a real SOCKS5 upstream up and kill it, and the defect it
closes was diagnosed on a live machine — but a locally signed system extension
cannot activate while SIP is enabled, so running the fixed build requires the
notarized path. Until that has run, treat macOS as beta in the strict sense. - The 48-hour soak has still not been completed. The harnesses exist on
both platforms. Every serious defect this project has found surfaced over
hours rather than minutes, including this release's. - Rootful Podman is unreliable. Podman commands against an attached
container hang in roughly two runs in five on podman 5.8.4 with netavark.
Docker and containerd via kind do not show it. Cause unknown. Use Docker. - The 5.10 kernel floor is unverified on this code. Current kernels are
tested continuously; the oldest supported one was last exercised before the
0.1.0 datapath changes.