From 779f0821eec2f7cbacbd3f8ec43e032b9d5c32ee Mon Sep 17 00:00:00 2001 From: laoshancun Date: Wed, 21 Jan 2026 19:00:22 +0800 Subject: [PATCH] refactor(WireGuard): Move client traffic restriction to WireGuard class --- .../plugins/clientTrafficRestriction.ts | 209 ------------------ src/server/utils/WireGuard.ts | 194 +++++++++++++++- 2 files changed, 192 insertions(+), 211 deletions(-) delete mode 100644 src/server/plugins/clientTrafficRestriction.ts diff --git a/src/server/plugins/clientTrafficRestriction.ts b/src/server/plugins/clientTrafficRestriction.ts deleted file mode 100644 index 420a0698..00000000 --- a/src/server/plugins/clientTrafficRestriction.ts +++ /dev/null @@ -1,209 +0,0 @@ -import { exec } from '../utils/cmd'; -import { WG_ENV } from '../utils/config'; -import { parseIpAndPort } from '../utils/ip'; -import type { ClientType } from '#db/repositories/client/types'; -import { isIPv6 } from 'is-ip'; - -declare module 'nitropack/types' { - interface NitroRuntimeHooks { - 'wireguard:config': () => void; - 'wireguard:start': () => void; - } -} - -/** - * Client Traffic Restriction Plugin - * - * This plugin manages iptables/ip6tables rules to restrict client traffic - * based on configured allowed IPs. It integrates with wireguard lifecycle hooks. - */ -export default defineNitroPlugin(async (nitroApp) => { - console.log('Client Traffic Restriction Plugin loaded'); - - /** - * Generates iptables rules for a specific client based on their allowed IPs. - * - * @param client - The client configuration - * @returns Array of iptables command strings - */ - const generateClientIptablesRules = ( - client: Omit - ): string[] => { - const rules: string[] = []; - - // Extract IP addresses from client configuration (remove CIDR prefix) - const clientIp = client.ipv4Address.split('/')[0]; - const clientIpv6 = client.ipv6Address?.split('/')[0]; - - // Generate rules for each allowed IP if configured - if (client.allowedIps && client.allowedIps.length > 0) { - client.allowedIps.forEach((allowedIpWithPort) => { - const { ip: allowedIp, port } = parseIpAndPort(allowedIpWithPort); - - if (isIPv6(allowedIp)) { - // IPv6: generate ip6tables rules - if (!WG_ENV.DISABLE_IPV6 && clientIpv6) { - if (port) { - // TCP port rules - 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 port rules - 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 { - // No port restriction - allow all traffic - rules.push( - `ip6tables -A FORWARD -s ${clientIpv6} -d ${allowedIp} -j ACCEPT` - ); - rules.push( - `ip6tables -A FORWARD -d ${clientIpv6} -s ${allowedIp} -j ACCEPT` - ); - } - } - } else { - // IPv4: generate iptables rules - if (port) { - // TCP port rules - 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 port rules - 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 { - // No port restriction - allow all traffic - rules.push( - `iptables -A FORWARD -s ${clientIp} -d ${allowedIp} -j ACCEPT` - ); - rules.push( - `iptables -A FORWARD -d ${clientIp} -s ${allowedIp} -j ACCEPT` - ); - } - } - }); - } else { - // Default: allow access to server only when no allowed IPs configured - const serverIps = ['10.8.0.1/32']; - serverIps.forEach((serverIp) => { - rules.push( - `iptables -A FORWARD -s ${clientIp} -d ${serverIp} -j ACCEPT` - ); - rules.push( - `iptables -A FORWARD -d ${clientIp} -s ${serverIp} -j ACCEPT` - ); - }); - } - - return rules; - }; - - /** - * Applies all client-specific iptables rules. - * Clears existing rules and sets up new ones based on current client configuration. - */ - const applyClientIptablesRules = async () => { - try { - console.log('Applying client-specific iptables rules...'); - - // Fetch all clients from database - const clients = await Database.clients.getAll(); - - // Flush existing FORWARD chain rules - await exec('iptables -F FORWARD'); - if (!WG_ENV.DISABLE_IPV6) { - await exec('ip6tables -F FORWARD'); - } - - // Set default FORWARD policy to DROP (block all traffic by default) - await exec('iptables -P FORWARD DROP'); - if (!WG_ENV.DISABLE_IPV6) { - await exec('ip6tables -P FORWARD DROP'); - } - - // Generate and apply rules for each enabled client - 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); - } - }; - - /** - * Removes FORWARD chain rules from hook commands. - * - * @param rule - The hook rule string - * @returns Filtered rule string without FORWARD rules - */ - 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; - }; - - /** - * Initializes traffic restriction by removing default FORWARD allow rules - * from hook commands. - */ - const initializeTrafficRestriction = async () => { - try { - const hooks = await Database.hooks.get(); - - // Remove default FORWARD allow rules from hooks - const updatedHooks = { - ...hooks, - postUp: await remove_forward_rule(hooks.postUp), - postDown: await remove_forward_rule(hooks.postDown), - }; - - // Update hooks in database - await Database.hooks.update(updatedHooks); - console.log('Removed default FORWARD rules from hooks'); - } catch (error) { - console.error('Error initializing traffic restriction:', error); - } - }; - - // Register event listeners for wireguard lifecycle hooks - 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 7cca4b32..f0e1e772 100644 --- a/src/server/utils/WireGuard.ts +++ b/src/server/utils/WireGuard.ts @@ -3,6 +3,11 @@ import debug from 'debug'; import { encodeQR } from 'qr'; import type { NitroApp } from 'nitropack/types'; import type { InterfaceType } from '#db/repositories/interface/types'; +import type { ClientType } from '#db/repositories/client/types'; +import { isIPv6 } from 'is-ip'; +import { parseIpAndPort } from './ip'; +import { exec } from './cmd'; +import { WG_ENV } from './config'; const WG_DEBUG = debug('WireGuard'); @@ -26,7 +31,7 @@ class WireGuard { const wgInterface = await Database.interfaces.get(); await this.#saveWireguardConfig(wgInterface); await this.#syncWireguardConfig(wgInterface); - await this.nitroApp.hooks.callHook('wireguard:config'); + await this.#syncFirewallRules(); } /** @@ -75,6 +80,187 @@ class WireGuard { WG_DEBUG('Config synced successfully.'); } + /** + * Generates iptables rules for a specific client based on their allowed IPs. + * + * @param client - The client configuration + * @returns Array of iptables command strings + */ + #generateClientIptablesRules( + client: Omit + ): string[] { + const rules: string[] = []; + + // Extract IP addresses from client configuration (remove CIDR prefix) + const clientIp = client.ipv4Address.split('/')[0]; + const clientIpv6 = client.ipv6Address?.split('/')[0]; + + // Generate rules for each allowed IP if configured + if (client.allowedIps && client.allowedIps.length > 0) { + client.allowedIps.forEach((allowedIpWithPort) => { + const { ip: allowedIp, port } = parseIpAndPort(allowedIpWithPort); + + if (isIPv6(allowedIp)) { + // IPv6: generate ip6tables rules + if (!WG_ENV.DISABLE_IPV6 && clientIpv6) { + if (port) { + // TCP port rules + 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 port rules + 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 { + // No port restriction - allow all traffic + rules.push( + `ip6tables -A FORWARD -s ${clientIpv6} -d ${allowedIp} -j ACCEPT` + ); + rules.push( + `ip6tables -A FORWARD -d ${clientIpv6} -s ${allowedIp} -j ACCEPT` + ); + } + } + } else { + // IPv4: generate iptables rules + if (port) { + // TCP port rules + 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 port rules + 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 { + // No port restriction - allow all traffic + rules.push( + `iptables -A FORWARD -s ${clientIp} -d ${allowedIp} -j ACCEPT` + ); + rules.push( + `iptables -A FORWARD -d ${clientIp} -s ${allowedIp} -j ACCEPT` + ); + } + } + }); + } else { + // Default: allow access to server only when no allowed IPs configured + const serverIps = ['10.8.0.1/32']; + serverIps.forEach((serverIp) => { + rules.push( + `iptables -A FORWARD -s ${clientIp} -d ${serverIp} -j ACCEPT` + ); + rules.push( + `iptables -A FORWARD -d ${clientIp} -s ${serverIp} -j ACCEPT` + ); + }); + } + + return rules; + } + + /** + * Removes FORWARD chain rules from hook commands. + * + * @param rule - The hook rule string + * @returns Filtered rule string without FORWARD rules + */ + async #removeForwardRule(rule: string): Promise { + 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; + } + + /** + * Applies all client-specific iptables rules. + * Clears existing rules and sets up new ones based on current client configuration. + */ + async #syncFirewallRules() { + try { + WG_DEBUG('Syncing firewall rules...'); + + // Fetch all clients from database + const clients = await Database.clients.getAll(); + + // Flush existing FORWARD chain rules + await exec('iptables -F FORWARD'); + if (!WG_ENV.DISABLE_IPV6) { + await exec('ip6tables -F FORWARD'); + } + + // Set default FORWARD policy to DROP (block all traffic by default) + await exec('iptables -P FORWARD DROP'); + if (!WG_ENV.DISABLE_IPV6) { + await exec('ip6tables -P FORWARD DROP'); + } + + // Generate and apply rules for each enabled client + const includedClients: string[] = []; + for (const client of clients) { + if (!client.enabled) { + WG_DEBUG(`Skipping disabled client: ${client.name}`); + continue; + } + const rules = this.#generateClientIptablesRules(client); + for (const rule of rules) { + await exec(rule); + } + + includedClients.push(client.name); + WG_DEBUG(`Applied firewall rules for client: ${client.name}`); + } + + WG_DEBUG( + `Applied firewall rules for ${includedClients.length} enabled clients` + ); + } catch (error) { + console.error('Error syncing firewall rules:', error); + throw error; + } + } + + /** + * Initializes traffic restriction by removing default FORWARD allow rules + * from hook commands. Should be called once during first startup. + */ + async #initializeTrafficRestriction() { + try { + const hooks = await Database.hooks.get(); + + // Remove default FORWARD allow rules from hooks + const updatedHooks = { + ...hooks, + postUp: await this.#removeForwardRule(hooks.postUp), + postDown: await this.#removeForwardRule(hooks.postDown), + }; + + // Update hooks in database + await Database.hooks.update(updatedHooks); + WG_DEBUG('Removed default FORWARD rules from hooks'); + } catch (error) { + console.error('Error initializing traffic restriction:', error); + } + } + async getClientsForUser(userId: ID, filter?: string) { const wgInterface = await Database.interfaces.get(); @@ -261,7 +447,11 @@ class WireGuard { await this.#syncWireguardConfig(wgInterface); WG_DEBUG(`Wireguard Interface ${wgInterface.name} started successfully.`); - await this.nitroApp.hooks.callHook('wireguard:start'); + + // Initialize firewall rules (only run once during first startup) + await this.#initializeTrafficRestriction(); + await this.#syncFirewallRules(); + WG_DEBUG('Starting Cron Job...'); await this.startCronJob(); WG_DEBUG('Cron Job started successfully.');