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 30d11f3..693237e 100644 --- a/src/functions/backend/setup/build-host-iptables-scripts.test.ts +++ b/src/functions/backend/setup/build-host-iptables-scripts.test.ts @@ -4,7 +4,6 @@ import buildHostIptablesScripts from "./build-host-iptables-scripts"; const base = { host_id: 0, interface_name: "wgui0", - target_interface: "eth0", }; describe("buildHostIptablesScripts", () => { @@ -39,10 +38,7 @@ describe("buildHostIptablesScripts", () => { expect(res.success).toBe(true); expect(res.post_up).toContain( - "iptables -A WGUI0FWD -s 10.0.0.2/32 -o eth0 -j ACCEPT", - ); - expect(res.post_up).toContain( - "iptables -A WGUI0FWD -s 10.0.0.2/32 -o wgui0 -j ACCEPT", + "iptables -A WGUI0FWD -s 10.0.0.2/32 -j ACCEPT", ); expect(res.post_up).toContain( "iptables -A WGUI0IN -s 10.0.0.2/32 -j ACCEPT", @@ -70,10 +66,7 @@ describe("buildHostIptablesScripts", () => { expect(res.success).toBe(true); expect(res.post_up).toContain( - "iptables -A WGUI0FWD -s 10.0.0.3/32 -o eth0 -p tcp -m multiport --dports 80,443 -j ACCEPT", - ); - expect(res.post_up).toContain( - "iptables -A WGUI0FWD -s 10.0.0.3/32 -o wgui0 -p tcp -m multiport --dports 80,443 -j ACCEPT", + "iptables -A WGUI0FWD -s 10.0.0.3/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", @@ -101,16 +94,10 @@ describe("buildHostIptablesScripts", () => { expect(res.success).toBe(true); expect(res.post_up).toContain( - "iptables -A WGUI0FWD -s 10.0.0.4/32 -o eth0 -p tcp --dport 53 -j ACCEPT", + "iptables -A WGUI0FWD -s 10.0.0.4/32 -p tcp --dport 53 -j ACCEPT", ); expect(res.post_up).toContain( - "iptables -A WGUI0FWD -s 10.0.0.4/32 -o eth0 -p udp --dport 53 -j ACCEPT", - ); - expect(res.post_up).toContain( - "iptables -A WGUI0FWD -s 10.0.0.4/32 -o wgui0 -p tcp --dport 53 -j ACCEPT", - ); - expect(res.post_up).toContain( - "iptables -A WGUI0FWD -s 10.0.0.4/32 -o wgui0 -p udp --dport 53 -j ACCEPT", + "iptables -A WGUI0FWD -s 10.0.0.4/32 -p udp --dport 53 -j ACCEPT", ); expect(res.post_up).toContain( "iptables -A WGUI0IN -s 10.0.0.4/32 -p tcp --dport 53 -j ACCEPT", @@ -196,7 +183,7 @@ describe("buildHostIptablesScripts", () => { ); }); - test("all access scopes same-host forward traffic to the host's own subnet", () => { + test("all access scopes input traffic to the host's own subnet", () => { const res = buildHostIptablesScripts({ ...base, host_subnet: "10.6.0.0/24", @@ -211,10 +198,7 @@ describe("buildHostIptablesScripts", () => { expect(res.success).toBe(true); expect(res.post_up).toContain( - "iptables -A WGUI0FWD -s 10.0.0.7/32 -o eth0 -j ACCEPT", - ); - expect(res.post_up).toContain( - "iptables -A WGUI0FWD -s 10.0.0.7/32 -o wgui0 -d 10.6.0.0/24 -j ACCEPT", + "iptables -A WGUI0FWD -s 10.0.0.7/32 -j ACCEPT", ); expect(res.post_up).toContain( "iptables -A WGUI0IN -s 10.0.0.7/32 -d 10.6.0.0/24 -j ACCEPT", @@ -241,17 +225,14 @@ describe("buildHostIptablesScripts", () => { expect(res.success).toBe(true); expect(res.post_up).toContain( - "iptables -A WGUI0FWD -s 10.0.0.8/32 -o eth0 -p tcp --dport 53 -j ACCEPT", - ); - expect(res.post_up).toContain( - "iptables -A WGUI0FWD -s 10.0.0.8/32 -o wgui0 -p tcp --dport 53 -j ACCEPT", + "iptables -A WGUI0FWD -s 10.0.0.8/32 -p tcp --dport 53 -j ACCEPT", ); expect(res.post_up).toContain( "iptables -A WGUI0IN -s 10.0.0.8/32 -p tcp --dport 53 -j ACCEPT", ); }); - test("client-to-client traffic on the same host is accepted via the scoped forward rule", () => { + test("client-to-client traffic on the same host is accepted on forward and input", () => { const res = buildHostIptablesScripts({ ...base, host_subnet: "10.0.0.0/24", @@ -266,10 +247,10 @@ describe("buildHostIptablesScripts", () => { expect(res.success).toBe(true); expect(res.post_up).toContain( - "iptables -A WGUI0FWD -s 10.0.0.2/32 -o wgui0 -d 10.0.0.0/24 -j ACCEPT", + "iptables -A WGUI0FWD -s 10.0.0.2/32 -j ACCEPT", ); expect(res.post_up).toContain( - "iptables -A WGUI0FWD -s 10.0.0.2/32 -o eth0 -j ACCEPT", + "iptables -A WGUI0IN -s 10.0.0.2/32 -d 10.0.0.0/24 -j ACCEPT", ); }); @@ -277,6 +258,7 @@ describe("buildHostIptablesScripts", () => { const res = buildHostIptablesScripts({ ...base, host_subnet: "10.0.0.0/24", + sibling_subnets: ["10.1.0.0/24", "10.2.0.0/24"], clients: [ { id: 1, @@ -286,7 +268,7 @@ describe("buildHostIptablesScripts", () => { { id: 2, wg_ip_address: "10.0.0.3", - rules: [{ rule_type: "all" }], + rules: [{ rule_type: "destination", ports: "80", protocol: "tcp" }], }, ], }); @@ -294,21 +276,38 @@ describe("buildHostIptablesScripts", () => { expect(res.success).toBe(true); const post_up = res.post_up ?? ""; - expect(post_up).not.toContain( + const sibling_drop = "iptables -A WGUI0FWD -s 10.0.0.2/32 -d 10.1.0.0/24 -j DROP"; + const sibling_drop_2 = "iptables -A WGUI0FWD -s 10.0.0.2/32 -d 10.2.0.0/24 -j DROP"; + const sibling_input_drop = "iptables -A WGUI0IN -s 10.0.0.2/32 -d 10.1.0.0/24 -j DROP"; + const accept = "iptables -A WGUI0FWD -s 10.0.0.2/32 -j ACCEPT"; + + expect(post_up).toContain(sibling_drop); + expect(post_up).toContain(sibling_drop_2); + expect(post_up).toContain(sibling_input_drop); + expect(post_up.indexOf(accept)).toBeGreaterThan( + post_up.indexOf(sibling_drop_2), + ); + }); + + test("a sibling subnet equal to the host's own subnet is ignored", () => { + const res = buildHostIptablesScripts({ + ...base, + host_subnet: "10.1.0.0/24", + sibling_subnets: ["10.1.0.0/24", "10.1.0.0/24"], + clients: [ + { + id: 1, + wg_ip_address: "10.0.0.2", + rules: [{ rule_type: "all" }], + }, + ], + }); + + expect(res.success).toBe(true); + expect(res.post_up).not.toContain("-d 10.1.0.0/24 -j DROP"); + expect(res.post_up).toContain( "iptables -A WGUI0FWD -s 10.0.0.2/32 -j ACCEPT", ); - expect(post_up).not.toContain( - "iptables -A WGUI0FWD -s 10.0.0.3/32 -j ACCEPT", - ); - - const scoped_accept_index = post_up.indexOf( - "iptables -A WGUI0FWD -s 10.0.0.2/32 -o wgui0 -d 10.0.0.0/24 -j ACCEPT", - ); - const catch_all_drop_index = post_up.indexOf( - "iptables -A WGUI0FWD -j DROP", - ); - expect(scoped_accept_index).toBeGreaterThan(-1); - expect(catch_all_drop_index).toBeGreaterThan(scoped_accept_index); }); test("rejects oversized host id", () => { diff --git a/src/functions/backend/setup/build-host-iptables-scripts.ts b/src/functions/backend/setup/build-host-iptables-scripts.ts index 142e60c..959aebb 100644 --- a/src/functions/backend/setup/build-host-iptables-scripts.ts +++ b/src/functions/backend/setup/build-host-iptables-scripts.ts @@ -15,8 +15,8 @@ type HostIptablesClient = { type Params = { host_id: number; interface_name: string; - target_interface: string; host_subnet?: string; + sibling_subnets?: string[]; clients?: HostIptablesClient[]; }; @@ -85,8 +85,6 @@ function acceptLines({ source, protocol, ports, - wg_interface, - target_interface, host_subnet, }: { forward_chain: string; @@ -94,8 +92,6 @@ function acceptLines({ source: string; protocol?: string; ports: string[]; - wg_interface: string; - target_interface: string; host_subnet?: string; }) { const lines: string[] = []; @@ -118,19 +114,6 @@ function acceptLines({ joinArgs([ `iptables -A ${forward_chain}`, `-s ${source}`, - `-o ${target_interface}`, - proto ? `-p ${proto}` : undefined, - port_match_str, - `-j ACCEPT`, - ]), - ); - - lines.push( - joinArgs([ - `iptables -A ${forward_chain}`, - `-s ${source}`, - `-o ${wg_interface}`, - host_subnet ? `-d ${host_subnet}` : undefined, proto ? `-p ${proto}` : undefined, port_match_str, `-j ACCEPT`, @@ -156,8 +139,8 @@ function acceptLines({ export default function buildHostIptablesScripts({ host_id, interface_name, - target_interface, host_subnet, + sibling_subnets, clients, }: Params) { if (!Number.isSafeInteger(host_id) || host_id < 0 || host_id > 999999999) { @@ -174,13 +157,6 @@ export default function buildHostIptablesScripts({ }; } - if (!SAFE_IFACE.test(target_interface)) { - return { - success: false, - msg: `Invalid target interface`, - }; - } - const scoped_subnet = host_subnet?.trim(); if (scoped_subnet && !validateIpv4Cidr({ value: scoped_subnet })) { @@ -190,6 +166,12 @@ export default function buildHostIptablesScripts({ }; } + const sibling_host_subnets = (sibling_subnets || []).filter( + (subnet) => + subnet.trim() !== scoped_subnet && + validateIpv4Cidr({ value: subnet.trim() }), + ); + const { forward, input } = deriveIptablesChainNames({ host_id }); const masquerade_match = scoped_subnet ? `-s ${scoped_subnet} ` : ``; @@ -222,6 +204,14 @@ export default function buildHostIptablesScripts({ const source = `${source_ip}/32`; const rules = client.rules || []; + for (let s = 0; s < sibling_host_subnets.length; s++) { + const sibling_host_subnet = sibling_host_subnets[s]; + accept_lines.push( + `iptables -A ${forward} -s ${source} -d ${sibling_host_subnet} -j DROP`, + `iptables -A ${input} -s ${source} -d ${sibling_host_subnet} -j DROP`, + ); + } + for (let r = 0; r < rules.length; r++) { const rule = rules[r]; @@ -243,14 +233,6 @@ export default function buildHostIptablesScripts({ joinArgs([ `iptables -A ${forward}`, `-s ${source}`, - `-o ${target_interface}`, - `-j ACCEPT`, - ]), - joinArgs([ - `iptables -A ${forward}`, - `-s ${source}`, - `-o ${interface_name}`, - scoped_subnet ? `-d ${scoped_subnet}` : undefined, `-j ACCEPT`, ]), joinArgs([ @@ -280,8 +262,6 @@ export default function buildHostIptablesScripts({ source, protocol: rule.protocol || "any", ports: parsed_ports.ports, - wg_interface: interface_name, - target_interface, 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 58c0a37..ec5e592 100644 --- a/src/functions/backend/setup/setup-wireguard-host.ts +++ b/src/functions/backend/setup/setup-wireguard-host.ts @@ -5,7 +5,6 @@ import type { } from "@/db/types/db"; import grabDirNames from "@/src/utils/grab-dir-names"; import { execSync } from "node:child_process"; -import grabHostNetworkInterface from "./grab-host-network-interface"; import type { APIResponseObject } from "@moduletrace/bunext/types"; import BunSQLite from "@moduletrace/bun-sqlite"; import type { TableType, User } from "@/src/types"; @@ -92,8 +91,31 @@ export default async function setupWireguardHost({ rules_by_client_id.set(rule.client_id, existing_rules); } - const TARGET_INTERFACE = - host?.interface || (await grabHostNetworkInterface()); + const sibling_hosts_res = await BunSQLite.select< + BUN_SQLITE_WGUI_HOSTS, + TableType + >({ + table: "hosts", + }); + + const sibling_subnets = [ + ...new Set( + (sibling_hosts_res.payload || []) + .filter( + (host_row) => + host_row.id != HOST_ID && + typeof host_row.wg_ip_address == "string" && + host_row.wg_ip_address.trim() != "", + ) + .map((host_row) => { + const parts = String(host_row.wg_ip_address) + .trim() + .split("."); + return `${parts.slice(0, 3).join(".")}.0/24`; + }), + ), + ]; + const HOST_WG_IP = host?.wg_ip_address || wg_subnet_ip; if (!HOST_WG_IP) { @@ -178,8 +200,8 @@ export default async function setupWireguardHost({ const iptables_scripts = buildHostIptablesScripts({ host_id: Number(HOST_ID), interface_name: INTERFACE_NAME, - target_interface: TARGET_INTERFACE || "eth0", host_subnet: HOST_SUBNET, + sibling_subnets, clients: clients.map((client) => ({ id: client.id, wg_ip_address: client.wg_ip_address,