Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 99 additions & 0 deletions src/host-iptables-cleanup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import execa from 'execa';
import { logger } from './logger';
import {
CHAIN_NAME,
CHAIN_NAME_V6,
enableIpv6ViaSysctl,
getNetworkBridgeName,
isIp6tablesAvailable,
} from './host-iptables-shared';

/**
* Cleans up host-level iptables rules (both IPv4 and IPv6)
*/
export async function cleanupHostIptables(): Promise<void> {
logger.debug('Cleaning up host-level iptables rules...');

try {
// Get the bridge name
const bridgeName = await getNetworkBridgeName();

// Clean up IPv4 rules
if (bridgeName) {
// Find and remove the rule that jumps to our chain
const { stdout } = await execa('iptables', [
'-t', 'filter', '-L', 'DOCKER-USER', '-n', '--line-numbers',
], { reject: false });

// Parse line numbers for rules that reference our bridge
const lines = stdout.split('\n');
const lineNumbers: number[] = [];
for (const line of lines) {
if ((line.includes(`-i ${bridgeName}`) || line.includes(`-o ${bridgeName}`)) && line.includes(CHAIN_NAME)) {
const match = line.match(/^(\d+)/);
if (match) {
lineNumbers.push(parseInt(match[1], 10));
}
}
}

// Delete rules in reverse order (to maintain line numbers)
for (const lineNum of lineNumbers.reverse()) {
logger.debug(`Removing rule ${lineNum} from DOCKER-USER (IPv4)`);
await execa('iptables', [
'-t', 'filter', '-D', 'DOCKER-USER', lineNum.toString(),
], { reject: false });
}
}

// Flush and delete our custom IPv4 chain
await execa('iptables', ['-t', 'filter', '-F', CHAIN_NAME], { reject: false });
await execa('iptables', ['-t', 'filter', '-X', CHAIN_NAME], { reject: false });

logger.debug('IPv4 iptables rules cleaned up');

// Clean up IPv6 rules (only if ip6tables is available)
const ip6tablesAvailable = await isIp6tablesAvailable();
if (ip6tablesAvailable) {
if (bridgeName) {
const { stdout: stdout6 } = await execa('ip6tables', [
'-t', 'filter', '-L', 'DOCKER-USER', '-n', '--line-numbers',
], { reject: false });

const lines6 = stdout6.split('\n');
const lineNumbers6: number[] = [];
for (const line of lines6) {
if (line.includes(CHAIN_NAME_V6)) {
const match = line.match(/^(\d+)/);
if (match) {
lineNumbers6.push(parseInt(match[1], 10));
}
}
}

for (const lineNum of lineNumbers6.reverse()) {
logger.debug(`Removing rule ${lineNum} from DOCKER-USER (IPv6)`);
await execa('ip6tables', [
'-t', 'filter', '-D', 'DOCKER-USER', lineNum.toString(),
], { reject: false });
}
}

// Flush and delete our custom IPv6 chain
await execa('ip6tables', ['-t', 'filter', '-F', CHAIN_NAME_V6], { reject: false });
await execa('ip6tables', ['-t', 'filter', '-X', CHAIN_NAME_V6], { reject: false });

logger.debug('IPv6 ip6tables rules cleaned up');
} else {
logger.debug('ip6tables not available, skipping IPv6 cleanup');
}

// Re-enable IPv6 if it was disabled via sysctl
await enableIpv6ViaSysctl();

logger.debug('Host-level iptables rules cleaned up');
} catch (error) {
logger.debug('Error cleaning up iptables rules:', error);
// Don't throw - cleanup should be best-effort
}
}
64 changes: 64 additions & 0 deletions src/host-iptables-network.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import execa from 'execa';
import { logger } from './logger';
import { getLocalDockerEnv } from './docker-manager';
import { NETWORK_NAME, NETWORK_SUBNET } from './host-iptables-shared';

/**
* Creates the dedicated firewall network if it doesn't exist
* Returns the firewall subnet and reserved container IPs (squid/agent/proxy)
*/
export async function ensureFirewallNetwork(): Promise<{
subnet: string;
squidIp: string;
agentIp: string;
proxyIp: string;
}> {
Comment on lines +6 to +15
logger.debug(`Ensuring firewall network '${NETWORK_NAME}' exists...`);

// Check if network already exists
let networkExists = false;
try {
await execa('docker', ['network', 'inspect', NETWORK_NAME], { env: getLocalDockerEnv() });
networkExists = true;
logger.debug(`Network '${NETWORK_NAME}' already exists`);
} catch {
// Network doesn't exist
}

if (!networkExists) {
// Network doesn't exist, create it with explicit bridge name
logger.debug(`Creating network '${NETWORK_NAME}' with subnet ${NETWORK_SUBNET}...`);
await execa('docker', [
'network',
'create',
NETWORK_NAME,
'--subnet',
NETWORK_SUBNET,
'--opt',
'com.docker.network.bridge.name=fw-bridge',
], { env: getLocalDockerEnv() });
logger.success(`Created network '${NETWORK_NAME}' with bridge 'fw-bridge'`);
}

return {
subnet: NETWORK_SUBNET,
squidIp: '172.30.0.10',
agentIp: '172.30.0.20',
proxyIp: '172.30.0.30',
};
}

/**
* Removes the firewall network
*/
export async function cleanupFirewallNetwork(): Promise<void> {
logger.debug(`Removing firewall network '${NETWORK_NAME}'...`);

try {
await execa('docker', ['network', 'rm', NETWORK_NAME], { reject: false, env: getLocalDockerEnv() });
logger.debug('Firewall network removed');
} catch (error) {
logger.debug('Error removing firewall network:', error);
// Don't throw - cleanup should be best-effort
}
}
Loading
Loading