9/23/2026

Per-process routing without TUN

Most transparent proxies on Linux work the same way: create a TUN device, point the default route at it, and run a user-space TCP/IP stack that turns raw packets back into connections. It works everywhere, but it has costs that matter on a developer machine. Specola’s Linux backend takes a different route: it attaches eBPF programs to the cgroup v2 hierarchy and redirects sockets, not packets.

What a TUN device does

With TUN, the kernel hands every routed IP packet to a user-space process. That process has to:

  1. reassemble TCP streams and track UDP flows with its own network stack;
  2. work out which process sent each packet after the fact, usually by scanning socket tables;
  3. change the routing table and system DNS so traffic reaches the device, and restore them later.

Every packet of every connection crosses into user space — including the ones you only want to send direct.

What Specola’s eBPF backend does

Specola attaches small programs to cgroup socket hooks. They run inside the kernel at the moment a program creates a socket, connects, or sends a datagram:

Hook Job
cgroup/sock_create, sock_release remember which process owns each socket
cgroup/connect4 decide per TCP connection whether to redirect it to Core
cgroup/sendmsg4, recvmsg4 the same for UDP, and restore the original peer on replies
cgroup/getpeername4 make redirected sockets still report the original destination
sockops mark accepted connections so Core can match them to their metadata

The decision in connect4 follows a fixed order:

  1. Core’s own sockets and the Specola UI are always bypassed.
  2. DNS queries are sent to Core’s DNS module, so domain rules keep working.
  3. Loopback, local addresses, private and link-local ranges, multicast and broadcast are bypassed.
  4. Everything else is checked against a candidate snapshot that Core pushes into BPF maps: process selectors from your rules, IPv4 ranges from IP and GeoIP rules, addresses that DNS resolved for domains with rules, and the current FINAL target.

A connection that is not a candidate follows FINAL right there in the kernel. With FINAL,DIRECT its socket is left completely untouched: it uses the kernel’s own TCP stack, the normal route, and never reaches user space. A candidate is redirected to Core’s local listener. Core looks up the original destination, socket cookie and process, runs the full ordered rule list, and then goes direct, rejects, or dials the chosen proxy or group.

Why that matters for developers

Current limits

When to still use TUN

TUN is the portable choice and captures everything on the machine the same way, regardless of cgroup placement. If you need IPv6 capture today, or you route a whole network namespace rather than individual tools, type = "tun" is the right backend. Both backends share the same rules, so switching is a one-line change in [enhanced-mode].

We are preparing reproducible benchmarks comparing both backends on the same machine. They will be published here with the scripts to run them yourself.