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
69 changes: 10 additions & 59 deletions src/host-iptables-cleanup.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import execa from 'execa';
import { logger } from './logger';
import {
CHAIN_NAME,
CHAIN_NAME_V6,
cleanupChain,
enableIpv6ViaSysctl,
getNetworkBridgeName,
isIp6tablesAvailable,
Expand All @@ -18,70 +18,21 @@ export async function cleanupHostIptables(): Promise<void> {
// 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 });
await cleanupChain('iptables', CHAIN_NAME, {
removeDockerUserReferences: Boolean(bridgeName),
matchPredicate: bridgeName
? (line: string) => (line.includes(`-i ${bridgeName}`) || line.includes(`-o ${bridgeName}`)) && line.includes(CHAIN_NAME)
: undefined,
});

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 });
await cleanupChain('ip6tables', CHAIN_NAME_V6, {
removeDockerUserReferences: Boolean(bridgeName),
});

logger.debug('IPv6 ip6tables rules cleaned up');
} else {
Expand Down
30 changes: 2 additions & 28 deletions src/host-iptables-rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
CHAIN_NAME_V6,
NETWORK_NAME,
addDnsRules,
cleanupChain,
disableIpv6ViaSysctl,
getDockerBridgeGateway,
getNetworkBridgeName,
Expand Down Expand Up @@ -110,34 +111,7 @@ export async function setupHostIptables(squidIp: string, squidPort: number, dnsS
const { exitCode } = await execa('iptables', ['-t', 'filter', '-L', CHAIN_NAME, '-n'], { reject: false });
if (exitCode === 0) {
logger.debug(`Chain '${CHAIN_NAME}' already exists, cleaning up...`);

// First, remove any references from DOCKER-USER
const { stdout } = await execa('iptables', [
'-t', 'filter', '-L', 'DOCKER-USER', '-n', '--line-numbers',
], { reject: false });

const lines = stdout.split('\n');
const lineNumbers: number[] = [];
for (const line of lines) {
if (line.includes(CHAIN_NAME)) {
const match = line.match(/^(\d+)/);
if (match) {
lineNumbers.push(parseInt(match[1], 10));
}
}
}

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

// Then flush and delete the chain
await execa('iptables', ['-t', 'filter', '-F', CHAIN_NAME], { reject: false });
await execa('iptables', ['-t', 'filter', '-X', CHAIN_NAME], { reject: false });
await cleanupChain('iptables', CHAIN_NAME);
}
} catch (error) {
// Ignore errors
Expand Down
40 changes: 40 additions & 0 deletions src/host-iptables-shared.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { execaResult, mockedExeca } from './test-helpers/host-iptables-test-setup';
import { cleanupChain } from './host-iptables-shared';

describe('host-iptables-shared', () => {
beforeEach(() => {
jest.clearAllMocks();
});

describe('cleanupChain', () => {
it('removes matching DOCKER-USER references in reverse order before deleting the chain', async () => {
mockedExeca.mockImplementation(((cmd: string, args: string[]) => {
if (cmd === 'iptables' && args.includes('DOCKER-USER') && args.includes('--line-numbers')) {
return Promise.resolve(execaResult({
stdout: '1 FW_WRAPPER all -- 0.0.0.0/0 0.0.0.0/0\n3 FW_WRAPPER all -- 0.0.0.0/0 0.0.0.0/0\n',
}));
}
return Promise.resolve(execaResult());
}) as any);

await cleanupChain('iptables', 'FW_WRAPPER');

expect(mockedExeca).toHaveBeenCalledWith('iptables', ['-t', 'filter', '-D', 'DOCKER-USER', '3'], { reject: false });
expect(mockedExeca).toHaveBeenCalledWith('iptables', ['-t', 'filter', '-D', 'DOCKER-USER', '1'], { reject: false });
expect(mockedExeca).toHaveBeenCalledWith('iptables', ['-t', 'filter', '-F', 'FW_WRAPPER'], { reject: false });
expect(mockedExeca).toHaveBeenCalledWith('iptables', ['-t', 'filter', '-X', 'FW_WRAPPER'], { reject: false });
});

it('skips DOCKER-USER reference removal when configured', async () => {
mockedExeca.mockResolvedValue(execaResult());

await cleanupChain('ip6tables', 'FW_WRAPPER_V6', { removeDockerUserReferences: false });

expect(mockedExeca).not.toHaveBeenCalledWith('ip6tables', [
'-t', 'filter', '-L', 'DOCKER-USER', '-n', '--line-numbers',
], { reject: false });
expect(mockedExeca).toHaveBeenCalledWith('ip6tables', ['-t', 'filter', '-F', 'FW_WRAPPER_V6'], { reject: false });
expect(mockedExeca).toHaveBeenCalledWith('ip6tables', ['-t', 'filter', '-X', 'FW_WRAPPER_V6'], { reject: false });
});
});
});
38 changes: 38 additions & 0 deletions src/host-iptables-shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,44 @@ export async function addDnsRules(
}
}

/**
* Removes references to a chain from DOCKER-USER, then flushes and deletes the chain.
*/
export async function cleanupChain(
cmd: 'iptables' | 'ip6tables',
chainName: string,
options: {
removeDockerUserReferences?: boolean;
matchPredicate?: (line: string) => boolean;
} = {},
): Promise<void> {
const { removeDockerUserReferences = true, matchPredicate } = options;

if (removeDockerUserReferences) {
const { stdout } = await execa(cmd, [
'-t', 'filter', '-L', 'DOCKER-USER', '-n', '--line-numbers',
], { reject: false });

const lineNumbers: number[] = [];
for (const line of stdout.split('\n')) {
const shouldDelete = matchPredicate ? matchPredicate(line) : line.includes(chainName);
if (shouldDelete) {
const match = line.match(/^(\d+)/);
if (match) {
lineNumbers.push(parseInt(match[1], 10));
}
}
}

for (const lineNum of lineNumbers.reverse()) {
await execa(cmd, ['-t', 'filter', '-D', 'DOCKER-USER', lineNum.toString()], { reject: false });
}
}

await execa(cmd, ['-t', 'filter', '-F', chainName], { reject: false });
await execa(cmd, ['-t', 'filter', '-X', chainName], { reject: false });
}

/**
* Re-enables IPv6 via sysctl if it was previously disabled.
*/
Expand Down
Loading