需求来了:需要在 XDP 里拦截已经建立的 tcp 连接,不是 DROP,而是要回 rst 包。

快速搜索一下,居然没有使用 XDP 来 reset tcp 连接的例子,只好亲自解决了。

解决办法

以为简单地处理一下:

  1. 对调一下 ether 头里的 h_desth_source 字段;
  2. 对调一下 ip 头里的 daddrsaddr 字段;
  3. 重新计算一下 ip 头的校验和;
  4. 对调一下 tcp 头里的 dportsport 字段;
  5. 重新计算一下 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(&eth_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 部分:

  1. 交换 ether 头里的 h_desth_source 字段;
  2. 更新 ip 头里的字段;
  3. 更新 tcp 头里的字段;
  4. 计算 tcp 头的校验和。

更新 ip 头里的字段

在上述代码片段里,忽略了 IP options 的处理,直接将 iph->ihl 设置为 5,表示没有 IP options。

在更新了 saddr, daddr, tot_len, frag_off, ttl, protocolcheck 字段后,重新计算了 iph->check 的值。

更新 tcp 头里的字段

先对调 sourcedest 字段。

seq 设置为原来的 ack_seqack_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 包如下:

TCP RST

源代码

完整源代码,请看 learn-by-example xdp-reset-tcp-conn

总结

在 XDP 里 reset tcp 连接,主要是对 etheriptcp 头部进行修改:

  1. 交换 ether 头里的 h_desth_source 字段;
  2. 更新 ip 头里的字段,对调 daddrsaddr 字段;
  3. 更新 tcp 头里的字段,对调 dportsport 字段;
  4. 计算 tcp 头的校验和时,需要向 ip 头部借用一个伪头部的空间。