需求来了:需要在 XDP 里拦截已经建立的 tcp 连接,不是 DROP,而是要回 rst 包。
快速搜索一下,居然没有使用 XDP 来 reset tcp 连接的例子,只好亲自解决了。
解决办法
以为简单地处理一下:
- 对调一下
ether
头里的 h_dest
和 h_source
字段;
- 对调一下
ip
头里的 daddr
和 saddr
字段;
- 重新计算一下
ip
头的校验和;
- 对调一下
tcp
头里的 dport
和 sport
字段;
- 重新计算一下
tcp
头的校验和。
就可以了。
实则不然。
一个可行的 demo 代码如下:
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
|
volatile const __be16 DPORT = bpf_htons(65535);
struct pseudo_header {
__be32 src;
__be32 dst;
__u8 zero;
__u8 proto;
__be16 len;
};
SEC("xdp")
int xdp_fn(struct xdp_md *ctx)
{
struct ethhdr *eth = (struct ethhdr *)(ctx_ptr(ctx, data)), eth_tmp;
struct iphdr *iph = (struct iphdr *)(eth + 1);
struct tcphdr *tcph = (struct tcphdr *)(iph + 1);
__u8 buff[sizeof(struct pseudo_header)];
struct pseudo_header *psh;
__be32 saddr, daddr, seq;
int total_len, delta;
__u16 sport, dport;
__u8 *tcp_flags;
if ((void *) (tcph + 1) > ctx_ptr(ctx, data_end))
return XDP_PASS;
if (iph->protocol != IPPROTO_TCP)
return XDP_PASS;
if (tcph->dest != DPORT)
return XDP_PASS;
/* swap eth addrs */
__builtin_memcpy(ð_tmp, eth, sizeof(struct ethhdr) - 2);
__builtin_memcpy(eth->h_dest, eth_tmp.h_source, ETH_ALEN);
__builtin_memcpy(eth->h_source, eth_tmp.h_dest, ETH_ALEN);
/* update iph */
iph->ihl = 5;
saddr = iph->saddr;
daddr = iph->daddr;
iph->saddr = daddr;
iph->daddr = saddr;
total_len = bpf_ntohs(iph->tot_len);
iph->tot_len = bpf_htons(sizeof(struct iphdr) + sizeof(struct tcphdr));
iph->frag_off = 0;
iph->ttl = 64;
iph->protocol = IPPROTO_TCP;
iph->check = 0;
iph->check = ipv4_csum((void *)iph, sizeof(*iph));
/* update tcph */
sport = tcph->source;
dport = tcph->dest;
tcph->source = dport;
tcph->dest = sport;
seq = tcph->seq;
tcph->seq = tcph->ack_seq;
tcph->ack_seq = seq + bpf_htonl(0x1);
tcph->doff = sizeof(struct tcphdr) >> 2;
tcp_flags = (typeof(tcp_flags)) ((void *) tcph + offsetof(struct tcphdr, window) - 1);
*tcp_flags = (TCP_FLAG_ACK | TCP_FLAG_RST) >> 8;
/* calculate tcp checksum by referencing http://www.tcpipguide.com/free/t_TCPChecksumCalculationandtheTCPPseudoHeader-2.htm */
psh = (struct pseudo_header *) ((void *) tcph - sizeof(struct pseudo_header));
__builtin_memcpy(buff, psh, sizeof(struct pseudo_header));
psh->src = iph->saddr;
psh->dst = iph->daddr;
psh->zero = 0;
psh->proto = IPPROTO_TCP;
psh->len = bpf_htons(sizeof(struct tcphdr));
tcph->check = 0;
tcph->check = ipv4_csum(psh, sizeof(struct pseudo_header) + sizeof(struct tcphdr));
__builtin_memcpy(psh, buff, sizeof(struct pseudo_header));
delta = total_len - (sizeof(struct iphdr) + sizeof(struct tcphdr));
if (delta > 0)
bpf_xdp_adjust_tail(ctx, -delta);
return XDP_TX;
}
|
分为 4 部分:
- 交换
ether
头里的 h_dest
和 h_source
字段;
- 更新
ip
头里的字段;
- 更新
tcp
头里的字段;
- 计算
tcp
头的校验和。
更新 ip
头里的字段
在上述代码片段里,忽略了 IP options 的处理,直接将 iph->ihl
设置为 5,表示没有 IP options。
在更新了 saddr
, daddr
, tot_len
, frag_off
, ttl
, protocol
和 check
字段后,重新计算了 iph->check
的值。
更新 tcp
头里的字段
先对调 source
和 dest
字段。
seq
设置为原来的 ack_seq
,ack_seq
设置为原来的 seq + 1
;此处 + 1
处理的方式不够鲁棒,应该是 bpf_htons(bpf_ntohs(seq) + 1)
。
doff
设置为 sizeof(struct tcphdr) >> 2
,表示没有 TCP options。
tcp_flags
设置为 TCP_FLAG_ACK | TCP_FLAG_RST
,表示 ACK 和 RST 标志位都设置为 1。
计算 tcp
头的校验和
参考 TCP Checksum Calculation and the TCP “Pseudo Header”,计算 tcp
头的校验和。
其中,伪头部里的 len
字段设置为 sizeof(struct tcphdr)
,因为不带 TCP options 和 TCP payload。
计算校验和时,将伪头部和 tcp
头部一起计算。
因为计算校验和时需要用到伪头部,所以需要向 ip
头部借用一个伪头部的空间;在使用前,将伪头部的内容保存到 buff
里,使用完后再恢复。
效果展示
1
2
3
4
5
6
|
$ telnet 192.168.241.133 8080
Trying 192.168.241.133...
Connected to 192.168.241.133.
Escape character is '^]'.
GET /
Connection closed by foreign host.
|
XDP 回复的 reset 包如下:

源代码
完整源代码,请看 learn-by-example xdp-reset-tcp-conn。
总结
在 XDP 里 reset tcp 连接,主要是对 ether
、ip
和 tcp
头部进行修改:
- 交换
ether
头里的 h_dest
和 h_source
字段;
- 更新
ip
头里的字段,对调 daddr
和 saddr
字段;
- 更新
tcp
头里的字段,对调 dport
和 sport
字段;
- 计算
tcp
头的校验和时,需要向 ip
头部借用一个伪头部的空间。