本来应该算是
五
的,结果一想内容上应该算是承接的二
的,那就算是个二.五
吧,讲一讲内核协议栈
的接收上的一些事。
链路层
以及以前的就不细说了,太麻烦了,而其中最为称道的有如下几个理念:
NAPI
和非NAPI
模式RPS
和RFS
input_pkt_queue
但这些涉及到了驱动开发,所以就直接看到了最后的函数__netif_receive_skb_core
,这个函数决定了怎样将一个skb
包送到ip layer
去。
type = skb->protocol;
从skb
获取到下层的协议是什么,然后再通过deliver_ptype_list_skb
设置设备的指定协议,最后再通过ret = pt_prev->func(skb, skb->dev, pt_prev, orig_dev);
发送数据,而这个协议函数在正常使用的情况下都会是ip_rcv
。
其实这部分还有非常多的判断和条件,并非如此简单
数据接收上其实比发送简单很多,大多是解包或者是
checksum
的计算
ip_rcv
接收到skb
后会根据RFC1122
去校验下iphdr
中的ihl
和version
,然后再计算一下checksum
是否正确,这儿是有固定的算法的,大致逻辑就是:
- 计算前把Checksum字段置0;
- 将IP Header中每两个连续的字节当成一个16bit数,对所有的16bit数进行求和,在求和过程中,任何溢出16bit数范围的求和结果都需要进行回卷——将溢出的高16bit和求和结果的低16bit相加;
- 对最终的求和结果按位取反,即可得到IP Header Checksum
然而关于验证的方法就简单很多:
只需进行Checksum计算中的第二步,若最终结果为0xFFFF则说明IP Header无差错。
关于
tcp/udp
也有checksum
的计算,不过得有伪首部参与其中,伪首部的大概数据结构如下
struct vhdr {
__be32 saddr;
__be32 daddr;
__u8 zeroadd;
__u8 protocol;
__u16 size;
};
等一系列检验操作结束后调用NF_INET_PRE_ROUTING
上注册的函数,这是一个netfilter
在协议栈中的钩子,主要是按照既定的规则去处理数据包,例如修改或者丢弃等等,最后还保留的数据包会由ip_rcv_finish
来作处理。
实际上没有去配什么iptables的话,这儿可以理解成数据包是直接进入到
ip_rcv_finish
中的
if (net->ipv4.sysctl_ip_early_demux &&
!skb_dst(skb) &&
!skb->sk &&
!ip_is_fragment(iph)) {
函数优先判断了skb
是否满足如下四个条件:
- 系统启用的
early_demux
skb
的路由缓存为空skb
的sock
为空- 非分片的包
关于第一点,其实先前在发包时候就简单提过,但是并不详细,这涉及到一点历史原因。假设一个tcp数据包到到达后,会优先查找skb
对应的路由,决定发送到哪儿,然后再去查找skb
对应的socket
,然而其实在通常情况下,只要socket
相同的话他们的路由就是相同的,那么将skb
的路由缓存到socket(skb->sk)
中,这样的话查找一次skb
的socket
就能同时把路由找到。但是这种行为也并非没有负面,就是针对包转发的情况,skb
是只需要查询路由的,因此在默认情况下,增加的ip_early_demux
导致转发包也会在查找路由前查找一次socket
从而导致转发效率降低。
默认此特性是打开的,可以直接查看下
sysctl net.ipv4.ip_early_demux
外界传来的包一般都会满足如上的条件,这样进入到判断后的逻辑中,获取到上层的协议后找到对应的early_demux
函数。
int protocol = iph->protocol;
ipprot = rcu_dereference(inet_protos[protocol]);
if (ipprot && (edemux = READ_ONCE(ipprot->early_demux))) {
例如tcp
就是6
,udp
是17
,假设此包是tcp
包的话那么对应的函数就是tcp_v4_early_demux
,逻辑很简单就是查找到established
的sock
,然后将其中的路由项赋值到skb->_skb_refdst
,接着就是校验路由项,如果这儿没有的话就进入到查路由的流程,再去设置路由缓存项。
接着再根据路由缓存项作各种判断,基本就是判断路由类型和数据包类型之类的,最后调用dst_input
函数,这个函数的具体调用也取决于路由缓存
static inline int dst_input(struct sk_buff *skb)
{
return skb_dst(skb)->input(skb);
}
这些因为跳过了路由缓存查找过程,但是如果跟如看的话主要是ip_route_input_slow
这个函数来实现,如果这个包是发往本地的,调用函数是ip_local_deliver
,相同的是这儿也有关于netfilter
的钩子,因此排除这部分后最终调用的函数是ip_local_deliver_finish
。
挑出重点的逻辑赋值如下:
int protocol = ip_hdr(skb)->protocol;
const struct net_protocol *ipprot;
ipprot = rcu_dereference(inet_protos[protocol]);
ret = ipprot->handler(skb);
还是向先前的情况一样,如果是tcp
的话ipprot
指向的是tcp_protocol
,那调用函数就是tcp_v4_rcv
,同理udp
就是udp_rcv
。
因为
udp
比较简单,所以写udp
的,反正逻辑差不了太多
剪裁校验完包信息后,先是调用skb_steal_sock
去尝试获取一下先前early_demux
设置的sock
,这儿我们假设是第一次收到信息,那么就进入到__udp4_lib_lookup_skb
的逻辑中,具体的就不说了,就是根据目的IP
和目的端口
找到对应的socket
,找到的话就设置一下,没找到的话一路返回err
然后丢弃掉。
不过这个函数很有意思,通过端口信息和地址信息查找到
socket
的信息,应该能够用在很多地方,比如在底层的恶意程序
检测上,从万千socket
中精确的查找到对应的那一条并kill
掉。
查找到后用udp_queue_rcv_skb
来处理报文,碰到如下两种情况就把包丢了:
- 队列满了
- 包不满足
filter
都没啥问题的话就把数据包放在socket
接收队列的队尾然后通知到socket
if (!sock_flag(sk, SOCK_DEAD))
sk->sk_data_ready(sk);
既然是通知了,那实际上在用户态下也要有个数据的接收方式。
- 通过
recvfrom
阻塞等待数据到来 - 通过
epoll
或者select
监听指定的socket
,最后也是调用到recvfrom
不管用户态是
recv
还是recvfrom
在系统调用后面都是__sys_recvfrom
关于netfilter
相关的知识的话,只记录点简单的。
netfilter
有5个hook
点,分别是:
-
NF_IP_PRE_ROUTING:刚刚进入网络层,还未进行路由查找的包,通过此处。
-
NF_IP_POST_ROUTING:进入网络层已经经过路由查找,确定转发,将要离开本设备的包,通过此处。
-
NF_IP_LOCAL_IN:通过路由查找,确定发往本机的包,通过此处。
-
NF_IP_LOCAL_OUT:从本机进程刚发出的包,通过此处。
-
NF_IP_FORWARD:经路由查找后,要转发的包,在POST_ROUTING之前。
就像先前的代码流程中看到的,数据包的接收流程中会有HOOK
函数执行的流程,执行完后才是进入到逻辑函数中,HOOK
函数中又有既定的规则,这些规则通常来源于INPUT
,OUTPUT
,FORWARD
三条链表中,根据这些规则决定如何处理这些数据包,例如丢弃掉,例如转发。而这些规则呢是由系统管理员认为配置的,因此用户态就有了iptables
这个工具,能够针对链表规则进行增删改查
。