tcpdump 语法在ebpf中的支持

边学边做,法力无边。

tcpdump抓包

tcpdump是网络领域de.facto的工具和标准,网工们往往也比较熟悉其抓包语法。 tcpdump的实现是基于cbpf的vm来实现高性能地抓包。 众多文档都会提到cbpf的后继者ebpf,但当越来越多的ebpf网络程序和dpdk网络程序出现后,抓包往往只能是各个技术栈内提供,好在都有一些解决方案,比如

实现原理

一、tcpdump 表达式 到 字节码

dpdk的dumpcap和xdp的xdpcap实现略有不同,但大体思路一致。

首先都是利用pcap库对tcpdump 表达式进行翻译,得到cbpf的字节码。

C 复制代码
    pcap = pcap_open_dead(DLT_EN10MB, intf->opts.snap_len);
    if (!pcap)
            rte_exit(EXIT_FAILURE, "can not open pcap\n");

    if (pcap_compile(pcap, &bf, intf->opts.filter,
                     1, PCAP_NETMASK_UNKNOWN) != 0) {
            fprintf(stderr,
                    "Invalid capture filter \"%s\": for interface '%s'\n",
                    intf->opts.filter, intf->name);
            rte_exit(EXIT_FAILURE, "\n%s\n",
                     pcap_geterr(pcap));
    }

cbpf 的字节码很好阅读。cbpf的指令也能看出来比较简化易懂。

比如 tcpdump -d ip 可以看到表达式 ip 过滤条件对应的cbpf字节码

scss 复制代码
(000) ldh      [12]
(001) jeq      #0x800           jt 2	jf 3
(002) ret      #262144
(003) ret      #0

从000开始执行,首先到第12字节位置夹在两个字节长度到寄存器(内存),然后比较这个双字节长度的数值是否等于0x0800.如果等于则跳转到002,不等则跳转到003. 这两个位置都是直接执行return,一个返回0,一个返回非0.

了解更多可以看内核cbpf文档 www.kernel.org/doc/Documen...

二、cbpf字节码翻译

dpdk是利用其内部集成的ubpf库,直接将cbpf翻译到了ebpf,并在其vm解释执行。

xdpcap则是利用github.com/cloudflare/... 实现了一个翻译为C代码的过程。而后利用ebpf的编译器实现c 代码翻译到 ebpf并执行的过程。 xdpcap由于其本身golang编译自带goebpf,所以这里就没有什么工作量了。

如果我们想要为bcc实现一个类似的功能,方便在在python代码来import使用。可以阅读cbpf文档,并实现反向逻辑。 由于cbpf指令有限,我们仅需要实现以下指令的翻译。

css 复制代码
    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

举例而言,

bash 复制代码
ret x ,直接翻译为 return x
ldh pos ,整个ld都是赋值,h代表位宽,翻译为 *((u16 *)({data} + {pos})))
jeq x, 跳转系列就是考虑不同的跳转条件。
add/sub/mul/div/mod/neg 四则运算
and/or/xor/lsh/rsh 逻辑运算
另外还有st/stx,tax/txa 涉及的赋值

ld系列和最后的st/stx,tax/txa这些操作要额外理解cbpf的vm内定义了A,X,M 主要寄存器,其中M有16个,操作就是拷贝来拷贝去。

arduino 复制代码
    A                32 bit wide accumulator
    X                32 bit wide X register
    M[]              16 x 32 bit wide misc registers aka "scratch memory
                    store", addressable from 0 to 15

到这里,cbpf已经没有其他不能解释清楚的了。我们既可以实现一个cbpf的vm,也可以实现反向翻译。

题外话,如果要实现一个ebpf的vm,情况就会稍微复杂。

翻译为C,首先定义一个函数给bcc编译器用

rust 复制代码
static inline u32
cbpf_filter_func (const u8 *const data, const u8 *const data_end) {
    __attribute__((unused)) u32 A, X, M[16];
    __attribute__((unused)) const u8 *indirect;
    // 翻译的程序体
}

具体翻译代码就不详细介绍,参看源码 github.com/junka/pycbp...

到这里如果我们写一个 bcc 脚本, 用%s 预留一个过滤函数cbpf_filter_func的实现,这个是cbpf2c生成,

scss 复制代码
BPFTEXT = """
#include <linux/skbuff.h>
#define MAX_PACKET_LEN (128)

struct filter_packet {
	u8 packet[MAX_PACKET_LEN];
};

BPF_PERF_OUTPUT(filter_event);

%s

int filter_packets (struct pt_regs *ctx) {
	struct filter_packet e = { };
	struct sk_buff *skb;
	u32 datalen = 0;
	u32 ret = 0;
	u8 *data;

	skb = (struct sk_buff*)PT_REGS_PARM1(ctx);
	data = skb->data;
	datalen = skb->len;

	/* use bpf_probe_read_user for uprobe OR bpf_probe_read_kernel for kprobe */
	if (datalen > MAX_PACKET_LEN) {
		datalen = MAX_PACKET_LEN;
	}
	bpf_probe_read_kernel(&e.packet, datalen, data);

	/* cbpf filter packet that match */
	ret = cbpf_filter_func(data, data + datalen);
	if (!ret) {
		return 0;
	}

	filter_event.perf_submit(ctx, &e, sizeof(e));
	return 0;
}


"""

之后,在python函数内补充完整cbpf_filter_func函数后,用bcc的attack_kprobe挂在到内核函数,或者attack_upobe,usdt等其他挂在点都可以,实现一个自定义程序内指定函数位置抓包能力。

ini 复制代码
def main(argv=None):
    
    cfunc = 编译得到的函数主体

    text = BPFTEXT % cfun
    bctx = BPF(text=text.encode(), debug=0)

    func_name = b"dev_queue_xmit"
    bctx.attach_kprobe(event=func_name, fn_name=b"filter_packets")

为了保存为pcap格式,方便后面分析,还可以直接用pcap库才做来保存过滤报文。

scss 复制代码
    pcap_dev = pcap.open_dead(pcap.DLT_EN10MB, 1000)

    dumper = pcap.dump_open(pcap_dev, ctypes.c_char_p(args.file.encode("utf-8")))

    print(f"Capturing packets from {func_name}... Hit Ctrl-C to end")

    counter = 0

    def filter_events_cb(_cpu, data, _size):
        nonlocal counter
        counter += 1
        event = ctypes.cast(data, ctypes.POINTER(FilterPacket)).contents
        now = time.time()
        sec = int(now)
        usec = int((now - sec) * 1e6)
        tval = pcap.timeval(sec, usec)
        hdr = pcap.pkthdr(tval, 100, 100)
        pcap.dump(
            ctypes.cast(dumper, ctypes.POINTER(ctypes.c_ubyte)), hdr, event.packet
        )

    bctx[b"filter_event"].open_perf_buffer(filter_events_cb)

    while counter < args.count:
        try:
            bctx.perf_buffer_poll(timeout=1000)
        except KeyboardInterrupt:
            pcap.dump_close(dumper)
            pcap.close(pcap_dev)
            sys.exit()
    print(f"{counter} packets cpatured")
    pcap.dump_close(dumper)
    pcap.close(pcap_dev)

至此完结。

知识回顾

  • 学习了cbpf 指令和vm解析
  • 了解了dpdkdumpcap,xdpcap实现
  • 学习了bcc程序如何编写并在指定位置执行
  • 学习了libpcap的用法,compile得到cbpf,dump_pcap实现抓包保存
  • 输出了python下cbpf2c最终可以做ebpf的工具
相关推荐
运维佬1 小时前
CentOS 9 配置网卡
linux·centos
轩轩曲觞阁1 小时前
Linux网络——网络初识
linux·网络
2401_840192271 小时前
python基础大杂烩
linux·开发语言·python
weixin_438197382 小时前
K8S创建云主机配置docker仓库
linux·云原生·容器·eureka·kubernetes
舞动CPU8 小时前
linux c/c++最高效的计时方法
linux·运维·服务器
秦jh_10 小时前
【Linux】多线程(概念,控制)
linux·运维·前端
keep__go11 小时前
Linux 批量配置互信
linux·运维·服务器·数据库·shell
矛取矛求11 小时前
Linux中给普通账户一次性提权
linux·运维·服务器
Fanstay98511 小时前
在Linux中使用Nginx和Docker进行项目部署
linux·nginx·docker
大熊程序猿11 小时前
ubuntu 安装kafka-eagle
linux·ubuntu·kafka