IPFilter TCP 状态追踪引擎技术分析

一、概述

1. 背景

IPFilter 是一个经典的防火墙软件,其 TCP 状态追踪引擎用于维护 TCP 连接状态并过滤数据包。OVS(Open vSwitch)的用户态 conntrack TCP 状态追踪功能即源于此。

2. 核心问题

旧版 IPFilter 过滤引擎存在三个关键缺陷:

  • 假定所有通过防火墙的数据包都能到达目的地
  • 对称处理窗口大小,导致边界计算错误
  • 使用最后一次看到的窗口尺寸处理后续新数据

3. 设计原则

新版引擎遵循「永不假设,只接受验证后的信息」原则,状态管理完全基于事实。


二、旧过滤引擎分析

1. 工作原理

旧引擎为每个 TCP 连接维护一个跟踪条目,包含以下核心数据:

graph LR
    A[数据包到达] --> B{计算 seqskew}
    B --> C{计算 ackskew}
    C --> D{seqskew <= 窗口?}
    D --> E{ackskew <= 窗口?}
    E --> F[通过: 更新状态]
    E --> G[拒绝: 丢弃数据包]
    D --> G

mermaid

旧引擎工作流程

A. 状态数据结构

  • is_seq:c-s 方向最后一个 seq 或 s-c 方向最后一个 ack
  • is_ack:c-s 方向最后一个 ack 或 s-c 方向最后一个 seq
  • is_swin:c-s 方向的接收窗口
  • is_dwin:s-c 方向的接收窗口

B. 判断逻辑

计算当前数据包的 seq、ack 与上次记录值的差值(取绝对值),然后比较是否在窗口范围内。

2. 问题场景

A. 场景 1:网络延迟导致乱序

假设 A→B 已建立连接,B→A 的确认包发生网络延迟:

sequenceDiagram
    participant A as 客户端 A
    participant F as 防火墙
    participant B as 服务端 B

    A->>F: 1. win 2048 ack 0
    F->>B: 1. win 2048 ack 0
    A->>F: 2. 数据 0:1000
    F->>B: 2. 数据 0:1000
    B->>F: 3. win 1048 ack 1000 (延迟)
    A->>F: 4. 数据 1000:2000
    F->>B: 4. 数据 1000:2000
    B->>F: 5. win 2048 ack 2000
    F->>B: 5. win 2048 ack 2000
    A->>F: 6. 数据 2000:3000
    F->>B: 6. 数据 2000:3000
    B->>F: 7. win 2048 ack 3000
    F->>B: 7. win 2048 ack 3000
    Note over F: 此时延迟包到达
    B->>F: 8. win 1048 ack 1000 (原第 3 包)
    F->>F: 状态: seq=1000, win=1048
    A->>F: 9. 重传 3000:4000
    F->>F: seqskew=2000 > 1048, 拒绝!

mermaid

乱序包处理时序图

B. 场景 2:窗口收缩导致重传拒绝

假设窗口从 5000 缩小到 300,之前的重传包被错误拒绝:

sequenceDiagram
    participant A as 客户端
    participant F as 防火墙
    participant B as 服务端

    A->>F: 连续发送数据
    F->>B: 转发数据
    B->>F: 窗口从 5000 缩小到 300
    Note over F: 记录新窗口: win=300
    A->>F: 发现数据丢失, 触发重传
    A->>F: 重传 [300, 5000] 范围数据
    F->>F: 检查: seq 在 [300, 5000] 范围
    F->>F: 但窗口已是 300, 判定非法!
    F->>B: 丢弃重传包

mermaid

窗口收缩问题示意图

3. 根本原因

当 TCP 出现乱序或丢包时,防火墙的状态与客户端、服务端状态不再同步,导致「一步错,步步错」。


三、新过滤引擎设计

1. 设计理念

核心原则:状态管理只基于事实,不做任何假设。

graph TB
    A[数据包] --> B{提取 seq/ack/win}
    B --> C[计算有效边界]
    C --> D[验证 seq 范围]
    C --> E[验证 ack 范围]
    D --> F{seq 在边界内?}
    E --> G{ack 在边界内?}
    F --> H[更新状态: 通过]
    G --> H
    F --> I[拒绝]
    G --> I

mermaid

新引擎验证流程

2. 有效数据边界(seq)

A. 上界约束

对于 A→B 方向的数据包,其序列号结束位置 s+n 必须满足:

s+n ≤ max{ack + max(win, 1)}

其中 max(win, 1) 处理零窗口探测的特殊情况。

B. 下界约束

序列号起始值 s 必须满足:

s ≥ max{s+n} - max{max(win, 1)}

这确保序列号不会小于已确认的最大序列号减去最大窗口。

C. 完整约束

max{s+n} - max{max(win, 1)} ≤ s ≤ max{ack}

3. 有效确认边界(ack)

A. 上界

ack 不能确认发送方未发送的数据:

a ≤ max{s+n}

B. 下界

考虑到 TCP 乱序特性,下界需要适当放宽。使用 MAXACKWINDOW(略大于 TCP 最大窗口 65535)作为缓冲:

a ≥ max{s+n} - MAXACKWINDOW

这样的设计:

  • 不会错误地将乱序 ack 判定为非法
  • 可能接收过期的 ack,但这不会对 TCP 连接造成影响

4. 总结公式

新引擎使用以下四个公式验证数据包合法性:

(I)   s+n ≤ max{ack + max(win, 1)}
(II)  s ≥ max{s+n} - max{max(win, 1)}
(III) a ≤ max{s+n}
(IV)  a ≥ max{s+n} - MAXACKWINDOW

四、实现细节

1. 数据结构

struct tcpdata {
    u_32_t td_end;      // seq + len 最大值
    u_32_t td_maxend;   // ack + max(win,1) 最大值
    u_short td_maxwin;  // F 看到的最大的 win
} tcpdata_t;

struct tcpstate {
    u_short ts_sport;           // 源端口
    u_short ts_dport;           // 目的端口
    tcpdata_t ts_data[2];       // src/dst 状态
    u_char ts_state[2];         // src/dst 状态机
} tcpstate_t;

2. 初始化处理

A. SYN 包处理

发送方向(ts_data[0]):
  td_end = SEQ + 1
  td_maxend = SEQ + 1
  td_maxwin = max(WIN, 1)

接收方向(ts_data[1]):
  td_end = 0
  td_maxend = 0
  td_maxwin = 1

B. SYN-ACK 包处理

接收方向(ts_data[1]):
  td_end = SEQ + 1
  td_maxend = SEQ + 1

3. 状态匹配核心代码

// 确定方向
source = (ip->ip_src.s_addr == is->is_src.s_addr);
fdata = &is->is_tcp.ts_data[!source];  // from 方向
tdata = &is->is_tcp.ts_data[source];   // to 方向

seq = ntohl(tcp->th_seq);
ack = ntohl(tcp->th_ack);
win = ntohs(tcp->th_win);
end = seq + payload_len;

// 处理 SYN-ACK 初始化
if (fdata->td_end == 0) {
    fdata->td_end = end;
    fdata->td_maxwin = 1;
    fdata->td_maxend = end + 1;
}

// 无 ACK 标志时
if (!(tcp->th_flags & TH_ACK)) {
    ack = tdata->td_end;
}

// 边界检查
if ((SEQ_GE(fdata->td_maxend, end)) &&           // 上界 (I)
    (SEQ_GE(seq, fdata->td_end - maxwin)) &&     // 下界 (II)
    (ackskew >= -MAXACKWINDOW) &&                // ACK 下界 (IV)
    (ackskew <= MAXACKWINDOW)) {                 // ACK 上界 (III)

    // 更新窗口
    if (fdata->td_maxwin < win)
        fdata->td_maxwin = win;

    // 更新序列号范围
    if (SEQ_GT(end, fdata->td_end))
        fdata->td_end = end;

    // 更新最大结束位置
    if (SEQ_GE(ack + win, tdata->td_maxend)) {
        tdata->td_maxend = ack + win;
        if (win == 0)
            tdata->td_maxend++;
    }
}

五、关键设计决策

1. 窗口探测特殊处理

当接收窗口 win = 0 时,发送方进入窗口探测模式,允许发送 1 字节数据。新引擎通过 max(win, 1) 优雅处理此情况。

2. ACK 边界放宽

使用 MAXACKWINDOW 放宽 ACK 下界,避免乱序 ACK 被错误拒绝。过期 ACK 虽然可能被接收,但不会对 TCP 连接产生负面影响。

3. 滤波器重启处理

论文指出两种可能的处理方案,但均未实现:

  • 持久化状态,重启后恢复
  • 重建连接状态

实际生产中需要考虑此场景。


六、未来工作

1. TCP 窗口缩放选项

论文指出尚未支持 RFC1323 定义的窗口缩放选项。OVS 已实现此功能。

2. 时间戳选项

需要支持 TCP 时间戳选项,以处理高速连接中的序列号回绕问题。

3. IP 分片处理

当前实现中对 IP 分片的处理存在临时方案,需要优化。

4. 会话恢复

更好地处理防火墙重启后已建立连接的会话状态恢复。


七、总结

IPFilter 的 TCP 状态追踪引擎从旧版本的「基于假设」转向新版本的「基于事实」,通过严格的数学边界定义,解决了乱序、丢包、窗口变化等复杂场景下的数据包过滤问题。其设计思想对现代防火墙和连接追踪系统仍有重要参考价值。

核心要点:

  • 永不假设,只基于验证过的信息
  • 使用数学公式定义精确的边界条件
  • 适当放宽边界以容忍 TCP 的复杂性
  • 状态管理必须与实际网络情况同步

参考资料

  1. Real Stateful TCP Packet Filtering in IP Filter - USENIX Security 2001
  2. RFC 793 - Transmission Control Protocol
  3. RFC 1323 - TCP Extensions for High Performance
最后修改:2026 年 01 月 18 日
如果觉得我的文章对你有用,请随意赞赏