Files

104 lines
3.3 KiB
Go
Raw Permalink Normal View History

package middleware
import (
"log"
"net"
"net/http"
"os"
"strings"
)
// metricsAllowList 是 METRICS_ALLOW_CIDR 解析出的内网白名单。
// 启动期由 InitMetricsAllowList 填充;运行期只读,无并发写。
var metricsAllowList []*net.IPNet
// metricsAllowConfigured 表示是否配置了非空的 METRICS_ALLOW_CIDR。
// 决定根路径 /metrics、/health 是否注册(未配置则完全不注册,访问 404)。
var metricsAllowConfigured bool
// InitMetricsAllowList 解析 METRICS_ALLOW_CIDR,启动期调用一次。
//
// 格式:逗号分隔的 CIDR 列表,例如 "10.0.0.0/8,172.16.0.0/12,192.168.0.0/16"。
// 也接受裸 IP(自动补 /32 或 /128),方便写 "127.0.0.1"。
//
// 为什么需要它:根路径 /metrics、/health 是给 Prometheus 抓取 / K8s 探针用的机器端点,
// 让它们带 Bearer 会强制改抓取配置;用内网白名单更省事,且默认不暴露。
//
// 【重要】Docker 环境不要填 127.0.0.1/32 —— 那是容器自身的回环,
// Prometheus 在宿主机或另一个容器里根本进不来。请填容器内网网段(如 172.16.0.0/12)。
func InitMetricsAllowList() {
raw := strings.TrimSpace(os.Getenv("METRICS_ALLOW_CIDR"))
metricsAllowList = nil
metricsAllowConfigured = false
if raw == "" {
return
}
for _, part := range strings.Split(raw, ",") {
entry := strings.TrimSpace(part)
if entry == "" {
continue
}
// 裸 IP 自动补全掩码
if !strings.Contains(entry, "/") {
if ip := net.ParseIP(entry); ip != nil {
if ip.To4() != nil {
entry += "/32"
} else {
entry += "/128"
}
}
}
_, ipnet, err := net.ParseCIDR(entry)
if err != nil {
log.Printf("[metrics-ip] 忽略非法 CIDR 条目 %q: %v", part, err)
continue
}
metricsAllowList = append(metricsAllowList, ipnet)
}
metricsAllowConfigured = len(metricsAllowList) > 0
if metricsAllowConfigured {
log.Printf("[metrics-ip] METRICS_ALLOW_CIDR 已启用,根路径 /metrics 仅对 %d 个网段开放", len(metricsAllowList))
} else {
log.Printf("[metrics-ip] METRICS_ALLOW_CIDR 无有效条目,根路径 /metrics 将不注册")
}
}
// MetricsAllowListConfigured 报告是否配置了有效的白名单网段。
// router 据此决定是否注册根路径 /metrics / /health。
func MetricsAllowListConfigured() bool { return metricsAllowConfigured }
// MetricsIPAllowList 只放行来源 IP 命中白名单的请求。
//
// 未命中返回 **404**(而不是 403):不向扫描者确认"这里存在一个只是你没权限的端点"。
// 客户端 IP 取自 GetClientIP,它已处理 X-Forwarded-For 与 TRUSTED_PROXY_HOPS,
// 所以反代后面的真实来源也能正确判定。
func MetricsIPAllowList(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !ipAllowed(GetClientIP(r)) {
// 不打印每个被拒请求,避免扫描流量刷爆日志;只按需在 debug 下输出
http.NotFound(w, r)
return
}
next.ServeHTTP(w, r)
})
}
// ipAllowed 判定客户端 IP 是否命中任一白名单网段。
// 空 IP(解析失败)一律拒绝 —— 宁可拒绝也不能误放行。
func ipAllowed(clientIP string) bool {
if clientIP == "" {
return false
}
ip := net.ParseIP(strings.TrimSpace(clientIP))
if ip == nil {
return false
}
for _, n := range metricsAllowList {
if n.Contains(ip) {
return true
}
}
return false
}