前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >「最佳实践」腾讯云CLB负载均衡通过TOA和XFF获取客户端真实IP:涵盖七层LB和NAT64 LB

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

原创
作者头像
RokasYang
发布2024-07-29 01:28:30
40815
发布2024-07-29 01:28:30
举报
文章被收录于专栏:RokasYang

一、前言

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

不同发行版,对应的压缩包不一样:

如果有适配的系统版本,可直接下载后解压文件,之后参考步骤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/.$//'

附带完整抓包文件:

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
  • $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"-$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读取:

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

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、前言
  • 二、NAT64 CLB场景通过TOA获取客户端真实IP
    • 1.启用NAT64的TOA选项
      • 2.RS加载TOA模块
        • 1)下载TOA压缩包
        • 2)从源码编译安装
        • 3)加载TOA模块
        • 4)监控TOA模块状态(可选)
      • 3.测试验证
        • 1)查看TOA表记录
        • 2)抓包验证
        • 3)适配后端服务
    • 三、七层CLB通过XFF获取客户端真实IP
      • 1.抓包验证
        • 2.Nginx通过XFF和X-Real-IP记录日志
          • 1)X-Forwarded-For
          • 2)X-Real-IP | remote_addr | realip_remote_addr
        • 3.CDN场景
        • 四、总结
        相关产品与服务
        负载均衡
        负载均衡(Cloud Load Balancer,CLB)提供安全快捷的流量分发服务,访问流量经由 CLB 可以自动分配到云中的多台后端服务器上,扩展系统的服务能力并消除单点故障。负载均衡支持亿级连接和千万级并发,可轻松应对大流量访问,满足业务需求。
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档