Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 25 additions & 8 deletions api/waf_vpconfig_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,27 +140,44 @@ func (w *WafVpConfigApi) UpdateManageTrustedProxiesApi(c *gin.Context) {
response.FailWithMessage("解析请求失败", c)
return
}
// 校验每个条目是合法 CIDR 或 IP(留空=不信任任何代理头,允许)
// 校验每个条目是合法 CIDR / IP / private 关键字(留空=不信任任何代理头,允许)
for _, entry := range strings.Split(req.TrustedProxies, ",") {
entry = strings.TrimSpace(entry)
if entry == "" {
continue
}
if strings.Contains(entry, "/") {
if _, _, err := net.ParseCIDR(entry); err != nil {
if !utils.IsValidManageTrustedProxyEntry(entry) {
if strings.Contains(entry, "/") {
response.FailWithMessage(fmt.Sprintf("非法的CIDR: %s", entry), c)
return
} else {
response.FailWithMessage(fmt.Sprintf("非法的IP: %s", entry), c)
}
} else if net.ParseIP(entry) == nil {
response.FailWithMessage(fmt.Sprintf("非法的IP: %s", entry), c)
return
}
}
if err := wafconfig.UpdateManageTrustedProxies(req.TrustedProxies); err != nil {
response.FailWithMessage("更新管理端可信代理网段失败: "+err.Error(), c)
} else {
response.OkWithMessage("更新管理端可信代理网段成功", c)
return
}
// 过宽的网段等于没有闸门,代理头不予采信、仍按网络层 IP 识别客户端,保存后明确告知一次
if broad, entry := utils.ManageTrustedProxiesHasOverBroad(req.TrustedProxies); broad {
zlog.Warn(fmt.Sprintf("管理端可信代理网段包含过宽条目 %s:该条目只能放行闸门,代理头里的客户端IP不予采信,仍按网络层IP识别。容器部署请改填网关地址或 private", entry))
response.OkWithMessage(fmt.Sprintf("更新成功,但 %s 过宽:这种网段无法用来判定代理头里的哪个IP是客户端,代理头将不被采信、仍按网络层IP识别(IP白名单与登录失败锁定也按它判定)。容器/内网部署请改填上游代理的地址或 private", entry), c)
return
}
response.OkWithMessage("更新管理端可信代理网段成功", c)
}

// GetManageClientIPProbeApi 管理端「本次访问」真实IP诊断
// @Summary 管理端真实IP诊断
// @Description 回显本次请求的直连对端、可信代理判定、各代理头原始值与逐跳判定、最终采信的客户端IP及原因
// @Tags 管理端配置
// @Produce json
// @Success 200 {object} response.Response "获取成功"
// @Security ApiKeyAuth
// @Router /vipconfig/manageClientIpProbe [get]
func (w *WafVpConfigApi) GetManageClientIPProbeApi(c *gin.Context) {
response.OkWithDetailed(utils.TraceManageClientIP(c), "获取管理端真实IP诊断成功", c)
}

// GetManageCDNProviderApi 获取管理端引用的 CDN 厂商码
Expand Down
4 changes: 4 additions & 0 deletions router/waf_vpconfig_router.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ func (receiver *WafVpConfigRouter) InitWafVpConfigRouter(group *gin.RouterGroup)
writeRouter.Use(middleware.RequireRole(enums.ROLE_SYSTEM_ADMIN))
writeRouter.POST("/api/v1/vipconfig/updateIpWhitelist", wafVpConfigApi.UpdateIpWhitelistApi)
writeRouter.POST("/api/v1/vipconfig/updateManageTrustedProxies", wafVpConfigApi.UpdateManageTrustedProxiesApi)
// 只读诊断:仅回显本次请求自身的判定过程,不接受任何入参指定IP。
// 放在系统管理员组:回显内容含代理头名(原本只在系统参数页可见)+可信网段,
// 凑齐即可推出"如何让自己在审计日志里显示成别的IP",不给审计/安全角色。
writeRouter.GET("/api/v1/vipconfig/manageClientIpProbe", wafVpConfigApi.GetManageClientIPProbeApi)
// CDN厂商快捷填充回源段(会触发对厂商官方端点的匿名拉取,故限系统管理员)
writeRouter.GET("/api/v1/vipconfig/cdnProviderRanges", wafVpConfigApi.GetCDNProviderRangesApi)
writeRouter.POST("/api/v1/vipconfig/updateManageCDNProvider", wafVpConfigApi.UpdateManageCDNProviderApi)
Expand Down
78 changes: 0 additions & 78 deletions utils/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"SamWaf/common/zlog"
"SamWaf/global"
"SamWaf/model"
"SamWaf/wafenginecore/clientip"
"SamWaf/wafenginecore/ipset"
"crypto/tls"
"errors"
Expand All @@ -19,8 +18,6 @@ import (
"strconv"
"strings"
"time"

"github.com/gin-gonic/gin"
)

// isPublicIP 判断 IP 是否为可安全对外访问的公网地址(用于防 SSRF)。
Expand Down Expand Up @@ -648,78 +645,3 @@ func IsIP(input string) bool {
return net.ParseIP(input) != nil
}

// GetManageClientIP 获取管理端客户端真实IP。
// 安全默认:未配置代理头(GCONFIG_MANAGE_PROXY_HEADER 为空)时直接返回网络层 IP(c.RemoteIP())。
// 即便配置了代理头,也仅当“直连对端 c.RemoteIP() 属于可信代理网段 GCONFIG_MANAGE_TRUSTED_PROXIES”
// 时才采信代理头;否则一律用网络层 IP。防止任意直连客户端伪造 X-Forwarded-For/X-Real-IP 绕过
// 登录错误锁定 / 管理端 IP 白名单 / 令牌 IP 绑定。
func GetManageClientIP(c *gin.Context) string {
remoteIP := c.RemoteIP()
// 未配置代理头 → 直接用网络层 IP
if global.GCONFIG_MANAGE_PROXY_HEADER == "" {
return remoteIP
}
// 配了代理头,但只信任来自“可信代理”的头;否则用网络层 IP
if !isTrustedManageProxy(remoteIP) {
return remoteIP
}
for _, header := range strings.Split(global.GCONFIG_MANAGE_PROXY_HEADER, ",") {
header = strings.TrimSpace(header)
if header == "" {
continue
}
val := c.GetHeader(header)
if val == "" {
continue
}
// 从右往左取第一个“非可信代理”的 IP:反向代理按追加语义
// (nginx $proxy_add_x_forwarded_for) 把真实客户端 IP 追加在右侧、客户端伪造的值留在左侧,
// 故取最右侧的非可信 hop 才是真实客户端;逐个跳过可信代理链。取最左会取到伪造值。
parts := strings.Split(val, ",")
for i := len(parts) - 1; i >= 0; i-- {
ip := strings.TrimSpace(parts[i])
if !IsValidIPv4(ip) && !IsValidIPv6(ip) {
continue
}
if isTrustedManageProxy(ip) {
continue // 跳过可信代理 hop
}
return ip
}
}
return remoteIP
}

// isTrustedManageProxy 判断直连对端 IP 是否落在管理端可信代理网段
// GCONFIG_MANAGE_TRUSTED_PROXIES(CIDR 或单 IP,逗号分隔)内。
// 留空 → 返回 false(不信任任何代理头,安全默认)。
func isTrustedManageProxy(remoteIP string) bool {
// 管理端引用了某 CDN 厂商 → 直连对端属于该厂商中心库最新回源段即视为可信(自动跟随更新)
if global.GCONFIG_MANAGE_CDN_PROVIDER != "" &&
clientip.IsProviderIP(global.GCONFIG_MANAGE_CDN_PROVIDER, strings.TrimSpace(remoteIP)) {
return true
}
if global.GCONFIG_MANAGE_TRUSTED_PROXIES == "" {
return false
}
ip := net.ParseIP(strings.TrimSpace(remoteIP))
if ip == nil {
return false
}
for _, entry := range strings.Split(global.GCONFIG_MANAGE_TRUSTED_PROXIES, ",") {
entry = strings.TrimSpace(entry)
if entry == "" {
continue
}
if strings.Contains(entry, "/") {
if _, ipnet, err := net.ParseCIDR(entry); err == nil && ipnet.Contains(ip) {
return true
}
continue
}
if single := net.ParseIP(entry); single != nil && single.Equal(ip) {
return true
}
}
return false
}
Loading
Loading