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的例子,并在工程中使用。
相关推荐
矛取矛求2 小时前
Linux如何更优质调节系统性能
linux
内核程序员kevin3 小时前
在Linux环境下使用Docker打包和发布.NET程序并配合MySQL部署
linux·mysql·docker·.net
kayotin4 小时前
Wordpress博客配置2024
linux·mysql·docker
Ztiddler4 小时前
【Linux Shell命令-不定期更新】
linux·运维·服务器·ssh
小小不董4 小时前
Oracle OCP认证考试考点详解082系列16
linux·运维·服务器·数据库·oracle·dba
a1denzzz5 小时前
Linux系统的网络设置
linux·服务器·网络
ac.char5 小时前
在CentOS下安装RabbitMQ
linux·centos·rabbitmq
m0_519523106 小时前
Linux——简单认识vim、gcc以及make/Makefile
linux·运维·vim
mit6.8246 小时前
[Docker#4] 镜像仓库 | 部分常用命令
linux·运维·docker·容器·架构
zyp2468106 小时前
Linux之DNS服务器
linux·运维·服务器