1. The problem I’m having:
What i want to achieve ?
I want to proxy all traffic to AdGuardHome through Caddy in order to preserve and forward the original client IP address.
why ?
When using rootless Podman, all incoming connections received by AdGuardHome appear to originate from the gateway IP of the internal-reverse-proxy
network defined by Podman.
This means AdGuardHome logs show internal NAT IPs instead of real client IPs, which breaks visibility and logging.
I deliberately avoid rootful Podman for security and privilege separation reasons.
How do i try to resolve this ?
To solve this while staying within a rootless environment, I’m exploring Caddy + systemd socket activation as a viable solution.
Since socket activation allows passing the original connection file descriptors directly to Caddy, I thought of using the Caddy layer4
module to proxy DNS traffic and preserve the source IP.
2. Error messages and/or full log output:
Error: loading initial config: loading new config: layer4 app module: start: file file+net fd/3: invalid argument
3. Caddy version:
v2.9.1 h1:OEYiZ7DbCzAWVb6TNEkjRcSCRGHVoZsJinoDR/n9oaY=
4. How I installed and ran Caddy:
Caddy is installed using the official Docker image and runs rootlessly with Podman, managed via Quadlets. Quadlet is a systemd integration tool that simplifies running Podman containers as systemd services.
a. System environment:
OS
Debian GNU/Linux 12 (bookworm)
Podman
Client: Podman Engine
Version: 5.5.0
API Version: 5.5.0
Go Version: go1.24.2
Git Commit: 0dbcb51477ee7ab8d3b47d30facf71fc38bb0c98
Built: Mon Jun 2 19:29:24 2025
OS/Arch: linux/arm64
b. Command:
systemctl --user start internal-reverse-proxy.socket
systemctl --user start internal-reverse-proxy.service
c. Service/unit/compose file:
internal-reverse-proxy.socket
[Socket]
### sockets for the internal reverse proxy
# fd/3
ListenDatagram=[::]:53
# fd/4
ListenStream=[::]:784
# fd/5
ListenStream=[::]:853
# fd/6
ListenStream=[::]:8853
# fd/7
ListenStream=[::]:3000
# fd/8
ListenStream=[::]:80
# fd/9
ListenStream=[::]:443
BindIPv6Only=both
[Install]
WantedBy=sockets.target
internal-reverse-proxy.container
[Unit]
Description=Internal Reverse Proxy (Caddy)
Requires=internal-reverse-proxy.socket
After=internal-reverse-proxy.socket
[Container]
ContainerName=internal-reverse-proxy
Image=localhost/internal-reverse-proxy
Notify=true
# Mount host paths
Volume=/var/log/caddy:/var/log/caddy
Volume=%h/.config/containers/internal-reverse-proxy/Caddyfile:/Caddyfile
# Named volume (external)
Volume=internal-reverse-proxy:/data
# Environment variables
Environment=EMAIL=bbarnoux@gmail.com
Environment=ACME_AGREE=true
Environment=TZ=Europe/Paris
# Secret mounted as a file
Secret=gandi_password,type=env,target=GANDI_BEARER_TOKEN
# Network
Network=internal-reverse-proxy.network
[Install]
WantedBy=default.target
d. My complete Caddy config:
{
persist_config off
log internal-reverse-proxy {
level INFO
output file /var/log/caddy/caddy.log
}
auto_https disable_redirects
default_bind fd/9 {
protocols h1 h2
}
servers {
timeouts {
# sets timeouts for various actions
idle 10s # timeout for idle connections
read_body 1m # timeout for reading request body
read_header 10s # timeout for reading request header
}
max_header_size 4KB # sets maximum header size for requests
}
acme_dns gandi {env.GANDI_BEARER_TOKEN}
layer4 {
fd/3 {
route {
proxy adguardhome:53
}
}
fd/4 {
route {
proxy adguardhome:784
}
}
fd/5 {
route {
proxy adguardhome:853
}
}
fd/6 {
route {
proxy adguardhome:8853
}
}
fd/7 {
route {
proxy adguardhome:3000
}
}
}
}
http:// {
bind fd/8 {
protocols h1
}
redir https://{host}{uri}
log
}
adguard.one4all.icu {
log
encode gzip zstd
reverse_proxy adguardhome:8080
}