UNPKG

@synet/net

Version:

Network abstraction layer for Synet. visit https://syntehtism.ai for more information.

554 lines (553 loc) 25.5 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.WireguardToolsAdapter = void 0; const path = __importStar(require("node:path")); const os = __importStar(require("node:os")); const patterns_1 = require("@synet/patterns"); const bytes_1 = require("../utils/bytes"); /** * Adapter for managing WireGuard using local wireguard-tools */ class WireguardToolsAdapter { constructor(keysRepository, configRepository, commandExecutor, interfaceName = "synet0", configDir = "/etc/.synet", logger, sudo = true) { this.keysRepository = keysRepository; this.configRepository = configRepository; this.commandExecutor = commandExecutor; this.interfaceName = interfaceName; this.configDir = configDir; this.logger = logger; this.sudo = sudo; this.configPath = path.join(this.configDir, `${this.interfaceName}.conf`); } async getKeys() { this.logger?.debug("Getting WireGuard keys"); return await this.keysRepository.getKeys(); } async generateKeys() { this.logger?.debug("Generating WireGuard keys"); try { // Generate private key const privateKeyResult = await this.commandExecutor.execute("wg", [ "genkey", ]); if (privateKeyResult.isFailure) { return patterns_1.Result.fail("Failed to generate private key", privateKeyResult.errorCause); } const privateKey = privateKeyResult.value.stdout.trim(); // Generate public key from private key const publicKeyCmd = `echo "${privateKey}" | wg pubkey`; const publicKeyResult = await this.commandExecutor.execute(publicKeyCmd); if (publicKeyResult.isFailure) { return patterns_1.Result.fail("Failed to generate public key", publicKeyResult.errorCause); } const publicKey = publicKeyResult.value.stdout.trim(); const keys = { privateKey, publicKey }; // Save keys const saveResult = await this.keysRepository.saveKeys(keys); if (saveResult.isFailure) { return patterns_1.Result.fail(`Failed to save keys: ${saveResult.errorMessage}`, saveResult.errorCause); } return patterns_1.Result.success(keys); } catch (error) { this.logger?.error("Failed to generate WireGuard keys", error); if (error instanceof Error) { return patterns_1.Result.fail(`Failed to generate keys: ${error.message}`, error); } return patterns_1.Result.fail("Failed to generate keys: Unknown error"); } } stripAddressSection(configContent) { // Remove Address=... lines from [Interface] section return configContent.replace(/(\[Interface\][^\[]*)Address\s*=\s*.*\n?/g, "$1"); } async setInterface(config) { this.logger?.debug(`Setting up WireGuard interface ${this.interfaceName}`); try { //this.logger?.info('Config : ', config); const existingConfig = await this.configRepository.getInterfaceConfig(this.interfaceName); if (existingConfig.isSuccess && existingConfig.value) { this.logger?.info(`Using existing configuration for interface ${this.interfaceName}`); return patterns_1.Result.success(undefined); } const output = { privateKey: config.privateKey.substring(0, 10), address: config.address, listenPort: config.listenPort, }; this.logger?.info("Creating new configuration", output); // Save the configuration const saveResult = await this.configRepository.saveInterfaceConfig(this.interfaceName, config); if (saveResult.isFailure) { return saveResult; } // If the interface already exists, try to update it in-place try { const checkResult = await this.checkInterface(this.interfaceName); if (checkResult.isSuccess) { const reloadResult = await this.reload(); //await this.fileSystem.deleteFile(tempPath); if (reloadResult.isFailure) { this.logger?.warn(`Failed to update interface configuration in-place: ${reloadResult.errorMessage}`); // Don't return failure here, configuration is saved and will be used on next bringUp } else { this.logger?.info(`Updated existing interface ${this.interfaceName} configuration`); } } } catch (err) { // Interface doesn't exist yet, which is fine this.logger?.debug(`Interface ${this.interfaceName} doesn't exist yet, will be created on bringUp`); } return patterns_1.Result.success(undefined); } catch (error) { this.logger?.error(`Failed to set interface ${this.interfaceName}`, error); if (error instanceof Error) { return patterns_1.Result.fail(`Failed to set interface: ${error.message}`, error); } return patterns_1.Result.fail("Failed to set interface: Unknown error"); } } async addPeer(config) { this.logger?.debug(`Adding peer ${config.publicKey.substring(0, 10)}... to ${this.interfaceName}`); try { // Save peer to configuration const saveResult = await this.configRepository.savePeer(this.interfaceName, config); if (saveResult.isFailure) { return saveResult; } // If interface is already up, update it with the new peer try { // Check if interface exists const checkResult = await this.checkInterface(this.interfaceName); if (checkResult.isSuccess) { // Interface exists, update it const syncResult = await this.reload(); if (syncResult.isFailure) { this.logger?.warn("Failed to sync configuration, will apply on next bring-up"); // Don't return failure, as the config is saved and will be applied on next bring-up } else { this.logger?.info(`Peer ${config.publicKey.substring(0, 10)}... added to running interface`); } } } catch { // Interface not up yet, will be configured on bringUp } return patterns_1.Result.success(undefined); } catch (error) { this.logger?.error(`Failed to add peer to ${this.interfaceName}`, error); if (error instanceof Error) { return patterns_1.Result.fail(`Failed to add peer: ${error.message}`, error); } return patterns_1.Result.fail("Failed to add peer: Unknown error"); } } async removeInterface() { try { const saveResult = await this.configRepository.removeInterfaceConfig(this.interfaceName); if (saveResult.isFailure) { return patterns_1.Result.fail(`Failed to remove interface configuration: ${saveResult.errorMessage}`, saveResult.errorCause); } this.logger?.info(`Removed [Interface] section from ${this.configPath}`); return patterns_1.Result.success(undefined); } catch (error) { this.logger?.error(`Failed to remove [Interface] section from ${this.configPath}`, error); if (error instanceof Error) { return patterns_1.Result.fail("Failed to remove [Interface] section", error); } return patterns_1.Result.fail("Failed to remove [Interface] section: Unknown error"); } } async removeAllPeers() { this.logger?.debug(`Removing all peers from ${this.interfaceName}`); try { // Use the repository to remove all peers const result = await this.configRepository.removeAllPeers(this.interfaceName); if (result.isFailure) { return result; } // If interface is up, also remove peers directly try { const checkResult = await this.checkInterface(this.interfaceName); if (checkResult.isSuccess) { // Reload the configuration to apply changes const reloadResult = await this.reload(); if (reloadResult.isFailure) { this.logger?.warn("Failed to sync configuration after removing all peers, changes will apply on next bring-up"); } else { this.logger?.info(`All peers removed from running interface ${this.interfaceName}`); } } } catch { // Interface might not be up, which is fine } return patterns_1.Result.success(undefined); } catch (error) { this.logger?.error(`Failed to remove all peers from ${this.interfaceName}`, error); if (error instanceof Error) { return patterns_1.Result.fail(`Failed to remove all peers: ${error.message}`, error); } return patterns_1.Result.fail("Failed to remove all peers: Unknown error"); } } async removePeer(publicKey) { this.logger?.debug(`Removing peer ${publicKey.substring(0, 10)}... from ${this.interfaceName}`); try { // Remove from configuration file const removeResult = await this.configRepository.removePeer(this.interfaceName, publicKey); if (removeResult.isFailure) { return removeResult; } // If interface is up, remove peer directly try { const checkResult = await this.checkInterface(this.interfaceName); if (checkResult.isSuccess) { const result = await this.commandExecutor.executeAsSuperuser("wg", ["set", this.interfaceName, "peer", publicKey, "remove"], this.sudo); if (result.isFailure) { this.logger?.warn("Failed to remove peer directly, but configuration was updated"); } else { this.logger?.info(`Peer ${publicKey.substring(0, 10)}... removed from running interface`); } } } catch { // Interface might not be up, ignore error } return patterns_1.Result.success(undefined); } catch (error) { this.logger?.error(`Failed to remove peer from ${this.interfaceName}`, error); if (error instanceof Error) { return patterns_1.Result.fail(`Failed to remove peer: ${error.message}`, error); } return patterns_1.Result.fail("Failed to remove peer: Unknown error"); } } async bringUp() { this.logger?.info(`Bringing up WireGuard interface ${this.interfaceName}`); try { // Ensure the config file exists const configResult = await this.configRepository.getInterfaceConfig(this.interfaceName); //console.log("Config Result", configResult); if (!configResult.isSuccess || !configResult.value) { return patterns_1.Result.fail(`Configuration file is missing interface section ${this.configPath}`, configResult.errorCause); } // Use wg-quick to bring up the interface const result = await this.commandExecutor.executeAsSuperuser("wg-quick", ["up", this.configPath], this.sudo); if (result.isFailure) { return patterns_1.Result.fail(`Failed to bring up interface: ${result.errorMessage}`, result.errorCause); } this.logger?.info(`Interface ${this.interfaceName} is now up`); return patterns_1.Result.success(undefined); } catch (error) { this.logger?.error(`Failed to bring up interface ${this.interfaceName}`, error); if (error instanceof Error) { return patterns_1.Result.fail(`Failed to bring up interface: ${error.message}`, error); } return patterns_1.Result.fail("Failed to bring up interface: Unknown error"); } } async bringDown() { this.logger?.info(`Bringing down WireGuard interface ${this.interfaceName}`); try { // Check if the interface exists before trying to bring it down try { const checkResult = await this.checkInterface(this.interfaceName); if (checkResult.isFailure) { // Interface doesn't exist, nothing to do this.logger?.info(`Interface ${this.interfaceName} is not up, nothing to do`); return patterns_1.Result.success(undefined); } } catch { // Error checking interface, assume it doesn't exist return patterns_1.Result.success(undefined); } // Use wg-quick to bring down the interface const result = await this.commandExecutor.executeAsSuperuser("wg-quick", ["down", this.configPath], this.sudo); if (result.isFailure) { return patterns_1.Result.fail(`Failed to bring down interface: ${result.errorMessage}`, result.errorCause); } this.logger?.info("Interface is now down", { interface: this.interfaceName, }); return patterns_1.Result.success(undefined); } catch (error) { this.logger?.error(`Failed to bring down interface ${this.interfaceName}`, error); if (error instanceof Error) { return patterns_1.Result.fail(`Failed to bring down interface: ${error.message}`, error); } return patterns_1.Result.fail("Failed to bring down interface: Unknown error"); } } async reload() { this.logger?.info(`Reloading WireGuard interface ${this.interfaceName}`); try { // Use wg-quick to reload the interface const command = `bash -c "wg syncconf ${this.interfaceName} <(wg-quick strip ${this.configPath})"`; const result = await this.commandExecutor.execute(command); /*const result = await this.commandExecutor.executeAsSuperuser("wg", [ "syncconf", this.interfaceName, this.configPath, ],); */ if (result.isFailure) { return patterns_1.Result.fail(`Failed to reload interface: ${result.errorMessage}`, result.errorCause); } this.logger?.info("Interface is reloaded:", this.interfaceName); return patterns_1.Result.success(undefined); } catch (error) { this.logger?.error(`Failed to reload interface ${this.interfaceName}`, error); if (error instanceof Error) { return patterns_1.Result.fail(`Failed to reload interface: ${error.message}`, error); } return patterns_1.Result.fail("Failed to reload interface: Unknown error"); } } async checkInterface(interfaceName) { let checkResult; if (os.platform() === "darwin") { // macOS: use ifconfig checkResult = await this.commandExecutor.execute("whoami"); } else checkResult = await this.commandExecutor.execute("ip", [ "link", "show", this.interfaceName, ]); if (checkResult.isSuccess) { return patterns_1.Result.success(undefined); } return patterns_1.Result.fail(`Interface ${this.interfaceName} is not up`); } async status() { this.logger?.info(`Checking status of WireGuard interface ${this.interfaceName}`); try { const result = await this.commandExecutor.execute("wg", [ "show", this.interfaceName, ]); if (result.isFailure) { return patterns_1.Result.fail(`Failed to check status: ${result.errorMessage}`, result.errorCause); } this.logger?.info(`Interface ${this.interfaceName} status: ${result.value.stdout}`); return patterns_1.Result.success(undefined); } catch (error) { this.logger?.error(`Failed to check status of interface ${this.interfaceName}`, error); if (error instanceof Error) { return patterns_1.Result.fail(`Failed to check status: ${error.message}`, error); } return patterns_1.Result.fail("Failed to check status: Unknown error"); } } async peerStatus(publicKey) { try { // Get the dump output let result; if (os.platform() === "darwin") { const interfaceName = await this.getInterfaceName(); if (!interfaceName.isSuccess || !interfaceName.value) { return patterns_1.Result.success(null); } result = await this.commandExecutor.execute("wg", [ "show", interfaceName.value, "dump", ], true); } else { result = await this.commandExecutor.execute("wg", [ "show", this.interfaceName, "dump", ]); } if (result.isFailure) { return patterns_1.Result.fail(`Failed to get peer status: ${result.errorMessage}`, result.errorCause); } const stdout = result.value.stdout; const lines = stdout.trim().split("\n"); if (lines.length < 2) { this.logger?.warn(`No peers found for interface ${this.interfaceName}`); return patterns_1.Result.success(null); } // Search for the peer by publicKey for (let i = 1; i < lines.length; i++) { const cols = lines[i].split("\t"); const peerPublicKey = cols[0]; if (peerPublicKey === publicKey) { const peerStatus = this.extractPeerConfig(cols); if (peerStatus) { return patterns_1.Result.success(peerStatus); } this.logger?.warn(`Failed to parse peer config for ${publicKey}`); return patterns_1.Result.success(null); } } // Peer not found this.logger?.warn(`Peer with publicKey ${publicKey.substring(0, 10)}... not found`); return patterns_1.Result.success(null); } catch (error) { this.logger?.error(`Failed to get peer status for ${publicKey.substring(0, 10)}...`, error); if (error instanceof Error) { return patterns_1.Result.fail("Failed to get peer status", error); } return patterns_1.Result.fail("Failed to get peer status: Unknown error"); } } async listPeers() { this.logger?.info(`Listing peers for interface ${this.interfaceName}`); try { const result = await this.commandExecutor.execute("wg", [ "show", this.interfaceName, "dump", ]); if (result.isFailure) { return patterns_1.Result.fail(`Failed to get peer status: ${result.errorMessage}`, result.errorCause); } const stdout = result.value.stdout; const lines = stdout.trim().split("\n"); if (lines.length < 2) { this.logger?.warn(`No peers found for interface ${this.interfaceName}`); return patterns_1.Result.success(null); } const results = []; for (let i = 1; i < lines.length; i++) { const cols = lines[i].split("\t"); const peerStatus = this.extractPeerConfig(cols); if (peerStatus) { results.push(peerStatus); } } return patterns_1.Result.success(results.length > 0 ? results : null); } catch (error) { this.logger?.error(`Failed to list peers for interface ${this.interfaceName}`, error); if (error instanceof Error) { return patterns_1.Result.fail("Failed to get peer status", error); } return patterns_1.Result.fail("Failed to get peer status: Unknown error"); } } extractPeerConfig(cols) { if (cols.length < 7) { this.logger?.warn(`Invalid peer data format: ${cols}`); return null; } // Extract relevant fields const peerPublicKey = cols[0]; const latestHandshake = Number(cols[4]); const transferRx = Number(cols[5]); const transferTx = Number(cols[6]); const persistentKeepalive = Number(cols[7]); this.logger?.debug("Latest Handshake", latestHandshake); // Handshake time let handshakeStatus = ""; if (latestHandshake === 0) { handshakeStatus = "No handshake"; } else if (latestHandshake) { const secondsAgo = Math.floor(Date.now() / 1000) - latestHandshake; if (secondsAgo < 60) { handshakeStatus = `${secondsAgo} seconds ago`; } else if (secondsAgo < 3600) { handshakeStatus = `${Math.floor(secondsAgo / 60)} minutes ago`; } else { handshakeStatus = "No recent handshake"; } } // Data transfer const rxStr = (0, bytes_1.formatBytes)(transferRx); const txStr = (0, bytes_1.formatBytes)(transferTx); this.logger?.info(`Peer ${peerPublicKey.substring(0, 10)}... | Handshake: ${handshakeStatus} | Data: ${rxStr} received, ${txStr} sent`); return { publicKey: peerPublicKey, latestHandshake: latestHandshake === 0 ? "" : new Date(latestHandshake * 1000).toISOString(), handshakeStatus: handshakeStatus, persistentKeepalive: persistentKeepalive, transfer: { rx: rxStr, tx: txStr, }, }; } async getInterfaceName() { try { const result = await this.commandExecutor.execute("wg", [ "show", "all", ], true); const stdout = result.value.stdout; const lines = stdout.trim().split("\n"); if (lines.length < 1) { this.logger?.warn("No WireGuard interfaces found"); return patterns_1.Result.fail("No WireGuard interfaces found"); } const interfaceLine = lines[0].split(':')[1].trim(); // Return the configured interface name return patterns_1.Result.success(interfaceLine); } catch (error) { this.logger?.error("Failed to get interface name", error); if (error instanceof Error) { return patterns_1.Result.fail(`Failed to get interface name: ${error.message}`, error); } return patterns_1.Result.fail("Failed to get interface name: Unknown error"); } } } exports.WireguardToolsAdapter = WireguardToolsAdapter;