「最佳实践」腾讯云CLB负载均衡通过TOA和XFF获取客户端真实IP:涵盖七层LB和NAT64 LB

一、前言

随着互联网技术的飞速发展以及数字化转型的浪潮中,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:

代码语言:bash
复制
wget "https://clb-toa-1255852779.file.myqcloud.com/tgw_toa_linux.tar.gz"

腾讯TLinux:

代码语言:bash
复制
wget "https://clb-toa-1255852779.file.myqcloud.com/tgw_toa_tlinux.tar.gz"

3)加载TOA模块

以Debian 12为例,步骤1)现成的toa.ko并没有适配的版本,因此需要编译一下:

代码语言:bash
复制
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文件:

此时我们加载此模块:

代码语言:bash
复制
insmod toa.ko

通过dmesg -T | grep -i TOA 查看内核缓冲区日志,如果出现"toa load success",则说明加载成功。

4)监控TOA模块状态(可选)

执行以下命令可以实时查看已经建立连接的IPv6客户端地址:

代码语言:bash
复制
cat /proc/net/toa_table

查看TOA的相关计数状态:

代码语言:bash
复制
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,并且保持连接不中断:

代码语言:bash
复制
telnet -6 2402:4e00:40:40::2:3b9 80

同时查看toa表是否有获取到客户端真实IP:

代码语言:bash
复制
cat /proc/net/toa_table

可以看到,/proc/net/toa_table成功记录到客户端的真实IP:PORT,但客户端关闭连接后,则清空记录。

同理,可以查看toa的计数状态:

代码语言:bash
复制
cat /proc/net/toa_stats

2)抓包验证

若存在 unknown-253字段,则说明在 NAT64 场景下的真实 IPv6 的源 IP 已经插入。

或者使用wireshark打开分析,筛选tcp options类型为253的包:

代码语言:bash
复制
tcp.option_kind == 253

红圈中的字段即为客户端真实V6地址。四层场景,第一次握手(SYN)、第三次握手(ACK),都会携带客户端真实IP插入到tcp option,其中Experiment Identifier携带的16进制内容0xb010为客户端源端口,转换为十进制为45072。

模拟七层场景的情况,NAT64 LB监听器依然还是四层,客户端curl NAT64 LB的80端口:

代码语言:bash
复制
curl -6 http://[2402:4e00:40:40::2:3b9]

七层场景,在客户端向服务端发起HTTP GET或别的请求方法时(如POST、PUT、DELETE等),也会携带真实IP插入到tcp option。

同时也可以通过tshark配合awk、sed,过滤到包中的v6客户端真实IP,并且添加冒号还原完整:

代码语言:bash
复制
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

md5sum:4b679dac8c48582f6177fe054c6c952e

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日志格式把这两个字段取值展示出来即可:

代码语言:nginx
复制
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:

代码语言:bash
复制
curl -H 'X-Forwarded-For:1.1.1.1' LBIP

可以看到X-Forward-For此时记录了两个地址,第一个是在LB之前客户端主动携带的,第二个是七层LB主动插入的。

我们再大胆点尝试下,假设在LB之前,有经过三个代理服务器,并且每一层都往XFF头加入自己的IP,客户端模拟同时携带三个XFF IP:

代码语言:bash
复制
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可以查看当前安装的模块:

代码语言:bash
复制
nginx -V

如果未安装,则无法正常读取两个变量值,需要进入到nginx源码目录,重新编译进去。

http_realip_module模块的用法可以参考nginx官方文档,内嵌了两个变量:

  • realip_remote_addr:客户端真实IP</li><li>realip_remote_port:客户端真实端口

增加如下nginx配置,把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&#34;-realip_remote_addr:$realip_remote_port';

access_log /var/log/nginx/access.log main;

其中remote_addr:remote_portrealip_remote_addr:realip_remote_port等同效果,测试都能通过日志拿到客户端真实IP和端口:

real_ip_header设置成从X-Forwarded-For获取,此时remote_addr则会读取XFF从左到右第一个IP,当real_ip_header 设置成从X-Real-IP获取,则直接通过TCP连接的方式读取和LB直接建联的相对客户端,无法通过指定七层头部进行伪造。</p><p>比如基于上面这段日志格式,remote_addr读取到的的是XFF的第一个IP,并且客户端端口是读取不到的,XFF没有记录端口的能力,而realip_remote_addr和realip_remote_port未受影响:

此时我们将real_ip_header设置成从X-Real-IP读取:

代码语言:nginx
复制
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&#34;-realip_remote_addr:$realip_remote_port';
access_log /var/log/nginx/access.log main;

可以正常读取到和LB直接建联的客户端IP和端口。

我们把remote_addr 替换成 http_x_real_ip:

代码语言:nginx
复制
log_format main 'http_x_real_ip:remote_port - remote_user [time_local] "$request" '
'status body_bytes_sent "$http_referer" '
'"http_user_agent&#34;-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加速域名,此时客户端模拟访问:

代码语言:bash
复制
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地址:

代码语言:nginx
复制
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] &#34;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地址,从而确保在复杂的网络环境中也能精准地识别和记录客户端身份。这对于网络安全、用户行为分析以及合规性要求等方面具有重大意义。