1. The problem I’m having:
TL/DR;
- What’s the matcher syntax to specify a
layer4matcher for TLS version in my otherwise-working json config? I have sni matching working OK.
I need to proxy (without TLS termination) traffic from clients that only support TLS v1.0 (ie, ancient, insecure client apps that I cannot change). On the same port I also wish to serve regular web traffic for modern clients, reverse_proxy’d to other servers.
I have been able to configure the layer4 module to do the proxying etc that I need, but I have not been able to work out how to get it to apply a match on the TLS version, which is provided in the clienthello.
If I use a matcher for “sni” the rest of my config works, I just don’t know the syntax for matching by tls version (or if it’s definitely possible - but mholt indicated it should be
My setup generally is:
- I build a docker image that includes the layer4 module (and installs curl)
- I start the container with a Caddyfile that holds my “normal” config (as it’s in a file format that supports comments)
- caddy then is able to serve my normal sites on port 443
- I then inject my layer4 config via the API
- this adds the layer4 app which listens on port 4433.
- it identifies the legacy clients (currently by SNI but I need tls version) and proxies their connections to the legacy server, without performing TLS termination.
- un-matched connections get proxied to port 443, so all my normal services are served via the http and tls apps that are defined in the main
Caddyfile.
I’ve done it this way so that I can keep the simple Caddyfile config, and at my firewall I redirect incoming port 443 to caddy’s port 4433.
2. Error messages and/or full log output:
When I try to inject my layer4 config block with the command
docker-compose exec caddy curl \
-X POST \
-H "Content-Type: application/json" \
-d @caddy-inject.json \
"http://127.0.0.1:2019/config/apps/layer4"
I get:
{"error":"loading new config: loading layer4 app module: provision layer4: server 'ajgtlswrap': route 0: loading matcher modules: module name 'tls': provision layer4.matchers.tls: loading TLS matchers: module name 'version': unknown module: tls.handshake_match.version"}
This is when the caddy-inject.json snippet contains (as opposed to the “working” config using SNI specified further below):
{
"servers": {
"ajgtlswrap": {
"listen": [
":4433"
],
"routes": [
{
"match": [
{
"tls": {
"version": [
"TLS10"
]
}
}
],
"handle": [
{
"handler": "proxy",
"upstreams": [
{
"dial": [
"legacyserver.internal.example.com:443"
]
}
]
}
]
},
{
"handle": [
{
"handler": "proxy",
"upstreams": [
{
"dial": [
"127.0.0.1:443"
]
}
]
}
]
}
]
}
}
}
3. Caddy version:
I build from caddy 2.7.5 locally using this Dockerfile:
ARG VERSION
FROM caddy:$VERSION-builder-alpine AS builder
RUN xcaddy build \
--with github.com/abiosoft/caddy-json-schema \
--with github.com/mholt/caddy-l4
FROM caddy:$VERSION-alpine
COPY --from=builder /usr/bin/caddy /usr/bin/caddy
RUN apk add curl
4. How I installed and ran Caddy:
a. System environment:
Debian 11.8 with its docker.io 20.10.5+dfsg1-1+deb11u2
docker-compose 1.25.0-1
b. Command:
docker-compose up -d
c. Service/unit/compose file:
My docker-compose.yaml includes:
services:
caddy:
build:
context: ./caddy-build
args:
- VERSION=2.7.5
restart: unless-stopped
cap_add:
- NET_ADMIN # Required for tweaking of UDP buffers for http/3
ports:
- "80:80"
- "443:443"
- "443:443/udp" # HTTP/3 support(!)
- "4433:4433" # Gateway directs traffic here, which handles proxying.
volumes:
- $PWD/Caddyfile:/etc/caddy/Caddyfile
- $PWD/caddy-srv:/srv
- $PWD/caddy-data:/data
- $PWD/caddy-config:/config
d. My complete Caddy config:
Note: I am redacting info here because I don’t believe its inclusion is relevant to my problem (ie all certs, redirections etc are working OK) and because my situation requires obfuscating details due to the presence of legacy systems. I can provide details by DM if they are relevant.
Initial Caddyfile loaded at startup:
{
email hostmaster@example.com
}
home.example.com {
reverse_proxy 192.168.35.42:8088
}
media.example.com {
reverse_proxy jellyfin:8096
}
“Injected” l4 config which DOES work, but I need to match on TLS version, not on SNI:
{
"servers": {
"ajgtlswrap": {
"listen": [
":4433"
],
"routes": [
{
"match": [
{
"tls": {
"sni": [
"legacy.example.com"
]
}
}
],
"handle": [
{
"handler": "proxy",
"upstreams": [
{
"dial": [
"legacyserver.internal.example.com:443"
]
}
]
}
]
},
{
"handle": [
{
"handler": "proxy",
"upstreams": [
{
"dial": [
"127.0.0.1:443"
]
}
]
}
]
}
]
}
}
}