收起左侧

基于Mosquitto和nftables实现动态修改防火墙规则

0
回复
30
查看
[ 复制链接 ]

4

主题

3

回帖

0

牛值

江湖小虾

一、部署概述

本文档详细介绍如何基于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 1883grep 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连接失败。
收藏
送赞
分享
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则