Browse Source

refactor(ip): Improve IP address parsing logic using professional validation library

refactor(clientTrafficRestriction): Refactor traffic restriction plugin and optimize rule generation

- Use is-ip library instead of custom IPv6 detection function to improve IP validation accuracy
- Refactor parseIpAndPort function to better handle IPv4/IPv6 and port parsing
- Optimize traffic restriction plugin code structure and comments for better readability
pull/2238/head
laoshancun 6 months ago
parent
commit
386cf2409d
  1. 73
      src/server/plugins/clientTrafficRestriction.ts
  2. 71
      src/server/utils/ip.ts

73
src/server/plugins/clientTrafficRestriction.ts

@ -1,7 +1,8 @@
import { exec } from '../utils/cmd'; import { exec } from '../utils/cmd';
import { WG_ENV } from '../utils/config'; import { WG_ENV } from '../utils/config';
import { isIPv6, parseIpAndPort } from '../utils/ip'; import { parseIpAndPort } from '../utils/ip';
import type { ClientType } from '#db/repositories/client/types'; import type { ClientType } from '#db/repositories/client/types';
import { isIPv6 } from 'is-ip';
declare module 'nitropack/types' { declare module 'nitropack/types' {
interface NitroRuntimeHooks { interface NitroRuntimeHooks {
@ -10,39 +11,47 @@ declare module 'nitropack/types' {
} }
} }
// 客户端流量限制插件 /**
* 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) => { export default defineNitroPlugin(async (nitroApp) => {
console.log('Client Traffic Restriction Plugin loaded'); console.log('Client Traffic Restriction Plugin loaded');
// 生成客户端特定的iptables规则 /**
* 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 = ( const generateClientIptablesRules = (
client: Omit<ClientType, 'createdAt' | 'updatedAt'> client: Omit<ClientType, 'createdAt' | 'updatedAt'>
): string[] => { ): string[] => {
const rules: string[] = []; const rules: string[] = [];
// 从客户端IP地址中提取IP(去掉CIDR部分) // Extract IP addresses from client configuration (remove CIDR prefix)
const clientIp = client.ipv4Address.split('/')[0]; const clientIp = client.ipv4Address.split('/')[0];
const clientIpv6 = client.ipv6Address?.split('/')[0]; const clientIpv6 = client.ipv6Address?.split('/')[0];
// 如果客户端有allowedIps配置,为每个允许的IP生成规则 // Generate rules for each allowed IP if configured
if (client.allowedIps && client.allowedIps.length > 0) { if (client.allowedIps && client.allowedIps.length > 0) {
client.allowedIps.forEach((allowedIpWithPort) => { client.allowedIps.forEach((allowedIpWithPort) => {
// 解析IP地址和端口
const { ip: allowedIp, port } = parseIpAndPort(allowedIpWithPort); const { ip: allowedIp, port } = parseIpAndPort(allowedIpWithPort);
// 根据IP版本选择合适的防火墙命令
if (isIPv6(allowedIp)) { if (isIPv6(allowedIp)) {
// 仅为IPv6地址生成ip6tables规则 // IPv6: generate ip6tables rules
if (!WG_ENV.DISABLE_IPV6 && clientIpv6) { if (!WG_ENV.DISABLE_IPV6 && clientIpv6) {
if (port) { if (port) {
// 对于TCP端口 // TCP port rules
rules.push( rules.push(
`ip6tables -A FORWARD -s ${clientIpv6} -d ${allowedIp} -p tcp --dport ${port} -j ACCEPT` `ip6tables -A FORWARD -s ${clientIpv6} -d ${allowedIp} -p tcp --dport ${port} -j ACCEPT`
); );
rules.push( rules.push(
`ip6tables -A FORWARD -d ${clientIpv6} -s ${allowedIp} -p tcp --sport ${port} -j ACCEPT` `ip6tables -A FORWARD -d ${clientIpv6} -s ${allowedIp} -p tcp --sport ${port} -j ACCEPT`
); );
// 对于UDP端口 // UDP port rules
rules.push( rules.push(
`ip6tables -A FORWARD -s ${clientIpv6} -d ${allowedIp} -p udp --dport ${port} -j ACCEPT` `ip6tables -A FORWARD -s ${clientIpv6} -d ${allowedIp} -p udp --dport ${port} -j ACCEPT`
); );
@ -50,7 +59,7 @@ export default defineNitroPlugin(async (nitroApp) => {
`ip6tables -A FORWARD -d ${clientIpv6} -s ${allowedIp} -p udp --sport ${port} -j ACCEPT` `ip6tables -A FORWARD -d ${clientIpv6} -s ${allowedIp} -p udp --sport ${port} -j ACCEPT`
); );
} else { } else {
// 没有端口限制时的规则 // No port restriction - allow all traffic
rules.push( rules.push(
`ip6tables -A FORWARD -s ${clientIpv6} -d ${allowedIp} -j ACCEPT` `ip6tables -A FORWARD -s ${clientIpv6} -d ${allowedIp} -j ACCEPT`
); );
@ -60,16 +69,16 @@ export default defineNitroPlugin(async (nitroApp) => {
} }
} }
} else { } else {
// 为IPv4地址生成iptables规则 // IPv4: generate iptables rules
if (port) { if (port) {
// 对于TCP端口 // TCP port rules
rules.push( rules.push(
`iptables -A FORWARD -s ${clientIp} -d ${allowedIp} -p tcp --dport ${port} -j ACCEPT` `iptables -A FORWARD -s ${clientIp} -d ${allowedIp} -p tcp --dport ${port} -j ACCEPT`
); );
rules.push( rules.push(
`iptables -A FORWARD -d ${clientIp} -s ${allowedIp} -p tcp --sport ${port} -j ACCEPT` `iptables -A FORWARD -d ${clientIp} -s ${allowedIp} -p tcp --sport ${port} -j ACCEPT`
); );
// 对于UDP端口 // UDP port rules
rules.push( rules.push(
`iptables -A FORWARD -s ${clientIp} -d ${allowedIp} -p udp --dport ${port} -j ACCEPT` `iptables -A FORWARD -s ${clientIp} -d ${allowedIp} -p udp --dport ${port} -j ACCEPT`
); );
@ -77,7 +86,7 @@ export default defineNitroPlugin(async (nitroApp) => {
`iptables -A FORWARD -d ${clientIp} -s ${allowedIp} -p udp --sport ${port} -j ACCEPT` `iptables -A FORWARD -d ${clientIp} -s ${allowedIp} -p udp --sport ${port} -j ACCEPT`
); );
} else { } else {
// 没有端口限制时的规则 // No port restriction - allow all traffic
rules.push( rules.push(
`iptables -A FORWARD -s ${clientIp} -d ${allowedIp} -j ACCEPT` `iptables -A FORWARD -s ${clientIp} -d ${allowedIp} -j ACCEPT`
); );
@ -88,10 +97,9 @@ export default defineNitroPlugin(async (nitroApp) => {
} }
}); });
} else { } else {
// 如果没有配置allowedIps,默认允许访问服务器 // Default: allow access to server only when no allowed IPs configured
const serverIps = ['10.8.0.1/32']; const serverIps = ['10.8.0.1/32'];
serverIps.forEach((serverIp) => { serverIps.forEach((serverIp) => {
// 这些都是IPv4地址,只生成iptables规则
rules.push( rules.push(
`iptables -A FORWARD -s ${clientIp} -d ${serverIp} -j ACCEPT` `iptables -A FORWARD -s ${clientIp} -d ${serverIp} -j ACCEPT`
); );
@ -104,27 +112,30 @@ export default defineNitroPlugin(async (nitroApp) => {
return rules; return rules;
}; };
// 应用客户端iptables规则 /**
* Applies all client-specific iptables rules.
* Clears existing rules and sets up new ones based on current client configuration.
*/
const applyClientIptablesRules = async () => { const applyClientIptablesRules = async () => {
try { try {
console.log('Applying client-specific iptables rules...'); console.log('Applying client-specific iptables rules...');
// 获取所有客户端 // Fetch all clients from database
const clients = await Database.clients.getAll(); const clients = await Database.clients.getAll();
// 清空现有的FORWARD链规则 // Flush existing FORWARD chain rules
await exec('iptables -F FORWARD'); await exec('iptables -F FORWARD');
if (!WG_ENV.DISABLE_IPV6) { if (!WG_ENV.DISABLE_IPV6) {
await exec('ip6tables -F FORWARD'); await exec('ip6tables -F FORWARD');
} }
// 设置FORWARD链默认策略为DROP(阻止所有流量) // Set default FORWARD policy to DROP (block all traffic by default)
await exec('iptables -P FORWARD DROP'); await exec('iptables -P FORWARD DROP');
if (!WG_ENV.DISABLE_IPV6) { if (!WG_ENV.DISABLE_IPV6) {
await exec('ip6tables -P FORWARD DROP'); await exec('ip6tables -P FORWARD DROP');
} }
// 为每个启用的客户端生成并应用规则 // Generate and apply rules for each enabled client
const includedClients: string[] = []; const includedClients: string[] = [];
for (const client of clients) { for (const client of clients) {
if (!client.enabled) { if (!client.enabled) {
@ -148,6 +159,12 @@ export default defineNitroPlugin(async (nitroApp) => {
} }
}; };
/**
* 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) => { const remove_forward_rule = async (rule: string) => {
try { try {
const filteredRules = rule.split(';').filter((line) => { const filteredRules = rule.split(';').filter((line) => {
@ -160,19 +177,22 @@ export default defineNitroPlugin(async (nitroApp) => {
return rule; return rule;
}; };
// 初始化:移除hooks中的默认FORWARD允许规则 /**
* Initializes traffic restriction by removing default FORWARD allow rules
* from hook commands.
*/
const initializeTrafficRestriction = async () => { const initializeTrafficRestriction = async () => {
try { try {
const hooks = await Database.hooks.get(); const hooks = await Database.hooks.get();
// 移除hooks中的默认FORWARD允许规则 // Remove default FORWARD allow rules from hooks
const updatedHooks = { const updatedHooks = {
...hooks, ...hooks,
postUp: await remove_forward_rule(hooks.postUp), postUp: await remove_forward_rule(hooks.postUp),
postDown: await remove_forward_rule(hooks.postDown), postDown: await remove_forward_rule(hooks.postDown),
}; };
// 更新hooks到数据库 // Update hooks in database
await Database.hooks.update(updatedHooks); await Database.hooks.update(updatedHooks);
console.log('Removed default FORWARD rules from hooks'); console.log('Removed default FORWARD rules from hooks');
} catch (error) { } catch (error) {
@ -180,9 +200,8 @@ export default defineNitroPlugin(async (nitroApp) => {
} }
}; };
// 注册事件监听器,监听客户端配置保存后的事件 // Register event listeners for wireguard lifecycle hooks
nitroApp.hooks.hook('wireguard:config', applyClientIptablesRules); nitroApp.hooks.hook('wireguard:config', applyClientIptablesRules);
// 注册启动事件
nitroApp.hooks.hook('wireguard:start', async () => { nitroApp.hooks.hook('wireguard:start', async () => {
await initializeTrafficRestriction(); await initializeTrafficRestriction();
await applyClientIptablesRules(); await applyClientIptablesRules();

71
src/server/utils/ip.ts

@ -1,6 +1,7 @@
import { Resolver } from 'node:dns/promises'; import { Resolver } from 'node:dns/promises';
import { networkInterfaces } from 'node:os'; import { networkInterfaces } from 'node:os';
import { stringifyIp } from 'ip-bigint'; import { stringifyIp } from 'ip-bigint';
import { isIPv4, isIPv6 } from 'is-ip';
import type { parseCidr } from 'cidr-tools'; import type { parseCidr } from 'cidr-tools';
import type { ClientNextIpType } from '#db/repositories/client/types'; import type { ClientNextIpType } from '#db/repositories/client/types';
@ -171,43 +172,61 @@ export const cachedGetIpInformation = cacheFunction(getIpInformation, {
expiry: 15 * 60 * 1000, expiry: 15 * 60 * 1000,
}); });
// 检查IP地址是否为IPv6 /**
export const isIPv6 = (ip: string): boolean => { * Parses an IP address with optional port into its components.
return ip.startsWith('[') || ip.split(':').length > 2; * Supports IPv4, IPv6 (with or without brackets), and CIDR notation.
}; *
* @param ipWithPort - IP address that may include a port, e.g.:
// 解析IP地址和端口 * - '192.168.1.1:8080' (IPv4 with port)
* - '[2001:db8::1]:8080' (IPv6 with brackets and port)
* - '2001:db8::1:8080' (IPv6 without brackets and port)
* - '10.0.0.1/24' (CIDR notation, returned as-is)
* @returns Object containing the IP address and optional port
*/
export const parseIpAndPort = ( export const parseIpAndPort = (
ipWithPort: string ipWithPort: string
): { ip: string; port?: string } => { ): { ip: string; port?: string } => {
// CIDR notation: return as-is
if (ipWithPort.includes('/')) { if (ipWithPort.includes('/')) {
return { ip: ipWithPort }; return { ip: ipWithPort };
} }
const parts = ipWithPort.split(':'); 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)) { // Single part: pure IP address without port
return { if (parts.length === 1) {
ip: ip, return { ip: ipWithPort };
port: potentialPort, }
};
} const potentialPort = parts[parts.length - 1];
} else if (!isIPv6(ipWithPort) && parts.length === 2) {
// 对于IPv4地址,格式为 ip:port // IPv4 with port: exactly 2 parts (ip:port)
if (parts.length === 2 && isIPv4(parts[0])) {
return { return {
ip: parts[0], ip: parts[0],
port: parts[1], port: potentialPort,
}; };
} }
// 如果没有端口,则返回原始IP
// IPv6 with port: more than 2 parts when split by ':'
if (parts.length > 3) {
const ip = parts.slice(0, -1).join(':');
if (isIPv6(ip)) {
// Extract IP, removing brackets if present
const matched = /^\[?([0-9a-fA-F:]+)]?$/.exec(ip);
const cleanIp = matched ? matched[1] : ip;
// Validate port is numeric
if (/^\d+$/.test(potentialPort)) {
return {
ip: cleanIp,
port: potentialPort,
};
}
}
}
// Unrecognized format: return original input
return { ip: ipWithPort }; return { ip: ipWithPort };
}; };

Loading…
Cancel
Save