Loading... # IPFilter TCP 状态追踪引擎技术分析 # 一、概述 ## 1. 背景 IPFilter 是一个经典的防火墙软件,其 TCP 状态追踪引擎用于维护 TCP 连接状态并过滤数据包。OVS(Open vSwitch)的用户态 conntrack TCP 状态追踪功能即源于此。 ## 2. 核心问题 旧版 IPFilter 过滤引擎存在三个关键缺陷: - 假定所有通过防火墙的数据包都能到达目的地 - 对称处理窗口大小,导致边界计算错误 - 使用最后一次看到的窗口尺寸处理后续新数据 ## 3. 设计原则 新版引擎遵循「永不假设,只接受验证后的信息」原则,状态管理完全基于事实。 *** # 二、旧过滤引擎分析 ## 1. 工作原理 旧引擎为每个 TCP 连接维护一个跟踪条目,包含以下核心数据: ```mermaid graph LR A[数据包到达] --> B{计算 seqskew} B --> C{计算 ackskew} C --> D{seqskew <= 窗口?} D --> E{ackskew <= 窗口?} E --> F[通过: 更新状态] E --> G[拒绝: 丢弃数据包] D --> G ```   ### 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 的确认包发生网络延迟: ```mermaid 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,之前的重传包被错误拒绝: ```mermaid 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. 设计理念 **核心原则**:状态管理只基于事实,不做任何假设。 ```mermaid 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 ```   ## 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. 数据结构 ```c 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. 状态匹配核心代码 ```c // 确定方向 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](https://www.usenix.org/legacy/events/sec01/invitedtalks/rooij.pdf) 2. [RFC 793 - Transmission Control Protocol](https://www.rfc-editor.org/rfc/rfc793) 3. [RFC 1323 - TCP Extensions for High Performance](https://www.rfc-editor.org/rfc/rfc1323) 最后修改:2026 年 01 月 18 日 © 允许规范转载 赞 如果觉得我的文章对你有用,请随意赞赏