From dc8700feaacd8b4fcd766758a62b2f0193019b9c Mon Sep 17 00:00:00 2001 From: Benjamin Toby Date: Tue, 22 Sep 2026 13:59:56 +0100 Subject: [PATCH] Iptables hardening and dev server bind fix --- .../setup/build-host-iptables-scripts.test.ts | 112 +++++++++++++++++- .../setup/build-host-iptables-scripts.ts | 22 ++-- src/server.ts | 2 +- 3 files changed, 122 insertions(+), 14 deletions(-) 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 08a77f2..efd5669 100644 --- a/src/functions/backend/setup/build-host-iptables-scripts.test.ts +++ b/src/functions/backend/setup/build-host-iptables-scripts.test.ts @@ -20,7 +20,9 @@ describe("buildHostIptablesScripts", () => { expect(res.post_up).toContain("iptables -A WGUI0IN -j DROP"); expect(res.post_up).not.toContain("WGUI0FWD -s"); expect(res.post_down).toContain("iptables -X WGUI0FWD"); - expect(res.post_down).toContain("iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE"); + expect(res.post_down).toContain( + "iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE 2>/dev/null || true", + ); }); test("all access accepts the client on forward and input", () => { @@ -67,7 +69,7 @@ describe("buildHostIptablesScripts", () => { expect(res.post_up).toContain( "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( + expect(res.post_up).not.toContain( "iptables -A WGUI0IN -s 10.0.0.3/32 -d 192.168.1.10/32 -p tcp -m multiport --dports 80,443 -j ACCEPT", ); }); @@ -131,4 +133,110 @@ describe("buildHostIptablesScripts", () => { "iptables -D FORWARD -i wgui0 -j ACCEPT 2>/dev/null || true", ); }); + + test("masquerade is scoped to the vpn subnet when host_subnet is set", () => { + const res = buildHostIptablesScripts({ + ...base, + host_subnet: "10.2.0.0/24", + clients: [ + { + id: 5, + wg_ip_address: "10.2.0.10", + rules: [{ rule_type: "all" }], + }, + ], + }); + + expect(res.success).toBe(true); + expect(res.post_up).toContain( + "iptables -t nat -C POSTROUTING -s 10.2.0.0/24 -o eth0 -j MASQUERADE 2>/dev/null || iptables -t nat -A POSTROUTING -s 10.2.0.0/24 -o eth0 -j MASQUERADE", + ); + expect(res.post_up).not.toContain( + "iptables -t nat -A POSTROUTING -i eth0 -j MASQUERADE", + ); + expect(res.post_down).toContain( + "iptables -t nat -D POSTROUTING -s 10.2.0.0/24 -o eth0 -j MASQUERADE 2>/dev/null || true", + ); + }); + + test("masquerade falls back to the target interface without a subnet", () => { + const res = buildHostIptablesScripts({ + ...base, + clients: [ + { + id: 6, + wg_ip_address: "10.0.0.9", + rules: [{ rule_type: "all" }], + }, + ], + }); + + expect(res.success).toBe(true); + expect(res.post_up).toContain( + "iptables -t nat -C POSTROUTING -o eth0 -j MASQUERADE 2>/dev/null || iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE", + ); + }); + + test("all access with host_subnet is scoped to the subnet", () => { + const res = buildHostIptablesScripts({ + ...base, + host_subnet: "10.6.0.0/24", + clients: [ + { + id: 6, + wg_ip_address: "10.0.0.7", + rules: [{ rule_type: "all" }], + }, + ], + }); + + expect(res.success).toBe(true); + expect(res.post_up).toContain( + "iptables -A WGUI0FWD -s 10.0.0.7/32 -d 10.6.0.0/24 -j ACCEPT", + ); + expect(res.post_up).toContain( + "iptables -A WGUI0IN -s 10.0.0.7/32 -j ACCEPT", + ); + expect(res.post_up).not.toContain( + "iptables -A WGUI0FWD -s 10.0.0.7/32 -o eth0 -j ACCEPT", + ); + }); + + test("port-only rules are not mirrored onto the input chain", () => { + const res = buildHostIptablesScripts({ + ...base, + clients: [ + { + id: 7, + wg_ip_address: "10.0.0.8", + rules: [ + { + rule_type: "destination", + ports: "53", + protocol: "any", + }, + ], + }, + ], + }); + + 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", + ); + expect(res.post_up).not.toContain( + "iptables -A WGUI0IN -s 10.0.0.8/32 -p tcp --dport 53 -j ACCEPT", + ); + }); + + test("rejects oversized host id", () => { + const res = buildHostIptablesScripts({ + ...base, + host_id: 1e15, + clients: [], + }); + + expect(res.success).toBe(false); + expect(res.msg).toBe("Invalid 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 2e1592c..2f28e7e 100644 --- a/src/functions/backend/setup/build-host-iptables-scripts.ts +++ b/src/functions/backend/setup/build-host-iptables-scripts.ts @@ -128,7 +128,11 @@ export default function buildHostIptablesScripts({ host_subnet, clients, }: Params) { - if (!Number.isInteger(host_id) || host_id < 0) { + if ( + !Number.isSafeInteger(host_id) || + host_id < 0 || + host_id > 999999999 + ) { return { success: false, msg: `Invalid host id`, @@ -159,6 +163,10 @@ export default function buildHostIptablesScripts({ } const { forward, input } = deriveIptablesChainNames({ host_id }); + + const masquerade_match = scoped_subnet + ? `-s ${scoped_subnet} -o ${target_interface}` + : `-o ${target_interface}`; const accept_lines: string[] = []; const host_clients = clients || []; @@ -208,7 +216,6 @@ export default function buildHostIptablesScripts({ if (scoped_subnet) { accept_lines.push( `iptables -A ${forward} -s ${source} -d ${scoped_subnet} -j ACCEPT`, - `iptables -A ${forward} -s ${source} -o ${target_interface} -j ACCEPT`, `iptables -A ${input} -s ${source} -j ACCEPT`, ); } else { @@ -241,13 +248,6 @@ export default function buildHostIptablesScripts({ protocol: rule.protocol || "any", ports: parsed_ports.ports, }), - ...acceptLines({ - chain: input, - source, - destination, - protocol: rule.protocol || "any", - ports: parsed_ports.ports, - }), ); } } @@ -280,7 +280,7 @@ export default function buildHostIptablesScripts({ `iptables -C FORWARD -o ${interface_name} -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT 2>/dev/null || iptables -I FORWARD 1 -o ${interface_name} -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT`, `iptables -C INPUT -i ${interface_name} -j ${input} 2>/dev/null || iptables -I INPUT 1 -i ${interface_name} -j ${input}`, ``, - `iptables -t nat -C POSTROUTING -o ${target_interface} -j MASQUERADE 2>/dev/null || iptables -t nat -A POSTROUTING -o ${target_interface} -j MASQUERADE`, + `iptables -t nat -C POSTROUTING ${masquerade_match} -j MASQUERADE 2>/dev/null || iptables -t nat -A POSTROUTING ${masquerade_match} -j MASQUERADE`, ``, ].join("\n"); @@ -298,7 +298,7 @@ export default function buildHostIptablesScripts({ `iptables -X ${forward} 2>/dev/null || true`, `iptables -F ${input} 2>/dev/null || true`, `iptables -X ${input} 2>/dev/null || true`, - `iptables -t nat -D POSTROUTING -o ${target_interface} -j MASQUERADE 2>/dev/null || true`, + `iptables -t nat -D POSTROUTING ${masquerade_match} -j MASQUERADE 2>/dev/null || true`, ``, ].join("\n"); diff --git a/src/server.ts b/src/server.ts index c9ea5cf..1ff52f3 100644 --- a/src/server.ts +++ b/src/server.ts @@ -13,7 +13,7 @@ const port = SiteData["ServerPort"]; await bunext.bunextInit(); const server = Bun.serve({ - hostname: development ? "localhost" : "0.0.0.0", + hostname: "0.0.0.0", async fetch(req, server) { try { const url = new URL(req.url);