wire-vpn 1.0.0 → 1.0.1
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/binding.gyp +32 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +195 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +5 -0
- package/dist/lib/vpn.d.ts +32 -0
- package/dist/lib/vpn.js +149 -0
- package/package/binding.gyp +32 -0
- package/package/build/Makefile +354 -0
- package/package/build/Release/.deps/Release/obj.target/wirevpn/src/addon/vpn.o.d +17 -0
- package/package/build/Release/.deps/Release/obj.target/wirevpn.node.d +1 -0
- package/package/build/Release/.deps/Release/wirevpn.node.d +1 -0
- package/package/build/Release/obj.target/wirevpn/src/addon/vpn.o +0 -0
- package/package/build/Release/obj.target/wirevpn.node +0 -0
- package/package/build/binding.Makefile +6 -0
- package/package/build/wirevpn.target.mk +157 -0
- package/package/dist/cli.d.ts +2 -0
- package/package/dist/cli.js +195 -0
- package/package/dist/index.d.ts +1 -0
- package/package/dist/index.js +5 -0
- package/package/dist/lib/vpn.d.ts +32 -0
- package/package/dist/lib/vpn.js +149 -0
- package/package/package.json +38 -0
- package/package/readme.md +555 -0
- package/package/src/addon/vpn.cc +366 -0
- package/package/src/cli.ts +185 -0
- package/package/src/index.ts +1 -0
- package/package/src/lib/vpn.ts +156 -0
- package/package/tsconfig.json +17 -0
- package/package.json +32 -7
- package/readme.md +555 -0
- package/src/addon/vpn.cc +366 -0
- package/src/cli.ts +185 -0
- package/src/index.ts +1 -0
- package/src/lib/vpn.ts +156 -0
- package/tsconfig.json +17 -0
package/src/addon/vpn.cc
ADDED
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
#include <napi.h>
|
|
2
|
+
#include <fcntl.h>
|
|
3
|
+
#include <unistd.h>
|
|
4
|
+
#include <sys/ioctl.h>
|
|
5
|
+
#include <net/if.h>
|
|
6
|
+
#include <linux/if_tun.h>
|
|
7
|
+
#include <string>
|
|
8
|
+
#include <cstring>
|
|
9
|
+
#include <vector>
|
|
10
|
+
#include <atomic>
|
|
11
|
+
#include <map>
|
|
12
|
+
#include <chrono>
|
|
13
|
+
#include <sodium.h>
|
|
14
|
+
|
|
15
|
+
class TunDevice : public Napi::ObjectWrap<TunDevice> {
|
|
16
|
+
private:
|
|
17
|
+
int tun_fd;
|
|
18
|
+
std::string iface_name;
|
|
19
|
+
|
|
20
|
+
public:
|
|
21
|
+
static Napi::Object Init(Napi::Env env, Napi::Object exports) {
|
|
22
|
+
Napi::Function func = DefineClass(env, "TunDevice", {
|
|
23
|
+
InstanceMethod<&TunDevice::Open>("open"),
|
|
24
|
+
InstanceMethod<&TunDevice::Read>("read"),
|
|
25
|
+
InstanceMethod<&TunDevice::Write>("write"),
|
|
26
|
+
InstanceMethod<&TunDevice::Configure>("configure"),
|
|
27
|
+
InstanceMethod<&TunDevice::Close>("close"),
|
|
28
|
+
InstanceMethod<&TunDevice::GetHexDump>("getHexDump"),
|
|
29
|
+
InstanceAccessor<&TunDevice::GetName, nullptr>("name")
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
Napi::FunctionReference* constructor = new Napi::FunctionReference();
|
|
33
|
+
*constructor = Napi::Persistent(func);
|
|
34
|
+
env.SetInstanceData(constructor);
|
|
35
|
+
exports.Set("TunDevice", func);
|
|
36
|
+
return exports;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
TunDevice(const Napi::CallbackInfo& info) : Napi::ObjectWrap<TunDevice>(info) {
|
|
40
|
+
tun_fd = -1;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
Napi::Value Open(const Napi::CallbackInfo& info) {
|
|
44
|
+
Napi::Env env = info.Env();
|
|
45
|
+
|
|
46
|
+
struct ifreq ifr;
|
|
47
|
+
memset(&ifr, 0, sizeof(ifr));
|
|
48
|
+
ifr.ifr_flags = IFF_TUN | IFF_NO_PI;
|
|
49
|
+
|
|
50
|
+
tun_fd = open("/dev/net/tun", O_RDWR);
|
|
51
|
+
if (tun_fd < 0) {
|
|
52
|
+
Napi::Error::New(env, "Failed to open TUN device").ThrowAsJavaScriptException();
|
|
53
|
+
return env.Null();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (ioctl(tun_fd, TUNSETIFF, &ifr) < 0) {
|
|
57
|
+
close(tun_fd);
|
|
58
|
+
Napi::Error::New(env, "Failed to configure TUN device").ThrowAsJavaScriptException();
|
|
59
|
+
return env.Null();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
iface_name = ifr.ifr_name;
|
|
63
|
+
int flags = fcntl(tun_fd, F_GETFL, 0);
|
|
64
|
+
fcntl(tun_fd, F_SETFL, flags | O_NONBLOCK);
|
|
65
|
+
|
|
66
|
+
return Napi::Number::New(env, tun_fd);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
Napi::Value Read(const Napi::CallbackInfo& info) {
|
|
70
|
+
Napi::Env env = info.Env();
|
|
71
|
+
if (tun_fd < 0) return Napi::Buffer<char>::New(env, 0);
|
|
72
|
+
|
|
73
|
+
char buffer[65536];
|
|
74
|
+
ssize_t n = read(tun_fd, buffer, sizeof(buffer));
|
|
75
|
+
if (n > 0) return Napi::Buffer<char>::Copy(env, buffer, n);
|
|
76
|
+
return Napi::Buffer<char>::New(env, 0);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
Napi::Value Write(const Napi::CallbackInfo& info) {
|
|
80
|
+
Napi::Env env = info.Env();
|
|
81
|
+
if (tun_fd < 0 || info.Length() < 1) return Napi::Number::New(env, -1);
|
|
82
|
+
|
|
83
|
+
Napi::Buffer<char> buffer = info[0].As<Napi::Buffer<char>>();
|
|
84
|
+
ssize_t n = write(tun_fd, buffer.Data(), buffer.Length());
|
|
85
|
+
return Napi::Number::New(env, n);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
Napi::Value Configure(const Napi::CallbackInfo& info) {
|
|
89
|
+
Napi::Env env = info.Env();
|
|
90
|
+
if (info.Length() < 2) return env.Null();
|
|
91
|
+
|
|
92
|
+
std::string ip = info[0].As<Napi::String>();
|
|
93
|
+
int mtu = info[1].As<Napi::Number>().Int32Value();
|
|
94
|
+
|
|
95
|
+
std::string cmd_ip = "ip addr add " + ip + " dev " + iface_name + " 2>/dev/null";
|
|
96
|
+
system(cmd_ip.c_str());
|
|
97
|
+
std::string cmd_up = "ip link set " + iface_name + " up 2>/dev/null";
|
|
98
|
+
system(cmd_up.c_str());
|
|
99
|
+
std::string cmd_mtu = "ip link set mtu " + std::to_string(mtu) + " dev " + iface_name + " 2>/dev/null";
|
|
100
|
+
system(cmd_mtu.c_str());
|
|
101
|
+
|
|
102
|
+
return env.Null();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
Napi::Value GetHexDump(const Napi::CallbackInfo& info) {
|
|
106
|
+
Napi::Env env = info.Env();
|
|
107
|
+
if (info.Length() < 1) return Napi::String::New(env, "");
|
|
108
|
+
|
|
109
|
+
Napi::Buffer<char> buffer = info[0].As<Napi::Buffer<char>>();
|
|
110
|
+
char* data = buffer.Data();
|
|
111
|
+
size_t len = buffer.Length();
|
|
112
|
+
|
|
113
|
+
std::string result;
|
|
114
|
+
for (size_t i = 0; i < len && i < 256; i++) {
|
|
115
|
+
char hex[4];
|
|
116
|
+
snprintf(hex, sizeof(hex), "%02x ", (unsigned char)data[i]);
|
|
117
|
+
result += hex;
|
|
118
|
+
if ((i + 1) % 16 == 0) result += "\n";
|
|
119
|
+
}
|
|
120
|
+
return Napi::String::New(env, result);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
void Close(const Napi::CallbackInfo& info) {
|
|
124
|
+
if (tun_fd >= 0) {
|
|
125
|
+
close(tun_fd);
|
|
126
|
+
tun_fd = -1;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
Napi::Value GetName(const Napi::CallbackInfo& info) {
|
|
131
|
+
return Napi::String::New(info.Env(), iface_name);
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
class CryptoEngine : public Napi::ObjectWrap<CryptoEngine> {
|
|
136
|
+
private:
|
|
137
|
+
unsigned char private_key[crypto_box_SECRETKEYBYTES];
|
|
138
|
+
unsigned char public_key[crypto_box_PUBLICKEYBYTES];
|
|
139
|
+
unsigned char peer_public_key[crypto_box_PUBLICKEYBYTES];
|
|
140
|
+
unsigned char shared_key[crypto_box_BEFORENMBYTES];
|
|
141
|
+
bool shared_key_initialized;
|
|
142
|
+
|
|
143
|
+
public:
|
|
144
|
+
static Napi::Object Init(Napi::Env env, Napi::Object exports) {
|
|
145
|
+
Napi::Function func = DefineClass(env, "CryptoEngine", {
|
|
146
|
+
InstanceMethod<&CryptoEngine::GenerateKeypair>("generateKeypair"),
|
|
147
|
+
InstanceMethod<&CryptoEngine::SetPrivateKey>("setPrivateKey"),
|
|
148
|
+
InstanceMethod<&CryptoEngine::SetPeerPublicKey>("setPeerPublicKey"),
|
|
149
|
+
InstanceMethod<&CryptoEngine::Encrypt>("encrypt"),
|
|
150
|
+
InstanceMethod<&CryptoEngine::Decrypt>("decrypt"),
|
|
151
|
+
InstanceMethod<&CryptoEngine::GetPublicKey>("getPublicKey")
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
Napi::FunctionReference* constructor = new Napi::FunctionReference();
|
|
155
|
+
*constructor = Napi::Persistent(func);
|
|
156
|
+
env.SetInstanceData(constructor);
|
|
157
|
+
exports.Set("CryptoEngine", func);
|
|
158
|
+
return exports;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
CryptoEngine(const Napi::CallbackInfo& info) : Napi::ObjectWrap<CryptoEngine>(info) {
|
|
162
|
+
if (sodium_init() < 0) {
|
|
163
|
+
Napi::Error::New(info.Env(), "Failed to initialize libsodium").ThrowAsJavaScriptException();
|
|
164
|
+
}
|
|
165
|
+
shared_key_initialized = false;
|
|
166
|
+
memset(shared_key, 0, crypto_box_BEFORENMBYTES);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
Napi::Value GenerateKeypair(const Napi::CallbackInfo& info) {
|
|
170
|
+
Napi::Env env = info.Env();
|
|
171
|
+
crypto_box_keypair(public_key, private_key);
|
|
172
|
+
|
|
173
|
+
Napi::Object result = Napi::Object::New(env);
|
|
174
|
+
result.Set("publicKey", Napi::Buffer<unsigned char>::Copy(env, public_key, crypto_box_PUBLICKEYBYTES));
|
|
175
|
+
result.Set("privateKey", Napi::Buffer<unsigned char>::Copy(env, private_key, crypto_box_SECRETKEYBYTES));
|
|
176
|
+
return result;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
void SetPrivateKey(const Napi::CallbackInfo& info) {
|
|
180
|
+
if (info.Length() < 1) return;
|
|
181
|
+
Napi::Buffer<unsigned char> key = info[0].As<Napi::Buffer<unsigned char>>();
|
|
182
|
+
if (key.Length() == crypto_box_SECRETKEYBYTES) {
|
|
183
|
+
memcpy(private_key, key.Data(), crypto_box_SECRETKEYBYTES);
|
|
184
|
+
crypto_scalarmult_base(public_key, private_key);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
void SetPeerPublicKey(const Napi::CallbackInfo& info) {
|
|
189
|
+
if (info.Length() < 1) return;
|
|
190
|
+
Napi::Buffer<unsigned char> key = info[0].As<Napi::Buffer<unsigned char>>();
|
|
191
|
+
if (key.Length() != crypto_box_PUBLICKEYBYTES) return;
|
|
192
|
+
|
|
193
|
+
memcpy(peer_public_key, key.Data(), crypto_box_PUBLICKEYBYTES);
|
|
194
|
+
crypto_box_beforenm(shared_key, peer_public_key, private_key);
|
|
195
|
+
shared_key_initialized = true;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
Napi::Value Encrypt(const Napi::CallbackInfo& info) {
|
|
199
|
+
Napi::Env env = info.Env();
|
|
200
|
+
if (!shared_key_initialized || info.Length() < 1) {
|
|
201
|
+
return Napi::Buffer<char>::New(env, 0);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
Napi::Buffer<char> plaintext = info[0].As<Napi::Buffer<char>>();
|
|
205
|
+
unsigned char nonce[crypto_box_NONCEBYTES];
|
|
206
|
+
randombytes_buf(nonce, sizeof(nonce));
|
|
207
|
+
|
|
208
|
+
size_t ciphertext_len = plaintext.Length() + crypto_box_MACBYTES;
|
|
209
|
+
std::vector<unsigned char> ciphertext(ciphertext_len);
|
|
210
|
+
|
|
211
|
+
crypto_box_afternm(ciphertext.data(),
|
|
212
|
+
reinterpret_cast<unsigned char*>(plaintext.Data()),
|
|
213
|
+
plaintext.Length(),
|
|
214
|
+
nonce,
|
|
215
|
+
shared_key);
|
|
216
|
+
|
|
217
|
+
size_t result_len = sizeof(nonce) + ciphertext_len;
|
|
218
|
+
std::vector<char> result(result_len);
|
|
219
|
+
memcpy(result.data(), nonce, sizeof(nonce));
|
|
220
|
+
memcpy(result.data() + sizeof(nonce), ciphertext.data(), ciphertext_len);
|
|
221
|
+
|
|
222
|
+
return Napi::Buffer<char>::Copy(env, result.data(), result_len);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
Napi::Value Decrypt(const Napi::CallbackInfo& info) {
|
|
226
|
+
Napi::Env env = info.Env();
|
|
227
|
+
if (!shared_key_initialized || info.Length() < 1) {
|
|
228
|
+
return Napi::Buffer<char>::New(env, 0);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
Napi::Buffer<char> ciphertext_with_nonce = info[0].As<Napi::Buffer<char>>();
|
|
232
|
+
if (ciphertext_with_nonce.Length() < crypto_box_NONCEBYTES + crypto_box_MACBYTES) {
|
|
233
|
+
return Napi::Buffer<char>::New(env, 0);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
unsigned char nonce[crypto_box_NONCEBYTES];
|
|
237
|
+
memcpy(nonce, ciphertext_with_nonce.Data(), crypto_box_NONCEBYTES);
|
|
238
|
+
|
|
239
|
+
size_t ciphertext_len = ciphertext_with_nonce.Length() - crypto_box_NONCEBYTES;
|
|
240
|
+
size_t plaintext_len = ciphertext_len - crypto_box_MACBYTES;
|
|
241
|
+
std::vector<unsigned char> plaintext(plaintext_len);
|
|
242
|
+
|
|
243
|
+
if (crypto_box_open_afternm(plaintext.data(),
|
|
244
|
+
reinterpret_cast<unsigned char*>(ciphertext_with_nonce.Data() + crypto_box_NONCEBYTES),
|
|
245
|
+
ciphertext_len,
|
|
246
|
+
nonce,
|
|
247
|
+
shared_key) != 0) {
|
|
248
|
+
return Napi::Buffer<char>::New(env, 0);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return Napi::Buffer<char>::Copy(env, reinterpret_cast<char*>(plaintext.data()), plaintext_len);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
Napi::Value GetPublicKey(const Napi::CallbackInfo& info) {
|
|
255
|
+
return Napi::Buffer<unsigned char>::Copy(info.Env(), public_key, crypto_box_PUBLICKEYBYTES);
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
struct PeerStats {
|
|
260
|
+
std::atomic<uint64_t> tx_bytes{0};
|
|
261
|
+
std::atomic<uint64_t> rx_bytes{0};
|
|
262
|
+
std::atomic<uint64_t> tx_packets{0};
|
|
263
|
+
std::atomic<uint64_t> rx_packets{0};
|
|
264
|
+
std::chrono::steady_clock::time_point last_seen;
|
|
265
|
+
std::string endpoint;
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
static std::map<std::string, PeerStats> global_stats;
|
|
269
|
+
|
|
270
|
+
class StatsCollector : public Napi::ObjectWrap<StatsCollector> {
|
|
271
|
+
public:
|
|
272
|
+
static Napi::Object Init(Napi::Env env, Napi::Object exports) {
|
|
273
|
+
Napi::Function func = DefineClass(env, "StatsCollector", {
|
|
274
|
+
StaticMethod<&StatsCollector::RecordTx>("recordTx"),
|
|
275
|
+
StaticMethod<&StatsCollector::RecordRx>("recordRx"),
|
|
276
|
+
StaticMethod<&StatsCollector::GetStats>("getStats"),
|
|
277
|
+
StaticMethod<&StatsCollector::GetAllPeers>("getAllPeers"),
|
|
278
|
+
StaticMethod<&StatsCollector::ResetStats>("resetStats")
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
exports.Set("StatsCollector", func);
|
|
282
|
+
return exports;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
StatsCollector(const Napi::CallbackInfo& info) : Napi::ObjectWrap<StatsCollector>(info) {}
|
|
286
|
+
|
|
287
|
+
static Napi::Value RecordTx(const Napi::CallbackInfo& info) {
|
|
288
|
+
Napi::Env env = info.Env();
|
|
289
|
+
if (info.Length() < 2) return env.Undefined();
|
|
290
|
+
|
|
291
|
+
std::string peerId = info[0].As<Napi::String>();
|
|
292
|
+
uint64_t bytes = info[1].As<Napi::Number>().Uint32Value();
|
|
293
|
+
|
|
294
|
+
auto& stats = global_stats[peerId];
|
|
295
|
+
stats.tx_bytes += bytes;
|
|
296
|
+
stats.tx_packets++;
|
|
297
|
+
stats.last_seen = std::chrono::steady_clock::now();
|
|
298
|
+
|
|
299
|
+
if (info.Length() >= 3) {
|
|
300
|
+
stats.endpoint = info[2].As<Napi::String>();
|
|
301
|
+
}
|
|
302
|
+
return env.Undefined();
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
static Napi::Value RecordRx(const Napi::CallbackInfo& info) {
|
|
306
|
+
Napi::Env env = info.Env();
|
|
307
|
+
if (info.Length() < 2) return env.Undefined();
|
|
308
|
+
|
|
309
|
+
std::string peerId = info[0].As<Napi::String>();
|
|
310
|
+
uint64_t bytes = info[1].As<Napi::Number>().Uint32Value();
|
|
311
|
+
|
|
312
|
+
auto& stats = global_stats[peerId];
|
|
313
|
+
stats.rx_bytes += bytes;
|
|
314
|
+
stats.rx_packets++;
|
|
315
|
+
stats.last_seen = std::chrono::steady_clock::now();
|
|
316
|
+
|
|
317
|
+
if (info.Length() >= 3) {
|
|
318
|
+
stats.endpoint = info[2].As<Napi::String>();
|
|
319
|
+
}
|
|
320
|
+
return env.Undefined();
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
static Napi::Value GetStats(const Napi::CallbackInfo& info) {
|
|
324
|
+
Napi::Env env = info.Env();
|
|
325
|
+
Napi::Object result = Napi::Object::New(env);
|
|
326
|
+
|
|
327
|
+
for (auto& [peerId, stats] : global_stats) {
|
|
328
|
+
Napi::Object peerStats = Napi::Object::New(env);
|
|
329
|
+
peerStats.Set("txBytes", Napi::Number::New(env, (double)stats.tx_bytes.load()));
|
|
330
|
+
peerStats.Set("rxBytes", Napi::Number::New(env, (double)stats.rx_bytes.load()));
|
|
331
|
+
peerStats.Set("txPackets", Napi::Number::New(env, (double)stats.tx_packets.load()));
|
|
332
|
+
peerStats.Set("rxPackets", Napi::Number::New(env, (double)stats.rx_packets.load()));
|
|
333
|
+
peerStats.Set("endpoint", Napi::String::New(env, stats.endpoint));
|
|
334
|
+
|
|
335
|
+
auto now = std::chrono::steady_clock::now();
|
|
336
|
+
auto elapsed = std::chrono::duration_cast<std::chrono::seconds>(now - stats.last_seen).count();
|
|
337
|
+
peerStats.Set("lastSeen", Napi::Number::New(env, (double)elapsed));
|
|
338
|
+
result.Set(peerId, peerStats);
|
|
339
|
+
}
|
|
340
|
+
return result;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
static Napi::Value GetAllPeers(const Napi::CallbackInfo& info) {
|
|
344
|
+
Napi::Env env = info.Env();
|
|
345
|
+
Napi::Array result = Napi::Array::New(env);
|
|
346
|
+
uint32_t idx = 0;
|
|
347
|
+
for (auto& [peerId, stats] : global_stats) {
|
|
348
|
+
result.Set(idx++, Napi::String::New(env, peerId));
|
|
349
|
+
}
|
|
350
|
+
return result;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
static Napi::Value ResetStats(const Napi::CallbackInfo& info) {
|
|
354
|
+
global_stats.clear();
|
|
355
|
+
return info.Env().Undefined();
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
static Napi::Object InitAll(Napi::Env env, Napi::Object exports) {
|
|
360
|
+
TunDevice::Init(env, exports);
|
|
361
|
+
CryptoEngine::Init(env, exports);
|
|
362
|
+
StatsCollector::Init(env, exports);
|
|
363
|
+
return exports;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
NODE_API_MODULE(vpn_native, InitAll)
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { program } from 'commander';
|
|
4
|
+
import * as fs from 'fs';
|
|
5
|
+
import * as path from 'path';
|
|
6
|
+
import * as os from 'os';
|
|
7
|
+
|
|
8
|
+
const CONFIG_DIR = os.homedir() + '/.vpn';
|
|
9
|
+
const CONFIG_FILE = CONFIG_DIR + '/config.json';
|
|
10
|
+
const KEY_FILE = CONFIG_DIR + '/keys.json';
|
|
11
|
+
|
|
12
|
+
function ensureConfigDir() {
|
|
13
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
14
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function loadConfig(): any | null {
|
|
19
|
+
try {
|
|
20
|
+
if (fs.existsSync(CONFIG_FILE)) {
|
|
21
|
+
return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf-8'));
|
|
22
|
+
}
|
|
23
|
+
} catch (e) {}
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let activeConnection: any = null;
|
|
28
|
+
|
|
29
|
+
program
|
|
30
|
+
.name('vpn')
|
|
31
|
+
.description('Native VPN CLI')
|
|
32
|
+
.version('1.0.0');
|
|
33
|
+
|
|
34
|
+
program
|
|
35
|
+
.command('init')
|
|
36
|
+
.alias('i')
|
|
37
|
+
.description('Generate new private and public keys')
|
|
38
|
+
.action(async () => {
|
|
39
|
+
ensureConfigDir();
|
|
40
|
+
|
|
41
|
+
const nativeModule = require('../build/Release/wirevpn.node');
|
|
42
|
+
const crypto = new nativeModule.CryptoEngine();
|
|
43
|
+
const keypair = crypto.generateKeypair();
|
|
44
|
+
|
|
45
|
+
const keys = {
|
|
46
|
+
privateKey: keypair.privateKey.toString('hex'),
|
|
47
|
+
publicKey: keypair.publicKey.toString('hex'),
|
|
48
|
+
createdAt: new Date().toISOString()
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
fs.writeFileSync(KEY_FILE, JSON.stringify(keys, null, 2));
|
|
52
|
+
|
|
53
|
+
console.log('Keys generated successfully!');
|
|
54
|
+
console.log(`Public Key: ${keys.publicKey}`);
|
|
55
|
+
console.log(`Private Key saved to: ${KEY_FILE}`);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
program
|
|
59
|
+
.command('up <config>')
|
|
60
|
+
.alias('u')
|
|
61
|
+
.description('Start VPN connection')
|
|
62
|
+
.option('-d, --debug', 'Enable debug mode')
|
|
63
|
+
.action(async (configPath: string, options: any) => {
|
|
64
|
+
let config;
|
|
65
|
+
|
|
66
|
+
if (fs.existsSync(configPath)) {
|
|
67
|
+
config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
68
|
+
} else {
|
|
69
|
+
const saved = loadConfig();
|
|
70
|
+
if (saved) {
|
|
71
|
+
config = saved;
|
|
72
|
+
} else {
|
|
73
|
+
console.error(`Config file not found: ${configPath}`);
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (fs.existsSync(KEY_FILE)) {
|
|
79
|
+
const keys = JSON.parse(fs.readFileSync(KEY_FILE, 'utf-8'));
|
|
80
|
+
config.privateKey = keys.privateKey;
|
|
81
|
+
config.publicKey = keys.publicKey;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const { VPNConnection } = require('./lib/vpn');
|
|
85
|
+
activeConnection = new VPNConnection(config, options.debug || false);
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
await activeConnection.init();
|
|
89
|
+
await activeConnection.up();
|
|
90
|
+
|
|
91
|
+
console.log('\nVPN is running. Press Ctrl+C to stop.\n');
|
|
92
|
+
|
|
93
|
+
const statusInterval = setInterval(() => {
|
|
94
|
+
if (activeConnection) {
|
|
95
|
+
const stats = activeConnection.getStatus();
|
|
96
|
+
const peerList = activeConnection.getPeers();
|
|
97
|
+
|
|
98
|
+
process.stdout.write(`\rActive: ${peerList.length} peers | `);
|
|
99
|
+
for (const [key, stat] of Object.entries(stats)) {
|
|
100
|
+
const s: any = stat;
|
|
101
|
+
process.stdout.write(`TX: ${(s.txBytes / 1024).toFixed(1)}KB RX: ${(s.rxBytes / 1024).toFixed(1)}KB `);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}, 1000);
|
|
105
|
+
|
|
106
|
+
process.on('SIGINT', async () => {
|
|
107
|
+
clearInterval(statusInterval);
|
|
108
|
+
if (activeConnection) {
|
|
109
|
+
await activeConnection.down();
|
|
110
|
+
}
|
|
111
|
+
console.log('\nVPN stopped.');
|
|
112
|
+
process.exit(0);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
} catch (err: any) {
|
|
116
|
+
console.error(`Failed to start VPN: ${err.message}`);
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
program
|
|
122
|
+
.command('down')
|
|
123
|
+
.alias('d')
|
|
124
|
+
.description('Stop VPN connection')
|
|
125
|
+
.action(async () => {
|
|
126
|
+
if (activeConnection) {
|
|
127
|
+
await activeConnection.down();
|
|
128
|
+
activeConnection = null;
|
|
129
|
+
console.log('VPN connection closed');
|
|
130
|
+
} else {
|
|
131
|
+
console.log('No active VPN connection found');
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
program
|
|
136
|
+
.command('status')
|
|
137
|
+
.alias('s')
|
|
138
|
+
.description('Display statistics')
|
|
139
|
+
.action(() => {
|
|
140
|
+
if (!activeConnection) {
|
|
141
|
+
console.log('VPN is not running. Use "vpn up" first.');
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const stats = activeConnection.getStatus();
|
|
146
|
+
|
|
147
|
+
if (Object.keys(stats).length === 0) {
|
|
148
|
+
console.log('No active peers');
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
console.log('Peer Statistics:');
|
|
153
|
+
for (const [peer, stat] of Object.entries(stats)) {
|
|
154
|
+
const s: any = stat;
|
|
155
|
+
console.log(` ${peer.substring(0, 16)}...`);
|
|
156
|
+
console.log(` TX: ${(s.txBytes / 1024).toFixed(2)} KB (${s.txPackets} packets)`);
|
|
157
|
+
console.log(` RX: ${(s.rxBytes / 1024).toFixed(2)} KB (${s.rxPackets} packets)`);
|
|
158
|
+
console.log(` Endpoint: ${s.endpoint}`);
|
|
159
|
+
console.log(` Last seen: ${s.lastSeen}s ago\n`);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
program
|
|
164
|
+
.command('list')
|
|
165
|
+
.alias('l')
|
|
166
|
+
.description('List connected peers')
|
|
167
|
+
.action(() => {
|
|
168
|
+
if (!activeConnection) {
|
|
169
|
+
console.log('VPN is not running. Use "vpn up" first.');
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const peers = activeConnection.getPeers();
|
|
174
|
+
|
|
175
|
+
if (peers.length === 0) {
|
|
176
|
+
console.log('No peers connected');
|
|
177
|
+
} else {
|
|
178
|
+
console.log(`Connected peers (${peers.length}):`);
|
|
179
|
+
peers.forEach((peer: string, idx: number) => {
|
|
180
|
+
console.log(` ${idx + 1}. ${peer.substring(0, 16)}...`);
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
program.parse();
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { VPNConnection, VPNConfig, VPNPeer } from './lib/vpn';
|
package/src/lib/vpn.ts
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { EventEmitter } from 'events';
|
|
2
|
+
import * as dgram from 'dgram';
|
|
3
|
+
import * as fs from 'fs';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
|
|
6
|
+
const nativeModule = require('../../build/Release/wirevpn.node');
|
|
7
|
+
|
|
8
|
+
export interface VPNPeer {
|
|
9
|
+
publicKey: string;
|
|
10
|
+
endpoint: string;
|
|
11
|
+
allowedIPs: string[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface VPNConfig {
|
|
15
|
+
privateKey?: string;
|
|
16
|
+
publicKey?: string;
|
|
17
|
+
address: string;
|
|
18
|
+
port: number;
|
|
19
|
+
peers: VPNPeer[];
|
|
20
|
+
mtu?: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class VPNConnection extends EventEmitter {
|
|
24
|
+
private tun: any;
|
|
25
|
+
private crypto: any;
|
|
26
|
+
private stats: any;
|
|
27
|
+
private running: boolean = false;
|
|
28
|
+
private debugMode: boolean = false;
|
|
29
|
+
private udpSocket: any;
|
|
30
|
+
private config: VPNConfig;
|
|
31
|
+
private peerMap: Map<string, VPNPeer>;
|
|
32
|
+
private readInterval: NodeJS.Timeout | null = null;
|
|
33
|
+
|
|
34
|
+
constructor(config: VPNConfig, debug: boolean = false) {
|
|
35
|
+
super();
|
|
36
|
+
this.config = config;
|
|
37
|
+
this.debugMode = debug;
|
|
38
|
+
this.tun = new nativeModule.TunDevice();
|
|
39
|
+
this.crypto = new nativeModule.CryptoEngine();
|
|
40
|
+
this.stats = nativeModule.StatsCollector;
|
|
41
|
+
this.peerMap = new Map();
|
|
42
|
+
this.udpSocket = null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async init(): Promise<void> {
|
|
46
|
+
let privateKey = this.config.privateKey;
|
|
47
|
+
if (!privateKey) {
|
|
48
|
+
const keypair = this.crypto.generateKeypair();
|
|
49
|
+
privateKey = keypair.privateKey.toString('hex');
|
|
50
|
+
this.config.privateKey = privateKey;
|
|
51
|
+
this.config.publicKey = keypair.publicKey.toString('hex');
|
|
52
|
+
console.log(`Generated new keypair - Public: ${this.config.publicKey}`);
|
|
53
|
+
} else {
|
|
54
|
+
this.crypto.setPrivateKey(Buffer.from(privateKey, 'hex'));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
for (const peer of this.config.peers) {
|
|
58
|
+
this.crypto.setPeerPublicKey(Buffer.from(peer.publicKey, 'hex'));
|
|
59
|
+
this.peerMap.set(peer.publicKey, peer);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async up(): Promise<void> {
|
|
64
|
+
const tunFd = this.tun.open();
|
|
65
|
+
if (tunFd < 0) {
|
|
66
|
+
throw new Error('Failed to open TUN device');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
this.tun.configure(this.config.address, this.config.mtu || 1500);
|
|
70
|
+
|
|
71
|
+
this.udpSocket = dgram.createSocket('udp4');
|
|
72
|
+
|
|
73
|
+
await new Promise<void>((resolve) => {
|
|
74
|
+
this.udpSocket.bind(this.config.port, () => resolve());
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
this.running = true;
|
|
78
|
+
|
|
79
|
+
this.udpSocket.on('message', (data: Buffer, rinfo: any) => {
|
|
80
|
+
const decrypted = this.crypto.decrypt(data);
|
|
81
|
+
if (decrypted && decrypted.length > 0) {
|
|
82
|
+
const peerKey = this.getPeerKey(rinfo.address, rinfo.port);
|
|
83
|
+
if (this.stats && this.stats.recordRx) {
|
|
84
|
+
this.stats.recordRx(peerKey, decrypted.length, `${rinfo.address}:${rinfo.port}`);
|
|
85
|
+
}
|
|
86
|
+
this.tun.write(decrypted);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
this.readInterval = setInterval(() => {
|
|
91
|
+
if (!this.running) return;
|
|
92
|
+
|
|
93
|
+
const packet = this.tun.read();
|
|
94
|
+
if (packet && packet.length > 0) {
|
|
95
|
+
if (this.debugMode) {
|
|
96
|
+
const hexdump = this.tun.getHexDump(packet);
|
|
97
|
+
console.log(`[DEBUG] TUN packet (${packet.length} bytes):\n${hexdump}`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const encrypted = this.crypto.encrypt(packet);
|
|
101
|
+
if (encrypted && encrypted.length > 0) {
|
|
102
|
+
for (const peer of this.peerMap.values()) {
|
|
103
|
+
const [host, port] = peer.endpoint.split(':');
|
|
104
|
+
this.udpSocket.send(encrypted, parseInt(port), host);
|
|
105
|
+
if (this.stats && this.stats.recordTx) {
|
|
106
|
+
this.stats.recordTx(peer.publicKey, encrypted.length, peer.endpoint);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}, 10);
|
|
112
|
+
|
|
113
|
+
console.log(`VPN UP - Interface: ${this.tun.name}, IP: ${this.config.address}, Port: ${this.config.port}`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async down(): Promise<void> {
|
|
117
|
+
this.running = false;
|
|
118
|
+
|
|
119
|
+
if (this.readInterval) {
|
|
120
|
+
clearInterval(this.readInterval);
|
|
121
|
+
this.readInterval = null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (this.udpSocket) {
|
|
125
|
+
this.udpSocket.close();
|
|
126
|
+
this.udpSocket = null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
this.tun.close();
|
|
130
|
+
|
|
131
|
+
console.log('VPN DOWN - Interface closed');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
getStatus(): any {
|
|
135
|
+
if (this.stats && this.stats.getStats) {
|
|
136
|
+
return this.stats.getStats();
|
|
137
|
+
}
|
|
138
|
+
return {};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
getPeers(): string[] {
|
|
142
|
+
if (this.stats && this.stats.getAllPeers) {
|
|
143
|
+
return this.stats.getAllPeers();
|
|
144
|
+
}
|
|
145
|
+
return [];
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
private getPeerKey(ip: string, port: number): string {
|
|
149
|
+
for (const [key, peer] of this.peerMap.entries()) {
|
|
150
|
+
if (peer.endpoint === `${ip}:${port}`) {
|
|
151
|
+
return key;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return `${ip}:${port}`;
|
|
155
|
+
}
|
|
156
|
+
}
|