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 */

内核OVS的学习总结


OVS里最重要的几个元素:vport,flow,datapath。
其中datapth是vport和flow的桥梁。

kernel ovs 核心结构体及其关联

VPORT

内核包含多个datapath(brige),上面包含一个或者多个vport。
其中一个VPORT表示一个端口,一个vport只能归于一个特定的datapath。
每个vport有自己的type, 对应不同的vport ops.
每个内核网口被注册为vport的时候。

Read More

net rx drop

问题来源

接OP问题报告,监控到部分机器的net rx drop统计值异常,触发报警,
需排查具体原因,并确认是否影响业务。

问题分析

复现问题

跟OP同学确认,通过采集/proc/net/dev下的rx drop。
登录到出问题的机器上, 确认内核该统计值确实异常。

  • 异常报文个数不多,大约1s一个左右。
  • 不是所有机器都有异常,有部分机器drop统计为0.

相关内核代码

当一个数据报文经过对端设备(交换机或者网卡)传输到本机物理网卡时候,需要经过
网卡-- 网卡驱动-- 网络协议栈
这几个模块的处理。我们看到的drop统计值在增加,是在网络协议栈的入口处理部分产生异常导致。

Read More

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

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