Cannot access reverse_proxy endpoint when using geofilter from LAN

1. The problem I’m having:

Reverse proxy shows a blank page when using a geofilter but only from internal networks. Everything was working just peachy but noticed some connections from outside Canada so decided to add the geofilter to a few sites of mine. When I added it I tested it from a VPN outside of Canada and it worked (blank screen). When I connected just through my home network I also got a blank screen. I turned off wifi on my phone and tested and was able to get through to the page without any issue. Removing the geo filter allows me to connect again through the reverse proxy internally.

2. Error messages and/or full log output:

{"level":"info","ts":1749053752.07467,"logger":"http.log.access.log1","msg":"NOP","request":{"remote_ip":"192.168.222.3","remote_port":"45362","client_ip":"192.168.222.3","proto":"HTTP/2.0","method":"GET","host":"request.media.ellisd.ca
","uri":"/sw.js","headers":{"Accept-Language":["en-CA,en-GB;q=0.9,en-US;q=0.8,en;q=0.7"],"Priority":["u=4, i"],"Accept":["*/*"],"Service-Worker":["script"],"Sec-Fetch-Dest":["serviceworker"],"Sec-Fetch-Mode":["same-origin"],"Cookie":["R
EDACTED"],"Cache-Control":["max-age=0"],"Referer":["https://request.media.ellisd.ca/sw.js"],"User-Agent":["Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Mobile Safari/537.36"],"Accept-Encodin
g":["gzip, deflate, br, zstd"],"Sec-Fetch-Site":["same-origin"]},"tls":{"resumed":false,"version":772,"cipher_suite":4865,"proto":"h2","server_name":"request.media.ellisd.ca"}},"bytes_read":0,"user_id":"","duration":0.000015728,"size":0
,"status":0,"resp_headers":{"Server":["Caddy"],"Alt-Svc":["h3=\":443\"; ma=2592000"]}}

3. Caddy version:

v2.10.0 h1:fonubSaQKF1YANl8TXqGcn4IbIRUDdfAkpcsfI/vX5U=

4. How I installed and ran Caddy:

a. System environment:

docker compose on debian host

b. Command:

docker compose up -d

c. Service/unit/compose file:

---
services:
  caddy:
    image: serfriz/caddy-cloudflare-ddns-crowdsec-geoip:latest
    container_name: caddy
    ports:
      - "80:80"
      - "443:443"
    environment:
      - CLOUDFLARE_API_TOKEN=${CLOUDFLARE_API_TOKEN}
      - CROWDSEC_API_KEY=${CROWDSEC_API_KEY}
      - DOMAIN=${DOMAIN}
    volumes:
      - caddy-data:/data
      - caddy-config:/config
      - caddy-logs:/var/log/caddy
      - ./Caddyfile:/etc/caddy/Caddyfile
      - ./caddy/GeoLite2-Country.mmdb:/etc/caddy/GeoLite2-Country.mmdb
    networks:
      crowdsec:
    security_opt:
      - no-new-privileges=true

  crowdsec:
    image: docker.io/crowdsecurity/crowdsec:latest
    container_name: crowdsec
    ports:
      - "8080:8080"
    environment:
      - GID=1000
      - COLLECTIONS=crowdsecurity/caddy crowdsecurity/http-cve crowdsecurity/whitelist-good-actors
      - BOUNCER_KEY_CADDY=${CROWDSEC_API_KEY}
    volumes:
      - crowdsec-db:/var/lib/crowdsec/data/
      - ./crowdsec/acquis.yaml:/etc/crowdsec/acquis.yaml
      - caddy-logs:/var/log/caddy:ro
    networks:
      crowdsec:
    restart: unless-stopped
    security_opt:
      - no-new-privileges=true

volumes:
  crowdsec-db:
  caddy-logs:
  caddy-data:
  caddy-config:

networks:
  crowdsec:
    driver: bridge

d. My complete Caddy config:

{
    acme_dns cloudflare {env.CLOUDFLARE_API_TOKEN}
    dynamic_dns {
        provider cloudflare {env.CLOUDFLARE_API_TOKEN}
        domains {
            {$DOMAIN}
        }
        dynamic_domains
        versions ipv4
    }
    order crowdsec first
    crowdsec {
        api_url http://crowdsec:8080
        api_key {env.CROWDSEC_API_KEY}
    }
#    log {
#        output file /var/log/caddy/access.log
#    }
}

(logging) {
    log {
        output file /var/log/caddy/{args[0]}.access.log
        format json
    }
}

(private_ranges) {
    @denied not client_ip private_ranges
    respond @denied "Private access only" 403
}

(geofilter) {
    @mygeofilter {
        maxmind_geolocation {
            db_path "/etc/caddy/GeoLite2-Country.mmdb"
            allow_countries CA
        }
    }
}

jelly.media.{$DOMAIN} {
    import logging jelly
    import geofilter
    route {
        crowdsec
        @jelly host jelly.media.{$DOMAIN}
        handle @jelly {
            reverse_proxy @mygeofilter 172.16.2.7:8096
        }
    }
}

Your geofilter says

allow_countries CA

But RFC1918 (your internal network) is not CA.

1 Like

Hmm okay that makes sense, but just looking through the geofilter docs and I can’t find anything in regards to adding RFC1918. Or would I handle this on the reverse_proxy @mygeofilter line somehow?

I quickly put this together - probably not the most polished solution, but it works. Adjust it to your needs:

{
	http_port 8080
}

(geofilter) {
	@lan_or_ca {
		not {
			not client_ip private_ranges
			not {
				maxmind_geolocation {
						db_path "/etc/caddy/GeoLite2-Country.mmdb"
						allow_countries CA
				}
			}
		}
	}
}

:8080 {
	import geofilter

	handle @lan_or_ca {
		reverse_proxy  127.0.0.1:8081
	}
	respond "Private access only" 403
}

:8081 {
	respond "Alive!"
}
1 Like

I mean, this one has one not less (I’m using deny_countries here instead):

(geofilter) {
	@lan_or_ca {
		not {
			not client_ip private_ranges
			maxmind_geolocation {
					db_path "/etc/caddy/GeoLite2-Country.mmdb"
					deny_countries CA
			}
		}
	}
}

but I think this version with the extra not feels easier to read:

(geofilter) {
	@lan_or_ca {
		not {
			not client_ip private_ranges
			not {
				maxmind_geolocation {
						db_path "/etc/caddy/GeoLite2-Country.mmdb"
						allow_countries CA
				}
			}
		}
	}
}

Up to you, whichever you prefer.

2 Likes

That works! Really appreciate it. I know with routes it goes in order, but lets say I want to keep the crowdsec check to be first, can I put it before the handle part all together in the block? Or would it also go in handle?