From 327d28945d79b9b0f148f47cfda594777dd758b3 Mon Sep 17 00:00:00 2001 From: Bernd Storath <999999bst@gmail.com> Date: Thu, 12 Feb 2026 11:45:56 +0100 Subject: [PATCH] add tests --- src/server/utils/firewall.ts | 23 +++++- src/server/utils/types.ts | 2 + src/test/unit/firewall.spec.ts | 136 +++++++++++++++++++++++++++++++++ 3 files changed, 157 insertions(+), 4 deletions(-) create mode 100644 src/test/unit/firewall.spec.ts diff --git a/src/server/utils/firewall.ts b/src/server/utils/firewall.ts index 448d3a5b..13d14cc5 100644 --- a/src/server/utils/firewall.ts +++ b/src/server/utils/firewall.ts @@ -53,7 +53,11 @@ function parseFirewallEntry(entry: string): ParsedEntry { if (remaining.startsWith('[')) { const match = remaining.match(/^\[(.+)\]:(\d+)$/); if (match) { - return { ip: match[1], port: parseInt(match[2], 10), proto: proto ?? 'both' }; + return { + ip: match[1], + port: parseInt(match[2], 10), + proto: proto ?? 'both', + }; } // Just bracketed IPv6 without port const ipMatch = remaining.match(/^\[(.+)\]$/); @@ -131,7 +135,9 @@ export const firewall = { * Initialize the custom chain if it doesn't exist */ async initChain(interfaceName: string): Promise { - FW_DEBUG(`Initializing firewall chain ${CHAIN_NAME} for interface ${interfaceName}`); + 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`); @@ -169,7 +175,7 @@ export const firewall = { const effectiveIps = client.firewallIps && client.firewallIps.length > 0 ? client.firewallIps - : client.allowedIps ?? defaultAllowedIps; + : (client.allowedIps ?? defaultAllowedIps); FW_DEBUG( `Applying firewall rules for client ${client.name} (${client.id}): ${effectiveIps.join(', ')}` @@ -231,7 +237,11 @@ export const firewall = { // Apply rules for each enabled client for (const client of clients) { if (!client.enabled) continue; - await this.applyClientRules(client, userConfig.defaultAllowedIps, enableIpv6); + await this.applyClientRules( + client, + userConfig.defaultAllowedIps, + enableIpv6 + ); } // Add final DROP for any traffic not explicitly allowed @@ -313,3 +323,8 @@ export const firewall = { iptablesAvailable = null; }, }; + +export const testExports = { + parseFirewallEntry, + generateRuleArgs, +}; diff --git a/src/server/utils/types.ts b/src/server/utils/types.ts index c2045ace..40aeeca2 100644 --- a/src/server/utils/types.ts +++ b/src/server/utils/types.ts @@ -239,3 +239,5 @@ export function validateZod( export function assertUnreachable(_: never): never { throw new Error("Didn't expect to get here"); } + +export const testExports = { FirewallIpEntrySchema }; diff --git a/src/test/unit/firewall.spec.ts b/src/test/unit/firewall.spec.ts new file mode 100644 index 00000000..e8e43f57 --- /dev/null +++ b/src/test/unit/firewall.spec.ts @@ -0,0 +1,136 @@ +import { describe, expect, test } from 'vitest'; +import { testExports } from '../../server/utils/firewall'; +import { testExports as typesTextExports } from '../../server/utils/types'; + +describe('firewall', () => { + describe('isValidFirewallEntry', () => { + test('invalid ips', () => { + expect(() => typesTextExports.FirewallIpEntrySchema.parse('')).toThrow(); + expect(() => + typesTextExports.FirewallIpEntrySchema.parse('255.255.255.256') + ).toThrow(); + expect(() => + typesTextExports.FirewallIpEntrySchema.parse('1.1.1.256') + ).toThrow(); + expect(() => + typesTextExports.FirewallIpEntrySchema.parse('1.1.1.1.1') + ).toThrow(); + + expect(() => + typesTextExports.FirewallIpEntrySchema.parse('[]:443/udp') + ).toThrow(); + expect(() => + typesTextExports.FirewallIpEntrySchema.parse('[]:443') + ).toThrow(); + expect(() => + typesTextExports.FirewallIpEntrySchema.parse('[::1]/32') + ).toThrow(); + expect(() => + typesTextExports.FirewallIpEntrySchema.parse('[1.1.1.1]/32') + ).toThrow(); + expect(() => + typesTextExports.FirewallIpEntrySchema.parse('[::g]/32') + ).toThrow(); + expect(() => + typesTextExports.FirewallIpEntrySchema.parse('2001:dbx::1') + ).toThrow(); + }); + test('invalid port, protocol or cidr', () => { + expect(() => + typesTextExports.FirewallIpEntrySchema.parse('1.1.1.1:80/tcpp') + ).toThrow(); + expect(() => + typesTextExports.FirewallIpEntrySchema.parse('1.1.1.1:65536') + ).toThrow(); + expect(() => + typesTextExports.FirewallIpEntrySchema.parse('1.1.1.1:0') + ).toThrow(); + expect(() => + typesTextExports.FirewallIpEntrySchema.parse('1.1.1.1/33') + ).toThrow(); + expect(() => + typesTextExports.FirewallIpEntrySchema.parse('1.1.1.1/32:0') + ).toThrow(); + }); + test('protocol without port', () => { + expect(() => + typesTextExports.FirewallIpEntrySchema.parse('1.1.1.1/tcp') + ).toThrow(); + }); + test('valid entries', () => { + expect(typesTextExports.FirewallIpEntrySchema.parse('1.1.1.1')).toBe( + '1.1.1.1' + ); + expect(typesTextExports.FirewallIpEntrySchema.parse('::/0')).toBe('::/0'); + expect(typesTextExports.FirewallIpEntrySchema.parse('::0/0')).toBe( + '::0/0' + ); + expect(typesTextExports.FirewallIpEntrySchema.parse('2001:db8::1')).toBe( + '2001:db8::1' + ); + expect(typesTextExports.FirewallIpEntrySchema.parse('::1')).toBe('::1'); + expect( + typesTextExports.FirewallIpEntrySchema.parse('2001:db8::1/32') + ).toBe('2001:db8::1/32'); + expect(typesTextExports.FirewallIpEntrySchema.parse('[::1]')).toEqual({ + ip: '[::1]', + }); + }); + }); + describe('parseFirewallEntry', () => { + test('IPv4 with port and protocol', () => { + expect(() => testExports.parseFirewallEntry('1.1.1.1/tcp')).toThrow(); + expect(() => testExports.parseFirewallEntry('1.1.1.1/udp')).toThrow(); + expect(testExports.parseFirewallEntry('1.1.1.1')).toEqual({ + ip: '1.1.1.1', + }); + expect(testExports.parseFirewallEntry('1.1.1.1:80')).toEqual({ + ip: '1.1.1.1', + port: 80, + proto: 'both', + }); + expect(testExports.parseFirewallEntry('1.1.1.1:80/tcp')).toEqual({ + ip: '1.1.1.1', + port: 80, + proto: 'tcp', + }); + expect(testExports.parseFirewallEntry('1.1.1.1:80/udp')).toEqual({ + ip: '1.1.1.1', + port: 80, + proto: 'udp', + }); + }); + + test('IPv6 with port and protocol', () => { + expect(() => + testExports.parseFirewallEntry('[2001:db8::1]/tcp') + ).toThrow(); + expect(() => + testExports.parseFirewallEntry('[2001:db8::1]/udp') + ).toThrow(); + expect(testExports.parseFirewallEntry('[2001:db8::1]')).toEqual({ + ip: '2001:db8::1', + }); + expect(testExports.parseFirewallEntry('[2001:db8::1]:443')).toEqual({ + ip: '2001:db8::1', + port: 443, + proto: 'both', + }); + expect(testExports.parseFirewallEntry('[2001:db8::1]:443/tcp')).toEqual({ + ip: '2001:db8::1', + port: 443, + proto: 'tcp', + }); + expect(testExports.parseFirewallEntry('[2001:db8::1]:443/udp')).toEqual({ + ip: '2001:db8::1', + port: 443, + proto: 'udp', + }); + + expect(testExports.parseFirewallEntry('::0/0')).toEqual({ + ip: '::0/0', + }); + expect(() => testExports.parseFirewallEntry('::0/0/tcp')).toThrow(); + }); + }); +});