feat: public status page endpoint with per-address rate limit

This commit is contained in:
2026-08-24 14:29:24 +00:00
parent 21a2d077d8
commit a3c6b2a305
5 changed files with 351 additions and 0 deletions
+27
View File
@@ -263,6 +263,14 @@ func serve() {
})
r := gin.New()
// Without this gin trusts every proxy and ClientIP() is whatever the
// caller wrote in X-Forwarded-For. That was survivable while ClientIP()
// only produced audit strings; the public status limiter makes it load
// bearing. Empty means trust nobody, which is correct for a direct
// exposure and wrong behind a proxy — hence the explicit setting.
if err := r.SetTrustedProxies(trustedProxies()); err != nil {
log.Fatalf("trusted proxies: %v", err)
}
r.Use(gin.Recovery())
r.Use(gin.LoggerWithConfig(gin.LoggerConfig{SkipPaths: []string{"/api/console/tunnel"}}))
r.Use(corsMiddleware())
@@ -318,6 +326,25 @@ func corsMiddleware() gin.HandlerFunc {
}
}
// trustedProxies reads TRUSTED_PROXIES, a comma-separated list of CIDRs or
// addresses. Unset means trust none: ClientIP() is then the peer address,
// which is right for a direct exposure and means every request behind an
// un-configured proxy shares one address for rate limiting. That is a visible
// failure (one client limited) rather than an invisible one (no limit at all).
func trustedProxies() []string {
v := strings.TrimSpace(os.Getenv("TRUSTED_PROXIES"))
if v == "" {
return nil
}
out := []string{}
for _, p := range strings.Split(v, ",") {
if p = strings.TrimSpace(p); p != "" {
out = append(out, p)
}
}
return out
}
func getEnv(key, fallback string) string {
if v := os.Getenv(key); v != "" {
return v