How IPv6 addresses are flushed on link down

IPv6协议栈里, 当一个网口被down之后,网口上对应的IP地址也会一起被flush掉。
面对IPv6跟IPv4不同的行为方式,内核提供了一个规避的开关。
在4.6内核之后提供了一个开关,用来避免IPv6地址别清理掉。
这个开关既有全局的设置,也有每个interface粒度的单独开关。

1
2
3
4
5
6
7
keep_addr_on_down - INTEGER
Keep all IPv6 addresses on an interface down event. If set static
global addresses with no expiration time are not flushed.
>0 : enabled
0 : system default
<0 : disabled
Default: 0 (addresses are removed)

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流量一种流量

【Q1】:在读取pcap文件时候,为什么不能读取到vlan报文呢?

分析结论

内核协议栈:

  1. 过滤条件 icmp:tcpdump 解析出来的 ebpf 指令,是要求报文是eth+IP+ICMP格式。通过ETH/IP 头里的协议类型分别做了限制: IPv4(0x0800)+ICMP(1)。

  2. 在网口上抓包:内核代码里,在tcpdump抓包过滤的钩子函数之前,会把报文的vlan解析并剔除掉。被剥离的vlan头信息,被保存到skb的metadata里了。所以 tcpdump(af_packet)在内核里,通过钩子函数运行过滤条件的 ebpf 指令时,被处理的报文已经不带vlan头了。因此只要满足icmp 头,带和不带 vlan头的报文都会被抓取到。

  3. 读取pcap文件:tcpdump 直接读取文件内容,vlan头并没有被剥离掉,所以vlan报文不满足过滤条件(eth+IP+ICMP),被丢弃了。

国外已经有人发现并分析过这个问题:BPF and tcpdump

Read More

tcpdump and ebpf

以内核v6.6代码,介绍tcpdump程序如何与内核交互,加载bpf程序的。

加载:

libpcap通过sockopt里的“SO_ATTACH_FILTER“,
在 packet socket下的”sk_filter“挂载prog呈现。

运行:

当skb报文到达packet_rcv时候, 通过调用___bpf_prog_run函数(注意,这个函数是3个下滑线,区别于2个下划线的函数)
运行”sk_filter“对应的prog。
其中prog的
==>packet_rcv
==> ==> run_filter
==> ==> ==>bpf_prog_run_clear_cb

Read More

how tcpdump work with cbpf

tcpdump通过libpcap库以及内核的af_packet对数据包问题进行抓取。
关于这两部分的如何协作抓包,之前blog里已经写过。
这里主要记录分析,在ebpf之前的内核(以v3.0)如何处理tcpdump里的filter的。

filter编译后,如何加载到内核里的:

在filter被翻译为一系列的指令后,这个指令buff被libpcap,
通过sockopt里的SO_ATTACH_FILTE选项,
最终挂载到AF_PACKET socket下的sk_filter上。

Read More

tcpdump如何实现参数-报文抓取长度

用法

tcpdump -s len

原理

tcpdump会把这个长度值编译到ebpf代码中。 ebpf代码执行完后,如果filter成功,则作为返回值(return)返回给调用方(内核协议栈),内核协议栈根据返回值,修改 skb报文到指定的长度,并借助 af socket 返回给用户空间。

tcpdump命令解析

tcpdump snaplen参数: ebpf指令对比

Read More

linux内核多路由表与策略路由的实现

使用场景

接上篇文章,我们提到在网口eth1 上增加一个 ip 地址,内核会生成 3 个路由。

  • local 路由表:增加/32 主机路由
  • main 路由表:增加本网段前缀的网络路由

为什么需要使用local/main 两个路由表呢? ip地址引发的路由变化或者手动添加一个目的网段的路由,这些场景看起来,完全可以使用一个路由表就能满足场景需求。那内核为什么要支持多个路由表?

其中一个场景就是 策略路由
比如公司有两个网关, 其中一个网关GW1,网络质量比较高。我们希望对网络质量有较高要求的个别网段的外访流量,走GW1.其他网段都用 GW2.
这时,我们就不能仅仅依赖目前 IP 去做路由。我们需要:

  • 先根据源 IP 地址做一次筛选,选中的这部分流量,缺省路由到GW1.
  • 没有选中的走我们的正常路由查找,其中缺省路由到GW2。
  • 这两个缺省路由只能放到两个单独的路由表里。

那下面详细展开介绍下策略路由及内核实现。

策略路由

策略路由是一个按优先级排列的规则链表。路由查找时,按优先级顺序遍历链。

每个策略路由规则,由match+action两部分组成。 match 也被称为SELECTOR

  1. 每个规则的match条件,可以支持多种字段,如源IP/协议/协议源端口/目的端口等,也可以这多个字段的组合。如果满足规则匹配条件就执行规则指定的动作。
  2. 每个策略路由的action有下面几种:
    • table X:查找对应的路由表 table X。X 是 table ID 或者 table 名字。
    • goto xx:跳转到更低优先级到规则。
    • nat …:ip地址替换
  3. 所有的策略路由通过 IP rule 命令添加。
  4. 策略路由按照优先级插入到一个链表里。
  5. 按照优先级从高(数值最小)到低(数值大)按顺序排列。
  6. 通过 ip rule show命令我们可以看到每条rule对应的优先级。添加rule时候,默认的是当前除0以外最高优先级的值-1, 即默认新建的rule优先级高。

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

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