Linux连接跟踪流量统计实战:从conntrack精确追踪每一条连接的带宽消耗
前一阵子帮某客户调一台NAT网关业务方反馈说有个内网IP流量异常几乎占满了出口带宽。我用iptables规则加计数器排查结果不理想——规则是按网段写的计数器只能看到整个网段的累计量根本定位不到具体连接。当时我直接在网关上开启了连接跟踪的字节计数几分钟内就抓到了具体连接看清是某个服务在持续向外同步数据。这件事让我决定把“连接跟踪流量统计”这套方法系统地写出来。连接跟踪connection tracking是Linux内核netfilter框架里负责维护连接状态的核心机制NAT、状态防火墙都依赖它。而“连接跟踪流量统计”就是利用这条现成的连接信息按连接、按IP、按端口实时统计包数和字节数。它特别适合NAT网关、防火墙、云服务器这类场景能补上ifstat、iptables计数器、抓包工具在“按连接维度统计流量”上的短板。这篇文章会覆盖原理、命令、脚本实现和生产环境里的坑适合Linux运维、网络工程师、安全同学参考。1. 为什么先想到连接跟踪1.1 传统工具的短板在正规流量统计工具还没部署到位的时候大家通常会用下面这几种方式凑合但它们各自的短板很明显ifstat或/proc/net/dev只能看整个网卡的进出流量总和无法回答“哪个IP流量大”。iptables计数器按规则匹配累加规则写粗了统计就粗写细了规则数量爆炸而且规则顺序会影响统计结果。tcpdump抓包要另起进程抓全量流量高带宽下CPU扛不住通常只能抓样本或抓固定时间窗口。ss/netstat只能看本机socket经过网关转发的那部分流量它完全看不到。NetFlow/sFlow需要网卡或交换机支持纯软件环境往往没有现成数据源。这些工具的另一个共性问题它们都游离在“连接”这个概念之外。可实际上真正需要判断“谁在消耗带宽”的时候最自然的单位就是一条连接五元组。连接是谁建立的、源地址是什么、访问了哪个端口、持续多长时间这些信息只有连接跟踪层才有。1.2 连接跟踪能补上哪块拼图连接跟踪最有价值的地方在于它本来就在数据路径上。每个经过网关的数据包内核都会在连接跟踪模块里查表、更新状态。如果我们把计数开关打开它就顺手把这一个方向上的包数和字节数累加到对应连接条目里。你做统计的动作只是去读一条已经存在的数据而不是像抓包那样额外复制一份数据流。还有一点是“无状态匹配”做不到的iptables计数器是按规则匹配的一个连接如果命中了多条规则你能看到每条规则的计数但很难还原“这一条连接总共跑了多少流量”。连接跟踪则是一连接一条目无论这个连接匹配了多少条规则它的累计字节数就集中放在一个地方查起来非常清爽。所以我的习惯是只要机器上有Linux内核优先从conntrack拿流量数据。不用装额外的agent也不用担心采样误差拿到的就是内核逐一数过的精确计数。它不能替代流量分析平台但作为“应急定位”和“轻量统计”的工具性价比极高。2. 连接跟踪的计数机制从钩子到字节计数2.1 netfilter钩子与连接状态的建立要理解连接跟踪怎么统计流量先得知道它在协议栈里站在哪。Linux网络协议栈在数据包路径上安排了几个hook点分别是PREROUTING、INPUT、FORWARD、OUTPUT、POSTROUTING。连接跟踪模块注册在PREROUTING和OUTPUT这两个位置也就是说不管是外部进来的包、本机发出的包还是经过转发的包都会先经过连接跟踪的处理。一个数据包到达时连接跟踪模块会先查连接表。如果这个包属于一条已有连接就直接更新对应条目的状态和计数如果找不到匹配项就判断是否新建一条连接。这里有个容易误解的点一条TCP连接要三次握手才能“建立”但连接跟踪里只要看到第一个SYN包就会先创建一个状态为NEW的条目等收到应答报文后状态会更新为ESTABLISHED。所以你在连接表里看到的ESTABLISHED条目代表的不是应用层握手完成而是“两个方向都见过报文了”。状态机里有几个关键状态NEW表示只见过单方向报文ESTABLISHED表示两个方向都见过RELATED表示与某条已有连接关联的子连接典型的如FTP数据连接INVALID表示无法识别的报文。做流量统计时我们通常只关注ESTABLISHED和NEW这两类RELATED的连接一般量不大但遇到FTP等协议时也不能忽略。2.2 连接条目的双向计数与开启方式连接跟踪条目里记录的是两个方向各自的信息每个方向都包含源地址、目的地址、源端口、目的端口以及包数packets和字节数bytes。之所以强调“每个方向”是因为在很多场景下连接的上下行流量差异非常大。比如一台服务器向外同步数据原始方向客户端到服务器字节数可能很小回应方向服务器到客户端字节数却很大如果你只统计了单方向就会得出完全错误的结论。在部分内核版本和发行版里字节计数不是默认开启的需要通过sysctl打开sysctl -w net.netfilter.nf_conntrack_acct1如果想永久生效在/etc/sysctl.conf或/etc/sysctl.d/下的配置文件里加一行net.netfilter.nf_conntrack_acct 1有个细节需要注意这个开关开启后对已经存在于连接表里的旧条目可能不会立即生效。因为计数是在报文更新连接条目时累加的旧条目在创建时没有初始化计数器字段新报文到达后内核才会补上计数。保险起见建议开启后清空一次连接表或者等待旧连接自然超时conntrack -F清空连接表会让所有现网连接重新建立状态代价是连接跟踪表会短时重建NAT映射也会重新学习。生产环境操作要挑低峰期而且确保防火墙规则不是依赖conntrack状态来放行的否则清表瞬间可能误伤正在进行的连接。2.3 超时、哈希表与内存占用影响统计的前提参数在做流量统计之前最好先摸清连接跟踪的几个基础参数因为它们直接决定了你“能看到多长时间的流量”。TCP连接的Establish状态超时时间默认是432000秒也就是5天。UDP默认只有30秒ICMP是30秒。这意味着一条TCP长连接如果持续存在它的计数器会一直累加直到连接关闭或超时而UDP“连接”只要30秒没有报文条目就会消失计数器归零。理解了这个机制你就知道为什么“瞬时流量”和“累计流量”要从不同时间尺度去统计。连接表大小由nf_conntrack_max控制哈希桶数量由nf_conntrack_buckets控制。粗略估算每个连接条目大概占用300字节左右内存再加上哈希表本身的开销。调大连接表前要算清楚内存账# 查看当前值和统计信息 sysctl net.netfilter.nf_conntrack_max sysctl net.netfilter.nf_conntrack_buckets conntrack -S如果连接数超过nf_conntrack_max新连接就会被丢弃表现为业务方报“网络不通”但你ping网关可能又是通的。做流量统计的人如果忽略了这层可能会看到某个IP的累计字节数莫名其妙暴涨或瞬间消失——那不是流量真的消失了而是连接条目因为表满被强制回收了。这个问题我在第4部分还会重点展开。3. 命令行实时读取连接流量3.1 字段解读读取连接跟踪数据有两条路径一条是直接读/proc/net/nf_conntrack文本文件另一条是用conntrack工具通过netlink读取。前者简单直接后者功能更强还支持事件流模式。先看一个典型的连接条目长什么样。假设我用conntrack -L -o extended查看会得到类似这样的行ipv4 2 tcp 6 431990 ESTABLISHED src192.168.1.10 dst192.0.2.5 sport42345 dport443 packets1024 bytes1048576 src192.0.2.5 dst192.168.1.10 sport443 dport42345 packets2048 bytes2097152 [ASSURED] mark0 zone0 use2逐个字段拆开看ipv4 2地址族2表示IPv4。tcp 6协议名和协议号。431990条目剩余生命周期秒会不断减少。ESTABLISHED连接状态。第一组src/dst/sport/dport原始方向的五元组。第一组packets/bytes原始方向的包数和字节数。第二组src/dst/sport/dport应答方向replies的五元组。注意这里src是原始方向的目的IPdst是原始方向的源IP端口也相应反转。第二组packets/bytes应答方向的包数和字节数。[ASSURED]双向都有流量的标记。mark/zone/use防火墙标记、zone区域和引用计数一般统计用不到。从/proc/net/nf_conntrack读取的内容格式基本一致只是每行开头没有“ipv4 2”这种格式化前缀而是直接以协议信息开头。这个文件内容非常直接适合写脚本快速解析。3.2 用Python脚本统计每个内网IP的实时流量实际工作中我最常做的统计维度是“按内网IP聚合”也就是不管这个IP访问了多少个外部地址把它的所有连接字节数加起来看谁占的带宽最多。下面这个Python脚本就是干这个事的#!/usr/bin/env python3 import collections import sys # 统计每个内网IP当前的累计流量并输出Top N def read_conntrack(path/proc/net/nf_conntrack): stats collections.Counter() try: with open(path, r) as f: for line in f: if not line.startswith(ipv4): continue parts line.split() # 从第6个字段起是src... dst... 这种形式 src_ip None sport None dport None packets None bytes_ None for field in parts[6:]: if field.startswith(src): src_ip field.split(, 1)[1] elif field.startswith(sport): sport int(field.split(, 1)[1]) elif field.startswith(dport): dport int(field.split(, 1)[1]) elif field.startswith(packets): if packets is None: packets int(field.split(, 1)[1]) elif field.startswith(bytes): if bytes_ is None: bytes_ int(field.split(, 1)[1]) if src_ip and bytes_ is not None: stats[src_ip] bytes_ except FileNotFoundError: print(未找到连接跟踪表请确认内核已加载 nf_conntrack 模块) sys.exit(1) return stats if __name__ __main__: top int(sys.argv[1]) if len(sys.argv) 1 else 10 for ip, total in read_conntrack().most_common(top): print(f{ip}\t{total / 1024 / 1024:.1f} MB)这段脚本的逻辑很简单逐行解析遇到src就取源IP遇到第一组bytes就取原始方向的字节数。注意我用了“如果packets还没赋值就取第一个”的小技巧这样不会把应答方向的计数也累加进去。实际生产中如果还要分TCP/UDP、按端口聚合可以在解析时把协议字段和端口也存下来逻辑是一样的。脚本运行后输出类似这样192.168.1.10 428.5 MB 192.168.1.63 102.1 MB 192.168.1.22 12.3 MB如果你只是想快速看一眼不想写脚本可以直接用命令行组合conntrack -L -o extended | awk {for(i1;iNF;i) if($i ~ /^src/){split($i,a,); s[a[2]]1}} END{for(ip in s) print ip, s[ip]} | sort -k2 -rn | head当然这种awk写法统计的是条目数不是字节数做粗筛可以用精确定位还是建议用Python。3.3 事件流方式conntrack -E 做增量统计conntrack -L是“读快照”每次执行都读取当前连接表的全部内容。如果你要持续观察新连接的建立和销毁或者想做增量统计更好的方式是conntrack -E事件流模式conntrack -E -o extended这个命令会阻塞持续输出内核推送的连接跟踪事件。新连接创建NEW、状态更新UPDATE、连接销毁DESTROY都会产生事件。每行事件里同样带有packets和bytes信息可以在应用层按事件变化量做累加。事件流模式的一个坑是netlink socket缓冲区有限。如果新连接事件产生的速度超过了用户态程序的消费速度内核会静默丢弃部分事件你的统计就会出现缺口。生产环境长时间跑conntrack -E建议显式调大缓冲区conntrack -E -o extended -b 52428800不同版本对参数的支持略有差异可以用conntrack -E -h查看本机的参数。我个人建议如果只是应急观察几十分钟用-E没问题如果要长期作为统计数据源要么做断线重连和完整性校验要么改成定时读快照做差量后者更容易保持在生产环境稳定运行。4. 生产环境中的三个大坑NAT、分片和表溢出4.1 NAT场景下原始方向与应答方向的地址变换连接跟踪本身就是NAT的基础一个数据包做地址转换时转换前后的信息都记录在同一个条目里。这就带来一个统计上的坑如果你在NAT网关上读连接表看到的“原始方向源地址”是转换前的内网地址而“应答方向的目的地址”也是内网地址两个方向看起来好像都指向内网IP。举个例子。内网192.168.1.10访问公网192.0.2.5网关做SNAT后数据包源地址变成了网关公网IP。但在连接跟踪条目里第一组还是记录src192.168.1.10 dst192.0.2.5第二组记录src192.0.2.5 dst192.168.1.10。所以在统计“内网IP流量”时按原始方向的src取就一定没错。反过来如果你只统计了NAT转换后的地址也就是网关公网IP就会把全内网的流量全算到网关头上什么都定位不出来。另外如果网关同时做DNAT把公网IP的某个端口映射到内网服务器内网服务器的IP反而会出现在原始方向的dst字段上。统计前先画清楚NAT拓扑比写脚本更重要。4.2 分片与特殊协议对计数的干扰IP分片是连接跟踪的老问题。当一个数据包被分片后只有第一个分片带有完整传输层头后续分片可能无法被正常匹配到连接条目。内核通过nf_defrag_ipv4模块做分片重组让连接跟踪和NAT能够正确处理分片报文。如果这个模块没加载分片报文可能被当作INVALID丢弃而连接计数也会偏低。生产环境看流量统计时如果发现某个IP的字节数和实际业务流量对不上尤其大量使用UDP大包的时候排查思路之一就是看是否涉及分片重组。检查模块是否加载lsmod | grep nf_defrag_ipv4特殊协议也需要额外照顾。比如GRE隧道、PPTP等连接跟踪需要对应模块才能正确识别。PPTP还涉及控制连接和数据连接的关联没有加载nf_conntrack_pptp的话数据连接可能完全不被跟踪。这类协议的流量统计准确率天然不如TCP/UDP如果业务里有隧道建议不要只看连接跟踪配合端口镜像一起判断。4.3 nf_conntrack表满导致丢包统计变负数真相前面提到过连接表满了会导致新连接无法建立。这个问题的表现很有迷惑性。有次我帮某个机房排查业务方反馈“某个IP累计流量突然变小了”。我查连接表发现那个IP的累计字节数确实在往下掉但不是流量倒流而是表满后旧条目被强制回收计数器随之消失再从新连接重新累计。判断表是否满看两个指标conntrack -S输出里重点看insert_failed、drop和early_drop。insert_failed表示新建连接失败次数如果持续增长说明表容量已经不够了。最直接的处理是调大nf_conntrack_max但前提是内存够。粗略估算如果设到100万个连接条目光连接跟踪就要占用300MB左右内存哈希表开销另算。调参前最好用free确认内存余量sysctl -w net.netfilter.nf_conntrack_max524288 sysctl -w net.netfilter.nf_conntrack_buckets131072还有一个办法是调短TCP超时时间让空闲连接更快释放sysctl -w net.netfilter.nf_conntrack_tcp_timeout_established7200不过调超时会改变NAT和防火墙的行为比如长连接如果超过超时时间中间没有报文连接状态会被丢弃后续报文会被当作新连接处理可能导致需要重新认证。所以这个参数要结合具体业务调不能一刀切。5. 统计之后怎么用落地到监控和策略联动5.1 定时快照与长期存储的取舍连接跟踪数据本质上是“当前状态”不是历史时序数据。要做长期趋势分析有两种思路一种是用cron定时抓快照比如每分钟执行一次conntrack -L -o extended把结果解析后写入文件或时序数据库。缺点是两次快照之间如果连接建立了又销毁累计字节数就被漏掉了优点是实现简单、稳定性好服务器重启也不怕丢事件。适合对精度要求不高的容量规划场景。另一种是用事件流模式把conntrack -E的输出持续导入时序数据库每个事件都作为一条记录。这样不会漏掉短生命周期连接但对数据接入端的写入性能要求高而且-E断线重连时要处理事件缺失的问题。我个人在“查历史流量峰值”场景下一般用快照就够了只有在做计费、做用户流量明细时才值得上事件流。如果要把数据存进时序库字段建议设计成时间戳、源IP、源端口、目的IP、目的端口、协议、方向、包数增量、字节数增量。方向这个字段很重要因为连接跟踪条目天然分两个方向统一存成一个值不利于后续查询。5.2 与iptables/nftables策略联动连接跟踪统计不只是用来看还能用来触发策略。比较常见的是nftables里按字节数做控制。比如要对内网某个IP限速可以在FORWARD链上匹配conntrack状态用limit或meter做令牌桶nft add table ip filter nft add chain ip filter forward { type filter hook forward priority 0 \; } nft add rule ip filter forward ip saddr 192.168.1.10 ct state established,related counter limit rate 10 mbytes/second accept这里的ct state就是直接用了连接跟踪的状态信息而counter则会在规则命中时计数。和生产环境里的做法类似你可以先跑一段统计确认异常流量来自哪些IP再针对这些IP叠加限速或日志规则。相比直接用iptables的-m conntracknftables的语法更清晰ct关键字也更直观。需要提醒的是动态改防火墙规则有风险。如果是自动化脚本在做阈值触发一定要加“恢复阈值”比如字节速率超过100MB持续5分钟才限速低于20MB持续10分钟才解除否则规则会在临界值反复抖动业务体验会非常糟糕。5.3 适合不同规模环境的方案选型参考环境规模推荐方案理由单台Linux网关/小规模NAT/proc/net/nf_conntrack Python脚本零依赖读取方便解析成本可控数台网关、需要集中查看conntrack -L快照 定时采集写入时序库实现简单稳定性好适合长期运行连接吞吐很高的核心网关conntrack -E事件流或内核态eBPF方案避免用户态反复读表减少CPU开销和事件丢失大型网络、需要出口全景交换机NetFlow/sFlow或硬件探针连接跟踪只在单机视角无法覆盖跨设备流量路径选型时没必要一上来就上重方案。大部分场景一台小网关用Python脚本就够了。如果日后连接数上去了再平滑迁移到事件流或eBPF方案之间数据格式可以保持统一。6. 个人实操手记踩过的坑和最终推荐做法最后说点实际经验都是踩过坑之后才总结出来的。第一开启计数器后记得清一次连接表。有次我改了nf_conntrack_acct1然后继续读/proc/net/nf_conntrack发现很多旧连接没有packets和bytes一开始还以为是内核版本太老不支持。后来才知道是旧条目没有补上计数。清表之后新条目就正常了。第二读/proc/net/nf_conntrack的时候尽量在脚本里加上“字段缺失就跳过”的容错。连接表是一直在变化的你读文件的时候可能有旧连接刚好销毁也可能有新连接刚插入解析到一半字段对不上很正常。脚本别因为一行解析失败就整体崩溃跳过即可。第三做“哪个IP流量最大”这种Top N统计时别忘了把/proc/net/nf_conntrack里标记为[UNREPLIED]的单向连接也算进去。有些攻击流量或端口扫描流量就是单向的如果只统计ESTABLISHED状态你会漏掉最需要关注的那部分。但也要注意单向连接可能只是UDP应用还没来得及回复或者TCP还在握手阶段不能一看到单向就觉得是攻击。第四如果机器上同时有iptables和nftables在用别混着读数据。iptables的conntrack模块和nftables的ct关键字读取的是同一套连接跟踪表但两者在规则匹配时的计数器位置不一样。做流量统计只读conntrack本身就好不要试图用防火墙规则的计数器反推连接流量。第五生产环境长期观察建议用systemd timer或cron跑快照脚本输出带着时间戳和主机名的CSV或JSON方便后续汇总。脚本里记得处理连接表读取耗时连接数很大的时候一次读取慢则几百毫秒如果定时周期太短可能还没读完下一次就开始了会造成CPU堆积。现在这套方法在我内部环境里的几台网关上跑了大半年日常流量突增、异常IP定位、容量规划都在用。比起重新部署一整套流量分析系统它几乎零成本效果却非常直接。如果你手里也有一台Linux网关正愁定位不到流量大户不妨先开个计数写个二十行脚本跑起来比口头扯皮快得多。