FreeBSD上的ipfilter防火墙以规则文件为核心,所有的过滤逻辑都写在ipf.conf里。规则一多,靠肉眼去逐条阅读就非常吃力,尤其是接手别人留下的配置时,往往要花不少时间才能理清数据包到底在哪一条规则上被放行或拦截。如果能把ipf的文本规则自动转换成一张可视化的链路图片,理解成本会大幅降低。本文就介绍一种纯Node.js的实现思路,从读取规则、解析字段到最终输出图片,整套流程都可以在服务端完成。

一、读取ipfilter规则的几种方式
要拿到当前系统生效的ipf规则,最直接的办法是调用ipfstat命令。不带参数时它输出统计摘要,加上-io参数则分别列出入站和出站的过滤规则。Node.js中可以通过内置的child_process模块执行命令并捕获标准输出,这种方式的优点是不依赖任何第三方库,代码也非常简短。
const { execFile } = require('child_process');
function loadIpfRules() {
return new Promise((resolve, reject) => {
// -i 表示入站规则,-o 表示出站规则,这里合并读取
execFile('ipfstat', ['-io'], (err, stdout) => {
if (err) return reject(err);
// 按行拆分,过滤空行和注释
const lines = stdout.split('\n')
.map(l => l.trim())
.filter(l => l.length > 0 && !l.startsWith('#'));
resolve(lines);
});
});
}除了调用命令,也可以直接读取/etc/ipf.conf文件。两种方式各有优劣:读配置文件不需要root权限,而且能看到注释信息,但文件内容可能与内核中实际加载的规则不一致;调用ipfstat则反映的是内存中真实生效的规则,但通常需要相应的执行权限。在生产环境做可视化时,建议优先使用ipfstat,确保看到的就是防火墙当前的行为。
还有一种做法是通过/dev/ipfilter设备接口配合ioctl获取规则,这属于比较底层的方案,实现复杂度高,除非有特殊性能要求,一般不推荐在Node.js层面尝试。前两种方式已经能覆盖绝大多数场景。
二、用正则表达式解析规则字段
一条典型的ipf规则长这样:block in quick on em0 proto tcp from any to 192.168.1.10 port = 22。它包含动作(block或pass)、方向(in/out)、quick关键字、接口名、协议、源地址、目标地址和端口等字段。解析的目标就是把这些字段提取成结构化对象,方便后续绘图时使用。
const RULE_RE = /^(block|pass)\s+(in|out)\s+(quick)?\s*(?:on\s+(\S+))?\s*(
?:proto\s+(\w+))?\s*from\s+(\S+)\s*(?:port\s+(\S+))?\s*to\s+(\S+)\s*(?:port\s+(\S+))?/x;
function parseRule(line) {
const m = line.match(RULE_RE);
if (!m) return null;
return {
raw: line,
action: m[1], // block 或 pass
dir: m[2], // in 或 out
quick: !!m[3], // 是否 quick 短路
iface: m[4] || '*',
proto: m[5] || 'any',
src: m[6],
srcPort: m[7] || 'any',
dst: m[8],
dstPort: m[9] || 'any'
};
}有几个细节需要注意。ipf规则中地址部分可能出现any、单个IP、CIDR网段,甚至带有port <>这类范围写法,正则不可能一次覆盖所有语法,实际项目中建议把地址和端口字段先整体捕获,再拆开做二次解析。另外,规则可能以@加编号开头,解析前记得把这类前缀剥掉。解析失败时不要直接抛异常,而是收集到一个unparsed数组里,绘图时单独标注,避免一条异常规则导致整个流程中断。
还要提一下quick关键字的重要性。ipf默认是“最后匹配优先”,即数据包会遍历所有规则,以最后一条命中的为准;而带quick的规则一旦匹配就立即生效。绘图时应该用明显的高亮或分支箭头体现quick规则的短路效果,否则图片传递的信息会有偏差。
三、生成可视化图片的两种方案
拿到结构化数据后,接下来就是把规则画成图片。第一种方案是使用node-canvas,它提供了与浏览器Canvas几乎一致的2D绘图API,可以直接输出PNG。安装时依赖系统层的Cairo库,在FreeBSD上可以通过pkg install cairo装好依赖后再npm install canvas。这种方案控制力最强,画布大小、配色、连线走向都由自己决定。
const { createCanvas } = require('canvas');
function renderRules(rules, outPath) {
const rowH = 46, width = 900;
const canvas = createCanvas(width, 40 + rules.length * rowH);
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#fafafa';
ctx.fillRect(0, 0, canvas.width, canvas.height);
rules.forEach((r, i) => {
const y = 30 + i * rowH;
// block 用红色,pass 用绿色
ctx.fillStyle = r.action === 'block' ? '#e74c3c' : '#27ae60';
ctx.fillRect(20, y, 8, rowH - 14);
ctx.fillStyle = '#333';
ctx.font = '14px monospace';
ctx.fillText(r.raw, 40, y + 22, width - 60);
if (r.quick) {
ctx.fillStyle = '#f39c12';
ctx.fillText('quick', width - 70, y + 22);
}
});
require('fs').writeFileSync(outPath, canvas.toBuffer('image/png'));
}第二种方案是先生成SVG字符串再转成PNG,例如借助sharp库完成转换。SVG的好处是纯文本、可读性好、方便嵌入到网页或报告中,而且不需要编译原生依赖,部署到CI环境更省事。缺点是精细排版要手写坐标计算,规则特别多时维护起来略麻烦。
如果只是给团队内部看,也可以停留在SVG阶段不转PNG,浏览器直接就能渲染。选择哪种方案主要看输出场景:需要定时生成报表归档就用node-canvas直接出PNG,需要嵌入Web页面就用SVG。
四、实践中的常见坑点
第一个坑是权限问题。ipfstat -io在部分系统配置下需要root才能完整输出,如果Node.js进程以普通用户运行,拿到的可能是空结果而不是报错,容易被误判为“没有规则”。解决办法是提前校验输出长度,或者在服务端用sudo白名单限制只允许执行这一条命令。
第二个坑是规则编码和特殊字符。ipf规则里可能出现<>、/等符号,如果解析结果要拼进SVG文本节点,必须做XML转义,否则生成的图片文件直接损坏。同理,输出到HTML报告时也要转义<和&。
第三个坑是规则数量很大时的性能。几千条规则如果一条一行画下去,图片高度会夸张到无法查看。建议按方向(in/out)分组,或者按接口名折叠,超过阈值的分组只显示前N条并提供汇总计数,这样生成的图片既紧凑又不失关键信息。
整体来看,用Node.js做这件事的核心工作量集中在规则解析这一步,绘图反而是相对机械的部分。把解析器写得健壮一些,后面无论换成node-canvas、SVG还是直接输出HTML表格,都能复用同一套结构化数据。需要类似功能的团队不妨先从读取和解析做起,绘图部分按实际需求逐步完善即可。