Browse Source

feat: use iptables filter traffics,only forward to allowip and port.

pull/2238/head
laoshancun 9 months ago
parent
commit
181716c2f7
  1. 190
      src/server/plugins/clientTrafficRestriction.ts
  2. 12
      src/server/utils/WireGuard.ts
  3. 41
      src/server/utils/ip.ts
  4. 6
      src/server/utils/wgHelper.ts

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

12
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.');

41
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 };
};

6
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}`;
},

Loading…
Cancel
Save