跳过正文
  1. Posts/

Linux 连接跟踪表 conntrack 详解

·1746 字·9 分钟
王二麻
作者
王二麻
混迹 Linux 运维多年,专注 Kubernetes 生产实战、Golang 工具开发与稳定性工程。写点踩坑心得,聊聊技术人生。
网络排查 - 这篇文章属于一个选集。
§ 1: 本文

什么是 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

为什么会这样?
#

  1. NLB 开启"客户端地址保持"后,ECS 看到的源 IP 是真实 Client IP
  2. ECS 的 conntrack 记录了 src=Client_IP
  3. 回包时,ECS 根据 conntrack 直接发给 Client IP
  4. 但 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 conntrackENI/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  <- 确认挂断(这个响应可能丢失!)

问题分析

  1. 用户 A 发起通话,NLB 创建 UDP 会话(20s 超时)
  2. OpenSIPS 创建 conntrack(180s 超时)
  3. 通话持续 5 分钟,期间只有 RTP 流量(可能走不同端口)
  4. NLB 的 SIP 会话早已过期(>20s)
  5. 用户 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 表满是常见的网络问题根因之一。

参考资料
#

网络排查 - 这篇文章属于一个选集。
§ 1: 本文

相关文章