在互联网上一个五元组标识一个应用程序到远端的另一个应用程序的连接。要保证端到端的可达性,显然在全局范围内,五元组必须是唯一的。

保证五元组的全局唯一性看起来是个重体力劳动,以IPv4网络为例,仅仅考虑TCP和UDP,一个五元组空间包括两个32位IPv4地址,两个16位端口以及一个协议,总共232×2+16×2+12^{32\times 2+16\times 2+1}232×2+16×2+1种组合。穷尽这么大一个空间来寻找一个没有被使用的元组绝对是重体力劳动。

然而在分布式的互联网环境,五元组的全局唯一性其实非常容易保证,互联网算力分布在世界的各个角落,这个五元组的唯一性是由全世界所有的计算机一起保证的:

  • 每个计算机的IP地址是不同的,这就卸载了穷举2322^{32}232种可能性的算力。
  • 访问目标如果不同,又将卸载穷举2322^{32}232种可能性的算力。
  • 最终,我们可能只需要在万数量级的端口范畴计算就可以了。

但是互联网上的主机并不是完全分布对等的,这完全是因为NAT的存在!

NAT的存在,将已经是分布在每台计算机的保证五元组唯一的计算重新集中了起来!

由于互联网计算机的分布式特性,五元组的唯一性是在连接初始时就可以保证的,当一个五元组经过任何一台中间设备时,该设备完全可以保证这个五元组的唯一性。

但如果一台设备对数据包的五元组做了NAT,由于该NAT设备并没有全局的五元组信息库,因此它在做NAT时不得不通过精心的计算以确保NAT过后五元组唯一性仍然可以保证。当然,它也仅仅保证经由本机的连接的五元组唯一性。

对于Linux内核Netfilter实现的NAT而言,五元组唯一性是通过 get_unique_tuple 函数实现的,我不会在这里分析该函数的实现,它大致说的是:

  • 对于SNAT,尽量使用保存的已知tuple,因为大概率它们访问的目标是不同的,这会大大卸载算力。( 【第一步】 )
  • 万不得已再穷举tuple命名空间,但依然有很多trick优化,盲搜多次,然后放弃。
  • get_unique_tuple不会穷尽整个命名空间,盲搜失败后便停止,NAT的失败并非意味着tuple命名空间的枯竭。

但以上的过程依然是个重体力劳动,特别是在流量很大的情况下。

有人可能会有疑问,我只配置了一条NAT规则,仅仅有针对性的转换特定流的一个源IP地址,这个怎么可能对全局产生影响呢?

很多人都会有这种疑问,其实这个很容易解释:

  • NAT只有开启和关闭之说,与规则如何无关。

只要开启了NAT,所有的数据流必须要同等对待。在真正匹配到NAT规则之前,系统对规则和匹配情况并不知情,事实上,五元组的唯一性完全是NAT自身来保证的。因此只要是开启了NAT,必须对每一条流施加get_unique_tuple这个重体力劳动!

即便一条流没有匹配到任何NAT规则,它依然要执行nf_nat_alloc_null_binding来将所有流纳入到一个全局的tuple命名空间,这为NAT真正执行get_unique_tuple时提供了优化:

  • 在执行SNAT的HOOK点,将tuple加入到一个nf_nat_bysource链表,为上述 【第一步】 提供依据。

曾经有一个问题,当NAT的HOOK函数注册的时候,之前的conntrack并没有被纳入NAT全局的tuple命名空间,也并没有加入到nf_nat_bysource链表,会不会有问题呢?我跟别人解答这个问题,答案是:

  • 不会有问题,只是损失些性能罢了。如果一个流进入nf_nat_fn时和已记录5-tuple有冲突,那么该流不会同时是NEW且!nf_nat_initialized,早就命中了不是吗?

然而真的是这样吗?非也!

我想表达的是,nf_nat_alloc_null_binding这个函数是必须的,同时它可能会默默改变你的连接的源端口,信吗?

我不想过多的解释细节,如果你懂nf_conntrack和NAT的细节,应该知道我下面的脚本再说什么。

给出测试拓扑环境:

  • 客户端 192.168.56.101:12345 连接服务器 192.168.56.102:80

首先,我们设置以下的iptables规则,仅仅TRACK到达80端口的连接,同时添加一条无关的NAT规则,以注册conntrack HOOK(由于conntrack HOOK的延迟注册,需要实际添加一条NAT规则):

*raw
-A PREROUTING -p udp -j NOTRACK
-A PREROUTING -p tcp -m tcp ! --sport 80 -j NOTRACK
-A OUTPUT -p tcp -m tcp ! --dport 80 -j NOTRACK
-A OUTPUT -p udp -j NOTRACK
*nat
-A OUTPUT -d 2.3.4.5/32 -p udp -j DNAT --to-destination 5.4.3.2

其次,我给出一个python程序,一个TCP客户端,bind特定的地址端口,连接特定的地址端口:

#!/usr/bin/python3import socketsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind(('192.168.56.101', 12345))server_address = ('192.168.56.102', 80)
sock.connect(server_address)
sock.close();

最后,我给出一个stap脚本,它的意思是:

  • 在python新建连接被conntrack记录前什么也不做。
  • 在python新建连接被NEW后模拟创建一个“与之reply方向冲突”的conntrack项,在实际中这完全是可能的。
  • 观察python新建连接发出的包,其端口号竟然变了!

脚本如下:

#!/usr/bin/stap -g%{#include <net/netfilter/nf_conntrack.h>
%}probe module("nf_nat").function("__nf_nat_alloc_null_binding")
{if ($manip == 1) {// 在OUTPUT NAT执行时,模拟一个完全正常的可能发生的conntrack item插入system("conntrack -I --protonum 6 --timeout 100 --reply-src 192.168.56.102 --reply-dst 192.168.56.101 --state SYN_SENT --reply-port-dst 12345 --reply-port-src 80 --src 1.1.1.1 --dst 192.168.56.102");// 防止stap同步问题,延迟一会儿再整mdelay(100);}
}// 打印一些看似无关紧要的信息,但确实给出了tuple冲突的结果
probe module("nf_conntrack").function("nf_conntrack_tuple_taken").return
{printf("nf_conntrack_tuple_taken   ret:%d\n", $return);
}%{struct nf_conn *thief = NULL;
%}function alertit(stp_ct:long)
%{struct nf_conn *ct = (struct nf_conn *)STAP_ARG_stp_ct;struct nf_conntrack_tuple *tuple;unsigned short port;tuple = &ct->tuplehash[IP_CT_DIR_REPLY].tuple;port = ntohs((unsigned short)tuple->dst.u.all);if (port == 80 && thief == NULL) {STAP_PRINTF("The thief coming!\n");thief = ct;}
%}probe module("nf_conntrack").function("nf_conntrack_hash_check_insert")
{alertit($ct);
}function run_away(stp_tuple:long, stp_ct:long)
%{struct nf_conntrack_tuple *tuple = (struct nf_conntrack_tuple *)STAP_ARG_stp_tuple;struct nf_conn *ct = (struct nf_conn *)STAP_ARG_stp_ct;struct nf_conntrack_tuple *t;if (thief) {t = &thief->tuplehash[IP_CT_DIR_REPLY].tuple;//t->dst.u.all = 100; // 这两条注释本来是想破坏掉这个conntrack项的//t->src.u.all = 100;thief = NULL;STAP_PRINTF("The thief ran away...\n");}
%}probe module("nf_conntrack").function("nf_conntrack_alter_reply")
{run_away($newreply, $ct);
}

执行stap脚本,然后执行client.py,但是我们看一下tcpdump抓包:

21:18:50.098848 IP 192.168.56.101.35010 > 192.168.56.102.80: Flags [S], seq 1990086505, win 64240, options [mss 1460,sackOK,TS val 814885365 ecr 0,nop,wscale 7], length 0
21:18:50.098872 IP 192.168.56.102.80 > 192.168.56.101.35010: Flags [S.], seq 1199002250, ack 1990086506, win 65160, options [mss 1460,sackOK,TS val 2915891341 ecr 814885365,nop,wscale 7], length 0
21:18:50.099064 IP 192.168.56.101.35010 > 192.168.56.102.80: Flags [.], ack 1, win 502, options [nop,nop,TS val 814885466 ecr 2915891341], length 0

可以看到,端口已经不是12345了,它变成了35010,这一切都是在 get_unique_tuple 中,此处不细聊。

一个真实的场景就是,在192.168.56.101.12345 > 192.168.56.102.80发起, conntrack初始NEW之后,confirm之前 有一个conntrack项被创建,或者是直接恶意插入的,或者某个流是真的命中了一条NAT规则:

src 1.1.1.1:12345 dst 192.168.56.102:80 --> src 192.168.56.101:12345 dst 192.168.56.102:80

那么原始测试流的源端口将会被偷偷地,默默地改变!

关于nf_nat_alloc_null_binding,我已经单独写了一篇文章,这其实是我一直都想说的话题:
https://blog.csdn.net/dog250/article/details/112691374


下图展示一个nf_conntrack/NAT的宏观场面,我把重体力劳动都用浅红色底色表示:

现在我们看到一些相对耗时的重体力劳动,如果要优化性能,避开这些位置,或者优化这些位置均可。

nf_conntrack已经在这些方面有所工作了,比方说如果没有一条NAT规则被添加,那么就干脆不注册conntrack HOOK(这并不完美,因为其它的模块只要注册了conntrack HOOK,NAT依然是数据包的必经之路)。

不管怎么说,认清conntrack & NAT的性能瓶颈到底在哪儿是必要的,如果你的机器连接数达到了几十万上百万,你看看你的conntrack hash表的大小是不是很小,遍历一次冲突链表的开销会不会很大,这些情况有时候并不是nf_conntrack本身的问题,可能仅仅是你的配置问题。必要的时候,想办法把不相关的流量NOTRACK掉也是一种优化方案,比方说,对于那些频繁的又没有NAT,status filter等需求的短连接,NOTRACK将会避开get_unique_tuple以及spin lock从而大大提高单机性能。


浙江温州皮鞋湿,下雨进水不会胖。

谁动了你的五元组-nf_conntrack与NAT的性能相关推荐

  1. 谁动了你的五元组-Linux Netfilter NAT之nf_nat_alloc_null_binding

    Linux的Netfilter NAT实现中,为什么会有一个nf_nat_alloc_null_binding(在低版本内核比如2.6,它叫alloc_null_binding)调用? 该函数是在一条 ...

  2. linux centos 7 系统性能查询、DHCP租期信息查询、网络五元组

    linux centos 7 系统性能查询 top CPU进程情况 killall 最后一列进程名 中止进程信息. killall -9 进程名 强制中断. sar -n DEV 1 每秒显示所有网卡 ...

  3. java解析五元组_pcap文件解析,并且按照五元组分类

    [实例简介] pcap文件解析,并按照五元组分包,全部用java语言实现. [实例截图] [核心代码] PcapTestZZ ├── PcapTestZ │   ├── 111.206.37.1930 ...

  4. TCP/IP的四元组 五元组 七元组

    四元组是: 源IP地址.目的IP地址.源端口.目的端口 五元组是:       源IP地址.目的IP地址.协议号.源端口.目的端口 七元组是: 源IP地址.目的IP地址.协议号.源端口.目的端口,服务 ...

  5. 计算机中flow和stream还有torrent有什么区别?(五元组、microflow、traffic flow)

    看RXW文档ApplicationNote/Rockchip_Instructions_Linux_MediaServer_CN.pdf,看见里面有关于stream和flow的描述,这俩不是都指&qu ...

  6. 基于交换芯片的五元组的PCL规则过滤功能

    2019独角兽企业重金招聘Python工程师标准>>> 基于交换芯片的五元组的PCL规则过滤功能作者: 韩大卫@吉林师范大学2012.12.10Not Approved by Doc ...

  7. tshark解析本地pcap数据包提取五元组{src_ip,src_port,proto,dst_ip,dst_port}与时间戳,包长

    tshark官方文档:https://www.wireshark.org/docs/man-pages/tshark.html wireshark官方特征参考:https://www.wireshar ...

  8. 基于交换芯片的五元组过滤功能

    基于交换芯片的五元组的PCL规则过滤功能作者: 韩大卫@吉林师范大学2012.12.10Not Approved by Document Control Review Copy Only基于Marve ...

  9. Linux:网络五元组tcp、udp特性

    标题 网络五元组信息 UDP简单的特性 TCP简单特性 网络字节序 IP地址+端口号就叫套接字,用于定位主机中的一个进程 我们在系统编程部分学过管道,管道是用于同一主机下不同进程间的通信 套接字则用于 ...

最新文章

  1. mt4指标最精准组合指标_股市最赚钱的黄金指标组合:KDJ+MACD指标的配合使用,助于买在低点卖在高点!...
  2. MyBatis 注释
  3. python是c语言吗-初学者python和c语言先学哪个好呢?
  4. 计算机视觉与深度学习 | 目标提取(代码实现)
  5. 语言的学习基础,100个经典的算法
  6. 引用 病毒是怎么命名的?教你认识病毒命名规则
  7. 虚拟化部署之Windows 7中远程管理Hyper-V
  8. 面试官:自己搭建过vue开发环境吗?
  9. Mapoutputcollector的几个方法
  10. [小工具] 文本形式转储二进制
  11. visio2016专业版2018最新密钥和下载方法 整理
  12. 终端模拟器免ROOT安装Linux,【全机型通用】不用电脑,用终端模拟器刷入第三方Recovery...
  13. jsp页面中文乱码解决方法
  14. 计算机很多文件无法删除,电脑有文件删不掉怎么办?电脑有文件删不掉解决方法介绍...
  15. PCB钻孔输出的一个简单动作,却带来工厂的命运转折
  16. Django介绍,mvc设计模式及mvt设计模式介绍及对比
  17. P3387 【模板】缩点 洛谷 java题解 连通图+拓扑排序
  18. 【开发工具】Office Tool Plus 安装 Office
  19. net.sf.cglib.beans.BeanCopier用途
  20. ARP局域网断网攻击原理分析及演示

热门文章

  1. FS7071是4.35v高精度锂电池保护IC电路
  2. 前端写全国省、市地址下拉框
  3. CIO访谈实录丨渤海人寿携手SmartX超融合大幅提升开发测试效率
  4. SAP IRPA ---LoginSAP的更简单方式
  5. 微软WSL2来了,以后可以用windows做生信了?
  6. 元宇宙,你是愿意成就这个世界,还是毁灭这个世界?
  7. 怎么使用xShell登录AIX 7小机
  8. 科比:我多么希望着一切都未发生
  9. 关注融入到我们生活当中方方面面的人工智能
  10. Unity里面CG和HLSL在写法上的一些区别