diff --git a/package.json b/package.json index 4d2e275..41e6eec 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,8 @@ { - "name": "bunext-mariadb-preset", + "name": "wireguard-ui", "module": "index.ts", "type": "module", + "version": "0.1.0", "private": true, "scripts": { "dev": "sudo bun src/server.ts", diff --git a/src/functions/backend/setup/build-host-iptables-scripts.test.ts b/src/functions/backend/setup/build-host-iptables-scripts.test.ts index 693237e..f9b4cb2 100644 --- a/src/functions/backend/setup/build-host-iptables-scripts.test.ts +++ b/src/functions/backend/setup/build-host-iptables-scripts.test.ts @@ -66,12 +66,43 @@ describe("buildHostIptablesScripts", () => { expect(res.success).toBe(true); expect(res.post_up).toContain( - "iptables -A WGUI0FWD -s 10.0.0.3/32 -p tcp -m multiport --dports 80,443 -j ACCEPT", + "iptables -A WGUI0FWD -s 10.0.0.3/32 -d 192.168.1.10/32 -p tcp -m multiport --dports 80,443 -j ACCEPT", ); expect(res.post_up).toContain( - "iptables -A WGUI0IN -s 10.0.0.3/32 -p tcp -m multiport --dports 80,443 -j ACCEPT", + "iptables -A WGUI0IN -s 10.0.0.3/32 -d 192.168.1.10/32 -p tcp -m multiport --dports 80,443 -j ACCEPT", + ); + }); + + test("destination overrides the host subnet scope on input", () => { + const res = buildHostIptablesScripts({ + ...base, + host_subnet: "10.5.0.0/24", + clients: [ + { + id: 8, + wg_ip_address: "10.5.0.2", + rules: [ + { + rule_type: "destination", + destination: "65.108.81.79", + ports: "443", + protocol: "tcp", + }, + ], + }, + ], + }); + + expect(res.success).toBe(true); + expect(res.post_up).toContain( + "iptables -A WGUI0FWD -s 10.5.0.2/32 -d 65.108.81.79/32 -p tcp --dport 443 -j ACCEPT", + ); + expect(res.post_up).toContain( + "iptables -A WGUI0IN -s 10.5.0.2/32 -d 65.108.81.79/32 -p tcp --dport 443 -j ACCEPT", + ); + expect(res.post_up).not.toContain( + "iptables -A WGUI0IN -s 10.5.0.2/32 -d 10.5.0.0/24 -p tcp --dport 443 -j ACCEPT", ); - expect(res.post_up).not.toContain("-d 192.168.1.10/32"); }); test("protocol any with ports emits tcp and udp", () => { @@ -310,6 +341,120 @@ describe("buildHostIptablesScripts", () => { ); }); + test("allow_all_ips clients get full input and forward access", () => { + const res = buildHostIptablesScripts({ + ...base, + host_subnet: "10.7.0.0/24", + clients: [ + { + id: 9, + wg_ip_address: "10.7.0.3", + allow_all_ips: true, + }, + ], + }); + + expect(res.success).toBe(true); + expect(res.post_up).toContain( + "iptables -A WGUI0FWD -s 10.7.0.3/32 -j ACCEPT", + ); + expect(res.post_up).toContain( + "iptables -A WGUI0IN -s 10.7.0.3/32 -j ACCEPT", + ); + expect(res.post_up).not.toContain("-d 10.7.0.0/24"); + }); + + test("allow_all_ips clients still cannot jump to sibling host subnets", () => { + const res = buildHostIptablesScripts({ + ...base, + host_subnet: "10.7.0.0/24", + sibling_subnets: ["10.3.0.0/24"], + clients: [ + { + id: 10, + wg_ip_address: "10.7.0.4", + allow_all_ips: true, + }, + ], + }); + + expect(res.success).toBe(true); + const post_up = res.post_up ?? ""; + + const sibling_drop = + "iptables -A WGUI0IN -s 10.7.0.4/32 -d 10.3.0.0/24 -j DROP"; + const input_accept = "iptables -A WGUI0IN -s 10.7.0.4/32 -j ACCEPT"; + + expect(post_up).toContain(sibling_drop); + expect(post_up).toContain(input_accept); + expect(post_up.indexOf(input_accept)).toBeGreaterThan( + post_up.indexOf(sibling_drop), + ); + }); + + test("explicit rules are ignored for allow_all_ips clients", () => { + const res = buildHostIptablesScripts({ + ...base, + clients: [ + { + id: 11, + wg_ip_address: "10.7.0.5", + allow_all_ips: true, + rules: [ + { + rule_type: "destination", + destination: "192.0.2.1", + ports: "80", + protocol: "tcp", + }, + ], + }, + ], + }); + + expect(res.success).toBe(true); + expect(res.post_up).toContain( + "iptables -A WGUI0FWD -s 10.7.0.5/32 -j ACCEPT", + ); + expect(res.post_up).not.toContain("192.0.2.1"); + }); + + test("destination rules cannot bypass sibling subnet isolation", () => { + const res = buildHostIptablesScripts({ + ...base, + host_subnet: "10.0.0.0/24", + sibling_subnets: ["10.1.0.0/24"], + clients: [ + { + id: 12, + wg_ip_address: "10.0.0.6", + rules: [ + { + rule_type: "destination", + destination: "10.1.0.25", + ports: "80", + protocol: "tcp", + }, + ], + }, + ], + }); + + expect(res.success).toBe(true); + const post_up = res.post_up ?? ""; + + const sibling_drop = + "iptables -A WGUI0IN -s 10.0.0.6/32 -d 10.1.0.0/24 -j DROP"; + const accept = + "iptables -A WGUI0IN -s 10.0.0.6/32 -d 10.1.0.25/32 -p tcp --dport 80 -j ACCEPT"; + + expect(post_up).toContain(sibling_drop); + expect(post_up).toContain(accept); + expect(post_up.indexOf(accept)).toBeGreaterThan( + post_up.indexOf(sibling_drop), + ); + }); + test("rejects oversized host id", () => { const res = buildHostIptablesScripts({ ...base, diff --git a/src/functions/backend/setup/build-host-iptables-scripts.ts b/src/functions/backend/setup/build-host-iptables-scripts.ts index 959aebb..f3ca6e3 100644 --- a/src/functions/backend/setup/build-host-iptables-scripts.ts +++ b/src/functions/backend/setup/build-host-iptables-scripts.ts @@ -9,6 +9,7 @@ import validateClientRule from "./validate-client-rule"; type HostIptablesClient = { id?: number | ""; wg_ip_address?: string; + allow_all_ips?: boolean; rules?: BUN_SQLITE_WGUI_CLIENT_RULES[]; }; @@ -85,6 +86,7 @@ function acceptLines({ source, protocol, ports, + destination, host_subnet, }: { forward_chain: string; @@ -92,6 +94,7 @@ function acceptLines({ source: string; protocol?: string; ports: string[]; + destination?: string; host_subnet?: string; }) { const lines: string[] = []; @@ -109,11 +112,14 @@ function acceptLines({ const port_match_str = proto ? portMatch({ ports: chunk }) : undefined; + const direction = destination ? `-d ${destination}` : undefined; + const input_destination = destination || host_subnet; lines.push( joinArgs([ `iptables -A ${forward_chain}`, `-s ${source}`, + direction, proto ? `-p ${proto}` : undefined, port_match_str, `-j ACCEPT`, @@ -124,7 +130,7 @@ function acceptLines({ joinArgs([ `iptables -A ${input_chain}`, `-s ${source}`, - host_subnet ? `-d ${host_subnet}` : undefined, + input_destination ? `-d ${input_destination}` : undefined, proto ? `-p ${proto}` : undefined, port_match_str, `-j ACCEPT`, @@ -212,6 +218,15 @@ export default function buildHostIptablesScripts({ ); } + if (client.allow_all_ips) { + accept_lines.push( + `iptables -A ${forward} -s ${source} -j ACCEPT`, + `iptables -A ${input} -s ${source} -j ACCEPT`, + ); + + continue; + } + for (let r = 0; r < rules.length; r++) { const rule = rules[r]; @@ -255,6 +270,10 @@ export default function buildHostIptablesScripts({ }; } + const destination = normalizeIptablesDestination({ + destination: rule.destination, + }); + accept_lines.push( ...acceptLines({ forward_chain: forward, @@ -262,6 +281,7 @@ export default function buildHostIptablesScripts({ source, protocol: rule.protocol || "any", ports: parsed_ports.ports, + destination, host_subnet: scoped_subnet, }), ); diff --git a/src/functions/backend/setup/setup-wireguard-host.ts b/src/functions/backend/setup/setup-wireguard-host.ts index ec5e592..ff74ef0 100644 --- a/src/functions/backend/setup/setup-wireguard-host.ts +++ b/src/functions/backend/setup/setup-wireguard-host.ts @@ -205,6 +205,7 @@ export default async function setupWireguardHost({ clients: clients.map((client) => ({ id: client.id, wg_ip_address: client.wg_ip_address, + allow_all_ips: client.allow_all_ips == 1, rules: client.id && typeof client.id == "number" ? rules_by_client_id.get(client.id) || []