eBPF Talk: XDP 解析所有 TCP options

文摘   2024-08-19 08:10   新加坡  

使用 fentry 能解析到 TOA option,eBPF Talk: BPF 读取 TOA 的 4 种方式

上难度,如何使用 XDP 解析所有的 TCP options 呢?

太长不读:使用 freplace 解析 TCP option,使用 XDP 遍历所有 TCP options。

这儿为什么要用上 freplace 呢?因为对于 verifier 来说,解析 TCP option 的函数过于复杂;如果为了通过 verifier,就无法解析所有的 TCP options 了。

因而,将遍历 TCP options 的逻辑和解析 TCP option 的逻辑分开,从而规避 verifier 的限制。

效果

跑起来,看看效果再说:

$ sudo ./tcpoptions -i ens33
2024/08/17 14:53:11 Attached xdp(ens33)
2024/08/17 14:53:11 Check TCP options by `cat /sys/kernel/debug/tracing/trace_pipe` ..

# check log in another terminal window
$ sudo cat /sys/kernel/debug/tracing/trace_pipe | grep -e 'Scale' -e 'SACK' -e 'MSS'
          <idle>-0       [006] ..s21 270732.630866: bpf_trace_printk: topts: MSS: opsize(4), val: 0x5b4
          <idle>-0       [006] ..s21 270732.630894: bpf_trace_printk: topts: Window Scale: opsize(3), val: 6
          <idle>-0       [006] ..s21 270732.630896: bpf_trace_printk: topts: SACK Permitted: opsize(2)
# 0x5b4 = 1460

# make a new tcp connection to current machine in another terminal window in order to trigger the logs

对比 Wireshark 的结果:

Wireshark TCP Options

TCP options

先来看看内核支持哪些 TCP options:

// https://github.com/torvalds/linux/blob/master/include/net/tcp.h
/*
 *  TCP option
 */


#define TCPOPT_NOP              1       /* Padding */
#define TCPOPT_EOL              0       /* End of options */
#define TCPOPT_MSS              2       /* Segment size negotiating */
#define TCPOPT_WINDOW           3       /* Window scaling */
#define TCPOPT_SACK_PERM        4       /* SACK Permitted */
#define TCPOPT_SACK             5       /* SACK Block */
#define TCPOPT_TIMESTAMP        8       /* Better RTT estimations/PAWS */
#define TCPOPT_MD5SIG           19      /* MD5 Signature (RFC2385) */
#define TCPOPT_AO               29      /* Authentication Option (RFC5925) */
#define TCPOPT_MPTCP            30      /* Multipath TCP (RFC6824) */
#define TCPOPT_FASTOPEN         34      /* Fast open (RFC7413) */
#define TCPOPT_EXP              254     /* Experimental */
/* Magic number to be after the option value for sharing TCP
 * experimental options. See draft-ietf-tcpm-experimental-options-00.txt
 */

#define TCPOPT_FASTOPEN_MAGIC   0xF989
#define TCPOPT_SMC_MAGIC        0xE2D4C3D9

似乎不够全面,找找其它资料:IANA Transmission Control Protocol (TCP) Parameters[1]

整理一下,得到如下 TCP options:

struct tcp_option {
    __u8 opsize;
    char opname[35];
} __attribute__((packed)) tcp_options[] = {
    [TCPOPT_MSS]        = { TCPOLEN_MSS,        "MSS" },                                /* 2 */
    [TCPOPT_WINDOW]     = { TCPOLEN_WINDOW,     "Window Scale" },                       /* 3 */
    [TCPOPT_SACK_PERM]  = { TCPOLEN_SACK_PERM,  "SACK Permitted" },                     /* 4 */
    [TCPOPT_SACK]       = { TCPOLEN_MARK,       "SACK" },                               /* 5 */
    [6]                 = { 6,                  "Echo" },                               /* 6 */
    [7]                 = { 6,                  "Echo Reply" },                         /* 7 */
    [TCPOPT_TIMESTAMP]  = { TCPOLEN_TIMESTAMP,  "Timestamp" },                          /* 8 */
    [9]                 = { 2,                  "Partial Order Connection Permitted" }, /* 9 */
    [10]                = { 3,                  "Partial Order Service Profile" },      /* 10 */
    [14]                = { 3,                  "TCP Alternate Checksum Request" },     /* 14 */
    [15]                = { TCPOLEN_MARK,       "TCP Alternate Checksum Data" },        /* 15 */
    [18]                = { 3,                  "Trailer Checksum Option" },            /* 18 */
    [TCPOPT_MD5SIG]     = { TCPOLEN_MD5SIG,     "MD5 Signature Option" },               /* 19 */
    [27]                = { 8,                  "Quick-Start Response" },               /* 27 */
    [28]                = { 4,                  "User Timeout Option" },                /* 28 */
    [30]                = { TCPOLEN_MARK,       "Multipath TCP (MPTCP)" },              /* 30 */
    [34]                = { TCPOLEN_MARK,       "TCP Fast Open Cookie" },               /* 34 */
    [69]                = { TCPOLEN_MARK,       "Encryption Negotiation (TCP-ENO)" },   /* 69 */
    [172]               = { TCPOLEN_MARK,       "Acceptable ECN Order 0" },             /* 172 */
    [174]               = { TCPOLEN_MARK,       "Acceptable ECN Order 1" },             /* 174 */
    [253]               = { 8,                  "TOA" },                                /* 253 */
    [254]               = { 8,                  "TOA" },                                /* 254 */
    [255]               = {},                                                           /* 255 */
};

freplace 解析 TCP option

在解析 TCP option 时,需要按需去判断 packet range,因为可能会遇到 TCPOPT_NOPTCPOPT_EOL 等 option。

static __always_inline bool
__check(void *data, void *data_end, int length)
{
    return data + length <= data_end;
}

static int
parse_option(struct xdp_md *xdp, __u8 /* should not be __u32 */ offset)
{
    void *data = ctx_ptr(xdp, data) + offset;
    void *data_end = ctx_ptr(xdp, data_end);
    struct tcp_option *topt;
    __u8 opcode, opsize;

    if (!__check(data, data_end, 1))
        return -1;

    opcode = *(__u8 *) data;
    data++;

    switch (opcode) {
    case TCPOPT_EOL: /* 0 */
        return -1;

    case TCPOPT_NOP: /* 1 */
        return 1;
    }

    if (!__check(data, data_end, 1))
        return -1;

    opsize = *(__u8 *) data;
    data++;

    if (opsize < 2)
        return -1;

    if (opcode == TCPOPT_TOA_AKAMAI || opcode == TCPOPT_TOA_COMPAT) {
        if (opsize == 8) {
            if (!__check(data, data_end, 6))
                return -1;

            struct toa_data *toa = (struct toa_data *) (data - 2);
            bpf_printk("topts: TOA: port=%d ip=%pI4\n", bpf_ntohs(toa->port), &toa->ip);
            return 8;
        }
    }

    topt = &tcp_options[opcode];
    if (topt->opsize == 0) {
        bpf_printk("topts: unknown opcode(%d)\n", opcode);
    } else if (topt->opsize != TCPOLEN_MARK && opsize != topt->opsize) {
        bpf_printk("topts: %s: invalid opsize(%d), exp opsize(%d)\n",
                   topt->opname, opsize, topt->opsize);
    }

    switch (opsize) {
    case 2:
        if (topt->opname[0] != '\0')
            bpf_printk("topts: %s: opsize(%d)\n", topt->opname, opsize);
        break;

    case 2+1:
        if (__check(data, data_end, 1))
            bpf_printk("topts: %s: opsize(%d), val: %d\n",
                       topt->opname, opsize, *(__u8 *) data);
        break;

    case 2+2:
        if (__check(data, data_end, 2))
            bpf_printk("topts: %s: opsize(%d), val: 0x%x\n",
                       topt->opname, opsize, bpf_ntohs(*(__u16 *) data));
        break;

    case 2+4:
        if (__check(data, data_end, 4))
            bpf_printk("topts: %s: opsize(%d), val: 0x%x\n",
                       topt->opname, opsize, bpf_ntohl(*(__u32 *) data));
        break;

    case 2+8:
        if (__check(data, data_end, 8))
            bpf_printk("topts: %s: opsize(%d), val: 0x%llx\n",
                       topt->opname, opsize, bpf_be64_to_cpu(*(__u64 *) data));
        break;
    }

    return opsize;
}

其中,parse_option() 返回 -1 时表示解析结束,返回其它值时表示解析成功后得到的当前 option 的长度。

parse_option() 处理逻辑:

  1. 读取 opcode
  2. 判断 opcode 是否为 TCPOPT_EOLTCPOPT_NOP
  3. 读取 opsize
  4. 判断 opsize 是否小于 2;如果小于 2,遇到的是畸形 option。
  5. 特殊处理 TCPOPT_TOA_AKAMAITCPOPT_TOA_COMPAT
  6. 查表 tcp_options,打印 option 的名称、长度和值。

XDP 遍历所有 TCP options

遍历所有 TCP options 的逻辑比较简单:

__noinline int
option_parser(struct xdp_md *xdp)
{
    int ret = 0;

    barrier_var(ret);
    return xdp ? 1 : ret;
}

static void
__parse_options(struct xdp_md *xdp, struct tcphdr *tcph)
{
    int length = (tcph->doff << 2) - sizeof(struct tcphdr);

    __u32 *offset = get_buf();
    if (!offset)
        return;

    /* Initialize offset to tcp options part. */
    *offset = (void *) (tcph + 1) - ctx_ptr(xdp, data);;

    for (int i = 0; i < ((1<<4 /* bits number of doff */)<<2)-sizeof(struct tcphdr); i++) {
        if (length <= 0)
            break;

        int ret = option_parser(xdp);
        if (ret <= 0)
            break;

        *offset += ret;
        length -= ret;
    }
}

其中:

  1. option_parser() 是提供给 freplace 的桩函数,用于解析 TCP option。
  2. for 循环遍历所有 TCP options,即使全部 option 都是 TCPOPT_NOP
  3. length 为剩余需要解析的 TCP options 的数据长度。
  4. offset 为当前需要解析的 TCP option 相对于 xdp->data 的偏移量。
  5. length <= 0 时表示已经解析完所有 TCP options。
  6. option_parser() 返回值小于等于 0 时表示解析结束,无需继续解析。

注意:option_parser() 不能使用 static 修饰,否则在加载 freplace 程序时 verifier 会报错。

总结

为了在 XDP 里解析所有的 TCP options,需要将遍历逻辑和解析逻辑分开,并将解析逻辑放到 freplace 程序里,从而规避 verifier 的限制。

源代码:learn-by-example tcpoptions[2]

参考资料
[1]

IANA Transmission Control Protocol (TCP) Parameters: https://www.iana.org/assignments/tcp-parameters/tcp-parameters.xhtml

[2]

learn-by-example tcpoptions: https://github.com/Asphaltt/learn-by-example/tree/main/ebpf/tcpoptions

eBPF Talk
专注于 eBPF 技术,以及 Linux 网络上的 eBPF 技术应用