From 181716c2f71b3681da1450f7373c056f05232ff4 Mon Sep 17 00:00:00 2001 From: laoshancun Date: Mon, 27 Oct 2025 11:33:47 +0800 Subject: [PATCH] feat: use iptables filter traffics,only forward to allowip and port. --- .../plugins/clientTrafficRestriction.ts | 190 ++++++++++++++++++ src/server/utils/WireGuard.ts | 12 ++ src/server/utils/ip.ts | 41 ++++ src/server/utils/wgHelper.ts | 6 +- 4 files changed, 248 insertions(+), 1 deletion(-) create mode 100644 src/server/plugins/clientTrafficRestriction.ts diff --git a/src/server/plugins/clientTrafficRestriction.ts b/src/server/plugins/clientTrafficRestriction.ts new file mode 100644 index 00000000..563b9dad --- /dev/null +++ b/src/server/plugins/clientTrafficRestriction.ts @@ -0,0 +1,190 @@ +import { exec } from '../utils/cmd'; +import { WG_ENV } from '../utils/config'; +import { isIPv6, parseIpAndPort } from '../utils/ip'; +import type { ClientType } from '#db/repositories/client/types'; + +declare module 'nitropack/types' { + interface NitroRuntimeHooks { + 'wireguard:config': () => void; + 'wireguard:start': () => void; + } +} + +// 客户端流量限制插件 +export default defineNitroPlugin(async (nitroApp) => { + console.log('Client Traffic Restriction Plugin loaded'); + + // 生成客户端特定的iptables规则 + const generateClientIptablesRules = ( + client: Omit + ): string[] => { + const rules: string[] = []; + + // 从客户端IP地址中提取IP(去掉CIDR部分) + const clientIp = client.ipv4Address.split('/')[0]; + const clientIpv6 = client.ipv6Address?.split('/')[0]; + + // 如果客户端有allowedIps配置,为每个允许的IP生成规则 + if (client.allowedIps && client.allowedIps.length > 0) { + client.allowedIps.forEach((allowedIpWithPort) => { + // 解析IP地址和端口 + const { ip: allowedIp, port } = parseIpAndPort(allowedIpWithPort); + + // 根据IP版本选择合适的防火墙命令 + if (isIPv6(allowedIp)) { + // 仅为IPv6地址生成ip6tables规则 + if (!WG_ENV.DISABLE_IPV6 && clientIpv6) { + if (port) { + // 对于TCP端口 + rules.push( + `ip6tables -A FORWARD -s ${clientIpv6} -d ${allowedIp} -p tcp --dport ${port} -j ACCEPT` + ); + rules.push( + `ip6tables -A FORWARD -d ${clientIpv6} -s ${allowedIp} -p tcp --sport ${port} -j ACCEPT` + ); + // 对于UDP端口 + rules.push( + `ip6tables -A FORWARD -s ${clientIpv6} -d ${allowedIp} -p udp --dport ${port} -j ACCEPT` + ); + rules.push( + `ip6tables -A FORWARD -d ${clientIpv6} -s ${allowedIp} -p udp --sport ${port} -j ACCEPT` + ); + } else { + // 没有端口限制时的规则 + rules.push( + `ip6tables -A FORWARD -s ${clientIpv6} -d ${allowedIp} -j ACCEPT` + ); + rules.push( + `ip6tables -A FORWARD -d ${clientIpv6} -s ${allowedIp} -j ACCEPT` + ); + } + } + } else { + // 为IPv4地址生成iptables规则 + if (port) { + // 对于TCP端口 + rules.push( + `iptables -A FORWARD -s ${clientIp} -d ${allowedIp} -p tcp --dport ${port} -j ACCEPT` + ); + rules.push( + `iptables -A FORWARD -d ${clientIp} -s ${allowedIp} -p tcp --sport ${port} -j ACCEPT` + ); + // 对于UDP端口 + rules.push( + `iptables -A FORWARD -s ${clientIp} -d ${allowedIp} -p udp --dport ${port} -j ACCEPT` + ); + rules.push( + `iptables -A FORWARD -d ${clientIp} -s ${allowedIp} -p udp --sport ${port} -j ACCEPT` + ); + } else { + // 没有端口限制时的规则 + rules.push( + `iptables -A FORWARD -s ${clientIp} -d ${allowedIp} -j ACCEPT` + ); + rules.push( + `iptables -A FORWARD -d ${clientIp} -s ${allowedIp} -j ACCEPT` + ); + } + } + }); + } else { + // 如果没有配置allowedIps,默认允许访问服务器 + const serverIps = ['10.8.0.1/32']; + serverIps.forEach((serverIp) => { + // 这些都是IPv4地址,只生成iptables规则 + rules.push( + `iptables -A FORWARD -s ${clientIp} -d ${serverIp} -j ACCEPT` + ); + rules.push( + `iptables -A FORWARD -d ${clientIp} -s ${serverIp} -j ACCEPT` + ); + }); + } + + return rules; + }; + + // 应用客户端iptables规则 + const applyClientIptablesRules = async () => { + try { + console.log('Applying client-specific iptables rules...'); + + // 获取所有客户端 + const clients = await Database.clients.getAll(); + + // 清空现有的FORWARD链规则 + await exec('iptables -F FORWARD'); + if (!WG_ENV.DISABLE_IPV6) { + await exec('ip6tables -F FORWARD'); + } + + // 设置FORWARD链默认策略为DROP(阻止所有流量) + await exec('iptables -P FORWARD DROP'); + if (!WG_ENV.DISABLE_IPV6) { + await exec('ip6tables -P FORWARD DROP'); + } + + // 为每个启用的客户端生成并应用规则 + const includedClients: string[] = []; + for (const client of clients) { + if (!client.enabled) { + console.log(`Skipping disabled client: ${client.name}`); + continue; + } + const rules = generateClientIptablesRules(client); + for (const rule of rules) { + await exec(rule); + } + + includedClients.push(client.name); + console.log(`Applied rules for client: ${client.name}`); + } + + console.log( + `Applied rules for ${includedClients.length} enabled clients: ${includedClients.join(', ')}` + ); + } catch (error) { + console.error('Error applying client iptables rules:', error); + } + }; + + const remove_forward_rule = async (rule: string) => { + try { + const filteredRules = rule.split(';').filter((line) => { + return !/FORWARD/i.test(line); + }); + return filteredRules.join(';'); + } catch (error) { + console.error('Error removing default FORWARD rule:', error); + } + return rule; + }; + + // 初始化:移除hooks中的默认FORWARD允许规则 + const initializeTrafficRestriction = async () => { + try { + const hooks = await Database.hooks.get(); + + // 移除hooks中的默认FORWARD允许规则 + const updatedHooks = { + ...hooks, + postUp: await remove_forward_rule(hooks.postUp), + postDown: await remove_forward_rule(hooks.postDown), + }; + + // 更新hooks到数据库 + await Database.hooks.update(updatedHooks); + console.log('Removed default FORWARD rules from hooks'); + } catch (error) { + console.error('Error initializing traffic restriction:', error); + } + }; + + // 注册事件监听器,监听客户端配置保存后的事件 + nitroApp.hooks.hook('wireguard:config', applyClientIptablesRules); + // 注册启动事件 + nitroApp.hooks.hook('wireguard:start', async () => { + await initializeTrafficRestriction(); + await applyClientIptablesRules(); + }); +}); diff --git a/src/server/utils/WireGuard.ts b/src/server/utils/WireGuard.ts index 8f9a96f2..7e925cd5 100644 --- a/src/server/utils/WireGuard.ts +++ b/src/server/utils/WireGuard.ts @@ -1,11 +1,21 @@ import fs from 'node:fs/promises'; import debug from 'debug'; import { encodeQR } from 'qr'; +import type { NitroApp } from 'nitropack/types'; import type { InterfaceType } from '#db/repositories/interface/types'; const WG_DEBUG = debug('WireGuard'); class WireGuard { + private _nitroApp: NitroApp | null = null; + + private get nitroApp(): NitroApp { + if (!this._nitroApp) { + this._nitroApp = useNitroApp(); + } + return this._nitroApp; + } + // 注册事件监听器,监听客户端配置保存后的事件 /** * Save and sync config */ @@ -13,6 +23,7 @@ class WireGuard { const wgInterface = await Database.interfaces.get(); await this.#saveWireguardConfig(wgInterface); await this.#syncWireguardConfig(wgInterface); + await this.nitroApp.hooks.callHook('wireguard:config'); } /** @@ -208,6 +219,7 @@ class WireGuard { await this.#syncWireguardConfig(wgInterface); WG_DEBUG(`Wireguard Interface ${wgInterface.name} started successfully.`); + await this.nitroApp.hooks.callHook('wireguard:start'); WG_DEBUG('Starting Cron Job...'); await this.startCronJob(); WG_DEBUG('Cron Job started successfully.'); diff --git a/src/server/utils/ip.ts b/src/server/utils/ip.ts index c3c097e1..12dd9d51 100644 --- a/src/server/utils/ip.ts +++ b/src/server/utils/ip.ts @@ -170,3 +170,44 @@ async function getIpInformation() { export const cachedGetIpInformation = cacheFunction(getIpInformation, { expiry: 15 * 60 * 1000, }); + +// 检查IP地址是否为IPv6 +export const isIPv6 = (ip: string): boolean => { + return ip.startsWith('[') || ip.split(':').length > 2; +}; + +// 解析IP地址和端口 +export const parseIpAndPort = ( + ipWithPort: string +): { ip: string; port?: string } => { + if (ipWithPort.includes('/')) { + return { ip: ipWithPort }; + } + const parts = ipWithPort.split(':'); + // 如果是IPv6地址(包含多个冒号),则最后一个部分可能是端口 + if (isIPv6(ipWithPort) && parts.length > 3) { + // 检查最后一部分是否是纯数字(端口) + const potentialPort = parts[parts.length - 1]; + let ip = parts.slice(0, -1).join(':'); + // remove [ ] if exist in ip + const matched = /^\[?([0-9a-f:]+)]?$/gi.exec(ip); + if (matched) { + ip = matched[1]; + } + + if (/^\d+$/.test(potentialPort)) { + return { + ip: ip, + port: potentialPort, + }; + } + } else if (!isIPv6(ipWithPort) && parts.length === 2) { + // 对于IPv4地址,格式为 ip:port + return { + ip: parts[0], + port: parts[1], + }; + } + // 如果没有端口,则返回原始IP + return { ip: ipWithPort }; +}; diff --git a/src/server/utils/wgHelper.ts b/src/server/utils/wgHelper.ts index fb692fc1..a407bbce 100644 --- a/src/server/utils/wgHelper.ts +++ b/src/server/utils/wgHelper.ts @@ -1,5 +1,6 @@ import { parseCidr } from 'cidr-tools'; import { stringifyIp } from 'ip-bigint'; +import { parseIpAndPort } from './ip'; import type { ClientType } from '#db/repositories/client/types'; import type { InterfaceType } from '#db/repositories/interface/types'; import type { UserConfigType } from '#db/repositories/userConfig/types'; @@ -101,6 +102,9 @@ PostDown = ${iptablesTemplate(hooks.postDown, wgInterface)}`; dnsServers.length > 0 ? `DNS = ${dnsServers.join(', ')}` : null; const extraLines = [dnsLine, ...hookLines].filter((v) => v !== null); + const allowIps = (client.allowedIps ?? userConfig.defaultAllowedIps).map( + (ip) => parseIpAndPort(ip).ip + ); return `[Interface] PrivateKey = ${client.privateKey} @@ -110,7 +114,7 @@ ${extraLines.length ? `${extraLines.join('\n')}\n` : ''} [Peer] PublicKey = ${wgInterface.publicKey} PresharedKey = ${client.preSharedKey} -AllowedIPs = ${(client.allowedIps ?? userConfig.defaultAllowedIps).join(', ')} +AllowedIPs = ${allowIps.join(', ')} PersistentKeepalive = ${client.persistentKeepalive} Endpoint = ${userConfig.host}:${userConfig.port}`; },