Browse Source

refactor(WireGuard): Move client traffic restriction to WireGuard class

pull/2238/head
laoshancun 6 months ago
parent
commit
779f0821ee
  1. 209
      src/server/plugins/clientTrafficRestriction.ts
  2. 194
      src/server/utils/WireGuard.ts

209
src/server/plugins/clientTrafficRestriction.ts

@ -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<ClientType, 'createdAt' | 'updatedAt'>
): 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();
});
});

194
src/server/utils/WireGuard.ts

@ -3,6 +3,11 @@ import debug from 'debug';
import { encodeQR } from 'qr'; import { encodeQR } from 'qr';
import type { NitroApp } from 'nitropack/types'; import type { NitroApp } from 'nitropack/types';
import type { InterfaceType } from '#db/repositories/interface/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'); const WG_DEBUG = debug('WireGuard');
@ -26,7 +31,7 @@ class WireGuard {
const wgInterface = await Database.interfaces.get(); const wgInterface = await Database.interfaces.get();
await this.#saveWireguardConfig(wgInterface); await this.#saveWireguardConfig(wgInterface);
await this.#syncWireguardConfig(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.'); 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<ClientType, 'createdAt' | 'updatedAt'>
): 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<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;
}
/**
* 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) { async getClientsForUser(userId: ID, filter?: string) {
const wgInterface = await Database.interfaces.get(); const wgInterface = await Database.interfaces.get();
@ -261,7 +447,11 @@ class WireGuard {
await this.#syncWireguardConfig(wgInterface); await this.#syncWireguardConfig(wgInterface);
WG_DEBUG(`Wireguard Interface ${wgInterface.name} started successfully.`); 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...'); WG_DEBUG('Starting Cron Job...');
await this.startCronJob(); await this.startCronJob();
WG_DEBUG('Cron Job started successfully.'); WG_DEBUG('Cron Job started successfully.');

Loading…
Cancel
Save