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:
| Stack | Implementation | Notes |
|---|---|---|
native | QuicProxy's own user-space stack, pure Rust | Experimental, opt-in |
system | TCP handled by the OS kernel, UDP in user space | Default on mobile |
lwip | lwIP, written in C | Default on desktop |
smoltcp | smoltcp, written in Rust | Optional |
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.
| Stack | Wake-ups with no traffic |
|---|---|
| native | 0 per second: only wakes for a packet, an application read/write, or a timer that is actually due |
| system | 0 per second (cleans its NAT table every 30 s) |
| lwip | 4 per second: checks timers every 250 ms, forever |
| smoltcp | up 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.
| Scenario | Upload (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.
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.