|
| 1 | +import { execaResult, mockedExeca, setupHostIptablesTestSuite } from './test-helpers/host-iptables-test-setup'; |
| 2 | +import { cleanupHostIptables, setupHostIptables, __testing } from './host-iptables'; |
| 3 | + |
| 4 | +describe('host-iptables (cleanup)', () => { |
| 5 | + setupHostIptablesTestSuite(__testing._resetIpv6State); |
| 6 | + |
| 7 | + describe('cleanupHostIptables', () => { |
| 8 | + it('should flush and delete both FW_WRAPPER and FW_WRAPPER_V6 chains', async () => { |
| 9 | + mockedExeca.mockResolvedValue(execaResult({ |
| 10 | + stdout: '', |
| 11 | + stderr: '', |
| 12 | + exitCode: 0, |
| 13 | + })); |
| 14 | + |
| 15 | + await cleanupHostIptables(); |
| 16 | + |
| 17 | + // Verify IPv4 chain cleanup operations |
| 18 | + expect(mockedExeca).toHaveBeenCalledWith('iptables', ['-t', 'filter', '-F', 'FW_WRAPPER'], { reject: false }); |
| 19 | + expect(mockedExeca).toHaveBeenCalledWith('iptables', ['-t', 'filter', '-X', 'FW_WRAPPER'], { reject: false }); |
| 20 | + |
| 21 | + // Verify IPv6 chain cleanup operations |
| 22 | + expect(mockedExeca).toHaveBeenCalledWith('ip6tables', ['-t', 'filter', '-F', 'FW_WRAPPER_V6'], { reject: false }); |
| 23 | + expect(mockedExeca).toHaveBeenCalledWith('ip6tables', ['-t', 'filter', '-X', 'FW_WRAPPER_V6'], { reject: false }); |
| 24 | + }); |
| 25 | + |
| 26 | + it('should re-enable IPv6 via sysctl on cleanup if it was disabled', async () => { |
| 27 | + // First, simulate setup that disabled IPv6 |
| 28 | + mockedExeca |
| 29 | + .mockResolvedValueOnce(execaResult({ stdout: 'fw-bridge', stderr: '', exitCode: 0 })) |
| 30 | + .mockResolvedValueOnce(execaResult({ stdout: '', stderr: '', exitCode: 0 })) |
| 31 | + .mockResolvedValueOnce(execaResult({ exitCode: 1 })); |
| 32 | + |
| 33 | + // Make ip6tables unavailable to trigger sysctl disable |
| 34 | + mockedExeca.mockImplementation(((cmd: string) => { |
| 35 | + if (cmd === 'ip6tables') { |
| 36 | + return Promise.reject(new Error('ip6tables not found')); |
| 37 | + } |
| 38 | + return Promise.resolve({ stdout: '', stderr: '', exitCode: 0 }); |
| 39 | + }) as any); |
| 40 | + |
| 41 | + await setupHostIptables('172.30.0.10', 3128, ['8.8.8.8', '8.8.4.4']); |
| 42 | + |
| 43 | + // Now run cleanup |
| 44 | + jest.clearAllMocks(); |
| 45 | + mockedExeca.mockImplementation(((cmd: string) => { |
| 46 | + if (cmd === 'ip6tables') { |
| 47 | + return Promise.reject(new Error('ip6tables not found')); |
| 48 | + } |
| 49 | + return Promise.resolve({ stdout: '', stderr: '', exitCode: 0 }); |
| 50 | + }) as any); |
| 51 | + |
| 52 | + await cleanupHostIptables(); |
| 53 | + |
| 54 | + // Verify IPv6 was re-enabled via sysctl |
| 55 | + expect(mockedExeca).toHaveBeenCalledWith('sysctl', ['-w', 'net.ipv6.conf.all.disable_ipv6=0']); |
| 56 | + expect(mockedExeca).toHaveBeenCalledWith('sysctl', ['-w', 'net.ipv6.conf.default.disable_ipv6=0']); |
| 57 | + }); |
| 58 | + |
| 59 | + it('should clean up IPv6 rules from DOCKER-USER when ip6tables is available', async () => { |
| 60 | + // Mock all calls to succeed (ip6tables available) |
| 61 | + mockedExeca.mockImplementation(((cmd: string, args: string[]) => { |
| 62 | + // getNetworkBridgeName |
| 63 | + if (cmd === 'docker' && args[0] === 'network') { |
| 64 | + return Promise.resolve({ stdout: 'fw-bridge', stderr: '', exitCode: 0 }); |
| 65 | + } |
| 66 | + // ip6tables -L -n (availability check) |
| 67 | + if (cmd === 'ip6tables' && args.includes('-L') && args.includes('-n') && !args.includes('--line-numbers')) { |
| 68 | + return Promise.resolve({ stdout: '', stderr: '', exitCode: 0 }); |
| 69 | + } |
| 70 | + // ip6tables DOCKER-USER listing with FW_WRAPPER_V6 reference |
| 71 | + if (cmd === 'ip6tables' && args.includes('DOCKER-USER') && args.includes('--line-numbers')) { |
| 72 | + return Promise.resolve({ stdout: '1 FW_WRAPPER_V6 all -- * * ::/0 ::/0\n', stderr: '', exitCode: 0 }); |
| 73 | + } |
| 74 | + // iptables DOCKER-USER listing with FW_WRAPPER reference |
| 75 | + if (cmd === 'iptables' && args.includes('DOCKER-USER') && args.includes('--line-numbers')) { |
| 76 | + return Promise.resolve({ stdout: '1 FW_WRAPPER all -- -i fw-bridge -o fw-bridge 0.0.0.0/0 0.0.0.0/0\n', stderr: '', exitCode: 0 }); |
| 77 | + } |
| 78 | + return Promise.resolve({ stdout: '', stderr: '', exitCode: 0 }); |
| 79 | + }) as any); |
| 80 | + |
| 81 | + await cleanupHostIptables(); |
| 82 | + |
| 83 | + // Verify IPv6 chain was flushed and deleted |
| 84 | + expect(mockedExeca).toHaveBeenCalledWith('ip6tables', ['-t', 'filter', '-F', 'FW_WRAPPER_V6'], { reject: false }); |
| 85 | + expect(mockedExeca).toHaveBeenCalledWith('ip6tables', ['-t', 'filter', '-X', 'FW_WRAPPER_V6'], { reject: false }); |
| 86 | + // Verify IPv6 DOCKER-USER rule was removed |
| 87 | + expect(mockedExeca).toHaveBeenCalledWith('ip6tables', ['-t', 'filter', '-D', 'DOCKER-USER', '1'], { reject: false }); |
| 88 | + // Verify IPv4 chain was also cleaned |
| 89 | + expect(mockedExeca).toHaveBeenCalledWith('iptables', ['-t', 'filter', '-F', 'FW_WRAPPER'], { reject: false }); |
| 90 | + expect(mockedExeca).toHaveBeenCalledWith('iptables', ['-t', 'filter', '-X', 'FW_WRAPPER'], { reject: false }); |
| 91 | + }); |
| 92 | + |
| 93 | + it('should skip IPv6 cleanup when ip6tables is not available', async () => { |
| 94 | + mockedExeca.mockImplementation(((cmd: string, args: string[]) => { |
| 95 | + if (cmd === 'docker') { |
| 96 | + return Promise.resolve({ stdout: 'fw-bridge', stderr: '', exitCode: 0 }); |
| 97 | + } |
| 98 | + if (cmd === 'ip6tables') { |
| 99 | + return Promise.reject(new Error('ip6tables not found')); |
| 100 | + } |
| 101 | + if (cmd === 'iptables' && args.includes('DOCKER-USER') && args.includes('--line-numbers')) { |
| 102 | + return Promise.resolve({ stdout: '', stderr: '', exitCode: 0 }); |
| 103 | + } |
| 104 | + return Promise.resolve({ stdout: '', stderr: '', exitCode: 0 }); |
| 105 | + }) as any); |
| 106 | + |
| 107 | + await cleanupHostIptables(); |
| 108 | + |
| 109 | + // Should NOT attempt ip6tables cleanup (except the availability check) |
| 110 | + expect(mockedExeca).not.toHaveBeenCalledWith('ip6tables', ['-t', 'filter', '-F', 'FW_WRAPPER_V6'], { reject: false }); |
| 111 | + }); |
| 112 | + |
| 113 | + it('should not throw on errors (best-effort cleanup)', async () => { |
| 114 | + mockedExeca.mockRejectedValue(new Error('iptables error')); |
| 115 | + |
| 116 | + // Should not throw |
| 117 | + await expect(cleanupHostIptables()).resolves.not.toThrow(); |
| 118 | + }); |
| 119 | + }); |
| 120 | + |
| 121 | + describe('cleanupHostIptables when bridge name is null', () => { |
| 122 | + it('should skip IPv4 DOCKER-USER rule removal when bridge name is not found', async () => { |
| 123 | + mockedExeca.mockImplementation(((cmd: string, args: string[]) => { |
| 124 | + // getNetworkBridgeName returns empty string → null |
| 125 | + if (cmd === 'docker' && args[0] === 'network' && args[1] === 'inspect') { |
| 126 | + return Promise.resolve({ stdout: '', stderr: '', exitCode: 0 }); |
| 127 | + } |
| 128 | + return Promise.resolve({ stdout: '', stderr: '', exitCode: 0 }); |
| 129 | + }) as any); |
| 130 | + |
| 131 | + await cleanupHostIptables(); |
| 132 | + |
| 133 | + // Should NOT attempt to list DOCKER-USER rules (bridge name is null) |
| 134 | + expect(mockedExeca).not.toHaveBeenCalledWith('iptables', [ |
| 135 | + '-t', 'filter', '-L', 'DOCKER-USER', '-n', '--line-numbers', |
| 136 | + ], { reject: false }); |
| 137 | + |
| 138 | + // Should still flush/delete the chain |
| 139 | + expect(mockedExeca).toHaveBeenCalledWith('iptables', ['-t', 'filter', '-F', 'FW_WRAPPER'], { reject: false }); |
| 140 | + expect(mockedExeca).toHaveBeenCalledWith('iptables', ['-t', 'filter', '-X', 'FW_WRAPPER'], { reject: false }); |
| 141 | + }); |
| 142 | + }); |
| 143 | +}); |
0 commit comments