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

保证五元组的全局唯一性看起来是个重体力劳动,以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. Linux ls信息给qt gui,如何使用Qt 4把ls命令的结果显示到GUI界面上去?
  2. MoeCTF 2021Re部分------Algorithm_revenge
  3. linux系统证书存储,Linux系统下如何配置Nginx的SSL安全证书
  4. Python | 如何强制除法运算为浮点数? 除数一直舍入为0?
  5. 嵌入式Linux系统编程学习之二常用命令
  6. Django框架(十九)—— drf:序列化组件(serializer)
  7. js des加密 java_java JS DES互相加密解密 通用!!!
  8. 字典树实现_leetcode之820. 单词的压缩编码 | python极简实现字典树
  9. AAAI 2020上的NLP有哪些研究风向?
  10. python操作系统存储管理作业答案_操作系统课后题答案一
  11. gpu内存大小 android,Android性能测试(内存、cpu、fps、流量、GPU、电量)——adb篇...
  12. 中国Linux内核开发者大会
  13. linux下ppt转图片的方法
  14. OpenCV/kornia/Pillow/Halcon/NI Vision/MIL/*计算机视觉资料汇总
  15. 从打车到专车,滴滴们除了烧钱还有什么?
  16. python绘制对数函数
  17. TC275——04Blinky-LED
  18. 前端学习之HTML第二天
  19. 打开netlogo model 出现failed to launch JVM
  20. systemctl笔记221029

热门文章

  1. vue3中使用百度地图BMAP
  2. AMD黑苹果万能显卡驱动
  3. 搜索结构之K模型与KV模型
  4. 【网络基础】第30章 虚拟专网
  5. 秒连的免费远程控制软件RdViewer
  6. 2021年中国防潮垫市场趋势报告、技术动态创新及2027年市场预测
  7. 直升机平移倾向(helicopter translating tendency)
  8. 《Python语言程序设计》王恺 机械工业出版社 第一章课后习题答案
  9. 随书光盘查找网站分享
  10. 16 年云存储历程,亚马逊云科技如何应对数据存储挑战