一、部署概述
本文档详细介绍如何基于Mosquitto(MQTT消息代理)和nftables(防火墙),结合Node.js脚本,实现防火墙规则的动态修改。核心流程为:客户端通过MQTT协议发送IP及操作指令(添加/删除/重置),Node.js脚本订阅MQTT主题,接收指令后执行nftables命令修改防火墙规则,并将操作记录存入SQLite数据库。
二、组件部署分步操作
2.1 基础依赖安装
首先安装部署所需的基础工具和依赖包,执行以下命令:
# 更新系统软件包
apt update && apt upgrade -y
# 安装基础依赖(openssl用于生成证书,nftables用于防火墙,nodejs用于运行脚本)
apt install -y openssl nftables nodejs npm sqlite3
2.2 Mosquitto部署与配置(含证书、ACL)
Mosquitto作为MQTT消息代理,负责接收客户端指令并转发给Node.js订阅脚本,配置包含明文监听(本地1883端口)和TLS加密监听(外网8883端口),同时配置ACL权限控制和用户认证。
2.2.1 Mosquitto安装
# 安装Mosquitto
apt install -y mosquitto mosquitto-clients
# 启动Mosquitto服务并设置开机自启
systemctl start mosquitto
systemctl enable mosquitto
# 查看服务状态,确认启动成功
systemctl status mosquitto
2.2.2 证书生成(TLS加密所需)
为Mosquitto 8883端口生成CA证书、服务端证书,确保外网连接加密,使用提供的openssl命令,步骤如下:
# 1. 创建证书存储目录,赋予权限
mkdir -p /etc/mosquitto/certs
chown -R mosquitto:mosquitto /etc/mosquitto/certs
chmod 700 /etc/mosquitto/certs
# 2. 生成CA证书(自签名,有效期10年)
openssl req -new -x509 -utf8 -nodes -subj "/C=CN/ST=ZheJiang/L=Hangzhou/O=xiaodouya/OU=xxxx/CN=ca/emailAddress=root@fnos.com" -extensions v3_ca -out /etc/mosquitto/certs/ca.crt -keyout /etc/mosquitto/certs/ca.key -days 3650
# 3. 生成服务端证书(由CA签名,有效期10年)
openssl req -new -x509 -utf8 -nodes -days 3650 -subj "/C=CN/ST=ZheJiang/L=Hangzhou/O=xiaodouya/OU=xxxx/CN=fnos" -extensions v3_req -keyout /etc/mosquitto/certs/server.key -out /etc/mosquitto/certs/server.crt -CAkey /etc/mosquitto/certs/ca.key -CA /etc/mosquitto/certs/ca.crt
# 4. 调整证书权限,避免权限过高导致Mosquitto无法读取
chmod 600 /etc/mosquitto/certs/*
chown mosquitto:mosquitto /etc/mosquitto/certs/*
说明:生成证书时,需确保系统中 /etc/ssl/openssl.conf文件包含以下重点配置(若缺失需补充),否则证书可能无法正常使用:
[ v3_req ]
basicConstraints = CA:FALSE
keyUsage = nonRepudiation, digitalSignature, keyEncipherment, keyAgreement
extendedKeyUsage=serverAuth
subjectAltName = @alt_names
[alt_names]
DNS.10 = mqtt.mosquitto.top
IP.11 = 192.168.5.254
IP.12 = 10.8.0.1
[ v3_ca ]
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid:always,issuer
basicConstraints = critical,CA:true
keyUsage = cRLSign, keyCertSign
2.2.3 Mosquitto用户认证配置(用户名/密码)
配置Mosquitto用户认证,禁止匿名访问,创建用户名(fnos)和密码(xxxxxx),步骤如下:
# 生成用户密码文件,用户名fnos,xxxxxx(执行后按提示输入密码)
mosquitto_passwd -c /etc/mosquitto/users.pass fnos
# 调整密码文件权限,仅mosquitto用户可读取
chmod 600 /etc/mosquitto/users.pass
chown mosquitto:mosquitto /etc/mosquitto/users.pass
2.2.4 Mosquitto ACL配置(主题权限控制)
创建ACL文件,控制用户对MQTT主题的访问权限,确保只有指定用户可订阅/发布目标主题(bussiness/nftables):
# 创建ACL文件
touch /etc/mosquitto/aclfile
# 编辑ACL文件,添加以下内容(允许fnos用户订阅和发布bussiness/nftables主题)
cat > /etc/mosquitto/aclfile << EOF
# 禁止匿名用户访问
topic deny #
# 允许fnos用户订阅和发布bussiness/nftables主题
user fnos
topic readwrite bussiness/nftables
EOF
# 调整ACL文件权限
chmod 600 /etc/mosquitto/aclfile
chown mosquitto:mosquitto /etc/mosquitto/aclfile
2.2.5 Mosquitto配置文件
替换Mosquitto默认配置文件,使用提供的配置(包含两个**、持久化、日志等配置):
# 备份默认配置文件(可选,建议备份)
mv /etc/mosquitto/mosquitto.conf /etc/mosquitto/mosquitto.conf.bak
# 创建新的配置文件,复制以下内容
cat > /etc/mosquitto/mosquitto.conf << EOF
per_listener_settings true
pid_file /run/mosquitto/mosquitto.pid
user mosquitto
persistence true
persistence_file mosquitto.db
persistence_location /var/lib/mosquitto
autosave_interval 5
autosave_on_changes true
memory_limit 67108864
log_type notice
connection_messages true
log_timestamp true
log_timestamp_format %Y-%m-%dT%H:%M:%S
log_dest file /var/log/mosquitto/mosquitto.log
include_dir /etc/mosquitto/conf.d
# ======================
# ** 1:本地明文 1883
# ======================
listener 1883 127.0.0.1
protocol mqtt
allow_anonymous false
acl_file /etc/mosquitto/aclfile
password_file /etc/mosquitto/users.pass
allow_zero_length_clientid true
auto_id_prefix auto-
check_retain_source true
max_inflight_bytes 1048576
max_inflight_messages 20
max_keepalive 300
max_packet_size 524288
max_queued_bytes 4194304
max_qos 2
max_queued_messages 1000
retain_available true
max_connections 5
# ======================
# ** 2:外网 TLS 8883
# ======================
listener 8883 0.0.0.0
protocol mqtt
allow_anonymous false
password_file /etc/mosquitto/users.pass
acl_file /etc/mosquitto/aclfile
certfile /etc/mosquitto/certs/server.crt
keyfile /etc/mosquitto/certs/server.key
cafile /etc/mosquitto/certs/ca.crt
tls_version tlsv1.3
ciphers_tls1.3 TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256
require_certificate false
max_inflight_bytes 1048576
max_inflight_messages 20
max_keepalive 300
max_packet_size 524288
max_queued_bytes 4194304
max_qos 2
max_queued_messages 1000
retain_available true
max_connections 30
EOF
2.2.6 Mosquitto服务重启与验证
配置完成后,重启Mosquitto服务,验证配置是否生效:
# 重启Mosquitto服务
systemctl restart mosquitto
# 查看服务状态,确认无报错
systemctl status mosquitto
# 验证本地1883端口监听(正常应显示LISTEN)
ss -tulnp | grep 1883
# 验证外网8883端口监听(正常应显示LISTEN)
ss -tulnp | grep 8883
# 验证日志(无报错即正常)
tail -f /var/log/mosquitto/mosquitto.log
2.3 nftables配置(动态规则基础)
nftables作为防火墙,需配置基础规则和动态集合(allowed_dynamic),后续Node.js脚本将通过修改该集合实现防火墙规则的动态调整。
2.3.1 替换nftables配置文件
替换默认的nftables配置文件,添加动态集合和基础过滤规则,执行以下命令:
# 备份默认配置文件(可选)
mv /etc/nftables.conf /etc/nftables.conf.bak
# 创建新的配置文件,复制以下内容
cat > /etc/nftables.conf << EOF
#!/usr/sbin/nft -f
flush ruleset
table inet filter {
set allowed_dynamic {
type ipv4_addr; flags dynamic
}
chain input {
type filter hook input priority filter; policy drop
ip6 nexthdr icmpv6 counter accept
udp dport mdns counter accept
ct state related,established counter accept
iifname "lo" accept
iifname "enp0s31f6-ovs" udp dport { 53, 67 } accept
iifname "enp0s31f6-ovs" tcp dport { 53, 67 } accept
ip saddr { 192.168.5.0/24, 10.8.0.0/24, 172.18.0.0/24 } counter accept
tcp dport { 1194, 8883 } counter accept
ip saddr @allowed_dynamic tcp dport { 5667,60022, 8443, 443 } counter accept
}
chain forward {
type filter hook forward priority filter; policy drop
iifname { "tun0", "enp0s31f6-ovs", "br-691295fe2d05" } counter accept
oifname { "tun0", "enp0s31f6-ovs", "br-691295fe2d05" } counter accept
}
chain output {
type filter hook output priority filter; policy accept
}
}
table inet nat {
chain postrouting {
type nat hook postrouting priority srcnat
ip saddr 10.8.0.0/24 counter masquerade
ip saddr 172.18.0.0/24 counter masquerade
}
chain prerouting {
type nat hook prerouting priority dstnat
ip daddr 192.168.5.254 tcp dport 8443 counter dnat to 172.18.0.2:8443
ip daddr 192.168.5.254 tcp dport 3000 counter dnat to 172.18.0.6:3000
ip daddr 192.168.5.254 tcp dport 9090 counter dnat to 172.18.0.8:9090
ip daddr 192.168.5.254 tcp dport 18789 counter dnat to 172.18.0.168:18789
ip daddr 192.168.5.254 tcp dport 18790 counter dnat to 172.18.0.168:18790
ip daddr 192.168.5.254 tcp dport 3001 counter dnat to 172.18.0.20:3000
ip daddr 192.168.5.254 tcp dport 12345 counter dnat to 172.18.0.99:12345
}
}
EOF
配置说明:
- 创建动态集合
allowed_dynamic,用于存储可动态添加/删除的IP地址(类型为ipv4_addr);
- input链默认策略为drop(拒绝所有入站流量);
- 允许本地回环、相关/已建立连接、指定内网段(192.168.5.0/24等)、指定端口(53、67等)的流量;
- 允许动态集合
@allowed_dynamic中的IP访问指定端口(5667、60022等)。
- 1194端口为openvpn端口,对所有地址开放,作为出现意外时的修复通道
2.3.2 启动nftables服务并加载配置
# 启动nftables服务并设置开机自启
systemctl start nftables
systemctl enable nftables
# 加载新的nftables配置
nft -f /etc/nftables.conf
# 查看当前nftables规则,确认配置生效
nft list ruleset
验证:执行 nft list set inet filter allowed_dynamic,应显示该集合为空(初始状态无IP)。
2.4 Node.js脚本部署(核心逻辑)
Node.js脚本负责订阅Mosquitto的bussiness/nftables主题,接收JSON格式的指令,执行nftables命令修改防火墙规则,并将操作记录存入SQLite数据库。
2.4.1 脚本目录创建与依赖安装
# 创建脚本目录,统一管理脚本、数据库文件
mkdir -p /home/xxxx/nftables
cd /home/xxxx/nftables
# 初始化npm项目(生成package.json)
npm init -y
# 安装所需依赖包
npm install mqtt better-sqlite3
2.4.2 创建Node.js脚本文件
创建app.js脚本文件,复制提供的代码,确保配置与实际环境一致:
cat > /home/xxxx/nftables/app.js <<EOF
const fs = require('fs')
const mqtt = require('mqtt');
const { exec } = require('child_process');
const util = require('util');
const net = require('net');
const Database = require('better-sqlite3');
// 将基于回调的 exec 转换为基于 Promise 的方法,方便 async/await 调用
const execPromise = util.promisify(exec);
// ==========================================
// 核心功能 4:独立实现的执行本地系统命令的方法
// ==========================================
/**
* 独立执行本地系统命令的方法
* @param {string} command - 需要执行的 Shell 命令
* @returns {Promise<{success: boolean, output: string, error: string}>}
*/
async function executeLocalCommand(command) {
try {
const { stdout, stderr } = await execPromise(command);
if (stderr) {
console.warn(`[CMD Warn] 命令执行产生警告信息: ${stderr.trim()}`);
}
return { success: true, output: stdout.trim() };
} catch (error) {
console.error(`[CMD Error] 命令执行失败: ${error.message}`);
return { success: false, error: error.message };
}
}
// 1. 初始化数据库
const db = new Database('./nftables.db');
// 2. 初始化表结构 (同步执行)
db.exec(`
CREATE TABLE IF NOT EXISTS firewall_rules (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ip_addr TEXT NOT NULL,
action TEXT NOT NULL,
status TEXT DEFAULT 'pending',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`);
// ==========================================
// 核心功能 2:初始化 SQLite 数据库并自动建表
// ==========================================
const insertStmt = db.prepare('INSERT INTO firewall_rules (ip_addr, action) VALUES (?, ?)');
const updateStmt = db.prepare('UPDATE firewall_rules SET status = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?');
// 数据库操作辅助函数
const dbHelpers = {
insertRule: (ip_addr, action) => {
let answer = insertStmt.run(ip_addr, action)
return answer.lastInsertRowid
},
updateStatus: (id, status) => {
updateStmt.run(status, id)
}
};
// ==========================================
// 核心功能 1 & 3:MQTT 订阅及业务逻辑处理
// ==========================================
//const MQTT_BROKER = 'mqtts://192.168.5.254:8883';
const MQTT_BROKER = 'mqtt://127.0.0.1:1883';
const MQTT_TOPIC = 'bussiness/nftables';
const options = {
clean: true,
connectTimeout: 10000,
clientId: 'nftables_dynamic_subscribe',
username: 'fnos',
password: 'xxxxxx',
// ca: fs.readFileSync('/etc/mosquitto/certs/ca.crt')
}
const client = mqtt.connect(MQTT_BROKER, options);
client.on('connect', () => {
console.log(`已连接到 MQTT Broker: ${MQTT_BROKER}`);
client.subscribe(MQTT_TOPIC, (err) => {
if (!err) {
console.log(`成功订阅队列: ${MQTT_TOPIC}`);
} else {
console.error('订阅队列失败:', err);
}
});
});
client.on('message', async (topic, message) => {
try {
// 解析接收到的 JSON 消息
const payload = JSON.parse(message.toString());
const { ip_addr, action } = payload;
console.log(`\n[+] 收到消息 -> IP: ${ip_addr}, Action: ${action}`);
// 【安全检查】严格校验是否为合法 IP,防止 Shell 命令注入攻击!
if (!net.isIP(ip_addr)) {
console.error(`[-] 安全拦截: 无效的 IP 地址格式 "${ip_addr}"`);
return;
}
// 验证 action 是否合法
if (!['add', 'remove', 'reset'].includes(action)) {
console.error(`[-] 验证失败: 未知的动作 "${action}"`);
return;
}
// 步骤 2:将接收到的消息存储到本地 sqlite 数据库中
const recordId = dbHelpers.insertRule(ip_addr, action);
console.log(`[*] 消息已存入数据库, 记录 ID: ${recordId}, 状态: pending`);
// 步骤 3:根据 action 的指示,拼接 nft 命令
// 注意:这里使用的是向已存在的 set 中添加/删除元素的方法
let nftCommand = 'nft list ruleset'
switch (action) {
case 'add':
nftCommand = `nft add element inet filter allowed_dynamic { ${ip_addr} }`;
break;
case 'remove':
nftCommand = `nft delete element inet filter allowed_dynamic { ${ip_addr} }`;
break;
case 'reset':
nftCommand = 'nft -f /etc/nftables.conf'
break;
}
console.log(`[*] 准备执行系统命令: ${nftCommand}`);
// 执行本地命令
const execResult = await executeLocalCommand(nftCommand);
// 步骤 4:成功后修改 sqlite 数据库中的状态
if (execResult.success) {
await dbHelpers.updateStatus(recordId, 'success');
console.log(`[+] 规则执行成功, 数据库状态已更新为 success`);
} else {
await dbHelpers.updateStatus(recordId, 'failed');
console.log(`[-] 规则执行失败, 数据库状态已更新为 failed`);
}
} catch (error) {
if (error instanceof SyntaxError) {
console.error('[-] 解析 JSON 消息失败:', message.toString());
} else {
console.error('[-] 处理消息时发生系统错误:', error);
}
}
});
client.on('error', (err) => {
console.error('MQTT 连接错误:', err);
});
EOF
配置说明:
- 默认连接本地Mosquitto(127.0.0.1:1883),若需连接外网8883端口,可注释本地连接地址,启用
mqtts://192.168.5.254:8883,并取消 ca证书配置的注释;
- 用户名(fnos)和密码(xxxxxxxx)需与Mosquitto配置的用户一致;
- 数据库文件(nftables.db)将自动生成在脚本目录下,用于存储操作记录;
- 支持的操作指令:add(添加IP到动态集合)、remove(从动态集合删除IP)、reset(重置nftables规则为配置文件默认状态);
- 包含IP格式校验和操作合法性校验,防止命令注入攻击。
2.4.3 启动Node.js脚本
为确保脚本持续运行,使用nohup命令将脚本后台启动,并输出日志到文件:
# 进入脚本目录
cd /home/root/nftables
# 后台启动脚本,日志输出到app.log
nohup node app.js > app.log 2>&1 &
# 查看脚本启动状态,确认无报错
ps aux | grep node
tail -f app.log
或写一个systemd的服务管理文件:
# /lib/systemd/system/dynamic_nftables.service
[Unit]
Description=Dynamic nftables rule modify base mosquitto
[Service]
User=root
WorkingDirectory=/home/xxxx/nftables
ExecStart=/home/xxxx/nftables/start.sh
[Install]
WantedBy=multi-user.target
# start.sh 脚本的内容如下:
#!/bin/bash
export PATH=/opt/nodejs/bin:$PATH
exec -a dynamic_nftables node /root/nftables/app.js
正常启动后,日志应显示:已连接到 MQTT Broker: mqtt://127.0.0.1:1883 和 成功订阅队列: bussiness/nftables。
三、功能测试(验证部署成功)
部署完成后,通过Mosquitto客户端发布MQTT消息,验证防火墙规则是否能动态修改,步骤如下:
3.1 测试1:添加IP到防火墙允许列表
# 发布添加IP的指令(JSON格式,IP替换为实际需要添加的地址)
mosquitto_pub -h 127.0.0.1 -p 1883 -u fnos -P xxxxxxxx -t bussiness/nftables -m '{"ip_addr":"192.168.5.100","action":"add"}'
# 查看Node.js脚本日志,确认执行成功
tail -f /home/xxxxxxxx/nftables/app.log
# 查看nftables动态集合,确认IP已添加
nft list set inet filter allowed_dynamic
# 查看数据库记录,确认状态为success
sqlite3 /home/xxxxxxxx/nftables/nftables.db "select * from firewall_rules;"
3.2 测试2:删除IP从防火墙允许列表
# 发布删除IP的指令
mosquitto_pub -h 127.0.0.1 -p 1883 -u fnos -P xxxxxxxx -t bussiness/nftables -m '{"ip_addr":"192.168.5.100","action":"remove"}'
# 验证IP已删除
nft list set inet filter allowed_dynamic
sqlite3 /home/xxxxxxxx/nftables/nftables.db "select * from firewall_rules;"
3.3 测试3:重置防火墙规则
# 发布重置指令
mosquitto_pub -h 127.0.0.1 -p 1883 -u fnos -P xxxxxx -t bussiness/nftables -m '{"ip_addr":"","action":"reset"}'
# 验证规则已重置(动态集合为空)
nft list set inet filter allowed_dynamic
3.4 测试4:外网TLS连接(可选)
若需从外网测试,使用带证书的Mosquitto客户端发布消息:
# 外网客户端执行(需将CA证书复制到客户端,替换IP为服务器外网IP)
mosquitto_pub -h 192.168.5.254 -p 8883 -u fnos -P xxxxxxxx -t bussiness/nftables -m '{"ip_addr":"192.168.5.200","action":"add"}' --cafile /path/to/ca.crt
四、常见问题排查
4.1 Mosquitto启动失败
- 原因1:证书权限过高/过低 → 执行
chmod 600 /etc/mosquitto/certs/*,确保mosquitto用户可读取;
- 原因2:配置文件语法错误 → 执行
mosquitto -c /etc/mosquitto/mosquitto.conf -v,查看错误提示并修正;
- 原因3:端口被占用 → 执行
ss -tulnp | grep 1883 或 grep 8883,杀死占用端口的进程后重启。
4.2 Node.js脚本连接MQTT失败
- 原因1:用户名/密码错误 → 确认脚本中options的username和password与Mosquitto的users.pass一致;
- 原因2:Mosquitto服务未启动 → 重启Mosquitto服务;
- 原因3:防火墙拦截1883/8883端口 → 检查nftables规则,确保允许本地连接1883端口、外网连接8883端口。
4.3 nftables命令执行失败
- 原因1:动态集合不存在 → 重新加载nftables配置
nft -f /etc/nftables.conf;
- 原因2:IP格式错误 → 确保发布的消息中ip_addr为合法IPv4地址;
- 原因3:脚本权限不足 → 切换到root用户启动脚本,或给脚本添加sudo权限。
4.4 数据库操作失败
- 原因1:脚本目录权限不足 → 执行
chmod 777 /home/xxxx/nftables(临时测试),或调整目录所有者为当前用户;
- 原因2:数据库文件损坏 → 删除nftables.db,重新启动脚本(将自动重建数据库和表)。
五、注意事项
- Mosquitto的8883端口用于外网访问,需确保服务器外网IP可访问,且nftables允许该端口的TLS流量;
- 动态添加的IP在重启nftables服务或执行reset指令后,动态集合会重置为空,需重新添加;
- 禁止匿名访问Mosquitto,定期更新用户密码,防止未授权访问;
- 证书有效期为10年,到期前需重新生成证书,避免TLS连接失败。