Thank you @Mohammed90 and @francislavoie for your help I managed to get it working using @matt 's L4 plugin.
For future time-travellers, here’s my caddy.json and docker-compose.yml files in case you’re interested:
caddy.json:
{
"apps": {
"tls": {
"certificates": {
"automate": [
"example.com"
]
}
},
"layer4": {
"servers": {
"doh": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"tls": {
"sni": [
"example.com"
]
}
}
],
"handle": [
{
"handler": "proxy",
"upstreams": [
{
"dial": [
"adguard:443"
]
}
]
}
]
}
]
},
"dot": {
"listen": [
":853"
],
"routes": [
{
"match": [
{
"tls": {
"sni": [
"example.com"
]
}
}
],
"handle": [
{
"handler": "proxy",
"upstreams": [
{
"dial": [
"adguard:853"
]
}
]
}
]
}
]
}
}
}
}
}
Caddy docker-compose.yml:
version: "3.9"
services:
caddy:
image: 0xlem0nade/caddy:latest # My custom built Caddy baked with l4 and a host of other plugins, you can substitute with any image that has l4
restart: unless-stopped
container_name: caddy
cap_add:
- NET_ADMIN
- CAP_NET_BIND_SERVICE
- CAP_NET_RAW
networks:
- caddy
ports:
- "80:80"
- "443:443"
- "443:443/udp"
- "853:853"
- "853:853/udp"
volumes:
- ./etc/caddy.json:/etc/caddy/caddy.json
- ./www:/srv
- /var/log:/var/log
- caddy_data:/data
- caddy_config:/config
command: caddy run --config /etc/caddy/caddy.json
volumes:
caddy_data:
external: true
caddy_config:
networks:
caddy:
external: true
AdGuardHome docker-compose.yml:
version: "3.9"
services:
adguard:
image: adguard/adguardhome:latest
restart: unless-stopped
container_name: adguard
cap_add:
- NET_ADMIN
- CAP_NET_BIND_SERVICE
- CAP_NET_RAW
networks:
- caddy
expose: # 'expose' instead of 'ports' to keep adguard away from the outside world and port conflicts!
- 53
- "53/udp"
- 443
- "443/udp"
- 853
- "853/udp"
dns:
- 1.1.1.1
- 9.9.9.9
volumes:
- caddy_data:/etc/letsencrypt:ro # Access Caddy-generated certs and add them to AdGuard so that TLS passes through directly to AdGuard
- adguard_data:/opt/adguardhome/work
- adguard_config:/opt/adguardhome/conf
volumes:
adguard_data:
external: true
adguard_config:
external: true
caddy_data:
external: true
networks:
caddy:
external: true
AdGuardHome.yml config:
# REDACTED TO THE RELEVANT PARTS!
bind_host: 0.0.0.0
dns:
bind_hosts:
- 0.0.0.0
port: 53
tls:
enabled: true
server_name: "example.com"
force_https: false
port_https: 443
port_dns_over_tls: 853
port_dns_over_quic: 853
allow_unencrypted_doh: false
certificate_path: "/etc/letsencrypt/caddy/certificates/acme-v02.api.letsencrypt.org-directory/example.com/example.com.crt"
private_key_path: "/etc/letsencrypt/caddy/certificates/acme-v02.api.letsencrypt.org-directory/example.com/example.com.key"
There was one quirk or maybe because I didn’t know how to make it work, and that was I had to add another domain to my server for this purpose and implement SNI based routing, because I run other apps on the server and I wanted to keep the domain and filter only the /dns-query
to AdGuard and other paths to my other apps (although that would break TLS passthrough), saw this comment here from matt but didn’t find any examples!
Path-based routing, DNS-over-QUIC, and HTTP3 are the only things, this setup left me wishing for, other than that pretty nifty! I hope future versions bring them along!
Cheers!