2025-02-25 00:56:01 +00:00
|
|
|
import { exec, execSync } from 'child_process';
|
2025-02-24 23:27:48 +00:00
|
|
|
import { promisify } from 'util';
|
|
|
|
|
|
|
|
const execAsync = promisify(exec);
|
|
|
|
|
2025-03-07 14:30:38 +00:00
|
|
|
/**
|
|
|
|
* Represents a port range for forwarding
|
|
|
|
*/
|
|
|
|
export interface IPortRange {
|
|
|
|
from: number;
|
|
|
|
to: number;
|
|
|
|
}
|
|
|
|
|
2025-02-25 00:56:01 +00:00
|
|
|
/**
|
|
|
|
* Settings for IPTablesProxy.
|
|
|
|
*/
|
2025-02-24 23:27:48 +00:00
|
|
|
export interface IIpTableProxySettings {
|
2025-03-07 14:30:38 +00:00
|
|
|
// Basic settings
|
|
|
|
fromPort: number | IPortRange | Array<number | IPortRange>; // Support single port, port range, or multiple ports/ranges
|
|
|
|
toPort: number | IPortRange | Array<number | IPortRange>;
|
2025-02-24 23:27:48 +00:00
|
|
|
toHost?: string; // Target host for proxying; defaults to 'localhost'
|
2025-03-07 14:30:38 +00:00
|
|
|
|
|
|
|
// Advanced settings
|
|
|
|
preserveSourceIP?: boolean; // If true, the original source IP is preserved
|
|
|
|
deleteOnExit?: boolean; // If true, clean up marked iptables rules before process exit
|
|
|
|
protocol?: 'tcp' | 'udp' | 'all'; // Protocol to forward, defaults to 'tcp'
|
|
|
|
enableLogging?: boolean; // Enable detailed logging
|
|
|
|
ipv6Support?: boolean; // Enable IPv6 support (ip6tables)
|
|
|
|
|
|
|
|
// Source filtering
|
|
|
|
allowedSourceIPs?: string[]; // If provided, only these IPs are allowed
|
|
|
|
bannedSourceIPs?: string[]; // If provided, these IPs are blocked
|
|
|
|
|
|
|
|
// Rule management
|
|
|
|
forceCleanSlate?: boolean; // Clear all IPTablesProxy rules before starting
|
|
|
|
addJumpRule?: boolean; // Add a custom chain for cleaner rule management
|
|
|
|
checkExistingRules?: boolean; // Check if rules already exist before adding
|
|
|
|
|
|
|
|
// Integration with PortProxy/NetworkProxy
|
|
|
|
netProxyIntegration?: {
|
|
|
|
enabled: boolean;
|
|
|
|
redirectLocalhost?: boolean; // Redirect localhost traffic to NetworkProxy
|
|
|
|
sslTerminationPort?: number; // Port where NetworkProxy handles SSL termination
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Represents a rule added to iptables
|
|
|
|
*/
|
|
|
|
interface IpTablesRule {
|
|
|
|
table: string;
|
|
|
|
chain: string;
|
|
|
|
command: string;
|
|
|
|
tag: string;
|
|
|
|
added: boolean;
|
2025-02-24 23:27:48 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* IPTablesProxy sets up iptables NAT rules to forward TCP traffic.
|
2025-03-07 14:30:38 +00:00
|
|
|
* Enhanced with multi-port support, IPv6, and integration with PortProxy/NetworkProxy.
|
2025-02-24 23:27:48 +00:00
|
|
|
*/
|
|
|
|
export class IPTablesProxy {
|
|
|
|
public settings: IIpTableProxySettings;
|
2025-03-07 14:30:38 +00:00
|
|
|
private rules: IpTablesRule[] = [];
|
2025-02-25 00:56:01 +00:00
|
|
|
private ruleTag: string;
|
2025-03-07 14:30:38 +00:00
|
|
|
private customChain: string | null = null;
|
2025-02-24 23:27:48 +00:00
|
|
|
|
|
|
|
constructor(settings: IIpTableProxySettings) {
|
2025-03-07 14:30:38 +00:00
|
|
|
// Validate inputs to prevent command injection
|
|
|
|
this.validateSettings(settings);
|
|
|
|
|
|
|
|
// Set default settings
|
2025-02-24 23:27:48 +00:00
|
|
|
this.settings = {
|
|
|
|
...settings,
|
|
|
|
toHost: settings.toHost || 'localhost',
|
2025-03-07 14:30:38 +00:00
|
|
|
protocol: settings.protocol || 'tcp',
|
|
|
|
enableLogging: settings.enableLogging !== undefined ? settings.enableLogging : false,
|
|
|
|
ipv6Support: settings.ipv6Support !== undefined ? settings.ipv6Support : false,
|
|
|
|
checkExistingRules: settings.checkExistingRules !== undefined ? settings.checkExistingRules : true,
|
|
|
|
netProxyIntegration: settings.netProxyIntegration || { enabled: false }
|
2025-02-24 23:27:48 +00:00
|
|
|
};
|
2025-03-07 14:30:38 +00:00
|
|
|
|
|
|
|
// Generate a unique identifier for the rules added by this instance
|
2025-02-25 00:56:01 +00:00
|
|
|
this.ruleTag = `IPTablesProxy:${Date.now()}:${Math.random().toString(36).substr(2, 5)}`;
|
2025-03-07 14:30:38 +00:00
|
|
|
|
|
|
|
if (this.settings.addJumpRule) {
|
|
|
|
this.customChain = `IPTablesProxy_${Math.random().toString(36).substr(2, 5)}`;
|
|
|
|
}
|
2025-02-25 00:56:01 +00:00
|
|
|
|
2025-03-07 14:30:38 +00:00
|
|
|
// Register cleanup handlers if deleteOnExit is true
|
2025-02-25 00:56:01 +00:00
|
|
|
if (this.settings.deleteOnExit) {
|
|
|
|
const cleanup = () => {
|
|
|
|
try {
|
2025-03-07 14:30:38 +00:00
|
|
|
this.stopSync();
|
2025-02-25 00:56:01 +00:00
|
|
|
} catch (err) {
|
|
|
|
console.error('Error cleaning iptables rules on exit:', err);
|
|
|
|
}
|
|
|
|
};
|
2025-03-07 14:30:38 +00:00
|
|
|
|
2025-02-25 00:56:01 +00:00
|
|
|
process.on('exit', cleanup);
|
|
|
|
process.on('SIGINT', () => {
|
|
|
|
cleanup();
|
|
|
|
process.exit();
|
|
|
|
});
|
|
|
|
process.on('SIGTERM', () => {
|
|
|
|
cleanup();
|
|
|
|
process.exit();
|
|
|
|
});
|
|
|
|
}
|
2025-02-24 23:27:48 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2025-03-07 14:30:38 +00:00
|
|
|
* Validates settings to prevent command injection and ensure valid values
|
2025-02-24 23:27:48 +00:00
|
|
|
*/
|
2025-03-07 14:30:38 +00:00
|
|
|
private validateSettings(settings: IIpTableProxySettings): void {
|
|
|
|
// Validate port numbers
|
|
|
|
const validatePorts = (port: number | IPortRange | Array<number | IPortRange>) => {
|
|
|
|
if (Array.isArray(port)) {
|
|
|
|
port.forEach(p => validatePorts(p));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (typeof port === 'number') {
|
|
|
|
if (port < 1 || port > 65535) {
|
|
|
|
throw new Error(`Invalid port number: ${port}`);
|
|
|
|
}
|
|
|
|
} else if (typeof port === 'object') {
|
|
|
|
if (port.from < 1 || port.from > 65535 || port.to < 1 || port.to > 65535 || port.from > port.to) {
|
|
|
|
throw new Error(`Invalid port range: ${port.from}-${port.to}`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
validatePorts(settings.fromPort);
|
|
|
|
validatePorts(settings.toPort);
|
|
|
|
|
|
|
|
// Define regex patterns at the method level so they're available throughout
|
|
|
|
const ipRegex = /^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\/([0-9]|[1-2][0-9]|3[0-2]))?$/;
|
|
|
|
const ipv6Regex = /^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))(\/([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8]))?$/;
|
|
|
|
|
|
|
|
// Validate IP addresses
|
|
|
|
const validateIPs = (ips?: string[]) => {
|
|
|
|
if (!ips) return;
|
|
|
|
|
|
|
|
for (const ip of ips) {
|
|
|
|
if (!ipRegex.test(ip) && !ipv6Regex.test(ip)) {
|
|
|
|
throw new Error(`Invalid IP address format: ${ip}`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
validateIPs(settings.allowedSourceIPs);
|
|
|
|
validateIPs(settings.bannedSourceIPs);
|
|
|
|
|
|
|
|
// Validate toHost - only allow hostnames or IPs
|
|
|
|
if (settings.toHost) {
|
|
|
|
const hostRegex = /^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$/;
|
|
|
|
if (!hostRegex.test(settings.toHost) && !ipRegex.test(settings.toHost) && !ipv6Regex.test(settings.toHost)) {
|
|
|
|
throw new Error(`Invalid host format: ${settings.toHost}`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Normalizes port specifications into an array of port ranges
|
|
|
|
*/
|
|
|
|
private normalizePortSpec(portSpec: number | IPortRange | Array<number | IPortRange>): IPortRange[] {
|
|
|
|
const result: IPortRange[] = [];
|
|
|
|
|
|
|
|
if (Array.isArray(portSpec)) {
|
|
|
|
// If it's an array, process each element
|
|
|
|
for (const spec of portSpec) {
|
|
|
|
result.push(...this.normalizePortSpec(spec));
|
|
|
|
}
|
|
|
|
} else if (typeof portSpec === 'number') {
|
|
|
|
// Single port becomes a range with the same start and end
|
|
|
|
result.push({ from: portSpec, to: portSpec });
|
|
|
|
} else {
|
|
|
|
// Already a range
|
|
|
|
result.push(portSpec);
|
|
|
|
}
|
|
|
|
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Gets the appropriate iptables command based on settings
|
|
|
|
*/
|
|
|
|
private getIptablesCommand(isIpv6: boolean = false): string {
|
|
|
|
return isIpv6 ? 'ip6tables' : 'iptables';
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Checks if a rule already exists in iptables
|
|
|
|
*/
|
|
|
|
private async ruleExists(table: string, command: string, isIpv6: boolean = false): Promise<boolean> {
|
2025-02-24 23:27:48 +00:00
|
|
|
try {
|
2025-03-07 14:30:38 +00:00
|
|
|
const iptablesCmd = this.getIptablesCommand(isIpv6);
|
|
|
|
const { stdout } = await execAsync(`${iptablesCmd}-save -t ${table}`);
|
|
|
|
// Convert the command to the format found in iptables-save output
|
|
|
|
// (This is a simplification - in reality, you'd need more parsing)
|
|
|
|
const rulePattern = command.replace(`${iptablesCmd} -t ${table} -A `, '-A ');
|
|
|
|
return stdout.split('\n').some(line => line.trim() === rulePattern);
|
2025-02-24 23:27:48 +00:00
|
|
|
} catch (err) {
|
2025-03-07 14:30:38 +00:00
|
|
|
this.log('error', `Failed to check if rule exists: ${err}`);
|
|
|
|
return false;
|
2025-02-24 23:27:48 +00:00
|
|
|
}
|
2025-03-07 14:30:38 +00:00
|
|
|
}
|
2025-02-24 23:27:48 +00:00
|
|
|
|
2025-03-07 14:30:38 +00:00
|
|
|
/**
|
|
|
|
* Sets up a custom chain for better rule management
|
|
|
|
*/
|
|
|
|
private async setupCustomChain(isIpv6: boolean = false): Promise<boolean> {
|
|
|
|
if (!this.customChain) return true;
|
|
|
|
|
|
|
|
const iptablesCmd = this.getIptablesCommand(isIpv6);
|
|
|
|
const table = 'nat';
|
|
|
|
|
|
|
|
try {
|
|
|
|
// Create the chain
|
|
|
|
await execAsync(`${iptablesCmd} -t ${table} -N ${this.customChain}`);
|
|
|
|
this.log('info', `Created custom chain: ${this.customChain}`);
|
|
|
|
|
|
|
|
// Add jump rule to PREROUTING chain
|
|
|
|
const jumpCommand = `${iptablesCmd} -t ${table} -A PREROUTING -j ${this.customChain} -m comment --comment "${this.ruleTag}:JUMP"`;
|
|
|
|
await execAsync(jumpCommand);
|
|
|
|
this.log('info', `Added jump rule to ${this.customChain}`);
|
|
|
|
|
|
|
|
// Store the jump rule
|
|
|
|
this.rules.push({
|
|
|
|
table,
|
|
|
|
chain: 'PREROUTING',
|
|
|
|
command: jumpCommand,
|
|
|
|
tag: `${this.ruleTag}:JUMP`,
|
|
|
|
added: true
|
|
|
|
});
|
|
|
|
|
|
|
|
return true;
|
|
|
|
} catch (err) {
|
|
|
|
this.log('error', `Failed to set up custom chain: ${err}`);
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Add a source IP filter rule
|
|
|
|
*/
|
|
|
|
private async addSourceIPFilter(isIpv6: boolean = false): Promise<boolean> {
|
|
|
|
if (!this.settings.allowedSourceIPs && !this.settings.bannedSourceIPs) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
const iptablesCmd = this.getIptablesCommand(isIpv6);
|
|
|
|
const table = 'nat';
|
|
|
|
const chain = this.customChain || 'PREROUTING';
|
|
|
|
|
|
|
|
try {
|
|
|
|
// Add banned IPs first (explicit deny)
|
|
|
|
if (this.settings.bannedSourceIPs && this.settings.bannedSourceIPs.length > 0) {
|
|
|
|
for (const ip of this.settings.bannedSourceIPs) {
|
|
|
|
const command = `${iptablesCmd} -t ${table} -A ${chain} -s ${ip} -j DROP -m comment --comment "${this.ruleTag}:BANNED"`;
|
|
|
|
|
|
|
|
// Check if rule already exists
|
|
|
|
if (this.settings.checkExistingRules && await this.ruleExists(table, command, isIpv6)) {
|
|
|
|
this.log('info', `Rule already exists, skipping: ${command}`);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
await execAsync(command);
|
|
|
|
this.log('info', `Added banned IP rule: ${command}`);
|
|
|
|
|
|
|
|
this.rules.push({
|
|
|
|
table,
|
|
|
|
chain,
|
|
|
|
command,
|
|
|
|
tag: `${this.ruleTag}:BANNED`,
|
|
|
|
added: true
|
|
|
|
});
|
2025-02-24 23:27:48 +00:00
|
|
|
}
|
|
|
|
}
|
2025-03-07 14:30:38 +00:00
|
|
|
|
|
|
|
// Add allowed IPs (explicit allow)
|
|
|
|
if (this.settings.allowedSourceIPs && this.settings.allowedSourceIPs.length > 0) {
|
|
|
|
// First add a default deny for all
|
|
|
|
const denyAllCommand = `${iptablesCmd} -t ${table} -A ${chain} -p ${this.settings.protocol} -j DROP -m comment --comment "${this.ruleTag}:DENY_ALL"`;
|
|
|
|
|
|
|
|
// Add allow rules for specific IPs
|
|
|
|
for (const ip of this.settings.allowedSourceIPs) {
|
|
|
|
const command = `${iptablesCmd} -t ${table} -A ${chain} -s ${ip} -p ${this.settings.protocol} -j ACCEPT -m comment --comment "${this.ruleTag}:ALLOWED"`;
|
|
|
|
|
|
|
|
// Check if rule already exists
|
|
|
|
if (this.settings.checkExistingRules && await this.ruleExists(table, command, isIpv6)) {
|
|
|
|
this.log('info', `Rule already exists, skipping: ${command}`);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
await execAsync(command);
|
|
|
|
this.log('info', `Added allowed IP rule: ${command}`);
|
|
|
|
|
|
|
|
this.rules.push({
|
|
|
|
table,
|
|
|
|
chain,
|
|
|
|
command,
|
|
|
|
tag: `${this.ruleTag}:ALLOWED`,
|
|
|
|
added: true
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
// Now add the default deny after all allows
|
|
|
|
if (this.settings.checkExistingRules && await this.ruleExists(table, denyAllCommand, isIpv6)) {
|
|
|
|
this.log('info', `Rule already exists, skipping: ${denyAllCommand}`);
|
|
|
|
} else {
|
|
|
|
await execAsync(denyAllCommand);
|
|
|
|
this.log('info', `Added default deny rule: ${denyAllCommand}`);
|
|
|
|
|
|
|
|
this.rules.push({
|
|
|
|
table,
|
|
|
|
chain,
|
|
|
|
command: denyAllCommand,
|
|
|
|
tag: `${this.ruleTag}:DENY_ALL`,
|
|
|
|
added: true
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
} catch (err) {
|
|
|
|
this.log('error', `Failed to add source IP filter rules: ${err}`);
|
|
|
|
return false;
|
2025-02-24 23:27:48 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2025-03-07 14:30:38 +00:00
|
|
|
* Adds a port forwarding rule
|
2025-02-24 23:27:48 +00:00
|
|
|
*/
|
2025-03-07 14:30:38 +00:00
|
|
|
private async addPortForwardingRule(
|
|
|
|
fromPortRange: IPortRange,
|
|
|
|
toPortRange: IPortRange,
|
|
|
|
isIpv6: boolean = false
|
|
|
|
): Promise<boolean> {
|
|
|
|
const iptablesCmd = this.getIptablesCommand(isIpv6);
|
|
|
|
const table = 'nat';
|
|
|
|
const chain = this.customChain || 'PREROUTING';
|
|
|
|
|
|
|
|
try {
|
|
|
|
// Handle single port case
|
|
|
|
if (fromPortRange.from === fromPortRange.to && toPortRange.from === toPortRange.to) {
|
|
|
|
// Single port forward
|
|
|
|
const command = `${iptablesCmd} -t ${table} -A ${chain} -p ${this.settings.protocol} --dport ${fromPortRange.from} ` +
|
|
|
|
`-j DNAT --to-destination ${this.settings.toHost}:${toPortRange.from} ` +
|
|
|
|
`-m comment --comment "${this.ruleTag}:DNAT"`;
|
|
|
|
|
|
|
|
// Check if rule already exists
|
|
|
|
if (this.settings.checkExistingRules && await this.ruleExists(table, command, isIpv6)) {
|
|
|
|
this.log('info', `Rule already exists, skipping: ${command}`);
|
|
|
|
} else {
|
|
|
|
await execAsync(command);
|
|
|
|
this.log('info', `Added port forwarding rule: ${command}`);
|
|
|
|
|
|
|
|
this.rules.push({
|
|
|
|
table,
|
|
|
|
chain,
|
|
|
|
command,
|
|
|
|
tag: `${this.ruleTag}:DNAT`,
|
|
|
|
added: true
|
|
|
|
});
|
|
|
|
}
|
|
|
|
} else if (fromPortRange.to - fromPortRange.from === toPortRange.to - toPortRange.from) {
|
|
|
|
// Port range forward with equal ranges
|
|
|
|
const command = `${iptablesCmd} -t ${table} -A ${chain} -p ${this.settings.protocol} --dport ${fromPortRange.from}:${fromPortRange.to} ` +
|
|
|
|
`-j DNAT --to-destination ${this.settings.toHost}:${toPortRange.from}-${toPortRange.to} ` +
|
|
|
|
`-m comment --comment "${this.ruleTag}:DNAT_RANGE"`;
|
|
|
|
|
|
|
|
// Check if rule already exists
|
|
|
|
if (this.settings.checkExistingRules && await this.ruleExists(table, command, isIpv6)) {
|
|
|
|
this.log('info', `Rule already exists, skipping: ${command}`);
|
|
|
|
} else {
|
|
|
|
await execAsync(command);
|
|
|
|
this.log('info', `Added port range forwarding rule: ${command}`);
|
|
|
|
|
|
|
|
this.rules.push({
|
|
|
|
table,
|
|
|
|
chain,
|
|
|
|
command,
|
|
|
|
tag: `${this.ruleTag}:DNAT_RANGE`,
|
|
|
|
added: true
|
|
|
|
});
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// Unequal port ranges need individual rules
|
|
|
|
for (let i = 0; i <= fromPortRange.to - fromPortRange.from; i++) {
|
|
|
|
const fromPort = fromPortRange.from + i;
|
|
|
|
const toPort = toPortRange.from + i % (toPortRange.to - toPortRange.from + 1);
|
|
|
|
|
|
|
|
const command = `${iptablesCmd} -t ${table} -A ${chain} -p ${this.settings.protocol} --dport ${fromPort} ` +
|
|
|
|
`-j DNAT --to-destination ${this.settings.toHost}:${toPort} ` +
|
|
|
|
`-m comment --comment "${this.ruleTag}:DNAT_INDIVIDUAL"`;
|
|
|
|
|
|
|
|
// Check if rule already exists
|
|
|
|
if (this.settings.checkExistingRules && await this.ruleExists(table, command, isIpv6)) {
|
|
|
|
this.log('info', `Rule already exists, skipping: ${command}`);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
await execAsync(command);
|
|
|
|
this.log('info', `Added individual port forwarding rule: ${command}`);
|
|
|
|
|
|
|
|
this.rules.push({
|
|
|
|
table,
|
|
|
|
chain,
|
|
|
|
command,
|
|
|
|
tag: `${this.ruleTag}:DNAT_INDIVIDUAL`,
|
|
|
|
added: true
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// If preserveSourceIP is false, add a MASQUERADE rule
|
|
|
|
if (!this.settings.preserveSourceIP) {
|
|
|
|
// For port range
|
|
|
|
const masqCommand = `${iptablesCmd} -t nat -A POSTROUTING -p ${this.settings.protocol} -d ${this.settings.toHost} ` +
|
|
|
|
`--dport ${toPortRange.from}:${toPortRange.to} -j MASQUERADE ` +
|
|
|
|
`-m comment --comment "${this.ruleTag}:MASQ"`;
|
|
|
|
|
|
|
|
// Check if rule already exists
|
|
|
|
if (this.settings.checkExistingRules && await this.ruleExists('nat', masqCommand, isIpv6)) {
|
|
|
|
this.log('info', `Rule already exists, skipping: ${masqCommand}`);
|
|
|
|
} else {
|
|
|
|
await execAsync(masqCommand);
|
|
|
|
this.log('info', `Added MASQUERADE rule: ${masqCommand}`);
|
|
|
|
|
|
|
|
this.rules.push({
|
|
|
|
table: 'nat',
|
|
|
|
chain: 'POSTROUTING',
|
|
|
|
command: masqCommand,
|
|
|
|
tag: `${this.ruleTag}:MASQ`,
|
|
|
|
added: true
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
} catch (err) {
|
|
|
|
this.log('error', `Failed to add port forwarding rule: ${err}`);
|
|
|
|
|
|
|
|
// Try to roll back any rules that were already added
|
|
|
|
await this.rollbackRules();
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
2025-02-24 23:27:48 +00:00
|
|
|
|
2025-03-07 14:30:38 +00:00
|
|
|
/**
|
|
|
|
* Special handling for NetworkProxy integration
|
|
|
|
*/
|
|
|
|
private async setupNetworkProxyIntegration(isIpv6: boolean = false): Promise<boolean> {
|
|
|
|
if (!this.settings.netProxyIntegration?.enabled) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
const netProxyConfig = this.settings.netProxyIntegration;
|
|
|
|
const iptablesCmd = this.getIptablesCommand(isIpv6);
|
|
|
|
const table = 'nat';
|
|
|
|
const chain = this.customChain || 'PREROUTING';
|
|
|
|
|
2025-02-24 23:27:48 +00:00
|
|
|
try {
|
2025-03-07 14:30:38 +00:00
|
|
|
// If redirectLocalhost is true, set up special rule to redirect localhost traffic to NetworkProxy
|
|
|
|
if (netProxyConfig.redirectLocalhost && netProxyConfig.sslTerminationPort) {
|
|
|
|
const redirectCommand = `${iptablesCmd} -t ${table} -A OUTPUT -p tcp -d 127.0.0.1 -j REDIRECT ` +
|
|
|
|
`--to-port ${netProxyConfig.sslTerminationPort} ` +
|
|
|
|
`-m comment --comment "${this.ruleTag}:NETPROXY_REDIRECT"`;
|
|
|
|
|
|
|
|
// Check if rule already exists
|
|
|
|
if (this.settings.checkExistingRules && await this.ruleExists(table, redirectCommand, isIpv6)) {
|
|
|
|
this.log('info', `Rule already exists, skipping: ${redirectCommand}`);
|
|
|
|
} else {
|
|
|
|
await execAsync(redirectCommand);
|
|
|
|
this.log('info', `Added NetworkProxy redirection rule: ${redirectCommand}`);
|
|
|
|
|
|
|
|
this.rules.push({
|
|
|
|
table,
|
|
|
|
chain: 'OUTPUT',
|
|
|
|
command: redirectCommand,
|
|
|
|
tag: `${this.ruleTag}:NETPROXY_REDIRECT`,
|
|
|
|
added: true
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
2025-02-24 23:27:48 +00:00
|
|
|
} catch (err) {
|
2025-03-07 14:30:38 +00:00
|
|
|
this.log('error', `Failed to set up NetworkProxy integration: ${err}`);
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Rolls back rules that were added in case of error
|
|
|
|
*/
|
|
|
|
private async rollbackRules(): Promise<void> {
|
|
|
|
// Process rules in reverse order (LIFO)
|
|
|
|
for (let i = this.rules.length - 1; i >= 0; i--) {
|
|
|
|
const rule = this.rules[i];
|
|
|
|
|
|
|
|
if (rule.added) {
|
|
|
|
try {
|
|
|
|
// Convert -A (add) to -D (delete)
|
|
|
|
const deleteCommand = rule.command.replace('-A', '-D');
|
|
|
|
await execAsync(deleteCommand);
|
|
|
|
this.log('info', `Rolled back rule: ${deleteCommand}`);
|
|
|
|
|
|
|
|
rule.added = false;
|
|
|
|
} catch (err) {
|
|
|
|
this.log('error', `Failed to roll back rule: ${err}`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Sets up iptables rules for port forwarding with enhanced features
|
|
|
|
*/
|
|
|
|
public async start(): Promise<void> {
|
|
|
|
// Optionally clean the slate first
|
|
|
|
if (this.settings.forceCleanSlate) {
|
|
|
|
await IPTablesProxy.cleanSlate();
|
2025-02-24 23:27:48 +00:00
|
|
|
}
|
2025-03-07 14:30:38 +00:00
|
|
|
|
|
|
|
// First set up any custom chains
|
|
|
|
if (this.settings.addJumpRule) {
|
|
|
|
const chainSetupSuccess = await this.setupCustomChain();
|
|
|
|
if (!chainSetupSuccess) {
|
|
|
|
throw new Error('Failed to set up custom chain');
|
|
|
|
}
|
|
|
|
|
|
|
|
// For IPv6 if enabled
|
|
|
|
if (this.settings.ipv6Support) {
|
|
|
|
const chainSetupSuccessIpv6 = await this.setupCustomChain(true);
|
|
|
|
if (!chainSetupSuccessIpv6) {
|
|
|
|
this.log('warn', 'Failed to set up IPv6 custom chain, continuing with IPv4 only');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Add source IP filters
|
|
|
|
await this.addSourceIPFilter();
|
|
|
|
if (this.settings.ipv6Support) {
|
|
|
|
await this.addSourceIPFilter(true);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Set up NetworkProxy integration if enabled
|
|
|
|
if (this.settings.netProxyIntegration?.enabled) {
|
|
|
|
const netProxySetupSuccess = await this.setupNetworkProxyIntegration();
|
|
|
|
if (!netProxySetupSuccess) {
|
|
|
|
this.log('warn', 'Failed to set up NetworkProxy integration');
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this.settings.ipv6Support) {
|
|
|
|
await this.setupNetworkProxyIntegration(true);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Normalize port specifications
|
|
|
|
const fromPortRanges = this.normalizePortSpec(this.settings.fromPort);
|
|
|
|
const toPortRanges = this.normalizePortSpec(this.settings.toPort);
|
|
|
|
|
|
|
|
// Handle the case where fromPort and toPort counts don't match
|
|
|
|
if (fromPortRanges.length !== toPortRanges.length) {
|
|
|
|
if (toPortRanges.length === 1) {
|
|
|
|
// If there's only one toPort, use it for all fromPorts
|
|
|
|
for (const fromRange of fromPortRanges) {
|
|
|
|
await this.addPortForwardingRule(fromRange, toPortRanges[0]);
|
|
|
|
|
|
|
|
if (this.settings.ipv6Support) {
|
|
|
|
await this.addPortForwardingRule(fromRange, toPortRanges[0], true);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
throw new Error('Mismatched port counts: fromPort and toPort arrays must have equal length or toPort must be a single value');
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// Add port forwarding rules for each port specification
|
|
|
|
for (let i = 0; i < fromPortRanges.length; i++) {
|
|
|
|
await this.addPortForwardingRule(fromPortRanges[i], toPortRanges[i]);
|
|
|
|
|
|
|
|
if (this.settings.ipv6Support) {
|
|
|
|
await this.addPortForwardingRule(fromPortRanges[i], toPortRanges[i], true);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Final check - ensure we have at least one rule added
|
|
|
|
if (this.rules.filter(r => r.added).length === 0) {
|
|
|
|
throw new Error('No rules were added');
|
|
|
|
}
|
|
|
|
}
|
2025-02-24 23:27:48 +00:00
|
|
|
|
2025-03-07 14:30:38 +00:00
|
|
|
/**
|
|
|
|
* Removes all added iptables rules
|
|
|
|
*/
|
|
|
|
public async stop(): Promise<void> {
|
|
|
|
// Process rules in reverse order (LIFO)
|
|
|
|
for (let i = this.rules.length - 1; i >= 0; i--) {
|
|
|
|
const rule = this.rules[i];
|
|
|
|
|
|
|
|
if (rule.added) {
|
|
|
|
try {
|
|
|
|
// Convert -A (add) to -D (delete)
|
|
|
|
const deleteCommand = rule.command.replace('-A', '-D');
|
|
|
|
await execAsync(deleteCommand);
|
|
|
|
this.log('info', `Removed rule: ${deleteCommand}`);
|
|
|
|
|
|
|
|
rule.added = false;
|
|
|
|
} catch (err) {
|
|
|
|
this.log('error', `Failed to remove rule: ${err}`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// If we created a custom chain, we need to clean it up
|
|
|
|
if (this.customChain) {
|
2025-02-24 23:27:48 +00:00
|
|
|
try {
|
2025-03-07 14:30:38 +00:00
|
|
|
// First flush the chain
|
|
|
|
await execAsync(`iptables -t nat -F ${this.customChain}`);
|
|
|
|
this.log('info', `Flushed custom chain: ${this.customChain}`);
|
|
|
|
|
|
|
|
// Then delete it
|
|
|
|
await execAsync(`iptables -t nat -X ${this.customChain}`);
|
|
|
|
this.log('info', `Deleted custom chain: ${this.customChain}`);
|
|
|
|
|
|
|
|
// Same for IPv6 if enabled
|
|
|
|
if (this.settings.ipv6Support) {
|
|
|
|
try {
|
|
|
|
await execAsync(`ip6tables -t nat -F ${this.customChain}`);
|
|
|
|
await execAsync(`ip6tables -t nat -X ${this.customChain}`);
|
|
|
|
this.log('info', `Deleted IPv6 custom chain: ${this.customChain}`);
|
|
|
|
} catch (err) {
|
|
|
|
this.log('error', `Failed to delete IPv6 custom chain: ${err}`);
|
|
|
|
}
|
|
|
|
}
|
2025-02-24 23:27:48 +00:00
|
|
|
} catch (err) {
|
2025-03-07 14:30:38 +00:00
|
|
|
this.log('error', `Failed to delete custom chain: ${err}`);
|
2025-02-24 23:27:48 +00:00
|
|
|
}
|
|
|
|
}
|
2025-03-07 14:30:38 +00:00
|
|
|
|
|
|
|
// Clear rules array
|
|
|
|
this.rules = [];
|
|
|
|
}
|
2025-02-24 23:27:48 +00:00
|
|
|
|
2025-03-07 14:30:38 +00:00
|
|
|
/**
|
|
|
|
* Synchronous version of stop, for use in exit handlers
|
|
|
|
*/
|
|
|
|
public stopSync(): void {
|
|
|
|
// Process rules in reverse order (LIFO)
|
|
|
|
for (let i = this.rules.length - 1; i >= 0; i--) {
|
|
|
|
const rule = this.rules[i];
|
|
|
|
|
|
|
|
if (rule.added) {
|
|
|
|
try {
|
|
|
|
// Convert -A (add) to -D (delete)
|
|
|
|
const deleteCommand = rule.command.replace('-A', '-D');
|
|
|
|
execSync(deleteCommand);
|
|
|
|
this.log('info', `Removed rule: ${deleteCommand}`);
|
|
|
|
|
|
|
|
rule.added = false;
|
|
|
|
} catch (err) {
|
|
|
|
this.log('error', `Failed to remove rule: ${err}`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// If we created a custom chain, we need to clean it up
|
|
|
|
if (this.customChain) {
|
|
|
|
try {
|
|
|
|
// First flush the chain
|
|
|
|
execSync(`iptables -t nat -F ${this.customChain}`);
|
|
|
|
|
|
|
|
// Then delete it
|
|
|
|
execSync(`iptables -t nat -X ${this.customChain}`);
|
|
|
|
this.log('info', `Deleted custom chain: ${this.customChain}`);
|
|
|
|
|
|
|
|
// Same for IPv6 if enabled
|
|
|
|
if (this.settings.ipv6Support) {
|
|
|
|
try {
|
|
|
|
execSync(`ip6tables -t nat -F ${this.customChain}`);
|
|
|
|
execSync(`ip6tables -t nat -X ${this.customChain}`);
|
|
|
|
} catch (err) {
|
|
|
|
// IPv6 failures are non-critical
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} catch (err) {
|
|
|
|
this.log('error', `Failed to delete custom chain: ${err}`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Clear rules array
|
|
|
|
this.rules = [];
|
2025-02-24 23:27:48 +00:00
|
|
|
}
|
2025-02-25 00:56:01 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Asynchronously cleans up any iptables rules in the nat table that were added by this module.
|
|
|
|
* It looks for rules with comments containing "IPTablesProxy:".
|
|
|
|
*/
|
|
|
|
public static async cleanSlate(): Promise<void> {
|
2025-03-07 14:30:38 +00:00
|
|
|
await IPTablesProxy.cleanSlateInternal();
|
|
|
|
|
|
|
|
// Also clean IPv6 rules
|
|
|
|
await IPTablesProxy.cleanSlateInternal(true);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Internal implementation of cleanSlate with IPv6 support
|
|
|
|
*/
|
|
|
|
private static async cleanSlateInternal(isIpv6: boolean = false): Promise<void> {
|
|
|
|
const iptablesCmd = isIpv6 ? 'ip6tables' : 'iptables';
|
|
|
|
|
2025-02-25 00:56:01 +00:00
|
|
|
try {
|
2025-03-07 14:30:38 +00:00
|
|
|
const { stdout } = await execAsync(`${iptablesCmd}-save -t nat`);
|
2025-02-25 00:56:01 +00:00
|
|
|
const lines = stdout.split('\n');
|
|
|
|
const proxyLines = lines.filter(line => line.includes('IPTablesProxy:'));
|
2025-03-07 14:30:38 +00:00
|
|
|
|
|
|
|
// First, find and remove any custom chains
|
|
|
|
const customChains = new Set<string>();
|
|
|
|
const jumpRules: string[] = [];
|
|
|
|
|
2025-02-25 00:56:01 +00:00
|
|
|
for (const line of proxyLines) {
|
2025-03-07 14:30:38 +00:00
|
|
|
if (line.includes('IPTablesProxy:JUMP')) {
|
|
|
|
// Extract chain name from jump rule
|
|
|
|
const match = line.match(/\s+-j\s+(\S+)\s+/);
|
|
|
|
if (match && match[1].startsWith('IPTablesProxy_')) {
|
|
|
|
customChains.add(match[1]);
|
|
|
|
jumpRules.push(line);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Remove jump rules first
|
|
|
|
for (const line of jumpRules) {
|
2025-02-25 00:56:01 +00:00
|
|
|
const trimmedLine = line.trim();
|
|
|
|
if (trimmedLine.startsWith('-A')) {
|
2025-03-07 14:30:38 +00:00
|
|
|
// Replace the "-A" with "-D" to form a deletion command
|
2025-02-25 00:56:01 +00:00
|
|
|
const deleteRule = trimmedLine.replace('-A', '-D');
|
2025-03-07 14:30:38 +00:00
|
|
|
const cmd = `${iptablesCmd} -t nat ${deleteRule}`;
|
2025-02-25 00:56:01 +00:00
|
|
|
try {
|
|
|
|
await execAsync(cmd);
|
2025-03-07 14:30:38 +00:00
|
|
|
console.log(`Cleaned up iptables jump rule: ${cmd}`);
|
2025-02-25 00:56:01 +00:00
|
|
|
} catch (err) {
|
2025-03-07 14:30:38 +00:00
|
|
|
console.error(`Failed to remove iptables jump rule: ${cmd}`, err);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Then remove all other rules
|
|
|
|
for (const line of proxyLines) {
|
|
|
|
if (!line.includes('IPTablesProxy:JUMP')) { // Skip jump rules we already handled
|
|
|
|
const trimmedLine = line.trim();
|
|
|
|
if (trimmedLine.startsWith('-A')) {
|
|
|
|
// Replace the "-A" with "-D" to form a deletion command
|
|
|
|
const deleteRule = trimmedLine.replace('-A', '-D');
|
|
|
|
const cmd = `${iptablesCmd} -t nat ${deleteRule}`;
|
|
|
|
try {
|
|
|
|
await execAsync(cmd);
|
|
|
|
console.log(`Cleaned up iptables rule: ${cmd}`);
|
|
|
|
} catch (err) {
|
|
|
|
console.error(`Failed to remove iptables rule: ${cmd}`, err);
|
|
|
|
}
|
2025-02-25 00:56:01 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2025-03-07 14:30:38 +00:00
|
|
|
|
|
|
|
// Finally clean up custom chains
|
|
|
|
for (const chain of customChains) {
|
|
|
|
try {
|
|
|
|
// Flush the chain
|
|
|
|
await execAsync(`${iptablesCmd} -t nat -F ${chain}`);
|
|
|
|
console.log(`Flushed custom chain: ${chain}`);
|
|
|
|
|
|
|
|
// Delete the chain
|
|
|
|
await execAsync(`${iptablesCmd} -t nat -X ${chain}`);
|
|
|
|
console.log(`Deleted custom chain: ${chain}`);
|
|
|
|
} catch (err) {
|
|
|
|
console.error(`Failed to delete custom chain ${chain}:`, err);
|
|
|
|
}
|
|
|
|
}
|
2025-02-25 00:56:01 +00:00
|
|
|
} catch (err) {
|
2025-03-07 14:30:38 +00:00
|
|
|
console.error(`Failed to run ${iptablesCmd}-save: ${err}`);
|
2025-02-25 00:56:01 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Synchronously cleans up any iptables rules in the nat table that were added by this module.
|
|
|
|
* It looks for rules with comments containing "IPTablesProxy:".
|
|
|
|
* This method is intended for use in process exit handlers.
|
|
|
|
*/
|
|
|
|
public static cleanSlateSync(): void {
|
2025-03-07 14:30:38 +00:00
|
|
|
IPTablesProxy.cleanSlateSyncInternal();
|
|
|
|
|
|
|
|
// Also clean IPv6 rules
|
|
|
|
IPTablesProxy.cleanSlateSyncInternal(true);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Internal implementation of cleanSlateSync with IPv6 support
|
|
|
|
*/
|
|
|
|
private static cleanSlateSyncInternal(isIpv6: boolean = false): void {
|
|
|
|
const iptablesCmd = isIpv6 ? 'ip6tables' : 'iptables';
|
|
|
|
|
2025-02-25 00:56:01 +00:00
|
|
|
try {
|
2025-03-07 14:30:38 +00:00
|
|
|
const stdout = execSync(`${iptablesCmd}-save -t nat`).toString();
|
2025-02-25 00:56:01 +00:00
|
|
|
const lines = stdout.split('\n');
|
|
|
|
const proxyLines = lines.filter(line => line.includes('IPTablesProxy:'));
|
2025-03-07 14:30:38 +00:00
|
|
|
|
|
|
|
// First, find and remove any custom chains
|
|
|
|
const customChains = new Set<string>();
|
|
|
|
const jumpRules: string[] = [];
|
|
|
|
|
2025-02-25 00:56:01 +00:00
|
|
|
for (const line of proxyLines) {
|
2025-03-07 14:30:38 +00:00
|
|
|
if (line.includes('IPTablesProxy:JUMP')) {
|
|
|
|
// Extract chain name from jump rule
|
|
|
|
const match = line.match(/\s+-j\s+(\S+)\s+/);
|
|
|
|
if (match && match[1].startsWith('IPTablesProxy_')) {
|
|
|
|
customChains.add(match[1]);
|
|
|
|
jumpRules.push(line);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Remove jump rules first
|
|
|
|
for (const line of jumpRules) {
|
2025-02-25 00:56:01 +00:00
|
|
|
const trimmedLine = line.trim();
|
|
|
|
if (trimmedLine.startsWith('-A')) {
|
2025-03-07 14:30:38 +00:00
|
|
|
// Replace the "-A" with "-D" to form a deletion command
|
2025-02-25 00:56:01 +00:00
|
|
|
const deleteRule = trimmedLine.replace('-A', '-D');
|
2025-03-07 14:30:38 +00:00
|
|
|
const cmd = `${iptablesCmd} -t nat ${deleteRule}`;
|
2025-02-25 00:56:01 +00:00
|
|
|
try {
|
|
|
|
execSync(cmd);
|
2025-03-07 14:30:38 +00:00
|
|
|
console.log(`Cleaned up iptables jump rule: ${cmd}`);
|
2025-02-25 00:56:01 +00:00
|
|
|
} catch (err) {
|
2025-03-07 14:30:38 +00:00
|
|
|
console.error(`Failed to remove iptables jump rule: ${cmd}`, err);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Then remove all other rules
|
|
|
|
for (const line of proxyLines) {
|
|
|
|
if (!line.includes('IPTablesProxy:JUMP')) { // Skip jump rules we already handled
|
|
|
|
const trimmedLine = line.trim();
|
|
|
|
if (trimmedLine.startsWith('-A')) {
|
|
|
|
const deleteRule = trimmedLine.replace('-A', '-D');
|
|
|
|
const cmd = `${iptablesCmd} -t nat ${deleteRule}`;
|
|
|
|
try {
|
|
|
|
execSync(cmd);
|
|
|
|
console.log(`Cleaned up iptables rule: ${cmd}`);
|
|
|
|
} catch (err) {
|
|
|
|
console.error(`Failed to remove iptables rule: ${cmd}`, err);
|
|
|
|
}
|
2025-02-25 00:56:01 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2025-03-07 14:30:38 +00:00
|
|
|
|
|
|
|
// Finally clean up custom chains
|
|
|
|
for (const chain of customChains) {
|
|
|
|
try {
|
|
|
|
// Flush the chain
|
|
|
|
execSync(`${iptablesCmd} -t nat -F ${chain}`);
|
|
|
|
|
|
|
|
// Delete the chain
|
|
|
|
execSync(`${iptablesCmd} -t nat -X ${chain}`);
|
|
|
|
console.log(`Deleted custom chain: ${chain}`);
|
|
|
|
} catch (err) {
|
|
|
|
console.error(`Failed to delete custom chain ${chain}:`, err);
|
|
|
|
}
|
|
|
|
}
|
2025-02-25 00:56:01 +00:00
|
|
|
} catch (err) {
|
2025-03-07 14:30:38 +00:00
|
|
|
console.error(`Failed to run ${iptablesCmd}-save: ${err}`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Logging utility that respects the enableLogging setting
|
|
|
|
*/
|
|
|
|
private log(level: 'info' | 'warn' | 'error', message: string): void {
|
|
|
|
if (!this.settings.enableLogging && level === 'info') {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const timestamp = new Date().toISOString();
|
|
|
|
|
|
|
|
switch (level) {
|
|
|
|
case 'info':
|
|
|
|
console.log(`[${timestamp}] [INFO] ${message}`);
|
|
|
|
break;
|
|
|
|
case 'warn':
|
|
|
|
console.warn(`[${timestamp}] [WARN] ${message}`);
|
|
|
|
break;
|
|
|
|
case 'error':
|
|
|
|
console.error(`[${timestamp}] [ERROR] ${message}`);
|
|
|
|
break;
|
2025-02-25 00:56:01 +00:00
|
|
|
}
|
|
|
|
}
|
2025-02-24 23:27:48 +00:00
|
|
|
}
|