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 f94f2ad..30d11f3 100644 --- a/src/functions/backend/setup/build-host-iptables-scripts.test.ts +++ b/src/functions/backend/setup/build-host-iptables-scripts.test.ts @@ -39,7 +39,10 @@ describe("buildHostIptablesScripts", () => { expect(res.success).toBe(true); expect(res.post_up).toContain( - "iptables -A WGUI0FWD -s 10.0.0.2/32 -j ACCEPT", + "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", ); expect(res.post_up).toContain( "iptables -A WGUI0IN -s 10.0.0.2/32 -j ACCEPT", @@ -67,7 +70,10 @@ 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 -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", ); expect(res.post_up).toContain( "iptables -A WGUI0IN -s 10.0.0.3/32 -p tcp -m multiport --dports 80,443 -j ACCEPT", @@ -95,10 +101,16 @@ describe("buildHostIptablesScripts", () => { expect(res.success).toBe(true); expect(res.post_up).toContain( - "iptables -A WGUI0FWD -s 10.0.0.4/32 -p tcp --dport 53 -j ACCEPT", + "iptables -A WGUI0FWD -s 10.0.0.4/32 -o eth0 -p tcp --dport 53 -j ACCEPT", ); expect(res.post_up).toContain( - "iptables -A WGUI0FWD -s 10.0.0.4/32 -p udp --dport 53 -j ACCEPT", + "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", ); expect(res.post_up).toContain( "iptables -A WGUI0IN -s 10.0.0.4/32 -p tcp --dport 53 -j ACCEPT", @@ -184,7 +196,7 @@ describe("buildHostIptablesScripts", () => { ); }); - test("all access accepts the client on forward and input regardless of host_subnet", () => { + test("all access scopes same-host forward traffic to the host's own subnet", () => { const res = buildHostIptablesScripts({ ...base, host_subnet: "10.6.0.0/24", @@ -199,13 +211,14 @@ describe("buildHostIptablesScripts", () => { expect(res.success).toBe(true); expect(res.post_up).toContain( - "iptables -A WGUI0FWD -s 10.0.0.7/32 -j ACCEPT", + "iptables -A WGUI0FWD -s 10.0.0.7/32 -o eth0 -j ACCEPT", ); expect(res.post_up).toContain( - "iptables -A WGUI0IN -s 10.0.0.7/32 -j ACCEPT", + "iptables -A WGUI0FWD -s 10.0.0.7/32 -o wgui0 -d 10.6.0.0/24 -j ACCEPT", + ); + expect(res.post_up).toContain( + "iptables -A WGUI0IN -s 10.0.0.7/32 -d 10.6.0.0/24 -j ACCEPT", ); - expect(res.post_up).not.toContain("-d 10.6.0.0/24"); - expect(res.post_up).not.toContain("-o eth0"); }); test("port-only rules are mirrored onto both forward and input chains", () => { @@ -228,13 +241,76 @@ describe("buildHostIptablesScripts", () => { expect(res.success).toBe(true); expect(res.post_up).toContain( - "iptables -A WGUI0FWD -s 10.0.0.8/32 -p tcp --dport 53 -j ACCEPT", + "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", ); 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", () => { + const res = buildHostIptablesScripts({ + ...base, + host_subnet: "10.0.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).toContain( + "iptables -A WGUI0FWD -s 10.0.0.2/32 -o wgui0 -d 10.0.0.0/24 -j ACCEPT", + ); + expect(res.post_up).toContain( + "iptables -A WGUI0FWD -s 10.0.0.2/32 -o eth0 -j ACCEPT", + ); + }); + + test("clients cannot jump to another host on the same machine", () => { + const res = buildHostIptablesScripts({ + ...base, + host_subnet: "10.0.0.0/24", + clients: [ + { + id: 1, + wg_ip_address: "10.0.0.2", + rules: [{ rule_type: "all" }], + }, + { + id: 2, + wg_ip_address: "10.0.0.3", + rules: [{ rule_type: "all" }], + }, + ], + }); + + expect(res.success).toBe(true); + const post_up = res.post_up ?? ""; + + expect(post_up).not.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", () => { 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 49b4a4e..142e60c 100644 --- a/src/functions/backend/setup/build-host-iptables-scripts.ts +++ b/src/functions/backend/setup/build-host-iptables-scripts.ts @@ -85,14 +85,18 @@ function acceptLines({ source, protocol, ports, - host_wg_ip, + wg_interface, + target_interface, + host_subnet, }: { forward_chain: string; input_chain: string; source: string; protocol?: string; ports: string[]; - host_wg_ip?: string; + wg_interface: string; + target_interface: string; + host_subnet?: string; }) { const lines: string[] = []; const protocols = protocolsForRule({ @@ -114,6 +118,19 @@ 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`, @@ -124,7 +141,7 @@ function acceptLines({ joinArgs([ `iptables -A ${input_chain}`, `-s ${source}`, - host_wg_ip ? `-d ${host_wg_ip}` : undefined, + host_subnet ? `-d ${host_subnet}` : undefined, proto ? `-p ${proto}` : undefined, port_match_str, `-j ACCEPT`, @@ -176,9 +193,7 @@ export default function buildHostIptablesScripts({ const { forward, input } = deriveIptablesChainNames({ host_id }); const masquerade_match = scoped_subnet ? `-s ${scoped_subnet} ` : ``; - const accept_lines: string[] = [ - `iptables -A ${forward} -o ${interface_name} -j DROP`, - ]; + const accept_lines: string[] = []; const host_clients = clients || []; for (let i = 0; i < host_clients.length; i++) { @@ -225,10 +240,25 @@ export default function buildHostIptablesScripts({ if (rule.rule_type == "all") { accept_lines.push( - `iptables -A ${forward} -s ${source} -j ACCEPT`, - scoped_subnet - ? `iptables -A ${input} -s ${source} -d ${scoped_subnet} -j ACCEPT` - : `iptables -A ${input} -s ${source} -j ACCEPT`, + 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([ + `iptables -A ${input}`, + `-s ${source}`, + scoped_subnet ? `-d ${scoped_subnet}` : undefined, + `-j ACCEPT`, + ]), ); continue; @@ -250,7 +280,9 @@ export default function buildHostIptablesScripts({ source, protocol: rule.protocol || "any", ports: parsed_ports.ports, - host_wg_ip: scoped_subnet, + wg_interface: interface_name, + target_interface, + host_subnet: scoped_subnet, }), ); }