Allow wildcard rule access host public IP ports

This commit is contained in:
2026-10-07 11:28:41 +01:00
parent c42f0440ac
commit b6f7eb0c75
4 changed files with 172 additions and 5 deletions
@@ -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,
@@ -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,
}),
);
@@ -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) || []