一、前言
随着互联网技术的飞速发展以及数字化转型的浪潮中,IPv6逐渐成为未来网络的主流协议,同时负载均衡也成为必不可少的组件,在使用过程中经常会遇到记录客户端真实IP地址的需求,本文将深入探讨NAT64 LB如何通过TOA(TCP Option Address)、以及七层LB如何通过XFF(X-Forwarded-For)机制获取客户端的真实IP地址,确保在复杂的网络环境和架构中也能精准地识别客户端身份。
二、NAT64 CLB场景通过TOA获取客户端真实IP
在 NAT64 CLB 场景中,客户端真实的 IPv6 源 IP 会被转换成 IPv4 的公网 IP,因此对于真实的服务端的服务而言,无法获得真实的客户端 IPv6 IP。 腾讯云 NAT64 CLB 提供获取客户端真实 IP 的功能,即将客户端真实的源 IP 放入 TCP 协议的自定义 option 中,当被嵌入真实源 IP 的 TCP 数据包发往服务端时,服务端插入的 TOA 内核模块可提取 TCP 数据包中的真实客户端源 IP,此时客户端应用只需要调用 TOA 内核模块提供的接口即可获取真实客户端源 IP。
此场景下,V6真实客户端存入在tcp option kind为253的字段。
1.启用NAT64的TOA选项
NAT64场景只支持四层TCP监听器,确保在监听器页面有勾选开启TOA选项:
2.RS加载TOA模块
1)下载TOA压缩包
不同发行版,对应的压缩包不一样:
发行版 | TOA包 |
---|---|
CentOS | CentOS 8.0 64 / CentOS 7.6 64/ CentOS 7.2 64 |
Debian | Debian 9.0 64 |
Suse Linux | SUSE 12 64/ SUSE 11 64 |
Ubuntu | Ubuntu 18.04.4 LTS 64 / Ubuntu 16.04.7 LTS 64 |
如果有适配的系统版本,可直接下载后解压文件,之后参考步骤3)的加载模块。
2)从源码编译安装
如果上面的TOA包没有对应的系统版本,那么需要对源码包进行编译,由于 Linux 内核版本众多,且 Linux 发行版操作系统市场庞大,版本繁多,因此考虑到内核模块的兼容性问题,建议在使用的系统上对 TOA 源码包进行编译后使用。
Linux:
wget "https://clb-toa-1255852779.file.myqcloud.com/tgw_toa_linux.tar.gz"
腾讯TLinux:
wget "https://clb-toa-1255852779.file.myqcloud.com/tgw_toa_tlinux.tar.gz"
3)加载TOA模块
以Debian 12为例,步骤1)现成的toa.ko并没有适配的版本,因此需要编译一下:
wget "https://clb-toa-1255852779.file.myqcloud.com/tgw_toa_linux.tar.gz"
tar xf tgw_toa_linux.tar.gz
cd tgw_toa/src/tgw_toa_linux
make # 确保编译前有安装gcc编译工具
编译后可以看到生成的toa.ko文件:
此时我们加载此模块:
insmod toa.ko
通过dmesg -T | grep -i TOA
查看内核缓冲区日志,如果出现"toa load success",则说明加载成功。
4)监控TOA模块状态(可选)
执行以下命令可以实时查看已经建立连接的IPv6客户端地址:
cat /proc/net/toa_table
查看TOA的相关计数状态:
cat /proc/net/toa_stats
指标含义如下:
指标名称 | 说明 |
---|---|
syn_recv_sock_toa | 接收带有 TOA 信息的连接个数。 |
syn_recv_sock_no_toa | 接收并不带有 TOA 信息的连接个数。 |
getname_toa_ok | 调用 getsockopt 获取源 IP 成功即会增加此计数,另外调用 accept 函数接收客户端请求时也会增加此计数。 |
getname_toa_mismatch | 调用 getsockopt 获取源 IP 时,当类型不匹配时,此计数增加。例如某条客户端连接内存放的是 IPv4 源 IP,并非为 IPv6 地址时,此计数便会增加。 |
getname_toa_empty | 对某一个不含有 TOA 的客户端文件描述符调用 getsockopt 函数时,此计数便会增加。 |
ip6_address_alloc | 当 TOA 内核模块获取 TCP 数据包中保存的源 IP、源 Port 时,会申请空间保存信息。 |
ip6_address_free | 当连接释放时,toa 内核模块会释放先前用于保存源 IP、源 port 的内存,在所有连接都关闭的情况下,所有 CPU 的此计数相加应等于 ip6_address_alloc 的计数。 |
3.测试验证
找一台具备公网IPv6的客户端来请求NAT64 CLB,并且同时在RS后端服务器抓包看看,是否有通过TOA拿到客户端的真实IP地址,环境如下:
角色 | IPv6 | 端口 |
---|---|---|
客户端 | 2402:4e00:101a:4f00:0:9c9f:be50:d15a | 随机 |
NAT64 CLB | 2402:4e00:40:40::2:3b9 | 80 |
RS | 不涉及 | 8080 |
1)查看TOA表记录
使用客户端telnet NAT64 LB,并且保持连接不中断:
telnet -6 2402:4e00:40:40::2:3b9 80
同时查看toa表是否有获取到客户端真实IP:
cat /proc/net/toa_table
可以看到,/proc/net/toa_table成功记录到客户端的真实IP:PORT,但客户端关闭连接后,则清空记录。
同理,可以查看toa的计数状态:
cat /proc/net/toa_stats
2)抓包验证
若存在 unknown-253
字段,则说明在 NAT64 场景下的真实 IPv6 的源 IP 已经插入。
或者使用wireshark打开分析,筛选tcp options类型为253的包:
tcp.option_kind == 253
红圈中的字段即为客户端真实V6地址。四层场景,第一次握手(SYN)、第三次握手(ACK),都会携带客户端真实IP插入到tcp option,其中Experiment Identifier携带的16进制内容0xb010为客户端源端口,转换为十进制为45072。
模拟七层场景的情况,NAT64 LB监听器依然还是四层,客户端curl NAT64 LB的80端口:
curl -6 http://[2402:4e00:40:40::2:3b9]
七层场景,在客户端向服务端发起HTTP GET或别的请求方法时(如POST、PUT、DELETE等),也会携带真实IP插入到tcp option。
同时也可以通过tshark配合awk、sed,过滤到包中的v6客户端真实IP,并且添加冒号还原完整:
tshark -n -r rs.pcap -Y 'tcp.option_kind == 253 ' -V |grep -Po '(?<=Data:\s).*'|sort -u|awk -F '' '{for(i=1;i<=NF;i++) {if(i%4==0) printf("%s:",$i); else printf("%s",$i)}}' | sed 's/.$//'
附带完整抓包文件:rs_toa.rar
3)适配后端服务
如果想在后端服务中拿到这个真实IP,需要对后端服务进行源码改造,可以参考官网的示例。
比如Nginx,将真实客户端V6地址保存为一个新变量,后续nginx去引用这个变量,再通过log_format输出呈现出来,但需要nginx跟lua脚本开发能力。
三、七层CLB通过XFF获取客户端真实IP
七层监听器,默认会插入X-Forwarded-For字段转发给RS,不管是纯V6 LB还是V4 LB,亦或者NAT64 LB,只要是七层监听器都会插入,其中NAT64 七层监听器的XFF携带的是V4转发IP,非客户端真实IP,因此更建议使用四层TOA进行记录。
在CLB与后端服务之间使用短连接时,在后端RS获取的源IP即为客户端IP;在CLB与后端服务之间使用长连接时,CLB 不再透传源 IP,可以通过 X-Forwarded-For 或 remote_addr 字段来直接获取客户端 IP。
1.抓包验证
客户端通过curl 七层LB监听器,我们在客户端RS同时抓包比对:
可以看到客户端发送GET请求时并没有携带XFF、X-Real-IP等HTTP头部字段,但在RS抓包,这些头部字段已经有取值了,因为七层监听器会经过LB的STGW网关,将这些值插入进去再转发给后端RS,同时也不能严格通过tcp.seq_raw序列号来比对TCP流,因为stgw到RS这一段,类似七层反向代理,客户端到LB和LB到RS,TCP流并非同一条,理解为client –> TGW 是一条TCP连接,STGW –> RS 是另一条TCP连接,因此不能通过绝对seq序列号进行比对。
RS正常返回业务响应后,最后收到了RST,ACK断连,而非正常的FIN,ACK四次挥手关闭连接,因为STGW拿到正常响应后,便可将响应转发给客户端,RST直接断连和正常四次挥手结束连接,前者更节省流量和开销。
2.Nginx通过XFF和X-Real-IP记录日志
1)X-Forwarded-For
既然客户端请求过来,RS收到包时已经有XFF和X-Real-IP字段,那么在Nginx设置log_format日志格式把这两个字段取值展示出来即可:
log_format main '$http_x_forwarded_for-[$time_local]'
'"$request"$status $body_bytes_sent'
'"$http_referer" "$http_user_agent"';
access_log /var/log/nginx/access.log main;
第一列即为XFF携带的IP:
而如果客户端在七层LB插入XFF之前,自己携带了一个XFF地址呢,这时候顺序应该会怎样?
不妨模拟下,客户端主动携带一个XFF地址再请求LB:
curl -H 'X-Forwarded-For:1.1.1.1' LBIP
可以看到X-Forward-For此时记录了两个地址,第一个是在LB之前客户端主动携带的,第二个是七层LB主动插入的。
我们再大胆点尝试下,假设在LB之前,有经过三个代理服务器,并且每一层都往XFF头加入自己的IP,客户端模拟同时携带三个XFF IP:
curl -H 'X-Forwarded-For:1.1.1.1' -H 'X-Forwarded-For:1.1.1.2' -H 'X-Forwarded-For:1.1.1.3' LBIP
效果一样,全部都会按顺序记录,但真实客户端永远是第一个IP,即第一个IP发出的原始请求。
因此,我们可以知道,七层LB对于XFF的处理,逻辑如下:
- 其中,代理服务器对于LB来说是相对客户端,因为是它和LB直接建立连接,因此LB记录的X-Real-IP也是相对客户端,即LB的直接上级,LB收到相对客户端的请求后,将相对客户端的IP存入到X-Real-IP内,同时附加在XFF后面;
- 绝对客户端IP:1.1.1.1,原始请求是它发出来的。
图中代理服务器可以是CDN、WAF、反向代理等等,全部适用,LB能做的就是把相对客户端的IP附加到X-Forwarded-For,如果LB收到的XFF已经有记录多个IP了,也并不会去改动这些IP,只会附加,但如果相对客户端并不会携带真实客户端的IP地址给LB,那LB也无能为力,不会自己变一个出来。
因此,客户端真实IP,往往是XFF的第一个IP,而XFF记录的最后一个IP,则是和LB之间建立连接的相对客户端。
2)X-Real-IP | remote_addr | realip_remote_addr
使用remote_addr或x-reap-ip变量的前提是nginx已经安装了http_realip_module
模块,使用Nginx -V可以查看当前安装的模块:
nginx -V
如果未安装,则无法正常读取两个变量值,需要进入到nginx源码目录,重新编译进去。
http_realip_module模块的用法可以参考nginx官方文档,内嵌了两个变量:
-
$realip_remote_addr:客户端真实IP
-
$realip_remote_port:客户端真实端口
增加如下nginx配置,把nginx的日志格式修改如下:
set_real_ip_from 0.0.0.0/0; #所有请求都从XFF中获取源IP
real_ip_header X-Forwarded-For; #所有请求都从XFF中获取源IP
real_ip_recursive on;
log_format main '$remote_addr:$remote_port - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent"-$realip_remote_addr:$realip_remote_port';
access_log /var/log/nginx/access.log main;
其中$remote_addr:$remote_port和$realip_remote_addr:$realip_remote_port等同效果,测试都能通过日志拿到客户端真实IP和端口:
当real_ip_header设置成从X-Forwarded-For获取,此时$remote_addr则会读取XFF从左到右第一个IP,当real_ip_header 设置成从X-Real-IP获取,则直接通过TCP连接的方式读取和LB直接建联的相对客户端,无法通过指定七层头部进行伪造。
比如基于上面这段日志格式,$remote_addr读取到的的是XFF的第一个IP,并且客户端端口是读取不到的,XFF没有记录端口的能力,而$realip_remote_addr和$realip_remote_port未受影响:
此时我们将real_ip_header设置成从X-Real-IP读取:
set_real_ip_from 0.0.0.0/0;
real_ip_header X-Real-IP;
real_ip_recursive on;
log_format main '$remote_addr:$remote_port - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent"-$realip_remote_addr:$realip_remote_port';
access_log /var/log/nginx/access.log main;
可以正常读取到和LB直接建联的客户端IP和端口。
我们把$remote_addr 替换成 $http_x_real_ip:
log_format main '$http_x_real_ip:$remote_port - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent"-$realip_remote_addr:$realip_remote_port';
access_log /var/log/nginx/access.log main;
此时不管real_ip_header从哪里获取,七层LB都会通过TCP连接获取到和LB之间建联的上级客户端,保存到七层X-Real-IP字段,伪造也不生效,到LB处理时会覆盖上去,和XFF的追加有点不一样。
综上,如果客户端真实IP记录在XFF,那么建议将real_ip_header设置成从X-Forwarded-For获取,如果没有中间代理层,真实客户端直接请求LB,那么可以将real_ip_header设置成从X-Real-IP获取或XFF获取,通过读取$http_x_real_ip或$realip_remote_addr和$realip_remote_port获取客户端IP和端口。
3.CDN场景
CDN边缘节点向LB回源转发,CDN对于LB来说就是前面说的相对客户端的概念,和CLB直接建立连接的客户端,处理过程如下图:
给七层LB套一个CDN加速域名,此时客户端模拟访问:
curl <cdn加速域名>
在RS抓包可以清晰看到,七层LB插入了XFF字段,同时X-Real-IP字段即和LB之间建联的相对客户端,即CDN厂商的边缘加速节点:
X-Forwarded-For包含了两个IP地址,第一个为客户端真实来源IP,第二个为CDN厂商的加速IP,同时此CDN厂商还携带了Client-IP字段用来传递客户端真实IP,在他们官网也能查阅到:
既然XFF保存了第一个IP作为客户端真实IP,那么Nginx只需要做如下设置,即可正确获取到客户端真实IP地址:
set_real_ip_from 0.0.0.0/0;
real_ip_header X-Forwarded-For;
real_ip_recursive on;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent"'';
access_log /var/log/nginx/access.log main;
如果想记录XFF所有的IP,包括CDN的加速IP,那么使用$http_x_forwarded_for变量即可。
四、总结
本文深入探讨了在复杂的网络环境和架构中,如何通过NAT64 CLB和七层CLB获取客户端的真实IP地址。在NAT64 CLB场景中,通过TOA(TCP Option Address)机制,可以在内核模块中提取TCP数据包中的真实客户端源IP,在SNAT或Full Nat场景下帮助极大。而在七层CLB场景中,通过XFF(X-Forwarded-For)机制,可以在后端服务器中获取客户端的真实IP地址。同时也详细阐述了在Nginx如何设置日志格式正确读取到XFF头部字段里的值,以及CDN常见场景下的演示。
通过探索本文,可以更好地理解在不同网络架构下如何获取客户端的真实IP地址,从而确保在复杂的网络环境中也能精准地识别和记录客户端身份。这对于网络安全、用户行为分析以及合规性要求等方面具有重大意义。