创建req scoket时的三个长度检查

创建半链接时的三个长度检查

在处理TCP-SYN首包时候, tcp_conn_request函数里, 会有三个不同条件的长度检查。

  • inet_csk_reqsk_queue_is_full 半链接的个数超过sk_max_ack_backlog, 则丢包。
  • sk_acceptq_is_full: accept 队列长度超过sk_max_ack_backlog,则丢包。
  • sysctl_tcp_syncookies禁用(值为0)时, sysctl_max_syn_backloginet_csk_reqsk_queue_len: 队列长度如果超过sysctl_max_syn_backlog的3/4则丢包

其中,

  • sysctl_max_syn_backlog: 初始化时,最小 128。如果 ehash_entries/128比 128 大,取最大值。
  • sysctl_tcp_syncookies: 初始值为 1

判断1: 半链接队列是否溢出 inet_csk_reqsk_queue_is_full

虽然不再维护半链接队列了, 但是每次创建req socket后,这个统计值都是在增加的。
因此如果队列长度超过了最大值sk_max_ack_backlog,则丢弃。

1
2
3
4
5
6
7
8
9
278 static inline int inet_csk_reqsk_queue_len(const struct sock *sk)
279 {
280 return reqsk_queue_len(&inet_csk(sk)->icsk_accept_queue);
281 }
282
283 static inline int inet_csk_reqsk_queue_is_full(const struct sock *sk)
284 {
285 return inet_csk_reqsk_queue_len(sk) > READ_ONCE(sk->sk_max_ack_backlog);
286 }

Read More

ifconfig通过别名给网口配置多个IP地址

内核如何管理 ip 地址

内核如何管理多网口多 IP 地址

命令行操作

有时候我们需要给一个网口配置的有个 IP 地址,这时候我们有两种配置方法:

  • 使用 ifconfig 配置到网口别名上。
  • 使用 ip addr 命令直接配置到网口上。

两种方法最终在内核实现是一样的,存储位置也一样,并且可以相互读写配置结果。

比如将9.9.9.199/24配置到 lo 口上,并起个别名lo:9

1
2
3
4
5
6
[root@VM-0-12-centos ~]# ifconfig  lo:9 9.9.9.199/24
[root@VM-0-12-centos ~]# ifconfig lo:9
lo:9: flags=73<UP,LOOPBACK,RUNNING> mtu 65536
inet 9.9.9.199 netmask 255.255.255.0
loop txqueuelen 1000 (Local Loopback)
[root@VM-0-12-centos ~]#

ifconfig命令的配置结果,也可以通用ip link命令来查看

1
2
3
4
5
6
7
8
9
[root@VM-0-12-centos ~]# ip a show dev lo
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet 9.9.9.199/24 scope global lo:9
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever

网口的别名在ip命令里被当做label输出,放在scope字段后面

Read More

网口状态标志位解析: part1

IP link命令结果里的网口状态标志位

我们先来看几个场景里,不同状态的网口,输出的标志位有哪些不一样。

正常状态下物理网口eth0 的输出

命令行ip link show dev eth0会输出网口eth0的一些状态标志位。
这里注意一个现象, ‘ifconfig’命令显示的结果里有’RUNNING’标志位, 但是’ip link’命令的结果里没有这个标志位。
图1: 正常网口的ip link 命令和ifconfig命令结果

场景2:拔掉物理口的网线后, 网口的状态变化。

网口状态标志位里有个明显变化,多了一个’NO-CARRIER’
图2: 拔掉网线后的网口输出结果

Read More

网口状态标志位解析part2: 内核如何维护网卡carrier的状态

内核如何维护网卡设备的RUNNING 状态

流程概述

主要几个部分:

  1. 网卡驱动有个看门狗:不同网卡驱动实现可能不太一样 ,功能负责监控网卡硬件上网线的状态, 当网线状态变换的时候,会激发内核的 carrier 处理函数。
  2. 内核两个通用的处理函数:netif_carrier_onnetif_carrier_off。这个函数会
    • 设置或者清除netdev->state上的__LINK_STATE_NOCARRIER标志位。
    • 发送事件消息给linkwatch,做后续处理.
    • 如果是网线插好了状态, 会启动一个通用的看门狗,这个看门狗是负责检测tx队列是否’HUNG’了, 如果’HUNG’了就调用网卡对应的处理函数ndo_tx_timeout, 做一些应急补救,比如对网卡队列复位等操作。这里的看门狗跟网卡驱动里的看门狗还不是同一个看门狗。具体差别待研究。
  3. linkwath模块:linkwatch本身是个workqueue 队列,对接受到的消息按照,分为紧急和非紧急两类。紧急的决定立即处理,非紧急的则挂到一个链表里,等定时器超时后,再集中处理。所有事件处理,最终都交给linkwatch_do_dev(struct net_device *dev)函数进行处理。 该函数更新netdev->operate标志位。同时调用通用的dev_activate或者dev_deactivate对网卡做网卡队列进行处理。 我们这里重点关注跟网卡状态位有管的部分,忽略跟网卡队列的处理。
    这里有两个重要函数rfc2863_policydefault_operstate 后面我们重点介绍。

carrier on 和 off 函数

netif_carrier_onnetif_carrier_off: 内核里的两个通用的处理函数,功能基本对称

carrier标志位

总结:
dev->state下的__LINK_STATE_NOCARRIER是 carrier是否OK 的唯一判断标准。

Read More

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

用法

tcpdump -s len

原理

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

tcpdump命令解析

tcpdump snaplen参数: ebpf指令对比

Read More

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