0%

TCPdump 原理与实现

tcpdump 使用 libpcap 这种链路层旁路处理的形式进行包捕获,大大提高了抓包效率。在 这篇文章中 介绍了 tcpdump 的基本使用,本文将介绍其实现的关键 libpcap 和 BPF。BPF 抓包机制将 tcpdump过滤规则 转化为一段 bpf 指令并加载到内核中的bpf虚拟机器上执行,本文主要介绍 cBPF 的基本原理,关于 eBPF 指令集,可以参考 这篇文章。文中设计到的代码可以参考我的 Github

libpcap 实践

libpcap(Packet Capture Library),即数据包捕获函数库,是Unix/Linux平台下的网络数据包捕获函数库,独立于系统的用户层包捕获的API接口,为底层网络监测提供了一个可移植的框架。

利用libpcap函数库开发应用程序的基本步骤:

  1. 捕获各种数据包,例如:网络流量统计。
  2. 过滤网络数据包,例如:过滤掉本地上的一些数据,类似防火墙。
  3. 分析网络数据包,例如:分析网络协议,数据的采集。
  4. 存储网络数据包,例如:保存捕获的数据以为将来进行分析。

安装部署

libpcap库在linux上的安装过程

1
sudo yum install libpcap-devel

测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//demo:查找当前系统的可用网络设备
#include <stdio.h>
#include <pcap.h>

int main(int argc, char *argv[])
{
char *dev,errbuf[1024];

dev = pcap_lookupdev(errbuf); //函数用来查找网络设备

if (dev == NULL){
printf("%s\n",errbuf);
return 0;
}

printf("Device: %s\n", dev);
return 0;
}

编译:

1
2
# 注意编译时加上:-lpcap
$ gcc test.c -o test -lpcap

常见 API

pcap_lookupdev

  • pcap_lookupdev():函数用于查找网络设备,返回可被 pcap_open_live() 函数调用的网络设备名指针。
1
2
3
4
/*errbuf:存放出错信息字符串,宏定义PCAP_ERRBUF_SIZE为错误缓冲区大小
返回值为设备名(第一个合适的网络接口的字符串指针)
*/
char *pcap_lookupdev(char *errbuf)

pcap_lookupnet

  • pcap_lookupnet():函数获得指定网络设备的网络号和掩码。
1
2
3
4
5
6
/*获取指定网卡的ip地址,子网掩码
device:网络设备名
netp:存放ip地址的指针
maskp:存放子网掩码的指针
errbuf:存放出错信息*/
int pcap_lookupnet(char *device, bpf_u_int32 *netp, bpf_u_int32 *maskp, char *errbuf);

pcap_open_live

  • pcap_open_live(): 函数用于打开网络设备,并且返回用于捕获网络数据包的数据包捕获描述字。对于此网络设备的操作都要基于此网络设备描述字。
1
2
3
4
5
6
7
8
9
/*
device: 网络接口名字
snaplen: 数据包大小,最大为65535字节
promise: “1” 代表混杂模式,其它非混杂模式。什么为混杂模式
to_ms:指定需要等待的毫秒数,超过这个数值后,获取数据包的函数就会立即返回(这个函数不会阻塞,后面的抓包函数才会阻塞)。0 表示一直等待直到有数据包到来。
ebuf:存储错误信息。
返回值:pcap_t类型指针,后面的所有操作都要用这个指针
*/
pcap_t *pcap_open_live(const char *device,int snaplen,int promisc,int to_ms,char *ebuf );

pcap_compile

  • pcap_compile(): 函数用于将用户制定的过滤策略编译到过滤程序中。
1
2
3
4
5
6
7
8
9
//函数用于将用户制定的过滤策略编译成BPF代码,然后存入bpf_program结构中
/*
p:pcap_open_live()返回的pcap_t类型的指针
fp:存放编译后的bpf
buf:过滤规则
optimize:是否需要优化过滤表达式
mask:指定本地网络的网络掩码,不需要时可以设置为0
*/
int pcap_compile(pcap_t *p, struct bpf_program *fp, char *str, int optimize, bpf_u_int32 netmask)

pcap_setfilter

  • pcap_setfilter():函数用于设置过滤器。
1
2
3
4
5
/*
p:pcap_open_live() 返回的 pcap_t 类型的指针
fp:pcap_compile() 的第二个参数
*/
int pcap_setfilter( pcap_t * p, struct bpf_program * fp );

pcap_loop

  • pcap_loop()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/*
p:pcap_open_live()返回的 pcap_t 类型的指针
cnt:指定捕获数据包的个数,一旦抓到了 cnt 个数据包,pcap_loop 立即返回。如果是 -1,就会永无休止的捕获,直到出现错误
callback:回调函数,名字任意,根据需要自行起名
user:向回调函数中传递的参数
*/
int pcap_loop( pcap_t *p, int cnt, pcap_handler callback, u_char *user);


/*
userarg:pcap_loop() 的最后一个参数,当收到足够数量的包后 pcap_loop 会调用callback 回调函数,同时将pcap_loop()的user参数传递给它
pkthdr:是收到数据包的 pcap_pkthdr 类型的指针,和 pcap_next() 第二个参数是一样的。
packet :收到的数据包数据
*/
void callback( u_char *userarg, const struct pcap_pkthdr * pkthdr, const u_char * packet)

循环捕获网络数据包,直到遇到错误或者满足退出条件。每次捕获一个数据包就会调用 callback 指定的回调函数,所以,可以在回调函数中进行数据包的处理操作。

pcap_dispatch

  • pcap_dispatch
1
int pcap_dispatch(pcap_t * p, int cnt, pcap_handler callback, u_char * user);

这个函数和 pcap_loop() 非常类似,只是在超过 to_ms 毫秒后就会返回( to_ms 是pcap_open_live() 的第4个参数 )

pcap_close

  • pcap_close():函数用于关闭网络设备,释放资源。
1
void pcap_close(pcap_t *p);

demo 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#include <pcap.h>
#include <time.h>
#include <stdlib.h>
#include <stdio.h>

void getPacket(u_char * arg, const struct pcap_pkthdr * pkthdr, const u_char * packet)
{
int *id = (int *)arg;

printf("id: %d\n", ++(*id));
printf("Packet length: %d\n", pkthdr->len);
printf("Number of bytes: %d\n", pkthdr->caplen);
printf("Recieved time: %s", ctime((const time_t *)&pkthdr->ts.tv_sec));

int i;
for(i = 0; i < pkthdr->len; ++i)
{
printf(" %02x", packet[i]);
if( (i + 1) % 16 == 0 )
{
printf("\n");
}
}

printf("\n\n");
}

int main()
{
char errBuf[PCAP_ERRBUF_SIZE];
char *devStr = "eth1";

/* open a device, wait until a packet arrives */
pcap_t * device = pcap_open_live(devStr, 65535, 1, 0, errBuf);

if(!device)
{
printf("error: pcap_open_live(): %s\n", errBuf);
exit(1);
}

/* construct a filter */
struct bpf_program filter;
pcap_compile(device, &filter, "dst port 80", 1, 0);
pcap_setfilter(device, &filter);

/* wait loop forever */
int id = 0;
pcap_loop(device, -1, getPacket, (u_char*)&id);

pcap_close(device);

return 0;
}

演示效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
[root@VM-218-214-centos ~/libpcap]# ./demo
id: 1
Packet length: 74
Number of bytes: 74
Recieved time: Sun Mar 13 16:22:13 2022
fe ee dd 6f 87 46 52 54 00 be 23 7a 08 00 45 00
00 3c d4 70 40 00 40 06 7e a5 09 86 da d6 dc b5
26 94 e2 74 00 50 45 96 05 5d 00 00 00 00 a0 02
39 08 4f cd 00 00 02 04 05 b4 04 02 08 0a d8 9e
d1 2d 00 00 00 00 01 03 03 07

id: 2
Packet length: 54
Number of bytes: 54
Recieved time: Sun Mar 13 16:22:13 2022
fe ee dd 6f 87 46 52 54 00 be 23 7a 08 00 45 00
00 28 d4 71 40 00 40 06 7e b8 09 86 da d6 dc b5
26 94 e2 74 00 50 45 96 05 5e 5b f7 ab 6d 50 10
00 73 92 9d 00 00

# ...

BPF 报文过滤机制

BPF,是类 Unix 系统上数据链路层的一种原始接口,提供原始链路层封包的收发。1992 年,Steven McCanne 和 Van Jacobson 写了一篇名为 The BSD Packet Filter: A New Architecture for User-level Packet Capture 的论文。在文中,作者描述了他们如何在 Unix 内核实现网络数据包过滤,这种新的技术比当时最先进的数据包过滤技术快 20 倍。

BPF 在数据包过滤上引入了两大革新:

  • 一个新的虚拟机 (VM) 设计,可以有效地工作在基于寄存器结构的 CPU 之上
  • 应用程序使用缓存只复制与过滤数据包相关的数据,不会复制数据包的所有信息,这样可以最大程度地减少BPF 处理的数据

从图中可以看出,BPF 是作为内核报文传输路径的一个旁路存在的,当报文到达内核驱动程序后,内核在将报文上送协议栈的同时,会额外将报文的一个副本交给 BPF。之后,报文会经过 BPF 内部逻辑的过滤(当然,这个逻设置),然后最终送给用户程序(比如 tcpdump)

BPF 虚拟机

tcpdump 如何过滤指定的报文呢? 举个例子,当我使用 tcpdump tcp dst port 8080 时,BPF 的过滤逻辑如何将目的端口为 8080 的 TCP 报文过滤出来? 可能最容易想到的方式就是粗暴的硬编码了, 比如像下面这样编写内核模块。

1
2
3
4
5
6
7
8
9
10
11
switch (protocol)
{
case (TCP):
if (dstport != 8080)
drop
......
case (UDP):
......
case (ICMP):
......
}

但是,这样的方式也太傻了,难道每次抓包都需要加载内核模块? 这显然不是 BPF 能成为经典的原因。

BPF 采用的是一种 Pseudo-Machine 的方式。

什么是 Pseudo-Machine ? 我更愿意将这个词翻译为虚拟机,它是 BPF 过滤功能的核心逻辑。这个虚拟机很简单,只包括:

  • 一个累加器( accumulator )
  • 一个索引寄存器 ( index register )
  • 一小段内存空间 ( memory store )
  • 一个隐式的 PC 指针( implicit program counter )

与能够在物理机上直接执行的机器码不同,BPF指令集是可以在BPF虚拟机上执行的指令集。bpf在内核中实际就是一个虚拟机,有自己定义的虚拟机寄存器组。在最早的cBPF汇编框架中的三种寄存器:

1
2
3
4
5
6
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位寄存器,可以自由使用

BPF 指令集

它支持的指令集也非常有限,可分为以下几类

  • LOAD 指令:将一个数值加载入 accumulator 或者 index register,这个值可以为一个立即数( immediate value )、报文的指定偏移、报文长度或者内存空间存放的值
  • STORE 指令:将 accumulator 或者 index register 中存储的值存入内存空间
  • ALU 指令:对 accumulator 存储的数进行逻辑或者算术运算
  • BRANCH 指令:简单的 if 条件控制指令的执行流
  • RETURN 指令:退出虚拟机,若返回 FALSE (0),则表示丢弃该报文
  • 其他指令:accumulator 和 index register 的值的相互传递

BPF Instruction Set

对应的解释,其中第二列为寻址模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
===========      ===================  =====================
指令 寻址模式 解释
=========== =================== =====================
ld 1, 2, 3, 4, 12 Load word into A
ldi 4 Load word into A
ldh 1, 2 Load half-word into A
ldb 1, 2 Load byte into A
ldx 3, 4, 5, 12 Load word into X
ldxi 4 Load word into X
ldxb 5 Load byte into X

st 3 Store A into M[]
stx 3 Store X into M[]

jmp 6 Jump to label
ja 6 Jump to label
jeq 7, 8, 9, 10 Jump on A == <x>
jneq 9, 10 Jump on A != <x>
jne 9, 10 Jump on A != <x>
jlt 9, 10 Jump on A < <x>
jle 9, 10 Jump on A <= <x>
jgt 7, 8, 9, 10 Jump on A > <x>
jge 7, 8, 9, 10 Jump on A >= <x>
jset 7, 8, 9, 10 Jump on A & <x>

add 0, 4 A + <x>
sub 0, 4 A - <x>
mul 0, 4 A * <x>
div 0, 4 A / <x>
mod 0, 4 A % <x>
neg !A
and 0, 4 A & <x>
or 0, 4 A | <x>
xor 0, 4 A ^ <x>
lsh 0, 4 A << <x>
rsh 0, 4 A >> <x>

tax Copy A into X
txa Copy X into A

ret 4, 11 Return
=========== =================== =====================

寻址模式

寻址模式的定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
===============  ===================  ===============================================
寻址模式 语法 解释
=============== =================== ===============================================
0 x/%x Register X
1 [k] BHW at byte offset k in the packet
2 [x + k] BHW at the offset X + k in the packet
3 M[k] Word at offset k in M[]
4 #k Literal value stored in k
5 4*([k]&0xf) Lower nibble * 4 at byte offset k in the packet
6 L Jump label L
7 #k,Lt,Lf Jump to Lt if true, otherwise jump to Lf
8 x/%x,Lt,Lf Jump to Lt if true, otherwise jump to Lf
9 #k,Lt Jump to Lt if predicate is true
10 x/%x,Lt Jump to Lt if predicate is true
11 a/%a Accumulator A
12 extension BPF extension
=============== =================== ===============================================

指令格式

其支持的指令的长度也是固定的:

  • opcode:16bit opcode,其中包括了特定的指令;
  • jt:jump if true
  • jf:jump if false
  • k:多功能字段,存放的什么内容,根据 op 类型来解释

BPF 程序经过 bpf_asm 处理之后变成一个 struct sock_filter 类型的数组 (这个结构体前面介绍过),因此数组中的每个元素都是以如下格式编码。

对应到内核的数据结构:

sock_filter

1
2
3
4
5
6
7
//  include/uapi/linux/filter.h
struct sock_filter { /* Filter block */
__u16 code; /* 真正的 bpf 汇编指令 */
__u8 jt; /* 结果为true时跳转指令 */
__u8 jf; /* 结果为false时跳转 */
__u32 k; /* 指令的参数 */
};

sock_fprog

要实现 socket filtering,需要通过 setsockopt(2) 将一个 struct sock_fprog 指针传递给内核(后面有例子)。 这个结构体的定义:

1
2
3
4
struct sock_fprog {                /* Required for SO_ATTACH_FILTER. */
unsigned short len; /* Number of filter blocks */
struct sock_filter __user *filter;
};

命令解析

tcpdump 支持使用 -d 参数来显示过滤规则转换后的bpf汇编指令。在抓包时我们并不关心如何具体的编写 struct sock_filter 内的东西,因为 tcpdump 已经内置了这样的功能。如想要对所接受的数据包过滤,只想抓取TCP协议、端口为8080数据包,那么在tcpdump当中的命令就是 tcpdump ip and tcp port 8080

以下是使用 tcpdump -d 看到过滤规则转换后的 bpf 汇编指令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ tcpdump -d  "ip and tcp port 8080"
(000) ldh [12] # 加载 Ethernet 偏移 12 字节,即以太网类型字段
(001) jeq #0x800 jt 2 jf 12 # 如果是以太网类型为 0x800 IPv4 则跳到 2, 否则跳到 12
(002) ldb [23] # 加载 Ethernet 偏移 23 字节,也就是 IP 偏移 9 字节,也即传输层协议
(003) jeq #0x6 jt 4 jf 12 # 如果传输层协议是 0x6 (TCP),则跳到 4,否则跳到12
(004) ldh [20] # 加载 Ethernet 偏移 20 字节,也就是 IP 偏移 6 字节,对应于 Fragment Offset
(005) jset #0x1fff jt 12 jf 6 # 如果 Fragment Offset 是全 1,则跳到 12
(006) ldxb 4*([14]&0xf) # 将 IPv4 首部中的 IHL * 4 的值加载到 index register,即得到 IPv4 首部的长度 (为了得到找到 TCP 首部的位置)
(007) ldh [x + 14] # 将 TCP 首部中的 Source Port 的值加载到 accumulator. eg. 不包含 IP 选项时,x = 20, 那么这里就等效于 [34]
(008) jeq #0x1f90 jt 11 jf 9 # 如果端口是 8080(0x1f90),则跳到 11
(009) ldh [x + 16] # 将 TCP 首部中的 Destination Port 的值加载到 accumulator. eg. 不包含 IP 选项时,x = 20, 那么这里就等效于 [36]
(010) jeq #0x1f90 jt 11 jf 12 # 如果端口是 8080(0x1f90),则跳到 11
(011) ret #262144 # 返回非0 表示该报文通过过滤
(012) ret #0 # 返回0 表示该报文需要丢弃

为了进一步分析这条案例的流程,先把数据包的帧格式和ip数据报的格式放下面便于分析:

以太网帧格式:

IP数据报格式:

ip-header-format

tcpdump 支持使用 -dd 参数将匹配信息包的代码以 C 语言程序段的格式给出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ tcpdump -dd  "ip and tcp port 8080"
{ 0x28, 0, 0, 0x0000000c },
{ 0x15, 0, 10, 0x00000800 },
{ 0x30, 0, 0, 0x00000017 },
{ 0x15, 0, 8, 0x00000006 },
{ 0x28, 0, 0, 0x00000014 },
{ 0x45, 6, 0, 0x00001fff },
{ 0xb1, 0, 0, 0x0000000e },
{ 0x48, 0, 0, 0x0000000e },
{ 0x15, 2, 0, 0x00001f90 },
{ 0x48, 0, 0, 0x00000010 },
{ 0x15, 0, 1, 0x00001f90 },
{ 0x6, 0, 0, 0x00040000 },
{ 0x6, 0, 0, 0x00000000 },

像 c 当中的数组的定义,这个就是过滤 tcp 8080 数据包的 struct sock_filter 的数组代码。

预定义 offset

我们最常见的用法莫过于从数据包中取某个字的数据来做判断。按照bpf的规定,我们可以使用偏移来指定数据包的任何位置,而很多协议很常用并且固定,例如端口和ip地址等,bpf就为我们提供了一些预定义的变量,只要使用这个变量就可以直接取值到对应的数据包位置。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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()

以上提到的负 offset 和具体 extension 的 offset,定义见 include/uapi/linux/filter.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/* RATIONALE. Negative offsets are invalid in BPF.
We use them to reference ancillary data.
Unlike introduction new instructions, it does not break
existing compilers/optimizers.
*/
#define SKF_AD_OFF (-0x1000)
#define SKF_AD_PROTOCOL 0
#define SKF_AD_PKTTYPE 4
#define SKF_AD_IFINDEX 8
#define SKF_AD_NLATTR 12
#define SKF_AD_NLATTR_NEST 16
#define SKF_AD_MARK 20
#define SKF_AD_QUEUE 24
#define SKF_AD_HATYPE 28
#define SKF_AD_RXHASH 32
#define SKF_AD_CPU 36
#define SKF_AD_ALU_XOR_X 40
#define SKF_AD_VLAN_TAG 44
#define SKF_AD_VLAN_TAG_PRESENT 48
#define SKF_AD_PAY_OFFSET 52
#define SKF_AD_RANDOM 56
#define SKF_AD_VLAN_TPID 60
#define SKF_AD_MAX 64

#define SKF_NET_OFF (-0x100000)
#define SKF_LL_OFF (-0x200000)

#define BPF_NET_OFF SKF_NET_OFF
#define BPF_LL_OFF SKF_LL_OFF

kernel/bpf/core.c 等地方使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* No hurry in this branch
*
* Exported for the bpf jit load helper.
*/
void *bpf_internal_load_pointer_neg_helper(const struct sk_buff *skb, int k, unsigned int size)
{
u8 *ptr = NULL;

if (k >= SKF_NET_OFF)
ptr = skb_network_header(skb) + k - SKF_NET_OFF;
else if (k >= SKF_LL_OFF)
ptr = skb_mac_header(skb) + k - SKF_LL_OFF;

if (ptr >= skb->head && ptr + size <= skb_tail_pointer(skb))
return ptr;

return NULL;
}

cBPF 与 eBPF 转换

cBPF 在一些平台还在使用,这个代码就和用户空间使用的那种汇编是一样的,但是在X86架构,现在在内核态已经都切换到使用eBPF作为中间语言了。由于用户可以提交 cBPF 的代码,所以首先是将用户提交来的结构体数组进行编译成 eBPF 代码(提交的是eBPF就不用了)。然后再将 eBPF 代码转变为可直接执行的二进制。eBPF 汇编框架下的 bpf 语句如下:

1
2
3
4
5
6
7
8
//  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; /* 存放立即值 */
};

关于 eBPF 的指令集,可以参考我的 这篇文章

加载 BPF 代码

linux 可以通过 setsockopt 接口加载 BFP 代码,从而在内核加上过滤规则

  • 在套接字socket 附加filter规则 :
1
setsockopt(sockfd, SOL_SOCKET, SO_ATTACH_FILTER, &val, sizeof(val));
  • 把filter从socket上移除 :
1
setsockopt(sockfd, SOL_SOCKET, SO_DETACH_FILTER, &val, sizeof(val));

下面一段代码演示了通过 setsockopt 将 BPF 代码加载进内核的方法,里面用到的两个结构体 struct sock_filter 和 struct sock_fprog 在前一节介绍过了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <linux/if_ether.h>
/* ... */

/* From the example above: tcpdump -i em1 port 22 -dd */
struct sock_filter code[] = {
{ 0x28, 0, 0, 0x0000000c },
{ 0x15, 0, 8, 0x000086dd },
{ 0x30, 0, 0, 0x00000014 },
{ 0x15, 2, 0, 0x00000084 },
{ 0x15, 1, 0, 0x00000006 },
{ 0x15, 0, 17, 0x00000011 },
{ 0x28, 0, 0, 0x00000036 },
{ 0x15, 14, 0, 0x00000016 },
{ 0x28, 0, 0, 0x00000038 },
{ 0x15, 12, 13, 0x00000016 },
{ 0x15, 0, 12, 0x00000800 },
{ 0x30, 0, 0, 0x00000017 },
{ 0x15, 2, 0, 0x00000084 },
{ 0x15, 1, 0, 0x00000006 },
{ 0x15, 0, 8, 0x00000011 },
{ 0x28, 0, 0, 0x00000014 },
{ 0x45, 6, 0, 0x00001fff },
{ 0xb1, 0, 0, 0x0000000e },
{ 0x48, 0, 0, 0x0000000e },
{ 0x15, 2, 0, 0x00000016 },
{ 0x48, 0, 0, 0x00000010 },
{ 0x15, 0, 1, 0x00000016 },
{ 0x06, 0, 0, 0x0000ffff },
{ 0x06, 0, 0, 0x00000000 },
};

struct sock_fprog bpf = {
.len = ARRAY_SIZE(code),
.filter = code,
};

sock = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
if (sock < 0)
/* ... bail out ... */

ret = setsockopt(sock, SOL_SOCKET, SO_ATTACH_FILTER, &bpf, sizeof(bpf));
if (ret < 0)
/* ... bail out ... */

/* ... */
close(sock);

以上代码将一个 filter attach 到了一个 PF_PACKET 类型的 socket,filter 的功能是 放行所有 IPv4/IPv6 22 端口的包,其他包一律丢弃。

以上只展示了 attach 代码;detach 时,setsockopt(2) 除了 SO_DETACH_FILTER 不需要其他参数; SO_LOCK_FILTER 可用于防止 filter 被 detach,需要带一个整形参数 0 或 1。

注意 socket filters 并不是只能用于 PF_PACKET 类型的 socket,也可以用于其他 socket 家族。

tcpdump 抓包原理

使用strace追踪

1
2
3
4
5
6
7
8
$ strace tcpdump tcp port 80
# ...
socket(PF_PACKET, SOCK_RAW, 768) = 3
ioctl(3, SIOCGIFINDEX, {ifr_name="lo", ifr_index=1}) = 0
ioctl(3, SIOCGIFHWADDR, {ifr_name="eth1", ifr_hwaddr=52:54:00:be:23:7a}) = 0
ioctl(3, SIOCGIFINDEX, {ifr_name="eth1", ifr_index=2}) = 0
bind(3, {sa_family=AF_PACKET, proto=0x03, if2, pkttype=PACKET_HOST, addr(0)={0, }, 20) = 0
# ...

创建 PF_PACKET 类型 Socket

可以看到tcpdump抓包创建的的套接字类型 AF_PACKET

在libpcap 库源码中也可以看到有调用socket系统调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
tatic int
pcap_can_set_rfmon_linux(pcap_t *handle)
{
...

sock_fd = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
if (sock_fd == -1) {
(void)snprintf(handle->errbuf, PCAP_ERRBUF_SIZE,
"socket: %s", pcap_strerror(errno));
return PCAP_ERROR;
}
...
}

AF_PACKET 和 socket 应用结合一般都是用于抓包分析,packet套接字提供的是L2的抓包能力。

1
socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL))

系统调用:socket

1
2
3
4
5
6
7
8
9
10
SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol)
{
// ......

retval = sock_create(family, type, protocol, &sock);
if (retval < 0)
return retval;

return sock_map_fd(sock, flags & (O_CLOEXEC | O_NONBLOCK));
}

socket 创建函数:

1
2
3
4
int sock_create(int family, int type, int protocol, struct socket **res)
{
return __sock_create(current->nsproxy->net_ns, family, type, protocol, res, 0);
}

__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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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;
......

sock = sock_alloc(); //分配socket结构空间
if (!sock) {
net_warn_ratelimited("socket: no more sockets\n");
return -ENFILE; /* Not exactly a match, but its the
closest posix thing */
}

sock->type = type; //记录socket类型

#ifdef CONFIG_MODULES

if (rcu_access_pointer(net_families[family]) == NULL)
request_module("net-pf-%d", family);
#endif

rcu_read_lock();
pf = rcu_dereference(net_families[family]); //根据family协议簇找到注册的(PF_PACKET)协议族操作表
err = -EAFNOSUPPORT;
if (!pf)
goto out_release;

if (!try_module_get(pf->owner))
goto out_release;
rcu_read_unlock();

err = pf->create(net, sock, protocol, kern); //执行该协议族(PF_PACKET)的创建函数
......

}

Linux内核中定义了 net_proto_family 结构体,用来指明不同的协议族对应的 socket 创建函数,family 字段是协议族的类型,create是创建 socket 的函数,如下是 PF_PACKET 对应结构体。

1
2
3
4
5
static const struct net_proto_family packet_family_ops = {
.family = PF_PACKET,
.create = packet_create,
.owner = THIS_MODULE,
};

packet_create

找到 AF_PACKET 协议族对应的 create 函数:

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

设置了回调函数:packet_rcv,并通过register_prot_hook(sk)完成了注册,其中注册过程将再下面分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
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;
// ......
po = pkt_sk(sk);
sk->sk_family = PF_PACKET;//设置sk协议族为PF_PACKET
po->num = proto; //数据包的类型ETH_P_ALL
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);

/*
* Attach a protocol block
*/

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);//将这个socket挂载到ptype_all连接串列上
}
......
}

packet_sock

1
2
3
4
5
6
7
8
9
// packet_sock结构体
struct packet_sock {
/* struct sock has to be the first member of packet_sock */
struct sock sk;
......
struct net_device __rcu *cached_dev;
int (*xmit)(struct sk_buff *skb);
struct packet_type prot_hook ____cacheline_aligned_in_smp; //packet_create函数中通过该字段进行下一步的设置:po->prot_hook
};

packet_type

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/*po->prot_hook其实packet_type,packet_type结构体:
数据包完成链路层的处理后,需要提交给协议栈上层继续处理,每个packet_type结构就是数据包的一个可能去向
packet_type 结构体第一个type 很重要,对应链路层中2个字节的以太网类型。而dev.c 链路层抓取的包上报给对应模块,就是根据抓取的链路层类型,然后给对应的模块处理,例如socket(PF_PACKET, SOCK_DGRAM, htons(ETH_P_ALL)); ETH_P_ALL表示所有的底层包都会给到PF_PACKET 模块的处理函数,这里处理函数就是packet_rcv 函数。
*/
struct packet_type {
__be16 type; /* type指定了协议的标识符,标记了packet_type收取什么类型的数据包,处理程序func会使用该标识符 ,保存了三层协议类型,ETH_P_IP、ETH_P_ARP等等*/
struct net_device *dev; /* NULL指针表示该处理程序对系统中所有网络设备都有效 */
/* func:packet_create函数通过该字段设置的回调函数:po->prot_hook.func = packet_rcv;
func是该结构的主要成员,它是一个指向网络层函数的指针,ip层处理时挂载的是ip_rcv
*/
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;
};

register_prot_hook(sk)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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);//将pacekt_type放到ptype_all链表上。

sock_hold(sk);
po->running = 1;
}
}

// ptype_all链表:
struct list_head ptype_all __read_mostly;//全局变量

dev_add_pack

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//将pacekt_type放到ptype_all链表上。
void dev_add_pack(struct packet_type *pt)
{
struct list_head *head = ptype_head(pt);//获取ptype_all链表

spin_lock(&ptype_lock);
list_add_rcu(&pt->list, head);//将po->prot_hook挂载到ptype_all链表
spin_unlock(&ptype_lock);
}

//获取ptype_all链表
static inline struct list_head *ptype_head(const struct packet_type *pt)
{
if (pt->type == htons(ETH_P_ALL))//type为ETH_P_ALL时,则挂在ptype_all上面
return pt->dev ? &pt->dev->ptype_all : &ptype_all;
else
return pt->dev ? &pt->dev->ptype_specific :
&ptype_base[ntohs(pt->type) & PTYPE_HASH_MASK];////否则,挂在ptype_base[type&15]上面
}

综上:tcpdump在刚开始工作时创建了 PF_PACKET 套接字,并在全局的 ptype_all 中挂载了该套接字的 pt(packet_type *pt) ,其中pt的字段func设置了相应的回调函数 packet_rcv (后面将分析该函数),到此tcpdump抓包的socket(AF_PACKET)创建完成,相应的准备工作完成。

网络收包时抓包

函数调用关系

  • netif_receive_skb —>
    • netif_receive_skb—>
      • netif_receive_skb_internal->
        • __netif_receive_skb—>
        • __netif_receive_skb_core

__netif_receive_skb_core

1
2
3
4
5
6
7
8
9
10
11
12
static int __netif_receive_skb_core(struct sk_buff *skb, bool pfmemalloc)
{
......
// 遍历ptype_all, tcpdump在创建socket时已将其packet_type挂载到了遍历ptype_all
list_for_each_entry_rcu(ptype, &ptype_all, list) {
if (pt_prev)
ret = deliver_skb(skb, pt_prev, orig_dev);//deliver函数回调用paket_type.func(),也就是packet_rcv
pt_prev = ptype;
}

......
}

__netif_receive_skb_core 函数在遍历ptype_all时,同时也执行了deliver_skb(skb, pt_prev, orig_dev);

deliver_skb

deliver函数调用了packet_type.func(),也就是packet_rcv ,如下源码所示:

1
2
3
4
5
6
7
8
9
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);//调用tcpdump挂载的packet_rcv 函数
}

packet_rcv

下面将展开packet_rcv函数进行分析;函数接收到链路层网口的数据包后,会根据应用层设置的bpf过滤数据包,符合要求的最终会加到struct sock sk 的接收缓存中。使用 BPF 过滤过程将在后面进行分析。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
static int packet_rcv(struct sk_buff *skb, struct net_device *dev,
struct packet_type *pt, struct net_device *orig_dev)
{
......
if (sk->sk_type != SOCK_DGRAM)// 当 SOCK_DGRAM类型的时候,会截取掉链路层的数据包,从而返回给应用层的数据包是不包含链路层数据的
skb_push(skb, skb->data - skb_mac_header(skb));
else if (skb->pkt_type == PACKET_OUTGOING) {
/* Special case: outgoing packets have ll header at head */
skb_pull(skb, skb_network_offset(skb));
}
}
......
//最后将底层网口符合应用层的数据复制到接收缓存队列中

res = run_filter(skb, sk, snaplen); //将用户指定的过滤条件使用BPF进行过滤
......
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);//将skb放到当前的接收队列中
spin_unlock(&sk->sk_receive_queue.lock);
sk->sk_data_ready(sk);
return 0;
......

}

packet_recvmsg

综上一旦关联上链路层抓到的包就会 copy 一份给上层接口(即PF_PACKET 注册的回调函数packet_rev)。而回调函数会根据应用层设置的 bpf 过滤数据包,最终放入接收缓存的数据包肯定是符合应用层想截取的数据。因此最后一步 recvfrom 也就是从接收缓存的数据包copy给应用层,如下源码:

1
2
3
4
5
6
7
8
9
10
11
12
static int packet_recvmsg(struct socket *sock, struct msghdr *msg, size_t len,
int flags)
{
// ......

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

// ......

err = skb_copy_datagram_msg(skb, 0, msg, copied);//将最终的数据copy到用户空间
// ......
}

到这,网络接收数据包时的抓包过程就结束了

网络发包时抓包

Linux协议栈中提供的报文发送函数有两个:

  • 一个是链路层提供给网络层的发包函数 dev_queue_xmit()
  • 一个是软中断发包函数之间调用的 sch_direct_xmit()
  • 这两个函数最终都会调用 dev_hard_start_xmit()

dev_hard_start_xmit

1
2
3
4
5
6
7
8
9
10
11
struct sk_buff *dev_hard_start_xmit(struct sk_buff *first, struct net_device *dev,
struct netdev_queue *txq, int *ret)
{
......
while (skb) {
struct sk_buff *next = skb->next;

skb->next = NULL;
rc = xmit_one(skb, dev, txq, next != NULL);//调用xmit_one来发送一个到多个数据包
......
}

xmit_one

发送一个到多个数据包

1
2
3
4
5
6
7
8
9
static int xmit_one(struct sk_buff *skb, struct net_device *dev,
struct netdev_queue *txq, bool more)
{
......

if (!list_empty(&ptype_all) || !list_empty(&dev->ptype_all))
dev_queue_xmit_nit(skb, dev);//通过调用dev_queue_xmit这个网络设备接口层函数发送给driver,
......
}

dev_queue_xmit_nit

将数据包发送给driver

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void dev_queue_xmit_nit(struct sk_buff *skb, struct net_device *dev)
{
......
list_for_each_entry_rcu(ptype, ptype_list, list) {
/* Never send packets back to the socket
* they originated from - MvS (miquels@drinkel.ow.org)
*/
if (skb_loop_sk(ptype, skb))
continue;

if (pt_prev) {
deliver_skb(skb2, pt_prev, skb->dev);
pt_prev = ptype;
continue;
}
......
}

在遍历 ptype_all 时,同时也执行了deliver_skb(skb, pt_prev, orig_dev)

deliver_skb

deliver函数调用了paket_type.func(),也就是packet_rcv ,如下源码所示:

1
2
3
4
5
6
7
8
9
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);//调用tcpdump挂载的 packet_rcv 函数
}

下面的流程就和网络收包时tcpdump进行抓包一样了:

  • packet_rcv 函数中会将用户设置的过滤条件,通过BPF进行过滤,并将过滤的数据包添加到接收队列中,应用层在libpcap库中调用recvfrom 。
  • PF_PACKET 协议簇模块调用packet_recvmsg 将接收队列中的数据copy应用层

BPF 虚拟机过滤报文

如前所述,tcpdump 根据通过 pcap_compile 将用户传来的过滤规则编译成 BPF 代码,然后通过 pcap_setfilter将 BPF程序注入到内核。在网络收发包路径中会遍历 ptype_all 链表,从而调用到 packet_rcv 函数,进而执行到对应的 BPF 代码根据用户指定的过滤条件进行过滤。其中最关键的函数即是 run_filter

本节围绕的重点是:BPF的过滤原理,如下源码所示:run_filter(skb, sk, snaplen),本次文章将对BPF的过滤原理进行一些分析。

1
2
3
4
5
6
7
8
9
10
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放到当前的接收队列中
// ......
}

pcap_setfilter_linux

libpcap 的 pcap_setfilter_linux 在 linux 平台中会调用 pcap_setfilter_linux

1
2
3
4
static int pcap_setfilter_linux(pcap_t *handle, struct bpf_program *filter)
{
return pcap_setfilter_linux_common(handle, filter, 0);
}

进一步调用 pcap_setfilter_linux_common:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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 如下所示:

1
2
3
4
5
6
7
8
9
10
11
// linux kernel
struct sock_fprog { /* Required for SO_ATTACH_FILTER. */
unsigned short len; /* Number of filter blocks */
struct sock_filter __user *filter;
};

// libpcap
struct bpf_program {
u_int bf_len;
struct bpf_insn *bf_insns;//该结构体上面介绍过,相当于Linux内核中的struct sock_filte
};

install_bpf_program

install_bpf_program(handle, filter) 把BPF代码拷贝到 pcap_t 数据结构的fcode上:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//该函数主要将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);
}

set_kernel_filter

pcap_setfilter_linux_common最终会在set_kernel_filter中调用setsockopt系统调用。函数执行到这才真正进入内核,开始在Linux内核上安装和设置BPF过滤器,通过 SO_ATTACH_FILTER 下发给内核底层,从而让规则生效,设置过滤器。

1
2
3
4
5
6
7
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 上面的数据了。

setsockopt

Linux 在安装和卸载过滤器时都使用了函数 setsockopt(),其中标志 SOL_SOCKET 代表了对 socket 进行设置,而 SO_ATTACH_FILTER 和 SO_DETACH_FILTER 则分别对应了安装和卸载。

  • 在套接字socket 附加filter规则 :
1
setsockopt(sockfd, SOL_SOCKET, SO_ATTACH_FILTER, &val, sizeof(val));
  • 把filter从socket上移除 :
1
setsockopt(sockfd, SOL_SOCKET, SO_DETACH_FILTER, &val, sizeof(val));

Linux内核在sock_setsockopt函数中进行设置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 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。

run_filter

回到抓包的引入:

1
2
3
4
5
6
7
8
9
10
11
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:

1
2
3
4
5
6
7
8
9
10
11
12
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;
}

而 run_filter 的功能就是取下 sk 上设置的 sk_filter 结构, 然后最终执行了 BPF_PROG_RUN

BPF_PROG_RUN

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static inline u32 bpf_prog_run_clear_cb(const struct bpf_prog *prog,
struct sk_buff *skb)
{
u8 *cb_data = bpf_skb_cb(skb);
u32 res;

if (unlikely(prog->cb_access))
memset(cb_data, 0, BPF_SKB_CB_LEN);

preempt_disable();
res = BPF_PROG_RUN(prog, skb);
preempt_enable();
return res;
}

原理总结

tcpdump进行抓包的内核流程梳理

  • 应用层通过libpcap库:
    • 调用系统调用创建socket,sock_fd = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
    • tcpdump在socket创建过程中创建packet_type(struct packet_type),并挂载到全局的ptype_all链表上。
    • 同时在packet_type设置回调函数packet_rcv
  • 网络收包/发包时,会在各自的处理函数中遍历ptype_all链表,并同时执行其回调函数,这里tcpdump的注册的回调函数就是packet_rcv
    • 收包时:__netif_receive_skb_core
    • 发包时:dev_queue_xmit_nit
  • packet_rcv函数中会将用户设置的过滤条件,通过BPF进行过滤,并将过滤的数据包添加到接收队列中
  • 应用层调用recvfrom 。 PF_PACKET 协议簇模块调用packet_recvmsg 将接收队列中的数据copy应用层,到此将数据包捕获到。

参考资料