183 lines
5.1 KiB
JavaScript
183 lines
5.1 KiB
JavaScript
import { spawn } from "child_process";
|
|
|
|
/**
|
|
* Execute a shell command and return stdout
|
|
*/
|
|
export function exec(cmd) {
|
|
return new Promise((resolve, reject) => {
|
|
const child = spawn("bash", ["-c", cmd], {
|
|
stdio: ["pipe", "pipe", "pipe"],
|
|
});
|
|
let stdout = "";
|
|
let stderr = "";
|
|
child.stdout.on("data", (data) => {
|
|
stdout += data.toString();
|
|
});
|
|
child.stderr.on("data", (data) => {
|
|
stderr += data.toString();
|
|
});
|
|
child.on("close", (code) => {
|
|
if (code !== 0) {
|
|
reject(new Error(`Command failed (exit ${code}): ${cmd}\n${stderr}`));
|
|
} else {
|
|
resolve(stdout.trim());
|
|
}
|
|
});
|
|
child.on("error", reject);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Generate a WireGuard private key
|
|
*/
|
|
export async function generatePrivateKey() {
|
|
return exec("wg genkey");
|
|
}
|
|
|
|
/**
|
|
* Derive a public key from a private key
|
|
*/
|
|
export async function getPublicKey(privateKey) {
|
|
return exec(`echo ${privateKey} | wg pubkey`);
|
|
}
|
|
|
|
/**
|
|
* Generate a pre-shared key
|
|
*/
|
|
export async function generatePreSharedKey() {
|
|
return exec("wg genpsk");
|
|
}
|
|
|
|
/**
|
|
* Bring up the WireGuard interface
|
|
*/
|
|
export async function wgUp(interfaceName) {
|
|
return exec(`wg-quick up ${interfaceName}`);
|
|
}
|
|
|
|
/**
|
|
* Bring down the WireGuard interface
|
|
*/
|
|
export async function wgDown(interfaceName) {
|
|
return exec(`wg-quick down ${interfaceName}`);
|
|
}
|
|
|
|
/**
|
|
* Sync WireGuard config without restarting
|
|
*/
|
|
export async function wgSync(interfaceName) {
|
|
return exec(`wg syncconf ${interfaceName} <(wg-quick strip ${interfaceName})`);
|
|
}
|
|
|
|
/**
|
|
* Get WireGuard status dump
|
|
* Returns array of peer objects
|
|
*/
|
|
export async function wgDump(interfaceName) {
|
|
const rawDump = await exec(`wg show ${interfaceName} dump`);
|
|
return rawDump
|
|
.trim()
|
|
.split("\n")
|
|
.slice(1) // skip interface line
|
|
.map((line) => {
|
|
const [publicKey, preSharedKey, endpoint, allowedIps, latestHandshakeAt, transferRx, transferTx, persistentKeepalive] = line.split("\t");
|
|
return {
|
|
publicKey,
|
|
preSharedKey,
|
|
endpoint: endpoint === "(none)" ? null : endpoint,
|
|
allowedIps,
|
|
latestHandshakeAt: latestHandshakeAt === "0" ? null : new Date(Number.parseInt(`${latestHandshakeAt}000`)),
|
|
transferRx: Number.parseInt(transferRx),
|
|
transferTx: Number.parseInt(transferTx),
|
|
persistentKeepalive,
|
|
};
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Generate the [Interface] section for the server config
|
|
*/
|
|
export function generateServerInterface({ privateKey, address, listenPort, mtu, dns, postUp, postDown }) {
|
|
const lines = ["[Interface]", `PrivateKey = ${privateKey}`, `Address = ${address}`, `ListenPort = ${listenPort}`];
|
|
if (mtu) lines.push(`MTU = ${mtu}`);
|
|
if (dns) lines.push(`DNS = ${dns}`);
|
|
if (postUp) lines.push(`PostUp = ${postUp}`);
|
|
if (postDown) lines.push(`PostDown = ${postDown}`);
|
|
return lines.join("\n");
|
|
}
|
|
|
|
/**
|
|
* Generate a [Peer] section for the server config
|
|
*/
|
|
export function generateServerPeer({ publicKey, preSharedKey, allowedIps }) {
|
|
const lines = ["[Peer]", `PublicKey = ${publicKey}`, `PresharedKey = ${preSharedKey}`, `AllowedIPs = ${allowedIps}`];
|
|
return lines.join("\n");
|
|
}
|
|
|
|
/**
|
|
* Generate complete client config file
|
|
*/
|
|
export function generateClientConfig({ clientPrivateKey, clientAddress, dns, mtu, serverPublicKey, preSharedKey, allowedIps, persistentKeepalive, endpoint }) {
|
|
const lines = [
|
|
"[Interface]",
|
|
`PrivateKey = ${clientPrivateKey}`,
|
|
`Address = ${clientAddress}`,
|
|
];
|
|
if (mtu) lines.push(`MTU = ${mtu}`);
|
|
if (dns) lines.push(`DNS = ${dns}`);
|
|
lines.push("", "[Peer]", `PublicKey = ${serverPublicKey}`, `PresharedKey = ${preSharedKey}`, `AllowedIPs = ${allowedIps}`, `PersistentKeepalive = ${persistentKeepalive}`, `Endpoint = ${endpoint}`);
|
|
return lines.join("\n");
|
|
}
|
|
|
|
/**
|
|
* Simple QR code generator (outputs SVG via qrencode)
|
|
*/
|
|
export async function generateQRCodeSVG(text) {
|
|
return exec(`echo -n '${text.replace(/'/g, "'\\''")}' | qrencode -t SVG -o -`);
|
|
}
|
|
|
|
/**
|
|
* Parse a CIDR string and return the network details
|
|
*/
|
|
export function parseCIDR(cidr) {
|
|
const [ip, prefix] = cidr.split("/");
|
|
const prefixLen = Number.parseInt(prefix, 10);
|
|
const parts = ip.split(".").map(Number);
|
|
const ipNum = ((parts[0] << 24) | (parts[1] << 16) | (parts[2] << 8) | parts[3]) >>> 0;
|
|
const mask = prefixLen === 0 ? 0 : (~0 << (32 - prefixLen)) >>> 0;
|
|
const networkNum = (ipNum & mask) >>> 0;
|
|
const broadcastNum = (networkNum | ~mask) >>> 0;
|
|
return {
|
|
network: numToIp(networkNum),
|
|
broadcast: numToIp(broadcastNum),
|
|
firstHost: numToIp(networkNum + 1),
|
|
lastHost: numToIp(broadcastNum - 1),
|
|
prefix: prefixLen,
|
|
networkNum,
|
|
broadcastNum,
|
|
};
|
|
}
|
|
|
|
function numToIp(num) {
|
|
return `${(num >>> 24) & 255}.${(num >>> 16) & 255}.${(num >>> 8) & 255}.${num & 255}`;
|
|
}
|
|
|
|
/**
|
|
* Find next available IP in a CIDR range given existing allocated IPs
|
|
* The first IP (network+1) is reserved for the server
|
|
*/
|
|
export function findNextAvailableIP(cidr, allocatedIPs) {
|
|
const parsed = parseCIDR(cidr);
|
|
// Start from network+2 (network+1 is server)
|
|
const startIP = parsed.networkNum + 2;
|
|
const endIP = parsed.broadcastNum - 1;
|
|
const allocatedSet = new Set(allocatedIPs);
|
|
|
|
for (let ip = startIP; ip <= endIP; ip++) {
|
|
const ipStr = numToIp(ip);
|
|
if (!allocatedSet.has(ipStr)) {
|
|
return ipStr;
|
|
}
|
|
}
|
|
throw new Error("No available IP addresses in the CIDR range");
|
|
}
|