用户态 tcpdump 如何实现抓到内核网络包的?
发布时间 - 2025-07-16 00:00:00 点击率:次大家好,我是飞哥!
今天聊聊大家工作中经常用到的 tcpdump。
在网络包的发送和接收过程中,绝大部分的工作都是在内核态完成的。那么问题来了,我们常用的运行在用户态的程序 tcpdump 是那如何实现抓到内核态的包的呢?有的同学知道 tcpdump 是基于 libpcap 的,那么 libpcap 的工作原理又是啥样的呢。如果让你裸写一个抓包程序,你有没有思路?
按照飞哥的风格,不搞到最底层的原理咱是不会罢休的。所以我对相关的源码进行了深入分析。通过本文,你将彻底搞清楚了以下这几个问题。
tcpdump 是如何工作的?netfilter 过滤的包 tcpdump 是否可以抓的到?让你自己写一个抓包程序的话该如何下手?借助这几个问题,我们来展开今天的探索之旅!
一、网络包接收过程在图解Linux网络包接收过程一文中我们详细介绍了网络包是如何从网卡到达用户进程中的。这个过程我们可以简单用如下这个图来表示。
我们在网络设备层的代码里找到了 tcpdump 的抓包入口。在 __netif_receive_skb_core 这个函数里会遍历 ptype_all 上的协议。还记得上文中我们提到 tcpdump 在 ptype_all 上注册了虚拟协议。这时就能执行的到了。来看函数:
代码语言:javascript代码运行次数:0运行复制//file: net/core/dev.cstatic int __netif_receive_skb_core(struct sk_buff *skb, bool pfmemalloc){ ...... //遍历 ptype_all (tcpdump 在这里挂了虚拟协议) 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; } }}在上面函数中遍历 ptype_all,并使用 deliver_skb 来调用协议中的回调函数。
代码语言:javascript代码运行次数:0运行复制//file: net/core/dev.c static inline int deliver_skb(...){ return pt_prev->func(skb, skb->dev, pt_prev, orig_dev);}对于 tcpdump 来说,就会进入 packet_rcv 了(后面我们再说为啥是进入这个函数)。这个函数在 net/packet/af_packet.c 文件中。
代码语言:javascript代码运行次数:0运行复制//file: net/packet/af_packet.cstatic int packet_rcv(struct sk_buff *skb, ...){ __skb_queue_tail(&sk->sk_receive_queue, skb); ......}可见 packet_rcv 把收到的 skb 放到了当前 packet socket 的接收队列里了。这样后面调用 recvfrom 的时候就可以获取到所抓到的包!!
再找 netfilter 过滤点为了解释我们开篇中提到的问题,这里我们再稍微到协议层中多看一些。在 ip_rcv 中我们找到了一个 netfilter 相关的执行逻辑。
代码语言:javascript代码运行次数:0运行复制//file: net/ipv4/ip_input.cint ip_rcv(...){ ...... return NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING, skb, dev, NULL, ip_rcv_finish);}如果你用 NF_HOOK 作为关键词来搜索,还能搜到不少 netfilter 的过滤点。不过所有的过滤点都是位于 IP 协议层的。
在接收包的过程中,数据包是先经过网络设备层然后才到协议层的。
那么我们开篇中的一个问题就有了答案了。假如我们设置了 netfilter 规则,在接收包的过程中,工作在网络设备层的 tcpdump 先开始工作。还没等 netfilter 过滤,tcpdump 就抓到包了!
所以,在接收包的过程中,netfilter 过滤并不会影响 tcpdump 的抓包!
二、网络包发送过程我们接着再来看网络包发送过程。在25 张图,一万字,拆解 Linux 网络包发送过程一文中,我们详细描述过网络包的发送过程。发送过程可以汇总成简单的一张图。
在发送的过程中,同样是在 IP 层进入各种 netfilter 规则的过滤。
代码语言:javascript代码运行次数:0运行复制//file: net/ipv4/ip_output.c int ip_local_out(struct sk_buff *skb){ //执行 netfilter 过滤 err = __ip_local_out(skb);}int __ip_local_out(struct sk_buff *skb){ ...... return nf_hook(NFPROTO_IPV4, NF_INET_LOCAL_OUT, skb, NULL, skb_dst(skb)->dev, dst_output);}在这个文件中,还能看到若干处 netfilter 过滤逻辑。
找到 tcpdump 抓包点发送过程在协议层处理完毕到达网络设备层的时候,也有 tcpdump 的抓包点。
代码语言:javascript代码运行次数:0运行复制//file: net/core/dev.cint dev_hard_start_xmit(struct sk_buff *skb, struct net_device *dev, struct netdev_queue *txq){ ... if (!list_empty(&ptype_all)) dev_queue_xmit_nit(skb, dev);}static void dev_queue_xmit_nit(struct sk_buff *skb, struct net_device *dev){ list_for_each_entry_rcu(ptype, &ptype_all, list) { 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; } ...... } } }在上述代码中我们看到,在 dev_queue_xmit_nit 中遍历 ptype_all 中的协议,并依次调用 deliver_skb。这就会执行到 tcpdump 挂在上面的虚拟协议。
在网络包的发送过程中,和接收过程恰好相反,是协议层先处理、网络设备层后处理。
如果 netfilter 设置了过滤规则,那么在协议层就直接过滤掉了。在下层网络设备层工作的 tcpdump 将无法再捕获到该网络包。
前面两小节我们说到了内核收发包都通过遍历 ptype_all 来执行抓包的。那么我们现在来看看用户态的 tcpdump 是如何挂载协议到内 ptype_all 上的。
我们通过 strace 命令我们抓一下 tcpdump 命令的系统调用,显示结果中有一行 socket 系统调用。Tcpdump 秘密的源头就藏在这行对 socket 函数的调用里。
代码语言:javascript代码运行次数:0运行复制# strace tcpdump -i eth0socket(AF_PACKET, SOCK_RAW, 768)......
socket 系统调用的第一个参数表示创建的 socket 所属的地址簇或者协议簇,取值以 AF 或者 PF 开头。在 Linux 里,支持很多种协议族,在 include/linux/socket.h 中可以找到所有的定义。这里创建的是 packet 类型的 socket。
代码语言:javascript代码运行次数:0运行复制//file: include/linux/socket.h#define AF_UNSPEC 0#define AF_UNIX 1 /* Unix domain sockets */#define AF_LOCAL 1 /* POSIX name for AF_UNIX */#define AF_INET 2 /* Internet IP Protocol */#define AF_INET6 10 /* IP version 6 */#define AF_PACKET 17 /* Packet family */......
另外上面第三个参数 768 代表的是 ETH_P_ALL,socket.htons(ETH_P_ALL) = 768。
我们来展开看这个 packet 类型的 socket 创建的过程中都干了啥,找到 socket 创建源码。
代码语言:javascript代码运行次数:0运行复制//file: net/socket.cSYSCALL_DEFINE3(socket, int, family, int, type, int, protocol) { ...... retval = sock_create(family, type, protocol, &sock); }int __sock_create(struct net *net, int family, int type, ...){ ...... pf = rcu_dereference(net_families[family]); err = pf->create(net, sock, protocol, kern);}在 __sock_create 中,从 net_families 中获取了指定协议。并调用了它的 create 方法来完成创建。
net_families 是一个数组,除了我们常用的 PF_INET( ipv4 ) 外,还支持很多种协议族。比如 PF_UNIX、PF_INET6(ipv6)、PF_PACKET等等。每一种协议族在 net_families 数组的特定位置都可以找到其 family 类型。在这个 family 类型里,成员函数 create 指向该协议族的对应创建函数。
根据上图,我们看到对于 packet 类型的 socket,pf->create 实际调用到的是 packet_create 函数。我们进入到这个函数中来一探究竟,这是理解 tcpdump 工作原理的关键!
代码语言:javascript代码运行次数:0运行复制//file: packet/af_packet.cstatic int packet_create(struct net *net, struct socket *sock, int protocol, int kern){ ... po = pkt_sk(sk); po->prot_hook.func = packet_rcv; //注册钩子 if (proto) { po->prot_hook.type = proto; register_prot_hook(sk); }}static void register_prot_hook(struct sock *sk){ struct packet_sock *po = pkt_sk(sk); dev_add_pack(&po->prot_hook);}在 packet_create 中设置回调函数为 packet_rcv,再通过 register_prot_hook => dev_add_pack 完成注册。注册完后,是在全局协议 ptype_all 链表中添加了一个虚拟的协议进来。
我们再来看下 dev_add_pack 是如何注册协议到 ptype_all 中的。回顾我们开头看到的 socket 函数调用,第三个参数 proto 传入的是 ETH_P_ALL。那 dev_add_pack 其实最后是把 hook 函数添加到了 ptype_all 里了,代码如下。
代码语言:javascript代码运行次数:0运行复制//file: net/core/dev.cvoid dev_add_pack(struct packet_type *pt){ struct list_head *head = ptype_head(pt); list_add_rcu(&pt->list, head);}static inline struct list_head *ptype_head(const struct packet_type *pt){ if (pt->type == htons(ETH_P_ALL)) return &ptype_all; else return &ptype_base[ntohs(pt->type) & PTYPE_HASH_MASK];}总结:tcpdump 启动的时候内部逻辑其实很简单,就是在 ptype_all 中注册了一个虚拟协议而已。
四、总结现在我们再回头看开篇提到的几个问题。
1. tcpdump是如何工作的
用户态 tcpdump 命令是通过 socket 系统调用,在内核源码中用到的 ptype_all 中挂载了函数钩子上去。无论是在网络包接收过程中,还是在发送过程中,都会在网络设备层遍历 ptype_all 中的协议,并执行其中的回调。tcpdump 命令就是基于这个底层原理来工作的。
2. netfilter 过滤的包 tcpdump是否可以抓的到关于这个问题,得分接收和发送过程分别来看。在网络包接收的过程中,由于 tcpdump 近水楼台先得月,所以完全可以捕获到命中 netfilter 过滤规则的包。
但是在发送的过程中,恰恰相反。网络包先经过协议层,这时候被 netfilter 过滤掉的话,底层工作的 tcpdump 还没等看见就啥也没了。
3. 让你自己写一个抓包程序的话该如何下手如果你想自己写一段类似 tcpdump 的抓包程序的话,使用 packet socket 就可以了。我用 c 写了一段抓包,并且解析源 IP 和目的 IP 的简单 demo。
源码地址:https://github.com/yanfeizhang/coder-kung-fu/blob/main/tests/network/test04/main.c
编译一下,注意运行需要 root 权限。
代码语言:javascript代码运行次数:0运行复制# gcc -o main main.c# ./main
运行结果预览如下。
# linux
# git
# ai
# c#
# JavaScript
# 成员函数
# include
# 回调函数
# github
# https
# tcpdump
# 关键词
# 过程中
# 遍历
# 的是
# 是在
# 让你
# 都是
# 抓到
# 回调
# 还没
相关栏目:
【
网站优化151355 】
【
网络推广146373 】
【
网络技术251813 】
【
AI营销90571 】
相关推荐:
详解Nginx + Tomcat 反向代理 负载均衡 集群 部署指南
网站制作免费,什么网站能看正片电影?
如何在 Python 中将列表项按字母顺序编号(a.、b.、c. …)
Laravel如何记录自定义日志?(Log频道配置)
JS碰撞运动实现方法详解
深圳网站制作设计招聘,关于服装设计的流行趋势,哪里的资料比较全面?
弹幕视频网站制作教程下载,弹幕视频网站是什么意思?
零服务器AI建站解决方案:快速部署与云端平台低成本实践
Linux虚拟化技术教程_KVMQEMU虚拟机安装与调优
如何在阿里云通过域名搭建网站?
Gemini怎么用新功能实时问答_Gemini实时问答使用【步骤】
QQ浏览器网页版登录入口 个人中心在线进入
Laravel如何安装使用Debugbar工具栏_Laravel性能调试与SQL监控插件【步骤】
Laravel如何处理文件下载请求?(Response示例)
Laravel项目如何进行性能优化_Laravel应用性能分析与优化技巧大全
Laravel如何使用Telescope进行调试?(安装和使用教程)
Win11怎么更改系统语言为中文_Windows11安装语言包并设为显示语言
浅谈javascript alert和confirm的美化
Laravel的Blade指令怎么自定义_创建你自己的Laravel Blade Directives
Laravel Seeder填充数据教程_Laravel模型工厂Factory使用
Linux系统运维自动化项目教程_Ansible批量管理实战
Laravel怎么为数据库表字段添加索引以优化查询
Laravel如何生成和使用数据填充?(Seeder和Factory示例)
详解jQuery中的事件
如何快速生成高效建站系统源代码?
如何自定义建站之星模板颜色并下载新样式?
HTML5打空格有哪些误区_新手常犯的空格使用错误【技巧】
HTML透明颜色代码在Angular里怎么设置_Angular透明颜色使用指南【详解】
ChatGPT回答中断怎么办 引导AI继续输出完整内容的方法
如何用景安虚拟主机手机版绑定域名建站?
Win11怎么关闭透明效果_Windows11辅助功能视觉效果设置
Python3.6正式版新特性预览
潮流网站制作头像软件下载,适合母子的网名有哪些?
Laravel怎么做缓存_Laravel Cache系统提升应用速度的策略与技巧
Laravel如何实现邮件验证激活账户_Laravel内置MustVerifyEmail接口配置【步骤】
网站制作报价单模板图片,小松挖机官方网站报价?
关于BootStrap modal 在IOS9中不能弹出的解决方法(IOS 9 bootstrap modal ios 9 noticework)
laravel怎么为API路由添加签名中间件保护_laravel API路由签名中间件保护方法
高配服务器限时抢购:企业级配置与回收服务一站式优惠方案
如何在七牛云存储上搭建网站并设置自定义域名?
JavaScript中如何操作剪贴板_ClipboardAPI怎么用
Laravel API资源类怎么用_Laravel API Resource数据转换
Laravel如何实现一对一模型关联?(Eloquent示例)
如何在VPS电脑上快速搭建网站?
Laravel如何操作JSON类型的数据库字段?(Eloquent示例)
Laravel的辅助函数有哪些_Laravel常用Helpers函数提高开发效率
大连网站制作费用,大连新青年网站,五年四班里的视频怎样下载啊?
Android自定义listview布局实现上拉加载下拉刷新功能
详解CentOS6.5 安装 MySQL5.1.71的方法
Midjourney怎样加参数调细节_Midjourney参数调整技巧【指南】

