Skip to content

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.

The L4 host listSwitch between TCP and UDP, or hover the note on postgres.DemoNothing you change here is saved
L4 Proxy Hosts
5
5 enabled
TCP
3
Connection-oriented streams
UDP
2
Datagram listeners
Agents
2
Bind the ports for these hosts
NameListeningMatcherUpstreamStatus
postgres
5432/tcp-db:5432
Active
minecraft
25565/tcp-mc:25565
Active
mqtt
8883/tcpSNI mqtt.example.commosquitto: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.

  • 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.
The L4 host editorCreate one or edit the sample. Saving shows what the form would send.DemoNothing you change here is saved
Save either one to see what it sends.

Create L4 Proxy Host

L4 Host EnabledThis host is active and proxying connections
Anything worth remembering about this host. Only people who can see the host can read them.
0/2000
Format: :PORT, HOST:PORT, or [IPv6]:PORT - an IPv6 address must be bracketed. PORT can be a range such as 27015-27030, up to 1000 ports. The agent publishes the ports on the Caddy container for you. Ports 80, 443, 2019, 3000, 9090 and the metrics port are reserved.
AgentsChoose which agents serve this host. Leave every box unticked to serve it on all of them.
Assigned agents
  • bundledConnected
  • edge-fraConnected
Served by every agent
No agent is selected, so this host is included in the configuration sent to all of them.
Each connection goes to the port it arrived on, so one host can forward a whole range. Upstreams are then hosts without a port.
One per line in host:port format.
Match incoming connections before proxying. 'None' matches all connections on this port.
Connections from addresses the list denies are closed. Only its IP rules apply here - there is no request to ask a password in - so lists without any are not offered.
Active Health Check
Passive Health Check
One per line. Used for upstream hostname resolution.
Fallback DNS servers (one per line).
Close connections from addresses your CrowdSec Local API has a decision against. Applies once CrowdSec is set up under Settings > CrowdSec. Behind PROXY protocol, the client's address is checked.
Block Rules
ISO 3166-1 alpha-2 codes, comma-separated
AF, AN, AS, EU, NA, OC, SA
Allow Rules (override blocks)
Geo blocking uses the client's direct IP at L4
There is no X-Forwarded-For support here. Blocked connections are immediately closed.
When enabled, upstream hostnames are resolved to IP addresses at config time, pinning DNS resolution.

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.

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.

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 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.

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.

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.