OVS conntrack-tcp 源码阅读

  • OVS 项目 conntrack-tcp.c 源码阅读记录

  • 资料来源:

    https://github.com/openvswitch/ovs

  • 更新

    1
    2024.11.23 初始

导语

上一篇的 flag 填坑; 先总结下 Real Stateful TCP Packet Filtering in IP Filter 的精华

  • 合理推到出了对 seq / seq +n / ack 的正确上下界约束 -> 总结
  • 算法复杂度 O(1) 非常有利于实现

ovs 实现了用户态的 conntrack 实现了类似 kernel 的 conntrack 的能力, 完整追踪每个连接, 因此这一套 conntrack 可以被移植用于 防火墙/NAT 等程序中, 久经考验.

  • 又见 cloudflare 的文章…😶‍🌫️

这一篇是 conntrack-tcp.c 的详细分析, 以代码为主, 可能比较抽象;

前日谈

conntrack(connection tracking) 是网络协议栈中的一个组件, 主要用于跟踪网络连接的状态.常用于防火墙 NAT 和负载均衡等网络功能中扮演着关键角色.

conntrack 能够记录并维护每个网络连接的状态信息,包括源地址 目标地址 端口号以及协议类型等, 这使其所在的实体能够做出更智能的网络决策:

  • 允许或阻止特定的数据包
  • 实现更精确的网络控制和安全策略

在 OVS(Open vSwitch)中, conntrack 的为虚拟网络环境提供了连接跟踪能力.

Ovs Conntrack

ovs conntrack 的代码集中在 lib/conntrack-*

  • conntrack.h: 模块入口
  • conntrack-private.h: 私有数据结构函数等
    • conntrack.c: conntrack.h / conntrack-private.h 的实现
  • conntrack-tp.h/conntrack-tp.c: 超时时间相关
  • conntrack-icmp.c / conntrack-tcp.c / conntrack-other.c: icmp icmp6 / tcp / other 的连接追踪实现.

每个 connect 被抽象为 struct conn

  • conn 主键是 struct conn_key_node [] 数组, 对应 orign 和 reply 两个反向的 struct conn_key
  • 每个 struct conn_key 相当于是 5 元组, struct ct_endpoint 真正的 ip:port
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
struct ct_endpoint {
union ct_addr addr; // ip 地址
union {
ovs_be16 port; // 端口
struct {
ovs_be16 icmp_id;
uint8_t icmp_type;
uint8_t icmp_code;
};
};
};

// 五元组
struct conn_key {
struct ct_endpoint src;
struct ct_endpoint dst;

ovs_be16 dl_type;
uint16_t zone;
uint8_t nw_proto;
};

struct conn_key_node {
enum key_dir dir; // 方向
struct conn_key key; // 五元组
struct cmap_node cm_node;
};

struct conn {
/* Immutable data. */
struct conn_key_node key_node[CT_DIRS]; // 主键数组[2]
struct conn_key parent_key; /* Only used for orig_tuple support. */
uint16_t nat_action;
char *alg;
atomic_flag reclaimed; /* False during the lifetime of the connection,
* True as soon as a thread has started freeing
* its memory. */
xxxx
};

与协议有关的 conn 操作被抽象为了 struct ct_l4_proto 指针数组, tcp icmp ohter 各自实现.

  • new_conn 新建 conn
  • valid_new 验证 pkt 是否是新 conn 开始
  • conn_update 更新 conn
  • conn_get_protoinfo: 可以忽略
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct ct_l4_proto {
struct conn *(*new_conn)(struct conntrack *ct, struct dp_packet *pkt,
long long now, uint32_t tp_id);
bool (*valid_new)(struct dp_packet *pkt);
enum ct_update_res (*conn_update)(struct conntrack *ct, struct conn *conn,
struct dp_packet *pkt, bool reply,
long long now);
void (*conn_get_protoinfo)(const struct conn *,
struct ct_dpif_protoinfo *);
};

extern struct ct_l4_proto ct_proto_tcp;
extern struct ct_l4_proto ct_proto_other;
extern struct ct_l4_proto ct_proto_icmp4;
extern struct ct_l4_proto ct_proto_icmp6;
  • other 相当于就是对 UDP 的处理, 除了 icmp icmp6 tcp 外都这样处理: 没有连接状态, 可以多路复用, 代码实现非常简单.

以 tcp 为例, 实际的 conn_tcp = struct conn + 拖车

1
2
3
4
5
struct conn_tcp
{
struct conn up;
struct tcp_peer peer[2]; /* 'conn' lock protected. */
};

接下来会专注在 conntrack-tcp 的实现上

Conntrack-tcp

与追踪有关的集中在 tcp_new_conn tcp_conn_update 两个函数中, 这两个函数均是操纵 struct conn_tcp 这个数据结构.

1
2
3
4
5
6
struct ct_l4_proto ct_proto_tcp = {
.new_conn = tcp_new_conn,
.valid_new = tcp_valid_new,
.conn_update = tcp_conn_update,
.conn_get_protoinfo = tcp_conn_get_protoinfo,
};

数据结构 struct conn_tcp

  • 一个 conn_tcp 包含了两个方向的状态 struct tcp_peer
  • struct tcp_peer 则包含了单个方向的所有状态, 包括了窗口缩放状态;
    • seqlo: 公式 II IV
    • seqhi: 公式 I
    • max_win: 公式 I II
    • state tcp 状态机定义的 tcp 状态
1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct tcp_peer
{
uint32_t seqlo; /* Max sequence number sent */
uint32_t seqhi; /* Max the other end ACKd + win */
uint16_t max_win; /* largest window (pre scaling) */
uint8_t wscale; /* window scaling factor */
enum ct_dpif_tcp_state state;
};

struct conn_tcp
{
struct conn up;
struct tcp_peer peer[2]; /* 'conn' lock protected. */
};

enum ct_dpif_tcp_state: X 宏, 定义了全部的 tcp 的状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// https://github.com/openvswitch/ovs/blob/dc7663f13ce73e69e2983af77a0342f216467b31/lib/ct-dpif.h#L80
#define CT_DPIF_TCP_STATES \
CT_DPIF_TCP_STATE(CLOSED) \
CT_DPIF_TCP_STATE(LISTEN) \
CT_DPIF_TCP_STATE(SYN_SENT) \
CT_DPIF_TCP_STATE(SYN_RECV) \
CT_DPIF_TCP_STATE(ESTABLISHED) \
CT_DPIF_TCP_STATE(CLOSE_WAIT) \
CT_DPIF_TCP_STATE(FIN_WAIT_1) \
CT_DPIF_TCP_STATE(CLOSING) \
CT_DPIF_TCP_STATE(LAST_ACK) \
CT_DPIF_TCP_STATE(FIN_WAIT_2) \
CT_DPIF_TCP_STATE(TIME_WAIT) \
CT_DPIF_TCP_STATE(MAX_NUM)

enum OVS_PACKED_ENUM ct_dpif_tcp_state {
#define CT_DPIF_TCP_STATE(STATE) CT_DPIF_TCPS_##STATE,
CT_DPIF_TCP_STATES
#undef CT_DPIF_TCP_STATE
};

tcp_new_conn tcp_conn_update 两个函数就是正确的更新 struct conn_tcp 的各种标志和状态.

tcp_new_conn

先来看下 tcp_new_conn, 这里直接上代码了…其函数实现非常清晰

  • 有可能进入 tcp_new_conn 的 pkt 通常是 tcp 的首个 syn 包, 但是代码中也对非 syn 包做了处理
    • 非 syn 包将状态标记为未知
  • 正常的 syn 包初始化各种状态 窗口缩放等
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
// tcp 类型的新连接
static struct conn *
tcp_new_conn(struct conntrack *ct, struct dp_packet *pkt, long long now,
uint32_t tp_id)
{
struct conn_tcp *newconn = NULL;
struct tcp_header *tcp = dp_packet_l4(pkt);
struct tcp_peer *src, *dst;
uint16_t tcp_flags = TCP_FLAGS(tcp->tcp_ctl);

newconn = xzalloc(sizeof *newconn); // 申请内存

src = &newconn->peer[0];
dst = &newconn->peer[1];

src->seqlo = ntohl(get_16aligned_be32(&tcp->tcp_seq)); // max: seq
src->seqhi = src->seqlo + dp_packet_get_tcp_payload_length(pkt) + 1; // max: ack + win

if (tcp_flags & TCP_SYN) // syn 包
{
src->seqhi++; // ack + win 最大值+1
src->wscale = tcp_get_wscale(tcp); // 取窗口缩放因子
}
else
{ // 其他情况, 理论上很少
src->wscale = CT_WSCALE_UNKNOWN;
dst->wscale = CT_WSCALE_UNKNOWN; // 窗口缩放 状态皆为未知
}
src->max_win = MAX(ntohs(tcp->tcp_winsz), 1); // 源端窗口大小
if (src->wscale & CT_WSCALE_MASK) // 源端窗口缩放因子
{
/* Remove scale factor from initial window */
uint8_t sws = src->wscale & CT_WSCALE_MASK; // 窗口缩放因子
src->max_win = DIV_ROUND_UP((uint32_t)src->max_win, 1 << sw s); // 源端窗口大小移位
}
if (tcp_flags & TCP_FIN) // fin 包
{
src->seqhi++; // ack + win 最大值+1
}
dst->seqhi = 1; // 对端 seqhi 初始化为 1
dst->max_win = 1; // 对端窗口大小初始化为 1
src->state = CT_DPIF_TCPS_SYN_SENT; // 源端状态
dst->state = CT_DPIF_TCPS_CLOSED; // 对端状态

newconn->up.tp_id = tp_id; // tp_id
conn_init_expiration(ct, &newconn->up, CT_TM_TCP_FIRST_PACKET, now); // 超时时间

return &newconn->up;
}

流程图渲染后就是比较丑…尝试优化 N 次就是没有满意的, 不影响信息表达, 暂时不改了.

flowchart TD
    A[开始 tcp_new_conn] --> B[分配内存 newconn]
    B --> C[设置源端和目的端指针]
    C --> D[设置源端初始序列号 seqlo]
    D --> E[计算源端序列号上限 seqhi]
    
    E --> F{是否为 SYN 包?}
    F -->|是| G[源端 seqhi++]
    G --> H[获取源端窗口缩放因子]
    
    F -->|否| I[设置双方窗口缩放状态为未知]
    
    H --> J[设置源端最大窗口大小]
    I --> J
    
    J --> K{窗口缩放因子是否有效?}
    K -->|是| L[根据缩放因子调整窗口大小]
    K -->|否| M[继续处理]
    
    L --> N{是否为 FIN 包?}
    M --> N
    
    N -->|是| O[源端 seqhi++]
    N -->|否| P[继续处理]
    
    O --> Q[初始化目的端参数]
    P --> Q
    
    Q --> R[设置连接状态]
    R --> S[设置 tp_id]
    S --> T[初始化超时时间]
    T --> U[返回连接对象]

tcp_conn_update

注意: 能够进入 tcp_conn_update 流程的已经不是首个包了, conn 已经被创建了

tcp_conn_update 实在是其处理细节太多了…依次划分为了 4 个流程

  • 基础检查和 SYN 包处理
  • 序列号追踪和窗口计算
  • 状态更新逻辑
  • 特殊情况处理

基础检查和 SYN 包处理

flowchart TD
    A[开始] --> B{检查TCP flags是否合法}
    B -->|不合法| C[返回CT_UPDATE_INVALID]
    B -->|合法| D{是否是SYN包}
    D -->|是| E{检查连接状态}
    E -->|双端>=FIN_WAIT_2| F[设置双端为CLOSED<br>返回CT_UPDATE_NEW]
    E -->|src<=SYN_SENT| G[设置src为SYN_SENT<br>更新超时时间<br>返回CT_UPDATE_VALID_NEW]
    D -->|否| H[继续处理]

这部分主要是处理 tcp_conn_update 处理 syn 包的情况

  • 当 conn 收个包不是 syn 包, syn 包非进入 tcp_conn_update ^3xaz
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
// 注意: 这个函数是在 conn 已存在以后才会调用; 通常单独的 syn 包回调用到 tcp_new_conn 而不是这里
static enum ct_update_res
tcp_conn_update(struct conntrack *ct, struct conn *conn_,
struct dp_packet *pkt, bool reply, long long now)
{
struct conn_tcp *conn = conn_tcp_cast(conn_);
struct tcp_header *tcp = dp_packet_l4(pkt);
/* The peer that sent 'pkt' */
struct tcp_peer *src = &conn->peer[reply ? 1 : 0];
/* The peer that should receive 'pkt' */
struct tcp_peer *dst = &conn->peer[reply ? 0 : 1];
uint8_t sws = 0, dws = 0;
uint16_t tcp_flags = TCP_FLAGS(tcp->tcp_ctl);

uint16_t win = ntohs(tcp->tcp_winsz);
uint32_t ack, end, seq, orig_seq;
uint32_t p_len = dp_packet_get_tcp_payload_length(pkt); // tcp 包长度

if (tcp_invalid_flags(tcp_flags))// 确保不是非法的 TCP flags
{
COVERAGE_INC(conntrack_invalid_tcp_flags);
return CT_UPDATE_INVALID;
}

if ((tcp_flags & (TCP_SYN | TCP_ACK)) == TCP_SYN) // syn 包
{
if (dst->state >= CT_DPIF_TCPS_FIN_WAIT_2 && src->state >= CT_DPIF_TCPS_FIN_WAIT_2)
{ // 原连接已接近关闭情况下重连
src->state = dst->state = CT_DPIF_TCPS_CLOSED; // 清理原连接
return CT_UPDATE_NEW;
}
else if (src->state <= CT_DPIF_TCPS_SYN_SENT) // 其他情况, conn 状态小于等于 SYN_SENT
{ // 可能是重复的 syn 包, 也可能是新连接
src->state = CT_DPIF_TCPS_SYN_SENT;
conn_update_expiration(ct, &conn->up, CT_TM_TCP_FIRST_PACKET, now);
return CT_UPDATE_VALID_NEW;
}
}

序列号追踪和窗口计算

flowchart TD
    A[计算序列号和窗口] --> B{是否是首次处理该方向}
    B -->|是| C[初始化序列号追踪]
    C --> D[处理窗口缩放]
    D --> E[计算seq_hi]
    B -->|否| F[常规序列号计算]
    F --> G[处理特殊情况的ACK]
    G --> H[无数据包特殊处理]
    H --> I[计算ackskew]

取窗口缩放 这里也被 首个包非 syn 这种情况影响到了;

流程进入首次处理该方向时, 对于 新建或非稳定状态的连接 关闭了 严格 ack 检查 (check_ackskew = false)

非首次处理这个方向时的 ack 特殊情况

  • pkt 干脆没有 ack, 直接让其通过检查 (ack = dst->seqlo;)
  • ack = 0. 但是设置了 ack 和 rst, 可能是一些协议栈实现中对 syn 超时发送 (ack = dst->seqlo;)
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
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
if (src->wscale & CT_WSCALE_FLAG && dst->wscale & CT_WSCALE_FLAG && !(tcp_flags & TCP_SYN)) 
{ // 窗口缩放是在 syn 包协商, 这里是已经协商完毕直接取值
sws = src->wscale & CT_WSCALE_MASK;
dws = dst->wscale & CT_WSCALE_MASK;
}
else if (src->wscale & CT_WSCALE_UNKNOWN && dst->wscale & CT_WSCALE_UNKNOWN && !(tcp_flags & TCP_SYN))
{ // 不是 syn 包 窗口缩放还没有协商. 可能触发创建连接建立的不是 syn 包
sws = TCP_MAX_WSCALE;
dws = TCP_MAX_WSCALE; // 保守的取最大值 确保不会限制连接
}

/*
* Sequence tracking algorithm from Guido van Rooij's paper:
* http://www.madison-gurkha.com/publications/tcp_filtering/
* tcp_filtering.ps
*/

orig_seq = seq = ntohl(get_16aligned_be32(&tcp->tcp_seq)); // seq
bool check_ackskew = true;
if (src->state < CT_DPIF_TCPS_SYN_SENT) // 比 SYN_SENT 状态还小, orign 方向第一个包, 初始化序列号追踪
{
/* First packet from this end. Set its state */

ack = ntohl(get_16aligned_be32(&tcp->tcp_ack)); // ack

end = seq + p_len; // seq + n
if (tcp_flags & TCP_SYN) // syn 包, 也就是首个包不是 syn 包
{
end++; // seq + n + 1
if (dst->wscale & CT_WSCALE_FLAG) // 对端设置了 窗口缩放
{
src->wscale = tcp_get_wscale(tcp); // tcp 可选项 提取窗口缩放
if (src->wscale & CT_WSCALE_FLAG) // orign 方向 设置窗口缩放?
{
/* Remove scale factor from initial window */
sws = src->wscale & CT_WSCALE_MASK; // 缩放因子
win = DIV_ROUND_UP((uint32_t)win, 1 << sws); // 窗口大小移位
dws = dst->wscale & CT_WSCALE_MASK; // 对端窗口缩放因子
}
else
{ // 没有窗口缩放
/* fixup other window */
dst->max_win <<= dst->wscale & CT_WSCALE_MASK;
/* in case of a retrans SYN|ACK */
dst->wscale = 0; // 对端窗口缩放因子清零
}
}
}
if (tcp_flags & TCP_FIN) // fin 包
{
end++; // end +1
}

src->seqlo = seq; // orign 方向最大 seq
src->state = CT_DPIF_TCPS_SYN_SENT; // orign 方向状态
/*
* May need to slide the window (seqhi may have been set by
* the crappy stack check or if we picked up the connection
* after establishment)
*/
// src->seqhi == 1 相当于是新连接
// SEQ_GEQ(end + MAX(1, dst->max_win << dws), src->seqhi) 当前包的结束序列号 + win > 源端最大的 seq, 需要更新 seqhi
if (src->seqhi == 1 || SEQ_GEQ(end + MAX(1, dst->max_win << dws), src->seqhi))
{
src->seqhi = end + MAX(1, dst->max_win << dws); // 更新 seqhi
/* We are either picking up a new connection or a connection which
* was already in place. We are more permissive in terms of
* ackskew checking in these cases.
*/
// 这个注释说明了为什么这里不严格检查 ack
// 新连接 或 已有连接但是需要更新 最大 seq 的; 这两种情况下 序列号尚不稳定. 因此 ackskew 检查不严格
check_ackskew = false; // 不严格检查 ack
}
if (win > src->max_win)
{
src->max_win = win; // orign 方向最大窗口
}
}
else
{
ack = ntohl(get_16aligned_be32(&tcp->tcp_ack)); // 取 ack
end = seq + p_len; // 取结束序列号
if (tcp_flags & TCP_SYN) // syn 包
{
end++; // seq + n + 1
}
if (tcp_flags & TCP_FIN) // fin 包
{
end++; // seq + n + 1 + 1
}
}

if ((tcp_flags & TCP_ACK) == 0) // 没有 ack or ack = 0
{
/* Let it pass through the ack skew check */
// 没有 ack , 作弊让其通过 ack 检查, 都没有检查个啥
ack = dst->seqlo;
}
// ack = 0. 但是设置了 ack 和 rst
else if ((ack == 0 && (tcp_flags & (TCP_ACK | TCP_RST)) == (TCP_ACK | TCP_RST))
/* broken tcp stacks do not set ack */)
{
/* Many stacks (ours included) will set the ACK number in an
* FIN|ACK if the SYN times out -- no sequence to ACK. */
// 一些堆栈(包括我们的)在 SYN 超时时会在 FIN|ACK 中设置 ACK 号码 -- 没有序列号来 ACK
// 用来处理一些现实的 tcp 协议栈实现
ack = dst->seqlo;
}

if (seq == end) // pkt 内没有有效数据
{
/* Ease sequencing restrictions on no data packets */
seq = src->seqlo;
end = seq;
}

int ackskew = check_ackskew ? dst->seqlo - ack : 0; // 不检查 ack 就取 0 , 检查就取 dst->seqlo - ack

状态更新逻辑

flowchart TD
    A[开始状态更新] --> B{序列号检查是否通过}
    B -->|通过| C[更新窗口和序列号]
    C --> D[状态转换处理]
    D --> E{检查SYN标志}
    E -->|有| F[更新SYN状态]
    D --> G{检查FIN标志}
    G -->|有| H[更新FIN状态]
    D --> I{检查ACK标志}
    I -->|有| J[更新ACK状态]
    D --> K{检查RST标志}
    K -->|有| L[设置TIME_WAIT]
    D --> M[更新连接超时时间]

正常连接状态更新最核心的部分;

这里的状态检查中的 seq 和 ack, 对应原论文公式 I II, 做了进一步拓展:

状态检测

  • 首先这里的 MAXACKWINDOW = 65535 + 1500 , 比原论文又 +1500 确保, ack 检查范围足够大
  • 检测 seq 对应公式 I
  • ack 回退不能超过 1 个 MAXACKWINDOW
  • ack 最大也不能超过 1 个 MAXACKWINDOW(带缩放)
  • rst 包下 seq 检测相当严格,只能在 ±1 范围内

之后的状态和超时时间更新, 严格按照 tcp 状态机处理.

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
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
#define MAXACKWINDOW (0xffff + 1500) /* 1500 is an arbitrary fudge factor */ // 在原论文最大窗口基础上再 +1500
if ((SEQ_GEQ(src->seqhi, end) // 对应公式 I
/* Last octet inside other's window space */
&& SEQ_GEQ(seq, src->seqlo - (dst->max_win << dws)) // 对应公式 II
/* Retrans: not more than one window back */
&& (ackskew >= -MAXACKWINDOW) // ack 最小也不应该小于 -MAXACKWINDOW | 最小不能先前回退一个 MAXACKWINDOW
/* Acking not more than one reassembled fragment backwards */
&& (ackskew <= (MAXACKWINDOW << sws)) // ack 最大也不应该大于 MAXACKWINDOW << sws(带窗口缩放) | 最大不能超过 MAXACKWINDOW(缩放后)
/* Acking not more than one window forward */
// rst 包? seq == 最大 send 过的最大 seq 或 seq == 最大 send 过的最大 seq + 1 或 seq + 1 == 最大 send 过的最大 seq
// rst 包的话 检查严格, 可能的话就是 +-1 范围内的检查
&& ((tcp_flags & TCP_RST) == 0 || orig_seq == src->seqlo || (orig_seq == src->seqlo + 1) || (orig_seq + 1 == src->seqlo))) ||
tcp_bypass_seq_chk(ct))
{
/* Require an exact/+1 sequence match on resets when possible */
// 全部成功, 那么可以开始更新状态了;
/* update max window */ // 源 最大窗口
if (src->max_win < win)
{
src->max_win = win;
}
/* synchronize sequencing */ // 源 seq
if (SEQ_GT(end, src->seqlo))
{
src->seqlo = end; //
}
/* slide the window of what the other end can send */
if (SEQ_GEQ(ack + (win << sws), dst->seqhi)) //
{
dst->seqhi = ack + MAX((win << sws), 1); // 对端窗口 (缩放) | 公式II
}

/* update states */
// syn 包 且 源状态 < SYN_SENT
if (tcp_flags & TCP_SYN && src->state < CT_DPIF_TCPS_SYN_SENT)
{
src->state = CT_DPIF_TCPS_SYN_SENT; // 源 syn 包已发送
}
// fin 包 且 源状态 < CLOSING
if (tcp_flags & TCP_FIN && src->state < CT_DPIF_TCPS_CLOSING)
{
src->state = CT_DPIF_TCPS_CLOSING; // 源 方向关闭中
}
// ack 包
if (tcp_flags & TCP_ACK)
{
if (dst->state == CT_DPIF_TCPS_SYN_SENT) // 对端状态为 SYN_SENT
{
dst->state = CT_DPIF_TCPS_ESTABLISHED; // 3次握手完成, 连接建立
}
else if (dst->state == CT_DPIF_TCPS_CLOSING) // 对端状态为 CLOSING, 等待确认关闭
{
dst->state = CT_DPIF_TCPS_FIN_WAIT_2; // 进入 fun_wait_2 状态
}
}
if (tcp_flags & TCP_RST) // rst 包
{
src->state = dst->state = CT_DPIF_TCPS_TIME_WAIT; // 双方均进入 超时
}
// 两端都已经处于 FIN_WAIT_2 状态 或 更高,
if (src->state >= CT_DPIF_TCPS_FIN_WAIT_2 && dst->state >= CT_DPIF_TCPS_FIN_WAIT_2)
{
conn_update_expiration(ct, &conn->up, CT_TM_TCP_CLOSED, now); // 可以关闭了, 更新超时时间
}
// 两端都处于 CLOSING 状态 或 更高
else if (src->state >= CT_DPIF_TCPS_CLOSING && dst->state >= CT_DPIF_TCPS_CLOSING)
{
conn_update_expiration(ct, &conn->up, CT_TM_TCP_FIN_WAIT, now); // 按照 FIN_WAIT 更新超时时间
}
// 任一方未完成连接建立
else if (src->state < CT_DPIF_TCPS_ESTABLISHED || dst->state < CT_DPIF_TCPS_ESTABLISHED)
{
conn_update_expiration(ct, &conn->up, CT_TM_TCP_OPENING, now); // 按照 OPENING 更新超时时间
}
// 任意一方开始关闭连接
else if (src->state >= CT_DPIF_TCPS_CLOSING || dst->state >= CT_DPIF_TCPS_CLOSING)
{
conn_update_expiration(ct, &conn->up, CT_TM_TCP_CLOSING, now); // 开始关闭的超时时间
}
else // 连接稳定状态
{
conn_update_expiration(ct, &conn->up, CT_TM_TCP_ESTABLISHED, now); //
}
}

特殊情况处理

flowchart TD
    A[特殊情况检查] --> B{是否满足特殊条件}
    B -->|是| C[保守更新窗口]
    C --> D[保守更新序列号]
    D --> E[处理FIN标志]
    E --> F[处理RST标志]
    B -->|否| G[序列号检查失败]
    G --> H[返回CT_UPDATE_INVALID]
    C --> I[返回CT_UPDATE_VALID]

处理完毕正常就该处理异常情况了, 有 3 种特殊网络行为

  1. 非标准 TCP 实现:在接收 ACK 之前重复发送 SYN
  2. 网络设备重启:如防火墙或 NAT 设备重启后重新追踪已建立的连接
  3. 连接关闭后的异常行为:如某些系统 (Solaris) 在连接关闭后发送额外的 ACK 或 FIN 包

这部分处理必须非常谨慎… 因为 ddos 的包也会进入这里的处理流程, 要限制其造成的影响.

这里会保守更新状态信息, 这里不更新 ttl 等.
对于特殊网络行为 对端会正常回复,然后之后流程就进入到正常状态更新了. 对于 ddos 流量 异常连接超时后直接自行关闭了, 其造成影响被降低到最低.

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
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
// 这会是一些特殊的情况了
// 对端还未发送 syn 或 对端已经处于 fin_wait_2 或更高 或 源端已经处于 fin_wait_2 或更高 且 pkt 的结束序号在窗口的 +方向还往后
else if ((dst->state < CT_DPIF_TCPS_SYN_SENT || dst->state >= CT_DPIF_TCPS_FIN_WAIT_2 || src->state >= CT_DPIF_TCPS_FIN_WAIT_2) && SEQ_GEQ(src->seqhi + MAXACKWINDOW, end)
/* Within a window forward of the originating packet */
// pkt 的结束序号在窗口 -方向还往前
&& SEQ_GEQ(seq, src->seqlo - MAXACKWINDOW))
{
/* Within a window backward of the originating packet */

/*
* This currently handles three situations:
* 1) Stupid stacks will shotgun SYNs before their peer
* replies.
* 2) When PF catches an already established stream (the
* firewall rebooted, the state table was flushed, routes
* changed...)
* 3) Packets get funky immediately after the connection
* closes (this should catch Solaris spurious ACK|FINs
* that web servers like to spew after a close)
*
* This must be a little more careful than the above code
* since packet floods will also be caught here. We don't
* update the TTL here to mitigate the damage of a packet
* flood and so the same code can handle awkward establishment
* and a loosened connection close.
* In the establishment case, a correct peer response will
* validate the connection, go through the normal state code
* and keep updating the state TTL.
*/
// 说了有 3 种特殊情况:
// 1. 一些古怪的 tcp 协议栈实现, 会在对端回复 ack 之前,重复发送 syn
// 2. 带 connect 实体(防火墙 nat表 ...)重启了, 重新对已建立的连接开始追踪
// 3. 连接关闭后的异常包, 例如 Solaris 会在关闭连接后继续发送 ack|fin 包
// 这部分代码必须非常谨慎... 因为 ddos 的包也会进入这里的处理流程
// 这里不更新 ttl 以减少 ddos 的影响, 异常连接超时后直接自行关闭了.

// 保守更新状态信息, 正常连接 对端会正常回复,然后之后流程就进入到正常状态更新了.
/* update max window */
if (src->max_win < win)
{
src->max_win = win; // 源端最大窗口
}
/* synchronize sequencing */
if (SEQ_GT(end, src->seqlo))
{
src->seqlo = end; // 源端最大 seq
}
/* slide the window of what the other end can send */
if (SEQ_GEQ(ack + (win << sws), dst->seqhi))
{
dst->seqhi = ack + MAX((win << sws), 1); // 最大可接收的窗口范围更新
}

/*
* Cannot set dst->seqhi here since this could be a shotgunned
* SYN and not an already established connection.
*/
// 不能在这 设置 dst->seqhi, 因为这可能是一个重复的 syn 包, 而不是已经建立的连接
// 怪不得作者吐槽一些 tcp 协议栈实现...

if (tcp_flags & TCP_FIN && src->state < CT_DPIF_TCPS_CLOSING) // fin 包
{
src->state = CT_DPIF_TCPS_CLOSING; // 源端关闭中
}

if (tcp_flags & TCP_RST) // rst 包
{
src->state = dst->state = CT_DPIF_TCPS_TIME_WAIT; // 双方进入超时状态
}
}
else // 超过了 +- 一个最大窗口了, 直接扔了
{
COVERAGE_INC(conntrack_tcp_seq_chk_failed);
return CT_UPDATE_INVALID; // 丢弃, 更新连接状态失败
}

return CT_UPDATE_VALID; // 更新有效
}

与原论文对比

最后看一下 ovs 实现的 conntrack 与原论文的对比

ovs 实现了完整的 tcp 状态机, 借助 tcp 状态更加精确的判断 pkt 是否合法, 顺带设置更合适的超时时间.

1
2
3
4
struct tcp_peer {
// …
enum ct_dpif_tcp_state state;
}

添加了额外跳过 seq 和 ack 检查 机制, 这可能让其在实际中更加实用.

1
2
3
check_ackskew = false;

tcp_bypass_seq_chk(struct conntrack *ct)

将用于 ack 检查的 MAXACKWINDOW 又 +1500, 额外的容错

1
#define MAXACKWINDOW (0xffff + 1500)    /* 1500 is an arbitrary fudge factor */

支持了窗口缩放, 这一点很重要,对于跨洋线路,窗口缩放非常实用.

1
2
3
4
5
6
7
8
if (src->wscale & CT_WSCALE_FLAG && dst->wscale & CT_WSCALE_FLAG && !(tcp_flags & TCP_SYN)) {
sws = src->wscale & CT_WSCALE_MASK;
dws = dst->wscale & CT_WSCALE_MASK;
} else if (src->wscale & CT_WSCALE_UNKNOWN && dst->wscale & CT_WSCALE_UNKNOWN && !(tcp_flags & TCP_SYN)) {
sws = TCP_MAX_WSCALE;
dws = TCP_MAX_WSCALE;
}
// 还有其他的代码 省略

特殊网络行为的处理

  • 这里也对应 filter 重启 的实现: conntrack 的实体重启后如何追踪已建立连接.
1
2
3
4
5
else if ((dst->state < CT_DPIF_TCPS_SYN_SENT || dst->state >= CT_DPIF_TCPS_FIN_WAIT_2 || src->state >= CT_DPIF_TCPS_FIN_WAIT_2) && SEQ_GEQ(src->seqhi + MAXACKWINDOW, end)
&& SEQ_GEQ(seq, src->seqlo - MAXACKWINDOW))
{
// 特殊情况处理
}

结语

这篇是在 GPT 完整辅助下完成的, 具体是在 代码阅读完毕, 添加注释后, 尝试全程使用 GPT 辅助:

  • 更加以往 blog 进行章节划分;
  • 流程图划分 & 绘制: 这个节约了大量时间, 同等规模自行绘制至少需要额外 4H 才能完成.
  • 对草稿的校对, 名词 事实 表达等, 纠正了一些名词.

总体评估: 只在撰写环境, 相对于没有 GPT 节约时间在 8H 左右.

说实在的这篇欠稿了好久, 看看草稿箱,还有好多… 不看长远,只看 2 天, 继续调整,继续完成.