Skip to content

itstomsh/caddy-geojs-blocker

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

10 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Caddy GeoJS Blocker Module

Go CI

A lightweight Caddy v2 HTTP handler module for geoblocking or geowhitelisting requests based on client IP country codes, using the free GeoJS API.
Supports both allowlisting (default allow, block others) and blocklisting (default allow, block specific).
Includes per-instance caching, concurrency-safe stats, and an optional debug endpoint for monitoring.


✨ Features

  • GeoIP Lookup: Fetches 2-letter country code (e.g., DE) from GeoJS.
  • Caching: In-memory LRU-like cache (default 50k entries, 15m TTL) to minimize API calls.
  • Concurrency: Optional singleflight deduplication for simultaneous lookups.
  • Stats: Atomic counters for allowed/blocked requests with JSON export.
  • Log Vars: Sets geojs_country and geojs_decision for access logs.
  • Debug Endpoint: Optional /debug/geojs endpoint for stats snapshot/reset (token-protected).

⚙️ Installation

Build Caddy with the Module

Best way is to use xcaddy:

xcaddy build --with github.com/itstomsh/caddy-geojs-blocker

or:

# Clone Caddy
git clone https://github.com/caddyserver/caddy.git && cd caddy

# Add the module import
# In cmd/caddy/main.go:
import _ "github.com/itstomsh/caddy-geojs-blocker"

go mod tidy

# Build Caddy
go build \
  -tags 'nobadger,nomysql,nopgx' \
  -trimpath \
  -ldflags '-w -s' \
  -o ./caddy ./cmd/caddy

Then run:

./caddy run --config Caddyfile

🧩 Usage

Place the directive in a route block.

  • Use geojs_allow for allowlisting (only specific countries allowed).
  • Use geojs_block for blocklisting (specific countries blocked).

Inline arguments are ISO2 country codes; options are set within the block.


✅ Allow Only Specific Countries

:80 {
  route {
    geojs_allow DE US RU CN {
      cache_ttl 30m
      debug_path /debug/geojs
    }
    respond "Hello from {http.vars.geojs_country}!"
  }
}

→ Allows Germany (DE), United States (US), Russia (RU), and China (CN); blocks all others with 403.


🚫 Block Specific Countries

:80 {
  route {
    geojs_block DE US RU CN
    respond "Access granted from {http.vars.geojs_country}!"
  }
}

→ Blocks Germany (DE), United States (US), Russia (RU), and China (CN); allows all others.


🧾 Full Example with All Options

:80 {
  log {
    format json
  }

  route {
    geojs_allow DE US RU CN {
      cache_ttl 10m
      cache_size 10000
      singleflight off
      allow_undetected off
      prune_interval 2m
      debug_path /debug/geojs
      debug_token mysecret
      stats_file /var/lib/caddy/geojs_stats.json
      stats_flush_interval 1m
    }

    respond "Welcome from {http.vars.geojs_country} ({http.vars.geojs_decision})"
  }
}

Directive placement & order

geojs_allow and geojs_block are HTTP handlers. Use them inside a route block, or configure global ordering if you prefer global (site-wide) geoblocking.

Preferred:

:80 {
  route {
    geojs_allow DE US RU CN
    respond "Hello from {http.vars.geojs_country}!"
  }
}

Alternative with global order:

{
  order geojs_allow before respond
  order geojs_block before respond
}
:80 {
  geojs_allow DE US RU CN
  respond "Hello from {http.vars.geojs_country}!"
}

🎯 Allowing or Blocking Specific IPs

This module only makes decisions by country. For plain IP/CIDR allow- or block-listing — e.g. "only my VPS may reach this site, regardless of country" — use Caddy's built-in remote_ip matcher instead of extending this module. It's evaluated before any GeoJS lookup happens, so no API call or cache entry is wasted on IPs you already have a fixed decision for.

Only allow one specific IP (e.g. your VPS), block everyone else:

:80 {
  @vps remote_ip 203.0.113.10
  route @vps {
    respond "Hello from the VPS!"
  }
  respond "Forbidden" 403
}

Combine it with geojs_allow/geojs_block, reaching the same handler either way — e.g. allow requests from Germany or from one trusted IP (your VPS), and serve identical content to both:

:80 {
  @trusted remote_ip 203.0.113.10 198.51.100.0/24
  @untrusted not remote_ip 203.0.113.10 198.51.100.0/24

  route @trusted {
    # trusted IP - skip the geo check entirely, fall through below
  }
  route @untrusted {
    geojs_allow DE
  }

  respond "Welcome!"
}

route @trusted { } has an empty body, so matching requests just fall through to respond untouched. Non-matching requests hit route @untrusted, which runs geojs_allow DE — if it blocks, the request stops there with a 403 and never reaches respond; if it allows, it falls through to the same respond the trusted IP gets. Requests handled by the @trusted branch never reach geojs_allow, so they won't appear in this module's stats or set geojs_country/geojs_decision — those log vars are only set for requests that actually went through the GeoJS check.


🔧 Options

Option Type Description Example Default
Country Codes (inline) []string ISO2 codes for allow/block list geojs_allow DE US RU CN
cache_ttl Duration Cache TTL for IP lookups cache_ttl 30m 15m
cache_size int Max cache entries cache_size 10000 50000
singleflight string Deduplicate concurrent lookups (on/off) singleflight off on
allow_undetected string Allow (on) or block (off) undetected IPs (lookup failures, etc.) allow_undetected off on
prune_interval Duration Prune expired cache entries prune_interval 1h 5m
debug_path string Path for stats JSON endpoint debug_path /debug/geojs (disabled)
debug_token string Token for debug auth (header X-Debug-Token) debug_token mysecret (none)
stats_file string File path to persist stats counters across restarts/reloads stats_file /var/lib/caddy/geojs_stats.json (disabled)
stats_flush_interval Duration How often stats_file is written to disk stats_flush_interval 1m same as prune_interval

⚙️ Recommended Settings by Use Case

Different environments benefit from different cache and pruning settings.
Here are suggested values you can tune to balance performance and accuracy.

Scenario Description cache_ttl cache_size prune_interval singleflight Notes
🏠 Home Server / Personal Use Few visitors, low load 30m 1000 10m on Keeps memory use minimal and still avoids repeat lookups.
🚀 Low-Traffic Site Small business, blog, small API 1h 10000 10m on Reduces API requests while maintaining fresh lookups.
🌍 High-Traffic / Production Many visitors or global access 6h 50000–100000 15m on Minimizes API hits, faster response, more RAM usage.
🧪 Debug / Testing Development, frequent restarts 5m 1000 1m off More frequent lookups help see live GeoJS behavior.

💡 Tip:
For very high traffic, keep singleflight on — it prevents multiple concurrent lookups for the same IP and saves both time and API requests.


🐛 Debug Endpoint

If debug_path is set (e.g., /debug/geojs):

GET → Returns JSON stats:

{
  "total_allowed": 4,
  "total_blocked": 0,
  "allowed_by_cc": { "DE": 2, "US": 1, "RU": 1, "CN": 0 },
  "blocked_by_cc": {},
  "cache_hits": 12,
  "api_calls": 4,
  "api_errors": 0
}
  • total_allowed/total_blocked (and their per-country breakdowns) only count fresh GeoJS lookups — requests served from the IP cache don't add to these totals.
  • cache_hits counts requests resolved straight from the in-memory cache, with no GeoJS call.
  • api_calls counts actual physical calls made to the GeoJS API. With singleflight on (the default), concurrent requests for the same uncached IP are collapsed into a single call, so this reflects real upstream traffic, not request volume.
  • api_errors is the subset of api_calls that failed (network error, non-200 status, or an unparseable response) — useful for spotting GeoJS outages or rate-limiting.

POST ?reset=1 → Resets counters.
Example:

curl http://localhost:80/debug/geojs
curl -X POST http://localhost:80/debug/geojs?reset=1

Requires header X-Debug-Token: {token} if debug_token is set.
Example with token:

curl -H "X-Debug-Token: mysecret" http://localhost:80/debug/geojs
curl -X POST -H "X-Debug-Token: mysecret" http://localhost:80/debug/geojs?reset=1

💾 Persisting Stats Across Restarts

By default, stats live only in memory and reset to zero on every Caddy restart and on config reloads (since a reload re-provisions the module).

Set stats_file to a writable path to persist counters across both:

geojs_allow DE US RU CN {
  debug_path /debug/geojs
  stats_file /var/lib/caddy/geojs_stats.json
  stats_flush_interval 1m
}

How it works:

  • On startup/reload, the module loads the last saved snapshot from stats_file (if present) instead of starting from zero.
  • The snapshot is written back to disk on a stats_flush_interval tick (defaults to the same value as prune_interval if not set), after a POST ?reset=1, and once more on shutdown/reload (Cleanup).
  • Writes are atomic (temp file + rename) and serialized internally, so the periodic flush, a debug reset, and shutdown can never race on the same file even if they land at the same instant.
  • Parent directories for stats_file are created automatically if they don't exist.
  • This is a lightweight, single-file mechanism — not a durable store. A hard SIGKILL between two flush ticks can still lose the most recent counts.

⚠️ Notes

  • API Usage: GeoJS currently has no fixed rate limits, but may restrict users who make excessive requests. The in-memory cache and optional singleflight mode help reduce unnecessary lookups.
  • IPv6 Support: Fully supported.
  • Proxy Headers: Prefers X-Forwarded-For, then X-Real-IP, then RemoteAddr.
  • Cache Eviction: Uses simple random eviction when full (not full LRU).
  • Validation: You cannot mix both blocklist and allowlist in one directive.

🧪 Testing

# Test with known IP
curl -H "X-Forwarded-For: 91.64.46.1" http://localhost:80
# → DE IP → allowed

# View stats
curl http://localhost:80/debug/geojs

# View stats with token
curl -H "X-Debug-Token: mysecret" http://localhost:80/debug/geojs

# Reset counters (token protected)
curl -X POST -H "X-Debug-Token: mysecret" http://localhost:80/debug/geojs?reset=1

🪵 Logging Integration

If you want GeoJS Blocker’s decision data (geojs_country, geojs_decision) to appear in your main access logs, you can append them manually to log entries using the following Caddyfile block:

log {
  format json
}

handle_errors {
  log_append geojs_country  {http.vars.geojs_country}
  log_append geojs_decision {http.vars.geojs_decision}
  respond "{http.error.status_code} {http.error.status_text}"
}

route {
  geojs_allow DE US RU CN
  
  log_append geojs_country  {http.vars.geojs_country}
  log_append geojs_decision {http.vars.geojs_decision}
}

This ensures both normal requests and blocked responses include the GeoJS decision context in your JSON access log output.


🌍 Supported Country Codes

GeoJS returns ISO 3166-1 alpha-2 country codes.
You can use any of these two-letter codes (case-insensitive) in geojs_allow or geojs_block directives.
The module automatically converts them to uppercase and ignores invalid entries.


📜 License

Licensed under the MIT License.

Contributions welcome!
Built with ❤️ for Caddy.

About

Caddy v2 module for geoblocking or geowhitelisting requests using the free GeoJS API — with caching, stats, and debug endpoint.

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages