目录

  • 背景
  • 获取client ip的几种方法
  • toa/uoa 获取client ip
    • 原理
    • tcp option 字段
    • lvs中的toa格式与插入
      • 格式
      • 插入
    • 后端获取client ip
    • ss/netstat 和 toa的关系
    • 其他实现方式
  • 参考

背景

FullNat 模式的特点,比如跨机房、可运维性强等优势。不过会存在一个问题,在后端服务器上,应用程序能够获取到的请求源 IP 是 lvs 的 LocalIP,并不是真实客户端的 ClientIP。而现在大多数业务都需要对用户信息进行分析画像,也有一些敏感业务需要对用户进行溯源,所以获取用户的真实客户端 IP 地址是非常重要和必要的。

获取client ip的几种方法

有一定流量的业务基本上都要经过负载均衡设备,所以后端服务器要获取客户端真实IP地址,也是常见的问题和需求,这里先罗列几种常见的获取源 IP 的方式:

  • 通过 L3 转发时,源 IP 不变。
    比如 lvs 的 dr、nat、tunnel 模式,后端服务器可以直接获取到真实客户端 IP 地址。
  • 通过 proxy protocol 协议实现源 IP 传递。
    原理是在三次握手后,发送请求数据前,在四层头之后插入一个 proxy protocol 数据包,数据包中可以携带 src ip、src port 等信息,该协议是由 haproxy 提出的,目前常见的 web 服务器都已经支持。
  • 通过 toa 模块获取源 IP。
    在三次握手最后一个 ack 数据包的 tcp option 中插入源 IP 和源 Port 等信息;后端服务器在调用 getpeername 获取源 IP 时读取 tcp option 数据即可获取真实客户端的 IP 地址。
  • 通过七层的 XFF 字段。
    HTTP 协议实现的字段,没什么可说的。业务层自己实现。

几种方案各有优缺点,以及自己适用的应用场景。本文重点要说的是 TOA ,TOA 工作在 L4 层,适用性更通用一些。

toa/uoa 获取client ip

原理

TOA 名字全称是 tcp option address,是 FullNat 模式下能够让后端服务器获取 ClientIP 的一种实现方式,它的基本原理比较简单。

  • 客户端用户请求数据包到达 LVS 时,LVS 在数据包的 tcp option 中插入 src ip 和 src port 信息。
  • 数据包到达后端服务器(装有 toa 模块)后,应用程序正常调用 getpeername 系统函数来获取连接的源端 IP 地址。
  • 由于在 toa 代码中 hook(修改)了 inet_getname 函数(getpeername 系统调用对应的内核处理函数),该函数会从 tcp option 中获取 lvs 填充的 src 信息。
  • 这样后端服务器应用程序就获取到了真实客户端的 ClientIP,而且对应用程序来说是透明的。

tcp option 字段


client ip 就是放在 tcp option 字段中。option 字段最长 40 字节,每个选项由三部分组成:op-kind、op-length、op-data,我们最常见的 MSS 字段就是在 option 里。
目前 option 使用的 op-kind 并不多,我们只需要构建一个不冲突的 op-kind 就可以把 clientIP 填充进去。IPv4 地址占用 4 个字节,IPv6 占用 16 字节,填充到 option 中是没有问题的。

lvs中的toa格式与插入

格式

首先要确定 toa 的具体数据格式:
(一)IPv4 toa 格式

+----------+----------+--------------------+
|  opcode  |  opsize  |         port       |
+----------+----------+--------------------+
|                clientIP                  |
+------------------------------------------+

各字段含义:

opcode: opcode = 254
opsize: toa 大小 8 字节
port: 客户端端口
clientIP: 客户端 IP(4 字节)
注:opsize 大小包含了自身opsize(2B) + port(2B) + ip(4B)

(二)IPv6 toa 格式

+----------+----------+--------------------+
|  opcode  |  opsize  |         port       |
+----------+----------+--------------------+
|                                          |
|               clientIPv6                 |
|               clientIPv6                 |
|                                          |
+------------------------------------------+

各字段含义:

opcode: opcode = 254
opsize: toa 大小 20 字节
port: 客户端端口
clientIP: 客户端 IP(16 字节)

插入

lvs 需要对每个 tcp 数据包都要插入 toa 信息么?如果这样会影响到 lvs 整体性能的,而且后端服务器也没必要对每个 tcp 数据包进行解析,当然也很影响服务器性能。
其实只需要在第 3 次握手 ack 数据包中插入 toa 选项即可,后端服务器从 ack 数据包中解析并获取即可。

注:
其实syn包中插入 tcp option也是没有什么意义的。因为后端时收到三次握手的ack,才会从ack中获取tcp option的。

后端获取client ip

TCP 协议栈中处理三次握手的 ack 数据包的函数是 tcp_v4_syn_recv_sock,完成连接的建立,并创建 newsock。

  1. toa 模块会将此函数通过 tcp_v4_syn_recv_sock_toa 函数进行劫持,也就是说第三次握手的 ack 到达协议栈后调用的是 tcp_v4_syn_recv_sock_toa 函数,而不是tcp_v4_syn_recv_sock 。
  2. 在 tcp_xx_toa 函数中首先会调用内核原有的处理函数 tcp_v4_syn_recv_sock 函数,这样兼容了那些不是通过 toa 的连接。然后解析 ack 数据包中 tcp option 内容,获取到 lvs 插入到 toa 的 src ip 和 src port 信息,将此信息挂在 newsock 结构变量中。
static struct sock *
tcp_v4_syn_recv_sock_toa(struct sock *sk, struct sk_buff *skb,struct request_sock *req, struct dst_entry *dst)
{newsock = tcp_v4_syn_recv_sock(sk, skb, req, dst);newsock->sk_user_data = get_toa_data_compatible(AF_INET, skb, &nat64);return newsock;
}

当应用程序,如 nginx 或 MySQL 调用 getpeername 系统函数时,正常情况会调用 inet_getname 函数来获取连接远端的 ClientIP。

  1. toa 模块对 inet_getname 函数也用 inet_getname_toa 函数进行了劫持,也就是说应用程序调用 getpeername 时,内核对应的处理函数是 inet_getname_toa。
static int
inet_getname_toa(struct socket *sock, struct sockaddr *uaddr,int *uaddr_len, int peer)
{int retval = 0;struct sock *sk = sock->sk;struct sockaddr_in *sin = (struct sockaddr_in *) uaddr;struct toa_ip4_data tdata;// 调用内核原来的函数,兼容那些不是toa的情况retval = inet_getname(sock, uaddr, uaddr_len, peer);// 如果是toa,则直接从sk->sk_user_data获取数据if (retval == 0 && NULL != sk->sk_user_data && peer) {memcpy(&tdata, &sk->sk_user_data, sizeof(tdata));sin->sin_port = tdata.port;sin->sin_addr.s_addr = tdata.ip;}return retval;
}

总结:

  • hook 三次握手中收到ack后建立连接的函数:tcp_v4_syn_recv_sock:
    将三次握手的ack中携带的 client-ip, client-port 保存到 新建sock 的 sk_user_data 中。
  • hook 后端server的 getpeer 的函数:
    sock 的 sk_user_data 非空时,则getpeer 返回调度时 sock 的 sk_user_data 的数据。

ss/netstat 和 toa的关系

  • ss/netstat -apn 看到的连接的 ip 是否为真实的 client-ip?
    经过测试发现:
    netstat -apn 中看到的 ip 不是client-ip ,而是lvs的 local-ip。
    因为netstat 并不是一个真实的server,监听某个端口,然后新建连接,进行getpeer 获取对端的ip,其是通过读取/proc/net下的内核文件来获取所有的连接信息。
(1)安装
> yum install -y nginx
> #which nginx
> #rpm -qf /sbin/nginx
(2) 安装 toa
(3) nginx 日志查看client ip
如下所示:此时看到的 ip 的确是 client-ip,但是netstat 看到的依然是 local-ip;

注:查看netstat /ss 的原理:
strace -e open netstat -apn ; 发现 netstat 其实是 打开/proc/net/tcp, /proc/net/tcp6,/proc/net/udp, /proc/net/udp6, /proc/net/raw, /proc/net/unix 等文件

其他实现方式

以上 toa 的方式的 两个 hook 函数是 内核中原始存在这样的函数,只不过将原有函数给劫持了, 所以不需要重新执行 nf_register_hook 进行注册 hook函数。
如果需要在netfilter 中添加新的函数,则需要考虑将 新的函数 添加到netfilter 的哪个链中(比如:PREROUTING链),以及对应的优先级的设置(相比较于该链中其他表/函数的优先级,比如:raw/mangle/nat/filter)。
【具体实现可参考 uoa 的实现以及百度 bcettm 的实现】

其他实现方法
比如:直接注册两个 hook 函数,执行 source nat操作,而不是劫持原有的函数。

具体行为是:将sip/sport 替换为 tcp option中的client-ip, client-port, 并且建立 session,后续的包中没有携带 clinet-ip、port,直接查找session,也可以进行snat 替换。

注:此时需要保证 lvs发给后端的 syn 包中携带有 client-ip, client-port 信息,而不仅仅是 三次握手的第三个包中携带有 client-ip, client-port。
这也是为什么 lvs会在 syn 以及三次握手的 ack中都携带有 client-ip、port信息了。因为后端可以通过nat方式,也可通过hook原有的函数的方式来获取 client-ip、port。

参考

https://blog.csdn.net/liwei0526vip/article/details/106108844

关于 FullNat 模式的 Toa 实现原理【转】相关推荐

  1. 6、HIVE JDBC开发、UDF、体系结构、Thrift服务器、Driver、元数据库Metastore、数据库连接模式、单/多用户模式、远程服务模式、Hive技术原理解析、优化等(整理的笔记)

    目录: 5 HIVE开发 5.1 Hive JDBC开发 5.2 Hive UDF 6 Hive的体系结构 6.2 Thrift服务器 6.3 Driver 6.4 元数据库Metastore 6.5 ...

  2. 静态代理模式(多线程底部原理)

    静态代理模式总结(线程底部原理) 真实对象和代理对象都要实现同一个接口 代理对象要代理真实角色 好处: - 代理对象可以做很多真实对象做不了的事情 - 真实对象专注做自己的事情 创建静态代理模式:一个 ...

  3. Vue-Router前端路由的两种模式、区别、原理?

    vue路由有⼏种模式?有什么区别?原理是什么? 一.vue路由有几种模式? 二.两者区别 三.原理 一.vue路由有几种模式? vue的路由模式⼀共有两种,分别是哈希和history 二.两者区别 哈 ...

  4. 8086的两种工作模式_Buck变换器工作原理

    一.Buck变换器另外三种叫法 1.降压变换器:输出电压小于输入电压. 2.串联开关稳压电源:单刀双掷开关(晶体管)串联于输入与输出之间. 3.三端开关型降压稳压电源: 1)输入与输出的一根线是公用的 ...

  5. MVC 模式/Servlet/JSP 编译原理剖析:Servlet 组件到底属于 MVC 模式的哪一层?

    文章目录 前言 一.回忆什么是 MVC 模式? 1.1.Model.View.Controller 组件介绍 1.2.明确 View 与 Controller 组件区别 二.什么是 Servlet? ...

  6. mysql xtrabackup 保护模式_MySQL Xtrabackup备份原理和实现细节

    备份原理: XtraBackup基于InnoDB的crash-recovery功能.它会复制innodb的data file,由于不锁表,复制出来的数据是不一致的,在恢复的时候使用crash-reco ...

  7. 华为ensp交换机vlan划分三种接入模式详解-----网络通信原理

    华为ensp交换机vlan划分三种接入模式详解 冲突域.交换机.广播域 VLAN概述 VLAN帧格式 access端口 Trunk端口 Hybrid端口 冲突域.交换机.广播域 定义:在一个网络范围内 ...

  8. [WCF权限控制]利用WCF自定义授权模式提供当前Principal[原理篇]

    在<通过扩展自行实现服务授权>一文中,我通过自定义CallContextInitializer的方式在操作方法之前之前根据认证用户设置了当前线程的安全主体,从而实现授权的目的.实际上,WC ...

  9. android 充电模式deamon_Android Lint工作原理剖析

    Android Lint是Android SDK提供的一项静态代码分析工具,对于提高代码质量具有重要作用.到目前为止,Android SDK自带的Lint检查项目达到了253项,我们在开发过程中经常见 ...

最新文章

  1. Python 之父:Python 4.0 可能不会来了
  2. IDC发布制造业预测,AI风险决策因何上榜?
  3. East Central North America Region 2015
  4. 给写新疆开放互联网一周纪念
  5. 网站架构相关PPT、文章整理(更新于2009-7-15)
  6. python中的序列类型和序列号_python~序列类型及操作
  7. 鼠标追踪没用_【擺评】赛睿里最好用的小手鼠标---Rival 3
  8. 19-数据持久化-Bind Mounting
  9. ROS笔记(20) Kinect仿真
  10. textarea选中行删除_Easy Data Transform如何在Excel中删除重复的行?
  11. word2vec原理知识铺垫
  12. MPLS virtual private network基础内容
  13. Spring【DAO模块】就是这么简单
  14. matlab输入一个正的实数x,VB程序题:用InputBox 输入一个正实数,用Pring方法在一行上显示出它的平方和平方根、立方和立方根,每个数保留三位小数,其间有间隔。...
  15. 计算机学科 集体备课记录,信息技术学科组集体备课活动记录.pdf
  16. 食堂饭卡管理系统C语言——课程设计实习
  17. Matconvnet学习笔记
  18. cistern java,basin是什么意思_basin怎么读_basin翻译_用法_发音_词组_同反义词_盆-新东方在线英语词典...
  19. jpi多表联查_多表连接查询详解
  20. Jasper 动态数据源

热门文章

  1. 数学建模重要算法简介及算法实现
  2. 区分事件的独立性与互不相容性
  3. 简述linux下用户与组相关的配置文件,Linux用户和群组管理的主要配置文件
  4. windows串口调试linux工具,推荐一款好用的串口调试软件PuTTY
  5. 弱引用(WeakReference)初识
  6. android os 1.5 下载地址,技德Remix OS 1.5发布 适配Android 5.0
  7. Python:fractions(分数)模块的使用
  8. 【专精特新周报】邦德股份北交所上市,首日收涨27.86%;12家创新层公司被降层;2022年国家新增支持五百家左右专精特新小巨人...
  9. 常规通知(Notification)模板
  10. vue3中使用tsx