PAWS 在tcp协议栈中的实现

PAWS 检查:

  1. tcp_timewait_state_process 中使用,也就是在syn 报文明中了 tw 状态的 socket 山海,才使用。 不是所有syn报文都做检查
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
 91 enum tcp_tw_status
92 tcp_timewait_state_process(struct inet_timewait_sock *tw, struct sk_buff *skb,
93 const struct tcphdr *th)
94 {
95 struct tcp_options_received tmp_opt;
96 struct tcp_timewait_sock *tcptw = tcp_twsk((struct sock *)tw);
97 bool paws_reject = false;
98
99 tmp_opt.saw_tstamp = 0;
100 if (th->doff > (sizeof(*th) >> 2) && tcptw->tw_ts_recent_stamp) {
101 tcp_parse_options(skb, &tmp_opt, 0, NULL);
102
103 if (tmp_opt.saw_tstamp) {
104 tmp_opt.rcv_tsecr -= tcptw->tw_ts_offset;
105 tmp_opt.ts_recent = tcptw->tw_ts_recent;
106 tmp_opt.ts_recent_stamp = tcptw->tw_ts_recent_stamp;
107 paws_reject = tcp_paws_reject(&tmp_opt, th->rst);
108 }
109 }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
1142
1143 static inline bool tcp_paws_check(const struct tcp_options_received *rx_opt,
1144 int paws_win)
1145 {
1146 if ((s32)(rx_opt->ts_recent - rx_opt->rcv_tsval) <= paws_win)
1147 return true;
1148 if (unlikely(get_seconds() >= rx_opt->ts_recent_stamp + TCP_PAWS_24DAYS))
1149 return true;
1150 /*
1151 * Some OSes send SYN and SYNACK messages with tsval=0 tsecr=0,
1152 * then following tcp messages have valid values. Ignore 0 value,
1153 * or else 'negative' tsval might forbid us to accept their packets.
1154 */
1155 if (!rx_opt->ts_recent)
1156 return true;
1157 return false;
1158 }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
1160 static inline bool tcp_paws_reject(const struct tcp_options_received *rx_opt,
1161 int rst)
1162 {
1163 if (tcp_paws_check(rx_opt, 0))
1164 return false;
1165
1166 /* RST segments are not recommended to carry timestamp,
1167 and, if they do, it is recommended to ignore PAWS because
1168 "their cleanup function should take precedence over timestamps."
1169 Certainly, it is mistake. It is necessary to understand the reasons
1170 of this constraint to relax it: if peer reboots, clock may go
1171 out-of-sync and half-open connections will not be reset.
1172 Actually, the problem would be not existing if all
1173 the implementations followed draft about maintaining clock
1174 via reboots. Linux-2.2 DOES NOT!
1175
1176 However, we can relax time bounds for RST segments to MSL.
1177 */
1178 if (rst && get_seconds() >= rx_opt->ts_recent_stamp + TCP_PAWS_MSL)
1179 return false;
1180 return true;
1181 }

tcp三次握手 --- 逐渐消失的tcp半链接队列

概要:十年演进对比

历史实现(v4.3)

网上大部分文档这部分内容讲的比较多,简单来说,可以把半链接队列的逻辑概括如下:

  • 收到syn报文,查找listen socket。
  • 创建一个半链接socket(req socket),并放入listen socket的半链接队列里,发送syn+ack报文
  • 收到client回复的ack报文,查找到对应的半链接socket(req socket)
  • 基于半链接socket和ack报文创建child socket
  • 把child socket(req)加入到accept队列中,等待accept系统调用/激活

当前实现(v6.14)

在v6.14内核里,三次握手的处理流程如下:

  • step1:收到一个syn请求报文:查找到对应的listen socket
  • step2:创建一个半链接socket(req socket),记录 syn 请求的一些字段信息,如seq, mss等。
  • step3:构造并发送syn+ack报文
  • step4: 把半链接socket(req socket)放入全局(netns)的ehash链表中,并统计半链接socket个数。通常说的半链接队列长度。
    + 全局:确切说是,当前netns(net namesapce)。一般默认是指当前物理机的内核协议栈。当有 docker实例(启用网络隔离)时候,全局则是指当前 docker 实例的内核协议栈。docker如何使用netsns隔离,不展开。为了理解方便,这里我们以没有docker 隔离的场景为例。
    + ehash链表:一个hash 链表,存放有全部establish状态的socket,当然tw状态的也在这个链表里。关联度不高,不展开。
    不同点 :使用全局ehash链表,而不是 listen socket的半链接队列
  • step5:收到client回复的ack报文,查找到对应的半链接socket(req socket)
  • step6:基于半链接socket 和ack报文创建child socket
  • step7:把新创建的child socket 插入到 ehash链表中。同时把req socket从链上摘掉。
  • step8:把req socket加入到listen socket的accept队列中,同时让req下的关联socket指针指向child。
  • step9: 发送listen socket的data_ready消息给应用程序, 等待listen socket上的accept系统调用/激活。
  • step10: accept系统调用把req socket从accept队列上拆除。同时,封装child socket,给用户返回其对应的fd.

tcp三次握手概要图

差异点

  • 使用全局ehash 链表而不是listen socket 下的半链接队列, 来存放半链接(req) socket。
    队列只是一个习惯性的叫法,严格意义上说不合适。很早(20+年)之前,内核就已经使用hash链接替换队列存放半链接socket了。
  • 长度检查:半链接个数(队列长度)和accept队列长度的检查
  • ack报文处理逻辑:查找半链接socket,减少锁listen socket的时长。

Read More

创建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大,取最大值。ehash_entries是 tcp 的 hash 桶个数。
  • sysctl_tcp_syncookies: 初始值为 1

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

虽然不再维护半链接队列了, 但是每次创建req socket后,这个统计值都是在增加的。
因此如果半链接个数超过了最大值sk_max_ack_backlog,则启用cookie(sysctl_tcp_syncookies为1或2),如果不支持cookie,则丢弃。

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 }

当前内核默认启用syncookie机制(sysctl_tcp_syncookies为1),队列溢出会触发synccookie 机制。
只有关闭了tcpcookie(0)后,才会在队列溢出时候丢弃syn报文。

Read More

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

####operstate的小插曲
2006年内核引入operstate特性,在当时协议栈的维护者中也是颇有争议的!!!
引入operstat特性的patch
图4: 2006年协议栈加入operstate 特性

标志位IFF_UP

设置/清除标志位IFF_UP

ip link set eth0 up:

  • 每个eth口在内核里有对应的struct net_device
  • 每个netdev设备里有一个上的flags用来存放标志位
  • ip link set eth0 up 设置 eth0口对应的IFF_UP标志。
  • ip link set eth0 down清除对应的IFF_UP标记。

ip link源码实现

ip link set eth0 up命令实现:

  • 通过ioctol获取eth0口对应的flags,
  • 然后将IFF_UP标志位设置到 flags 上,
  • 再通过ioctol 命令SIOCSIFFLAGS下发会内核。

具体实现在函数do_setdo_chflags中。

  1. do_set: 解析命令,转换为需要设置的标志位。
1
2
3
4
5
6
7
8
9
10
11
12
13
1370 static int do_set(int argc, char **argv)
1371 {
...
1383 while (argc > 0) {
1384 if (strcmp(*argv, "up") == 0) {
1385 mask |= IFF_UP;
1386 flags |= IFF_UP;
1387 } else if (strcmp(*argv, "down") == 0) {
1388 mask |= IFF_UP;
1389 flags &= ~IFF_UP;
...
1536 if (mask)
1537 return do_chflags(dev, flags, mask);
  1. do_chflags: 借助ioctl函数接口,与内核交互。
    • 首先,读取内核的网口标志位netdev->flags
    • 把期望更新的标志位更新成期望值。
    • 最后,把期望的标志位状态下发回内核。
1
2
3
4
5
6
7
8
9
static int do_chflags(const char *dev, __u32 flags, __u32 mask)
...
err = ioctl(fd, SIOCGIFFLAGS, &ifr);
...
if ((ifr.ifr_flags^flags)&mask) {
ifr.ifr_flags &= ~mask;
ifr.ifr_flags |= mask&flags;
err = ioctl(fd, SIOCSIFFLAGS, &ifr);
...

*** 注意: *** ioctol命令参数里,获取和设置的命令名字, 只有一个字母GS的差别。

内核代码实现

ioctl在内核的对应实现比较复杂, 避免歪楼,单拉一篇去介绍实现吧。

内核如何维护网卡设备的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

网口状态标志位详解(Part 1/2)

常见问题

我们在平常定位问题,经常会通过ip linkifconfig查看网络环境和配置。其中网口的标志位是其中很重要的一个检查环节。在输出结果里,会有两组标志位信息

  • 一组是尖括弧里包含的值<BROADCAST,MULTICAST,UP,LOWER_UP\>,有很多标志位的组合。
  • 另一组是 state UP。还有其他状态如DOWN, LOWERLAYERDOWN等。
    图1:ip link 输出结果
    这些信息有时候看起来是一致的, 有时候是重复的,甚至矛盾的。难免让人产生一些疑惑,这两组标志位分别代表什么?他们是什么关系?下面我们就这两组标志,逐个展开分析下。
    我们先来看几个场景里,网口在不同状态下,输出的标志位有哪些差异。

场景1:网线没插,显示NO-CARRIER

图2: 拔掉网线后的网口输出结果

  • 【Q1】 网口状态标志位里有个明显特征,有个NO-CARRIER标志,但是状态里一个UP,一个DOWN,why? :(

    Read More

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 ```

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

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