183 lines
6.4 KiB
TypeScript
183 lines
6.4 KiB
TypeScript
import { exec, execSync } from 'child_process';
|
|
import { promisify } from 'util';
|
|
|
|
const execAsync = promisify(exec);
|
|
|
|
/**
|
|
* Settings for IPTablesProxy.
|
|
*/
|
|
export interface IIpTableProxySettings {
|
|
fromPort: number;
|
|
toPort: number;
|
|
toHost?: string; // Target host for proxying; defaults to 'localhost'
|
|
preserveSourceIP?: boolean; // If true, the original source IP is preserved.
|
|
deleteOnExit?: boolean; // If true, clean up marked iptables rules before process exit.
|
|
}
|
|
|
|
/**
|
|
* IPTablesProxy sets up iptables NAT rules to forward TCP traffic.
|
|
* It only supports basic port forwarding and uses iptables comments to tag rules.
|
|
*/
|
|
export class IPTablesProxy {
|
|
public settings: IIpTableProxySettings;
|
|
private rulesInstalled: boolean = false;
|
|
private ruleTag: string;
|
|
|
|
constructor(settings: IIpTableProxySettings) {
|
|
this.settings = {
|
|
...settings,
|
|
toHost: settings.toHost || 'localhost',
|
|
};
|
|
// Generate a unique identifier for the rules added by this instance.
|
|
this.ruleTag = `IPTablesProxy:${Date.now()}:${Math.random().toString(36).substr(2, 5)}`;
|
|
|
|
// If deleteOnExit is true, register cleanup handlers.
|
|
if (this.settings.deleteOnExit) {
|
|
const cleanup = () => {
|
|
try {
|
|
IPTablesProxy.cleanSlateSync();
|
|
} catch (err) {
|
|
console.error('Error cleaning iptables rules on exit:', err);
|
|
}
|
|
};
|
|
process.on('exit', cleanup);
|
|
process.on('SIGINT', () => {
|
|
cleanup();
|
|
process.exit();
|
|
});
|
|
process.on('SIGTERM', () => {
|
|
cleanup();
|
|
process.exit();
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets up iptables rules for port forwarding.
|
|
* The rules are tagged with a unique comment so that they can be identified later.
|
|
*/
|
|
public async start(): Promise<void> {
|
|
const dnatCmd = `iptables -t nat -A PREROUTING -p tcp --dport ${this.settings.fromPort} ` +
|
|
`-j DNAT --to-destination ${this.settings.toHost}:${this.settings.toPort} ` +
|
|
`-m comment --comment "${this.ruleTag}:DNAT"`;
|
|
try {
|
|
await execAsync(dnatCmd);
|
|
console.log(`Added iptables rule: ${dnatCmd}`);
|
|
this.rulesInstalled = true;
|
|
} catch (err) {
|
|
console.error(`Failed to add iptables DNAT rule: ${err}`);
|
|
throw err;
|
|
}
|
|
|
|
// If preserveSourceIP is false, add a MASQUERADE rule.
|
|
if (!this.settings.preserveSourceIP) {
|
|
const masqueradeCmd = `iptables -t nat -A POSTROUTING -p tcp -d ${this.settings.toHost} ` +
|
|
`--dport ${this.settings.toPort} -j MASQUERADE ` +
|
|
`-m comment --comment "${this.ruleTag}:MASQ"`;
|
|
try {
|
|
await execAsync(masqueradeCmd);
|
|
console.log(`Added iptables rule: ${masqueradeCmd}`);
|
|
} catch (err) {
|
|
console.error(`Failed to add iptables MASQUERADE rule: ${err}`);
|
|
// Roll back the DNAT rule if MASQUERADE fails.
|
|
try {
|
|
const rollbackCmd = `iptables -t nat -D PREROUTING -p tcp --dport ${this.settings.fromPort} ` +
|
|
`-j DNAT --to-destination ${this.settings.toHost}:${this.settings.toPort} ` +
|
|
`-m comment --comment "${this.ruleTag}:DNAT"`;
|
|
await execAsync(rollbackCmd);
|
|
this.rulesInstalled = false;
|
|
} catch (rollbackErr) {
|
|
console.error(`Rollback failed: ${rollbackErr}`);
|
|
}
|
|
throw err;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Removes the iptables rules that were added in start(), by matching the unique comment.
|
|
*/
|
|
public async stop(): Promise<void> {
|
|
if (!this.rulesInstalled) return;
|
|
|
|
const dnatDelCmd = `iptables -t nat -D PREROUTING -p tcp --dport ${this.settings.fromPort} ` +
|
|
`-j DNAT --to-destination ${this.settings.toHost}:${this.settings.toPort} ` +
|
|
`-m comment --comment "${this.ruleTag}:DNAT"`;
|
|
try {
|
|
await execAsync(dnatDelCmd);
|
|
console.log(`Removed iptables rule: ${dnatDelCmd}`);
|
|
} catch (err) {
|
|
console.error(`Failed to remove iptables DNAT rule: ${err}`);
|
|
}
|
|
|
|
if (!this.settings.preserveSourceIP) {
|
|
const masqueradeDelCmd = `iptables -t nat -D POSTROUTING -p tcp -d ${this.settings.toHost} ` +
|
|
`--dport ${this.settings.toPort} -j MASQUERADE ` +
|
|
`-m comment --comment "${this.ruleTag}:MASQ"`;
|
|
try {
|
|
await execAsync(masqueradeDelCmd);
|
|
console.log(`Removed iptables rule: ${masqueradeDelCmd}`);
|
|
} catch (err) {
|
|
console.error(`Failed to remove iptables MASQUERADE rule: ${err}`);
|
|
}
|
|
}
|
|
|
|
this.rulesInstalled = false;
|
|
}
|
|
|
|
/**
|
|
* 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> {
|
|
try {
|
|
const { stdout } = await execAsync('iptables-save -t nat');
|
|
const lines = stdout.split('\n');
|
|
const proxyLines = lines.filter(line => line.includes('IPTablesProxy:'));
|
|
for (const line of proxyLines) {
|
|
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 = `iptables -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);
|
|
}
|
|
}
|
|
}
|
|
} catch (err) {
|
|
console.error(`Failed to run iptables-save: ${err}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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 {
|
|
try {
|
|
const stdout = execSync('iptables-save -t nat').toString();
|
|
const lines = stdout.split('\n');
|
|
const proxyLines = lines.filter(line => line.includes('IPTablesProxy:'));
|
|
for (const line of proxyLines) {
|
|
const trimmedLine = line.trim();
|
|
if (trimmedLine.startsWith('-A')) {
|
|
const deleteRule = trimmedLine.replace('-A', '-D');
|
|
const cmd = `iptables -t nat ${deleteRule}`;
|
|
try {
|
|
execSync(cmd);
|
|
console.log(`Cleaned up iptables rule: ${cmd}`);
|
|
} catch (err) {
|
|
console.error(`Failed to remove iptables rule: ${cmd}`, err);
|
|
}
|
|
}
|
|
}
|
|
} catch (err) {
|
|
console.error(`Failed to run iptables-save: ${err}`);
|
|
}
|
|
}
|
|
} |