2024-07-24-struct-group

__struct_group

浏览IPv6代码时候,看到这样一个新玩法,
__struct_group

1
2
3
4
5
6
7
8
9
118 struct ipv6hdr {
...
132 __u8 hop_limit;
133
134 __struct_group(/* no tag */, addrs, /* no attrs */,
135 struct in6_addr saddr;
136 struct in6_addr daddr;
137 );
138 };

###用法
再看一下用法:

 922 /* copy IPv6 saddr & daddr to flow_keys, possibly using 64bit load/store
 923  * Equivalent to :      flow->v6addrs.src = iph->saddr;
 924  *                      flow->v6addrs.dst = iph->daddr;
 925  */
 926 static inline void iph_to_flow_copy_v6addrs(struct flow_keys *flow,
 927                                             const struct ipv6hdr *iph)
 928 {
 ...
 932         memcpy(&flow->addrs.v6addrs, &iph->addrs, sizeof(flow->addrs.v6addrs));
 ...
 
1
2
3

###

11 /** 12 * __struct_group() - Create a mirrored named and anonyomous struct 13 * 14 * @TAG: The tag name for the named sub-struct (usually empty) 15 * @NAME: The identifier name of the mirrored sub-struct 16 * @ATTRS: Any struct attributes (usually empty) 17 * @MEMBERS: The member declarations for the mirrored structs 18 * 19 * Used to create an anonymous union of two structs with identical layout 20 * and size: one anonymous and one named. The former's members can be used 21 * normally without sub-struct naming, and the latter can be used to 22 * reason about the start, end, and size of the group of struct members. 23 * The named struct can also be explicitly tagged for layer reuse, as well 24 * as both having struct attributes appended. 25 */ 26 #define __struct_group(TAG, NAME, ATTRS, MEMBERS...) \ 27 union { \ 28 struct { MEMBERS } ATTRS; \ 29 struct TAG { MEMBERS } ATTRS NAME; \ 30 } ATTRS ```

IPv6: how to support IPv6 ext header

概述

与IPv4的几点不同

IPv6在扩展头和协议处理上跟IPv4还有些不一样。

  1. IPv6报文格式设计上,上层协议和扩展头都作为IPv6的nexthdr类型串联在一起,不像IPv4那样扩展头是单独的option,上层协议类型放到ip头里的proto字段。
  2. IPv6的扩展头大部分采用tlv格式,大部分扩展头前几个字节会保存nexthdr,本扩展的长度这两个信息,然后跟本扩展头相关的一些数据。
  3. 我们无法像IPv4那样通过IP头里的字段就能简洁的判读出报文四层协议类型以及4层协议的offer和lenth等信息。必须逐个解析全部的扩展头。

原理

协议栈通过一个inet6_protocol类型的数组,保存IPv6所有的4层处理协议入口。

1
2
struct inet6_protocol __rcu *inet6_protos[MAX_INET_PROTOS] __read_mostly;
EXPORT_SYMBOL(inet6_protos)
  1. 数组inet6_protos的下标对应的就是每个扩展协议在IPv6扩展头里的nexthdr

inet6_protocol结构体

  1. struct inet6_protocol结构体里的handler是扩展头的处理入口函数。
  2. flags字段有两个标志位: INET6_PROTO_NOPOLICYINET6_PROTO_FINAL
  • INET6_PROTO_FINAL: 这个扩展是否可以作为IPv6的最后一个扩展, 比如
    TCP可以, 但是IPPROTO_DSTOPTS不可以。
  • INET6_PROTO_NOPOLICY: 这个要求必须有对应的IPsec/xfrm XFRM_POLICY_IN的规则
    这里有个疑问, esp6和ah6 为什么也有这个标志位, 我理解不应该有这个标志位, 有esp和ah头的,必须要有xfrm规则才可。
1
2
3
4
5
6
7
8
9
10
11
12
13
53 struct inet6_protocol {
54 int (*handler)(struct sk_buff *skb);
55
56 /* This returns an error if we weren't able to handle the error. */
57 int (*err_handler)(struct sk_buff *skb,
58 struct inet6_skb_parm *opt,
59 u8 type, u8 code, int offset,
60 __be32 info);
61
62 unsigned int flags; /* INET6_PROTO_xxx */
63 u32 secret;
64 };
65
1
2
66 #define INET6_PROTO_NOPOLICY    0x1
67 #define INET6_PROTO_FINAL 0x2

函数调用栈

1
2
3
4
5
6
7
==> ipv6_rcv
==> ==> ip6_rcv_finish
==> ==> ==> dst_input(skb); <== ip6_input
==> ==> ==> ==> ip6_input
==> ==> ==> ==> ==> ip6_input_finish
==> ==> ==> ==> ==> ==> ip6_protocol_deliver_rcu(net, skb, 0, false);
==> ==> ==> ==> ==> ==> ==> ipprot = rcu_dereference(inet6_protos[`nexthdr`]);

注册和注销

1
2
3
4
5
6
7
8
9
10
11
52 struct inet6_protocol {
53 int (*handler)(struct sk_buff *skb);
54
55 /* This returns an error if we weren't able to handle the error. */
56 int (*err_handler)(struct sk_buff *skb,
57 struct inet6_skb_parm *opt,
58 u8 type, u8 code, int offset,
59 __be32 info);
60
61 unsigned int flags; /* INET6_PROTO_xxx */
62 };
1
2
3
4
5
28 int inet6_add_protocol(const struct inet6_protocol *prot, unsigned char protocol)
29 {
30 return !cmpxchg((const struct inet6_protocol **)&inet6_protos[protocol],
31 NULL, prot) ? 0 : -1;
32 }
1
2
3
4
5
6
7
8
9
10
11
35 int inet6_del_protocol(const struct inet6_protocol *prot, unsigned char protocol)
36 {
37 int ret;
38
39 ret = (cmpxchg((const struct inet6_protocol **)&inet6_protos[protocol],
40 prot, NULL) == prot) ? 0 : -1;
41
42 synchronize_net();
43
44 return ret;
45 }

内核总共支持的IPv6四层协议类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
➜  linux git:(master) grep inet6_add_protocol net include -Rw
net/dccp/ipv6.c: err = inet6_add_protocol(&dccp_v6_protocol, IPPROTO_DCCP);
net/l2tp/l2tp_ip6.c: err = inet6_add_protocol(&l2tp_ip6_protocol, IPPROTO_L2TP);
net/sctp/ipv6.c: if (inet6_add_protocol(&sctpv6_protocol, IPPROTO_SCTP) < 0)
net/ipv6/udplite.c: ret = inet6_add_protocol(&udplitev6_protocol, IPPROTO_UDPLITE);
net/ipv6/xfrm6_protocol.c: if (inet6_add_protocol(netproto(protocol), protocol)) {
net/ipv6/exthdrs.c: ret = inet6_add_protocol(&rthdr_protocol, IPPROTO_ROUTING);
net/ipv6/exthdrs.c: ret = inet6_add_protocol(&destopt_protocol, IPPROTO_DSTOPTS);
net/ipv6/exthdrs.c: ret = inet6_add_protocol(&nodata_protocol, IPPROTO_NONE);
net/ipv6/ip6mr.c: if (inet6_add_protocol(&pim6_protocol, IPPROTO_PIM) < 0) {
net/ipv6/udp.c: ret = inet6_add_protocol(&net_hotdata.udpv6_protocol, IPPROTO_UDP);
net/ipv6/ip6_gre.c: err = inet6_add_protocol(&ip6gre_protocol, IPPROTO_GRE);
net/ipv6/reassembly.c: ret = inet6_add_protocol(&frag_protocol, IPPROTO_FRAGMENT);
net/ipv6/protocol.c:int inet6_add_protocol(const struct inet6_protocol *prot, unsigned char protocol)
net/ipv6/protocol.c:EXPORT_SYMBOL(inet6_add_protocol);
net/ipv6/tcp_ipv6.c: ret = inet6_add_protocol(&net_hotdata.tcpv6_protocol, IPPROTO_TCP);
net/ipv6/icmp.c: if (inet6_add_protocol(&icmpv6_protocol, IPPROTO_ICMPV6) < 0)
net/ipv6/tunnel6.c: if (inet6_add_protocol(&tunnel6_protocol, IPPROTO_IPV6)) {
net/ipv6/tunnel6.c: if (inet6_add_protocol(&tunnel46_protocol, IPPROTO_IPIP)) {
net/ipv6/tunnel6.c: inet6_add_protocol(&tunnelmpls6_protocol, IPPROTO_MPLS)) {
include/net/protocol.h:int inet6_add_protocol(const struct inet6_protocol *prot, unsigned char num);
➜ linux git:(master)

IPPROTO_ROUTING 字段处理

1
2
3
4
835 static const struct inet6_protocol rthdr_protocol = {
836 .handler = ipv6_rthdr_rcv,
837 .flags = INET6_PROTO_NOPOLICY,
838 };
1
ret = inet6_add_protocol(&rthdr_protocol, IPPROTO_ROUTING);

TCP协议处理

1
2
3
4
5
6
2406         net_hotdata.tcpv6_protocol = (struct inet6_protocol) {
2407 .handler = tcp_v6_rcv,
2408 .err_handler = tcp_v6_err,
2409 .flags = INET6_PROTO_NOPOLICY | INET6_PROTO_FINAL,
2410 };
2411 ret = inet6_add_protocol(&net_hotdata.tcpv6_protocol, IPPROTO_TCP);

IPV6-in-IPv4 tunnel协议处理

1
2
3
4
5
6
7
8
9
10
11
12
13
239 static const struct inet6_protocol tunnel6_protocol = {
240 .handler = tunnel6_rcv,
241 .err_handler = tunnel6_err,
242 .flags = INET6_PROTO_NOPOLICY|INET6_PROTO_FINAL,
243 };
...

257 static int __init tunnel6_init(void)
258 {
259 if (inet6_add_protocol(&tunnel6_protocol, IPPROTO_IPV6)) {
...

52 IPPROTO_IPV6 = 41, /* IPv6-in-IPv4 tunnelling */

ebpf如何访问skb 的fileds

ebpf如何访问skb 的fileds

  • 加载:
  1. 转换: 所有skb的fields都转换成相对skb结构体头部的偏移量
  2. 根据偏移量重新校验 bpf指令
  • 报文运行:
  1. Skb的地址在skb是作为ctx寄存器传递给bfp run函数的。

BPF CTX与SKB

这里以tcpdump(PF_PACKET)为例,结合函数调用关系说明,
skb是如何被当做ctx参数传递给bpf程序的

注: 内核版本v6.6

函数调用关系

1
2
3
4
5
6
7
8
9
10
11
12
--> packet_rcv
--> --> run_filter(skb, sk, snaplen)
--> --> --> bpf_prog_run_clear_cb
--> --> --> --> bpf_prog_run_pin_on_cpu(prog, skb); <== !!! skb作为第二个参数ctx传递
--> --> --> --> --> bpf_prog_run(prog, ctx);
--> --> --> --> --> --> __bpf_prog_run(prog, ctx, bpf_dispatcher_nop_func);
--> --> --> --> --> --> --> dfunc(ctx, prog->insnsi, prog->bpf_func);
dfun是__bpf_prog_run被调用时候的参数,相当于
bpf_dispatcher_nop_func(ctx, prog->insnsi, prog->bpf_func);
--> --> --> --> --> --> --> --> bpf_func(ctx, insnsi);
bpf_func是bpf_dispatcher_nop_func被调用时候最后一个参数,相当于
prog->bpf_func(ctx, prog->insnsi)

Read More

协议栈是如何调用xdp程序处理报文的

函数调用栈

以xdp SKB模式为例,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
--> bpf_prog_run_generic_xdp
--> --> bpf_prog_run_xdp
--> --> --> u32 act = __bpf_prog_run(prog, xdp, BPF_DISPATCHER_FUNC(xdp));
展开BPF_DISPATCHER_FUNC(xdp), 相当于
u32 act = __bpf_prog_run(prog, xdp, bpf_dispatcher_xdp_func));
--> --> --> --> ret = dfunc(ctx, prog->insnsi, prog->bpf_func);
这里的dfun是`__bpf_prog_run`的第三个参数,因此相当于
ret = bpf_dispatcher_xdp_func(ctx, prog->insnsi, prog->bpf_func); <== 这里的第三个函数就是我们之前提到的,当bpf程序被加载时候,在bpf_prog结构体保存的bpf_func。
--> --> --> --> --> return __BPF_DISPATCHER_CALL(name); <== 根据bpf_dispatcher_xdp_func的定义
展开__BPF_DISPATCHER_CALL,相当于
bpf_func(ctx, insnsi) <=== **这里很有意思**,bpf_fun, ctx, insnsi分别代表bpf_dispatcher_xdp_func的三个函数入口参数,
根据顺序依次为 第三个,第一个,第二个,按照这个顺序展开
prog->bpf_func(ctx, prog->insnsi); <== 至此,就把我们上面一节里总结的`prog->bpf_func`这个函数指针用上了。
最终这个函数根据不同stacksize入口函数的包装,调用到
--> --> --> --> --> --> ___bpf_prog_run(ctx, prog->insnsi)

Read More

bfp 在内核运行的核心入口函数及其变形

bpf prog内核运行核心入口函数

总结:___bpf_prog_run

bfp 在内核运行的核心入口函数:___bpf_prog_run
___bpf_prog_run是bfp的核心函数入口,该函数被多个不同stack size的函数调用。
函数指针数组interpreters这把上面的这些函数汇集到一起。
当bpf程序被加载到内核时候,内核创建为它一个bpf_prog结构体,根据prog的stacksize,选择对应的interpreters里的对应的
函数,并保存到bpf_prog里的bpf_func上。
这样后续hook点运行bpf_prog程序时候,就使用bpf_func运行。

Read More

xdp 是如何加载到内核并运行的

XDP framework

xdp在内核里的有三个关键步骤:

  • load: 加载到内核
  • attach: 绑定到一个网口
  • run:网口收包时候,调用并执行bpf prog

load加载: 通过ebpf系统调用, 把prog加载到内核

fd = sys_bpf(BPF_PROG_LOAD, attr, size);

  • 在内核里创建一个bfp_prog结构体用以存储bpf prog。
  • 通过bpf_check检查prog程序的安全性和合法性。
  • 通过bpf_prog_select_runtime指定bpf prog对应的执行函数
  • 这个函数指针保存在bpf_func这个字段里。这里的function最终指向通用的bfp run函数___bpf_prog_run
    关于___bpf_prog_run这个具体封装和实现见另外一篇文章。

attach绑定: 将prog程序绑定到一个特定的网口的struct net_device

libpf函数do_attach将上一步加载在内核里的prog跟一个网口绑定, 具体实现是通过下发netlink命令。
这是个generic类型的netlink命令,最终通过dev_change_xdp_fd将prog挂载到对应netdev下面。

Read More

how tcpdump work with vlan filter

  • 问题描述:
    tcpdump在网口抓包和读取pcap文件,相同的filter表达式”icmp“,对vlan报文有不同的处理结果。
    在网口上抓包可以看到vlan和非vlan两种流量, 而读取pcap只能看到非vlan流量一种流量。

  • 原因:
    kernel里在tcpdump抓包之前会把报文的vlan提前解析掉,并把vlan信息放到skb的metadata里了。
    所以tcpdump(内核的af_packet对应的ptype_all)处理的报文都是不带vlan的,因此这两类报文都会被过滤出来。
    而读取pcap时候,因为vlan报文并没有被剥离掉,所以vlan报文不满足过滤条件,被丢弃了。

国外已经有人发现并分析过这个问题
https://andreaskaris.github.io/blog/networking/bpf-and-tcpdump/

Read More

xfrm: configure xfrm state and policy with iproute2

测试环境

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ip netns exec ns1 ip l s veth1 down
ip l delete veth0
ip netns delete ns1

ip l add type veth
ip netns add ns1

ip l s dev veth1 netns ns1
ip l set veth0 up
ip netns exec ns1 ip l set veth1 up

ip a a dev veth0 192.168.100.1/24
ip netns exec ns1 ip a a dev veth1 192.168.100.2/24
ip netns exec ns1 ip r a default via 192.168.100.1

Read More

fib: how ipv4 lookup route with rule route

概述

规则路由(rule route)

这部分详见ip rule命令帮助手册,
https://man7.org/linux/man-pages/man8/ip-rule.8.html
简要总结:

  • 支持多种形式的路由查找,不再仅仅局限于根据目的地址查找一种模式。
  • rule route采用了类似match-action模式, 不过rule route称之为SELECTORACTION
  • SELECTOR 支持多种形式,比如IP、PORT、进出网口、tos以及非操作(not)
  • ACTION 中几个重要的类型:
    • table:到指定的TABLE_ID对应的路由表里查找(所以这里要求必须支持多table)
    • nat: 支持IP地址nat
    • goto: 跳转到指定的rule route,通过这个可以做成多级级联。

几点说明

优先级

rule route是支持优先级的。 通过 ip rule show命令我们可以看到每条rule对应的优先级。添加rule时候,默认的是当前除0以外最高优先级的值-1, 即默认新建的rule优先级高。

Read More