fix: skip ip6tables when IPv6 is disabled (#2701)

fix: skip ip6tables when IPv6 is disabled (#2698)

Co-authored-by: potatosips <potatosips@users.noreply.github.com>
This commit is contained in:
potatosips
2026-07-21 09:40:08 +02:00
committed by GitHub
co-authored by potatosips
parent 0b97cf6d23
commit c50e99cd90
2 changed files with 67 additions and 18 deletions
+20 -7
View File
@@ -164,32 +164,38 @@ export const firewall = {
/** /**
* Initialize the custom chain if it doesn't exist * Initialize the custom chain if it doesn't exist
*/ */
async initChain(interfaceName: string): Promise<void> { async initChain(interfaceName: string, enableIpv6: boolean): Promise<void> {
FW_DEBUG( FW_DEBUG(
`Initializing firewall chain ${CHAIN_NAME} for interface ${interfaceName}` `Initializing firewall chain ${CHAIN_NAME} for interface ${interfaceName}`
); );
// Create chain if not exists (iptables returns error if exists, so we ignore) // Create chain if not exists (iptables returns error if exists, so we ignore)
await exec(`iptables -N ${CHAIN_NAME} 2>/dev/null || true`); await exec(`iptables -N ${CHAIN_NAME} 2>/dev/null || true`);
if (enableIpv6) {
await exec(`ip6tables -N ${CHAIN_NAME} 2>/dev/null || true`); await exec(`ip6tables -N ${CHAIN_NAME} 2>/dev/null || true`);
}
// Ensure chain is referenced from FORWARD (if not already) // Ensure chain is referenced from FORWARD (if not already)
// Insert at position 1 to process before generic ACCEPT rules // Insert at position 1 to process before generic ACCEPT rules
await exec( await exec(
`iptables -C FORWARD -i ${interfaceName} -j ${CHAIN_NAME} 2>/dev/null || iptables -I FORWARD 1 -i ${interfaceName} -j ${CHAIN_NAME}` `iptables -C FORWARD -i ${interfaceName} -j ${CHAIN_NAME} 2>/dev/null || iptables -I FORWARD 1 -i ${interfaceName} -j ${CHAIN_NAME}`
); );
if (enableIpv6) {
await exec( await exec(
`ip6tables -C FORWARD -i ${interfaceName} -j ${CHAIN_NAME} 2>/dev/null || ip6tables -I FORWARD 1 -i ${interfaceName} -j ${CHAIN_NAME}` `ip6tables -C FORWARD -i ${interfaceName} -j ${CHAIN_NAME} 2>/dev/null || ip6tables -I FORWARD 1 -i ${interfaceName} -j ${CHAIN_NAME}`
); );
}
}, },
/** /**
* Flush all rules in the custom chain * Flush all rules in the custom chain
*/ */
async flushChain(): Promise<void> { async flushChain(enableIpv6: boolean): Promise<void> {
FW_DEBUG(`Flushing firewall chain ${CHAIN_NAME}`); FW_DEBUG(`Flushing firewall chain ${CHAIN_NAME}`);
await exec(`iptables -F ${CHAIN_NAME} 2>/dev/null || true`); await exec(`iptables -F ${CHAIN_NAME} 2>/dev/null || true`);
if (enableIpv6) {
await exec(`ip6tables -F ${CHAIN_NAME} 2>/dev/null || true`); await exec(`ip6tables -F ${CHAIN_NAME} 2>/dev/null || true`);
}
}, },
/** /**
@@ -245,7 +251,7 @@ export const firewall = {
): Promise<void> { ): Promise<void> {
if (!wgInterface.firewallEnabled) { if (!wgInterface.firewallEnabled) {
FW_DEBUG('Firewall filtering disabled, removing any existing rules'); FW_DEBUG('Firewall filtering disabled, removing any existing rules');
await this.removeFiltering(wgInterface.name); await this.removeFiltering(wgInterface.name, enableIpv6);
return; return;
} }
@@ -262,10 +268,10 @@ export const firewall = {
FW_DEBUG('Rebuilding firewall rules...'); FW_DEBUG('Rebuilding firewall rules...');
// Initialize chain structure // Initialize chain structure
await this.initChain(wgInterface.name); await this.initChain(wgInterface.name, enableIpv6);
// Flush existing rules // Flush existing rules
await this.flushChain(); await this.flushChain(enableIpv6);
// Apply rules for each enabled client // Apply rules for each enabled client
for (const client of clients) { for (const client of clients) {
@@ -299,22 +305,29 @@ export const firewall = {
/** /**
* Remove all firewall filtering (when feature is disabled) * Remove all firewall filtering (when feature is disabled)
*/ */
async removeFiltering(interfaceName: string): Promise<void> { async removeFiltering(
interfaceName: string,
enableIpv6: boolean
): Promise<void> {
FW_DEBUG(`Removing firewall filtering for interface ${interfaceName}`); FW_DEBUG(`Removing firewall filtering for interface ${interfaceName}`);
// Remove jump rules from FORWARD chain // Remove jump rules from FORWARD chain
await exec( await exec(
`iptables -D FORWARD -i ${interfaceName} -j ${CHAIN_NAME} 2>/dev/null || true` `iptables -D FORWARD -i ${interfaceName} -j ${CHAIN_NAME} 2>/dev/null || true`
); );
if (enableIpv6) {
await exec( await exec(
`ip6tables -D FORWARD -i ${interfaceName} -j ${CHAIN_NAME} 2>/dev/null || true` `ip6tables -D FORWARD -i ${interfaceName} -j ${CHAIN_NAME} 2>/dev/null || true`
); );
}
// Flush and delete the chain // Flush and delete the chain
await exec(`iptables -F ${CHAIN_NAME} 2>/dev/null || true`); await exec(`iptables -F ${CHAIN_NAME} 2>/dev/null || true`);
await exec(`ip6tables -F ${CHAIN_NAME} 2>/dev/null || true`);
await exec(`iptables -X ${CHAIN_NAME} 2>/dev/null || true`); await exec(`iptables -X ${CHAIN_NAME} 2>/dev/null || true`);
if (enableIpv6) {
await exec(`ip6tables -F ${CHAIN_NAME} 2>/dev/null || true`);
await exec(`ip6tables -X ${CHAIN_NAME} 2>/dev/null || true`); await exec(`ip6tables -X ${CHAIN_NAME} 2>/dev/null || true`);
}
}, },
/** /**
+38 -2
View File
@@ -1,9 +1,45 @@
import { describe, expect, test } from 'vitest'; import { beforeEach, describe, expect, test, vi } from 'vitest';
import { firewallTestExports } from '#server/utils/firewall'; import { exec } from '#server/utils/cmd';
import { firewall, firewallTestExports } from '#server/utils/firewall';
import { typesTestExports } from '#server/utils/types'; import { typesTestExports } from '#server/utils/types';
vi.mock('#server/utils/cmd', () => ({
exec: vi.fn().mockResolvedValue(''),
}));
const execMock = vi.mocked(exec);
describe('firewall', () => { describe('firewall', () => {
beforeEach(() => {
execMock.mockClear();
});
describe('IPv4-only chain management', () => {
test('does not invoke ip6tables when initializing and flushing', async () => {
await firewall.initChain('wg0', false);
await firewall.flushChain(false);
expect(execMock).toHaveBeenCalledWith(
expect.stringContaining('iptables -C FORWARD -i wg0')
);
expect(execMock).not.toHaveBeenCalledWith(
expect.stringContaining('ip6tables')
);
});
test('does not invoke ip6tables when removing filtering', async () => {
await firewall.removeFiltering('wg0', false);
expect(execMock).toHaveBeenCalledWith(
expect.stringContaining('iptables -D FORWARD -i wg0')
);
expect(execMock).not.toHaveBeenCalledWith(
expect.stringContaining('ip6tables')
);
});
});
describe('isValidFirewallEntry', () => { describe('isValidFirewallEntry', () => {
test('invalid ips', () => { test('invalid ips', () => {
expect(() => typesTestExports.FirewallIpEntrySchema.parse('')).toThrow(); expect(() => typesTestExports.FirewallIpEntrySchema.parse('')).toThrow();