易雾君
易雾君
发布于 2025-02-14 / 1 阅读
0
0

Traefik 3 代理层层穿透,客户端真实 IP如何破局?

我家里的网络均采用了 Traefik 3 作为 Web 入口网关,主要有三个,一个是 DMZ 区的网关,另两个为内部可信区域的网关。Web 代理层级难免会存在多级连的情形,取真实IP苦不堪言,本篇重点来讲讲 DMZ 的区网关配合内部应用获取客户端真实 IP 。这里顺便透漏个我选择 Traefik 作为网关一个原因是它内生支持 TCP 转发,暴露点 ssh 之类的协议嘎嘎香,比如 jumpserver 的 web 控制台和 ssh 入口统一由 Traefik 暴露,优雅而规范。

背景介绍

言归正传,DMZ 区的 Traefik 主要有四个暴露用户渠道。一是显示由 Cloudflare 反代访问形式,理论上同时支持 IPv4 和 IPv6 ,我其实关闭了 IPv6 的访问的,IPv6 的地理信息不健全,不便于管理。二是通过公网 IPv4 渠道,是通过打洞实现,现阶段暂未公开,但实际别人扫 IP 和端口是可以直达访问到的。三是公网 IPv6 渠道,这个 IPv6 地址也暂未对外公开解析,也就是没做 DDNS 解析,但实际也能被扫描端口直达访问的。四是通过内网 IP 访问,这个渠道是在接入家庭WIFI时的场景。大致四个访问渠道可简单归纳为如下示意图。

聊下痛点吧。看到这里其实我的访问需求是很明晰,网络架构也不是太复杂,但在实际网络管理过程中对客户端真实 IP 问题头疼至极,Traefik 的访问日志在这种网络架构下,如果 Traefik 不配置信任转发代理头,有些渠道的客户端 IP 是真实,有些又只能获取到前置代理 IP,这种不统一造成了阻碍排查、统计数据等诸多问题。那么在看下设置 X-Real-IP 头,在设置了信任转发头的可信网段后,如 CLOUDFLARE 的网段,直连渠道又无法设置 X-Real-IP ,步伐不统一,还是会扰乱网络管理。

前段时间我给家里布置了一套蜜罐系统,它取客户端的 IP 是从 XFF 头取的首个 IP ,如果恶意用户在 XFF 头伪造 IP ,会扰乱蜜罐系统的业务逻辑,给告警分析增加负担,还先去确认下 XFF 链,告警的这个远程IP是不是真实 IP ,如果外层网关做一定处理,清除掉伪造IP可以大大减轻这个负担。还有就是家里的 Web 应用防火墙 WAF 为了规范化管理,统一从 X-Real-IP头 取客户端真实IP,还有就是好些后端应用系统喜欢从 X-Real-IP 头提取真实 IP。这个网关比较特殊,既需要接收前置代理 Cloudflare 的代理流量,还需要接收外网直连流量,还需要接收内网客户端直连流量。该网关担负着获取真实IP的重担,它需要设置 X-Real-IP 和 X-Forwarded-For 这两个 HTTP 头,确保他们没有被恶意篡改,而官方的最佳实践配置是无法满足我这个场景需求的,终究还是从源码插件下手。评论区 1 楼即可免费获取。

找了网上有些插件,基本都是信任客户端传递来的特定 HTTP 头,没有核对前置端点是不是可信代理还是真实用户,本插件解决了如下问题:

  • 与内置信任转发头 forwardedHeaders.trustedIPs 轻松共用环境变量,降低维护代理 IP 成本
  • 重写 XFF 头,避免客户端伪造 IP,绕过 WAF 防护等
  • 重写 X-Real-IP 头,确保后端各类服务取到各个渠道访问的真实客户端 IP ,避免业务层来实现复杂判断逻辑

配置实践

这里以 Docker Compose 编排为例,在前期做的异地组网基础上改造。在 headscale 根目录下创建如下目录结构,并切换到新目录下

mkdir -p traefik/plugins-local/src/github.com/evling2020/
cd traefik/plugins-local/src/github.com/evling2020/

然后克隆从我后台拿到的源码仓库地址,得到如下目录结构。

配置Cloudflare可信IP网段和本地可信代理IP地址,分别用两个环境变量表示,CLOUDFLARE_IPS 和 LOCAL_IPS

其中cloudflare我们可以用一个crontab计划任务执行脚本来动态更新.env中的环境变量,脚本内容如下:

#!/bin/bash
# 配置文件路径
config_file="./.env"
IPV4_LIST=$(curl -s https://www.cloudflare.com/ips-v4)
# 如果 curl 失败,则退出并打印错误信息
if [ $? -ne 0 ]; then
    echo "Failed to fetch Cloudflare IPv4 addresses."
    exit 1
fi
IPV6_LIST=$(curl -s https://www.cloudflare.com/ips-v6)
# 如果 curl 失败,则退出并打印错误信息
if [ $? -ne 0 ]; then
    echo "Failed to fetch Cloudflare IPv6 addresses."
    exit 1
fi
ALL_IPS=$(echo -e "$IPV4_LIST\n$IPV6_LIST" | tr '\n' ',')
new_ips="\"${ALL_IPS%,}\""
current_ips=$(grep "^CLOUDFLARE_IPS=" .env | cut -d'=' -f2-)
echo new_ips: $new_ips
echo current_ips: $current_ips
# 如果当前值和新值不同,则更新配置文件
if [[ "$current_ips" != "$new_ips" ]]; then
    # 使用 sed 替换配置文件中的 CLOUDFLARE_IPS 环境变量值
    sed -i "s#^CLOUDFLARE_IPS=.*#CLOUDFLARE_IPS=$new_ips#" "$config_file"
    
    # 输出新值确认替换是否成功
    echo "CLOUDFLARE_IPS has been updated to:"
    grep "CLOUDFLARE_IPS" "$config_file"
    /usr/bin/docker compose up -d
else
    echo "CLOUDFLARE_IPS has not changed. No update needed."
fi

这个插件需要配合转发可信IP插件使用,在traefik的启动命令添加一行代码如下:

- "--entrypoints.websecure.forwardedHeaders.trustedIPs=$CLOUDFLARE_IPS,$LOCAL_IPS"

启动命令还需要加一行本文的主角插件

- "--experimental.localPlugins.traefik-forwarded-real-ip.modulename=github.com/evling2020/traefik-forwarded-real-ip"

随后引入环境变量到容器中,我这里对外的Traefik没有本地代理,就不引入了,仅引入Cloudflare的环境变量。

万事俱备,一键重建容器

docker compose up -d

有效性验证

伪造一个可信 Cloudflare 代理 IP 到 XFF 头,透过 CF 代理访问 wiki 蜜罐

查看蜜罐后台日志,发现伪造的 XFF 头已被正确清理掉,第二个 IPv6 为 Cloudflare 的反代 IP ,符合预期。

接着验证直连方式,通过强制指定 IP 形式访问我打洞出去的 Traefik 端口。

查看蜜罐后台,发现符合预期,伪造 XFF 正确被清理,蜜罐能正确呈现攻击源头,符合预期

最后再测试一个极端情况,倘若有人从可信代理 CF 来访问,比如 CF worker 代理方式会怎样。这里就不贴了,结果当然是符合预期的。

小结

其实到这,我再抛一个问题供思考,那么如何优雅地与内置白名单访问插件配合使用呢?那样岂不是更加完美哉!

#神技能 #家庭基建 #最佳实践


评论