HTTP vs. L4 Routing — When to Use Which?
Explains the difference between HTTP routing (Layer 7, domain-based) and L4 routing (Layer 4, port-based) — when each type is used, how TLS modes work and which features are available in each case.
What does it do?
GateControl supports two fundamentally different routing types:
- HTTP routing (Layer 7): Caddy analyzes the HTTP request, matches by domain name and forwards to the backend. Full access to all HTTP features.
- L4 routing (Layer 4): Caddy forwards raw TCP/UDP traffic without inspecting the content. Matching by port, not by domain.
HTTP routing:
Client → https://app.example.com → Caddy (matched domain "app.example.com") → Backend 10.8.0.3:8080
Client → https://api.example.com → Caddy (matched domain "api.example.com") → Backend 10.8.0.4:3000
↑ Both on port 443, differentiated by domain
L4 routing:
Client → server.example.com:25565 → Caddy (matched port 25565) → Backend 10.8.0.5:25565
Client → server.example.com:2222 → Caddy (matched port 2222) → Backend 10.8.0.6:22
↑ Different ports, domain doesn't matter
HTTP routing (Layer 7) in detail
HTTP routing is the default mode. Caddy terminates TLS, reads HTTP headers and matches based on the Host header.
How it works:
- Client connects to port 443 (or 80)
- TLS handshake — Caddy picks the correct certificate via SNI
- Caddy reads the HTTP
Hostheader - Match against the configured domain → forwarding to the backend
- Full HTTP processing: header manipulation, compression, auth, etc.
All route features are available:
- Force HTTPS with Let's Encrypt
- Backend HTTPS
- Compression (Gzip/Zstd)
- Rate Limiting
- Basic Auth / Route Auth
- Peer ACL
- IP Access Control
- Request Mirroring
- Retry on Error
- Circuit Breaker
- Custom Headers
- Load Balancing (multiple backends)
- Uptime Monitoring (HTTP check)
- Sticky Sessions
L4 routing (Layer 4) in detail
L4 routing forwards raw TCP or UDP traffic. Caddy opens its own port and tunnels the traffic to the backend.
Three L4-specific fields:
Protocol: TCP or UDP
| Protocol | Use cases |
|---|---|
| TCP | SSH, Minecraft, SMTP, databases, most services |
| UDP | DNS, game servers (some), VoIP, WireGuard |
Listen Port
The port that Caddy opens on the GateControl server. This is the port clients connect to.
- Not the target port (the port on the backend)
- Can be equal or different to the target port
- Must be free on the GateControl server
TLS mode
| Mode | Description | Caddy behavior |
|---|---|---|
| None | No TLS | Caddy forwards raw TCP/UDP traffic |
| Passthrough | TLS pass-through | Caddy matches via SNI, forwards encrypted traffic without decrypting |
| Terminate | TLS termination | Caddy decrypts TLS (with LE certificate), then forwards unencrypted TCP to the backend |
Target Port vs. Listen Port
This is the most important distinction and often causes confusion:
| Field | Applies to | Description |
|---|---|---|
| Target Port | All routes (HTTP + L4) | The port on the backend peer where the service runs |
| Listen Port | Only L4 routes | The port Caddy opens on the GateControl server |
Example 1: Same ports
Listen Port 25565 (GateControl) → Target Port 25565 (Minecraft on peer 10.8.0.4)
Client connects to: server.example.com:25565
Example 2: Different ports
Listen Port 8022 (GateControl) → Target Port 22 (SSH on peer 10.8.0.2)
Client connects to: server.example.com:8022
SSH command: ssh -p 8022 user@server.example.com
Example 3: Multiple services, different ports
Listen Port 25565 → Target Port 25565 (Minecraft on 10.8.0.4)
Listen Port 2222 → Target Port 22 (SSH on 10.8.0.2)
Listen Port 5433 → Target Port 5432 (PostgreSQL on 10.8.0.3)
For HTTP routes there is no Listen Port — all HTTP routes share port 80/443 and are differentiated by domain.
TLS modes in detail
None — No TLS
Client ──TCP/UDP──→ Caddy:25565 ──TCP/UDP──→ Backend:25565
unencrypted unencrypted
- No TLS handshake, no SNI
- Caddy does not see the traffic content
- Simplest setup, no certificate required
- Use cases: Minecraft, game servers, DNS, plain SMTP, databases in the VPN
Passthrough — TLS pass-through
Client ──TLS──→ Caddy:443 ──TLS──→ Backend:443
encrypted encrypted (same connection)
- Caddy only reads the SNI (server name) from the TLS ClientHello
- The TLS tunnel is not broken — end-to-end encryption
- The backend must have its own valid certificate
- Caddy cannot inspect or modify the content
- Use cases: Backend with its own LE certificate, strict end-to-end encryption requirements
Terminate — TLS termination
Client ──TLS──→ Caddy:993 ──TCP──→ Backend:143
encrypted unencrypted
(Let's Encrypt)
- Caddy terminates TLS with a Let's Encrypt certificate
- The traffic to the backend is unencrypted (but within the VPN)
- Matched via SNI, domain must be specified
- Use cases: TLS for services that don't natively support it, IMAPS/SMTPS in front of a plaintext backend
Blocked ports
The following ports are reserved by default and cannot be used as Listen Port (list from config/default.js → l4.blockedPorts, adjustable via GC_L4_BLOCKED_PORTS):
| Port | Usage |
|---|---|
| 22 | SSH (host service) |
| 53 | DNS (host service) |
| 80 | Caddy HTTP (ACME challenge + redirect) |
| 443 | Caddy HTTPS (HTTP routes) |
| 2019 | Caddy Admin API |
| 3000 | GateControl Web UI |
| 51820 | WireGuard VPN |
Use cases
Minecraft server (TCP, port 25565, TLS: None)
Player connects to: mc.example.com:25565
L4 route: Listen Port 25565 → Peer "Gaming-Server" Target Port 25565
Protocol: TCP, TLS: None
SSH access (TCP, port 2222 → 22, TLS: None)
ssh -p 2222 admin@server.example.com
L4 route: Listen Port 2222 → Peer "Homeserver" Target Port 22
Protocol: TCP, TLS: None
Port 22 is not used as the Listen Port to avoid conflicts with the SSH of the GateControl server.
Mail server SMTP (TCP, port 25, TLS: None)
Mail server connects to: mail.example.com:25
L4 route: Listen Port 25 → Peer "Mailserver" Target Port 25
Protocol: TCP, TLS: None (STARTTLS is handled by the backend)
Database (TCP, port 5433 → 5432, TLS: None)
psql -h server.example.com -p 5433 -U myuser mydb
L4 route: Listen Port 5433 → Peer "DB-Server" Target Port 5432
Protocol: TCP, TLS: None
Game server (UDP)
Player connects to: game.example.com:27015
L4 route: Listen Port 27015 → Peer "Game-Server" Target Port 27015
Protocol: UDP, TLS: None
Feature comparison: HTTP vs. L4
| Feature | HTTP (Layer 7) | L4 (Layer 4) |
|---|---|---|
| Routing method | Domain-based | Port-based |
| HTTPS / Let's Encrypt | Yes | Only with TLS Terminate |
| Compression (Gzip/Zstd) | Yes | No |
| Rate Limiting | Yes | No |
| Custom Headers | Yes | No |
| Basic Auth | Yes | No |
| Route Auth | Yes | No |
| Peer ACL | Yes | No |
| IP Access Control | Yes | No |
| Request Mirroring | Yes | No |
| Retry on Error | Yes | No |
| Circuit Breaker | Yes | No |
| Uptime Monitoring | HTTP check | TCP check |
| Multiple backends | Yes (load balancing) | No |
| Sticky Sessions | Yes | No |
| WebSocket | Yes (automatic) | Yes (as TCP) |
| Protocol | HTTP/HTTPS | TCP / UDP |
WebSocket on HTTP routes: WebSocket connections start as a normal HTTP request with a special Connection: Upgrade header. Caddy detects this header automatically and switches the connection to a persistent WebSocket connection. No additional configuration is required — it works out-of-the-box on any HTTP route.
WebSocket on L4 routes: Since L4 forwards the raw TCP stream without inspecting the content, WebSocket also works here — Caddy only sees TCP packets and forwards them 1:1.
Setup
The route type (HTTP vs. L4) is toggled in the route wizard in Step 1 — Target. The L4-specific fields (Protocol, Listen Port, TLS Mode) then appear in the same step.
Creating an HTTP route (UI)
- In Step 1 Route Type: HTTP (default)
- Enter domain (e.g.
app.example.com) - Select Target Peer
- Enter Target Port (e.g. 8080)
- Configure features in Step 2-5 (HTTPS, Auth, ACL, Reliability)
- Save
Creating an L4 route (UI)
- In Step 1 toggle Route Type: L4
- Enter domain (required for TLS passthrough/terminate, optional for TLS None)
- Select Target Peer
- Enter Target Port (port on the backend)
- Select Protocol: TCP or UDP
- Enter Listen Port (port on the GateControl server)
- Select TLS Mode (None, Passthrough, Terminate)
- Save
Via the API
# Create HTTP route
curl -X POST https://gatecontrol.example.com/api/v1/routes \
-H "Authorization: Bearer gc_..." \
-H "Content-Type: application/json" \
-d '{
"route_type": "http",
"domain": "app.example.com",
"peer_id": 1,
"target_port": 8080,
"https_enabled": true
}'
# Create L4 route (Minecraft)
curl -X POST https://gatecontrol.example.com/api/v1/routes \
-H "Authorization: Bearer gc_..." \
-H "Content-Type: application/json" \
-d '{
"route_type": "l4",
"domain": "mc.example.com",
"peer_id": 2,
"target_port": 25565,
"l4_protocol": "tcp",
"l4_listen_port": "25565",
"l4_tls_mode": "none"
}'
Important notes
- L4 routes occupy exclusive ports. Every L4 route (without TLS) needs its own Listen Port. Two routes on the same port and protocol without TLS cause a conflict.
- Multiple L4 routes with TLS (Passthrough or Terminate) can share the same port — Caddy differentiates them via SNI (domain in the TLS ClientHello).
- Port ranges are possible (e.g.
25565-25575for multiple Minecraft servers). - UDP routes do not support TLS (TLS runs over TCP).
- L4 routes have no HTTP features: no Rate Limiting, no Auth, no Compression, no ACL, no Mirroring, no Retry, no Circuit Breaker.
- The Caddy L4 configuration is generated in a separate block (
layer4app), separate from the HTTP servers. - If you're unsure which type you need: if the service is accessed in the browser (HTTP/HTTPS), use HTTP routing. If it's a non-HTTP protocol (SSH, database, game server), use L4.
See also
- concepts/routing.md — Canonical deep-dive into the request flow
- features/gateway-l4-routes.md — L4 behind a home gateway
- guides/adding-a-route.md — End-to-end route setup