使用 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 的结果:
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_NOP
、TCPOPT_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()
处理逻辑:
读取 opcode
。判断 opcode
是否为TCPOPT_EOL
或TCPOPT_NOP
。读取 opsize
。判断 opsize
是否小于 2;如果小于 2,遇到的是畸形 option。特殊处理 TCPOPT_TOA_AKAMAI
和TCPOPT_TOA_COMPAT
。查表 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;
}
}
其中:
option_parser()
是提供给freplace
的桩函数,用于解析 TCP option。for
循环遍历所有 TCP options,即使全部 option 都是TCPOPT_NOP
。length
为剩余需要解析的 TCP options 的数据长度。offset
为当前需要解析的 TCP option 相对于xdp->data
的偏移量。length <= 0
时表示已经解析完所有 TCP options。option_parser()
返回值小于等于 0 时表示解析结束,无需继续解析。
注意:option_parser()
不能使用 static
修饰,否则在加载 freplace
程序时 verifier 会报错。
总结
为了在 XDP 里解析所有的 TCP options,需要将遍历逻辑和解析逻辑分开,并将解析逻辑放到 freplace
程序里,从而规避 verifier 的限制。
源代码:learn-by-example tcpoptions[2]。
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