Linux内核协议栈以及抓包原理小结

Linux数据包的接收过程

  • https://segmentfault.com/a/1190000008836467

对于该文章的批注及摘要如下:

1 网卡接收数据包->DMA写入内存->驱动程序转换为skb

  • 大致流程就是网卡获取数据包,然后通过DMA写入内存,然后驱动程序将内存中的数据包转换成内核网络模块能识别的skb格式

2 对数据包进行处理

  • 驱动程序将调用napi_gro_receive函数。内核的网络模块中,第11条提到“napi_gro_receive会处理GRO相关的内容,也就是将可以合并的数据包进行合并,这样就只需要调用一次协议栈”,这个地方提到的GRO计数和TSO计数相对。一台主机的网卡如果不支持TSO技术,那么TCP数据包会限制长度使得IP层不会发生数据分片,而如果支持TSO技术,则IP数据包可以分片。TSO技术可以使得网卡直接对大的运输层数据包进行IP分片,然后发送出去。而GRO技术是接收端的技术,是对于这种分片的数据包进行整合,重整为一个数据包,避免同一个数据包的多个分片都进入一遍协议栈,造成资源浪费。GRO这个地方涉及的关键函数是napi_gro_receive

    gro_result_t napi_gro_receive(struct napi_struct *napi, struct sk_buff *skb)
    {
    	skb_mark_napi_id(skb, napi);
    	trace_napi_gro_receive_entry(skb);
    
    	skb_gro_reset_offset(skb); 
    
    	return napi_skb_finish(dev_gro_receive(napi, skb), skb);
    }
    
  • 接下来napi_gro_receive会直接调用__netif_receive_skb_core__netif_receive_skb_core会看是不是有AF_PACKET类型的socket(也就是我们常说的原始套接字),如果有的话,拷贝一份数据给它。tcpdump抓包就是抓的这里的包。然后,会进入内核协议栈。

3 进入协议栈

IP层
  • ip_rcv: ip_rcv函数是IP模块的入口函数,在该函数里面,第一件事就是将垃圾数据包(目的mac地址不是当前网卡,但由于网卡设置了混杂模式而被接收进来)直接丢掉,然后调用注册在NF_INET_PRE_ROUTING上的函数
UDP层
  • udp_rcv: udp_rcv函数是UDP模块的入口函数,它里面会调用其它的函数,主要是做一些必要的检查,其中一个重要的调用是__udp4_lib_lookup_skb,该函数会根据目的IP和端口找对应的socket,如果没有找到相应的socket,那么该数据包将会被丢弃,否则继续
  • sock_queue_rcv_skb: 主要干了两件事,一是检查这个socket的receive buffer是不是满了,如果满了的话,丢弃该数据包,然后就是调用sk_filter看这个包是否是满足条件的包,如果当前socket上设置了filter,且该包不满足条件的话,这个数据包也将被丢弃(在Linux里面,每个socket上都可以像tcpdump里面一样定义filter,不满足条件的数据包将会被丢弃)
  • __skb_queue_tail: 将数据包放入socket接收队列的末尾
  • sk_data_ready: 通知socket数据包已经准备好。调用完sk_data_ready之后,一个数据包处理完成,等待应用层程序来读取,上面所有函数的执行过程都在软中断的上下文中。
          |
          |
          ↓
      +---------+            +-----------------------+
      | udp_rcv |----------->| __udp4_lib_lookup_skb |
      +---------+            +-----------------------+
          |
          |
          ↓
 +--------------------+      +-----------+
 | sock_queue_rcv_skb |----->| sk_filter |
 +--------------------+      +-----------+
          |
          |
          ↓
 +------------------+
 | __skb_queue_tail |
 +------------------+
          |
          |
          ↓
  +---------------+
  | sk_data_ready |
  +---------------+

Linux数据包的发送过程

  • https://segmentfault.com/a/1190000008926093?utm_source=sf-similar-article

抓包原理

目前的理解(未手动验证)

BPF_PROG_TYPE_SOCKET_FILTER可能不止一个埋点函数,和创建的raw socket类型有关系。

  • 如果抓的是进入协议栈之后的IP数据报,可能走的是raw_rcv -> raw_rcv_skb -> sock_queue_rcv_skb执行路径,这个时候sk_filter就是BPF_PROG_TYPE_SOCKET_FILTER埋点函数:

    int sock_queue_rcv_skb(struct sock *sk, struct sk_buff *skb)
    {
             int err;
    
             err = sk_filter(sk, skb);
             if (err)
                     return err;
    
             return __sock_queue_rcv_skb(sk, skb);
     }
    
  • 如果抓的是未在协议栈之内的数据包,就是下面讲的这种情况:

    • 通过socket系统调用创建一个raw socket,这种socket和datagram socket以及stream socket不同,datagram socket一般用于获取UDP数据,stream socket用于获取TCP数据,此时经过协议栈处理,只包含数据负载。而raw socket是没有经过协议栈处理的,可以获得网卡上的完整数据包。

    • socket(PF_PACKET, SOCK_RAW | SOCK_NONBLOCK | SOCK_CLOEXEC, htons(ETH_P_ALL));通过这种方式创建socket时,会在内核创建一个packet_type(struct packet_type),并挂载到全局的ptype_all链表上(同时在packet_type设置回调函数packet_rcv)。这相当于是创建了一个虚拟协议,在内核处理数据包时会先匹配协议,并把数据包交给对应的处理逻辑。

    • 网络收包/发包时,会在各自的处理函数(收包时:__netif_receive_skb_core,发包时:dev_queue_xmit_nit)中遍历ptype_all链表,并同时执行其回调函数,tcpdump的注册的回调函数就是packet_rcv(所有协议都是这样被匹配然后递交到对应协议栈逻辑处理的,比如有一些后续就是raw_rcv()ip_rcv()arp_rcv()来负责处理)。比如下面的代码:

      list_for_each_entry_rcu(ptype, &ptype_all, list) { //遍历ptype_all链表注册的所有协议
      	if (pt_prev)
      		ret = deliver_skb(skb, pt_prev, orig_dev); //拷贝一份数据包
      	pt_prev = ptype;
      }
      
    • packet_rcv函数中调用了run_filter函数,会执行用户挂载(SOCKET_FILTER)的eBPF程序,对于数据包做相应的处理。

  • 参考https://baike.baidu.com/item/RAW%20SOCKET/995623?fr=aladdin对于raw socket类型的解释

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值