Repository: caddyserver/caddy (language: go)

Issue:
Remote_ip gets local ipv6 adresses with %eth0 (zone_id) but can't handle it
Hello together,
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 back and that I can’t use to restrict access from out of the local network.
My caddy version is: 2.4.6

Thats the relevant part of my Caddyfile:
```
pihole.{$DOMAIN}:443 {
  tls {$EMAIL}
  @internalv6 {
    remote_ip fe80::/64
  }
  handle @internalv6 {
    uri replace / /admin/ 1
    reverse_proxy 192.168.178.3:81
  }
```
If I use it, I get this error back. Nothing else in the log in debug mode from interesst:
```
{"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"}
```

Seems like caddy can't handle zone ids, but it is a normal behavior in `Scoped link local IPv6 addresses`. I found no way to disable the `zone_id`.
I have to mention, that I adressed the local adresses of the raspberry in my homenetwork with pihole, otherwise I accessed caddy with the global ipv6, what obviosly not works.


Discussion / hints:

Fix patch:
diff --git a/modules/caddyhttp/matchers.go b/modules/caddyhttp/matchers.go
index 272c92421fd..9f10e2c31f2 100644
--- a/modules/caddyhttp/matchers.go
+++ b/modules/caddyhttp/matchers.go
@@ -131,7 +131,10 @@ type (
 		// to spoof request headers. Default: false
 		Forwarded bool `json:"forwarded,omitempty"`
 
+		// cidrs and zones vars should aligned always in the same
+		// length and indexes for matching later
 		cidrs  []*net.IPNet
+		zones  []string
 		logger *zap.Logger
 	}
 
@@ -877,10 +880,19 @@ func (m *MatchRemoteIP) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
 func (m *MatchRemoteIP) Provision(ctx caddy.Context) error {
 	m.logger = ctx.Logger(m)
 	for _, str := range m.Ranges {
+		// Exclude the zone_id from the IP
+		if strings.Contains(str, "%") {
+			split := strings.Split(str, "%")
+			str = split[0]
+			// write zone identifiers in m.zones for matching later
+			m.zones = append(m.zones, split[1])
+		} else {
+			m.zones = append(m.zones, "")
+		}
 		if strings.Contains(str, "/") {
 			_, ipNet, err := net.ParseCIDR(str)
 			if err != nil {
-				return fmt.Errorf("parsing CIDR expression: %v", err)
+				return fmt.Errorf("parsing CIDR expression '%s': %v", str, err)
 			}
 			m.cidrs = append(m.cidrs, ipNet)
 		} else {
@@ -898,8 +910,9 @@ func (m *MatchRemoteIP) Provision(ctx caddy.Context) error {
 	return nil
 }
 
-func (m MatchRemoteIP) getClientIP(r *http.Request) (net.IP, error) {
+func (m MatchRemoteIP) getClientIP(r *http.Request) (net.IP, string, error) {
 	remote := r.RemoteAddr
+	zoneID := ""
 	if m.Forwarded {
 		if fwdFor := r.Header.Get("X-Forwarded-For"); fwdFor != "" {
 			remote = strings.TrimSpace(strings.Split(fwdFor, ",")[0])
@@ -909,25 +922,40 @@ func (m MatchRemoteIP) getClientIP(r *http.Request) (net.IP, error) {
 	if err != nil {
 		ipStr = remote // OK; probably didn't have a port
 	}
+	// Some IPv6-Adresses can contain zone identifiers at the end,
+	// which are separated with "%"
+	if strings.Contains(ipStr, "%") {
+		split := strings.Split(ipStr, "%")
+		ipStr = split[0]
+		zoneID = split[1]
+	}
 	ip := net.ParseIP(ipStr)
 	if ip == nil {
-		return nil, fmt.Errorf("invalid client IP address: %s", ipStr)
+		return nil, zoneID, fmt.Errorf("invalid client IP address: %s", ipStr)
 	}
-	return ip, nil
+	return ip, zoneID, nil
 }
 
 // Match returns true if r matches m.
 func (m MatchRemoteIP) Match(r *http.Request) bool {
-	clientIP, err := m.getClientIP(r)
+	clientIP, zoneID, err := m.getClientIP(r)
 	if err != nil {
 		m.logger.Error("getting client IP", zap.Error(err))
 		return false
 	}
-	for _, ipRange := range m.cidrs {
+	zoneFilter := true
+	for i, ipRange := range m.cidrs {
 		if ipRange.Contains(clientIP) {
-			return true
+			// Check if there are zone filters assigned and if they match.
+			if m.zones[i] == "" || zoneID == m.zones[i] {
+				return true
+			}
+			zoneFilter = false
 		}
 	}
+	if !zoneFilter {
+		m.logger.Debug("zone ID from remote did not match", zap.String("zone", zoneID))
+	}
 	return false
 }
 


Test patch: