Remote_ip gets local ipv6 adresses with %eth0 (zone_id) but can't handle it

1. Caddy version (caddy version):

v2.4.6

2. How I run Caddy:

I run caddy in a docker container on a raspberry pi. At the moment i run it in the host network, because in a bridge network I got the docker ip and that I can’t use to restrict access from out of the local network.

a. System environment:

raspberryOS, docker

c. Service/unit/compose file:

docker-compose.yml file:

version: '2.3'
networks:
  caddy:
    external: true
services:
  vaultwarden:
    image: vaultwarden/server:latest
    container_name: vaultwarden
    restart: always
    environment:
      - SIGNUPS_ALLOWED=false
      - WEBSOCKET_ENABLED=true  # Enable WebSocket notifications.
    ports:
      - 880:80
      - 3012:3012
    volumes:
      - ./vw-data:/data
    networks:
      caddy:
        ipv4_address: 192.168.112.7
        ipv6_address: 2001:ab12::7

  caddy:
    image: caddy:2
    container_name: caddy
    restart: always
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - ./caddy-config:/config
      - ./caddy-data:/data
    environment:
      - DOMAIN=mydomain.de
      - LDOMAIN=test.local
      - EMAIL=something@example.de
      - LOG_FILE=/data/access.log
    network_mode: host

d. My complete Caddyfile or JSON config:

pihole.{$DOMAIN}:443 {
  tls {$EMAIL}
  @internalv4 {
    remote_ip 192.168.178.0/24
  }
  @internalv6 {
    remote_ip fe80::/64
  }
  handle @internalv4 {
    uri replace / /admin/ 1
    reverse_proxy 192.168.178.3:81
  }
  handle @internalv6 {
    uri replace / /admin/ 1
    respond "im ipv6 Filter"
    #reverse_proxy 192.168.178.3:81
  }
  respond {remote_host}
#  reverse_proxy if {remote_host} starts_with fe80 {
#    to 192.168.178.3:81
#  }
}

3. The problem I’m having:

I wanted to use caddy to access my pihole server, that runs on the pi, over the subdomain “pihole”. But I dont want to open pihole to the hole internet. For that reason the idea was to restrict the access over “remote_ip”. When i was still let caddy run in a bridge network, remote_ip used only the interface IP from the bridge network. For my case, not usable. So now i run caddy over the host network. As long as I still let it run over the bridge network I got although the interface ip but a real ip back. Now i get the ip from the host+%eth0 back. (example: fe80::dea6:32ff:fe42:9fd0%eth0)
Seems like caddy can’t handle theese. If I apply the filter fe80::/64, it doesn’t recognise it as a right ip.

4. Error messages and/or full log output:

{"level":"debug","ts":1644416842.4225502,"logger":"tls.handshake","msg":"no certificate matching TLS ClientHello","server_name":"localhost","remote":"[::1]:59662","identifier":"localhost","cipher_suites":[4867,4866,4865,52393,52392,49196,49195,49200,49199,255],"cert_cache_fill":0.0005,"load_if_necessary":true,"obtain_if_necessary":true,"on_demand":false}
{"level":"debug","ts":1644416842.4231417,"logger":"http.stdlib","msg":"http: TLS handshake error from [::1]:59662: no certificate available for 'localhost'"}
{"level":"debug","ts":1644416889.9301133,"logger":"http.handlers.rewrite","msg":"rewrote request","request":{"remote_addr":"192.168.178.4:62516","proto":"HTTP/2.0","method":"GET","host":"pihole.example.de","uri":"/api.php?summary","headers":{"Dnt":["1"],"Sec-Fetch-Dest":["empty"],"Sec-Fetch-Mode":["cors"],"Sec-Fetch-Site":["same-origin"],"Te":["trailers"],"Accept":["application/json, text/javascript, */*; q=0.01"],"Accept-Encoding":["gzip, deflate, br"],"X-Requested-With":["XMLHttpRequest"],"Referer":["https://pihole.example.de/"],"Cookie":["PHPSESSID=749quu30s5ci8d820if8rnk8ea"],"Sec-Gpc":["1"],"User-Agent":["Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:96.0) Gecko/20100101 Firefox/96.0"],"Accept-Language":["de,en-US;q=0.7,en;q=0.3"]},"tls":{"resumed":true,"version":772,"cipher_suite":4867,"proto":"h2","proto_mutual":true,"server_name":"pihole.example.de"}},"method":"GET","uri":"/admin/api.php?summary"}
{"level":"debug","ts":1644416889.9357963,"logger":"http.handlers.reverse_proxy","msg":"upstream roundtrip","upstream":"192.168.178.3:81","duration":0.005473962,"request":{"remote_addr":"192.168.178.4:62516","proto":"HTTP/2.0","method":"GET","host":"pihole.example.de","uri":"/admin/api.php?summary","headers":{"Sec-Fetch-Site":["same-origin"],"Te":["trailers"],"Accept":["application/json, text/javascript, */*; q=0.01"],"Sec-Fetch-Mode":["cors"],"Referer":["https://pihole.example.de/"],"Sec-Gpc":["1"],"User-Agent":["Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:96.0) Gecko/20100101 Firefox/96.0"],"X-Requested-With":["XMLHttpRequest"],"Sec-Fetch-Dest":["empty"],"X-Forwarded-For":["192.168.178.4"],"X-Forwarded-Proto":["https"],"Accept-Encoding":["gzip, deflate, br"],"Dnt":["1"],"Accept-Language":["de,en-US;q=0.7,en;q=0.3"],"Cookie":["PHPSESSID=749quu30s5ci8d820if8rnk8ea"]},"tls":{"resumed":true,"version":772,"cipher_suite":4867,"proto":"h2","proto_mutual":true,"server_name":"pihole.example.de"}},"headers":{"Content-Type":["application/json"],"X-Frame-Options":["DENY"],"Server":["lighttpd/1.4.53"],"Cache-Control":["no-store, no-cache, must-revalidate"],"Pragma":["no-cache"],"X-Pi-Hole":["The Pi-hole Web interface is working!"],"Content-Length":["481"],"Date":["Wed, 09 Feb 2022 14:28:09 GMT"],"Expires":["Thu, 19 Nov 1981 08:52:00 GMT"]},"status":200}
{"level":"error","ts":1644416904.9481893,"logger":"http.matchers.remote_ip","msg":"getting client IP","error":"invalid client IP address: fe80::9457:35e3:3b21:173e%eth0"}

5. What I already tried:

Searched in other forums and posts but found no way to change the behavior of remote_ip or to change the ip to not contain “%eth0”.
As you can see in the Caddyfile I tried to filter for local ipv6 with an if argument. It seems so filter right but something doesn’t work. It loads the site only if I press F5 a few times and than only partially.

Hang on, this sounds familiar… I swear we had a discussion or issue recently where interfaces appearing in IPv6 addresses were more correctly handled. Can’t seem to find it right now though.

Edit: The %eth0 suffix is called a zone identifier, and I know I recall a discussion about them once upon a time. Where was it :sweat:

Thank you for your anwer. With knowing that it is called zone identifier i searched for a solution. I now know that the last part is called zone_id. It seems like this is a normal behavior in Scoped link local IPv6 addresses. But I can’t find a way to disable the zone_id for local adresses.
So it seems like caddy isn’t handle it correctly.

Do you have maybe a solution for my workaround, that I mentiont before?

I now try to use:

pihole.{$DOMAIN}:443 {
  tls {$EMAIL}
  reverse_proxy if {remote_host} starts_with fe80 {
    to 192.168.178.3:81
  }
  respond {remote_host}
}

The log shows nothing more interessting than the good old:

{"level":"error","ts":1644497213.9798095,"logger":"http.matchers.remote_ip","msg":"getting client IP","error":"invalid client IP address: fe80::9457:35e3:3b21:173e%eth0"}

The thing is, that the filter does work about every 7 reloads of the side. But only if I remove the respond {remote_host} at the end. Otherwise it shows everytime the remote host ip.

I made a bug report with pull request for the remote_ip problem.

Has nobody an idea for the workaround problem?

FYI, that’s invalid syntax. Please see the docs.

As a workaround, you might be able to use the expression matcher with the {remote} placeholder (which contains both the host and port, Caddy will not attempt to parse this). Where you can do a prefix match on the IPv6 string.

Thank you very much! works well…
For other people that have the same problem. I used it like this now:

pihole.{$DOMAIN}:443 {
  tls {$EMAIL}
  @internalv6 {
    expression {remote_host}.startsWith("fe80")
  }
  handle @internalv6 {
    uri replace / /admin/ 1
    reverse_proxy 192.168.178.3:81
  }
}

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