A developer has published a detailed technical account of building PAYPHONE, a custom IPv4 VPN written in Rust that uses QUIC as the primary transport and an optional TLS-over-TCP fallback. The project was created to give the author full control over packet handling, routing, and traffic shaping while exploring techniques that might resist the detection methods used against standard WireGuard in Russia.
Applications on the client send packets to a TUN interface. The client reads these packets, prepends a 16-byte PAYPHONE header containing version, type, flags, length, and sequence number, then transmits them over QUIC datagrams or an obfuscated TLS stream. On the server the header is stripped and the original IP packet is written to the server-side TUN, after which Linux routing and NAT forward it to the internet. Return traffic follows the reverse path.
The protocol deliberately uses QUIC datagrams rather than a single ordered stream so that loss of one packet does not stall unrelated TCP connections inside the tunnel, following the approach described in RFC 9221. The implementation relies on the Quinn library; an early mistake with send_datagram_wait was corrected by switching to non-blocking send_datagram calls and pre-checking datagram size.
Authentication uses short-lived Ed25519-signed tokens that encode subscription limits. The server re-validates tokens on every session event and maintains a list of revoked identifiers. Rate limiting is performed with a token-bucket algorithm tied to the subscription record.
Several practical problems surfaced after the first working connection. An initial MTU of 1280 exceeded Quinn’s available datagram budget once the 40-byte PAYPHONE header and QUIC overhead were added, producing TooLarge errors under load. The value was reduced to 1100 with dynamic adjustment based on the path’s max_datagram_size. On macOS, the /32 route to the server was occasionally removed by networksetup calls, causing the tunnel to route its own QUIC packets back into the TUN; the final solution combined a persistent /32 route with IP_BOUND_IF socket binding.
Tokio timer handling also required correction. Creating a fresh sleep inside each select! iteration prevented PING and route-check timers from ever firing. The timers are now created once before the loop and reset after each expiration. The client additionally polls routes every 400 ms to restore them after network configuration changes.
Obfuscation that rounded QUIC packet lengths to fixed sizes (128, 296, 568, 1200, 1440) was later removed because it distorted QUIC’s path-MTU discovery probes. Only random padding between 0 and 32 bytes is now applied after XOR masking.

