@sourceregistry/node-wireguard 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +105 -0
- package/bin/x86_64-linux-gnu/node-wireguard.node +0 -0
- package/binding.gyp +53 -0
- package/lib/binding.d.ts +5 -0
- package/lib/binding.js +45 -0
- package/lib/index.d.ts +56 -0
- package/lib/index.js +115 -0
- package/lib/types/AllowedIP.d.ts +5 -0
- package/lib/types/AllowedIP.js +2 -0
- package/lib/types/Config.d.ts +17 -0
- package/lib/types/Config.js +2 -0
- package/lib/types/Device.d.ts +23 -0
- package/lib/types/Device.js +2 -0
- package/lib/types/Key.d.ts +5 -0
- package/lib/types/Key.js +2 -0
- package/lib/types/Peer.d.ts +22 -0
- package/lib/types/Peer.js +2 -0
- package/lib/types/PeerConfig.d.ts +24 -0
- package/lib/types/PeerConfig.js +2 -0
- package/lib/types/index.d.ts +6 -0
- package/lib/types/index.js +22 -0
- package/package.json +80 -0
- package/src/WireGuardClient.cpp +490 -0
- package/src/WireGuardClient.h +32 -0
- package/src/WireGuardTypes.h +68 -0
- package/src/crypto/Key.cpp +77 -0
- package/src/crypto/Key.h +30 -0
- package/src/helpers/Array.h +26 -0
- package/src/helpers/AsyncPromise.h +91 -0
- package/src/helpers/IfName.cpp +27 -0
- package/src/helpers/IfName.h +17 -0
- package/src/netlink/NlAttr.cpp +398 -0
- package/src/netlink/NlAttr.h +40 -0
- package/src/netlink/NlSocket.cpp +143 -0
- package/src/netlink/NlSocket.h +44 -0
- package/src/netlink/RtLink.cpp +217 -0
- package/src/netlink/RtLink.h +34 -0
- package/src/netlink/wireguard_uapi.h +68 -0
- package/src/node-wireguard.cpp +48 -0
- package/src/uapi/UapiCodec.cpp +185 -0
- package/src/uapi/UapiCodec.h +22 -0
- package/src/uapi/UapiSocket.cpp +116 -0
- package/src/uapi/UapiSocket.h +27 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Key } from './Key';
|
|
2
|
+
import { AllowedIP } from './AllowedIP';
|
|
3
|
+
/**
|
|
4
|
+
* Read-only status of one configured peer, as returned by
|
|
5
|
+
* WireGuardClient.device()/devices() (mirrors wgtypes.Peer).
|
|
6
|
+
*/
|
|
7
|
+
export interface Peer {
|
|
8
|
+
publicKey: Key;
|
|
9
|
+
/** Empty string if no preshared key is configured. */
|
|
10
|
+
presharedKey: Key;
|
|
11
|
+
/** "host:port" (IPv6 as "[host]:port"), or undefined if never connected. */
|
|
12
|
+
endpoint?: string;
|
|
13
|
+
/** Seconds; 0 = disabled. */
|
|
14
|
+
persistentKeepaliveInterval: number;
|
|
15
|
+
/** null if no handshake has occurred yet. */
|
|
16
|
+
lastHandshakeTime: Date | null;
|
|
17
|
+
receiveBytes: bigint;
|
|
18
|
+
transmitBytes: bigint;
|
|
19
|
+
allowedIPs: AllowedIP[];
|
|
20
|
+
/** 0 = most recent WireGuard protocol version. */
|
|
21
|
+
protocolVersion: number;
|
|
22
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Key } from './Key';
|
|
2
|
+
import { AllowedIP } from './AllowedIP';
|
|
3
|
+
/**
|
|
4
|
+
* Peer configuration input for WireGuardClient.configureDevice() (mirrors
|
|
5
|
+
* wgtypes.PeerConfig). Optional fields follow wgctrl-go's pointer semantics:
|
|
6
|
+
* `undefined` = leave unchanged, a present value (including 0 / '') = apply it.
|
|
7
|
+
*/
|
|
8
|
+
export interface PeerConfig {
|
|
9
|
+
/** Identifies which peer this entry applies to. Mandatory. */
|
|
10
|
+
publicKey: Key;
|
|
11
|
+
/** Remove this peer from the device's peer list. */
|
|
12
|
+
remove?: boolean;
|
|
13
|
+
/** Only apply this entry if the peer already exists on the device. */
|
|
14
|
+
updateOnly?: boolean;
|
|
15
|
+
/** undefined = unchanged; '' (all-zero key) clears the preshared key. */
|
|
16
|
+
presharedKey?: Key;
|
|
17
|
+
/** "host:port" / "[host]:port". */
|
|
18
|
+
endpoint?: string;
|
|
19
|
+
/** Seconds; undefined = unchanged, 0 = clears (disables keepalive). */
|
|
20
|
+
persistentKeepaliveInterval?: number;
|
|
21
|
+
/** Replace this peer's allowed-ips list instead of appending to it. */
|
|
22
|
+
replaceAllowedIPs?: boolean;
|
|
23
|
+
allowedIPs?: AllowedIP[];
|
|
24
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./Key"), exports);
|
|
18
|
+
__exportStar(require("./AllowedIP"), exports);
|
|
19
|
+
__exportStar(require("./Peer"), exports);
|
|
20
|
+
__exportStar(require("./Device"), exports);
|
|
21
|
+
__exportStar(require("./PeerConfig"), exports);
|
|
22
|
+
__exportStar(require("./Config"), exports);
|
package/package.json
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sourceregistry/node-wireguard",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Native N-API addon for managing WireGuard interfaces and peers via the Linux kernel netlink interface",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"author": "ProjectSource V.O.F.",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/SourceRegistry/node-wireguard.git"
|
|
10
|
+
},
|
|
11
|
+
"main": "lib/index.js",
|
|
12
|
+
"types": "lib/index.d.ts",
|
|
13
|
+
"os": [
|
|
14
|
+
"linux"
|
|
15
|
+
],
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=14.0.0"
|
|
18
|
+
},
|
|
19
|
+
"publishConfig": {
|
|
20
|
+
"access": "public"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"lib/**/*.js",
|
|
24
|
+
"lib/**/*.d.ts",
|
|
25
|
+
"binding.gyp",
|
|
26
|
+
"src",
|
|
27
|
+
"bin",
|
|
28
|
+
"LICENSE"
|
|
29
|
+
],
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build:cpp": "node-gyp rebuild",
|
|
32
|
+
"build:ts": "tsc -p tsconfig.json",
|
|
33
|
+
"build": "npm run build:cpp && npm run build:ts",
|
|
34
|
+
"test": "node --require tsx/cjs --test tests/index.ts",
|
|
35
|
+
"setup": "bash scripts/setup/setup.sh",
|
|
36
|
+
"package": "bash scripts/package/package.sh",
|
|
37
|
+
"prepublishOnly": "npm run package"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"node-addon-api": "^8.8.0"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@semantic-release/changelog": "^6.0.3",
|
|
44
|
+
"@semantic-release/commit-analyzer": "^13.0.1",
|
|
45
|
+
"@semantic-release/git": "^10.0.1",
|
|
46
|
+
"@semantic-release/npm": "^13.1.5",
|
|
47
|
+
"@semantic-release/release-notes-generator": "^14.1.0",
|
|
48
|
+
"@types/node": "^26.0.0",
|
|
49
|
+
"node-gyp": "^12.4.0",
|
|
50
|
+
"semantic-release": "^24.2.0",
|
|
51
|
+
"tsx": "^4.19.2",
|
|
52
|
+
"typescript": "^6.0.3"
|
|
53
|
+
},
|
|
54
|
+
"gypfile": true,
|
|
55
|
+
"release": {
|
|
56
|
+
"branches": [
|
|
57
|
+
"main",
|
|
58
|
+
{
|
|
59
|
+
"name": "alpha",
|
|
60
|
+
"prerelease": true
|
|
61
|
+
}
|
|
62
|
+
],
|
|
63
|
+
"plugins": [
|
|
64
|
+
"@semantic-release/commit-analyzer",
|
|
65
|
+
"@semantic-release/release-notes-generator",
|
|
66
|
+
"@semantic-release/changelog",
|
|
67
|
+
"@semantic-release/npm",
|
|
68
|
+
[
|
|
69
|
+
"@semantic-release/git",
|
|
70
|
+
{
|
|
71
|
+
"assets": [
|
|
72
|
+
"package.json",
|
|
73
|
+
"CHANGELOG.md"
|
|
74
|
+
],
|
|
75
|
+
"message": "@sourceregistry/node-wireguard(release): 🚀 v${nextRelease.version}\n\n${nextRelease.notes}"
|
|
76
|
+
}
|
|
77
|
+
]
|
|
78
|
+
]
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
#include "WireGuardClient.h"
|
|
2
|
+
#include "WireGuardTypes.h"
|
|
3
|
+
#include "crypto/Key.h"
|
|
4
|
+
#include "helpers/AsyncPromise.h"
|
|
5
|
+
#include "netlink/NlAttr.h"
|
|
6
|
+
#include "netlink/RtLink.h"
|
|
7
|
+
#include "uapi/UapiCodec.h"
|
|
8
|
+
#include "uapi/UapiSocket.h"
|
|
9
|
+
|
|
10
|
+
extern "C" {
|
|
11
|
+
#include <libmnl/libmnl.h>
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
#include <algorithm>
|
|
15
|
+
#include <cmath>
|
|
16
|
+
#include <stdexcept>
|
|
17
|
+
#include <unordered_set>
|
|
18
|
+
#include <vector>
|
|
19
|
+
|
|
20
|
+
namespace {
|
|
21
|
+
|
|
22
|
+
// Rejects, rather than silently truncating, a JS number that isn't an
|
|
23
|
+
// integer in [0, 65535] - Uint32Value()/static_cast<uint16_t> would otherwise
|
|
24
|
+
// wrap e.g. 70000 or -1 into an unrelated, valid-looking port/interval.
|
|
25
|
+
uint16_t RequireUint16(const Napi::Value &v, const char *field) {
|
|
26
|
+
double d = v.As<Napi::Number>().DoubleValue();
|
|
27
|
+
if (std::isnan(d) || std::floor(d) != d || d < 0 || d > 65535) {
|
|
28
|
+
throw std::invalid_argument(std::string(field) + " must be an integer in 0..65535");
|
|
29
|
+
}
|
|
30
|
+
return static_cast<uint16_t>(d);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Same rationale as RequireUint16 - Uint32Value() does an ECMAScript ToUint32
|
|
34
|
+
// conversion, which wraps negatives and non-integers instead of rejecting them.
|
|
35
|
+
uint32_t RequireUint32(const Napi::Value &v, const char *field) {
|
|
36
|
+
double d = v.As<Napi::Number>().DoubleValue();
|
|
37
|
+
if (std::isnan(d) || std::floor(d) != d || d < 0 || d > 4294967295.0) {
|
|
38
|
+
throw std::invalid_argument(std::string(field) + " must be an integer in 0..4294967295");
|
|
39
|
+
}
|
|
40
|
+
return static_cast<uint32_t>(d);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
std::string RequireString(const Napi::Object &obj, const char *key) {
|
|
44
|
+
Napi::Value v = obj.Get(key);
|
|
45
|
+
if (!v.IsString()) {
|
|
46
|
+
throw std::invalid_argument(std::string(key) + " must be a string");
|
|
47
|
+
}
|
|
48
|
+
return v.As<Napi::String>().Utf8Value();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Reads info[idx] as a string into `out`. Returns false (leaving `out`
|
|
52
|
+
// untouched) if the argument is missing or not a string, so callers can
|
|
53
|
+
// reject the returned Promise instead of letting a failed N-API cast
|
|
54
|
+
// (NAPI_DISABLE_CPP_EXCEPTIONS is set - see binding.gyp) throw synchronously
|
|
55
|
+
// from a method that's documented to always return a Promise.
|
|
56
|
+
bool GetStringArg(const Napi::CallbackInfo &info, size_t idx, std::string &out) {
|
|
57
|
+
if (info.Length() <= idx || !info[idx].IsString()) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
out = info[idx].As<Napi::String>().Utf8Value();
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
Napi::Promise RejectPromise(Napi::Env env, const std::string &message) {
|
|
65
|
+
auto deferred = Napi::Promise::Deferred::New(env);
|
|
66
|
+
deferred.Reject(Napi::Error::New(env, message).Value());
|
|
67
|
+
return deferred.Promise();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// --- native -> JS -------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
Napi::Object PeerToJs(Napi::Env env, const wg::Peer &peer) {
|
|
73
|
+
Napi::Object obj = Napi::Object::New(env);
|
|
74
|
+
obj.Set("publicKey", crypto::KeyToBase64(peer.publicKey));
|
|
75
|
+
// The kernel/UAPI always report this attribute, even when unset (as an
|
|
76
|
+
// all-zero key) - normalize that to '' to match the documented "no preshared key" case.
|
|
77
|
+
bool hasPsk = peer.presharedKey && !crypto::IsZeroKey(*peer.presharedKey);
|
|
78
|
+
obj.Set("presharedKey", hasPsk ? crypto::KeyToBase64(*peer.presharedKey) : std::string());
|
|
79
|
+
if (peer.endpoint) {
|
|
80
|
+
obj.Set("endpoint", *peer.endpoint);
|
|
81
|
+
} else {
|
|
82
|
+
obj.Set("endpoint", env.Undefined());
|
|
83
|
+
}
|
|
84
|
+
obj.Set("persistentKeepaliveInterval", peer.persistentKeepaliveInterval);
|
|
85
|
+
if (peer.lastHandshakeTimeSec > 0) {
|
|
86
|
+
obj.Set("lastHandshakeTime", Napi::Date::New(env, static_cast<double>(peer.lastHandshakeTimeSec) * 1000.0));
|
|
87
|
+
} else {
|
|
88
|
+
obj.Set("lastHandshakeTime", env.Null());
|
|
89
|
+
}
|
|
90
|
+
obj.Set("receiveBytes", Napi::BigInt::New(env, peer.receiveBytes));
|
|
91
|
+
obj.Set("transmitBytes", Napi::BigInt::New(env, peer.transmitBytes));
|
|
92
|
+
|
|
93
|
+
Napi::Array ips = Napi::Array::New(env, peer.allowedIPs.size());
|
|
94
|
+
for (size_t i = 0; i < peer.allowedIPs.size(); i++) {
|
|
95
|
+
ips.Set(static_cast<uint32_t>(i), netlink::FormatCIDR(peer.allowedIPs[i]));
|
|
96
|
+
}
|
|
97
|
+
obj.Set("allowedIPs", ips);
|
|
98
|
+
obj.Set("protocolVersion", peer.protocolVersion);
|
|
99
|
+
return obj;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
Napi::Object DeviceToJs(Napi::Env env, const wg::Device &dev) {
|
|
103
|
+
Napi::Object obj = Napi::Object::New(env);
|
|
104
|
+
obj.Set("name", dev.name);
|
|
105
|
+
obj.Set("type", dev.userspace ? "userspace" : "linux-kernel");
|
|
106
|
+
obj.Set("privateKey", dev.privateKey ? crypto::KeyToBase64(*dev.privateKey) : std::string());
|
|
107
|
+
obj.Set("publicKey", dev.publicKey ? crypto::KeyToBase64(*dev.publicKey) : std::string());
|
|
108
|
+
obj.Set("listenPort", dev.listenPort);
|
|
109
|
+
obj.Set("firewallMark", dev.firewallMark);
|
|
110
|
+
|
|
111
|
+
Napi::Array peers = Napi::Array::New(env, dev.peers.size());
|
|
112
|
+
for (size_t i = 0; i < dev.peers.size(); i++) {
|
|
113
|
+
peers.Set(static_cast<uint32_t>(i), PeerToJs(env, dev.peers[i]));
|
|
114
|
+
}
|
|
115
|
+
obj.Set("peers", peers);
|
|
116
|
+
return obj;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// --- JS -> native --------------------------------------------------------
|
|
120
|
+
|
|
121
|
+
wg::PeerConfig PeerConfigFromJs(const Napi::Object &obj) {
|
|
122
|
+
wg::PeerConfig pc;
|
|
123
|
+
pc.publicKey = crypto::KeyFromBase64(RequireString(obj, "publicKey"));
|
|
124
|
+
|
|
125
|
+
if (obj.Has("remove") && obj.Get("remove").IsBoolean()) {
|
|
126
|
+
pc.remove = obj.Get("remove").As<Napi::Boolean>().Value();
|
|
127
|
+
}
|
|
128
|
+
if (obj.Has("updateOnly") && obj.Get("updateOnly").IsBoolean()) {
|
|
129
|
+
pc.updateOnly = obj.Get("updateOnly").As<Napi::Boolean>().Value();
|
|
130
|
+
}
|
|
131
|
+
if (obj.Has("presharedKey") && obj.Get("presharedKey").IsString()) {
|
|
132
|
+
pc.presharedKey = crypto::KeyFromBase64(RequireString(obj, "presharedKey"));
|
|
133
|
+
}
|
|
134
|
+
if (obj.Has("endpoint") && obj.Get("endpoint").IsString()) {
|
|
135
|
+
pc.endpoint = obj.Get("endpoint").As<Napi::String>().Utf8Value();
|
|
136
|
+
}
|
|
137
|
+
if (obj.Has("persistentKeepaliveInterval") && obj.Get("persistentKeepaliveInterval").IsNumber()) {
|
|
138
|
+
pc.persistentKeepaliveInterval =
|
|
139
|
+
RequireUint16(obj.Get("persistentKeepaliveInterval"), "persistentKeepaliveInterval");
|
|
140
|
+
}
|
|
141
|
+
if (obj.Has("replaceAllowedIPs") && obj.Get("replaceAllowedIPs").IsBoolean()) {
|
|
142
|
+
pc.replaceAllowedIPs = obj.Get("replaceAllowedIPs").As<Napi::Boolean>().Value();
|
|
143
|
+
}
|
|
144
|
+
if (obj.Has("allowedIPs") && obj.Get("allowedIPs").IsArray()) {
|
|
145
|
+
Napi::Array arr = obj.Get("allowedIPs").As<Napi::Array>();
|
|
146
|
+
for (uint32_t i = 0; i < arr.Length(); i++) {
|
|
147
|
+
Napi::Value item = arr.Get(i);
|
|
148
|
+
if (!item.IsString()) {
|
|
149
|
+
throw std::invalid_argument("allowedIPs entries must be strings");
|
|
150
|
+
}
|
|
151
|
+
pc.allowedIPs.push_back(netlink::ParseCIDR(item.As<Napi::String>().Utf8Value()));
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return pc;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
wg::Config ConfigFromJs(const Napi::Object &obj) {
|
|
158
|
+
wg::Config cfg;
|
|
159
|
+
if (obj.Has("privateKey") && obj.Get("privateKey").IsString()) {
|
|
160
|
+
cfg.privateKey = crypto::KeyFromBase64(RequireString(obj, "privateKey"));
|
|
161
|
+
}
|
|
162
|
+
if (obj.Has("listenPort") && obj.Get("listenPort").IsNumber()) {
|
|
163
|
+
cfg.listenPort = RequireUint16(obj.Get("listenPort"), "listenPort");
|
|
164
|
+
}
|
|
165
|
+
if (obj.Has("firewallMark") && obj.Get("firewallMark").IsNumber()) {
|
|
166
|
+
cfg.firewallMark = RequireUint32(obj.Get("firewallMark"), "firewallMark");
|
|
167
|
+
}
|
|
168
|
+
if (obj.Has("replacePeers") && obj.Get("replacePeers").IsBoolean()) {
|
|
169
|
+
cfg.replacePeers = obj.Get("replacePeers").As<Napi::Boolean>().Value();
|
|
170
|
+
}
|
|
171
|
+
if (obj.Has("peers") && obj.Get("peers").IsArray()) {
|
|
172
|
+
Napi::Array arr = obj.Get("peers").As<Napi::Array>();
|
|
173
|
+
for (uint32_t i = 0; i < arr.Length(); i++) {
|
|
174
|
+
Napi::Value item = arr.Get(i);
|
|
175
|
+
if (!item.IsObject()) {
|
|
176
|
+
throw std::invalid_argument("peers entries must be objects");
|
|
177
|
+
}
|
|
178
|
+
cfg.peers.push_back(PeerConfigFromJs(item.As<Napi::Object>()));
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return cfg;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Runs one WG_CMD_GET_DEVICE dump for `name` against `sock` and returns the
|
|
185
|
+
// assembled Device. Must only be called from a PromiseWorker's Execute()
|
|
186
|
+
// (background thread) - touches no Napi:: types.
|
|
187
|
+
wg::Device FetchDeviceKernel(netlink::NlSocket &sock, const std::string &name) {
|
|
188
|
+
uint16_t familyId = sock.WireGuardFamilyId();
|
|
189
|
+
std::vector<char> buf(MNL_SOCKET_BUFFER_SIZE);
|
|
190
|
+
unsigned int seq = sock.NextSeq();
|
|
191
|
+
auto *nlh = netlink::BuildGetDeviceMessage(buf.data(), familyId, seq, name);
|
|
192
|
+
|
|
193
|
+
wg::Device device;
|
|
194
|
+
device.name = name;
|
|
195
|
+
sock.SendAndReceive(nlh, [&](const struct nlmsghdr *reply) {
|
|
196
|
+
netlink::ParseDeviceMessage(reply, device);
|
|
197
|
+
return true;
|
|
198
|
+
});
|
|
199
|
+
return device;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Fetches one device, dispatching to the UAPI socket backend (userspace
|
|
203
|
+
// implementations like wireguard-go) if one is present for `name`, otherwise
|
|
204
|
+
// the kernel netlink backend. Must only be called off the JS thread.
|
|
205
|
+
wg::Device FetchDevice(netlink::NlSocket &sock, const std::string &name) {
|
|
206
|
+
if (uapi::HasSocket(name)) {
|
|
207
|
+
std::string response = uapi::Transact(name, uapi::BuildGetRequest());
|
|
208
|
+
return uapi::ParseGetResponse(name, response);
|
|
209
|
+
}
|
|
210
|
+
return FetchDeviceKernel(sock, name);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Applies `cfg` to `name`, dispatching to the UAPI socket backend if present,
|
|
214
|
+
// otherwise kernel netlink. Must only be called off the JS thread.
|
|
215
|
+
void ApplyConfig(netlink::NlSocket &sock, const std::string &name, const wg::Config &cfg) {
|
|
216
|
+
if (uapi::HasSocket(name)) {
|
|
217
|
+
std::string response = uapi::Transact(name, uapi::BuildSetRequest(cfg));
|
|
218
|
+
uapi::ParseSetResponse(response);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
uint16_t familyId = sock.WireGuardFamilyId();
|
|
223
|
+
size_t bufSize = std::max<size_t>(MNL_SOCKET_BUFFER_SIZE, netlink::EstimateSetDeviceMessageSize(cfg));
|
|
224
|
+
std::vector<char> buf(bufSize);
|
|
225
|
+
unsigned int seq = sock.NextSeq();
|
|
226
|
+
auto *nlh = netlink::BuildSetDeviceMessage(buf.data(), familyId, seq, name, cfg);
|
|
227
|
+
sock.SendAndReceive(nlh, [](const struct nlmsghdr *) { return true; });
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
} // namespace
|
|
231
|
+
|
|
232
|
+
Napi::Object WireGuardClient::Init(Napi::Env env, Napi::Object exports) {
|
|
233
|
+
Napi::Function func = DefineClass(env, "WireGuardClient", {
|
|
234
|
+
InstanceMethod("createDevice", &WireGuardClient::CreateDevice),
|
|
235
|
+
InstanceMethod("deleteDevice", &WireGuardClient::DeleteDevice),
|
|
236
|
+
InstanceMethod("devices", &WireGuardClient::Devices),
|
|
237
|
+
InstanceMethod("device", &WireGuardClient::Device),
|
|
238
|
+
InstanceMethod("configureDevice", &WireGuardClient::ConfigureDevice),
|
|
239
|
+
InstanceMethod("setUp", &WireGuardClient::SetUp),
|
|
240
|
+
InstanceMethod("setDown", &WireGuardClient::SetDown),
|
|
241
|
+
InstanceMethod("setAddress", &WireGuardClient::SetAddress),
|
|
242
|
+
InstanceMethod("deleteAddress", &WireGuardClient::DeleteAddress),
|
|
243
|
+
InstanceMethod("close", &WireGuardClient::Close),
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
exports.Set("WireGuardClient", func);
|
|
247
|
+
return exports;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
WireGuardClient::WireGuardClient(const Napi::CallbackInfo &info) : Napi::ObjectWrap<WireGuardClient>(info) {
|
|
251
|
+
Napi::Env env = info.Env();
|
|
252
|
+
try {
|
|
253
|
+
sock_ = std::make_shared<netlink::NlSocket>();
|
|
254
|
+
} catch (const std::exception &e) {
|
|
255
|
+
Napi::Error::New(env, e.what()).ThrowAsJavaScriptException();
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
Napi::Value WireGuardClient::CreateDevice(const Napi::CallbackInfo &info) {
|
|
260
|
+
Napi::Env env = info.Env();
|
|
261
|
+
if (!sock_) {
|
|
262
|
+
// Reject (not throw synchronously) - every other method here returns a
|
|
263
|
+
// Promise, and a sync throw from a Promise-returning method breaks
|
|
264
|
+
// callers using `await`/`assert.rejects` against it.
|
|
265
|
+
auto deferred = Napi::Promise::Deferred::New(env);
|
|
266
|
+
deferred.Reject(Napi::Error::New(env, "client is closed").Value());
|
|
267
|
+
return deferred.Promise();
|
|
268
|
+
}
|
|
269
|
+
std::string name;
|
|
270
|
+
if (!GetStringArg(info, 0, name)) {
|
|
271
|
+
return RejectPromise(env, "expected interface name (string) as argument 0");
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
auto *worker = new helpers::PromiseWorker(env, [name]() { netlink::CreateWireGuardLink(name); });
|
|
275
|
+
worker->Queue();
|
|
276
|
+
return worker->Promise();
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
Napi::Value WireGuardClient::DeleteDevice(const Napi::CallbackInfo &info) {
|
|
280
|
+
Napi::Env env = info.Env();
|
|
281
|
+
if (!sock_) {
|
|
282
|
+
// Reject (not throw synchronously) - every other method here returns a
|
|
283
|
+
// Promise, and a sync throw from a Promise-returning method breaks
|
|
284
|
+
// callers using `await`/`assert.rejects` against it.
|
|
285
|
+
auto deferred = Napi::Promise::Deferred::New(env);
|
|
286
|
+
deferred.Reject(Napi::Error::New(env, "client is closed").Value());
|
|
287
|
+
return deferred.Promise();
|
|
288
|
+
}
|
|
289
|
+
std::string name;
|
|
290
|
+
if (!GetStringArg(info, 0, name)) {
|
|
291
|
+
return RejectPromise(env, "expected interface name (string) as argument 0");
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
auto *worker = new helpers::PromiseWorker(env, [name]() { netlink::DeleteLink(name); });
|
|
295
|
+
worker->Queue();
|
|
296
|
+
return worker->Promise();
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
Napi::Value WireGuardClient::Devices(const Napi::CallbackInfo &info) {
|
|
300
|
+
Napi::Env env = info.Env();
|
|
301
|
+
if (!sock_) {
|
|
302
|
+
// Reject (not throw synchronously) - every other method here returns a
|
|
303
|
+
// Promise, and a sync throw from a Promise-returning method breaks
|
|
304
|
+
// callers using `await`/`assert.rejects` against it.
|
|
305
|
+
auto deferred = Napi::Promise::Deferred::New(env);
|
|
306
|
+
deferred.Reject(Napi::Error::New(env, "client is closed").Value());
|
|
307
|
+
return deferred.Promise();
|
|
308
|
+
}
|
|
309
|
+
auto sock = sock_;
|
|
310
|
+
auto results = std::make_shared<std::vector<wg::Device>>();
|
|
311
|
+
|
|
312
|
+
auto *worker = new helpers::PromiseWorker(
|
|
313
|
+
env,
|
|
314
|
+
[sock, results]() {
|
|
315
|
+
// A name can't be both kernel- and UAPI-backed (the kernel module's
|
|
316
|
+
// /sys/class/net/<name>/wireguard marker and a userspace daemon's
|
|
317
|
+
// socket are mutually exclusive in practice), so a plain set union
|
|
318
|
+
// is enough - no risk of double-fetching the same interface.
|
|
319
|
+
std::unordered_set<std::string> names;
|
|
320
|
+
for (auto &name : netlink::ListWireGuardInterfaceNames()) {
|
|
321
|
+
names.insert(std::move(name));
|
|
322
|
+
}
|
|
323
|
+
for (auto &name : uapi::ListInterfaceNames()) {
|
|
324
|
+
names.insert(std::move(name));
|
|
325
|
+
}
|
|
326
|
+
for (const auto &name : names) {
|
|
327
|
+
results->push_back(FetchDevice(*sock, name));
|
|
328
|
+
}
|
|
329
|
+
},
|
|
330
|
+
[results](Napi::Env resolveEnv) -> Napi::Value {
|
|
331
|
+
Napi::Array arr = Napi::Array::New(resolveEnv, results->size());
|
|
332
|
+
for (size_t i = 0; i < results->size(); i++) {
|
|
333
|
+
arr.Set(static_cast<uint32_t>(i), DeviceToJs(resolveEnv, (*results)[i]));
|
|
334
|
+
}
|
|
335
|
+
return arr;
|
|
336
|
+
});
|
|
337
|
+
worker->Queue();
|
|
338
|
+
return worker->Promise();
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
Napi::Value WireGuardClient::Device(const Napi::CallbackInfo &info) {
|
|
342
|
+
Napi::Env env = info.Env();
|
|
343
|
+
if (!sock_) {
|
|
344
|
+
// Reject (not throw synchronously) - every other method here returns a
|
|
345
|
+
// Promise, and a sync throw from a Promise-returning method breaks
|
|
346
|
+
// callers using `await`/`assert.rejects` against it.
|
|
347
|
+
auto deferred = Napi::Promise::Deferred::New(env);
|
|
348
|
+
deferred.Reject(Napi::Error::New(env, "client is closed").Value());
|
|
349
|
+
return deferred.Promise();
|
|
350
|
+
}
|
|
351
|
+
std::string name;
|
|
352
|
+
if (!GetStringArg(info, 0, name)) {
|
|
353
|
+
return RejectPromise(env, "expected interface name (string) as argument 0");
|
|
354
|
+
}
|
|
355
|
+
auto sock = sock_;
|
|
356
|
+
auto result = std::make_shared<wg::Device>();
|
|
357
|
+
|
|
358
|
+
auto *worker = new helpers::PromiseWorker(
|
|
359
|
+
env,
|
|
360
|
+
[sock, name, result]() { *result = FetchDevice(*sock, name); },
|
|
361
|
+
[result](Napi::Env resolveEnv) -> Napi::Value { return DeviceToJs(resolveEnv, *result); });
|
|
362
|
+
worker->Queue();
|
|
363
|
+
return worker->Promise();
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
Napi::Value WireGuardClient::ConfigureDevice(const Napi::CallbackInfo &info) {
|
|
367
|
+
Napi::Env env = info.Env();
|
|
368
|
+
if (!sock_) {
|
|
369
|
+
// Reject (not throw synchronously) - every other method here returns a
|
|
370
|
+
// Promise, and a sync throw from a Promise-returning method breaks
|
|
371
|
+
// callers using `await`/`assert.rejects` against it.
|
|
372
|
+
auto deferred = Napi::Promise::Deferred::New(env);
|
|
373
|
+
deferred.Reject(Napi::Error::New(env, "client is closed").Value());
|
|
374
|
+
return deferred.Promise();
|
|
375
|
+
}
|
|
376
|
+
std::string name;
|
|
377
|
+
if (!GetStringArg(info, 0, name)) {
|
|
378
|
+
return RejectPromise(env, "expected interface name (string) as argument 0");
|
|
379
|
+
}
|
|
380
|
+
if (info.Length() <= 1 || !info[1].IsObject()) {
|
|
381
|
+
return RejectPromise(env, "expected config (object) as argument 1");
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
wg::Config cfg;
|
|
385
|
+
try {
|
|
386
|
+
// Must parse here (main thread) - Napi types are unsafe to touch off-thread.
|
|
387
|
+
// ConfigFromJs/PeerConfigFromJs/ParseCIDR/KeyFromBase64 can throw on bad
|
|
388
|
+
// input (e.g. out-of-range CIDR mask). configureDevice() is documented as
|
|
389
|
+
// Promise-returning, so this must reject rather than throw synchronously
|
|
390
|
+
// (and rejecting, not throwing, is what an uncaught C++ exception across
|
|
391
|
+
// the N-API boundary would otherwise risk turning into a process crash).
|
|
392
|
+
cfg = ConfigFromJs(info[1].As<Napi::Object>());
|
|
393
|
+
} catch (const std::exception &e) {
|
|
394
|
+
auto deferred = Napi::Promise::Deferred::New(env);
|
|
395
|
+
deferred.Reject(Napi::Error::New(env, e.what()).Value());
|
|
396
|
+
return deferred.Promise();
|
|
397
|
+
}
|
|
398
|
+
auto sock = sock_;
|
|
399
|
+
|
|
400
|
+
auto *worker = new helpers::PromiseWorker(env, [sock, name, cfg]() { ApplyConfig(*sock, name, cfg); });
|
|
401
|
+
worker->Queue();
|
|
402
|
+
return worker->Promise();
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
Napi::Value WireGuardClient::SetUp(const Napi::CallbackInfo &info) {
|
|
406
|
+
Napi::Env env = info.Env();
|
|
407
|
+
if (!sock_) {
|
|
408
|
+
// Reject (not throw synchronously) - every other method here returns a
|
|
409
|
+
// Promise, and a sync throw from a Promise-returning method breaks
|
|
410
|
+
// callers using `await`/`assert.rejects` against it.
|
|
411
|
+
auto deferred = Napi::Promise::Deferred::New(env);
|
|
412
|
+
deferred.Reject(Napi::Error::New(env, "client is closed").Value());
|
|
413
|
+
return deferred.Promise();
|
|
414
|
+
}
|
|
415
|
+
std::string name;
|
|
416
|
+
if (!GetStringArg(info, 0, name)) {
|
|
417
|
+
return RejectPromise(env, "expected interface name (string) as argument 0");
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
auto *worker = new helpers::PromiseWorker(env, [name]() { netlink::SetLinkUp(name, true); });
|
|
421
|
+
worker->Queue();
|
|
422
|
+
return worker->Promise();
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
Napi::Value WireGuardClient::SetDown(const Napi::CallbackInfo &info) {
|
|
426
|
+
Napi::Env env = info.Env();
|
|
427
|
+
if (!sock_) {
|
|
428
|
+
// Reject (not throw synchronously) - every other method here returns a
|
|
429
|
+
// Promise, and a sync throw from a Promise-returning method breaks
|
|
430
|
+
// callers using `await`/`assert.rejects` against it.
|
|
431
|
+
auto deferred = Napi::Promise::Deferred::New(env);
|
|
432
|
+
deferred.Reject(Napi::Error::New(env, "client is closed").Value());
|
|
433
|
+
return deferred.Promise();
|
|
434
|
+
}
|
|
435
|
+
std::string name;
|
|
436
|
+
if (!GetStringArg(info, 0, name)) {
|
|
437
|
+
return RejectPromise(env, "expected interface name (string) as argument 0");
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
auto *worker = new helpers::PromiseWorker(env, [name]() { netlink::SetLinkUp(name, false); });
|
|
441
|
+
worker->Queue();
|
|
442
|
+
return worker->Promise();
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
Napi::Value WireGuardClient::SetAddress(const Napi::CallbackInfo &info) {
|
|
446
|
+
Napi::Env env = info.Env();
|
|
447
|
+
if (!sock_) {
|
|
448
|
+
// Reject (not throw synchronously) - every other method here returns a
|
|
449
|
+
// Promise, and a sync throw from a Promise-returning method breaks
|
|
450
|
+
// callers using `await`/`assert.rejects` against it.
|
|
451
|
+
auto deferred = Napi::Promise::Deferred::New(env);
|
|
452
|
+
deferred.Reject(Napi::Error::New(env, "client is closed").Value());
|
|
453
|
+
return deferred.Promise();
|
|
454
|
+
}
|
|
455
|
+
std::string name;
|
|
456
|
+
std::string cidr;
|
|
457
|
+
if (!GetStringArg(info, 0, name) || !GetStringArg(info, 1, cidr)) {
|
|
458
|
+
return RejectPromise(env, "expected interface name and cidr (strings)");
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
auto *worker = new helpers::PromiseWorker(env, [name, cidr]() { netlink::AddAddress(name, cidr); });
|
|
462
|
+
worker->Queue();
|
|
463
|
+
return worker->Promise();
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
Napi::Value WireGuardClient::DeleteAddress(const Napi::CallbackInfo &info) {
|
|
467
|
+
Napi::Env env = info.Env();
|
|
468
|
+
if (!sock_) {
|
|
469
|
+
// Reject (not throw synchronously) - every other method here returns a
|
|
470
|
+
// Promise, and a sync throw from a Promise-returning method breaks
|
|
471
|
+
// callers using `await`/`assert.rejects` against it.
|
|
472
|
+
auto deferred = Napi::Promise::Deferred::New(env);
|
|
473
|
+
deferred.Reject(Napi::Error::New(env, "client is closed").Value());
|
|
474
|
+
return deferred.Promise();
|
|
475
|
+
}
|
|
476
|
+
std::string name;
|
|
477
|
+
std::string cidr;
|
|
478
|
+
if (!GetStringArg(info, 0, name) || !GetStringArg(info, 1, cidr)) {
|
|
479
|
+
return RejectPromise(env, "expected interface name and cidr (strings)");
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
auto *worker = new helpers::PromiseWorker(env, [name, cidr]() { netlink::DeleteAddress(name, cidr); });
|
|
483
|
+
worker->Queue();
|
|
484
|
+
return worker->Promise();
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
Napi::Value WireGuardClient::Close(const Napi::CallbackInfo &info) {
|
|
488
|
+
sock_.reset();
|
|
489
|
+
return info.Env().Undefined();
|
|
490
|
+
}
|