From 4aac24cf2749ea631e3e5978144d31225dbb2c5a Mon Sep 17 00:00:00 2001 From: potatosips Date: Mon, 20 Jul 2026 16:06:10 +0600 Subject: [PATCH] fix: skip ip6tables when IPv6 is disabled (#2698) --- src/server/utils/firewall.ts | 45 ++++++++++++++++++++++------------ src/test/unit/firewall.spec.ts | 40 ++++++++++++++++++++++++++++-- 2 files changed, 67 insertions(+), 18 deletions(-) diff --git a/src/server/utils/firewall.ts b/src/server/utils/firewall.ts index da07f13a..ef7998c6 100644 --- a/src/server/utils/firewall.ts +++ b/src/server/utils/firewall.ts @@ -164,32 +164,38 @@ export const firewall = { /** * Initialize the custom chain if it doesn't exist */ - async initChain(interfaceName: string): Promise { + async initChain(interfaceName: string, enableIpv6: boolean): Promise { FW_DEBUG( `Initializing firewall chain ${CHAIN_NAME} for interface ${interfaceName}` ); // 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(`ip6tables -N ${CHAIN_NAME} 2>/dev/null || true`); + if (enableIpv6) { + await exec(`ip6tables -N ${CHAIN_NAME} 2>/dev/null || true`); + } // Ensure chain is referenced from FORWARD (if not already) // Insert at position 1 to process before generic ACCEPT rules await exec( `iptables -C FORWARD -i ${interfaceName} -j ${CHAIN_NAME} 2>/dev/null || iptables -I FORWARD 1 -i ${interfaceName} -j ${CHAIN_NAME}` ); - await exec( - `ip6tables -C FORWARD -i ${interfaceName} -j ${CHAIN_NAME} 2>/dev/null || ip6tables -I FORWARD 1 -i ${interfaceName} -j ${CHAIN_NAME}` - ); + if (enableIpv6) { + await exec( + `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 */ - async flushChain(): Promise { + async flushChain(enableIpv6: boolean): Promise { FW_DEBUG(`Flushing firewall chain ${CHAIN_NAME}`); await exec(`iptables -F ${CHAIN_NAME} 2>/dev/null || true`); - await exec(`ip6tables -F ${CHAIN_NAME} 2>/dev/null || true`); + if (enableIpv6) { + await exec(`ip6tables -F ${CHAIN_NAME} 2>/dev/null || true`); + } }, /** @@ -245,7 +251,7 @@ export const firewall = { ): Promise { if (!wgInterface.firewallEnabled) { FW_DEBUG('Firewall filtering disabled, removing any existing rules'); - await this.removeFiltering(wgInterface.name); + await this.removeFiltering(wgInterface.name, enableIpv6); return; } @@ -262,10 +268,10 @@ export const firewall = { FW_DEBUG('Rebuilding firewall rules...'); // Initialize chain structure - await this.initChain(wgInterface.name); + await this.initChain(wgInterface.name, enableIpv6); // Flush existing rules - await this.flushChain(); + await this.flushChain(enableIpv6); // Apply rules for each enabled client for (const client of clients) { @@ -299,22 +305,29 @@ export const firewall = { /** * Remove all firewall filtering (when feature is disabled) */ - async removeFiltering(interfaceName: string): Promise { + async removeFiltering( + interfaceName: string, + enableIpv6: boolean + ): Promise { FW_DEBUG(`Removing firewall filtering for interface ${interfaceName}`); // Remove jump rules from FORWARD chain await exec( `iptables -D FORWARD -i ${interfaceName} -j ${CHAIN_NAME} 2>/dev/null || true` ); - await exec( - `ip6tables -D FORWARD -i ${interfaceName} -j ${CHAIN_NAME} 2>/dev/null || true` - ); + if (enableIpv6) { + await exec( + `ip6tables -D FORWARD -i ${interfaceName} -j ${CHAIN_NAME} 2>/dev/null || true` + ); + } // Flush and delete the chain 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(`ip6tables -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`); + } }, /** diff --git a/src/test/unit/firewall.spec.ts b/src/test/unit/firewall.spec.ts index b21afd8d..53710112 100644 --- a/src/test/unit/firewall.spec.ts +++ b/src/test/unit/firewall.spec.ts @@ -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'; +vi.mock('#server/utils/cmd', () => ({ + exec: vi.fn().mockResolvedValue(''), +})); + +const execMock = vi.mocked(exec); + 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', () => { test('invalid ips', () => { expect(() => typesTestExports.FirewallIpEntrySchema.parse('')).toThrow();