18| 剖析conntrack的底层实现

18| 剖析conntrack的底层实现
前面我们介绍了 netfilter 相关的知识这里我们来介绍一下 conntrack 工具。Conntrack 是 netfilter 的一部分。它的主要作用是跟踪网络连接的状态做 NAT 转换以及负载均衡等。我们可以使用 conntrack 命令查看当前系统的连接跟踪表:# 列出所有连接 conntrack -L # 只列出 TCP 连接 conntrack -L -p tcp # 显示更详细的信息 conntrack -L -e接下来我们看看 conntrack 在 NAT 中发挥的作用。我们启动一个 docker 容器这里以 busybox 为例:docker run -it busybox /bin/sh不出意外此时 docker 已经可以访问外网这里 docker 使用的是 bridge 方式进行容器的通信。容器 ip 是 172.17.0.4。宿主机的网卡 ip 是192.168.1.4此时 172.17.0.4 想访问 157.148.69.80.80 这个 ip 时需要做源地址 NATSNAT。这里的 SNAT 是通过 iptables 来实现的。我们此时用 iptables 来看一下其中-A POSTROUTING表示该规则将被添加到 POSTROUTING 链中前面 netfilter 中介绍过POSTROUTING 阶段是已经经过路由准备发往另外的网络设备。-s 172.17.0.0/16表示该规则只应用于源IP地址在 172.17.0.0/16 网段内的数据包这是我安装的 Docker 默认使用的私有网络地址范围。! -o docker0这里的!表示取反-o docker0表示目标网络接口为 docker0 网桥因此这里的意思就是除了目的地是发往docker0网桥的数据包。-j MASQUERADE对匹配的数据包进行 MASQUERADE伪装也就是做源地址转换SNAT把数据包的源 IP 改为发送此数据包的主机 IP 地址。接下来我们来做实验看看 conntrack 的实际效果。在 busybox 容器内执行$ nc 157.148.69.80 80在宿主机执行conntrack -L就可以看到对应的连接信息了。sudo conntrack -L | grep 157.148.69.80 tcp 6 431990 ESTABLISHED src172.17.0.4 dst157.148.69.80 sport41231 dport80 src157.148.69.80 dst192.168.1.4 sport80 dport41231 [ASSURED] mark0 use1这里的tcp 6 431990 ESTABLISHED表示这是一条 ID 为 431990 的 TCP 连接状态为 ESTABLISHED。接下来的src172.17.0.4 dst157.148.69.80 sport41231 dport80表示 DNAT 前的TCP 四元组172.17.0.4.41231-157.148.69.80.80接下来的src157.148.69.80 dst192.168.1.4 sport80 dport41231表示 DNAT 后的反向流量 TCP 四元组157.148.69.80.80-192.168.1.4.41231。值得注意的是DNAT 前的临时端口号是 41231对应到宿主机的端口号也是 41231这是因为在没有冲突的情况下可以这样处理。如果我们两个容器的临时端口号相同时就不能这样处理了。我们可以来做一下实验看一下。busybox 的 nc 在 connect 时可以使用-p指定临时端口$ nc -p 29999 157.148.69.80 80在启动一个 busybox 容器运行同样的命令然后在宿主机上查看 conntrack 信息$ sudo conntrack -L | grep 157.148.69.80 tcp 6 431997 ESTABLISHED src172.17.0.5 dst157.148.69.80 sport29999 dport80 src157.148.69.80 dst192.168.1.4 sport80 dport29999 [ASSURED] mark0 use1 tcp 6 431990 ESTABLISHED src172.17.0.4 dst157.148.69.80 sport29999 dport80 src157.148.69.80 dst192.168.1.4 sport80 dport34105 [ASSURED] mark0 use1可以看到 conntrack 的 DNAT 记录如下172.17.0.5.29999-57.148.69.80.80 - 157.148.69.80.80-192.168.1.4.29999 172.17.0.4.29999-57.148.69.80.80 - 157.148.69.80.80-192.168.1.4.34105也就是在容器内都是使用 29999 这个临时端口与服务端建连但在宿主机上一个使用了 29999一个使用了 34105不然的话如果网卡收到发往 29999 的包应该发给哪个容器处理呢这就是 conntrack 在其中扮演的重要作用。conntrack 一些底层细节conntrack 连接跟踪表底层使用哈希桶链表来记录相关的信息哈希桶的每一个 bucket 都包含一个链表用来解决哈希冲突如下所示。--------------- | Bucket 0 | --------------- | Bucket 1 | --------------- ----------------------------------- | Bucket 2 |----| Entry 1 -| Entry 2 -| Entry 3 -| --------------- ----------------------------------- | Bucket 3 | --------------- | .... | --------------- | .... | --------------- | .... | --------------- ---------------------- | Bucket N |----| Entry -| Entry -|--| --------------- ----------------------当收到一个新的数据包内核先根据数据包的信息源IP、目标 IP、port 等计算出一个 hash 值以这个哈希值作为索引找到数据包所属的 bucket 链表这一步哈希计算时间相对固定且比较快O(1)。接下来遍历 bucket 的链表逐一匹配是否有匹配的 conntrack 连接。可以看到如果 bucket 链表的元素越多遍历链表所花的就会越长O(n)这里可能带来性能上的问题。因此建议 bucket 链表的元素的个数需要控制在一个比较小的值建议小于8。bucket 的数量由net.netfilter.nf_conntrack_buckets参数决定在我机器上默认值是 65536。$ sudo sysctl -a | grep nf_conntrack_buckets net.netfilter.nf_conntrack_buckets 65536一切记录都是有代价的conntrack 也不例外每条 conntrack 都需要占用一定的内存因此 linux 不会无上限的存储 conntrack 条目。net.nf_conntrack_max配置值决定了最多允许 conntrack 跟踪多少连接。在我的机器上这个默认值为 262144$ sudo sysctl -a | grep net.nf_conntrack_max net.nf_conntrack_max 262144net.nf_conntrack_max 默认值的计算方式是 nf_conntrack_buckets * 4 65536 * 4 262144。不过在更新版本的 linux 内核中已经默认将nf_conntrack_max设置为与nf_conntrack_buckets相同的值。conntrack 连接跟踪表满会发生什么接下来我们来看一下如果 conntrack 超过了net.nf_conntrack_max会发生什么。我们把当前的系统的 nf_conntrack_max 改小为 10000$ sysctl -w net.nf_conntrack_max10000然后写一个程序大量建连超过 10000使用conntrack -C可以当前跟踪的连接条目的数量。$ sudo conntrack -C 10000通过 dmesg 可以看到确实此时 conntrack 的表满了导致了丢包还可以通过conntrack -S来观察 drop 是否在持续的增加小结在本文中,我们详细介绍了 Linux 中的 conntrack 工具及其在网络连接跟踪和 NAT 转换中的作用。主要内容包括:conntrack 命令基本用法使用 conntrack -L 列出当前系统的连接跟踪表使用 -p 参数过滤特定协议的连接使用 -e 参数显示更详细的连接信息使用 -C 和 conntrack -S 监控 conntrack 表的使用情况接着我们介绍了 conntrack 使用哈希桶链表的数据结构说明了哈希桶数量和链表长度对性能的影响介绍了 net.netfilter.nf_conntrack_buckets 和 net.nf_conntrack_max 这两个关键参数。文中有两个实战实验一个是通过实验演示了 Docker 容器访问外网时conntrack 如何记录 SNAT 转换前后的连接信息希望你可以直观的感受 SNAT 的效果。一个是 conntrack 表满时的行为会导致新连接被丢弃。