eBPF Talk: 使用 bpf 给流量设置 TOS
文章目录
给流量(特指 TCP 流量)打上 TOS,可以在 cgroup bpf sockops 的这 3 个 HOOK 里调 bpf_setsockopt(skops, SOL_IP, IP_TOS, &tos, sizeof(tos)) 给 socket 设置 TOS。
BPF_SOCK_OPS_TCP_CONNECT_CBBPF_SOCK_OPS_ACTIVE_ESTABLISHED_CBBPF_SOCK_OPS_PASSIVE_ESTABLISHED_CB
不过,同事最近遇到个问题:bpf_setsockopt() 返回了 -EINVAL 导致 TOS 没打上。
背景
同事需要按需给特定流量打上 TOS,从而更好地搞流量治理。
他分析发现,bpf_setsockopt() 失败的 socket 的 sk_family 是 AF_INET6,但 tcpdump 抓包却是 IPv4,觉得有点奇怪;经过一番分析,发现是内核协议栈的 IPv4 over IPv6 的特性,也就是 ipv4-mapped ipv6,内核代码里的 ipv6-mapped。
分析
看下 bpf_setsockopt() helper 的源代码就知道了:
|
|
sol_ip_sockopt() 里检查 sk->sk_family 是否为 AF_INET;对于 ipv4-mapped ipv6 socket,这里就会返回 -EINVAL。
修复
修起来并不难;只需要在这里放开 AF_INET6 的检查就好:
|
|
这里并不能简单地放行 AF_INET6:
SOCK_RAW: 参考ipv6_setsockopt()的做法:
|
|
ipv6_only_sock():tcp_v6_connect()里处理 ipv4-mapped ipv6 case 的逻辑:
|
|
翻看了一下 tcp_ipv6.c::tcp_v6_connect() 和 ipv6_sockglue.c::ipv6_setsockopt(),就确定了要这么改。
有趣的是,字节的 Feng Zhou 大佬在 2 年前就尝试修复这个问题;我在 tg 频道上发了个碎碎念,他看到后就聊了起来。
绕过
内核里修复起来并不复杂;可是,受到影响的线上机器有好多,全都要升级内核就不合适了。合适的做法是:热补丁、或者绕过。
其实,维护热补丁还挺麻烦的,就研究了一下绕过该问题的办法:如何给 ipv4-mapped ipv6 socket 设置 TOS?
用 AI 分析一下,它说通过用户态可以设置 TOS,并且给另一个进程的 socket 设置 TOS 也是可行的。
|
|
在这条路径上,确实没有 sk->sk_family != AF_INET 的检查。
给另一个进程设置 TOS 的核心逻辑:
int syscall(SYS_pidfd_open, pid_t pid, unsigned int flags);int syscall(SYS_pidfd_getfd, int pidfd, int targetfd, unsigned int flags);int setsockopt(int sockfd, int level, int optname, const void optval[.optlen], socklen_t optlen);
其中,pid 和 fd 信息可以通过 fexit __sys_socket 拿到:
|
|
使用 2 个 netns 来验证一下:
|
|
通过 tcpdump 抓包确认,tos 确实会被更新为指定的值;不过,前 2 个包没有设置成功。
该办法的缺陷:因为是异步设置 TOS 的,所以会导致新创的 socket 前几个包还没来得及设置 TOS。
该办法的 PoC 代码:tos-marker
小结
使用 tc-bpf 来补上 TOS?不可接受的原因是 tc-bpf 会处理每个经过 tc 的包;而基于 cgroup 的 sockops 是 socket 事件驱动的,性能损耗更低。
对于内核问题,能绕过则绕过;绕不过,优先考虑热补丁;最后才是升级内核。
文章作者 Leon Hwang
上次更新 2026-06-21