Skip to main content

TUN stacks

With TUN enabled, the operating system hands every IP packet to QuicProxy. The stack turns those packets back into TCP connections and UDP datagrams for routing and proxying, so it largely decides speed, battery use and memory in TUN mode.

Pick the stack with tun_stack on the TUN inbound:

StackImplementationNotes
nativeQuicProxy's own user-space stack, pure RustExperimental, opt-in
systemTCP handled by the OS kernel, UDP in user spaceDefault on mobile
lwiplwIP, written in CDefault on desktop
smoltcpsmoltcp, written in RustOptional

Why native uses less battery​

Zero wake-ups when idle​

With the screen locked, most connections sit idle. A stack that keeps waking up to check timers keeps pulling the CPU out of sleep.

StackWake-ups with no traffic
native0 per second: only wakes for a packet, an application read/write, or a timer that is actually due
system0 per second (cleans its NAT table every 30 s)
lwip4 per second: checks timers every 250 ms, forever
smoltcpup to 200 per second: falls back to a 5 ms poll even with no timers

native keeps every connection's timers in one place and sleeps until the earliest one; with no timers it sleeps until the next packet.

Fewer trips through the kernel​

The system stack hands TCP to the kernel: a segment is read from TUN, rewritten, written back to TUN, processed by kernel TCP and finally read from a local socket, crossing the kernel three or four times. native terminates TCP in user space, so each packet is read once and written once. Less CPU work means less energy.

Memory on demand​

Buffers grow as needed and are released when drained, instead of a fixed buffer pre-allocated per connection as in smoltcp. Mobile uses a smaller profile (32 KB initial receive window, 256 KB max, at most 1024 connections) to stay within the 50 MB limit of iOS Network Extensions.

Other advantages​

  • No reliance on OS quirks: the system stack borrows a fake address in the TUN subnet, binds its listener to utun and may trigger the Windows firewall. native needs none of this and behaves the same everywhere.
  • Faster on lossy networks: SACK, fast recovery and tail loss probes, so a single loss does not wait out a 300 ms+ retransmission timeout.
  • ping works: ICMP / ICMPv6 echo is answered locally; the system stack drops it.
  • Simpler IPv6: when TUN only gets a single IPv6 address (for example /128), the system stack cannot carry IPv6 TCP; native can.
  • Pure Rust: no C dependency, simpler cross-compilation and memory safety.

Measurements​

Setup: a real Linux TUN device, MTU 1500, the kernel's TCP as client, iperf3 and curl reaching local services through the native stack, with random packet loss injected.

ScenarioUpload (client sends)Download (client receives)
No loss~3.1 Gbps~2.9 Gbps
1% loss~1.4 Gbps~480 Mbps (~1.9 Gbps with 8 streams)
5% loss~25 Mbps~67 Mbps
  • A 50 MB download matched its checksum at every loss rate from 0% to 5%.
  • At 3% loss, 800 connections with half-close and server-first close all completed correctly, with no leaked connections.
  • Zero context switches over 10 idle seconds.
note

Upload is sent by the client's kernel and download by the native stack. At 5% loss, native downloads faster than the kernel uploads.

Current limitations​

native is still experimental:

  • Battery use has not yet been measured on real iOS / Android devices; the analysis above comes from the design and the Linux tests.
  • IPv4 fragments are not reassembled and IPv6 extension headers are not parsed; such packets are dropped.
  • The handshake completes as soon as the SYN arrives, so when the proxy side fails to connect the application sees a reset rather than a timeout.

Enabling it​

Set tun_stack on the TUN inbound:

{
"type": "tun",
"tun_address": ["172.19.0.1/24"],
"mtu": 1384,
"auto_route": true,
"tun_stack": "native"
}

If anything goes wrong, switch tun_stack back to system or lwip.