Skip to content

Latest commit

 

History

History
149 lines (106 loc) · 8.38 KB

linux网络(二.五).md

File metadata and controls

149 lines (106 loc) · 8.38 KB

本来应该算是的,结果一想内容上应该算是承接的的,那就算是个二.五吧,讲一讲内核协议栈的接收上的一些事。

链路层以及以前的就不细说了,太麻烦了,而其中最为称道的有如下几个理念:

  1. NAPI非NAPI模式
  2. RPSRFS
  3. 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

其实这部分还有非常多的判断和条件,并非如此简单

IP layer

数据接收上其实比发送简单很多,大多是解包或者是checksum的计算

ip_rcv接收到skb后会根据RFC1122去校验下iphdr中的ihlversion,然后再计算一下checksum是否正确,这儿是有固定的算法的,大致逻辑就是:

  1. 计算前把Checksum字段置0;
  2. 将IP Header中每两个连续的字节当成一个16bit数,对所有的16bit数进行求和,在求和过程中,任何溢出16bit数范围的求和结果都需要进行回卷——将溢出的高16bit和求和结果的低16bit相加;
  3. 对最终的求和结果按位取反,即可得到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是否满足如下四个条件:

  1. 系统启用的early_demux
  2. skb的路由缓存为空
  3. skbsock为空
  4. 非分片的包

关于第一点,其实先前在发包时候就简单提过,但是并不详细,这涉及到一点历史原因。假设一个tcp数据包到到达后,会优先查找skb对应的路由,决定发送到哪儿,然后再去查找skb对应的socket,然而其实在通常情况下,只要socket相同的话他们的路由就是相同的,那么将skb的路由缓存到socket(skb->sk)中,这样的话查找一次skbsocket就能同时把路由找到。但是这种行为也并非没有负面,就是针对包转发的情况,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就是6udp17,假设此包是tcp包的话那么对应的函数就是tcp_v4_early_demux,逻辑很简单就是查找到establishedsock,然后将其中的路由项赋值到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 layer

因为udp比较简单,所以写udp的,反正逻辑差不了太多

剪裁校验完包信息后,先是调用skb_steal_sock去尝试获取一下先前early_demux设置的sock,这儿我们假设是第一次收到信息,那么就进入到__udp4_lib_lookup_skb的逻辑中,具体的就不说了,就是根据目的IP目的端口找到对应的socket,找到的话就设置一下,没找到的话一路返回err然后丢弃掉。

不过这个函数很有意思,通过端口信息和地址信息查找到socket的信息,应该能够用在很多地方,比如在底层的恶意程序检测上,从万千socket中精确的查找到对应的那一条并kill掉。

查找到后用udp_queue_rcv_skb来处理报文,碰到如下两种情况就把包丢了:

  1. 队列满了
  2. 包不满足filter

都没啥问题的话就把数据包放在socket接收队列的队尾然后通知到socket

 if (!sock_flag(sk, SOCK_DEAD))
  sk->sk_data_ready(sk);

Socket layer

既然是通知了,那实际上在用户态下也要有个数据的接收方式。

  1. 通过recvfrom阻塞等待数据到来
  2. 通过epoll或者select监听指定的socket,最后也是调用到recvfrom

不管用户态是recv还是recvfrom在系统调用后面都是__sys_recvfrom

关于netfilter相关的知识的话,只记录点简单的。 netfilter有5个hook点,分别是:

  1. NF_IP_PRE_ROUTING:刚刚进入网络层,还未进行路由查找的包,通过此处。

  2. NF_IP_POST_ROUTING:进入网络层已经经过路由查找,确定转发,将要离开本设备的包,通过此处。

  3. NF_IP_LOCAL_IN:通过路由查找,确定发往本机的包,通过此处。

  4. NF_IP_LOCAL_OUT:从本机进程刚发出的包,通过此处。

  5. NF_IP_FORWARD:经路由查找后,要转发的包,在POST_ROUTING之前。

就像先前的代码流程中看到的,数据包的接收流程中会有HOOK函数执行的流程,执行完后才是进入到逻辑函数中,HOOK函数中又有既定的规则,这些规则通常来源于INPUTOUTPUTFORWARD三条链表中,根据这些规则决定如何处理这些数据包,例如丢弃掉,例如转发。而这些规则呢是由系统管理员认为配置的,因此用户态就有了iptables这个工具,能够针对链表规则进行增删改查

参考文章