说人话读论文--实时有状态 TCP 数据包过滤在 IP Filter 中的实现

  • Real Stateful TCP Packet Filtering in IP Filter 论文精读

  • 资料来源:

  • 更新

    1
    2024.11.17 初始

导语

OVS 实现用户态 conntrack tcp 状态追踪来源于 Real Stateful TCP Packet Filtering in IP Filter, 这篇 blog 其阅读笔记, 接下来应该还有对 ovs conntrack-tcp.c 的详细分析 ^i2ly

摘要

旧 ip filter 过滤引擎的 3 个问题

  • 始终假定通过的数据包一定能到达目的地

([[Real Stateful TCP Packet Filtering in Ip-filter .pdf#page=1&selection=64,0,69,38&color=yellow|Real Stateful TCP Packet Filtering in Ip-filter , p.1]])
The main problem being that IP Filter assumed, when it detected packets traversing through it, that the destination would also see the packets

  • 对称的处理了窗口大小

([[Real Stateful TCP Packet Filtering in Ip-filter .pdf#page=1&selection=70,44,72,14&color=yellow|Real Stateful TCP Packet Filtering in Ip-filter , p.1]])
Furthermore, it previously looked at window sizes symmetrically:

  • 数据有效 (seq) 范围,错误的使用了当前窗口大小作为上下界
    • 已确认序号 1000, window 500: 正确上下界 [1000, 1000+500], 这里错误的取了下界: [1000 - 500, 1000+500]
    • 没有理由这样做.
  • 始终使用最后一次看到的 window 尺寸处理 之后的 新数据

([[Real Stateful TCP Packet Filtering in Ip-filter .pdf#page=1&selection=79,22,81,35&color=yellow|Real Stateful TCP Packet Filtering in Ip-filter , p.1]])
Additionally, the window size taken as the upper bound for new data was the last window advertisement seen.

  • 例如最后一个 pkt 信息是: 窗口从 5000 -> 300
  • 但是在窗口变化之前的 pkt 有丢失 -> 重传, 此时 在 [300, 5000] 范围的重传包可能被判定为非法

新 ip filter 引擎, 解决了上述问题,主要设计标准是 只接收验证后的信息从不做假设.

([[Real Stateful TCP Packet Filtering in Ip-filter .pdf#page=1&selection=85,7,87,44&color=yellow|Real Stateful TCP Packet Filtering in Ip-filter , p.1]])
he main design criteria was to never assume anything, but to only take information for granted when it can be proven to be correct.

但是为了实现设计和保持向前兼容, 在一些标准上会放宽, 论文中有详细讨论;

旧过滤引擎

旧的 IP Filter 的状态过滤器, 精简代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/*
* 找出上次检查的数据包和这个数据包之间的差异.
*/
seq = ntohl(tcp->th_seq); // 序号
ack = ntohl(tcp->th_ack); // 确认号
source = (ip->ip_src.s_addr == is->is_src.s_addr); // 方向
if (source) { // source 和 dst 取值是反着来的
seqskew = seq - is->is_seq;
ackskew = (ack - 1) - is->is_ack;
} else {
seqskew = (ack - 1) - is->is_seq;
ackskew = seq - is->is_ack;
}
/*
* 使偏差值为绝对值
*/
if (seqskew < 0)
seqskew = -seqskew;
if (ackskew < 0)
ackskew = -ackskew;
/*
* 如果序列和确认号的差异在连接的窗口大小内,
* 存储这些值并匹配数据包.
*/
win = ntohs(tcp->th_win); // 取窗口大小
// 如果 seqskew 在 s-c 窗口内 and ackskew 在 c-s 窗口内
if ((seqskew <= is->is_dwin) && (ackskew <= is->is_swin)) {
/* 数据包匹配状态条目 */
if (source) { // c-s
is->is_seq = seq;
is->is_ack = ack;
if (win != 0) is->is_swin = win;
} else { // s-c
is->is_seq = ack;
is->is_ack = seq;
if (win != 0)
is->is_dwin = win;
}
do statistics;
set timeout values;
permit packet to pass;
}
/* 数据包不匹配状态条目 */
deny packet to pass;

每个连接有一个跟踪条目 is, 从源创建记录有 4 个数据:

  • is_seq: c-s 方向最后一个 seq OR s-c 反向最后一个 ack
  • is_ack: c-s 方向最后一个 ack OR s-c 方向的最后一个 seq
  • is_swin: c-s 方向的 win
  • is_dwin: s-c 方向的 win

以 c-s 方向看

  • 计算当前 pkt 的 seq ack 与 上一个 c-s 方向 pkt 的 seq ack 的差值 seqskew ackskew, 取绝对值.
  • seqskew 比较上次有效的 c-s 方向的 server 的接收窗口, ackskew 比较 s-c 方向的 client 的接收窗口. 两者同时成立则 pkt 合法.

([[Real Stateful TCP Packet Filtering in Ip-filter .pdf#page=5&selection=294,0,310,12&color=yellow|Real Stateful TCP Packet Filtering in Ip-filter , p.5]])
Both examples show that the state engine is not coping well with out of order packets and packet loss.
旧引擎无法很好的处理乱序和丢包

旧过滤引擎最大问题: 当出现 tcp 乱序/丢包时, filter 的状态不再与 client server 同步,一步错步步错.

问题示例

来看两个例子, 前提条件

  • A->B 已经建立连接
  • 不考虑分片等情况

Example 1

B→A 确认 (A→B, 0:1000) , win 1048 ack 1000 发生了 网络延迟, 其原本应该是 第 3 个包, 延迟到了第 7 个包

FromContentNr
B→Awin 2048 ack 01
A→B0:10002
B→Awin 1048 ack 10007
A→B1000:20003
B→Awin 2048 ack 20004
A→B2000:30005
B→Awin 2048 ack 30006

旧过滤引擎中状态的变化 (状态条目值是在 数据包 判定后,更新 is_seq is_ack 之前的值)
#todo

nrstate entrypacket contentcode
is_seqis_ackis_swinis_dwinseqackwinseqskewackskew
1n/an/an/an/a02048--
20204800
30204810001000
41000204820002048999
52000204820000
62000204830002048999
730002048100010482001

此时发送 8a 或 8b(重传原来的 6 号 pkt), 8a 8b 都是合法的数据包.

A→B3000:40008a
B→Awin 2048 ack 30008b
nrstate entrypacket contentcode
is_seqis_ackis_swinis_dwinseqackwinseqskewackskew
8a1000104830002000
8b10001048300020481999

8a 8b 对应的状态表:

  • 8a 的 seqskew = 2000 大于 is_dwin 的 1048,非法.
  • 8b 的 seqskew = 1999 大于 1048 , 非法

Example2

假定第 2 3 个 ack 出现了网络延迟

FromContentNr
A→B0:10001
B→Awin 4000 ack 10002
A→B1000:20003
A→B2000:30004
A→B3000:40005
A→B4000:50006
B→Awin 2000 ack 50008
B→Awin 4000 ack 50007
目前为止所有的包都会通过过滤器 ( seqskew < is_dwin )
nrstate entrypacket contentcode seqskew
is_seqis_dwinseqackwin
1not relevant0-
2010004000999
31000400010000
41000400020001000
52000400030001000
63000400040001000
74000400050004000999
850004000500020001

A 发现 [1000:2000] 还未确认, 触发重传, 很不幸 4000 > 2000 重传包被阻止

A→B1000:20009
nrstate entrypacket contentcode seqskew
is_seqis_dwinseqackwin
95000200010004000

新过滤引擎


所谓过滤过程/有状态防火墙 等等, 可以简化思考:

  • 输入: pkt 的 方向 + seq ack win
  • 输出: pkt 合法 or 非法
  • pkt 合法条件: seq 在窗口内 AND ack 在窗口内
  • 难点在于: 如何确认 seq / ack 合法的上下界, 不至于过于宽泛/紧张 导致错判 漏判.
  • 有时理论可能不得不让步于实现

([[Real Stateful TCP Packet Filtering in Ip-filter .pdf#page=6&selection=4,0,5,38&color=yellow|Real Stateful TCP Packet Filtering in Ip-filter , p.6]])
Never assume anything: the state administration should only be based on facts.
永远不能假定条件, 状态管理只基于事实

  • 旧的状态引擎就是假定所有包都能送达才出现了 bug.

有效的数据边界 (seq)

对于 A->B 方向 seq 的限制

上界

A -> B 发送了一个数据包 区间是 [s, s+n), 那么按照 tcp 协议

A>B数据包中的最后一个字节A被允许发送的最大字节A->B 数据包中的最后一个字节 ≤ A 被允许发送的最大字节

代入 s+n

s+nA可能发送的最大字节+1maxB发送的数据包A看到的{ack+win}s+n \leq \text{A可能发送的最大字节} + 1 \leq \max_{\substack{\text{B发送的数据包} \\ \text{A看到的}}} \{ \text{ack} + \text{win} \}

同时所有 A-B 的数据包都会经过 F 过滤系统

s+nmaxB发送的数据包F看到的{ack+win}(Ia)s+n \leq \max_{\substack{\text{B发送的数据包} \\ \text{F看到的}}} \{ \text{ack} + \text{win} \} \tag{Ia}

对于公式 (Ia) 只有一种例外, 但 B 通告接收窗口 win = 0, 此时 A 进入窗口探测模式, A 向 B 发送第一个未确认的数据, 这是唯一允许发送超出 B 接收窗口边界的情况.[1]

BSD 系统上总是使用 1 字节的 pkt 进行 0 窗口探测.那么公式 (Ia) -> (I)

s+nmaxB发送的数据包F看到的{ack+max(win, 1)}(I)s+n \leq \max_{\substack{\text{B发送的数据包} \\ \text{F看到的}}} \{ \text{ack} + \max(\text{win, 1}) \} \tag{I}

下界

理论下界 s 的最小值

smaxB发送的数据包A看到的{ack}(i)s \geq \max_{\substack{\text{B发送的数据包} \\ \text{A看到的}}} \{ \text{ack} \} \tag{i}

公式 (I) (i) 并列,代入 s 的最大值 可以得到 s+n 的最大值的约束 公式 (ii)

maxA发送的数据包{s+n}maxB发送的数据包A看到的{ack}+maxB发送的数据包A看到的{max(win, 1)}(ii)\max_{\text{A发送的数据包}} \{ s+n \} \leq \max_{\substack{\text{B发送的数据包} \\ \text{A看到的}}} \{ \text{ack} \} + \max_{\substack{\text{B发送的数据包} \\ \text{A看到的}}} \{ \max(\text{win, 1}) \} \tag{ii}

公式 (i) (ii)

smaxB发送的数据包A看到的{ack}maxA发送的数据包{s+n}maxB发送的数据包A看到的{max(win, 1)s \geq \max_{\substack{\text{B发送的数据包} \\ \text{A看到的}}} \{ \text{ack} \} \geq \max_{\text{A发送的数据包}} \{ s+n \} - \max_{\substack{\text{B发送的数据包} \\ \text{A看到的}}} \{ \max(\text{win, 1})

smaxA发送的数据包{s+n}maxB发送的数据包A看到的{max(win, 1)}maxA发送的数据包F看到的{s+n}maxB发送的数据包F看到的{max(win, 1)}(II)\Rightarrow s \geq \max_{\text{A发送的数据包}} \{ s+n \} - \max_{\substack{\text{B发送的数据包} \\ \text{A看到的}}} \{ \max(\text{win, 1}) \} \geq \max_{\substack{\text{A发送的数据包} \\ \text{F看到的}}} \{ s+n \} - \max_{\substack{\text{B发送的数据包} \\ \text{F看到的}}} \{ \max(\text{win, 1}) \} \tag{II}

smaxA发送的数据包F看到的{s+n}maxB发送的数据包F看到的{max(win, 1)}(II)\Rightarrow s \geq \max_{\substack{\text{A发送的数据包} \\ \text{F看到的}}} \{ s+n \} - \max_{\substack{\text{B发送的数据包} \\ \text{F看到的}}} \{ \max(\text{win, 1}) \} \tag{II}

有效的 确认边界 (ack)

对于 A->B 方向 ack 值的限制

上界

非常明确 ack 不能确认发送方没有发送的数据, 假设 A 是接收, B 发送那么 公式 (III)

amaxB发送的数据包A看到的{s+n}maxB发送的数据包F看到的{s+n}(III)a \leq \max_{\substack{\text{B发送的数据包} \\ \text{A看到的}}} \{ s+n \} \leq \max_{\substack{\text{B发送的数据包} \\ \text{F看到的}}} \{ s+n \} \tag{III}

下界

首先不能以 最后收到的 ack 作为下限:

  • tcp 乱序抵达, ack 10 和 ack 11 ^gqaj
  • ack 11 先到, ack 10 就会被误判非法

那么放宽下限制呢?

{amaxA发送的数据包F看到的{ack}确认数据有效(根据公式(i)(ii)) 就无需校验 ack 了\left\{ \begin{array}{l} a \geq \max_{\substack{\text{A发送的数据包} \\ \text{F看到的}}} \{ \text{ack} \} \\ \text{确认数据有效(根据公式(i)(ii)) 就无需校验 ack 了} \end{array} \right.

  • 规则 F 看到的 A 最大的 ack 作为下界, 出现 tcp 乱序 时,仍然会有 ack 被阻止.
  • 另一条, 数据有效就忽略 ack 检查, 完全没必要.

规则需要进一步放宽 -> 取到一个最小的下界 = 公式 (IV)

amaxB发送的数据包F看到的{s+n}MAXACKWINDOW(IV)a \geq \max_{\substack{\text{B发送的数据包} \\ \text{F看到的}}} \{ s+n \} - \text{MAXACKWINDOW} \tag{IV}

  • A->B 方向的 ack 最小值必须大于 F 看到的 B 发送数据包的 s+n - MAXACKWINDOW,
  • MAXACKWINDOW 是一个略大于 tcp 最大窗口 (66000) 的值.

这样的好处:

  • 不会错误将 乱序 ack 判定为非法
  • F 判定 ack 的窗口可以相当大. 当然这样会造成 接收到过期的 ack 的问题, 但是过期 ack 不会对 tcp 连接造成任何影响.
    • 已经确认到 ack 100 了, 突然一个因为网络拥堵来了个 ack10, 到就到吧, 不会产生任何影响.

总结

s+nmaxB发送的数据包F看到的{ack+max(win, 1)}(I)s+n \leq \max_{\substack{\text{B发送的数据包} \\ \text{F看到的}}} \{ \text{ack} + \max(\text{win, 1}) \} \tag{I}

smaxA发送的数据包F看到的{s+n}maxB发送的数据包F看到的{max(win, 1)}(II)s \geq \max_{\substack{\text{A发送的数据包} \\ \text{F看到的}}} \{ s+n \} - \max_{\substack{\text{B发送的数据包} \\ \text{F看到的}}} \{ \max(\text{win, 1}) \} \tag{II}

amaxB发送的数据包F看到的{s+n}(III)a \leq \max_{\substack{\text{B发送的数据包} \\ \text{F看到的}}} \{ s+n \} \tag{III}

amaxB发送的数据包F看到的{s+n}MAXACKWINDOW(IV)a \geq \max_{\substack{\text{B发送的数据包} \\ \text{F看到的}}} \{ s+n \} - \text{MAXACKWINDOW} \tag{IV}

实现

1
2
3
4
5
6
7
8
9
10
11
12
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;

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

初始化

显然 说人话读论文–实时有状态 TCP 数据包过滤在 IP Filter 中的实现#总结 是在 tcp 传输建立后的中间过程成立, tcp 建立连接时还需要特殊处理:

([[Real Stateful TCP Packet Filtering in Ip-filter .pdf#page=9&selection=0,9,2,31&color=yellow|Real Stateful TCP Packet Filtering in Ip-filter , p.9]])
The possibilities for the next packet in this session are retransmission of the SYN and the receiver sending a SYN/ACK.

  • 首个 syn 包, syn 包亦可能发生重传.
  • 首个 syn-ack 包, 其也可能发生重传
    为初始化给出特定值, 使其符合边界条件 I II III IV.

重传 Syn

1
2
3
4
5
6
ts_data[0].td_end = SEQ + 1
ts_data[0].td_maxend = SEQ + 1
ts_data[1].td_end = 0
ts_data[1].td_maxend = 0
ts_data[1].td_maxwin = 1
ts_data[0].td_maxwin = max(WIN)

此时发生了 syn 重传

1
2
3
4
5
6
7
8
9
10
// 对于 I II 重传 syn 自然符合边界条件
s+n = SEQ + 1
s = SEQ
s+n <= ts_data[0].td_maxend // (I)
s >= ts_data[0].td_end - ts_data[1].td_maxwin// (II)

// syn 重传包没有 ack 标志, 那就默认 反方向的 ack = 0 好了, 也符合边界条件 III IV
a = 0
a <= ts_date[1].td_end // (III)
a >= ts_data[1].td_end - MAXACKWINDOW // (IV)

接收方 Syn-ack

1
2
ts_data[1].td_end = SEQ + 1
ts_data[1].td_maxend = SEQ + 1
1
2
3
4
5
6
7
8
// s+n = SEQ + 1
// s = SEQ
// a = ACK
// 全部符合边界条件
s+n <= ts_data[1].td_maxend // (I)
s >= ts_data[1].td_end - ts_data[0].td_maxwin// (II)
a <= ts_date[0].td_end // (III)
a >= ts_data[0].td_end - MAXACKWINDOW // (IV)

([[Real Stateful TCP Packet Filtering in Ip-filter .pdf#page=9&selection=139,1,142,39&color=yellow|Real Stateful TCP Packet Filtering in Ip-filter , p.9]])
he above analysis is correct for connections that enter the state table when being setup.
上述分析仅在这个连接开始到结束 filter 全程没有重启. 每一步 filter 连接表状态都是正确.

filter 重启,历史状态都没了, 完蛋了. 如果 filter 重启且需要保留已有会话? 需要以某种方式保留历史状态

  • 始终记录状态,重启就恢复.
  • 先让其通过, 然后随着连接一步步重建历史状态.

([[Real Stateful TCP Packet Filtering in Ip-filter .pdf#page=10&selection=0,0,13,10&color=yellow|Real Stateful TCP Packet Filtering in Ip-filter , p.10]])
Neither of these methods have been implemented yet
两者都没有实现 2333

^bx7v

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// syn 包
// 计算第一个包的序列号结束位置
is->is_tcp.ts_data[0].td_end = ntohl(tcp->th_seq) + ip->ip_len -
fin->fin_hlen - (tcp->th_off << 2) +
((tcp->th_flags & TH_SYN) ? 1 : 0) +
((tcp->th_flags & TH_FIN) ? 1 : 0);

// 其他字段
// 设置发送方向的最大结束位置
is->is_tcp.ts_data[0].td_maxend = is->is_tcp.ts_data[0].td_end;
// 接收方向初始化为0
is->is_tcp.ts_data[1].td_end = 0;
is->is_tcp.ts_data[1].td_maxend = 0;
// 接收窗口最小为1
is->is_tcp.ts_data[1].td_maxwin = 1;
// 发送窗口取自包头
is->is_tcp.ts_data[0].td_maxwin = ntohs(tcp->th_win);
if (is->is_tcp.ts_data[0].td_maxwin == 0)
is->is_tcp.ts_data[0].td_maxwin = 1;

// 状态匹配代码的核心实现
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; // 假设ACK值
}
// 边界检查和状态更新
if ((SEQ_GE(fdata->td_maxend, end)) && // 上界检查
(SEQ_GE(seq, fdata->td_end - maxwin)) && // 下界检查
(ackskew >= -MAXACKWINDOW) && // ACK范围检查
(ackskew <= MAXACKWINDOW)) {

// 更新窗口
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++;
}
}

未来工作

([[Real Stateful TCP Packet Filtering in Ip-filter .pdf#page=13&selection=347,32,359,41&color=yellow|Real Stateful TCP Packet Filtering in Ip-filter , p.13]])
For connections involving the TCP window scale option [RFC1323], the results are thus incorrect
目前尚未支持 tcp 窗口缩放

  • 这一点在 ovs 中已经实现了

([[Real Stateful TCP Packet Filtering in Ip-filter .pdf#page=13&selection=364,6,367,23&color=yellow|Real Stateful TCP Packet Filtering in Ip-filter , p.13]])
also the timestamp option is to be taken into account so that the state engine will be able to handle wrapped sequence numbers within high speed connections.
需要考虑支持 tcp 时间戳

([[Real Stateful TCP Packet Filtering in Ip-filter .pdf#page=14&selection=0,0,2,14&color=yellow|Real Stateful TCP Packet Filtering in Ip-filter , p.14]])
Of course, sessions that enter the state table, when they are already established, should be handled better
更好的处理 filter 重启情况

([[Real Stateful TCP Packet Filtering in Ip-filter .pdf#page=14&selection=4,13,8,11&color=yellow|Real Stateful TCP Packet Filtering in Ip-filter , p.14]])
the workarounds in the implementation for dealing with fragments should be eliminated.
处理 IP 分片


  1. https://www.rfc-editor.org/rfc/rfc793#section-1.5 ↩︎