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 --> GA. 状态数据结构
- 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, 拒绝!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: 丢弃重传包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 --> I2. 有效数据边界(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 = 1B. SYN-ACK 包处理
接收方向(ts_data[1]):
td_end = SEQ + 1
td_maxend = SEQ + 13. 状态匹配核心代码
// 确定方向
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 的复杂性
- 状态管理必须与实际网络情况同步