IPv4 or IPv6 preference for individual subdomains

1. The problem I’m having:

I want specific subdomains to be either IPv4 or IPv6 only.

My overall goal is that I run a service that relies on the X-Forwarded-For header to show the client’s IP address. My server has both IPv4 and IPv6, but Caddy always forwards the IPv6 address when it’s available. I need to be able to access the client’s IPv4 as well, without disabling IPv6 entirely. So I want to make dedicated v4/v6 subdomains but I don’t know how…

2. Error messages and/or full log output:

I can’t provide logs for it, since I can’t figure out how can I even do this. Currently everything responds to both v4 and v6.

3. Caddy version:

caddy version
v2.10.0 h1:fonubSaQKF1YANl8TXqGcn4IbIRUDdfAkpcsfI/vX5U=

4. How I installed and ran Caddy:

I used xcaddy to build it with caddy-l4 and caddy-dns/cloudflare.
xcaddy build --with github.com/caddy-dns/cloudflare --with github.com/mholt/caddy-l4

a. System environment:

Debian 12 & 6.1.0-37-amd64, with systemd-- no docker.
Domain: (*.)albert.lol

b. Command:

c. Service/unit/compose file:

cat /lib/systemd/system/caddy.service
[Unit]
Description=Caddy
Documentation=https://caddyserver.com/docs/
After=network.target network-online.target
Requires=network-online.target

[Service]
Type=notify
User=caddy
Group=caddy
ExecStart=/usr/local/bin/caddy run --environ --config /etc/caddy/Caddyfile
ExecReload=/usr/local/bin/caddy reload --config /etc/caddy/Caddyfile --force
TimeoutStopSec=5s
LimitNOFILE=1048576
LimitNPROC=512
PrivateTmp=true
ProtectSystem=full
AmbientCapabilities=CAP_NET_BIND_SERVICE
Environment="CLOUDFLARE_API_KEY=REDACTED"
Restart=on-failure
RestartSec=5s

d. My complete Caddy config:

caddy fmt /etc/caddy/Caddyfile
{
        layer4 {
                udp/:9987 {
                        route {
                                proxy udp/192.168.1.103:9987
                        }
                }
                :25565 {
                        route {
                                proxy {
                                        proxy_protocol v2
                                        upstream 192.168.1.104:25565
                                }
                        }
                }
                :5432 {
                        route {
                                proxy 192.168.1.103:5432
                        }
                }
                :1022 {
                        route {
                                proxy 192.168.1.118:22
                        }
                }
                :3022 {
                        route {
                                proxy 192.168.1.119:22
                        }
                }
        }
}

(common) {
        header * {
                -Server
                -Via
                Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
                X-Content-Type-Options "nosniff"
                X-Frame-Options "SAMEORIGIN"
                Referrer-Policy "strict-origin-when-cross-origin"
                Permissions-Policy "geolocation=(), microphone=(), camera=(), interest-cohort=()"
        }
        log {
                output file /var/log/caddy/access.log {
                        roll_size 50MB
                        roll_keep 5
                        roll_keep_for 168h
                }
                format json {
                        time_format "iso8601"
                }
        }
        encode zstd gzip
}

(insecure) {
        transport http {
                tls_insecure_skip_verify
        }
}

(cf) {
        tls {
                dns cloudflare {env.CLOUDFLARE_API_KEY}
        }
}

:80, http://albert.lol {
        redir https://albert.lol{uri} permanent
}

*.demo.albert.lol {
        import common
        import cf

        @btdemo host budgetable.demo.albert.lol
        handle @btdemo {
                reverse_proxy 192.168.1.103:3006
        }

        handle {
                abort
        }
}

albert.lol {
        import common
        import cf

        reverse_proxy 192.168.1.106:3000
}

*.albert.lol {
        import common
        import cf
        import sites/*

        handle {
                redir https://albert.lol permanent
        }
}

Example from my sites/ directory:

cat /etc/caddy/sites/bin.albert.lol
@bin host bin.albert.lol
handle @bin {
    reverse_proxy 192.168.1.103:8002
}

5. Links to relevant resources:

Not sure if this is what you’re looking for but you could try bind

IPv4/IPv6 is a preference of the client, not the server.

A connection over IPv6 is fully distinct form a connection over IPv4

If the service does not work with IPv6, remove the AAAA from the domain in the DNS

1 Like

Sadly this doesn’t work, I tried.

Almost true, but not quite. Even if the server has IPv6, you can still make a service bind only to IPv4, so it’s only accessible via IPv4. The same goes for IPv6. The issue with Caddy is that everything binds globally to either one or both IP versions, and you can’t control that on a per-subdomain basis. With Nginx, for example, you can define a server block to listen only on IPv4 port 80 and not on IPv6.
DNS is a separate matter entirely and isn’t related to this issue.

Your are incorrect, binding a server to just a single ip doesn’t work.

Taking your example with nignx, if one site block is bound to IPv6 and IPv4, while the other is only bound to IPv4, if you try to access the latter over IPv6, the browser connects to the server, then gets an error from the server saying it doesn’t host the site. From the browsers perspective, there was a vaid connection and it never falls back to another IP

Even in the case when you are hosting one website, it is still unreliable, as the browser might end up connecting to the other IP and hit a timeout. (or in the case of a DNS64 gateway, it never even sees the A record if there is an AAAA record introduced). You would also be depending on the Happy Eyeballs algoritmh in browsers, which is unreliable.

And remember that letsencrypt and Zerotier doesn’t support this. If your website has an AAAA and A record, letsencrypt only tries AAAA (with a 1 time A request fallback that gets eaten up by the HTTP to HTTPS redirect). This is a common issue that gets posted on these forums and the letsencrypt forums by people asking why they cannot get a certificate

1 Like

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.