English | Русский
An authenticated QUIC tunnel with local SOCKS5 and HTTP proxies. Each TCP connection uses an independent HTTP/3 CONNECT stream; UDP uses QUIC DATAGRAM. The client reconnects in the background and the server enforces destination ACLs.
Protocol v2 prioritizes bounded resource use, cancellation and correct forwarding. It requires Go 1.27.2 and quic-go 0.64.0. Client and server must both use v2.
- Independent streams avoid TCP-in-TCP retransmission coupling. Congestion control and the connection receive window remain shared, so flows still compete.
- QUIC is not automatically faster than direct TCP. CPU, RTT, loss, payload size and the host network stack determine the result. Use the included comparison.
- A lost tunnel terminates existing TCP connections. New requests wait within a deadline and recover after reconnect. There is no transparent TCP session replay or guarantee that a network/IP change preserves an active connection.
- UDP is unreliable and has an 850-byte payload limit in both directions. Larger packets are rejected and counted; there is no fragmentation/reassembly. Applications requiring larger UDP packets need another transport.
- Compression is off by default. Enable it on both endpoints for compressible traffic only. Bounded 32 KiB blocks independently choose raw or DEFLATE encoding. Raw blocks still add a 5-byte header and incur a compression trial; TLS, video and archives usually gain nothing. Compressing mixed secrets and attacker-chosen plaintext can expose length information.
The client verifies the server's SHA-256 SPKI pin in the TLS 1.3 handshake. Both
peers then authenticate with HMAC over a shared PSK and the TLS exporter for that
connection. This also runs after TLS session resumption. Authentication and proxy
data are never sent as 0-RTT early data. Tickets improve reconnect handshakes;
disable_session_resumption: true disables the client ticket cache.
TCP and UDP egress require authentication. ACLs check ports, the requested name and the actual resolved IP before opening a socket. DNS resolution is bounded; the checked address is used directly so the dial cannot resolve it again. Deny rules take precedence. A domain allow rule still cannot bypass an IP deny rule. Empty allow lists permit all destinations, including private networks: configure an egress policy appropriate to your server. The PSK grants shared access, with no per-user isolation or credentials.
Local proxies have no user authentication and bind to loopback by default.
Non-loopback binds require allow_remote_proxy: true; use them only with an
external access policy. SOCKS UDP associations are bound to their TCP peer and
UDP source port. SOCKS authentication methods other than no-auth and fragmented
SOCKS datagrams are not supported.
The optional HTTP/3 site (decoy.mode: builtin | proxy | dir | off) is off by
default. It offers no indistinguishability guarantee against active probes or
traffic analysis. The tunnel uses recognizable HTTP/3 CONNECT authorities and a
version header. This is a custom protocol, not interoperable MASQUE CONNECT-UDP.
cp configs/server.example.yaml configs/server.yaml
cp configs/client.example.yaml configs/client.yaml
# Writes new cert.pem/key.pem in configs and prints a PSK and pin.
# Existing certificate/key files are never overwritten.
go run ./cmd/keygen -cert -out configsSet psk in both files, pin and server in the client file. Keep the generated
server certificate and private key across restarts. Certificate/key/site paths
are relative to the configuration file. Store the PSK and private key securely;
never commit your filled configuration files. An ephemeral certificate requires
explicit allow_ephemeral_cert: true and changes the pin on every restart.
go run ./cmd/server -config configs/server.yaml
go run ./cmd/client -config configs/client.yaml
curl -x socks5h://127.0.0.1:1080 https://example.com
curl -x http://127.0.0.1:8080 https://example.comPlain HTTP supports keep-alive, streamed bodies, 100-continue, early responses and protocol upgrade. CONNECT and SOCKS preserve TCP half-close, allowing a peer to finish sending its response after the other side sends FIN. Application idle timeouts still apply.
# Run once on Linux after generating the files. Preserve the private key mode.
sudo chown 10001:10001 configs/key.pem configs/cert.pem configs/server.yaml
sudo chmod 600 configs/key.pem configs/server.yaml
# The directory must be traversable by UID 10001.
docker compose up --build -dThe pinned multi-stage build produces a scratch image with CA certificates and two static binaries. It runs as UID/GID 10001. Compose uses a read-only root filesystem, drops capabilities, forbids privilege escalation and rotates logs. It publishes UDP 4242. Metrics bound to container loopback are only accessible inside its network namespace; intentionally configure and restrict access when using an external collector. The key-generation helper can also run from the image with a writable directory explicitly mounted and owned by UID 10001.
Examples list every supported operational limit. Unknown YAML keys, multiple YAML documents, invalid pins, mismatched cert/key paths, invalid ports and inconsistent receive windows fail at startup.
| Control | Default | Scope |
|---|---|---|
| Handshake/auth deadline | 10 s | Each tunnel |
| TCP open/reply deadline | 15 s | Each request |
| Server connections | 256 | Whole server, including authentication |
| Proxy streams | 128 plus one control stream | Each server connection |
| Pending client requests | 256 | Whole client |
| Local proxy connections | 256 | Shared between HTTP and SOCKS |
| Local handshake / application idle | 10 s / 300 s | Each proxy connection |
| HTTP response headers | 120 s, 64 KiB | Each forwarded response |
| DNS | 5 s, 32 concurrent | Whole server resolver |
| UDP flows / new flows | 256 / 32 per second | Each tunnel |
| UDP queues / admission workers | 128 / 4 | Each tunnel; existing flows have a separate queue |
| UDP flow idle | 60 s | Each outbound socket |
| Client UDP sessions | 128 | Whole client |
| Maximum stream / connection receive window | 16 / 24 MiB | Each tunnel |
Windows grow on demand; these limits are not a total process memory budget. Choose smaller connection/flow limits for a single-user or low-memory host and measure RSS and CPU under the expected concurrency. For a high-bandwidth link, size the receive window against bandwidth-delay product and available memory. No BBR selection is exposed by this application.
Shutdown cancels listeners, pending opens, streams and UDP workers and waits for them to exit. The server releases its UDP socket for immediate same-port restart. Limits reject excess work instead of creating unbounded waiters. Existing UDP flows continue while new-flow DNS work is saturated.
Set metrics_listen to expose Prometheus /metrics (empty disables it). Keep the
endpoint private. Labels have bounded cardinality; destinations are not labels.
wanopt_relay_bytes_total{dir}counts successfully relayed payload continuously, including HTTP and UDP. It does not measure QUIC wire bytes or compression ratio.wanopt_active_streams,wanopt_streams_total,wanopt_dial_errors_total.wanopt_tunnel_connectionsandwanopt_tls_resumptions_totalcount authenticated tunnels.wanopt_tunnel_reconnects_totalcounts reconnect attempts, including after failed authentication.wanopt_tunnel_rtt_secondsis the maximum current smoothed RTT;wanopt_congestion_window_bytesis the sum over active tunnels.wanopt_lost_packetsis a gauge summing cumulative packet-loss declarations across active authenticated tunnels. Retransmission or a later spurious-loss report does not erase that history; disconnecting a tunnel decreases the sum. Older experimental BBR builds did not populate these statistics.wanopt_udp_flows,wanopt_udp_datagrams_total{dir}andwanopt_udp_dropped_total{reason}report UDP load and rejected/lost packets.
Connection statistics are sampled at scrape time without per-packet tracing.
go mod verify
go vet ./...
go test ./...
go test -race ./... # Linux/CGO toolchain
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -trimpath -o bin/wanopt-server ./cmd/server
GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build -trimpath -o bin/wanopt-client.exe ./cmd/clientCI runs Linux and Windows tests, Linux race tests, fuzz smoke tests, govulncheck, cross builds and the Docker build. Regression tests use real loopback QUIC/TLS, TCP and UDP, including unauthenticated egress rejection and oversized QPACK headers. They cover cancellation, capacity recovery, server restart, resumed pin validation, HTTP edge cases, SOCKS source checks and malformed codecs.
See benchmark instructions for direct TCP, TCP relay and QUIC comparisons with reproducible RTT/loss injection. Measurements are local experiments and do not replace a soak test on the deployment network.
- Back up the existing binaries and configuration, including the persistent key.
- Update both endpoints together. v2 replaces custom stream hijacking with standard HTTP/3 CONNECT and changes the compressed stream framing. v1 peers are incompatible; in-flight sessions cannot be migrated during upgrade.
- Copy the new example settings and transfer your endpoint, PSK, pin and ACL. Generate a persistent cert/key only if you do not already have one, and update the pin when changing keys. Compression now defaults to off, as does the site.
allow_0rttandenable_0rttare accepted only as ignored legacy keys. Usedisable_session_resumptionfor session tickets. ALPN must beh3.- Update dashboards for the new RTT/loss/resumption semantics and validate your required UDP packet sizes. Rollback requires restoring both v1 endpoints and their configuration together; do not mix protocol generations.
See high-latency transport notes for reviewed QUIC/BBR, KCP, SRT and Hysteria ideas, the HTTP compression fix, and cloud benchmark limits. Ordinary HTTP forwarding now honors the opt-in compression setting as well as SOCKS and CONNECT; it safely falls back when the server declines compression.
For global admission, adaptive compression, bounded PSK/pin rotation, Reno/BBR selection and measurement controls, see operations and limits.
The UDP send-error follow-up on pr4/pr5 includes the traced cause, regression tests, 72 matrix flows, 30 minutes of mixed load and 18 final-binary invocations. The earlier campaign retains 900 short transfers, 108 sustained flows, faults, controller interruptions and remaining limits.
v0.2.0 - pr4/pr5 report, complete test plan, real-channel cases.