什么是 conntrack#
conntrack(Connection Tracking,连接跟踪)是 Linux 内核 Netfilter 框架的核心组件,用于跟踪网络连接的状态。它记录了经过系统的所有网络连接信息,包括源/目标 IP、端口、协议、连接状态等。
flowchart LR
subgraph Netfilter
A[PREROUTING] --> B[conntrack]
B --> C[路由决策]
C --> D[FORWARD/INPUT]
D --> E[POSTROUTING]
end
PKT[数据包] --> A
E --> OUT[出站]
conntrack 工作在 Netfilter 的最前端,在数据包进入任何 iptables 规则之前,就会被 conntrack 模块处理。
conntrack 记录的结构#
每条 conntrack 记录包含以下关键信息:
# 查看 conntrack 表
conntrack -L
# 输出示例
tcp 6 431999 ESTABLISHED src=10.0.0.1 dst=10.0.0.2 sport=45678 dport=80 \
src=10.0.0.2 dst=10.0.0.1 sport=80 dport=45678 [ASSURED] mark=0 use=1
字段说明:
| 字段 | 说明 |
|---|---|
tcp 6 | 协议名称和协议号 |
431999 | 剩余超时时间(秒) |
ESTABLISHED | 连接状态 |
| 第一组 src/dst/sport/dport | 原始方向(请求方) |
| 第二组 src/dst/sport/dport | 回复方向(期望的响应) |
[ASSURED] | 双向都有流量,不会被提前清理 |
连接状态#
conntrack 定义了以下连接状态:
NEW - 新建连接的第一个包
ESTABLISHED - 双向都有流量的连接
RELATED - 与已有连接相关的新连接(如 FTP 数据连接)
INVALID - 无法识别或不合法的包
UNTRACKED - 明确标记为不跟踪的包
stateDiagram-v2
[*] --> NEW: 首包
NEW --> ESTABLISHED: 收到回复
ESTABLISHED --> ESTABLISHED: 持续通信
ESTABLISHED --> [*]: 超时/关闭
NEW --> [*]: 超时无回复
什么情况下会产生 conntrack 记录#
核心规则:只要数据包经过 Netfilter 并且没有被标记为 NOTRACK,就会产生 conntrack 记录。
1. 会产生 conntrack 的场景#
场景一:本机发起的连接#
# 本机 curl 外部服务
curl https://example.com
# 会产生 conntrack 记录
tcp 6 ESTABLISHED src=本机IP dst=外部IP sport=随机 dport=443 ...
场景二:外部访问本机服务#
# 外部访问本机的 nginx
# 会产生 conntrack 记录
tcp 6 ESTABLISHED src=客户端IP dst=本机IP sport=随机 dport=80 ...
场景三:转发流量(路由/NAT)#
# 作为网关转发流量
# 会产生 conntrack 记录
tcp 6 ESTABLISHED src=内网IP dst=外网IP sport=随机 dport=443 ...
场景四:Kubernetes 中的 Service 访问#
# Pod 访问 ClusterIP Service
# kube-proxy 使用 iptables 做 DNAT
# 会产生 conntrack 记录,且包含 NAT 映射
tcp 6 ESTABLISHED src=PodIP dst=ClusterIP sport=随机 dport=80 \
src=后端PodIP dst=PodIP sport=80 dport=随机 ...
2. 不会产生 conntrack 的场景#
场景一:显式标记 NOTRACK#
# 使用 raw 表标记不跟踪
iptables -t raw -A PREROUTING -p tcp --dport 80 -j NOTRACK
iptables -t raw -A OUTPUT -p tcp --sport 80 -j NOTRACK
场景二:使用 nftables 的 notrack#
nft add rule ip raw prerouting tcp dport 80 notrack
场景三:内核未加载 conntrack 模块#
# 如果没有任何 iptables 规则使用状态匹配
# 且没有 NAT 规则,conntrack 模块可能不会加载
lsmod | grep nf_conntrack
查看和管理 conntrack#
安装工具#
# CentOS/RHEL
yum install conntrack-tools
# Ubuntu/Debian
apt install conntrack
常用命令#
# 查看所有连接
conntrack -L
# 统计连接数
conntrack -C
# 按协议过滤
conntrack -L -p tcp
conntrack -L -p udp
# 按源IP过滤
conntrack -L -s 10.0.0.1
# 按目标端口过滤
conntrack -L --dport 80
# 按状态过滤
conntrack -L --state ESTABLISHED
# 删除特定连接
conntrack -D -s 10.0.0.1 -d 10.0.0.2 -p tcp --dport 80
# 清空所有连接(慎用)
conntrack -F
查看内核参数#
# 最大连接数
cat /proc/sys/net/netfilter/nf_conntrack_max
# 当前连接数
cat /proc/sys/net/netfilter/nf_conntrack_count
# 各协议超时时间
cat /proc/sys/net/netfilter/nf_conntrack_tcp_timeout_established
cat /proc/sys/net/netfilter/nf_conntrack_tcp_timeout_time_wait
cat /proc/sys/net/netfilter/nf_conntrack_udp_timeout
conntrack 表满问题#
当连接数达到 nf_conntrack_max 时,新连接会被丢弃,内核日志会出现:
nf_conntrack: table full, dropping packet
排查方法#
# 检查当前连接数和最大值
echo "当前: $(cat /proc/sys/net/netfilter/nf_conntrack_count)"
echo "最大: $(cat /proc/sys/net/netfilter/nf_conntrack_max)"
# 查看连接分布
conntrack -L | awk '{print $3}' | sort | uniq -c | sort -rn
# 查看 TIME_WAIT 连接数
conntrack -L --state TIME_WAIT | wc -l
调优方案#
# 方案一:增大最大连接数
echo 'net.netfilter.nf_conntrack_max = 1048576' >> /etc/sysctl.conf
# 方案二:减少超时时间
echo 'net.netfilter.nf_conntrack_tcp_timeout_established = 1800' >> /etc/sysctl.conf
echo 'net.netfilter.nf_conntrack_tcp_timeout_time_wait = 30' >> /etc/sysctl.conf
# 方案三:对高流量服务禁用跟踪
iptables -t raw -A PREROUTING -p tcp --dport 80 -j NOTRACK
iptables -t raw -A OUTPUT -p tcp --sport 80 -j NOTRACK
# 生效
sysctl -p
NLB + UDP 场景的 conntrack 陷阱#
这是一个生产环境常见的坑:NLB 会话超时 < ECS conntrack 超时,导致回包路径异常。
场景描述#
架构:Client --> NLB(UDP 5080) --> ECS 后端服务
NLB 配置:
- UDP 会话保持:20 秒
- 客户端地址保持:开启(透传源 IP)
ECS 配置:
- net.netfilter.nf_conntrack_udp_timeout_stream = 30 秒
正常流程(T=0s)#
请求:Client(IP_C:1234) -> NLB(VIP:5080) -> ECS(IP_E:5080)
NLB 透传源 IP,ECS 看到 src=IP_C:1234
ECS conntrack 记录:
udp src=IP_C dst=IP_E sport=1234 dport=5080 \
src=IP_E dst=IP_C sport=5080 dport=1234
响应:ECS -> NLB -> Client
Client 收到 src=VIP:5080(正常)
问题场景(T=25s)#
NLB 会话已过期(>20s),但 ECS conntrack 还有效(<30s)
请求:Client(IP_C:1234) -> NLB -> ECS
NLB 创建新会话,流量正常到达 ECS
响应:ECS 查 conntrack,发现匹配记录
直接回复 dst=IP_C:1234,不经过 NLB!
Client 收到:src=IP_E:5080(不是 VIP!)
结果:Client 丢弃响应,因为源 IP 不匹配
sequenceDiagram
participant C as Client
participant N as NLB VIP
participant E as ECS
rect rgb(144, 238, 144)
Note over C,E: T=0s 正常流程
C->>N: UDP to VIP:5080
N->>E: 透传 src=ClientIP
E->>N: 响应
N->>C: src=VIP:5080
end
Note over N: T=25s 会话过期
Note over E: conntrack 还有 5s
rect rgb(255, 182, 193)
Note over C,E: T=25s 问题出现
C->>N: UDP to VIP:5080
N->>E: 透传 src=ClientIP
E--xC: 直接回 Client
Note over C: src=ECS_IP 不是 VIP
丢弃!
end
为什么会这样?#
- NLB 开启"客户端地址保持"后,ECS 看到的源 IP 是真实 Client IP
- ECS 的 conntrack 记录了
src=Client_IP - 回包时,ECS 根据 conntrack 直接发给 Client IP
- 但 Client 期望收到来自 NLB VIP 的响应,源 IP 不匹配则丢弃
解决方案#
方案一:调整超时时间
# ECS 上设置 UDP conntrack 超时 < NLB 会话超时
echo 'net.netfilter.nf_conntrack_udp_timeout = 10' >> /etc/sysctl.conf
echo 'net.netfilter.nf_conntrack_udp_timeout_stream = 15' >> /etc/sysctl.conf
sysctl -p
方案二:ECS 配置策略路由
确保发往 Client 网段的流量都经过 NLB:
# 添加策略路由,响应流量走 NLB
ip rule add from <ECS_IP> to <Client网段> lookup 100
ip route add default via <NLB网关> table 100
方案三:关闭客户端地址保持
如果业务不需要获取真实 Client IP,可以关闭透传:
NLB 配置:客户端地址保持 = 关闭
ECS 看到的源 IP = NLB IP
回包自然走 NLB 路径
方案四:使用 TOA 模块获取真实 IP
# 关闭 NLB 客户端地址保持
# 使用 TOA(TCP Option Address)在 TCP 层传递真实 IP
# 应用层通过 TOA 模块获取
# 适用于 TCP,UDP 需要使用 Proxy Protocol
云环境的隐藏陷阱:ENI/SDN 层会话跟踪#
在阿里云、AWS 等云环境中,存在一个容易被忽视的问题:ECS 内部的 conntrack 可能不是实际生效的那一层!
两层会话跟踪架构#
┌─────────────────────────────────────────────────────────────┐
│ ECS 实例 (Guest OS) │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ 应用层 (OpenSIPS / Nginx / ...) │ │
│ │ ↓ │ │
│ │ Linux Kernel │ │
│ │ - iptables / nftables │ │
│ │ - conntrack (你能看到,但可能不生效!) │ │
│ │ ↓ │ │
│ │ virtio-net 驱动 │ │
│ └───────────────────────────────────────────────────────┘ │
└──────────────────────────│──────────────────────────────────┘
↓ vhost-net
┌──────────────────────────│──────────────────────────────────┐
│ 宿主机 (Hypervisor) │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ vSwitch (SDN 数据平面) │ │
│ │ - 流表 (Flow Table) │ │
│ │ - 会话跟踪 (ENI conntrack) ← 实际生效! │ │
│ │ - NAT / 安全组 / ACL │ │
│ └───────────────────────────────────────────────────────┘ │
└──────────────────────────│──────────────────────────────────┘
↓
物理网络 (VPC)
两层 conntrack 对比#
| 特性 | Guest OS conntrack | ENI/SDN conntrack |
|---|---|---|
| 位置 | ECS 内部 Linux 内核 | 宿主机 vSwitch |
| 配置方式 | sysctl 参数 | 云控制台 / API |
| 作用范围 | 仅影响 iptables 规则 | 实际控制流量转发 |
| UDP 默认超时 | 30s / 180s | 通常 15-30s |
| 可见性 | conntrack -L 可见 | 用户不可见 |
为什么 OS conntrack “不生效”?#
场景:ECS 回包
1. 应用发送 UDP 响应
2. 经过 Guest OS 内核 → conntrack 记录存在
3. 到达 virtio 网卡,交给宿主机 vSwitch
4. vSwitch 查自己的流表/会话表:
- ENI 会话过期 → 包被丢弃或走默认路由
- ENI 会话有效 → 正常转发
关键:即使 Guest OS conntrack 有效,SDN 层会话过期了,包也出不去!
配置 ENI 超时(阿里云)#
控制台方式:
ECS 实例 → 网络与安全 → 弹性网卡 → 选择 ENI
→ 高级配置 → 连接跟踪
→ UDP 空闲超时时间: 80 秒(范围 15-900 秒)
API 方式:
aliyun ecs ModifyNetworkInterfaceAttribute \
--NetworkInterfaceId eni-xxx \
--ConnectionTrackingConfiguration.UdpTimeout 80
完整的超时链路设计#
NLB UDP timeout: 90s ← 云产品层
↓
ENI UDP idle timeout: 80s ← SDN 层(实际生效!)
↓
Guest OS conntrack: 180s ← OS 层(可能不生效)
正确配置:
应用保活间隔 < ENI timeout < NLB timeout
30s < 80s < 90s
诊断技巧#
# 1. Guest OS 抓包,确认包已发出
tcpdump -i eth0 udp port 5060 -nn
# 如果 Guest OS 抓到出向包,但对端没收到
# → 说明 SDN 层丢弃了(会话过期或安全组)
# 2. 使用 VPC 流日志(阿里云)
# 控制台 → VPC → 流日志
# 可以看到 ENI 层的实际流量和丢包
# 3. 检查安全组状态
# 安全组是状态防火墙,会话过期后出向包可能被拦截
# 即使安全组配置了允许,也可能因会话过期而不通
实战案例:OpenSIPS “偶尔挂不断”#
场景:VoIP 系统使用 NLB + OpenSIPS,用户反馈"偶尔挂不断电话"。
架构:
用户A --> NLB(UDP 5060) --> OpenSIPS --> NLB --> 用户B
会话保持: 20s conntrack: 180s
客户端地址保持: 开
SIP 信令流程:
INVITE -> 建立通话
200 OK <- 接受
ACK -> 确认
... RTP 媒体流(通话可能持续几分钟)...
BYE -> 挂断
200 OK <- 确认挂断(这个响应可能丢失!)
问题分析:
- 用户 A 发起通话,NLB 创建 UDP 会话(20s 超时)
- OpenSIPS 创建 conntrack(180s 超时)
- 通话持续 5 分钟,期间只有 RTP 流量(可能走不同端口)
- NLB 的 SIP 会话早已过期(>20s)
- 用户 A 发送 BYE 挂断:
- BYE 通过 NLB 到达 OpenSIPS(NLB 新建会话)
- OpenSIPS 回复 200 OK
- OpenSIPS 根据旧 conntrack,直接发给用户 A
- 用户 A 收到 src=OpenSIPS_IP(不是 NLB_VIP),丢弃
- 用户 A 认为 BYE 没响应,通话状态不一致
为什么"偶尔"?
- 短通话 (<20s):NLB 会话还在,正常挂断
- 长通话 (>20s):NLB 会话过期,200 OK 绕过 NLB,挂断失败
解决方案:
# 方案一:降低 OpenSIPS 服务器的 UDP conntrack 超时
# 设置为小于 NLB 会话超时
echo 'net.netfilter.nf_conntrack_udp_timeout = 10' >> /etc/sysctl.conf
echo 'net.netfilter.nf_conntrack_udp_timeout_stream = 15' >> /etc/sysctl.conf
sysctl -p
# 方案二:增大 NLB 的 UDP 会话超时(如果云厂商支持)
# 阿里云 NLB:UDP 会话保持可设置 60-900 秒
# 方案三:OpenSIPS 配置策略路由
# 确保 SIP 响应都经过 NLB
ip rule add from <OpenSIPS_IP> lookup 100
ip route add default via <NLB_Gateway> table 100
方案四:SIP 保活续约(推荐)#
既然 NLB 和 ECS 的超时都是 idle timeout(空闲超时),只要有持续流量就能续约会话。可以让 OpenSIPS 主动发送心跳来续约。
当前配置:
NLB UDP timeout: 90s
ECS ENI UDP idle timeout: 80s
问题:SIP 信令端口 5060 在通话期间可能没有流量
(RTP 媒体流走 10000-20000 端口)
导致 SIP 信令会话过期
SIP 保活方式对比:
| 方式 | 原理 | 流量开销 | 双向续约 |
|---|---|---|---|
| NAT Ping (OPTIONS) | 发送 SIP OPTIONS 请求 | 中 | ✅ |
| Session Timers | 定期 re-INVITE/UPDATE | 高 | ✅ |
| CRLF Keep-Alive | 发送空的 \r\n\r\n | 低 | ❌ 单向 |
NAT Ping 流程:
sequenceDiagram
participant A as 用户A
participant N as NLB
participant S as OpenSIPS
Note over A,S: 通话建立后,每 30s
S->>N: OPTIONS (心跳)
N->>A: OPTIONS
A->>N: 200 OK (响应)
N->>S: 200 OK
Note over N: NLB 会话续约
Note over S: conntrack 续约
OpenSIPS NAT Ping 配置:
# /etc/opensips/opensips.cfg
###### 加载模块 ######
loadmodule "nathelper.so"
loadmodule "usrloc.so"
loadmodule "registrar.so"
###### NAT 保活配置(核心)######
# 每 30 秒发送 SIP OPTIONS 保活
modparam("nathelper", "natping_interval", 30) # < 80s (ECS ENI timeout)
modparam("nathelper", "ping_nated_only", 0) # ping 所有用户,不只是 NAT 后的
modparam("nathelper", "sipping_bflag", "SIP_PING")
modparam("nathelper", "sipping_method", "OPTIONS")
modparam("nathelper", "sipping_from", "sip:ping@$fd")
modparam("nathelper", "natping_processes", 2)
# 用户位置数据库
modparam("usrloc", "nat_bflag", "NAT")
###### 路由逻辑 ######
route {
# 处理 OPTIONS 请求(保活探测的响应)
if (is_method("OPTIONS") && uri==myself) {
sl_send_reply(200, "OK");
exit;
}
# 用户注册时,标记需要 NAT ping
if (is_method("REGISTER")) {
setbflag("SIP_PING"); # 标记需要发送 SIP ping
save("location");
exit;
}
# 其他路由逻辑...
}
时间线设计:
NLB UDP timeout: 90s
ECS ENI timeout: 80s
NAT ping interval: 30s
时间线:
0s 30s 60s 80s 90s
|--ping--|--ping--|--ping--|
^ ECS 超时点(已被 ping 续约)
^ NLB 超时点(已被 ping 续约)
只要 ping_interval < min(NLB_timeout, ECS_timeout),会话就不会过期
验证配置:
# 1. 抓包确认 OPTIONS 心跳正在发送
tcpdump -i eth0 udp port 5060 -nn -A 2>&1 | grep -i "options"
# 2. 实时观察 conntrack 续约(timeout 应该不断被重置)
watch -n 5 'conntrack -L -p udp --dport 5060 2>/dev/null | head -3'
# 3. 完整测试流程
# - 建立通话
# - 等待 5 分钟(超过原 NLB 会话超时)
# - 挂断
# - 确认正常挂断(不再出现"挂不断")
诊断方法#
# 在 OpenSIPS 上抓包,观察 BYE 响应的目的 IP
tcpdump -i eth0 udp port 5060 -nn
# 正常情况:响应应该发给 NLB 网关
# 异常情况:响应直接发给用户 IP(绕过 NLB)
# 在 ECS 上抓包,观察响应流量的目的 IP
tcpdump -i eth0 udp port 5080 -nn
# 查看 conntrack 表
conntrack -L -p udp --dport 5080
# 检查是否有流量直接发给 Client(绕过 NLB)
# 如果 conntrack 的回复方向 dst 是 Client IP,就有风险
Kubernetes 中的 conntrack#
在 Kubernetes 环境中,conntrack 尤为重要:
kube-proxy iptables 模式#
# Service 访问会产生 DNAT conntrack 记录
# ClusterIP -> PodIP 的映射存储在 conntrack 表中
conntrack -L | grep "dport=30080"
常见问题:conntrack 条目冲突#
当 Pod 快速重启时,旧的 conntrack 条目可能导致流量发送到已不存在的 Pod:
# 现象:访问 Service 偶发超时
# 原因:conntrack 条目指向旧 Pod IP
# 解决:删除相关 conntrack 条目
conntrack -D -d <旧PodIP>
IPVS 模式的区别#
kube-proxy IPVS 模式使用独立的连接跟踪,但仍依赖 conntrack 处理 SNAT:
# IPVS 连接查看
ipvsadm -Lnc
# 但 SNAT 仍需要 conntrack
conntrack -L | grep SNAT
总结#
| 场景 | 是否产生 conntrack |
|---|---|
| 本机发起连接 | 是 |
| 外部访问本机 | 是 |
| 转发/NAT 流量 | 是 |
| 标记 NOTRACK | 否 |
| 纯二层转发(bridge) | 否 |
| conntrack 模块未加载 | 否 |
conntrack 是 Linux 网络栈的核心组件,理解它的工作原理对于网络排查和性能调优至关重要。特别是在 Kubernetes 环境中,conntrack 表满是常见的网络问题根因之一。