fosrl / fosrl/pangolin

enhance(badger): Expose configuration options for badger@http middleware

Open
#2,597 14 comments 0 reactions 0 assignees View on GitHub
enhancement
Dominant language
TypeScript
Stars
22.8k
Forks
783
Avg merge
1d 7h
Merged PRs (30d)
53

Description

### Describe the Bug

The log files do not show the user's IP address, but rather that of the Docker host. Basically, I have a standard

setup: Cloudflare Tunnel (proxy) - VM with Docker-based Pangolin and Traefik.

In Traefik, the forwardedHeaders: trustedIPs are configured and pangolin server has trust_proxy: 2 configured. If I use the fork github.com/hajimAIM/badger in version v1.2.1 instead of the badger traefik plugin github.com/fosrl/badger v1.3.1, it works as expected.

Image

Image

### Environment

- OS Type & Version: Debian GNU/Linux 12 (bookworm)
- Pangolin Version: 1.16.2
- Gerbil Version: 1.3.0
- Traefik Version: 3.6
- Newt Version: not used - whoami on local VM
- Badger Version: v1.3.1

### To Reproduce

docker-compose.yml
```
name: pangolin
services:
pangolin:
image: docker.io/fosrl/pangolin:1.16.2
container_name: pangolin
restart: unless-stopped
deploy:
resources:
limits:
memory: 1g
reservations:
memory: 256m
volumes:
- ./config:/app/config
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3001/api/v1/"]
interval: "10s"
timeout: "10s"
retries: 15
networks:
- pangolin

gerbil:
image: docker.io/fosrl/gerbil:1.3.0
container_name: gerbil
restart: unless-stopped
depends_on:
pangolin:
condition: service_healthy
command:
- --reachableAt=http://gerbil:3004
- --generateAndSaveKeyTo=/var/config/key
- --remoteConfig=http://pangolin:3001/api/v1/
volumes:
- ./config/:/var/config
cap_add:
- NET_ADMIN
- SYS_MODULE
networks:
- pangolin
- cftunnel-transport
ports:
- 51820:51820/udp
- 21820:21820/udp
#- 443:443
#- 80:80

traefik:
image: docker.io/traefik:v3.6
container_name: traefik
restart: unless-stopped
network_mode: service:gerbil # Ports appear on the gerbil service
depends_on:
pangolin:
condition: service_healthy
command:
- --configFile=/etc/traefik/traefik_config.yml
volumes:
- ./config/traefik:/etc/traefik:ro # Volume to store the Traefik configuration
- ./config/letsencrypt:/letsencrypt # Volume to store the Let's Encrypt certificates
- ./config/traefik/logs:/var/log/traefik # Volume to store Traefik logs
environment:
TZ: "Europe/Berlin"
CF_API_EMAIL: "xxx"
CF_DNS_API_TOKEN: "xxxx"
CF_ZONE_API_TOKEN: "xxx"

whoami:
image: "traefik/whoami"
container_name: whoami
hostname: whoami
depends_on:
pangolin:
condition: service_healthy
restart: unless-stopped
networks:
- pangolin

cf-tunnel:
image: cloudflare/cloudflared:latest
container_name: cf-tunnel
hostname: cf-tunnel
restart: unless-stopped
networks:
- cftunnel-transport
command: tunnel --no-autoupdate run
environment:
- TUNNEL_TOKEN=xxx

networks:
pangolin:
driver: bridge
name: pangolin
enable_ipv6: true
cftunnel-transport:
driver: bridge
name: cftunnel-transport
enable_ipv6: true
```

config.yml
```
# To see all available options, please visit the docs:
# https://docs.pangolin.net/

gerbil:
start_port: 51820
base_endpoint: "xxx.goodsrv.de"

app:
dashboard_url: "https://pangolin.xxx.me"
log_level: "debug"
#log_level: "info"
telemetry:
anonymous_usage: true

domains:
domain1:
base_domain: "xxx.me"

server:
trust_proxy: 2
secret: "xxx"
cors:
origins: ["https://pangolin.xxx.me"]
methods: ["GET", "POST", "PUT", "DELETE", "PATCH"]
allowed_headers: ["X-CSRF-Token", "Content-Type"]
credentials: false
maxmind_db_path: "./config/GeoLite2-Country.mmdb"

email:
smtp_host: "xxx"
smtp_port: 587
smtp_user: "xxx"
smtp_pass: "xxx"
no_reply: "xxx"

flags:
require_email_verification: true
disable_signup_without_invite: true
disable_user_create_org: false
allow_raw_resources: true
```
traefik_config.yml
```
api:
insecure: true
dashboard: true

providers:
http:
endpoint: "http://pangolin:3001/api/v1/traefik-config"
pollInterval: "5s"
file:
filename: "/etc/traefik/dynamic_config.yml"

experimental:
plugins:
badger:
moduleName: "github.com/fosrl/badger"
version: "v1.3.1"
real-ip:
moduleName: "github.com/Paxxs/traefik-get-real-ip"
version: "v1.0.4"

log:
level: "INFO"
format: "common"
maxSize: 100
maxBackups: 3
maxAge: 3
compress: true

certificatesResolvers:
letsencrypt:
acme:
httpChallenge:
entryPoint: web
email: "xxx"
storage: "/letsencrypt/acme.json"
caServer: "https://acme-v02.api.letsencrypt.org/directory"
cloudflare:
acme:
email: xxx
storage: /letsencrypt/acme_cf.json
keyType: EC384
dnschallenge:
provider: cloudflare
resolvers:
- "1.1.1.1:53"
- "1.0.0.1:53"

entryPoints:
web:
address: ":80"
http:
#middlewares:
# - real-ip@file
forwardedHeaders:
trustedIPs: &trustedIps
# Cloudflare 2024 12 14 https://www.cloudflare.com/ips
- 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
# local
- 172.16.0.0/16
- 172.17.0.0/16
- 172.18.0.0/16
- 172.19.0.0/16
- 172.20.0.0/16
- 172.21.0.0/16
- 172.22.0.0/16
- 172.23.0.0/16

#proxyProtocol:
# trustedIPs: *trustedIps

websecure:
address: ":443"
forwardedHeaders:
trustedIPs: *trustedIps
#proxyProtocol:
# trustedIPs: *trustedIps
transport:
respondingTimeouts:
readTimeout: "30m"
http:
#middlewares:
# - real-ip@file
#tls:
# certResolver: "letsencrypt"
tls:
certResolver: cloudflare
domains:
- main: xxx.me
encodedCharacters:
allowEncodedSlash: true
allowEncodedQuestionMark: true

serversTransport:
insecureSkipVerify: true

ping:
entryPoint: "web"

tls:
options:
default:
minVersion: VersionTLS13
sniStrict: true
cipherSuites:
- TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
- TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
- TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
- TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
- TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305
- TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305
curvePreferences:
- CurveP521
- CurveP384
```
dynamic_config.yml
```
http:
middlewares:
badger:
plugin:
badger:
disableForwardAuth: true
redirect-to-https:
redirectScheme:
scheme: https
real-ip:
plugin:
real-ip:
enableLog: true # default: false, enable to see detailed logs
deny403OnFail: true # default: false, when true returns 403 if no matching CDN header found
eraseProxyHeaders: false # default: false, erase CDN-specific headers after processing
Proxy:
- proxyHeadername: X-From-Cdn
proxyHeadervalue: cf-foo
realIP: Cf-Connecting-Ip
OverwriteXFF: true # default: false, v1.0.2 or above
- proxyHeadername: "*"
realIP: RemoteAddr

routers:
# HTTP to HTTPS redirect router
main-app-router-redirect:
rule: "Host(`pangolin.xxx.me`)"
service: next-service
entryPoints:
- web
middlewares:
- redirect-to-https
#- real-ip
- badger

# Next.js router (handles everything except API and WebSocket paths)
next-router:
rule: "Host(`pangolin.xxx.me`) && !PathPrefix(`/api/v1`)"
service: next-service
entryPoints:
- websecure
middlewares:
#- real-ip
- badger
tls:
certResolver: cloudflare
domains:
- main: "xxx.me"
sans:
- "*.xxx.me"

# API router (handles /api/v1 paths)
api-router:
rule: "Host(`pangolin.xxx.me`) && PathPrefix(`/api/v1`)"
service: api-service
entryPoints:
- websecure
middlewares:
#- real-ip
- badger
tls:
certResolver: cloudflare
domains:
- main: "xxx.me"
sans:
- "*.xxx.me"

# WebSocket router
ws-router:
rule: "Host(`pangolin.xxx.me`)"
service: api-service
entryPoints:
- websecure
middlewares:
#- real-ip
- badger
tls:
certResolver: cloudflare
domains:
- main: "xxx.me"
sans:
- "*.xxx.me"
services:
next-service:
loadBalancer:
servers:
- url: "http://pangolin:3002" # Next.js server

api-service:
loadBalancer:
servers:
- url: "http://pangolin:3000" # API/WebSocket server

tcp:
serversTransports:
pp-transport-v1:
proxyProtocol:
version: 1
pp-transport-v2:
proxyProtocol:
version: 2
```

### Expected Behavior

I would expect my IPv4 (or IPv6, if applicable) address to be in the log file and also at Whoami under X-Real-Ip.

I would expect my IPv4 (or IPv6, if applicable) address to appear in the log file and also under X-Real-Ip in Whoami. This seems to work when I use the fork github.com/hajimAIM/badger in version v1.2.1 for Badger. However, I then have to deactivate the middleware.

Contributor guide

Open the contributing guide

Research direction

Start with the badger middleware configuration in dynamic_config.yml and its Traefik plugin declaration in traefik_config.yml, then reproduce the request path through the Cloudflare Tunnel and Docker setup. Done means the configured middleware preserves the client IPv4 or IPv6 address in logs and exposes it as X-Real-Ip in Whoami, matching the expected behavior described here.

Written by the indexing model from the issue text.

Assessment

Tech stack
docker
Domain
backend, networking
Issue type
Feature
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.