现在的位置: 首页 > 自动控制 > 工业·编程 > 正文

linux 下 tcpdump详解 中篇(内核源码分析)

2019-09-22 18:22 工业·编程 ⁄ 共 8503字 ⁄ 字号 暂无评论

一 概述

前篇通过libpcap分析,可以很清楚的发现其实用户层调用了三个系统调用,就实现了将内核网卡抓的包,返回给用户层。

· 1.创建一个socket ; sock_fd = cooked ?socket(PF_PACKET, SOCK_DGRAM, protocol) :socket(PF_PACKET, SOCK_RAW, protocol);

· 2 设置bpf 规则,使得规则在内核返回给用户的包就已经经过bpf过滤。

· 3 recvfrom 接收抓到的数据包

而对于内核做了那些呢?

· 链路层网口抓的包,是如何给到对应的socket?

· 最终应用层recvfrom 获取的包,内核是如何返回的?

我们不妨先假设下,创建了socket,内核抓到的包应该就会先复制一份给bpf过滤器,要是设置的bpf,就会对内核复制的数据包过滤。过滤后的数据包,放到接收队列中。而recvfrom 根据接收队列,将队列中的数据从内核copy到用户中。

二 内核实现抓包

1 应用层socket与内核底层关联

我们先看问题1 应用层socket(PF_PACKET, SOCK_DGRAM, protocol) 创建的socket底层内核是如何关联上的。
内核底层根据协议簇(即创建socket的第一个参数 int af )是有很多不同的模块的,例如常用的AF_INET,PF_PACKET 。 而这些模块初始化的时候就会注册上。下面代码对应PF_PACKET 模块

// # net/packet/af_packet.c

static const struct net_proto_family packet_family_ops = {

.family = PF_PACKET,

.create = packet_create,

.owner = THIS_MODULE,

};

static int __init packet_init(void)

{

int rc = proto_register(&packet_proto, 0);

if (rc != 0)

goto out;

sock_register(&packet_family_ops);

register_pernet_subsys(&packet_net_ops);

register_netdevice_notifier(&packet_netdev_notifier);

out:

return rc;

}

module_init(packet_init);

module_exit(packet_exit);

module_init 模块初始化,程序运行的时候会自动调用packet_init初始化模块。至于module_init 原理有兴趣可看我之前写的 liunx 内核动态模块初始化这篇博客。而packet_init 函数中sock_register 本质就是将 struct net_proto_family packet_family_ops 注册到socket.c 一个全局的 static const struct net_proto_family __rcu *net_families[NPROTO] __read_mostly; 变量中, 注册上有啥用呢?看下面代码

// # socket.c

static const struct net_proto_family __rcu *net_families[NPROTO] __read_mostly;

int __sock_create(struct net *net, int family, int type, int protocol,

struct socket **res, int kern)

{

int err;

struct socket *sock;

const struct net_proto_family *pf;

if (family < 0 || family >= NPROTO)

return -EAFNOSUPPORT;

if (type < 0 || type >= SOCK_MAX)

return -EINVAL;

if (family == PF_INET && type == SOCK_PACKET) {

pr_info_once("%s uses obsolete (PF_INET,SOCK_PACKET)\n",

    current->comm);

family = PF_PACKET;

}

err = security_socket_create(family, type, protocol, kern);

if (err)

return err;

sock = sock_alloc();  // 创建分配 struct socket  * sock

rcu_read_lock();

pf = rcu_dereference(net_families[family]);   // 根据family协议簇找到 注册的net_proto_family 

err = -EAFNOSUPPORT;

if (!pf)

goto out_release;

err = pf->create(net, sock, protocol, kern);  // 调用注册的create函数

if (err < 0)

goto out_module_put;

module_put(pf->owner);

.

应用层创建socket(int af, int type, int protocol) 最终会调用上面的__sock_create 函数 而__sock_create 函数主要就是创建了socket . 同时根据之前PF_PACKET 模块注册到全局变量net_families 。 找到af_packet.c 中初始化的 static const struct net_proto_family packet_family_ops。而__sock_create 函数中 err = pf->create(net, sock, protocol, kern); 最终就会调用 packet_family_ops 里的packet_create 。packet_create 函数又做了那些事情呢?

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

int kern)

{

struct sock *sk;

struct packet_sock *po;

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

int err;

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, kern);  // 创建 struct sock * sk

if (sk == NULL)

goto out;

sock->ops = &packet_ops;  //  设置struct socket * sock 的操作集

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;

po->xmit = dev_queue_xmit;

err = packet_alloc_pending(po);

if (err)

goto out2;

packet_cached_dev_reset(po);

sk->sk_destruct = packet_sock_destruct;

sk_refcnt_debug_inc(sk);

spin_lock_init(&po->bind_lock);

mutex_init(&po->pg_vec_lock);

po->rollover = NULL;

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;

__register_prot_hook(sk);    // 最终挂载到dev.c 中的ptype_all链表上

}

.

.

.

}

static void __register_prot_hook(struct sock *sk)

{

struct packet_sock *po = pkt_sk(sk);

if (!po->running) {

if (po->fanout)

__fanout_link(sk, po);

else

dev_add_pack(&po->prot_hook);

sock_hold(sk);

po->running = 1;

}

}

packet_create函数两个重要的作用

· 创建了struct sock * sk ,同时初始化struct socket *sock 中结构体的操作集。struct sock 和 struct socket 这两个结构体是内核底层网络通信重要的结构体。这里不做过多阐述。

· 初始化了packet_type 结构体,然后注册到了dev.c 中的ptype_all链表中。上面代码没看到packet_type结构体? 其实po->prot_hook 这个就是packet_type 结构体

struct packet_type {

__be16 type; /* This is really htons(ether_type). */

struct net_device *dev; /* NULL is wildcarded here     */

int (*func) (struct sk_buff *,

struct net_device *,

struct packet_type *,

struct net_device *);

bool (*id_match)(struct packet_type *ptype,

   struct sock *sk);

void *af_packet_priv;

struct list_head list;

};

packet_type 结构体第一个type 很重要,对应链路层中2个字节的以太网类型。而dev.c 链路层抓取的包上报给对应模块,就是根据抓取的链路层类型,然后给对应的模块处理,例如socket(PF_PACKET, SOCK_DGRAM, htons(ETH_P_ALL)); ETH_P_ALL表示所有的底层包都会给到PF_PACKET 模块的处理函数,这里处理函数就是packet_rcv 函数。

// dev.c

static inline int deliver_skb(struct sk_buff *skb,

     struct packet_type *pt_prev,

     struct net_device *orig_dev)

{

if (unlikely(skb_orphan_frags_rx(skb, GFP_ATOMIC)))

return -ENOMEM;

refcount_inc(&skb->users);

return pt_prev->func(skb, skb->dev, pt_prev, orig_dev);

}

可以看到最终dev.c 会根据符合的以太网类型调用注册的回调函数。那么dev.c 抓到数据包后,将数据包copy出一份给PF_PACKET 模块的packet_rcv 函数。packet_rcv 函数做了什么那些东西呢?

static int packet_rcv(struct sk_buff *skb, struct net_device *dev,

     struct packet_type *pt, struct net_device *orig_dev)

{

struct sock *sk;

struct sockaddr_ll *sll;

struct packet_sock *po;

u8 *skb_head = skb->data;

int skb_len = skb->len;

if (dev->header_ops) {

if (sk->sk_type != SOCK_DGRAM)

skb_push(skb, skb->data - skb_mac_header(skb));  // 当 SOCK_DGRAM类型的时候,会截取掉链路层的数据包,从而返回给应用层的数据包是不包含链路层数据的

else if (skb->pkt_type == PACKET_OUTGOING) {

/* Special case: outgoing packets have ll header at head */

skb_pull(skb, skb_network_offset(skb));

}

}

snaplen = skb->len;

res = run_filter(skb, sk, snaplen);  //  这部分就是根据对于socket设置的filter ,过滤数据包

if (!res)

goto drop_n_restore;

if (snaplen > res)

snaplen = res;

if (skb_shared(skb)) {

struct sk_buff *nskb = skb_clone(skb, GFP_ATOMIC);

if (nskb == NULL)

goto drop_n_acct;

if (skb_head != skb->data) {

skb->data = skb_head;

skb->len = skb_len;

}

consume_skb(skb);

skb = nskb;

}

.

// 最后将底层网口符合应用层的数据复制到接收缓存队列中

spin_lock(&sk->sk_receive_queue.lock);

po->stats.stats1.tp_packets++;

sock_skb_set_dropcount(sk, skb);

__skb_queue_tail(&sk->sk_receive_queue, skb);

spin_unlock(&sk->sk_receive_queue.lock);

sk->sk_data_ready(sk);

return 0;

packet_rev函数接收到链路层网口的数据包后,会根据应用层设置的bpf过滤数据包,符合要求的最终会加到struct sock sk 的接收缓存中。

2 应用层获取数据包

通过1步骤你已经知道应用层创建了socket,底层会关联上这个socket,一旦关联上链路层抓到的包就会copy一份给上层接口(即PF_PACKET 注册的回调函数packet_rev). 而回调函数会根据应用层设置的bpf过滤数据包,最终放入接收缓存的数据包肯定是符合应用层想截取的数据。因此最后一步recvfrom 也就是从接收缓存的数据包copy给应用层。代码如下

//  #  af_packet.c

static int packet_recvmsg(struct socket *sock, struct msghdr *msg, size_t len,

int flags)

{

struct sock *sk = sock->sk;

struct sk_buff *skb;

int copied, err;

int vnet_hdr_len = 0;

unsigned int origlen = 0;

err = -EINVAL;

if (flags & ~(MSG_PEEK|MSG_DONTWAIT|MSG_TRUNC|MSG_CMSG_COMPAT|MSG_ERRQUEUE))

goto out;

if (flags & MSG_ERRQUEUE) {

err = sock_recv_errqueue(sk, msg, len,

SOL_PACKET, PACKET_TX_TIMESTAMP);

goto out;

}

skb = skb_recv_datagram(sk, flags, flags & MSG_DONTWAIT, &err); // 从接收缓存中获取数据包

if (skb == NULL)

goto out;

if (pkt_sk(sk)->pressure)

packet_rcv_has_room(pkt_sk(sk), NULL);

if (pkt_sk(sk)->has_vnet_hdr) {

err = packet_rcv_vnet(msg, skb, &len);

if (err)

goto out_free;

vnet_hdr_len = sizeof(struct virtio_net_hdr);

}

copied = skb->len;

if (copied > len) {

copied = len;

msg->msg_flags |= MSG_TRUNC;

}

   // 将最终的数据copy到用户空间

err = skb_copy_datagram_msg(skb, 0, msg, copied);

if (err)

goto out_free;

if (sock->type != SOCK_PACKET) {

struct sockaddr_ll *sll = &PACKET_SKB_CB(skb)->sa.ll;

/* Original length was stored in sockaddr_ll fields */

origlen = PACKET_SKB_CB(skb)->sa.origlen;

sll->sll_family = AF_PACKET;

sll->sll_protocol = skb->protocol;

}

sock_recv_ts_and_drops(msg, sk, skb);

if (msg->msg_name) {

/* If the address length field is there to be filled

* in, we fill it in now.

*/

if (sock->type == SOCK_PACKET) {

__sockaddr_check_size(sizeof(struct sockaddr_pkt));

msg->msg_namelen = sizeof(struct sockaddr_pkt);

} else {

struct sockaddr_ll *sll = &PACKET_SKB_CB(skb)->sa.ll;

msg->msg_namelen = sll->sll_halen +

offsetof(struct sockaddr_ll, sll_addr);

}

memcpy(msg->msg_name, &PACKET_SKB_CB(skb)->sa,

      msg->msg_namelen);

}

.

.

.

}

主要函数就 skb_recv_datagram 从接收缓存获取数据包,然后后面调用 skb_copy_datagram_msg(skb, 0, msg, copied); 将数据包复制到用户空间,这里就不深入函数内部。至此整个内核抓包过程已经分析完。。。

三 总结

通过上面分析下来,你会发现这跟我们最初的假设相差不大。这里回顾下整个过程

· 应用层创建socket,底层会关联上这个socket,具体到代码先创建struct socket ,同时根据socket 函数的第一个参数,找到对应的协议簇模块(PF_PACKET)。调用协议簇模块的注册函数(packet_create)。而注册函数会创建 struct sock 。同时将模块注册到底层链路层dev.c中。

· 而链路层dev.c 抓到数据包,会根据PF_PACKET 协议簇注册到dev.c的模块。将数据包给到 PF_PACKET 的packet_rcv 回调函数。最后packet_rcv函数将符合bpf的数据包添加到接收队列中

· 应用层调用recvfrom 。 PF_PACKET 协议簇模块调用packet_recvmsg 将接收队列中的数据copy应用层。

整个过程你要是对底层tcp,udp有过了解,你会发现上面的抓包,底层其实就涉及了链路层,传输层。也就是链路层抓的数据包,直接给到传输层。 而常规的tcp,udp数据 链路层抓的包,先给到网络层的ip模块,再给到传输层的tcp,udp模块,最终将应用层数据给到应用。因此这里有个问题内核返回给用户层的包,应该是乱序的,没组装的。但是tcpdump显示的抓包却是组装好的? tcpdump 源码没有深入不太了解。不知道是我底层那块代码遗落了,还是tcpdump确实是在应用层组装过包。如果有大神了解或者对这方面有兴趣可沟通我,本人后续会深入了解下,互相学习。

最后额外抛出个话题,你发现链路层抓到的包是根据链路层类型来处理包的,那么你其实完全可以内核底层加入你自己定义的链路层类型。然后通过__register_prot_hook 中的dev_add_pack 函数将自己定义的模块注册到dev.c 中,那么dev.c当抓到的包是你定义的链路层的时候,就会将包给到你模块注册的回调函数。这样一来你就可以让内核底层处理自己定义的链路层数据包了。最后一篇 tcpdump 详解后篇 会将实战如何抓包。

给我留言

留言无头像?