L4 TCP/UDP proxy
Not everything is HTTP. L4 proxy hosts forward raw TCP and UDP streams: a database, a game server, an SSH endpoint, anything that speaks its own protocol. Like HTTP hosts, each can carry notes, shown as an icon beside its name.
| Name | Listening | Matcher | Upstream | Status |
|---|---|---|---|---|
postgres | 5432/tcp | - | db:5432 | Active |
minecraft | 25565/tcp | - | mc:25565 | Active |
mqtt | 8883/tcp | SNI mqtt.example.com | mosquitto:8883 | Active |
wireguard | 51820/udp | - | wg:51820 | Port pending |
game servers | 27015-27030/udp | - | srcds on the listen port | Active |
Tick hosts in the list to enable, disable or delete them together. As for proxy hosts, a batch is
all or nothing, audited per host and applied once; the API has it as
the bulkL4ProxyHosts mutation.
What it can match on
Section titled “What it can match on”- Port - the listening port on the Caddy host.
- TLS SNI - several TLS backends can share one port, routed by the name in the handshake. This works without terminating TLS, so the certificate stays with the backend - though a host can terminate TLS itself when that is what you want.
- HTTP Host - plain-HTTP backends share a port the same way, routed by the Host header.
- PROXY protocol - connections that arrive wrapped in a PROXY protocol header, from a load balancer in front of Caddy.
Port ranges
Section titled “Port ranges”The listen address can name a range, such as :27015-27030, of up to 1000 ports - for game
servers, RTP media, or passive FTP. By default every connection goes to the upstreams’ own port as
written. Turn on Upstreams get the listen port and each upstream becomes a host without a port:
a connection that arrived on 27020 is sent to port 27020, so one host forwards the whole range.
That works because Caddy’s container publishes each port as itself. Active health checks are not
available in that mode, since there is no single port to probe; passive ones still work.
Hosts share a port only with the exact same listen address, so a range that overlaps another host’s port on a different address is refused - Caddy could not bind both. The IP rules, geo blocking and CrowdSec apply to a range host as to any other.
PROXY protocol
Section titled “PROXY protocol”Sent to the backend in v1 or v2, so it sees the real client address instead of Caddy’s, and accepted from a load balancer in front for the same reason. The backend has to be configured to expect it - sending PROXY protocol to something that is not listening for it breaks the connection outright.
Load balancing and health
Section titled “Load balancing and health”Seven selection policies, with active and passive health checks - a smaller set than the HTTP hosts have, since a stream has no URI or status to check: port, interval and timeout for active checks, fail duration and max fails for passive ones. There are no retries: a try duration and interval keep a new connection looking for a healthy upstream instead.
Geo blocking below HTTP
Section titled “Geo blocking below HTTP”Geo blocking applies here too, per host. At layer 4 there is no response body to send, so a blocked connection is closed rather than answered. On a host that accepts PROXY protocol, the country is looked up for the client address in the header, not the load balancer’s.
IP rules from an access list
Section titled “IP rules from an access list”An L4 host can use an access list, and its IP rules decide who gets a connection: one from an address the list denies is closed before anything is proxied. Only the IP rules apply - there is no request at this layer to ask a password in - so the editor offers only lists that have some, and a list an L4 host uses keeps at least one rule. Behind a load balancer, turn on Accept inbound PROXY protocol: the rules, like geo blocking, then check the client in the header rather than the balancer.
With CrowdSec set up, an L4 host also closes connections from addresses CrowdSec has decided against, after the IP rules and geo blocking, unless its CrowdSec section opts it out.
Ports are managed for you
Section titled “Ports are managed for you”A published port has to exist on the container, and container ports are fixed when the container is created. Adding an L4 host therefore needs the Caddy container recreated - the agent writes a compose override with the new port list and recreates Caddy, so the port appears without you editing a compose file. The dashboard shows when a port change is pending, and Apply Ports is what recreates the container - it is not done behind your back.
An agent publishes at most 2000 ports. Docker starts a docker-proxy process for every published
port and address family, so for large ranges set "userland-proxy": false in Docker’s
daemon.json - which also keeps client addresses intact, as above. An agent older than the
controller is sent a range one port at a time, and takes it just the same.