边学边做,法力无边
PEG (parsing expression grammar), 可以做编程语言设计,也可以设计DSL,类似yacc等BNF方式,但要简单的多。 比如python,zig都是用peg设计的语法.
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的例子,并在工程中使用。