From e8e61133ffcc87ef0523d8f66ea6706645985835 Mon Sep 17 00:00:00 2001 From: Matheus Sampaio Queiroga Date: Mon, 28 Aug 2023 13:01:46 -0300 Subject: [PATCH] Merge userpsace and wg_binding in wginterface --- addons/tools/wginterface-dummy.cpp | 21 +-- addons/tools/wginterface-linux.cpp | 23 ++- addons/tools/wginterface.cpp | 28 +++- addons/tools/wginterface.hh | 27 +++- binding.gyp | 18 ++- libs/prebuildifyLoad.cjs | 4 +- src/index.ts | 3 +- src/mergeSets.ts | 55 ------- src/utils/config.ts | 2 +- src/wg_binding.ts | 35 ---- ...interfaces.test.ts => wginterface.test.ts} | 14 +- src/{userspace.ts => wginterface.ts} | 153 +++++++++++++----- 12 files changed, 215 insertions(+), 168 deletions(-) delete mode 100644 src/mergeSets.ts delete mode 100644 src/wg_binding.ts rename src/{interfaces.test.ts => wginterface.test.ts} (76%) rename src/{userspace.ts => wginterface.ts} (53%) diff --git a/addons/tools/wginterface-dummy.cpp b/addons/tools/wginterface-dummy.cpp index 7096434..c72ac46 100644 --- a/addons/tools/wginterface-dummy.cpp +++ b/addons/tools/wginterface-dummy.cpp @@ -7,28 +7,11 @@ #include #endif - -int maxName() { +unsigned long maxName() { return IFNAMSIZ; } -Napi::Value listDevicesSync(const Napi::CallbackInfo& info) { - const Napi::Env env = info.Env(); - Napi::Error::New(env, "Use userpace implementation, kernel only on linux!").ThrowAsJavaScriptException(); - return env.Undefined(); -} - -Napi::Value setupInterfaceSync(const Napi::CallbackInfo& info) { - const Napi::Env env = info.Env(); - Napi::Error::New(env, "Use userpace implementation, kernel only on linux!").ThrowAsJavaScriptException(); - return env.Undefined(); -} - -Napi::Value parseWgDeviceSync(const Napi::CallbackInfo& info) { - const Napi::Env env = info.Env(); - Napi::Error::New(env, "Use userpace implementation, kernel only on linux!").ThrowAsJavaScriptException(); - return env.Undefined(); -} +void listDevices::Execute() {} void setConfig::Execute() { SetError("Use userpace implementation, kernel only on linux!"); diff --git a/addons/tools/wginterface-linux.cpp b/addons/tools/wginterface-linux.cpp index 464463b..0dc14e0 100644 --- a/addons/tools/wginterface-linux.cpp +++ b/addons/tools/wginterface-linux.cpp @@ -24,12 +24,21 @@ #include #include "wginterface.hh" #include "linux/set_ip.cpp" - extern "C" { #include "linux/wireguard.h" } -int maxName() { +#ifndef SETCONFIG +#define SETCONFIG +#endif +#ifndef GETCONFIG +#define GETCONFIG +#endif +#ifndef LISTDEV +#define LISTDEV +#endif + +unsigned long maxName() { return IFNAMSIZ; } @@ -47,6 +56,16 @@ Napi::Value listDevicesSync(const Napi::CallbackInfo& info) { return devicesArray; } +void listDevices::Execute() { + char *device_name, *devicesList = wg_list_device_names(); + if (!devicesList) SetError("Unable to get device names"); + else { + size_t len; + for ((device_name) = (devicesList), (len) = 0; ((len) = strlen(device_name)); (device_name) += (len) + 1) deviceNames.push_back(device_name); + free(devicesList); + } +} + int setInterface(std::string wgName) { size_t len = 0; char *device_name, *devicesList = wg_list_device_names(); diff --git a/addons/tools/wginterface.cpp b/addons/tools/wginterface.cpp index 42b9c26..e5f5823 100644 --- a/addons/tools/wginterface.cpp +++ b/addons/tools/wginterface.cpp @@ -29,6 +29,7 @@ Napi::Value setConfigAsync(const Napi::CallbackInfo &info) { } return env.Undefined(); } + Napi::Value getConfigAsync(const Napi::CallbackInfo &info) { const Napi::Env env = info.Env(); const auto wgName = info[0]; @@ -53,6 +54,23 @@ Napi::Value getConfigAsync(const Napi::CallbackInfo &info) { return env.Undefined(); } +Napi::Value listDevicesAsync(const Napi::CallbackInfo &info) { + const Napi::Env env = info.Env(); + const auto callback = info[0]; + if (!(callback.IsFunction())) { + Napi::Error::New(env, "Require callback").ThrowAsJavaScriptException(); + return env.Undefined(); + } + + try { + const auto devicesFind = new listDevices(callback.As()); + devicesFind->Queue(); + } catch (const Napi::Error &err) { + err.ThrowAsJavaScriptException(); + } + return env.Undefined(); +} + Napi::Object Init(Napi::Env env, Napi::Object exports) { const Napi::Object constants = Napi::Object::New(env); constants.Set("MAX_NAME_LENGTH", maxName()); @@ -61,11 +79,15 @@ Napi::Object Init(Napi::Env env, Napi::Object exports) { exports.Set("constants", constants); // async function + #ifdef SETCONFIG exports.Set("setConfigAsync", Napi::Function::New(env, setConfigAsync)); + #endif + #ifdef GETCONFIG exports.Set("getConfigAsync", Napi::Function::New(env, getConfigAsync)); - - // Sync function lock loop - exports.Set("listDevicesSync", Napi::Function::New(env, listDevicesSync)); + #endif + #ifdef LISTDEV + exports.Set("listDevicesAsync", Napi::Function::New(env, listDevicesAsync)); + #endif return exports; } NODE_API_MODULE(addon, Init); \ No newline at end of file diff --git a/addons/tools/wginterface.hh b/addons/tools/wginterface.hh index 53f350c..96924ea 100644 --- a/addons/tools/wginterface.hh +++ b/addons/tools/wginterface.hh @@ -4,7 +4,7 @@ #include #include -int maxName(); +unsigned long maxName(); /* Esta função consegela o loop event @@ -13,6 +13,28 @@ Pegar todas as interfaces do Wireguard e retorna em forma de String sendo tratad */ Napi::Value listDevicesSync(const Napi::CallbackInfo& info); +class listDevices : public Napi::AsyncWorker { + private: + std::vector deviceNames; + public: + ~listDevices() {} + listDevices(const Napi::Function &callback) : AsyncWorker(callback) {} + void OnOK() override { + Napi::HandleScope scope(Env()); + const Napi::Env env = Env(); + const auto deviceArray = Napi::Array::New(env); + if (deviceNames.size() > 0) { + for (auto it = deviceNames.begin(); it != deviceNames.end(); ++it) deviceArray.Set(deviceArray.Length(), it->append("")); + } + Callback().Call({ Env().Undefined(), deviceArray }); + }; + void OnError(const Napi::Error& e) override { + Napi::HandleScope scope(Env()); + Callback().Call({ e.Value() }); + } + void Execute() override; +}; + class Peer { public: bool removeMe; @@ -163,9 +185,6 @@ class getConfig : public Napi::AsyncWorker { // Interface address'es std::vector Address; - // Replace peers - bool replacePeers; - /* Wireguard peers Map: diff --git a/binding.gyp b/binding.gyp index 528ca8c..dacb9c0 100644 --- a/binding.gyp +++ b/binding.gyp @@ -18,9 +18,7 @@ }], ["OS=='mac'", { "xcode_settings": { - "GCC_ENABLE_CPP_EXCEPTIONS": "YES", - "CLANG_CXX_LIBRARY": "libc++", - "MACOSX_DEPLOYMENT_TARGET": "10.7", + "GCC_ENABLE_CPP_EXCEPTIONS": "YES" }, }], ], @@ -61,11 +59,25 @@ ], "conditions": [ ["OS=='linux'", { + "defines": [ + "LISTDEV", + "GETCONFIG", + "SETCONFIG" + ], "sources": [ "addons/tools/linux/wireguard.c", "addons/tools/wginterface-linux.cpp" ] }], + ["OS=='mac'", { + "cflags!": [ "-fno-exceptions" ], + "cflags_cc!": [ "-fno-exceptions" ], + "cflags_cc": [ "-fexceptions" ], + "cflags": [ "-fexceptions" ], + "xcode_settings": { + "GCC_ENABLE_CPP_EXCEPTIONS": "YES" + }, + }], ["OS!='linux'", { "sources": [ "addons/tools/wginterface-dummy.cpp" diff --git a/libs/prebuildifyLoad.cjs b/libs/prebuildifyLoad.cjs index af09286..022280f 100644 --- a/libs/prebuildifyLoad.cjs +++ b/libs/prebuildifyLoad.cjs @@ -10,7 +10,7 @@ module.exports = main; */ function main(name, pathLocation) { if (!pathLocation) pathLocation = process.cwd(); - else pathLocation = path.resolve(pathLocation); + else pathLocation = path.resolve(process.cwd(), pathLocation); const folders = [ path.join(pathLocation, "build", "Release"), path.join(pathLocation, "build", "Debug"), @@ -23,7 +23,7 @@ function main(name, pathLocation) { if (typeof name === "number") return require(path.join(folder, files.at(name))); else if (!name) name = files.at(0); if (typeof name === "string") { - const bname = name; + const bname = name.concat(""); if ((name = files.find(s => s.startsWith(name)))) return require(path.join(folder, name)); name = bname; } diff --git a/src/index.ts b/src/index.ts index 5e50d24..7c90f5f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,2 @@ -export * as userspace from "./userspace"; export * as utils from "./utils/index"; -export * from "./mergeSets"; \ No newline at end of file +export * from "./wginterface"; \ No newline at end of file diff --git a/src/mergeSets.ts b/src/mergeSets.ts deleted file mode 100644 index 19bf00c..0000000 --- a/src/mergeSets.ts +++ /dev/null @@ -1,55 +0,0 @@ -import * as wgBinding from "./wg_binding"; -import * as userspace from "./userspace"; -import { wireguardInterface, peerConfig, WgError } from "./userspace"; -export { wireguardInterface, peerConfig, WgError }; - -/** - * List wireguard interface's in userspace and kernel (linux) - * - * @returns Devices name - */ -export async function listDevices() { - if (process.platform !== "linux") return userspace.listDevices(); - return (await wgBinding.listDevices()).concat(...(await userspace.listDevices())) -} - -/** - * Get config wireguard config from wireguard interface - * @param deviceName - Device name - * @returns Config object - */ -export async function parseWgDevice(deviceName: string): Promise { - if (process.platform !== "linux") return userspace.parseWgDevice(deviceName); - if ((await wgBinding.listDevices()).includes(deviceName)) return wgBinding.parseWgDevice(deviceName); - return userspace.parseWgDevice(deviceName); -} - -/** - * Configure a Wireguard interface or even update it, if I was configuring it in linux, if it does not exist, it will create a new interface with the name - * - * @example - * - * ```js - await addDevice("wg0", { - privateKey: "AAAAAAAAAAAAAAAAAAAA=", - portListen: 8080, - peers: { - "GGGGAGGAGAGAGGAGGA=": { - presharedKey: "googleInCloud==", - allowedIPs: [ - "10.66.66.5/32", - "10.66.45.2/32" - ] - } - } - }); - * ``` - * - * @param deviceName - Wireguard interface name - * @param interfaceConfig - Config to interface (if update get config with parseWgDevice) - * @returns nothing - */ -export async function addDevice(deviceName: string, interfaceConfig: wireguardInterface) { - if (process.platform !== "linux") return userspace.addDevice(deviceName, interfaceConfig); - return wgBinding.addDevice(deviceName, interfaceConfig); -} \ No newline at end of file diff --git a/src/utils/config.ts b/src/utils/config.ts index 01cede5..8e5426a 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -1,4 +1,4 @@ -import { wireguardInterface } from "../userspace"; +import { wireguardInterface } from "../wginterface"; /** * Create wg-quick config file diff --git a/src/wg_binding.ts b/src/wg_binding.ts deleted file mode 100644 index a73a979..0000000 --- a/src/wg_binding.ts +++ /dev/null @@ -1,35 +0,0 @@ -import path from "path"; -import { wireguardInterface } from "./userspace"; -import { promisify } from "util"; -const wg_binding = require("../libs/prebuildifyLoad.cjs")("wginterface", path.join(__dirname, "..")); - -export const constants: { MAX_NAME_LENGTH: number } = wg_binding.constants; - -/** - * List all Wireguard interfaces - * @returns Interfaces array - */ -export async function listDevices(): Promise { - if (typeof wg_binding.listDevicesSync !== "function") throw new Error("Cannot list wireguard devices, check if avaible to current system!"); - return wg_binding.listDevicesSync(); -} - -/** - * Get interface config. - * @param deviceName - Wireguard interface name. - * @returns - */ -export async function parseWgDevice(deviceName: string): Promise { - if (typeof wg_binding.getConfigAsync !== "function") throw new Error("Cannot get device configs, check if wireguard is avaible to the system!"); - return promisify(wg_binding.getConfigAsync)(deviceName); -} - -/** - * Set Wireguard interface config. - * @param deviceName - Wireguard interface name. - * @param interfaceConfig - Peers and Interface config. - */ -export async function addDevice(deviceName: string, interfaceConfig: wireguardInterface): Promise { - if (typeof deviceName !== "string" || deviceName.length > 16 || deviceName.length <= 0) throw new Error("Check interface name!"); - return promisify(wg_binding.setConfigAsync)(deviceName, interfaceConfig); -} \ No newline at end of file diff --git a/src/interfaces.test.ts b/src/wginterface.test.ts similarity index 76% rename from src/interfaces.test.ts rename to src/wginterface.test.ts index 23fee80..e53b526 100644 --- a/src/interfaces.test.ts +++ b/src/wginterface.test.ts @@ -1,8 +1,8 @@ import { randomInt } from "crypto"; import { writeFileSync } from "fs"; import { userInfo } from "os"; -import * as Bridge from "../src/mergeSets"; -import * as utils from "../src/utils/index"; +import * as Bridge from "./wginterface"; +import * as utils from "./utils/index"; if (process.platform !== "win32" && (userInfo()).gid === 0) { // Make base config @@ -33,16 +33,18 @@ if (process.platform !== "win32" && (userInfo()).gid === 0) { describe(`Wireguard interface (${interfaceName})`, () => { it("Fist list", () => Bridge.listDevices()); it("Maneger", async () => { - await Bridge.addDevice(interfaceName, deviceConfig); - if (!((await Bridge.listDevices()).includes(interfaceName))) throw new Error("Invalid list devices"); + await Bridge.setConfig(interfaceName, deviceConfig); + if (!((await Bridge.listDevices()).some(s => s.name === interfaceName))) throw new Error("Invalid list devices"); }); it("Get config", async () => { - const raw = await Bridge.parseWgDevice(interfaceName); + const raw = await Bridge.getConfig(interfaceName); + let keyNot: string; writeFileSync(`${__dirname}/../${interfaceName}.addrs.json`, JSON.stringify({ + raw, deviceConfig, - raw }, null, 2)); + if (Object.keys(deviceConfig.peers).some(s => !(deviceConfig.peers[s].removeMe) && !(raw.peers[(keyNot = s)]))) throw new Error(("Invalid return config, key not exists: ").concat(keyNot)); }); it("After list", async () => await Bridge.listDevices()); diff --git a/src/userspace.ts b/src/wginterface.ts similarity index 53% rename from src/userspace.ts rename to src/wginterface.ts index 52777ca..4ac86c7 100644 --- a/src/userspace.ts +++ b/src/wginterface.ts @@ -1,8 +1,18 @@ import { promises as fs } from "node:fs"; -import net, { isIPv4 } from "node:net"; -import path from "node:path"; +import net from "node:net"; import { createInterface as readline } from "node:readline"; import { finished } from "node:stream/promises"; +import path from "path"; +import { promisify } from "util"; +import { createConfig } from "./utils/config"; +import * as ipManipulation from "./utils/ipm"; +import * as keygen from "./utils/keygen"; +import { randomInt } from "node:crypto"; +const wg_binding = require("../libs/prebuildifyLoad.cjs")("wginterface", path.join(__dirname, "..")); + +/** default location to run socket's */ +export const defaultPath = (process.env.WIRWGUARD_GO_RUN||"").length > 0 ? path.resolve(process.cwd(), process.env.WIRWGUARD_GO_RUN) : process.platform === "win32" ? "\\\\.\\pipe\\WireGuard" : "/var/run/wireguard"; +export const constants: { MAX_NAME_LENGTH: number } = wg_binding.constants; export type peerConfig = { /** Mark this peer to be removed, any changes remove this option */ @@ -32,12 +42,66 @@ export type wireguardInterface = { } }; -export class WgError extends Error { - payload?: string; -} +export class wgConfig extends Map { + publicKey?: string; + privateKey?: string; + portListen?: number; + fwmark?: number; + Address?: string[] = []; + DNS?: string[] = []; + replacePeers = true; + + constructor(config?: wireguardInterface) { + super(); + if (!config) return; + const { privateKey, publicKey, portListen, replacePeers, fwmark, Address, DNS, peers } = config; + this.privateKey = privateKey; + this.publicKey = publicKey; + this.portListen = portListen; + this.replacePeers = !!replacePeers; + this.fwmark = fwmark; + this.Address = Address || []; + this.DNS = DNS || []; + + if (typeof peers === "object") { + Object.keys(peers).forEach(pub => this.set(pub, peers[pub])); + } + } -/** default location to run socket's */ -export const defaultPath = (process.env.WIRWGUARD_GO_RUN||"").length > 0 ? path.resolve(process.cwd(), process.env.WIRWGUARD_GO_RUN) : process.platform === "win32" ? "\\\\.\\pipe\\WireGuard" : "/var/run/wireguard"; + async newPeer(withPreshared?: boolean) { + let peerKey: keygen.keyObjectPreshered; + while (!peerKey) { + peerKey = await keygen.keygenAsync(true); + if (this.has(peerKey.public)) peerKey = undefined; + } + + const allowedIPs = Array.from(this.values()).map(s => s.allowedIPs).flat(2).filter(s => net.isIPv4(s.split("/")[0])); + const IPv4 = ipManipulation.randomIp(this.Address.at(this.Address.length === 1 ? 0 : randomInt(0, this.Address.length - 1)), allowedIPs), IPv6 = ipManipulation.toV6(IPv4); + this.set(peerKey.public, { + allowedIPs: [IPv4, IPv6], + presharedKey: withPreshared ? peerKey.preshared : undefined + }); + return this.get(peerKey.public); + } + + toJSON(): wireguardInterface { + const { privateKey, publicKey, portListen, replacePeers, fwmark, Address, DNS } = this; + return { + privateKey, + publicKey, + portListen, + fwmark, + replacePeers, + Address, + DNS, + peers: Array.from(this).reduce((acc, [pubKey, info]) => Object.assign(acc, { [pubKey]: info}), {}) + }; + } + + toString() { + return createConfig(this.toJSON()); + } +} async function connectSocket(path: string): Promise { return new Promise((done, reject) => { @@ -46,17 +110,30 @@ async function connectSocket(path: string): Promise { }); } +/** + * List all Wireguard interfaces + * @returns Interfaces array + */ export async function listDevices() { - return (await fs.readdir(defaultPath).catch(() => [])).map(inter => { - if (inter.endsWith(".sock")) inter = inter.slice(0, -5); - return inter; - }); + let interfaceNames: {from: "userspace"|"kernel", name: string}[] = []; + if (typeof wg_binding.listDevicesAsync === "function") interfaceNames = interfaceNames.concat((await promisify(wg_binding.listDevicesAsync)() as string[]).map(name => ({from: "kernel", name})) ); + return interfaceNames.concat((await fs.readdir(defaultPath).catch((): string[] => [])).map(name => ({from: "userspace", name: name.endsWith(".sock") ? name.slice(0, -5) : name}))); } -export async function parseWgDevice(deviceName: string): Promise { - if (!deviceName || deviceName.length <= 2) throw new Error("Set valid device name"); +/** + * Get interface config. + * @param deviceName - Wireguard interface name. + * @returns + */ +export async function getConfig(deviceName: string): Promise { + const info = (await listDevices()).find(int => int.name === deviceName); + if (!info) throw new Error("Create interface, not exists"); + if (info.from === "kernel") { + if (typeof wg_binding.getConfigAsync === "function") return promisify(wg_binding.getConfigAsync)(deviceName); + else throw new Error("Cannot get config"); + } const client = await connectSocket(path.join(defaultPath, deviceName).concat(".sock")); - const config: wireguardInterface = {} as any; + const config = new wgConfig(); let latestPeer: string, previewKey: string; client.write("get=1\n\n"); @@ -72,46 +149,47 @@ export async function parseWgDevice(deviceName: string): Promise tetrisBreak.on("error", reject).once("close", done)); await finished(client.end()); - return Object.assign({peers: {}}, config); + return config.toJSON(); } -export async function addDevice(deviceName: string, interfaceConfig: wireguardInterface) { - if (!deviceName || deviceName.length <= 2) throw new Error("Set valid device name"); - +/** + * Set Wireguard interface config. + * @param deviceName - Wireguard interface name. + * @param interfaceConfig - Peers and Interface config. + */ +export async function setConfig(deviceName: string, interfaceConfig: wireguardInterface): Promise { + if (typeof wg_binding.setConfigAsync === "function") return promisify(wg_binding.setConfigAsync)(deviceName, interfaceConfig); const client = await connectSocket(path.join(defaultPath, deviceName).concat(".sock")); const writel = (...data: (string|number|boolean)[]) => client.write(("").concat(...(data.map(String)), "\n")); // Init set config in interface writel("set=1"); - // Keys - if (typeof interfaceConfig.privateKey === "string" && interfaceConfig.privateKey.length > 0) writel(("private_key="), (Buffer.from(interfaceConfig.privateKey, "base64").toString("hex"))); - if (typeof interfaceConfig.publicKey === "string" && interfaceConfig.publicKey.length > 0) writel(("private_key="), (Buffer.from(interfaceConfig.publicKey, "base64").toString("hex"))); - // Port listening if (interfaceConfig.portListen !== undefined && Math.floor(interfaceConfig.portListen) >= 0) writel(("listen_port="), ((Math.floor(interfaceConfig.portListen)))); @@ -121,6 +199,10 @@ export async function addDevice(deviceName: string, interfaceConfig: wireguardIn // Replace peer's if (interfaceConfig.replacePeers) writel("replace_peers=true"); + // Keys + if (typeof interfaceConfig.privateKey === "string" && interfaceConfig.privateKey.length > 0) writel(("private_key="), (Buffer.from(interfaceConfig.privateKey, "base64").toString("hex"))); + if (typeof interfaceConfig.publicKey === "string" && interfaceConfig.publicKey.length > 0) writel(("private_key="), (Buffer.from(interfaceConfig.publicKey, "base64").toString("hex"))); + // Mount peer for (const publicKey of Object.keys(interfaceConfig.peers||{})) { const { presharedKey, endpoint, keepInterval, removeMe, allowedIPs = [] } = interfaceConfig.peers[publicKey]; @@ -137,7 +219,7 @@ export async function addDevice(deviceName: string, interfaceConfig: wireguardIn if (typeof keepInterval === "number" && Math.floor(keepInterval) > 0) writel(("persistent_keepalive_interval="), (String(Math.floor(keepInterval)))); if (allowedIPs.length > 0) { writel("replace_allowed_ips=true"); - const fixed = allowedIPs.map(i => i.indexOf("/") === -1 ? i.concat("/", (isIPv4(i) ? "32" : "128")) : i) + const fixed = allowedIPs.map(i => i.indexOf("/") === -1 ? i.concat("/", (net.isIPv4(i) ? "32" : "128")) : i) for (const IIP of fixed) writel(("allowed_ip="), (IIP)); } } @@ -155,8 +237,7 @@ export async function addDevice(deviceName: string, interfaceConfig: wireguardIn await finished(client, { error: true }); const payloadKeys = payload.split("\n").filter(i => i.length > 3).map(line => { const iit = line.indexOf("="); return [ line.slice(0, iit), line.slice(iit+1) ]; }) if (payloadKeys.at(-1)[1] !== "0") { - const err = new WgError("Invalid send config, check log"); - err.payload = payload; + const err = new Error("Invalid send config, check log"); throw err; } return;