[上篇文章] 介绍了在内核角度tcpdump的抓包原理(1),主要流程如下:
sock_fd = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
tcpdump在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。本文围绕的重点是:BPF的过滤原理,如下源码所示:run_filter(skb, sk, snaplen),本次文章将对BPF的过滤原理进行一些分析。
static int packet_rcv(struct sk_buff *skb, struct net_device *dev,
struct packet_type *pt, struct net_device *orig_dev)
{
......
res = run_filter(skb, sk, snaplen); //将用户指定的过滤条件使用BPF进行过滤
......
__skb_queue_tail(&sk->sk_receive_queue, skb);//将skb放到当前的接收队列中
......
}
tcpdump依附标准的的BPF机器,tcpdump的过滤规则会被转化成一段bpf指令并加载到内核中的bpf虚拟机器上执行,显然,由用户来写过滤代码太过复杂,因此 libpcap 允许用户书写高层的、容易理解的过滤字符串,然后将其编译为BPF代码,tcpdump自动完成,不为用户所见。
关于BPF的介绍以及学习路线可以参考该链接的文章(https://linux.cn/article-9507-1.html),该文章给出了一些阅读清单。
BPF指令集
是一个伪机器码,与能够在物理机上直接执行的机器码不同,BPF指令集是可以在BPF虚拟机上执行的指令集。bpf在内核中实际就是一个虚拟机,有自己定义的虚拟机寄存器组。在最早的cBPF汇编框架中的三种寄存器:
Element Description
A 32 bit wide accumulator//所以加载指令的目的地址和所有指令运算结果的存储地址
X 32 bit wide X register//二元指令计算A中参数的辅助寄存器(例如移位的位数,除法的除数)
M[] 16 x 32 bit wide misc registers aka "scratch memory
store", addressable from 0 to 15// 0-15共16个32位寄存器,可以自由使用
在cBPF每条汇编指令如下这种格式:
// include\uapi\linux\filter.h
struct sock_filter { /* Filter block */
__u16 code; /* 真正的bpf汇编指令 */
__u8 jt; /* 结果为true时跳转指令 */
__u8 jf; /* 结果为false时跳转 */
__u32 k; /* 指令的参数 */
};
我们最常见的用法莫过于从数据包中取某个字的数据来做判断。按照bpf的规定,我们可以使用偏移来指定数据包的任何位置,而很多协议很常用并且固定,例如端口和ip地址等,bpf就为我们提供了一些预定义的变量,只要使用这个变量就可以直接取值到对应的数据包位置。例如:
len skb->len
proto skb->protocol
type skb->pkt_type
poff Payload start offset
ifidx skb->dev->ifindex
nla Netlink attribute of type X with offset A
nlan Nested Netlink attribute of type X with offset A
mark skb->mark
queue skb->queue_mapping
hatype skb->dev->type
rxhash skb->hash
cpu raw_smp_processor_id()
vlan_tci skb_vlan_tag_get(skb)
vlan_avail skb_vlan_tag_present(skb)
vlan_tpid skb->vlan_proto
rand prandom_u32()
cBPF在一些平台还在使用,这个代码就和用户空间使用的那种汇编是一样的,但是在X86架构,现在在内核态已经都切换到使用eBPF作为中间语言了。由于用户可以提交cBPF的代码,所以首先是将用户提交来的结构体数组进行编译成eBPF代码(提交的是eBPF就不用了)。然后再将eBPF代码转变为可直接执行的二进制。eBPF汇编框架下的bpf语句如下:
// include\uapi\linux\bpf.h
struct bpf_insn {
__u8 code; /* 存放真正的指令码 */
__u8 dst_reg:4; /* 存放指令用到的寄存器号(R0~R10) */
__u8 src_reg:4; /* 同上,存放指令用到的寄存器号(R0~R10) */
__s16 off; /* signed offset 取决于指令的类型*/
__s32 imm; /* 存放立即值 */
};
tcpdump支持使用-d参数来显示过滤规则转换后的bpf汇编指令。在抓包时我们并不关心如何具体的编写struct sock_filter内的东西,因为tcpdump已经内置了这样的功能。如想要对所接受的数据包过滤,只想抓取TCP协议、端口为8080数据包,那么在tcpdump当中的命令就是tcpdump ip and tcp port 8080 。如果你想让tcpdump帮你编译这样的过滤器,则用tcpdump -d 'ip and tcp port 8080',如下案例(参考《Linux内核观测技术BPF》)如下图所示,显示“tcpdump抓取tcp端口8080数据包”的bpf汇编指令:
(000) ldh [12]
(001) jeq #0x800 jt 2 jf 12
(002) ldb [23]
(003) jeq #0x6 jt 4 jf 12
(004) ldh [20]
(005) jset #0x1fff jt 12 jf 6
(006) ldxb 4*([14]&0xf)
(007) ldh [x + 14]
(008) jeq #0x1f90 jt 11 jf 9
(009) ldh [x + 16]
(010) jeq #0x1f90 jt 11 jf 12
(011) ret #262144
(012) ret #0
为了进一步分析这条案例的流程,先把数据包的帧格式和ip数据报的格式放下面便于分析:
以太网帧格式:
IP数据报格式:
(000) ldh [12]:
ldh指令表示累加器在偏移量12处进行加载一个半字(16位),从以太网帧的格式中可以看到偏移量为12字节处为以太网类型字段。
(001) jeq #0x800 jt 2 jf 12:
jeq指令bioassay如果相等则跳转,也就是检查上一条指令返回的以太网类型的值是否为ox800(ipv4的标识),如果为true(jt),就跳转到指令2,否则跳转到指令12。
(002) ldb [23]:
ldb指令在偏移量23字节处进行加载(计算一下:以太网帧的头部是14个字节,那么第23个字节,也就是IP头部的第9个字节),根据IP数据报的格式可以看到第9个字节是“协议”字段。
(003) jeq #0x6 jt 4 jf 12:
jeq指令根据第9个字节的值进行再一次的判断和跳转,如果第9个字节“协议字段”为0x6(TCP),如果是TCP则跳转到下一条指令004,否则跳转到012,数据包进行丢弃。
上面的规则对应代码结构体在Linux内核中的表示其实就是struct sock_filter,在libpcap库中对应的结构体为struct bpf_insn
struct bpf_insn {
u_short code;
u_char jt;
u_char jf;
bpf_u_int32 k;
};
上述使用tcpdump -d看到了过滤规则转换后的bpf汇编指令,上面几个指令:ld开头的表示加载某地址数据,jeq是比较,jt就是jump when true,jf就是jump when false,后面表示行号;tcpdump支持使用-dd参数将匹配信息包的代码以c语言程序段的格式给出:
像c当中的数组的定义,这个就是过滤tcp8080数据包的struct sock_filter的数组代码。
在libpcap 设置过滤规则用到了两个接口,pcap_compile()和 pcap_setfilter ()
pcap_compile 函数的主要工作就是创建一个bpf的结构体,后面pcap_setfilter 会把生产的规则设置到内核,让规则生效。
在进行分析pcap_compile()和 pcap_setfilter ()前,先介绍一个结构体:struct pcap
,介绍后面的过程便于查阅该结构体的成员变量,该结构体在libpcap源码的pcap-int.h中定义,该结构体抓包过程的一个句柄。
struct pcap
{
int fd; /* 文件描述字,实际就是 socket */
/* 在 socket 上,可以使用 select() 和 poll() 等 I/O 复用类型函数 */
int selectable_fd;
int snapshot; /* 用户期望的捕获数据包最大长度 */
int linktype; /* 设备类型 */
int tzoff; /* 时区位置,实际上没有被使用 */
int offset; /* 边界对齐偏移量 */
int break_loop; /* 强制从读数据包循环中跳出的标志 */
struct pcap_sf sf; /* 数据包保存到文件的相关配置数据结构 */
struct pcap_md md; /* 具体描述如下 */
int bufsize; /* 读缓冲区的长度 */
u_char buffer; /* 读缓冲区指针 */
u_char *bp;
int cc;
u_char *pkt;
/* 相关抽象操作的函数指针,最终指向特定操作系统的处理函数 */
int (*read_op)(pcap_t *, int cnt, pcap_handler, u_char *);
int (*setfilter_op)(pcap_t *, struct bpf_program *);
int (*set_datalink_op)(pcap_t *, int);
int (*getnonblock_op)(pcap_t *, char *);
int (*setnonblock_op)(pcap_t *, int, char *);
int (*stats_op)(pcap_t *, struct pcap_stat *);
void (*close_op)(pcap_t *);
/*如果 BPF 过滤代码不能在内核中执行,则将其保存并在用户空间执行 */
struct bpf_program fcode;
/* 函数调用出错信息缓冲区 */
char errbuf[PCAP_ERRBUF_SIZE + 1];
/* 当前设备支持的、可更改的数据链路类型的个数 */
int dlt_count;
/* 可更改的数据链路类型号链表,在 linux 下没有使用 */
int *dlt_list;
/* 数据包自定义头部,对数据包捕获时间、捕获长度、真实长度进行描述 [pcap.h] */
struct pcap_pkthdr pcap_header;
};
/* 包含了捕获句柄的接口、状态、过滤信息 [pcap-int.h] */
struct pcap_md {
/* 捕获状态结构 [pcap.h] */
struct pcap_stat stat;
int use_bpf; /* 如果为1,则代表使用内核过滤*/
u_long TotPkts;
u_long TotAccepted; /* 被接收数据包数目 */
u_long TotDrops; /* 被丢弃数据包数目 */
long TotMissed; /* 在过滤进行时被接口丢弃的数据包数目 */
long OrigMissed; /*在过滤进行前被接口丢弃的数据包数目*/
#ifdef linux
int sock_packet; /* 如果为 1,则代表使用 2.0 内核的 SOCK_PACKET 模式 */
int timeout; /* pcap_open_live() 函数超时返回时间*/
int clear_promisc; /* 关闭时设置接口为非混杂模式 */
int cooked; /* 使用 SOCK_DGRAM 类型 */
int lo_ifindex; /* 回路设备索引号 */
char *device; /* 接口设备名称 */
/* 以混杂模式打开 SOCK_PACKET 类型 socket 的 pcap_t 链表*/
struct pcap *next;
#endif
};
//函数用于将用户制定的过滤策略编译成BPF代码,然后存入bpf_program结构中
/*
p:pcap_open_live()返回的pcap_t类型的指针
fp:存放编译后的bpf
buf:过滤规则
optimize:是否需要优化过滤表达式
mask:指定本地网络的网络掩码,不需要时可以设置为0
*/
pcap_compile(pcap_t *p,struct bpf_program *fp,char *filterstr,int opt,bpf_u_int32 netmask);
//将过滤的规则注入内核
int pcap_setfilter (pcap_t *p, struct bpf_program *fp)
函数原型:
int pcap_setfilter(pcap_t *p, struct bpf_program *fp)
{
return (p->setfilter_op(p, fp)); //在Linux中将调用pcap_setfilter_linux
}
调用pcap_setfilter_linux:
static int pcap_setfilter_linux(pcap_t *handle, struct bpf_program *filter)
{
return pcap_setfilter_linux_common(handle, filter, 0);
}
进一步调用pcap_setfilter_linux_common:
static int pcap_setfilter_linux_common(pcap_t *handle, struct bpf_program *filter,
int is_mmapped)
{
......
struct sock_fprog fcode;
......
if (install_bpf_program(handle, filter) < 0)//把BPF代码拷贝到pcap_t 数据结构的fcode上
......
/*Linux内核设置过滤器时使用的数据结构是sock_fprog,而不是BPF的结构bpf_program,因此应做结构之间的转换*/
switch (fix_program(handle, &fcode, is_mmapped)) {
//严重错误直接退出
case -1:
default:
return -1;
//通过检查,但不能工作在内核中
case 0:
can_filter_in_kernel = 0;
break;
//BPF可以在内核中工作
case 1:
can_filter_in_kernel = 1;
break;
}
}
//通过检查后,如果可以则在内核中安装过滤器
if (can_filter_in_kernel) {
if ((err = set_kernel_filter(handle, &fcode)) == 0)
{
handlep->filter_in_userland = 0;
}
......
return 0;
}
上面涉及到的Linux内核中的struct sock_fprog和libpcap库中的struct bpf_program如下所示:
struct sock_fprog { /* Required for SO_ATTACH_FILTER. */ unsigned short len; /* Number of filter blocks */ struct sock_filter __user *filter; };
struct bpf_program { u_int bf_len; struct bpf_insn *bf_insns;//该结构体上面介绍过,相当于Linux内核中的struct sock_filte };
install_bpf_program(handle, filter)
的拷贝过程:
//该函数主要将libpcap bpf规则结构体,转换成符合liunx内核的bpf规则,同时校验规则是否符合要求格式。
int install_bpf_program(pcap_t *p, struct bpf_program *fp)
{
......
pcap_freecode(&p->fcode);//释放可能存在的BPF代码
prog_size = sizeof(*fp->bf_insns) * fp->bf_len;//计算过滤代码的长度,分配内存空间
p->fcode.bf_len = fp->bf_len;
p->fcode.bf_insns = (struct bpf_insn *)malloc(prog_size);
if (p->fcode.bf_insns == NULL) {
snprintf(p->errbuf, sizeof(p->errbuf),
"malloc: %s", pcap_strerror(errno));
return (-1);
}
//把过滤代码保存在捕获句柄中
memcpy(p->fcode.bf_insns, fp->bf_insns, prog_size);//p->fcode就是struct bpf_program
return (0);
}
pcap_setfilter_linux_common
最终会在set_kernel_filter
中调用setsockopt
系统调用(执行到这才真正进入内核,开始在Linux内核上安装和设置BPF过滤器),通过SO_ATTACH_FILTER 下发给内核底层,从而让规则生效,设置过滤器。
static int set_kernel_filter(pcap_t *handle, struct sock_fprog *fcode)
{
......
ret = setsockopt(handle->fd, SOL_SOCKET, SO_ATTACH_FILTER,
fcode, sizeof(*fcode));
......
}
在liunx上,只需要简单的创建的filter代码,通过SO_ATTTACH_FILTER选项发送到内核,并且filter代码能通过内核的检查,这样你就可以立即过滤socket上面的数据了。
Linux 在安装和卸载过滤器时都使用了函数 setsockopt(),其中标志SOL_SOCKET 代表了对 socket 进行设置,而 SO_ATTACH_FILTER 和 SO_DETACH_FILTER 则分别对应了安装和卸载。
setsockopt(sockfd, SOL_SOCKET, SO_ATTACH_FILTER, &val, sizeof(val));
setsockopt(sockfd, SOL_SOCKET, SO_DETACH_FILTER, &val, sizeof(val));
Linux内核在sock_setsockopt
函数中进行设置:
//: net\core\sock.c
int sock_setsockopt(struct socket *sock, int level, int optname,
char __user *optval, unsigned int optlen)
{
......
case SO_ATTACH_FILTER:
ret = -EINVAL;
if (optlen == sizeof(struct sock_fprog)) {
struct sock_fprog fprog;
ret = -EFAULT;
//把过滤条件结构体从用户空间拷贝到内核空间
if (copy_from_user(&fprog, optval, sizeof(fprog)))
break;
//在socket上安装过滤器
ret = sk_attach_filter(&fprog, sk);
}
break;
......
case SO_DETACH_FILTER:
//解除过滤器
ret = sk_detach_filter(sk);
break;
......
}
上面出现的 sk_attach_filter()
定义在 net/core/filter.c,它把结构sock_fprog 转换为结构 sk_filter, 最后把此结构设置为 socket 的过滤器:sk->filter = fp。
回到文章开始(抓包的引入):
static int packet_rcv(struct sk_buff *skb, struct net_device *dev,
struct packet_type *pt, struct net_device *orig_dev)
{
......
res = run_filter(skb, sk, snaplen); //将用户指定的过滤条件使用BPF进行过滤
......
__skb_queue_tail(&sk->sk_receive_queue, skb);//将skb放到当前的接收队列中
......
}
run_filter:
static unsigned int run_filter(struct sk_buff *skb,const struct sock *sk,unsigned int res)
{
struct sk_filter *filter;
rcu_read_lock();
filter = rcu_dereference(sk->sk_filter);//获取之前设置的过滤器
if (filter != NULL)
res = bpf_prog_run_clear_cb(filter->prog, skb);//进行数据包过滤
rcu_read_unlock();
return res;
}
综上,结合上一篇[文章] ,和本文就将Linux内核角度将tcpdump的工作原理分析完毕。
本文由哈喽比特于3年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/KXODO0IYtiQ9CyoR1BpYZg
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。