一 概述
前篇通过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 详解后篇 会将实战如何抓包。