Droping rebory and migrate to cmake-js #14
13
.github/workflows/test.yaml
vendored
13
.github/workflows/test.yaml
vendored
@ -13,7 +13,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [ 16.x, 18.x, 20.x, latest ]
|
||||
node-version: [ 18.x, 20.x, 21.x, latest ]
|
||||
steps:
|
||||
- name: Disable sudo PATH replace
|
||||
run: |
|
||||
@ -32,18 +32,13 @@ jobs:
|
||||
- uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version-file: addon/userspace/go/go.mod
|
||||
go-version: ">=1.22.0"
|
||||
|
||||
- name: "Setup zig"
|
||||
uses: korandoru/setup-zig@v1
|
||||
with:
|
||||
zig-version: "master"
|
||||
go-version: ">=1.22"
|
||||
|
||||
- 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
|
||||
run: npm install --no-save --no-audit --no-fund --ignore-scripts
|
||||
|
||||
- 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
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()
|
36
README.md
36
README.md
@ -2,8 +2,42 @@
|
||||
|
||||
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
|
||||
|
@ -30,7 +30,7 @@ std::string getWireguardVersion() {
|
||||
#endif
|
||||
|
||||
std::string version = "Unknown";
|
||||
|
||||
|
||||
// /sys/module/wireguard/version - for kernel module
|
||||
if (std::filesystem::exists("/sys/module/wireguard/version")) {
|
||||
std::ifstream file("/sys/module/wireguard/version");
|
||||
@ -100,9 +100,9 @@ void WireguardConfig::getWireguardConfig() {
|
||||
for ((peer) = (devConfig)->first_peer; (peer); (peer) = (peer)->next_peer) {
|
||||
auto PeerConfig = Peer();
|
||||
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);
|
||||
|
||||
PeerConfig.keepInterval = peer->persistent_keepalive_interval;
|
||||
PeerConfig.lastHandshake = peer->last_handshake_time.tv_sec*1000;
|
||||
PeerConfig.rxBytes = peer->rx_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 {
|
||||
const Napi::Env env = info.Env();
|
||||
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/ipc"
|
||||
"golang.zx2c4.com/wireguard/tun"
|
||||
|
||||
)
|
||||
|
||||
const levelLog = device.LogLevelError
|
||||
|
20
addon/wg.hh
20
addon/wg.hh
@ -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 {
|
||||
public:
|
||||
void Execute() {
|
||||
|
59
binding.yaml
59
binding.yaml
@ -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"
|
16
package.json
16
package.json
@ -33,19 +33,19 @@
|
||||
"node": ">=16.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"install": "rebory prebuild",
|
||||
"dev": "rebory build",
|
||||
"test": "rebory build && node --no-warnings --loader ts-node/esm src/index_test.js",
|
||||
"prepack": "tsc --build --clean && tsc --build && rebory build --release",
|
||||
"install": "cmake-js compile",
|
||||
"build": "cmake-js rebuild",
|
||||
"test": "cmake-js compile && node --no-warnings --loader ts-node/esm src/index_test.js",
|
||||
"prepack": "tsc --build --clean && tsc --build",
|
||||
"postpack": "tsc --build --clean"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.11.26",
|
||||
"@types/node": "^20.14.10",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.4.2"
|
||||
"typescript": "^5.5.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"node-addon-api": "^8.0.0",
|
||||
"rebory": "^0.2.10"
|
||||
"cmake-js": "^7.3.0",
|
||||
"node-addon-api": "^8.0.0"
|
||||
}
|
||||
}
|
||||
|
55
src/addons.ts
Normal file
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 { loadAddon } from "rebory";
|
||||
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));
|
||||
import { LoadAddon, projectRoot } from "./addons.js";
|
||||
|
||||
interface Peer {
|
||||
/** Preshared key to peer */
|
||||
@ -34,7 +31,7 @@ export interface GetPeer extends Peer {
|
||||
lastHandshake: Date;
|
||||
}
|
||||
|
||||
interface Config<T extends Peer> {
|
||||
export interface Config<T extends Peer = Peer> {
|
||||
/** Wireguard interface name */
|
||||
name: string;
|
||||
|
||||
@ -63,9 +60,15 @@ export interface SetConfig extends Config<SetPeer> {
|
||||
replacePeers?: boolean;
|
||||
};
|
||||
|
||||
export const addon = (await loadAddon(path.resolve(__dirname, "../binding.yaml"))).wginterface.load_addon<{
|
||||
/** Current Wireguard drive version */
|
||||
driveVersion?: string;
|
||||
/**
|
||||
* Exported wireguard-tools.js addon
|
||||
*/
|
||||
export const addon = await LoadAddon<{
|
||||
/** External functions or drive info */
|
||||
constants: {
|
||||
/** Current Wireguard drive version */
|
||||
driveVersion?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete interface if exists
|
||||
@ -73,13 +76,6 @@ export const addon = (await loadAddon(path.resolve(__dirname, "../binding.yaml")
|
||||
*/
|
||||
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
|
||||
* @param name - Interface name
|
||||
@ -91,303 +87,13 @@ export const addon = (await loadAddon(path.resolve(__dirname, "../binding.yaml")
|
||||
* @param config - Interface config
|
||||
*/
|
||||
setConfig(config: SetConfig): Promise<void>;
|
||||
}>({
|
||||
WIN32DLLPATH: path.resolve(__dirname, "../addon/win", (process.arch === "x64" && "amd64") || (process.arch === "ia32" && "x86") || process.arch, "wireguard.dll")
|
||||
}>("wg", {
|
||||
WIN32DLLPATH: path.resolve(projectRoot, "addon/win", (process.arch === "x64" && "amd64") || (process.arch === "ia32" && "x86") || process.arch, "wireguard.dll")
|
||||
});
|
||||
|
||||
export const {
|
||||
driveVersion,
|
||||
listDevices,
|
||||
constants: {driveVersion = "Unknown"},
|
||||
getConfig,
|
||||
setConfig,
|
||||
deleteInterface
|
||||
} = 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;
|
||||
} = addon;
|
@ -1,51 +1,39 @@
|
||||
import test from "node:test";
|
||||
import { Wireguard, getConfig, setConfig } from "./wginterface.js";
|
||||
import { presharedKey, privateKey, publicKey } from "./key.js";
|
||||
import test from "node:test"
|
||||
import { SetConfig, GetConfig, Config, setConfig, getConfig, deleteInterface } from "./wginterface.js"
|
||||
import { privateKey, publicKey, presharedKey } from "./key.js"
|
||||
import { format } from "node:util";
|
||||
import assert from "node:assert";
|
||||
|
||||
await test("Wireguard interface", async t => {
|
||||
const config = new Wireguard;
|
||||
config.name = "wg23";
|
||||
if (process.platform === "darwin") config.name = "utun23";
|
||||
const newConfig: Config = {
|
||||
name: process.platform === "darwin" ? "utun23" : "wg10",
|
||||
privateKey: await privateKey(),
|
||||
portListen: 8260,
|
||||
address: [
|
||||
"10.66.66.1/24"
|
||||
],
|
||||
peers: {}
|
||||
}
|
||||
for (let i = 0; i != 10; i++) {
|
||||
newConfig.peers[publicKey(await privateKey())] = {
|
||||
presharedKey: await presharedKey(),
|
||||
keepInterval: 25,
|
||||
allowedIPs: [
|
||||
format("10.66.66.%d/24", i+2)
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
config.setPrivateKey(await privateKey());
|
||||
config.addNewAddress("10.66.66.1/32");
|
||||
config.addNewAddress("fd42:42:42::1/128");
|
||||
|
||||
const peer1 = await privateKey();
|
||||
config.addNewPeer(publicKey(peer1), {
|
||||
keepInterval: 15,
|
||||
presharedKey: await presharedKey(),
|
||||
allowedIPs: [
|
||||
"10.66.66.2/32"
|
||||
]
|
||||
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")
|
||||
}
|
||||
});
|
||||
|
||||
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());
|
||||
await t.test("Deleting interface", async () => deleteInterface(newConfig.name));
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user