@synet/net
Version:
Network abstraction layer for Synet. visit https://syntehtism.ai for more information.
554 lines (553 loc) • 25.5 kB
JavaScript
;
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;