Tcpdump抓包内核代码分析

本文解析了Linux内核PF_PACKET套接字的工作原理,包括创建时如何注册抓包钩子函数、数据接收和发送方向的数据包处理流程,以及销毁时的回调机制。


注册pf_packet协议

   .create函数是在PF_PACKET类型socket创建时调用,调用时注册了钩子函数具体看packet_create函数的实现。

static const struct net_proto_familypacket_family_ops = {

         .family=   PF_PACKET,

         .create=  packet_create,

         .owner     =       THIS_MODULE,

};

 

static int __init packet_init(void)

{

         …………..

         sock_register(&packet_family_ops);

         …………..

}

创建SOCK_PACKET sock时注册回调函数

/*

 *     Create a packet of type SOCK_PACKET.

 */

static int packet_create(struct net *net,struct socket *sock, int protocol,

                             int kern)

{

         structsock *sk;

         structpacket_sock *po;

         __be16proto = (__force __be16)protocol; /* weird, but documented */

         interr;

 

         if(!ns_capable(net->user_ns, CAP_NET_RAW))

                   return-EPERM;

         if(sock->type != SOCK_DGRAM && sock->type != SOCK_RAW &&

             sock->type != SOCK_PACKET)

                   return-ESOCKTNOSUPPORT;

 

         sock->state= SS_UNCONNECTED;

 

         err= -ENOBUFS;

         sk= sk_alloc(net, PF_PACKET, GFP_KERNEL, &packet_proto);

         if(sk == NULL)

                   gotoout;

 

         sock->ops= &packet_ops;

         if(sock->type == SOCK_PACKET)

                   sock->ops= &packet_ops_spkt;

 

         sock_init_data(sock,sk);

 

         po= pkt_sk(sk);

         sk->sk_family= PF_PACKET;

         po->num= proto;

 

         err= packet_alloc_pending(po);

         if(err)

                   gotoout2;

 

         packet_cached_dev_reset(po);

 

         sk->sk_destruct= packet_sock_destruct;

         sk_refcnt_debug_inc(sk);

 

         /*

          *     Attacha protocol block

          */

 

         spin_lock_init(&po->bind_lock);

         mutex_init(&po->pg_vec_lock);

         po->prot_hook.func= packet_rcv;

 

         //注册处理函数

         if (sock->type == SOCK_PACKET)

                   po->prot_hook.func =packet_rcv_spkt;

 

         po->prot_hook.af_packet_priv= sk;

 

         if (proto) {

                   po->prot_hook.type =proto;

                   将这个socket挂载到ptype_all链表上

                   register_prot_hook(sk);

         }

 

         mutex_lock(&net->packet.sklist_lock);

         sk_add_node_rcu(sk,&net->packet.sklist);

         mutex_unlock(&net->packet.sklist_lock);

 

         preempt_disable();

         sock_prot_inuse_add(net,&packet_proto, 1);

         preempt_enable();

 

         return0;

out2:

         sk_free(sk);

out:

         returnerr;

}

 

接收方向内核抓包函数

    两个调用场景,一个是网卡启用NAPI,在轮询流程中调用process_backlog;另外一个是非NAPI场景,直接netif_receive_skb接收数据报文,递交给网络层。

static int __netif_receive_skb_core(structsk_buff *skb, bool pfmemalloc)

{

         structpacket_type *ptype, *pt_prev;

         rx_handler_func_t*rx_handler;

         structnet_device *orig_dev;

         structnet_device *null_or_dev;

         booldeliver_exact = false;

         intret = NET_RX_DROP;

         __be16type;

 

         net_timestamp_check(!netdev_tstamp_prequeue,skb);

 

         trace_netif_receive_skb(skb);

 

         orig_dev= skb->dev;

 

         skb_reset_network_header(skb);

         if(!skb_transport_header_was_set(skb))

                   skb_reset_transport_header(skb);

         skb_reset_mac_len(skb);

 

         pt_prev= NULL;

 

another_round:

         skb->skb_iif= skb->dev->ifindex;

 

         __this_cpu_inc(softnet_data.processed);

 

         if(skb->protocol == cpu_to_be16(ETH_P_8021Q) ||

             skb->protocol ==cpu_to_be16(ETH_P_8021AD)) {

                   skb= vlan_untag(skb);

                   if(unlikely(!skb))

                            gotoout;

         }

 

#ifdef CONFIG_NET_CLS_ACT

         if(skb->tc_verd & TC_NCLS) {

                   skb->tc_verd= CLR_TC_NCLS(skb->tc_verd);

                   gotoncls;

         }

#endif

 

         if(pfmemalloc)

                   gotoskip_taps;

 

//遍历tcpdumpsocket创建时挂载的钩子

         list_for_each_entry_rcu(ptype,&ptype_all, list) {

                   if (!ptype->dev ||ptype->dev == skb->dev) {

                            if (pt_prev)

                                     //拷贝数据报文

                                     ret =deliver_skb(skb, pt_prev, orig_dev);

                            pt_prev = ptype;

                   }

         }

 

skip_taps:

#ifdef CONFIG_NET_CLS_ACT

         skb= handle_ing(skb, &pt_prev, &ret, orig_dev);

         if(!skb)

                   gotoout;

ncls:

#endif

 

         if(pfmemalloc && !skb_pfmemalloc_protocol(skb))

                   gotodrop;

 

         if(skb_vlan_tag_present(skb)) {

                   if(pt_prev) {

                            ret= deliver_skb(skb, pt_prev, orig_dev);

                            pt_prev= NULL;

                   }

                   if(vlan_do_receive(&skb))

                            gotoanother_round;

                   elseif (unlikely(!skb))

                            gotoout;

         }

 

         rx_handler= rcu_dereference(skb->dev->rx_handler);

         if(rx_handler) {

                   if(pt_prev) {

                            ret= deliver_skb(skb, pt_prev, orig_dev);

                            pt_prev= NULL;

                   }

                   switch(rx_handler(&skb)) {

                   caseRX_HANDLER_CONSUMED:

                            ret= NET_RX_SUCCESS;

                            gotoout;

                   caseRX_HANDLER_ANOTHER:

                            gotoanother_round;

                   caseRX_HANDLER_EXACT:

                            deliver_exact= true;

                   caseRX_HANDLER_PASS:

                            break;

                   default:

                            BUG();

                   }

         }

 

         if(unlikely(skb_vlan_tag_present(skb))) {

                   if(skb_vlan_tag_get_id(skb))

                            skb->pkt_type= PACKET_OTHERHOST;

                   /*Note: we might in the future use prio bits

                    * and set skb->priority like invlan_do_receive()

                    * For the time being, just ignore PriorityCode Point

                    */

                   skb->vlan_tci= 0;

         }

 

         /*deliver only exact match when indicated */

         null_or_dev= deliver_exact ? skb->dev : NULL;

 

         type= skb->protocol;

         //真实的数据报文处理流程,如果是ip那么调用ip_rcv函数了

        list_for_each_entry_rcu(ptype,

                            &ptype_base[ntohs(type)& PTYPE_HASH_MASK], list) {

                   if (ptype->type == type&&

                       (ptype->dev == null_or_dev ||ptype->dev == skb->dev ||

                        ptype->dev == orig_dev)) {

                            if (pt_prev)

                                     ret =deliver_skb(skb, pt_prev, orig_dev);

                            pt_prev = ptype;

                   }

         }

 

         if(pt_prev) {

                   if(unlikely(skb_orphan_frags(skb, GFP_ATOMIC)))

                            gotodrop;

                   else

                            ret= pt_prev->func(skb, skb->dev, pt_prev, orig_dev);

         }else {

drop:

                   atomic_long_inc(&skb->dev->rx_dropped);

                   kfree_skb(skb);

                   /*Jamal, now you will not able to escape explaining

                    * me how you were going to use this. :-)

                    */

                   ret= NET_RX_DROP;

         }

 

out:

         returnret;

}

发送方向内核抓包函数

         数据发送也存在两个分支,一个是调用dev_queue_xmit直接将数据递交到网卡(没有配置qdisc);另外一个分支是如果配置了qdisc,dev_queue_xmit流程检查是否配置了queue,如果配置了将调用__dev_xmit_skb函数将数据放入到了qdisc队列中,然后等待发送中断函数net_tx_action轮询调用,进而触发拷贝调用流程。

/*

 *     Support routine. Sends outgoing frames toany network

 *     taps currently in use.

 */

 

static void dev_queue_xmit_nit(structsk_buff *skb, struct net_device *dev)

{

         structpacket_type *ptype;

         structsk_buff *skb2 = NULL;

         structpacket_type *pt_prev = NULL;

 

         rcu_read_lock();

        //遍历tcpdumpsocket创建时挂载的钩子

         list_for_each_entry_rcu(ptype,&ptype_all, list) {

                   /* Never send packets back tothe socket

                    * they originated from - MvS(miquels@drinkel.ow.org)

                    */

                   if ((ptype->dev == dev ||!ptype->dev) &&

                       (!skb_loop_sk(ptype, skb))) {

                            if (pt_prev) {

                                     //拷贝数据报文

                                     deliver_skb(skb2,pt_prev, skb->dev);

                                     pt_prev =ptype;

                                     continue;

                            }

 

                            skb2 =skb_clone(skb, GFP_ATOMIC);

                            if (!skb2)

                                     break;

 

                            net_timestamp_set(skb2);

 

                            /* skb->nh shouldbe correctly

                               set by sender, so that the second statementis

                               just protection against buggy protocols.

                             */

                            skb_reset_mac_header(skb2);

 

                            if(skb_network_header(skb2) < skb2->data ||

                                skb_network_header(skb2) >skb_tail_pointer(skb2)) {

                                     net_crit_ratelimited("protocol%04x is buggy, dev %s\n",

                                                             ntohs(skb2->protocol),

                                                             dev->name);

                                     skb_reset_network_header(skb2);

                            }

 

                            skb2->transport_header= skb2->network_header;

                            skb2->pkt_type =PACKET_OUTGOING;

                            pt_prev = ptype;

                   }

         }

         if(pt_prev)

                   pt_prev->func(skb2,skb->dev, pt_prev, skb->dev);

         rcu_read_unlock();

}

 

 

 

销毁SOCK_PACKET sock时注册回调

当sock_packet类型 socket 关闭时会调用release函数,这时候会摘掉之前的注册函数

static int packet_release(struct socket*sock)

{

         structsock *sk = sock->sk;

         structpacket_sock *po;

         structnet *net;

         uniontpacket_req_u req_u;

 

         if(!sk)

                   return0;

 

         net= sock_net(sk);

         po= pkt_sk(sk);

 

         mutex_lock(&net->packet.sklist_lock);

         sk_del_node_init_rcu(sk);

         mutex_unlock(&net->packet.sklist_lock);

 

         preempt_disable();

         sock_prot_inuse_add(net,sk->sk_prot, -1);

         preempt_enable();

         spin_lock(&po->bind_lock);

         //从ptype_all函数中摘掉注册的钩子函数

         unregister_prot_hook(sk, false);

         packet_cached_dev_reset(po);

 

         if(po->prot_hook.dev) {

                   dev_put(po->prot_hook.dev);

                   po->prot_hook.dev= NULL;

         }

         spin_unlock(&po->bind_lock);

 

         packet_flush_mclist(sk);

 

         if(po->rx_ring.pg_vec) {

                   memset(&req_u,0, sizeof(req_u));

                   packet_set_ring(sk,&req_u, 1, 0);

         }

 

         if(po->tx_ring.pg_vec) {

                   memset(&req_u,0, sizeof(req_u));

                   packet_set_ring(sk,&req_u, 1, 1);

         }

 

         fanout_release(sk);

 

         synchronize_net();

         /*

          *     Nowthe socket is dead. No more input will appear.

          */

         sock_orphan(sk);

         sock->sk= NULL;

 

         /*Purge queues */

 

         skb_queue_purge(&sk->sk_receive_queue);

         packet_free_pending(po);

         sk_refcnt_debug_release(sk);

 

         sock_put(sk);

         return0;

}

总结

        Tcpdump抓包时创建SOCK_PACKET类型的socket,并且在socket创建流程时调用了packet_family_opspacket_create函数(packet_create),进而将抓包的钩子函数注册到ptype_all链表,当在数据接收方向__netif_receive_skb_core函数中调用注册的钩子函数将数据报文拷贝到af_packet.c文件的具体处理流程函数中;同样在发送函数dev_queue_xmit_nit中调用钩子函数实现数据报文拷贝。


Jensonqiujensonqiu@163.com 2018/05/08


### 如何使用 tcpdump 抓取内核发出的数据包 `tcpdump` 是一个广泛使用的网络协议分析工具,它能够捕获网络接口上流过的数据包并显示其相关信息,为网络管理员和开发者提供了强大的网络数据捕获和分析能力。作为一个命令行工具,它的使用门槛相对较高,但同时提供了极其灵活的数据包捕获选项和条件。对于捕获内核发送的数据包这一需求,`tcpdump` 提供了多种方式来实现。 在 Linux 系统中,`tcpdump` 通过 `libpcap` 库与内核交互,在数据链路层捕获数据包。它依赖于 **BPF(Berkeley Packet Filter)** 技术,在内核中运行过滤程序,只将符合条件的数据包复制到用户空间,从而减少性能开销。在创建 socket 套接字时,`tcpdump` 会绑定到指定的网络接口,并通过 `ioctl` 设置混杂模式(promiscuous mode),以便捕获所有经过该接口的数据包。BPF 程序则决定了哪些数据包会被捕获并传递到用户空间。 #### 1. 使用 `tcpdump` 捕获内核发送的数据包 在 Linux 系统中,`tcpdump` 通常默认捕获的是通过网络接口接收的数据包。然而,如果需要捕获内核发出的数据包(即发往网络接口的数据包),可以通过以下几种方式进行: ##### 1.1 指定网络接口进行捕获 `tcpdump` 支持指定特定的网络接口进行数据包捕获。例如,如果需要捕获从 `eth0` 接口发出的数据包,可以使用以下命令: ```bash tcpdump -i eth0 ``` 该命令表示捕获 `eth0` 接口上的所有数据包,包括内核发出的数据包[^2]。 ##### 1.2 使用过滤表达式捕获特定类型的数据包 如果需要进一步细化捕获条件,可以结合 `tcpdump` 的过滤表达式来筛选特定类型的数据包。例如,捕获内核发出的 TCP 数据包可以使用以下命令: ```bash tcpdump -i eth0 tcp ``` 此命令将仅捕获 `eth0` 接口上发出的 TCP 数据包。 ##### 1.3 捕获内核发出的特定端口数据包 如果需要捕获内核发出的特定端口数据包,可以使用 `port` 关键字进行过滤。例如,捕获从 `eth0` 接口发出的 HTTP 流量(端口 80)可以使用以下命令: ```bash tcpdump -i eth0 port 80 ``` ##### 1.4 捕获特定源 IP 地址发出的数据包 如果需要捕获内核发出的特定源 IP 地址的数据包,可以使用 `src` 关键字进行过滤。例如,捕获从 `eth0` 接口发出的源 IP 为 `192.168.1.1` 的数据包可以使用以下命令: ```bash tcpdump -i eth0 src 192.168.1.1 ``` #### 2. 捕获内核发出的数据包时的注意事项 ##### 2.1 混杂模式与非混杂模式 `tcpdump` 默认会将网络接口设置为混杂模式,以便捕获所有经过该接口的数据包。然而,在某些情况下,可能希望仅捕获内核发出的数据包,而不捕获其他设备发送到该接口的数据包。此时可以使用 `-p` 选项禁用混杂模式: ```bash tcpdump -i eth0 -p ``` 此命令将仅捕获内核发出的数据包,而不会捕获其他设备发送到该接口的数据包。 ##### 2.2 数据包详细信息输出 `tcpdump` 默认只按照最简约模式对相应数据包进行解码。如果希望查看更详细的解码信息,可以使用 `-v`、`-vv` 或 `-vvv` 启动选项。需要注意的是,`-v` 会产生非常详细的信息,例如对单一的一个 SMB 数据包,将产生一屏幕或更多的信息,因此建议仅在确实需要时才使用此选项[^3]。 ##### 2.3 将捕获的数据保存到文件 如果需要将捕获的数据包保存到文件中以便后续分析,可以使用 `-w` 选项: ```bash tcpdump -i eth0 -w output.pcap ``` 此命令将把 `eth0` 接口上捕获的所有数据包保存到 `output.pcap` 文件中,可以使用 Wireshark 或其他工具进行后续分析。 ####
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值