Big code refactoring #10

Merged
Sirherobrine23 merged 15 commits from code_refactoring into main 2024-03-15 04:08:27 +00:00
7 changed files with 385 additions and 134 deletions
Showing only changes of commit 8bae917f40 - Show all commits

56
.github/workflows/test.yaml vendored Normal file

@ -0,0 +1,56 @@
name: Test
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
test_host:
name: Test Host
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up latest Node.js
uses: actions/setup-node@v4
with:
node-version: "latest"
- name: Install build dependencies
run: sudo apt update && sudo apt install -y build-essential
- 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
test_matrix:
name: Test Matrix
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [ 16.x, 18.x, 20.x ]
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- name: "Setup zig"
uses: korandoru/setup-zig@v1
with:
zig-version: "master"
- 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

@ -9,7 +9,9 @@
"${workspaceFolder}/**", "${workspaceFolder}/**",
"${workspaceFolder}/addon" "${workspaceFolder}/addon"
], ],
"defines": [], "defines": [
"AF_NETLINK=16"
],
"macFrameworkPath": [ "macFrameworkPath": [
"/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/System/Library/Frameworks" "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/System/Library/Frameworks"
], ],

@ -21,9 +21,24 @@ extern "C" {
#include <iostream> #include <iostream>
#include <sys/ioctl.h> #include <sys/ioctl.h>
#include <net/if.h> #include <net/if.h>
#include <filesystem>
#include <fstream>
std::string getWireguardVersion() { std::string getWireguardVersion() {
return "0.0.0"; #ifdef WIREGUARD_VERSION
return std::string(WIREGUARD_VERSION);
#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");
file >> version;
file.close();
}
return version;
} }
std::string driveLoad(std::map<std::string, std::string> load) {} std::string driveLoad(std::map<std::string, std::string> load) {}

@ -36,16 +36,16 @@
"install": "rebory prebuild", "install": "rebory prebuild",
"dev": "rebory build", "dev": "rebory build",
"test": "rebory build && node --no-warnings --loader ts-node/esm src/index_test.js", "test": "rebory build && node --no-warnings --loader ts-node/esm src/index_test.js",
"prepack": "tsc --build --clean && tsc --build", "prepack": "tsc --build --clean && tsc --build && rebory build --release",
"postpack": "tsc --build --clean" "postpack": "tsc --build --clean"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^20.11.19", "@types/node": "^20.11.26",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"typescript": "^5.3.3" "typescript": "^5.4.2"
}, },
"dependencies": { "dependencies": {
"node-addon-api": "^7.1.0", "node-addon-api": "^8.0.0",
"rebory": "^0.2.4" "rebory": "^0.2.4"
} }
} }

@ -1,8 +1,7 @@
import path from "node:path"; import path from "node:path";
import net from "node:net";
import readline from "node:readline";
import { loadAddon } from "rebory"; import { loadAddon } from "rebory";
import { key } from "./index.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)); const __dirname = import.meta.dirname || path.dirname((await import("node:url")).fileURLToPath(import.meta.url));
interface Peer { interface Peer {
@ -65,19 +64,37 @@ export interface SetConfig extends Config<SetPeer> {
}; };
export const addon = (await loadAddon(path.resolve(__dirname, "../binding.yaml"))).wginterface.load_addon<{ export const addon = (await loadAddon(path.resolve(__dirname, "../binding.yaml"))).wginterface.load_addon<{
/** Current Wireguard drive version */
driveVersion?: string; driveVersion?: string;
/**
* Delete interface if exists
* @param name - Interface name
*/
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[]>; listDevices(): Promise<string[]>;
/**
* Get current config from Wireguard interface
* @param name - Interface name
*/
getConfig(name: string): Promise<GetConfig>; getConfig(name: string): Promise<GetConfig>;
/**
* Set new config to Wireguard interface or create new interface if not exists
* @param config - Interface config
*/
setConfig(config: SetConfig): Promise<void>; setConfig(config: SetConfig): Promise<void>;
}>({ }>({
WIN32DLLPATH: path.resolve(__dirname, "../addon/win", (process.arch === "x64" && "amd64") || (process.arch === "ia32" && "x86") || process.arch, "wireguard.dll") WIN32DLLPATH: path.resolve(__dirname, "../addon/win", (process.arch === "x64" && "amd64") || (process.arch === "ia32" && "x86") || process.arch, "wireguard.dll")
}); });
/**
* Wireguard interface to kernel level
*/
export namespace Kernel {
export const { export const {
driveVersion, driveVersion,
listDevices, listDevices,
@ -85,131 +102,292 @@ export namespace Kernel {
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;
}
/** /**
* Wireguard userspace (wireguard-go) * 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.
*/ */
export namespace Userspace { setPresharedKey(): Promise<this> & this;
const { userspace } = addon; /**
export const { * Sets the preshared key for the peer.
driveVersion, * @param presharedKey - The preshared key to set. If not provided, a new preshared key will be generated.
createTunel, * @returns The updated WireGuard interface object.
deleteTunel, * @throws {Error} If the provided preshared key is invalid.
checkTunel, */
listTunels, setPresharedKey(presharedKey: string): this;
} = userspace || {}; /**
}; * 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 { export class Wireguard {
address = new Set<string>; constructor(config?: SetConfig | GetConfig | Config<Peer>) {
// super({});
#_fwmark: number = 0; if (!config) return;
if (typeof config === "object") {
#_portListen: number = 0; if (config instanceof Wireguard) return config;
}
#_privateKey: string;
set privateKey(value: string) {
this.#_privateKey = value;
} }
get privateKey() { private _name: string;
return this.#_privateKey; get name() {
return this._name;
} }
set publicKey(_key: string) {} /**
get publicKey() { * Set Wireguard interface name
return key.publicKey(this.#_privateKey); * @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;
} }
#_replacePeers?: boolean; private _portListen: number;
set replacePeers(value: boolean) { /**
this.#_replacePeers = !!value; * Sets the port to listen on.
} * @param port - The port number to listen on.
get replacePeers() { * @returns The current instance of the `Wireguard` class.
return this.#_replacePeers; * @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;
} }
#_peers = new Map<string, SetPeer>; private _fwmark: number;
addPeer(publicKey: string, value: SetPeer) {
this.#_peers.set(publicKey, value); /**
* 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) { removePeer(publicKey: string) {
this.#_peers.delete(publicKey); if (this._peers) this._peers.delete(publicKey);
return this;
} }
toJSON() {
const config: Omit<SetConfig, "name"> = Object.create({});
config.privateKey = this.#_privateKey;
config.publicKey = this.publicKey;
config.portListen = this.#_portListen;
config.fwmark = this.#_fwmark;
config.address = Array.from(this.address);
config.peers = Object.create({});
for (const [key, value] of this.#_peers) config.peers[key] = value; /**
* 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; return config;
} }
async setConfig(name: string): Promise<void> { /**
if (!!Kernel.driveVersion) { * Set new config to Wireguard interface or create new interface if not exists
return Kernel.setConfig({ name, ...this.toJSON() }); * @returns Promise<void>
} */
if (!(await Userspace.checkTunel(name))) await Userspace.createTunel(name); async deploy() {
const sockPath = (await Userspace.listTunels()).find((tun) => { let tt = tun.split("/").pop(); return (tt.endsWith(".sock") ? tt.slice(0, -5) : tt) === name; }); return setConfig({
if (!sockPath) throw new Error("Wireguard interface not found"); ...(this.toJSON()),
const sock = net.connect(sockPath); replacePeers: true,
await new Promise<void>((resolve, reject) => sock.once("connect", resolve).once("error", reject));
sock.write(`set=1\n`);
if (this.#_privateKey.length == key.Base64Length) sock.write(`\nprivate_key=${key.keyToHex(this.privateKey)}\npublic_key=${key.keyToHex(this.publicKey)}`);
if (this.#_portListen >= 0) sock.write(`\nlisten_port=${this.#_portListen}`);
if (this.#_fwmark >= 0) sock.write(`\nfwmark=${this.#_fwmark}`);
if (this.address.size > 0) sock.write(`\naddress=${Array.from(this.address).join(",")}`);
if (this.#_replacePeers) sock.write(`\nreplace_peers=1`);
for (const [key, value] of this.#_peers) {
sock.write(`\npublic_key=${key}`);
if (value.removeMe) sock.write(`\nremove=${key}`);
else {
if (value.presharedKey) sock.write(`\npreshared_key=${value.presharedKey}`);
if (value.keepInterval) sock.write(`\npersistent_keepalive_interval=${value.keepInterval}`);
if (value.endpoint) sock.write(`\nendpoint=${value.endpoint}`);
if (value.allowedIPs) sock.write(`\nallowed_ips=${value.allowedIPs.join(",")}`);
}
}
sock.end("\n\n");
return new Promise<void>((resolve, reject) => sock.once("close", () => resolve()).once("error", reject));
}
async getConfig(name: string) {
if (!!Kernel.driveVersion) return Kernel.getConfig(name);
const sockPath = (await Userspace.listTunels()).find((tun) => { let tt = tun.split("/").pop(); return (tt.endsWith(".sock") ? tt.slice(0, -5) : tt) === name; });
if (!sockPath) throw new Error("Wireguard interface not found");
const sock = net.connect(sockPath);
await new Promise<void>((resolve, reject) => sock.once("connect", resolve).once("error", reject));
const rl = readline.createInterface({ input: sock, output: sock });
let stop = [];
rl.on("line", (line): any => {
if (stop.length) {
stop.push(line);
if (line === "") sock.end();
return;
}
if (line.startsWith("errno=")) {
if (parseInt(line.slice(6)) !== 0) stop.push("");
}
console.log(line);
}); });
rl.once("close", () => {
if (stop.length) throw new Error(stop.join("\n").trim());
});
sock.write("get=1\n\n");
return null;
} }
async deleteInterface(name: string): Promise<void> { /**
if (!!Kernel.driveVersion) return Kernel.deleteInterface(name); * Deletes the WireGuard interface.
if (await Userspace.checkTunel(name)) return Userspace.deleteTunel(name); * @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; export default Wireguard;

@ -3,16 +3,16 @@ import { Wireguard } from "./wginterface.js";
import { presharedKey, privateKey, publicKey } from "./key.js"; import { presharedKey, privateKey, publicKey } from "./key.js";
await test("Wireguard interface", async t => { await test("Wireguard interface", async t => {
let wgName = "wg23";
if (process.platform === "darwin") wgName = "utun23";
const config = new Wireguard; const config = new Wireguard;
config.privateKey = await privateKey(); config.name = "wg23";
config.replacePeers = true; if (process.platform === "darwin") config.name = "utun23";
config.address.add("10.66.66.1/32");
config.address.add("fd42:42:42::1/128"); config.setPrivateKey(await privateKey());
config.addNewAddress("10.66.66.1/32");
config.addNewAddress("fd42:42:42::1/128");
const peer1 = await privateKey(); const peer1 = await privateKey();
config.addPeer(publicKey(peer1), { config.addNewPeer(publicKey(peer1), {
keepInterval: 15, keepInterval: 15,
presharedKey: await presharedKey(), presharedKey: await presharedKey(),
allowedIPs: [ allowedIPs: [
@ -21,7 +21,7 @@ await test("Wireguard interface", async t => {
}); });
const peer2 = await privateKey(); const peer2 = await privateKey();
config.addPeer(publicKey(peer2), { config.addNewPeer(publicKey(peer2), {
keepInterval: 0, keepInterval: 0,
allowedIPs: [ allowedIPs: [
"10.66.66.3/32" "10.66.66.3/32"
@ -29,11 +29,11 @@ await test("Wireguard interface", async t => {
}); });
let skip: string; let skip: string;
await t.test("Create and Set config in interface", async () => config.setConfig(wgName).catch(err => { skip = "Cannot set wireguard config"; return Promise.reject(err); })); await t.test("Create and Set config in interface", async () => config.deploy().catch(err => { skip = "Cannot set wireguard config"; return Promise.reject(err); }));
await t.test("Get config from interface", { skip }, async () => { await t.test("Get config from interface", { skip }, async () => {
const fromInterface = await config.getConfig(wgName); await config.getConfig();
console.dir(fromInterface, { depth: null }); console.dir(config.toJSON(), { depth: null });
}); });
await t.test("Delete interface if exists", { skip }, async () => config.deleteInterface(wgName)); await t.test("Delete interface if exists", { skip }, async () => config.delete());
}); });