拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Knock源码剖析(二):BPF过滤器如何生成,让守护进程只看见敲门包

Knock源码剖析二BPF过滤器如何生成让守护进程只看见敲门包【免费下载链接】knockA port-knocking daemon项目地址: https://gitcode.com/gh_mirrors/knock/knockKnock 是一个开源的端口敲击Port Knocking实现其守护进程 knockd 监听网卡、识别预设的端口敲击序列并动态放行防火墙规则。本文深入剖析它的核心机制knockd 如何生成 BPFBerkeley Packet Filter伯克利包过滤器并加载到内核使抓包层只看见敲门包大幅降低 CPU 消耗并缩小攻击面。一、为什么需要 BPF 过滤器 knockd 基于 libpcap 构建主循环非常简洁cap pcap_open_live(o_int, 65535, 0, 50, pcap_err); /* 打开抓包句柄 */ ... ret pcap_dispatch(cap, -1, sniff, NULL); /* 每收到包就回调 sniff() */打开抓包句柄src/knockd.c分发循环src/knockd.c如果不加过滤器接口上的每一个包——ARP、DNS、视频流、IM 心跳——都会被拷贝到用户态逐个交给sniff()处理绝大多数是无意义的开销。BPF 正是解决方案把我关心什么写成一条过滤表达式编译成字节码后由内核或驱动层执行只有匹配字节码的包才会送到用户态。也就是说knockd 平时处于睡眠状态只有潜在的敲门包到达时才被唤醒。二、生成流程总览全部过滤逻辑集中在generate_pcap_filter()一个函数中分为四步步骤做什么关键位置① 收集本机 IP生成dst host ...地址条件src/knockd.c② 逐门生成子过滤器每扇门的端口 TCP 标志条件src/knockd.c③ 拼接完整过滤器各门表达式用or连接IPv4/IPv6 各一套src/knockd.c④ 编译并加载pcap_compile()→pcap_setfilter()src/knockd.c一个细节每个门配置文件中的一个[section]在结构体opendoor_t中持有两个过滤器表达式指针——pcap_filter_expIPv4与pcap_filter_expv6IPv6见 src/knockd.c双栈网络下两套规则各自独立生成。三、第一步先锁定目的地过滤器必须以地址条件开头只接受发往本机的包。knockd 在启动时通过getifaddrs()收集监听接口的所有 IP存进myips链表src/knockd.c然后逐门拼接若该门配置了Target指令则用指定 IP否则用接口上的每个本机 IP用or连接。最终生成类似这样的前缀示意((dst host 192.168.1.10 or dst host 192.168.1.11) and ( ... ))and (之后的内容才进入第二步的端口与标志匹配。四、第二步端口序列 TCP 标志 → 子过滤器knockd 从配置文件示例见 knockd.conf读取每扇门的SEQUENCE与TCPFLAGS[openSSH] sequence 7000,8000,9000 tcpflags syn生成逻辑可以概括为三件事把序列中所有TCP 端口拼成((tcp dst port 7000 or 8000 or 9000)按TCPFLAGS追加标志条件后补上)再把所有UDP 端口以or (udp dst port ...)追加在后。TCPFLAGS的三态设计DONT_CARE / SET / NOT_SET见 src/knockd.c是整个过滤器的点睛之笔配置写法源码状态生成的 BPF 条件synSETtcp[tcpflags] tcp-syn ! 0必须携带!ackNOT_SETtcp[tcpflags] tcp-ack 0必须不携带未指定DONT_CARE不生成条件不过滤正因为支持必须不携带tcpflags syn,!ack这类精确写法才成为可能。此外还有一个 IPv6 的特殊处理老版本 libpcap 的 IPv6 包不支持tcp[tcpflags]符号源码改用裸字节偏移替代例如 src/knockd.c/* IPv6 用原始偏移读取标志位而非 tcp[tcpflags] */ bufsize realloc_strcat(buffer, and ip6[1340] tcp-syn , bufsize);整个拼接过程都借助自研的realloc_strcat()初始只分配 200 字节不够时自动倍增扩容src/knockd.c。对序列8000,8001,8002:tcp 4000,4001:udp要求 syn的单扇门生成的子过滤器大致是((dst host 192.168.1.10) and ( ((tcp dst port 8000 or 8001 or 8002) and tcp[tcpflags] tcp-syn ! 0) or (udp dst port 4000 or 4001) ))五、第三、四步拼接、编译、加载内核多扇门之间即使有端口重叠源码也不去重而是把所有子过滤器用or直接串起来交给 libpcap 自行优化。源码注释里给出的双门完整示例src/knockd.cdst host 1.2.3.4 and ( ((tcp dst port 8000 or 8001 or 8002) and tcp[tcpflags] tcp-syn ! 0) or (udp dst port 4000 or 4001) or ((tcp dst port 1234 or 4567 or 8901) and tcp[tcpflags] tcp-syn ! 0 and tcp[tcpflags] tcp-ack ! 0) )最后三行才是真正的生效动作src/knockd.cpcap_compile(cap, bpf_prog, buffer, 1, 0); /* 第3参数1启用编译优化 */ pcap_setfilter(cap, bpf_prog); /* 字节码装入内核 */ pcap_freecode(bpf_prog); /* 释放临时字节码 */自此pcap_dispatch()只会收到敲门包——守护进程只看见敲门包靠的就是这寥寥数行。六、过滤器会在哪三个时机重新生成 ⚙️过滤器并非一次性的源码中有三个入口会触发重新生成启动时解析完配置、拿到接口 IP 后先生成过滤器src/knockd.c配置热重载收到 SIGHUP 后reload()清空所有门、重读配置再重新生成src/knockd.c一次性序列轮换使用one_time_sequences时每次成功敲门后旧序列被禁用、读取新序列此时释放该门旧表达式、置 NULL 后重新生成src/knockd.c——这正是每扇门持有表达式指针而非固定字符串的原因。因此函数内部有个modified_filters标志只有当确有门的表达式发生变化时才重新pcap_compilepcap_setfilter否则直接跳过避免无谓的重编译开销。七、小结generate_pcap_filter()是 knockd 抓包性能优化的枢纽地址条件 逐门端口/标志条件 →or拼接 → 编译 → 装入内核DONT_CARE / SET / NOT_SET三态设计让tcpflags同时支持必须携带与必须不携带的匹配pcap_filter_exp / pcap_filter_expv6双表达式指针既支持 IPv4/IPv6 独立过滤也支撑一次性序列的热更新这一切只为保证一件事sniff()不会因为任何一个无关数据包而醒来。相关资源守护进程主源码src/knockd.c配置文件示例knockd.conf完整参数手册doc/knockd.1.in客户端敲门方源码src/knock.c【免费下载链接】knockA port-knocking daemon项目地址: https://gitcode.com/gh_mirrors/knock/knock创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门