Skip to content

Raw TCP and UDP

For traffic that is not HTTP — Postgres, SSH, a binary protocol — the proxy gates the connection once and then splices bytes.

net {
allow_tcp ["db.internal:5432", "ssh.internal:22"]
allow_udp ["ntp.internal:123"]
deny_tcp ["*:25"]
}
allow_tcp ["db.internal"] # ← error
allow_tcp ["db.internal:*"] # every port, said explicitly

A missing port is never an implicit :*. Forgetting one cannot quietly grant every port on a host.

Compared to an allow_http block:

allow_httpallow_tcp
Per-request rulesyesno
Credential injectionyesno
Learn-mode detailmethod and pathdestination only

So prefer allow_http for anything HTTP — see allow_http or allow_tcp? for the full tradeoff. One destination cannot have both: naming it in each is a resolve-time error, because the opaque grant would carry the same traffic uninspected and the request rules would be decoration.

Raw grants are where the two backends diverge most.

macOS, capturemacOS, fallbackLinux
TCPtransparentSOCKS5 via ALL_PROXYtransparent
UDPconnected sockets, QUIC includednot available — deniedtransparent where xt_TPROXY exists

On Linux and under macOS transparent capture, any client is intercepted with nothing to configure. The macOS fallback (no system extension installed) is the constrained case — the client must speak SOCKS5, and allow_udp grants nothing — and a couple of clients (libpq, psycopg, most MySQL/Redis drivers) do not speak SOCKS5 at all. The macOS reference has the detail, including which flows need an IP rule rather than a name. Every failure fails closed: a flow wardcell cannot name or route loses egress rather than bypassing the gate.

deny_tcp and deny_udp block their own protocol. deny_hosts blocks every protocol to a destination, which is usually what you want:

deny_hosts ["telemetry.example.com:*"] # http, tcp and udp