Compare commits

..

2 Commits

Author SHA1 Message Date
3fc856c77a README Add souin description 2025-04-18 16:18:32 +08:00
3662ab2605 Add souin http cache middleware 2025-04-18 15:30:53 +08:00
5 changed files with 96 additions and 209 deletions

View File

@@ -48,13 +48,13 @@ label:
- "traefik.http.routers.service-name.entrypoints=websecure"
```
Besides the entrypoint setup, I add CrowdSec firewall bouncer plus a compression middleware (brotli/gzip/zstd) defined in `dynamic.yml`. Cloudflares IP ranges are injected directly into `traefik.yml` by a helper script, so no extra plugin middleware is required anymore.
Besides the entrypoint setup, I add cloudflare proxy (for exposing real ip to access.log for crowdsec to read), crowdsec-firewall-bouncer, compression with brotli middlrewares method in traefik.yml and dynamic.yml
Adding middlewares is also guided by labels:
```yaml
label:
- "traefik.http.routers.service-name.middlewares=crowdsec@file,compress-middleware@file"
- "traefik.http.routers.service-name.middlewares=cloudflarewarp@file,crowdsec@file,compress-middleware@file"
```
The order of middlewares is meaningful.
@@ -73,7 +73,7 @@ labels:
- "traefik.http.routers.ghost.rule=Host(`blog.gbanyan.net`)"
- "traefik.http.services.ghost.loadbalancer.server.port=2368"
- "traefik.http.routers.ghost.tls.certresolver=letsencrypt"
- "traefik.http.routers.ghost.middlewares=crowdsec@file,compress-middleware@file"
- "traefik.http.routers.ghost.middlewares=cloudflarewarp@file,crowdsec@file,compress-middleware@file"
- "com.centurylinklabs.watchtower.enable=true"
- "traefik.docker.network=traefik_default"
```
@@ -90,16 +90,4 @@ PS: Because I access my traefik dashboard through my local network. I commented
2. ChangeLog:
- 2025.4.21 Add the defaulthost rule for container name for lazy writing. But commented out for precision.
- 2025.4.21 Fix the trusted IP settings; later replaced by an internal updater instead of the traefik-plugin-cloudflare.
- 2025.4.18 Add Souin HTTP Cache Middleware (in feature branch, not merge into main)
- 2025.4.18 Temp disable the compression middleware. It has MIME type bugs.
## Notes on Host Networking
Traefik currently runs with `network_mode: host` so it can bind directly to both `10.0.0.225` (public) and `192.168.50.4` (internal) entrypoints. Moving back to bridge mode would break that dual-IP isolation because Docker cannot publish the same container port on two different host interfaces. Host networking also means:
- Traefik reaches app containers like any other host process, ignoring `traefik.docker.network` labels.
- Linux handles firewalling/routing between the two interfaces; Dockers conntrack optimizations arent used.
If you ever want to switch to bridge networking, youd need either separate Traefik instances (one per subnet) or an external L4 proxy in front of a single Traefik that listens on generic `:80/:443` ports. For now the host-mode trade-off is intentional to keep the internal/external split simple.
- 2025.4.18 Add Souin HTTP Cache Middleware.

View File

@@ -6,7 +6,7 @@ services:
# ports:
# - 10.0.0.225:80:80
# - 10.0.0.225:443:443
# - 192.168.50.4:8080:8080
# - 192.168.50.4:9090:9090
# - 192.168.50.4:80:80
# - 192.168.50.4:443:443 # Added port mapping for the dashboard
restart: unless-stopped
@@ -33,18 +33,15 @@ services:
- "com.centurylinklabs.watchtower.enable=true" # Added label for Watchtower
# "traefik.http.middlewares.auth.basicauth.usersfile=/dashboard_authfile"
- "traefik.http.services.traefik.loadbalancer.server.port=9090"
#networks:
redis:
image: valkey/valkey:latest
container_name: traefik-redis
restart: unless-stopped
networks:
internal_traefik_default:
ipv4_address: 172.20.0.100
networks:
# traefik_default:
# external: true
# internal_traefik_default:
# external: true
networks:
default:
name: traefik_default
driver: bridge
ipam:
config:
- subnet: 172.19.0.0/16
gateway: 172.19.0.1
internal_traefik_default:
external: true

View File

@@ -1,5 +1,15 @@
http:
middlewares:
block-ip-access:
headers:
customRequestHeaders:
Host: "" # This will catch requests with no Host header or invalid ones
cloudflarewarp:
plugin:
cloudflare:
trustedCIDRs: []
overwriteRequestHeader: true
debug: true
crowdsec:
plugin:
bouncer:
@@ -20,16 +30,41 @@ http:
- application/javascript
- application/json
- text/plain
http-cache:
plugin:
souin:
default_cache:
ttl: 10s
default_cache_control: public, max-age=600
redis:
url: 172.20.0.100://redis:6379
allowed_http_verbs:
- GET
- HEAD
- POST
log_level: debug
api:
souin: {}
prometheus: {}
routers:
qbit:
rule: Host(`qbit.gbanyan.net`)
service: qbit
block-direct-access:
rule: "HostRegexp(`{host:.+}`)" # Matches any host
service: noop@internal
priority: 1 # Low priority to catch unmatched requests
entryPoints:
- web
- websecure
middlewares:
- block-ip-access
netdata:
rule: Host(`netdata.gbanyan.net`)
service: netdata
entryPoints: ["internal_websecure"]
tls:
certResolver: letsencrypt
services:
qbit:
netdata:
loadBalancer:
servers:
- url: "http://192.168.50.4:8083"
- url: "http://127.0.0.1:19999"

View File

@@ -1,107 +0,0 @@
#!/usr/bin/env python3
"""Refresh Cloudflare IP ranges in Traefik config and restart Traefik if needed."""
from __future__ import annotations
import hashlib
import subprocess
import sys
from pathlib import Path
from typing import Iterable, List
from urllib.request import Request, urlopen
CLOUDFLARE_IPV4_URL = "https://www.cloudflare.com/ips-v4"
CLOUDFLARE_IPV6_URL = "https://www.cloudflare.com/ips-v6"
REPO_ROOT = Path(__file__).resolve().parents[1]
TRAEFIK_CONFIG_PATH = REPO_ROOT / "traefik.yml"
ENTRYPOINTS = [" web:", " websecure:"]
USER_AGENT = "TraefikCloudflareUpdater/1.0 (+https://gbanyan.net)"
def fetch_ip_ranges() -> List[str]:
ips: List[str] = []
for url in (CLOUDFLARE_IPV4_URL, CLOUDFLARE_IPV6_URL):
req = Request(url, headers={"User-Agent": USER_AGENT})
with urlopen(req, timeout=10) as resp: # noqa: S310 (trusted upstream)
for raw_line in resp.read().decode().splitlines():
line = raw_line.strip()
if line and not line.startswith("#"):
ips.append(line)
# Remove duplicates while preserving order
seen = set()
deduped: List[str] = []
for ip in ips:
if ip not in seen:
deduped.append(ip)
seen.add(ip)
return deduped
def update_trusted_ips(original: str, ips: Iterable[str], entrypoint_label: str) -> str:
try:
entry_idx = original.index(entrypoint_label)
except ValueError:
raise RuntimeError(f"EntryPoint {entrypoint_label.strip()} not found in traefik.yml") from None
forwarded_token = "forwardedHeaders:\n"
fwd_idx = original.index(forwarded_token, entry_idx)
line_start = original.rfind("\n", 0, fwd_idx) + 1
indent = original[line_start:fwd_idx]
inner_indent = indent + " "
list_indent = inner_indent + " "
block_start = fwd_idx + len(forwarded_token)
cursor = block_start
while cursor < len(original):
next_newline = original.find("\n", cursor)
if next_newline == -1:
next_newline = len(original)
line = original[cursor:next_newline]
stripped = line.lstrip()
current_indent = len(line) - len(stripped)
if not stripped.startswith("-") and current_indent <= len(indent):
break
cursor = next_newline + 1
block_end = cursor
new_block = forwarded_token
new_block += f"{inner_indent}trustedIPs:\n"
for ip in ips:
new_block += f"{list_indent}- \"{ip}\"\n"
return original[:fwd_idx] + new_block + original[block_end:]
def main() -> None:
ips = fetch_ip_ranges()
config_text = TRAEFIK_CONFIG_PATH.read_text()
before_hash = hashlib.sha256(config_text.encode()).hexdigest()
for entry_label in ENTRYPOINTS:
config_text = update_trusted_ips(config_text, ips, entry_label)
after_hash = hashlib.sha256(config_text.encode()).hexdigest()
if after_hash == before_hash:
print("Traefik trusted IP ranges already up-to-date.")
return
TRAEFIK_CONFIG_PATH.write_text(config_text)
print(f"Updated {TRAEFIK_CONFIG_PATH} with {len(ips)} Cloudflare IP ranges.")
try:
subprocess.run(
["docker", "compose", "up", "-d", "traefik"],
check=True,
cwd=REPO_ROOT,
)
print("Traefik container restarted via docker compose.")
except subprocess.CalledProcessError as exc: # pragma: no cover
print("Failed to restart Traefik via docker compose:", exc, file=sys.stderr)
sys.exit(exc.returncode)
if __name__ == "__main__":
try:
main()
except Exception as exc: # noqa: BLE001
print(f"Error: {exc}", file=sys.stderr)
sys.exit(1)

View File

@@ -1,27 +1,14 @@
## STATIC CONFIGURATION
log:
level: "INFO"
level: "DEBUG"
filePath: "/var/log/traefik/traefik.log"
accessLog:
filePath: "/var/log/traefik/access.log"
bufferingSize: 256
filters:
statusCodes:
- "400-599" # focus on failed http requests
fields:
defaultMode: drop
names:
ClientHost: keep
ClientPort: keep
Duration: keep
RequestMethod: keep
RequestPath: keep
RequestProtocol: keep
RouterName: keep
ServiceName: keep
ServiceURL: keep
Status: keep
- "200-299" # log successful http requests
- "400-599" # log failed http requests
api:
insecure: false
@@ -29,32 +16,34 @@ api:
entryPoints:
web:
address: "10.0.0.225:80"
forwardedHeaders:
trustedIPs:
- "173.245.48.0/20"
- "103.21.244.0/22"
- "103.22.200.0/22"
- "103.31.4.0/22"
- "141.101.64.0/18"
- "108.162.192.0/18"
- "190.93.240.0/20"
- "188.114.96.0/20"
- "197.234.240.0/22"
- "198.41.128.0/17"
- "162.158.0.0/15"
- "104.16.0.0/13"
- "104.24.0.0/14"
- "172.64.0.0/13"
- "131.0.72.0/22"
- "2400:cb00::/32"
- "2606:4700::/32"
- "2803:f800::/32"
- "2405:b500::/32"
- "2405:8100::/32"
- "2a06:98c0::/29"
- "2c0f:f248::/32"
http:
address: "10.0.0.225:80"
forwardedHeaders:
trustedIPs: &trustedIps
# Start of Cloudlare's public IP list
- 103.21.244.0/22
- 103.22.200.0/22
- 103.31.4.0/22
- 104.16.0.0/13
- 104.24.0.0/14
- 108.162.192.0/18
- 131.0.72.0/22
- 141.101.64.0/18
- 162.158.0.0/15
- 172.64.0.0/13
- 173.245.48.0/20
- 188.114.96.0/20
- 190.93.240.0/20
- 197.234.240.0/22
- 198.41.128.0/17
- 2400:cb00::/32
- 2606:4700::/32
- 2803:f800::/32
- 2405:b500::/32
- 2405:8100::/32
- 2a06:98c0::/29
- 2c0f:f248::/32
# End of Cloudlare's public IP list
http:
redirections: # HTTPS redirection (80 to 443)
entryPoint:
to: "websecure" # The target element
@@ -62,29 +51,8 @@ entryPoints:
websecure:
address: "10.0.0.225:443"
forwardedHeaders:
trustedIPs:
- "173.245.48.0/20"
- "103.21.244.0/22"
- "103.22.200.0/22"
- "103.31.4.0/22"
- "141.101.64.0/18"
- "108.162.192.0/18"
- "190.93.240.0/20"
- "188.114.96.0/20"
- "197.234.240.0/22"
- "198.41.128.0/17"
- "162.158.0.0/15"
- "104.16.0.0/13"
- "104.24.0.0/14"
- "172.64.0.0/13"
- "131.0.72.0/22"
- "2400:cb00::/32"
- "2606:4700::/32"
- "2803:f800::/32"
- "2405:b500::/32"
- "2405:8100::/32"
- "2a06:98c0::/29"
- "2c0f:f248::/32"
# Reuse the list of Cloudflare's public IPs from above
trustedIPs: *trustedIps
http3: {}
internal_web:
address: "192.168.50.4:80"
@@ -97,7 +65,7 @@ entryPoints:
address: "192.168.50.4:443"
http3: {}
metrics:
address: ":8082"
address: "127.0.0.1:8082"
dashboard:
address: "127.0.0.1:9090"
@@ -108,7 +76,7 @@ global:
providers:
docker:
exposedByDefault: false
# defaultRule: "Host(`{{ .ContainerName }}.gbanyan.net`)"
# network: traefik_default # Ensure this matches the Docker network
file:
filename: "/dynamic.yml" # Enable dynamic configuration file
certificatesResolvers:
@@ -129,6 +97,12 @@ metrics:
experimental:
plugins:
cloudflare:
moduleName: github.com/agence-gaya/traefik-plugin-cloudflare
version: v1.2.0
bouncer:
moduleName: github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
version: v1.4.2
souin:
moduleName: github.com/darkweak/souin
version: v1.7.6