From ddc1b766b9de311e06e638c2472847558bb47231 Mon Sep 17 00:00:00 2001 From: Bernd Storath <999999bst@gmail.com> Date: Wed, 18 Feb 2026 08:41:28 +0100 Subject: [PATCH] improve tests, typechecking, documentation --- docs/content/guides/clients.md | 13 ++-- src/server/utils/types.ts | 10 +-- src/test/unit/firewall.spec.ts | 123 +++++++++++++++++++++++++++------ 3 files changed, 115 insertions(+), 31 deletions(-) diff --git a/docs/content/guides/clients.md b/docs/content/guides/clients.md index 5c723a9f..eebb9c9c 100644 --- a/docs/content/guides/clients.md +++ b/docs/content/guides/clients.md @@ -35,8 +35,8 @@ Unlike "Allowed IPs" which only controls routing on the client side, these rules **Supported Formats:** -- `10.10.0.3` - Allow access to a single IP address -- `10.10.0.0/24` - Allow access to an entire subnet +- `10.10.0.3`, `2001:db8::1` - Allow access to a single IP address +- `10.10.0.0/24`, `2001:db8::/32` - Allow access to an entire subnet - `192.168.1.5:443` - Allow access to specific port (TCP+UDP) - `192.168.1.5:443/tcp` - Allow access to specific port (TCP only) - `192.168.1.5:443/udp` - Allow access to specific port (UDP only) @@ -62,10 +62,11 @@ Protocol specifiers (`/tcp` or `/udp`) require a port number. The following form - **Disable for specific client**: To disable firewall filtering for a single client while keeping it enabled for others, add `0.0.0.0/0, ::/0` to allow all traffic **Use Case Examples**: - - Allow only specific servers: `10.10.0.5` - - Allow only internal network: `10.10.0.0/24, 192.168.1.0/24` - - Allow only web browsing: `0.0.0.0/0:80, 0.0.0.0/0:443, ::/0:80, ::/0:443` - - Block internet, allow LAN: Leave "Allowed IPs" as `0.0.0.0/0, ::/0` but set Firewall IPs to `10.0.0.0/8, 192.168.0.0/16` + +- Allow only specific servers: `10.10.0.5` +- Allow only internal network: `10.10.0.0/24, 192.168.1.0/24` +- Allow only web browsing: `0.0.0.0/0:80, 0.0.0.0/0:443, [::/0]:80, [::/0]:443` +- Block internet, allow LAN: Leave "Allowed IPs" as `0.0.0.0/0, ::/0` but set Firewall IPs to `10.0.0.0/8, 192.168.0.0/16` ## Server Allowed IPs diff --git a/src/server/utils/types.ts b/src/server/utils/types.ts index 029637a0..a0e9694e 100644 --- a/src/server/utils/types.ts +++ b/src/server/utils/types.ts @@ -111,8 +111,8 @@ const FirewallIpEntrySchema = z return false; } const [, ipPart, portPart] = portMatch; - const port = parseInt(portPart, 10); - const cleanIp = ipPart.replace(/^\[|\]$/g, ''); + const port = parseInt(portPart!, 10); + const cleanIp = ipPart!.replace(/^\[|\]$/g, ''); return (isIP(cleanIp) || isCidr(cleanIp)) && port >= 1 && port <= 65535; } @@ -125,17 +125,17 @@ const FirewallIpEntrySchema = z const bracketedMatch = entryWithoutProto.match(/^\[(.+)\]$/); if (bracketedMatch) { const innerIp = bracketedMatch[1]; - return isIP(innerIp) || isCidr(innerIp); + return isIP(innerIp!) || isCidr(innerIp!); } // Check if it's IP:port format (IPv4:port or [IPv6]:port) const portMatch = entryWithoutProto.match(/^(.+):(\d+)$/); if (portMatch) { const [, ipPart, portPart] = portMatch; - const port = parseInt(portPart, 10); + const port = parseInt(portPart!, 10); // Remove IPv6 brackets if present - const cleanIp = ipPart.replace(/^\[|\]$/g, ''); + const cleanIp = ipPart!.replace(/^\[|\]$/g, ''); // Validate IP and port return (isIP(cleanIp) || isCidr(cleanIp)) && port >= 1 && port <= 65535; diff --git a/src/test/unit/firewall.spec.ts b/src/test/unit/firewall.spec.ts index 00fd76a7..2b95ca21 100644 --- a/src/test/unit/firewall.spec.ts +++ b/src/test/unit/firewall.spec.ts @@ -75,27 +75,43 @@ describe('firewall', () => { expect(typesTestExports.FirewallIpEntrySchema.parse('[::1]')).toBe( '[::1]' ); - expect(firewallTestExports.parseFirewallEntry('[::1]')).toEqual({ - ip: '::1', - }); + expect(typesTestExports.FirewallIpEntrySchema.parse('[::1/32]')).toBe( + '[::1/32]' + ); }); }); describe('parseFirewallEntry', () => { - test('IPv4 with port and protocol', () => { + test('IPv4', () => { + expect(firewallTestExports.parseFirewallEntry('1.1.1.1')).toEqual({ + ip: '1.1.1.1', + }); + }); + test('IPv4 with Protocol', () => { expect(() => firewallTestExports.parseFirewallEntry('1.1.1.1/tcp') ).toThrow(); expect(() => firewallTestExports.parseFirewallEntry('1.1.1.1/udp') ).toThrow(); - expect(firewallTestExports.parseFirewallEntry('1.1.1.1')).toEqual({ - ip: '1.1.1.1', + }); + test('IPv4 with CIDR', () => { + expect(firewallTestExports.parseFirewallEntry('1.1.1.1/32')).toEqual({ + ip: '1.1.1.1/32', }); + }); + test('IPv4 with CIDR and Protocol', () => { + expect(() => + firewallTestExports.parseFirewallEntry('1.1.1.1/32/tcp') + ).toThrow(); + }); + test('IPv4 with Port', () => { expect(firewallTestExports.parseFirewallEntry('1.1.1.1:80')).toEqual({ ip: '1.1.1.1', port: 80, proto: 'both', }); + }); + test('IPv4 with Port and Protocol', () => { expect(firewallTestExports.parseFirewallEntry('1.1.1.1:80/tcp')).toEqual({ ip: '1.1.1.1', port: 80, @@ -107,17 +123,61 @@ describe('firewall', () => { proto: 'udp', }); }); - - test('IPv6 with port and protocol', () => { + test('IPv4 with CIDR and Port', () => { + expect( + firewallTestExports.parseFirewallEntry('10.10.0.0/24:443') + ).toEqual({ + ip: '10.10.0.0/24', + port: 443, + proto: 'both', + }); + }); + test('IPv4 with CIDR, Port and Protocol', () => { + expect( + firewallTestExports.parseFirewallEntry('10.10.0.0/24:443/tcp') + ).toEqual({ + ip: '10.10.0.0/24', + port: 443, + proto: 'tcp', + }); + expect( + firewallTestExports.parseFirewallEntry('10.10.0.0/24:443/udp') + ).toEqual({ + ip: '10.10.0.0/24', + port: 443, + proto: 'udp', + }); + }); + test('IPv6', () => { + expect(firewallTestExports.parseFirewallEntry('[2001:db8::1]')).toEqual({ + ip: '2001:db8::1', + }); + expect(firewallTestExports.parseFirewallEntry('2001:db8::1')).toEqual({ + ip: '2001:db8::1', + }); + }); + test('IPv6 with Protocol', () => { expect(() => firewallTestExports.parseFirewallEntry('[2001:db8::1]/tcp') ).toThrow(); expect(() => - firewallTestExports.parseFirewallEntry('[2001:db8::1]/udp') + firewallTestExports.parseFirewallEntry('2001:db8::1/udp') ).toThrow(); - expect(firewallTestExports.parseFirewallEntry('[2001:db8::1]')).toEqual({ - ip: '2001:db8::1', + }); + test('IPv6 with CIDR', () => { + expect(firewallTestExports.parseFirewallEntry('::0/0')).toEqual({ + ip: '::0/0', }); + expect(firewallTestExports.parseFirewallEntry('[::0/0]')).toEqual({ + ip: '::0/0', + }); + }); + test('IPv6 with CIDR and Protocol', () => { + expect(() => + firewallTestExports.parseFirewallEntry('::0/0/tcp') + ).toThrow(); + }); + test('IPv6 with Port', () => { expect( firewallTestExports.parseFirewallEntry('[2001:db8::1]:443') ).toEqual({ @@ -125,6 +185,8 @@ describe('firewall', () => { port: 443, proto: 'both', }); + }); + test('IPv6 with Port and Protocol', () => { expect( firewallTestExports.parseFirewallEntry('[2001:db8::1]:443/tcp') ).toEqual({ @@ -139,13 +201,24 @@ describe('firewall', () => { port: 443, proto: 'udp', }); - - expect(firewallTestExports.parseFirewallEntry('::0/0')).toEqual({ - ip: '::0/0', + }); + test('IPv6 with CIDR and Port', () => { + expect( + firewallTestExports.parseFirewallEntry('[2001:db8::/32]:443') + ).toEqual({ + ip: '2001:db8::/32', + port: 443, + proto: 'both', + }); + }); + test('IPv6 with CIDR, Port and Protocol', () => { + expect( + firewallTestExports.parseFirewallEntry('[2001:db8::/32]:443/tcp') + ).toEqual({ + ip: '2001:db8::/32', + port: 443, + proto: 'tcp', }); - expect(() => - firewallTestExports.parseFirewallEntry('::0/0/tcp') - ).toThrow(); }); }); describe('sanitizeComment', () => { @@ -189,11 +262,21 @@ describe('firewall', () => { ]); }); test('omits comment when not provided', () => { - const rules = firewallTestExports.generateRuleArgs('10.8.0.2', { + const rulesTcp = firewallTestExports.generateRuleArgs('10.8.0.2', { ip: '10.0.0.1', + port: 80, + proto: 'tcp', }); - expect(rules).toEqual([ - '-A WG_CLIENTS -s 10.8.0.2 -d 10.0.0.1 -j ACCEPT', + expect(rulesTcp).toEqual([ + '-A WG_CLIENTS -s 10.8.0.2 -d 10.0.0.1 -p tcp --dport 80 -j ACCEPT', + ]); + const rulesUdp = firewallTestExports.generateRuleArgs('10.8.0.2', { + ip: '10.0.0.1', + port: 80, + proto: 'udp', + }); + expect(rulesUdp).toEqual([ + '-A WG_CLIENTS -s 10.8.0.2 -d 10.0.0.1 -p udp --dport 80 -j ACCEPT', ]); }); test('comment with port generates two rules for both proto', () => {