PEG 实现一个DSL小试实战

边学边做,法力无边

PEG (parsing expression grammar), 可以做编程语言设计,也可以设计DSL,类似yacc等BNF方式,但要简单的多。 比如python,zig都是用peg设计的语法.

github.com/ziglang/zig...

PEG语法

它的语法简单,仅记住几种

sql 复制代码
Expr    <- Sum
Sum     <- Product (('+' / '-') Product)*
Product <- Power (('*' / '/') Power)*
Power   <- Value ('^' Power)?
Value   <- [0-9]+ / '(' Expr ')'

这里的规则其实基本就是正则表达式。

  • <- 表示赋值
  • / 表示或者, 但是字符串 match 时候是有顺序的,前面的优先
  • ? 表示匹配0个或一个
  • * 表示匹配0个或多个
  • () 表示是一个group,比如上面的匹配规则生效范围一般需要在某个group范围

实现一个抓包DSL

我们一般用 tcpdump 抓包,但当遇到隧道存在时候,抓内层包需要自己计算一个偏移。 比如希望抓vxlan内层源IP 172.16.0.9,这个过滤条件可以这样指定。 udp[42:4]=0xAC100009 它的意思是从外层UDP开始的地方开始,取相对偏移42字节位置的4个字节来做match。 42 是如何计算的,udp 8 + vxlan 8 + inner ether 14 + inner ip 12 到偏移src ip。

定义 DSL

我们可以实现一个类似scapy的语法,比如这样直接给出报文的格式ETHER/IP/UDP/VXLAN/ETHER/IP(src=172.16.0.9),当然完整的scapy格式也可以支持更多的协议,

这里有协议关键字ETHER等,然后协议关键字后面可以带括号,括号内跟协议内数据结构一些内容,比如src,dst等等。

用peg语法描述,表达式之间是没有顺序要求的,举一个例子,ETHER(src=4a:aa:bb:cc:dd:ee,dst=5b:ee:cc:bb:aa:ff,type=0x0806)

sql 复制代码
EOL             <- '\r\n' / '\n' / '\r'
EOF             <- !.
Comment         <- '#' ( !EOL . )* EOL
Space           <- ' ' / '\t' / EOL
Open            <- '(' Skip
Close           <- ')' Skip
Skip            <- ( Space / Comment )*

Packet       <- ETHER_P / IP_P / UDP_P / VXLAN_P / ICMP_P / TCP_P
ETHER_P         <- 'ETHER'                                                  { YY_ETHER_START(yy); }
                    (Open (
                    ('dst' EQUAL <macaddr (SLASH macaddr)?> COMMA?)         { YY_ETHER_MAC(yy, yytext, false); }
                    / ('src' EQUAL <macaddr (SLASH macaddr)?> COMMA?)       { YY_ETHER_MAC(yy, yytext, true); }
                    / ('type' EQUAL <bits16 (SLASH bits16)?> COMMA?)        { YY_ETHER_TYPE(yy, yytext);}
                    )* Close)?   

Open表示的是左括号,Close表示的是右括号。

当匹配'ETHER'字符串执行action:YY_ETHER_START。

当匹配 '(' 内部的'dst' 或者 'src' 都是用 YY_ETHER_MAC 来对yytext解析,yytext是 < > 符号内匹配的字符串。

当匹配'type' 的时候用 YY_ETHER_TYPE 来对yytext 执行一些定义的action

其他协议字段我们不再赘述。

到这里我们定义了一个language的语法,用peg程序就能生成解析程序,我这里用的是 www.piumarta.com/software/pe... 这个来生成C程序。

实现解释翻译

在这篇文章里面,介绍了cbpf的指令是如何看的 juejin.cn/post/726556...

这里我们要做的类似libpcap的做法,从表达式翻译成cbpf,然后注入内核。

在cbpf过滤报文字段的时候,比如过滤一个EHTER(type=0x0800),可以用tcpdump -d ip 看到

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

意思是比较12字节开始的两个字节内容是否是0x800,是则返回262144,否则返回0.

类似的大多数的过滤内容都是基于ldx + jeq 的组合实现,仅一些不完全的位比较,带mask的比较等实现是需要and 来实现的

对 YY_ETHER_TYPE(yy, yytext) 我们可以定义

scss 复制代码
YY_BPF(yy, (BPF_ABS | BPF_H | BPF_LD), 0, 0, offset + 12);\
YY_BPF(yy, (BPF_JEQ | BPF_K | BPF_JMP), 0, 1, integervalue(yytext)); \

考虑到DSL是stacked layer设计,对隧道还需要过滤内层,用offset记录表示到某一个层的开头,YY_BPF 也是一个宏定义实现的是bpf命令的添加。

arduino 复制代码
#define YY_BPF(yy, code, jt, jf, k)  自己定义实现一个数组或者链表来追加cbpf的数据结构

其他协议如法炮制。

需要注意的是jt, jf 的是相对指令数,如果有两个过滤条件第一个jmp的要加上后面出现的指令数的偏移量。

到这里完成一个简单的DSL到cbpf的转换。

实现一个抓包

上面已经可以组织出来bpf程序,到此为止我们仅剩一步操作,注入内核即可完成抓包过滤的实现。

ini 复制代码
  struct bpf_program prog;
  // 这里是我们封装了上面的逻辑,等于到pcap_compile的替换
  if (depkt_compile(argv[1], &prog)) {
    return -1;
  }
  // 后面的程序都和pcap 的程序一样
  hdl = pcap_open_live(argv[2], 100, 0, 1000, errbuf);
  if (hdl == NULL) {
    fprintf(stderr, "Couldn't open device %s: %s\n", argv[2], errbuf);
    return -1;
  }
 
  if (pcap_setfilter(hdl, &prog) == -1) {
    fprintf(stderr, "Couldn't install filter %s\n",
            pcap_geterr(hdl));
    goto end;
  }

  struct pcap_pkthdr hdr;
  const u_char *packet = pcap_next(hdl, &hdr);
  printf("Capture a packet length %d, type 0x%x\n", hdr.len, packet[12] << 8 | packet[13]);

更进一步,在tcpdump项目中,也仅需要链接我们用peg生成的程序,然后替换下pcap_compile就能实现自定义的类似scapy的语法来抓包了。

再更进一步,scapy的语法,实际上可以用来给dpdk生成rte_flow规则,做bifurcation或者offload设置,这里验证做了部分可以实现一些新的action,来对rte_flow的不同的action进行实现。

感兴趣可以看代码实现,action部分基本都是宏替换。 github.com/junka/j2dep...

回顾小结

  • 学习了PEG的语法,调研的zig/python PEG的定义
  • 用PEG设计了一个类似scapy的DSL
  • 实现了对自定义DSL翻译到cbpf的程序库
  • 了解了libcap/tcpdump的使用,替换实现了自定义cbpf程序可以跑抓包
  • 实现了自定义DSL翻译到rte_flow的规则程序库,并在mlx5上验证
  • 了解了下gtest的例子,并在工程中使用。
相关推荐
Evan芙4 分钟前
Linux 进程状态与进程管理命令
linux·运维·服务器
码农12138号1 小时前
Bugku HackINI 2022 Whois 详解
linux·web安全·ctf·命令执行·bugku·换行符
Joren的学习记录1 小时前
【Linux运维进阶知识】Nginx负载均衡
linux·运维·nginx
用户2190326527351 小时前
Java后端必须的Docker 部署 Redis 集群完整指南
linux·后端
胡先生不姓胡1 小时前
如何获取跨系统调用的函数调用栈
linux
里纽斯3 小时前
RK平台Watchdog硬件看门狗验证
android·linux·rk3588·watchdog·看门狗·rk平台·wtd
chem41113 小时前
魔百盒 私有网盘seafile搭建
linux·运维·网络
早睡的叶子3 小时前
VM / IREE 的调度器架构
linux·运维·架构
兄台の请冷静3 小时前
linux 安装sentinel 并加入systemctl
linux·运维·sentinel
skywalk81634 小时前
postmarketos一个专为智能手机和平板设备设计的开源 Linux 发行版 支持红米2
linux·智能手机·电脑