192 lines
5.5 KiB
TypeScript
192 lines
5.5 KiB
TypeScript
import { execSync } from "node:child_process";
|
|
import validateIpv4Cidr from "@/src/utils/validate-ipv4-cidr";
|
|
import detectFirewallManager, {
|
|
type FirewallManager,
|
|
} from "./detect-firewall-manager";
|
|
|
|
export type AllowSubnetFirewallRulesResult = {
|
|
success: boolean;
|
|
manager: FirewallManager | "none";
|
|
msg: string;
|
|
added_rules: string[];
|
|
skipped_rules: string[];
|
|
};
|
|
|
|
type Params = {
|
|
subnet: string;
|
|
listen_port: number;
|
|
};
|
|
|
|
type ShellResult = {
|
|
success: boolean;
|
|
output: string;
|
|
};
|
|
|
|
const tryShell = ({ command }: { command: string }): ShellResult => {
|
|
try {
|
|
const result = execSync(command, {
|
|
encoding: "utf-8",
|
|
stdio: "pipe",
|
|
timeout: 30000,
|
|
});
|
|
|
|
return {
|
|
success: true,
|
|
output: String(result).trim(),
|
|
};
|
|
} catch (error: any) {
|
|
const stdout = error?.stdout ? String(error.stdout).trim() : "";
|
|
const stderr = error?.stderr ? String(error.stderr).trim() : "";
|
|
|
|
return {
|
|
success: false,
|
|
output: [stdout, stderr].filter(Boolean).join("\n").trim(),
|
|
};
|
|
}
|
|
};
|
|
|
|
const isValidPort = ({ port }: { port: number }) =>
|
|
Number.isInteger(port) && port >= 1 && port <= 65535;
|
|
|
|
export default async function allowSubnetFirewallRules({
|
|
subnet,
|
|
listen_port,
|
|
}: Params): Promise<AllowSubnetFirewallRulesResult> {
|
|
const subnet_trimmed = subnet?.trim();
|
|
|
|
if (!validateIpv4Cidr({ value: subnet_trimmed })) {
|
|
return {
|
|
success: false,
|
|
manager: "none",
|
|
msg: `Invalid subnet "${subnet_trimmed}"`,
|
|
added_rules: [],
|
|
skipped_rules: [],
|
|
};
|
|
}
|
|
|
|
if (!isValidPort({ port: listen_port })) {
|
|
return {
|
|
success: false,
|
|
manager: "none",
|
|
msg: `Invalid listen port ${listen_port}`,
|
|
added_rules: [],
|
|
skipped_rules: [],
|
|
};
|
|
}
|
|
|
|
const manager = detectFirewallManager();
|
|
|
|
const added_rules: string[] = [];
|
|
const skipped_rules: string[] = [];
|
|
|
|
if (manager == "firewalld") {
|
|
const subnet_rule = `rule family="ipv4" source address="${subnet_trimmed}" accept`;
|
|
const subnet_add = tryShell({
|
|
command: `firewall-cmd --permanent --add-rich-rule='${subnet_rule}'`,
|
|
});
|
|
|
|
if (subnet_add.success) {
|
|
added_rules.push(`allow all traffic from ${subnet_trimmed}`);
|
|
} else {
|
|
skipped_rules.push(
|
|
`firewalld subnet rule not applied: ${subnet_add.output}`,
|
|
);
|
|
}
|
|
|
|
const port_rule = `${listen_port}/udp`;
|
|
const port_add = tryShell({
|
|
command: `firewall-cmd --permanent --add-port=${port_rule}`,
|
|
});
|
|
|
|
if (port_add.success) {
|
|
added_rules.push(`allow ${port_rule} from anywhere (wireguard)`);
|
|
} else {
|
|
skipped_rules.push(
|
|
`firewalld port rule not applied: ${port_add.output}`,
|
|
);
|
|
}
|
|
|
|
const reload = tryShell({ command: `firewall-cmd --reload` });
|
|
|
|
if (!reload.success) {
|
|
skipped_rules.push(`firewalld reload failed: ${reload.output}`);
|
|
}
|
|
} else if (manager == "ufw") {
|
|
const ufw_status = tryShell({ command: `ufw status` });
|
|
|
|
if (
|
|
!ufw_status.success ||
|
|
!ufw_status.output.includes(`Status: active`)
|
|
) {
|
|
return {
|
|
success: false,
|
|
manager,
|
|
msg: `ufw is not active — cannot add firewall rules`,
|
|
added_rules,
|
|
skipped_rules,
|
|
};
|
|
}
|
|
|
|
if (ufw_status.output.includes(subnet_trimmed)) {
|
|
skipped_rules.push(`subnet ${subnet_trimmed} already allowed`);
|
|
} else {
|
|
const subnet_add = tryShell({
|
|
command: `ufw allow from ${subnet_trimmed}`,
|
|
});
|
|
|
|
if (subnet_add.success) {
|
|
added_rules.push(`allow all traffic from ${subnet_trimmed}`);
|
|
} else {
|
|
skipped_rules.push(
|
|
`ufw subnet rule not applied: ${subnet_add.output}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
const port_rule = `${listen_port}/udp`;
|
|
|
|
if (ufw_status.output.includes(port_rule)) {
|
|
skipped_rules.push(`port ${port_rule} already allowed`);
|
|
} else {
|
|
const port_add = tryShell({ command: `ufw allow ${port_rule}` });
|
|
|
|
if (port_add.success) {
|
|
added_rules.push(
|
|
`allow ${port_rule} from anywhere (wireguard)`,
|
|
);
|
|
} else {
|
|
skipped_rules.push(
|
|
`ufw port rule not applied: ${port_add.output}`,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (manager == "none") {
|
|
return {
|
|
success: true,
|
|
manager,
|
|
msg: `no active firewall manager detected (firewalld/ufw) — no rules added; if you use iptables/nftables directly, allow ${subnet_trimmed} and udp port ${listen_port} manually`,
|
|
added_rules,
|
|
skipped_rules,
|
|
};
|
|
}
|
|
|
|
const summary = [
|
|
added_rules.length
|
|
? `firewall(${manager}): added — ${added_rules.join("; ")}`
|
|
: `firewall(${manager}): no new rules added`,
|
|
skipped_rules.length ? `skipped — ${skipped_rules.join("; ")}` : ``,
|
|
]
|
|
.filter(Boolean)
|
|
.join(` `);
|
|
|
|
return {
|
|
success: true,
|
|
manager,
|
|
msg: summary,
|
|
added_rules,
|
|
skipped_rules,
|
|
};
|
|
}
|