Droping rebory and migrate to cmake-js #14

Merged
Sirherobrine23 merged 5 commits from SonicCDPast into main 2024-07-09 03:02:27 +00:00
12 changed files with 238 additions and 466 deletions
Showing only changes of commit d8c7be1818 - Show all commits

@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
node-version: [ 16.x, 18.x, 20.x, latest ] node-version: [ 18.x, 20.x, 21.x, latest ]
steps: steps:
- name: Disable sudo PATH replace - name: Disable sudo PATH replace
run: | run: |
@ -32,18 +32,13 @@ jobs:
- uses: actions/setup-go@v4 - uses: actions/setup-go@v4
with: with:
go-version-file: addon/userspace/go/go.mod go-version-file: addon/userspace/go/go.mod
go-version: ">=1.22.0" go-version: ">=1.22"
- name: "Setup zig"
uses: korandoru/setup-zig@v1
with:
zig-version: "master"
- name: Install build dependencies - name: Install build dependencies
run: sudo apt update && sudo apt install -y build-essential run: sudo apt update && sudo apt install -y build-essential cmake
- name: Install node dependencies - name: Install node dependencies
run: npm install --no-save --no-audit --no-fund --ignore-scripts run: npm install --no-save --no-audit --no-fund --ignore-scripts
- name: Run tests - name: Run tests
run: ./node_modules/.bin/rebory build && sudo node --no-warnings --loader ts-node/esm src/index_test.js run: npm run build && sudo node --no-warnings --loader ts-node/esm src/index_test.js

86
CMakeLists.txt Normal file

@ -0,0 +1,86 @@
cmake_minimum_required(VERSION 3.15)
cmake_policy(SET CMP0091 NEW)
cmake_policy(SET CMP0042 NEW)
project (wg)
add_compile_definitions(NAPI_VERSION=8 NAPI_CPP_EXCEPTIONS)
# set_target_properties(PROPERTIES CXX_STANDARD 17)
# set_target_properties(PROPERTIES C_STANDARD 17)
set(CMAKE_C_STANDARD 17)
set(CMAKE_CXX_STANDARD 17)
include_directories(${CMAKE_JS_INC})
include_directories("node_modules/node-addon-api")
include_directories("addon")
include_directories("addon/genKey")
if(UNIX)
add_definitions(-fpermissive -fexceptions -w -fpermissive -fPIC)
endif()
file(GLOB GSOURCE_FILES
"addon/main.cpp"
"addon/genKey/wgkeys.cpp"
)
if(MSVC)
file(GLOB SOURCE_FILES "addon/win/wginterface.cpp")
include_directories("addon/win")
add_compile_definitions(_HAS_EXCEPTIONS=1 ONSTARTADDON)
target_link_libraries(${PROJECT_NAME}
"wbemuuid.lib"
"bcrypt.lib"
"crypt32.lib"
"iphlpapi.lib"
"kernel32.lib"
"ntdll.lib"
"ws2_32.lib"
"setupapi.lib"
)
elseif(UNIX AND NOT APPLE OR ANDROID)
include_directories("addon/linux")
file(GLOB SOURCE_FILES
"addon/linux/wireguard.c"
"addon/linux/wginterface.cpp"
)
else()
message(STATUS "Buiding go Userspace")
if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/addon/userspace/wg-go.o)
file(REMOVE_RECURSE ${CMAKE_CURRENT_SOURCE_DIR}/addon/userspace/wg-go.o)
endif()
if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/addon/userspace/wg-go.h)
file(REMOVE_RECURSE ${CMAKE_CURRENT_SOURCE_DIR}/addon/userspace/wg-go.h)
endif()
set(ENV{CGO_ENABLED} 1)
set(ENV{LDFLAGS} -w)
# Remove CXX and CC envs to CGO
set(ENV{DCXX} ENV{CXX})
set(ENV{DCC} ENV{CC})
set(ENV{CXX})
set(ENV{CC})
execute_process(
COMMAND go build -trimpath -v -o ../wg-go.o -buildmode c-archive .
# COMMAND env
RESULT_VARIABLE GOCODE
OUTPUT_VARIABLE GOBUILDLOG
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/addon/userspace/go
)
set(ENV{CXX} ENV{DCXX})
set(ENV{CC} ENV{DCC})
if(NOT GOCODE EQUAL "0")
message(FATAL_ERROR "cannot build go userspace code exit ${GOCODE}\n${GOBUILDLOG}")
endif()
include_directories("addon/userspace")
set(USERSPACEOBJ ${CMAKE_CURRENT_SOURCE_DIR}/addon/userspace/wg-go.o)
file(GLOB SOURCE_FILES "addon/userspace/wginterface.cpp")
endif()
add_library(${PROJECT_NAME} SHARED ${GSOURCE_FILES} ${SOURCE_FILES} ${CMAKE_JS_SRC})
set_target_properties(${PROJECT_NAME} PROPERTIES PREFIX "" SUFFIX ".node")
target_link_libraries(${PROJECT_NAME} ${USERSPACEOBJ} ${CMAKE_JS_LIB})
if(MSVC AND CMAKE_JS_NODELIB_DEF AND CMAKE_JS_NODELIB_TARGET)
# Generate node.lib
execute_process(COMMAND ${CMAKE_AR} /def:${CMAKE_JS_NODELIB_DEF} /out:${CMAKE_JS_NODELIB_TARGET} ${CMAKE_STATIC_LINKER_FLAGS})
endif()

@ -2,8 +2,42 @@
Manage your Wireguard interfaces directly from Node.js without any wrappers over `wg` or `wg-quick` Manage your Wireguard interfaces directly from Node.js without any wrappers over `wg` or `wg-quick`
```js > [!WARNING]
> Require cmake and tools (GCC/GCC++, clang or Visual Studio) to build this addon
>
> New versions does't include prebuilt binaries
```js
import { setConfig, getConfig, key, Config } from "../index.js"
const tunName = process.platform === "darwin" ? "utun10" : "wg3" // Tunnel name, in MacOS/Darwin require start with utun prefix
let currentConfig: Config
try {
currentConfig = await getConfig(tunName) // Check if exists tun
} catch {
// Create new wireguard tun
currentConfig = {
name: tunName,
privateKey: await key.privateKey(),
portListen: 5820,
address: [
"10.66.66.1/24"
],
peers: {}
}
}
// Add new Peer
const peerPrivate = await key.privateKey()
currentConfig.peers[key.publicKey(peerPrivate)] = {
presharedKey: await key.presharedKey(),
allowedIPs: [
"10.66.66.2/24"
]
}
// Deploy new Config
await setConfig(currentConfig)
``` ```
## Licences ## Licences

@ -100,9 +100,9 @@ void WireguardConfig::getWireguardConfig() {
for ((peer) = (devConfig)->first_peer; (peer); (peer) = (peer)->next_peer) { for ((peer) = (devConfig)->first_peer; (peer); (peer) = (peer)->next_peer) {
auto PeerConfig = Peer(); auto PeerConfig = Peer();
if (peer->flags & WGPEER_HAS_PRESHARED_KEY) PeerConfig.presharedKey = wgKeys::toString(peer->preshared_key); if (peer->flags & WGPEER_HAS_PRESHARED_KEY) PeerConfig.presharedKey = wgKeys::toString(peer->preshared_key);
if (peer->flags & WGPEER_HAS_PERSISTENT_KEEPALIVE_INTERVAL) PeerConfig.keepInterval = peer->persistent_keepalive_interval;
if (peer->endpoint.addr.sa_family == AF_INET||peer->endpoint.addr.sa_family == AF_INET6) PeerConfig.endpoint = HostAdresses(true, &peer->endpoint.addr); if (peer->endpoint.addr.sa_family == AF_INET||peer->endpoint.addr.sa_family == AF_INET6) PeerConfig.endpoint = HostAdresses(true, &peer->endpoint.addr);
PeerConfig.keepInterval = peer->persistent_keepalive_interval;
PeerConfig.lastHandshake = peer->last_handshake_time.tv_sec*1000; PeerConfig.lastHandshake = peer->last_handshake_time.tv_sec*1000;
PeerConfig.rxBytes = peer->rx_bytes; PeerConfig.rxBytes = peer->rx_bytes;
PeerConfig.txBytes = peer->tx_bytes; PeerConfig.txBytes = peer->tx_bytes;

@ -36,18 +36,6 @@ Napi::Object StartAddon(const Napi::Env env, const Napi::Object exports) {
} }
})); }));
exports.Set("listDevices", Napi::Function::New(env, [](const Napi::CallbackInfo &info) -> Napi::Value {
const Napi::Env env = info.Env();
try {
ListDevices *worker = new ListDevices(env);
worker->Queue();
return worker->NodePromise.Promise();
} catch (std::string &err) {
Napi::Error::New(env, err).ThrowAsJavaScriptException();
return env.Undefined();
}
}));
exports.Set("setConfig", Napi::Function::New(env, [](const Napi::CallbackInfo &info) -> Napi::Value { exports.Set("setConfig", Napi::Function::New(env, [](const Napi::CallbackInfo &info) -> Napi::Value {
const Napi::Env env = info.Env(); const Napi::Env env = info.Env();
if (!(info[0].IsObject())) Napi::Error::New(env, "Set wireguard config!").ThrowAsJavaScriptException(); if (!(info[0].IsObject())) Napi::Error::New(env, "Set wireguard config!").ThrowAsJavaScriptException();

@ -16,7 +16,6 @@ import (
"golang.zx2c4.com/wireguard/device" "golang.zx2c4.com/wireguard/device"
"golang.zx2c4.com/wireguard/ipc" "golang.zx2c4.com/wireguard/ipc"
"golang.zx2c4.com/wireguard/tun" "golang.zx2c4.com/wireguard/tun"
) )
const levelLog = device.LogLevelError const levelLog = device.LogLevelError

@ -40,26 +40,6 @@ class DeleteInterface : public Promised {
} }
}; };
class ListDevices : public Promised {
WireguardDevices wgDevs;
public:
ListDevices(const Napi::Env &env): Promised(env), wgDevs{} {}
void Execute() override {
try {
wgDevs.getInterfaces();
} catch (std::string &err) { SetError(err); }
}
void runOk(std::function<void(Napi::Value)> callback) override {
Napi::HandleScope scope(Env());
const Napi::Env env = Env();
const Napi::Array interf = Napi::Array::New(env);
for (auto &ip : wgDevs) interf.Set(interf.Length(), ip);
callback(interf);
}
};
class SetConfig : public WireguardConfig, public Promised { class SetConfig : public WireguardConfig, public Promised {
public: public:
void Execute() { void Execute() {

@ -1,59 +0,0 @@
name: wginterface
defines:
- "NODE_VERSION=8"
- "NAPI_CPP_EXCEPTIONS"
includes:
- node_modules/node-addon-api
- ./addon
sources:
- "addon/main.cpp"
- "addon/genKey/wgkeys.cpp"
- "addon/userspace/wginterface.cpp"
prebuild:
- shell: bash
cwd: ./addon/userspace/go
ifOs:
- "!win32"
- "!linux"
env:
CGO_ENABLED: "1"
LDFLAGS: "-w"
run: |
go build -trimpath -v -o ../wg-go.o -buildmode c-archive .
mv -fv ../wg-go.o "${BUILDDIR}"
target:
linux:
sources:
- "!addon/userspace/wginterface.cpp"
- "addon/linux/wginterface.cpp"
- "addon/linux/wireguard.c"
flags:
- "!-fno-exceptions"
- "-fpermissive"
- "-fexceptions"
- "-w"
- "-fpermissive"
- "-fPIC"
win32:
sources:
- "!addon/userspace/wginterface.cpp"
- "addon/win/wginterface.cpp"
libraries:
- wbemuuid.lib
- bcrypt.lib
- crypt32.lib
- iphlpapi.lib
- kernel32.lib
- ntdll.lib
- ws2_32.lib
- setupapi.lib
defines:
- "_HAS_EXCEPTIONS=1"
- "ONSTARTADDON"
darwin:
flags:
- "!-fno-exceptions"
- "-fpermissive"
- "-fexceptions"
- "-w"
- "-fpermissive"

@ -33,19 +33,19 @@
"node": ">=16.0.0" "node": ">=16.0.0"
}, },
"scripts": { "scripts": {
"install": "rebory prebuild", "install": "cmake-js compile",
"dev": "rebory build", "build": "cmake-js rebuild",
"test": "rebory build && node --no-warnings --loader ts-node/esm src/index_test.js", "test": "cmake-js compile && node --no-warnings --loader ts-node/esm src/index_test.js",
"prepack": "tsc --build --clean && tsc --build && rebory build --release", "prepack": "tsc --build --clean && tsc --build",
"postpack": "tsc --build --clean" "postpack": "tsc --build --clean"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^20.11.26", "@types/node": "^20.14.10",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"typescript": "^5.4.2" "typescript": "^5.5.3"
}, },
"dependencies": { "dependencies": {
"node-addon-api": "^8.0.0", "cmake-js": "^7.3.0",
"rebory": "^0.2.10" "node-addon-api": "^8.0.0"
} }
} }

55
src/addons.ts Normal file

@ -0,0 +1,55 @@
import path from "node:path";
import fs from "node:fs/promises";
const __dirname = import.meta.dirname || path.dirname((await import("node:url")).fileURLToPath(import.meta.url));
export const projectRoot: string = !process["resourcesPath"] ? path.resolve(__dirname, "..") : process["resourcesPath"];
declare global {
namespace NodeJS {
export interface DlopenModule {
exports: any;
}
interface Process {
/**
* The `process.dlopen()` method allows dynamically loading shared objects. It is primarily used by `require()` to load C++ Addons, and should not be used directly, except in special cases. In other words, `require()` should be preferred over `process.dlopen()` unless there are specific reasons such as custom dlopen flags or loading from ES modules.
*
* An important requirement when calling `process.dlopen()` is that the `module` instance must be passed. Functions exported by the C++ Addon are then accessible via `module.exports`.
* @param module - module to export
* @param filename - Addon path
* @param flags - The flags argument is an integer that allows to specify dlopen behavior. See the [os.constants.dlopen](https://nodejs.org/docs/latest/api/os.html#dlopen-constants) documentation for details.
* @default flags `os.constants.dlopen.RTLD_LAZY`
* @since v9.0.0
*/
dlopen(module: DlopenModule, filename: string, flags: number): void;
/**
* The `process.dlopen()` method allows dynamically loading shared objects. It is primarily used by `require()` to load C++ Addons, and should not be used directly, except in special cases. In other words, `require()` should be preferred over `process.dlopen()` unless there are specific reasons such as custom dlopen flags or loading from ES modules.
*
* An important requirement when calling `process.dlopen()` is that the `module` instance must be passed. Functions exported by the C++ Addon are then accessible via `module.exports`.
* @param module - module to export
* @param filename - Addon path
* @since v0.1.16
*/
dlopen(module: DlopenModule, filename: string): void;
}
}
}
async function exists(filePath: string) {
return fs.access(path.resolve(filePath)).then(() => true, () => false);
}
export async function LoadAddon<T = any>(addonFile: string, exports?: Record<string, any>): Promise<T> {
let _addonFile: string = null
if (await exists(addonFile)) _addonFile = addonFile;
else if (await exists(path.resolve(projectRoot, addonFile))) _addonFile = path.resolve(projectRoot, addonFile)
else if (await exists(path.resolve(projectRoot, addonFile+".node"))) _addonFile = path.resolve(projectRoot, addonFile+".node")
else if (await exists(path.resolve(projectRoot, "build/Release", addonFile))) _addonFile = path.resolve(projectRoot, "build/Release", addonFile)
else if (await exists(path.resolve(projectRoot, "build/Release", addonFile+".node"))) _addonFile = path.resolve(projectRoot, "build/Release", addonFile+".node")
else if (await exists(path.resolve(projectRoot, "build/Debug", addonFile))) _addonFile = path.resolve(projectRoot, "build/Debug", addonFile)
else if (await exists(path.resolve(projectRoot, "build/Debug", addonFile+".node"))) _addonFile = path.resolve(projectRoot, "build/Debug", addonFile+".node")
if (!_addonFile) throw new Error("Cannot load required addon")
let ext: NodeJS.DlopenModule = {exports: Object.assign({}, exports)}
process.dlopen(ext, _addonFile)
return ext.exports
}

@ -1,8 +1,5 @@
import path from "node:path"; import path from "node:path";
import { loadAddon } from "rebory"; import { LoadAddon, projectRoot } from "./addons.js";
import { key } from "./index.js";
import { isIP } from "node:net";
const __dirname = import.meta.dirname || path.dirname((await import("node:url")).fileURLToPath(import.meta.url));
interface Peer { interface Peer {
/** Preshared key to peer */ /** Preshared key to peer */
@ -34,7 +31,7 @@ export interface GetPeer extends Peer {
lastHandshake: Date; lastHandshake: Date;
} }
interface Config<T extends Peer> { export interface Config<T extends Peer = Peer> {
/** Wireguard interface name */ /** Wireguard interface name */
name: string; name: string;
@ -63,9 +60,15 @@ export interface SetConfig extends Config<SetPeer> {
replacePeers?: boolean; replacePeers?: boolean;
}; };
export const addon = (await loadAddon(path.resolve(__dirname, "../binding.yaml"))).wginterface.load_addon<{ /**
* Exported wireguard-tools.js addon
*/
export const addon = await LoadAddon<{
/** External functions or drive info */
constants: {
/** Current Wireguard drive version */ /** Current Wireguard drive version */
driveVersion?: string; driveVersion?: string;
}
/** /**
* Delete interface if exists * Delete interface if exists
@ -73,13 +76,6 @@ export const addon = (await loadAddon(path.resolve(__dirname, "../binding.yaml")
*/ */
deleteInterface(name: string): Promise<void>; deleteInterface(name: string): Promise<void>;
/**
* Get Wireguard interfaces list
*
* if running in userspace return socket (UAPI) path's
*/
listDevices(): Promise<string[]>;
/** /**
* Get current config from Wireguard interface * Get current config from Wireguard interface
* @param name - Interface name * @param name - Interface name
@ -91,303 +87,13 @@ export const addon = (await loadAddon(path.resolve(__dirname, "../binding.yaml")
* @param config - Interface config * @param config - Interface config
*/ */
setConfig(config: SetConfig): Promise<void>; setConfig(config: SetConfig): Promise<void>;
}>({ }>("wg", {
WIN32DLLPATH: path.resolve(__dirname, "../addon/win", (process.arch === "x64" && "amd64") || (process.arch === "ia32" && "x86") || process.arch, "wireguard.dll") WIN32DLLPATH: path.resolve(projectRoot, "addon/win", (process.arch === "x64" && "amd64") || (process.arch === "ia32" && "x86") || process.arch, "wireguard.dll")
}); });
export const { export const {
driveVersion, constants: {driveVersion = "Unknown"},
listDevices,
getConfig, getConfig,
setConfig, setConfig,
deleteInterface deleteInterface
} = addon; } = addon;
export class WireGuardPeer {
constructor(public publicKey: string, private __Wg: Wireguard) { }
async getStats() {
const { rxBytes, txBytes, lastHandshake } = await getConfig(this.__Wg.name).then((config) => config.peers[this.publicKey]);
return {
rxBytes,
txBytes,
lastHandshake
};
}
addNewAddress(address: string) {
if (isIP(address.split("/")[0]) === 0) throw new Error("Invalid IP address");
if (!this.__Wg._peers) this.__Wg._peers = new Map();
const _addr = new Set(this.__Wg._peers.get(this.publicKey).allowedIPs);
_addr.add(address.split("/")[0]);
this.__Wg._peers.get(this.publicKey).allowedIPs = Array.from(_addr);
return this;
}
removeAddress(address: string) {
if (isIP(address.split("/")[0]) === 0) throw new Error("Invalid IP address");
if (!this.__Wg._peers) this.__Wg._peers = new Map();
const _addr = new Set(this.__Wg._peers.get(this.publicKey).allowedIPs);
_addr.delete(address.split("/")[0]);
this.__Wg._peers.get(this.publicKey).allowedIPs = Array.from(_addr);
return this;
}
setKeepInterval(keepInterval: number) {
if (typeof keepInterval !== "number" || keepInterval < 0) throw new Error("Invalid keepInterval");
if (!this.__Wg._peers) this.__Wg._peers = new Map();
if (keepInterval > 0) this.__Wg._peers.get(this.publicKey).keepInterval = keepInterval;
else delete this.__Wg._peers.get(this.publicKey).keepInterval;
return this;
}
setEndpoint(endpoint: string) {
if (typeof endpoint !== "string") throw new Error("Invalid endpoint");
if (!this.__Wg._peers) this.__Wg._peers = new Map();
if (endpoint.length > 0) this.__Wg._peers.get(this.publicKey).endpoint = endpoint;
else delete this.__Wg._peers.get(this.publicKey).endpoint;
return this;
}
/**
* Sets the preshared key for the peer.
* @param presharedKey - The preshared key to set. If not provided, a new preshared key will be generated.
* @returns The updated WireGuard interface object.
* @throws {Error} If the provided preshared key is invalid.
*/
setPresharedKey(): Promise<this> & this;
/**
* Sets the preshared key for the peer.
* @param presharedKey - The preshared key to set. If not provided, a new preshared key will be generated.
* @returns The updated WireGuard interface object.
* @throws {Error} If the provided preshared key is invalid.
*/
setPresharedKey(presharedKey: string): this;
/**
* Sets the preshared key for the peer.
* @param presharedKey - The preshared key to set. If not provided, a new preshared key will be generated.
* @returns The updated WireGuard interface object.
* @throws {Error} If the provided preshared key is invalid.
*/
setPresharedKey(presharedKey?: string) {
if (!this.__Wg._peers) this.__Wg._peers = new Map();
if (!presharedKey) return Object.assign(key.presharedKey().then((presharedKey) => this.__Wg._peers.get(this.publicKey).presharedKey = presharedKey), this);
if (typeof presharedKey !== "string" || presharedKey.length !== key.Base64Length) throw new Error("Invalid presharedKey");
if (!this.__Wg._peers) this.__Wg._peers = new Map();
this.__Wg._peers.get(this.publicKey).presharedKey = presharedKey;
return this;
}
/**
* Removes the peer from the WireGuard interface.
* @returns The updated WireGuard interface.
*/
remove() {
if (!this.__Wg._peers) this.__Wg._peers = new Map();
this.__Wg._peers.get(this.publicKey)["removeMe"] = true;
return this;
}
/**
* Converts the `WireGuard Peer` object to a JSON representation.
* @returns The JSON representation of the `WireGuard Peer` object.
*/
toJSON(): [string, SetPeer] {
if (!this.__Wg._peers) this.__Wg._peers = new Map();
const { keepInterval, endpoint, presharedKey, allowedIPs } = this.__Wg._peers.get(this.publicKey);
const peer: SetPeer = Object.create({});
if (presharedKey) peer.presharedKey = presharedKey;
if (keepInterval) peer.keepInterval = keepInterval;
if (endpoint) peer.endpoint = endpoint;
if (allowedIPs) peer.allowedIPs = allowedIPs;
return [this.publicKey, peer];
}
}
/**
* Maneger Wireguard interface and peers simple and fast
*/
export class Wireguard {
constructor(config?: SetConfig | GetConfig | Config<Peer>) {
// super({});
if (!config) return;
if (typeof config === "object") {
if (config instanceof Wireguard) return config;
}
}
private _name: string;
get name() {
return this._name;
}
/**
* Set Wireguard interface name
* @param name - Interface name
* @returns Wireguard
*/
set name(name: string) {
if (typeof name !== "string" || name.length === 0) throw new Error("Invalid name");
this._name = name;
}
private _portListen: number;
/**
* Sets the port to listen on.
* @param port - The port number to listen on.
* @returns The current instance of the `Wireguard` class.
* @throws {Error} If the provided port is not a number or is less than 0.
*/
setPortListen(port: number) {
if (typeof port !== "number" || port < 0) throw new Error("Invalid port");
this._portListen = port;
return this;
}
private _fwmark: number;
/**
* Sets the fwmark value for the WireGuard interface.
*
* @param fwmark - The fwmark value to set.
* @returns The current instance of the `Wireguard` class.
* @throws {Error} If the `fwmark` value is not a number or is less than 0.
*/
setFwmark(fwmark: number) {
if (typeof fwmark !== "number" || fwmark < 0) throw new Error("Invalid fwmark");
this._fwmark = fwmark;
return this;
}
private _privateKey: string;
/**
* Get interface public key
*/
public get publicKey() {
return key.publicKey(this._privateKey);
}
/**
* Generate new private key and set to Wireguard interface
*/
setPrivateKey(): Promise<this> & this;
/**
* Set private key to Wireguard interface
* @param privateKey - Private key
* @returns Wireguard
*/
setPrivateKey(privateKey: string): this;
setPrivateKey(privateKey?: string): this {
if (!privateKey) return Object.assign(key.privateKey().then((privateKey) => this._privateKey = privateKey), this);
else this._privateKey = privateKey;
return this;
}
private _address: string[];
addNewAddress(address: string) {
if (isIP(address.split("/")[0]) === 0) throw new Error("Invalid IP address");
const _addr = new Set(this._address);
_addr.add(address.split("/")[0]);
this._address = Array.from(_addr);
return this;
}
removeAddress(address: string) {
if (isIP(address.split("/")[0]) === 0) throw new Error("Invalid IP address");
const _addr = new Set(this._address);
_addr.delete(address.split("/")[0]);
this._address = Array.from(_addr);
return this;
}
_peers: Map<string, Peer>;
/**
* Adds a new peer to the Wireguard interface.
*
* @param publicKey - The public key of the peer.
* @param peer - other configuration options for the peer.
* @throws Error if the peer is invalid.
*/
addNewPeer(publicKey: string, peer: Peer) {
if (!this._peers) this._peers = new Map();
if (!((typeof publicKey === "string" && publicKey.length === key.Base64Length) && typeof peer === "object")) throw new Error("Invalid peer");
let { allowedIPs, endpoint, keepInterval, presharedKey } = peer;
this._peers.set(publicKey, {});
if ((typeof presharedKey === "string" && presharedKey.length === key.Base64Length)) this._peers.get(publicKey).presharedKey = presharedKey;
if (typeof keepInterval === "number") this._peers.get(publicKey).keepInterval = keepInterval;
if (typeof endpoint === "string") this._peers.get(publicKey).endpoint = endpoint;
if (Array.isArray(allowedIPs)) this._peers.get(publicKey).allowedIPs = allowedIPs.filter((ip) => isIP(ip.split("/")[0]) !== 0);
return new WireGuardPeer(publicKey, this);
}
/**
* Removes a peer from the WireGuard interface.
* @param publicKey - The public key of the peer to remove.
* @returns The updated WireGuard interface.
*/
removePeer(publicKey: string) {
if (this._peers) this._peers.delete(publicKey);
return this;
}
/**
* Converts the `Wireguard Interface` object to a JSON representation.
* @returns The JSON representation of the `Wireguard Interface` object.
*/
toJSON(): SetConfig {
const config: SetConfig = Object.create({});
config.name = this._name;
config.privateKey = this._privateKey;
if (this._portListen) config.portListen = this._portListen;
if (this._fwmark) config.fwmark = this._fwmark;
if (this._address) config.address = this._address;
if (this._peers) config.peers = Array.from(this._peers||[]).map(([pubKey]) => new WireGuardPeer(pubKey, this).toJSON()).reduce((obj, [pubKey, peer]) => (obj[pubKey] = peer, obj), {});
return config;
}
/**
* Set new config to Wireguard interface or create new interface if not exists
* @returns Promise<void>
*/
async deploy() {
return setConfig({
...(this.toJSON()),
replacePeers: true,
});
}
/**
* Deletes the WireGuard interface.
* @returns A promise that resolves when the interface is successfully deleted.
*/
async delete() {
return deleteInterface(this._name);
}
/**
* Retrieves the configuration for the Wireguard interface.
*/
async getConfig() {
const { peers, privateKey, address, fwmark, portListen } = await getConfig(this._name);
this._privateKey = privateKey;
this._portListen = portListen;
this._address = address;
this._fwmark = fwmark;
this._peers = new Map(Object.entries(peers));
for (const [publicKey, { allowedIPs, endpoint, keepInterval, presharedKey }] of this._peers) {
this._peers.set(publicKey, { allowedIPs, endpoint, keepInterval, presharedKey });
if (keepInterval === 0) delete this._peers.get(publicKey).keepInterval;
if (!presharedKey) delete this._peers.get(publicKey).presharedKey;
if (!endpoint) delete this._peers.get(publicKey).endpoint;
if (!allowedIPs) delete this._peers.get(publicKey).allowedIPs;
else if (allowedIPs.length === 0) delete this._peers.get(publicKey).allowedIPs;
}
}
}
export default Wireguard;

@ -1,51 +1,39 @@
import test from "node:test"; import test from "node:test"
import { Wireguard, getConfig, setConfig } from "./wginterface.js"; import { SetConfig, GetConfig, Config, setConfig, getConfig, deleteInterface } from "./wginterface.js"
import { presharedKey, privateKey, publicKey } from "./key.js"; import { privateKey, publicKey, presharedKey } from "./key.js"
import { format } from "node:util";
import assert from "node:assert"; import assert from "node:assert";
await test("Wireguard interface", async t => { await test("Wireguard interface", async t => {
const config = new Wireguard; const newConfig: Config = {
config.name = "wg23"; name: process.platform === "darwin" ? "utun23" : "wg10",
if (process.platform === "darwin") config.name = "utun23"; privateKey: await privateKey(),
portListen: 8260,
config.setPrivateKey(await privateKey()); address: [
config.addNewAddress("10.66.66.1/32"); "10.66.66.1/24"
config.addNewAddress("fd42:42:42::1/128"); ],
peers: {}
const peer1 = await privateKey(); }
config.addNewPeer(publicKey(peer1), { for (let i = 0; i != 10; i++) {
keepInterval: 15, newConfig.peers[publicKey(await privateKey())] = {
presharedKey: await presharedKey(), presharedKey: await presharedKey(),
keepInterval: 25,
allowedIPs: [ allowedIPs: [
"10.66.66.2/32" format("10.66.66.%d/24", i+2)
] ],
}
}
await t.test("Set config", async () => setConfig(newConfig));
await t.test("Get config and check", async () => {
const currentConfig = await getConfig(newConfig.name)
assert.equal(currentConfig.privateKey, newConfig.privateKey)
for (const pubKey in newConfig.peers) {
console.log("Current: %O\nIn set: %O", currentConfig.peers[pubKey], newConfig.peers[pubKey])
if (!currentConfig.peers[pubKey]) throw new Error("one peer not exists in currentConfig")
else if (currentConfig.peers[pubKey].presharedKey != newConfig.peers[pubKey].presharedKey) throw new Error("presharedKey is mismatch")
else if (currentConfig.peers[pubKey].keepInterval != newConfig.peers[pubKey].keepInterval) throw new Error("keepInterval is mismatch")
}
}); });
await t.test("Deleting interface", async () => deleteInterface(newConfig.name));
const peer2 = await privateKey();
config.addNewPeer(publicKey(peer2), {
keepInterval: 0,
allowedIPs: [
"10.66.66.3/32"
]
});
const jsonConfig = config.toJSON();
let skip: string;
await t.test("Create and Set config in interface", async () => setConfig(jsonConfig).catch(err => { skip = "Cannot set wireguard config"; return Promise.reject(err); }));
await t.test("Get config from interface", { skip }, async () => {
const config = await getConfig(jsonConfig.name);
// console.dir(config, { depth: null });
if (!config.peers[publicKey(peer1)]) throw new Error("Peer not exists in interface");
if (!config.peers[publicKey(peer2)]) throw new Error("Peer not exists in interface");
assert.equal(config.peers[publicKey(peer1)].keepInterval, jsonConfig.peers[publicKey(peer1)].keepInterval);
assert.equal(config.peers[publicKey(peer1)].presharedKey, jsonConfig.peers[publicKey(peer1)].presharedKey);
assert.deepEqual(config.peers[publicKey(peer1)].allowedIPs, jsonConfig.peers[publicKey(peer1)].allowedIPs);
assert.deepEqual(config.peers[publicKey(peer2)].allowedIPs, jsonConfig.peers[publicKey(peer2)].allowedIPs);
});
await t.test("Delete interface if exists", { skip }, async () => config.delete());
}); });