vpn-split 21.0.18 → 21.0.19
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/browser/package.json +1 -1
- package/browser-prod/fesm2022/vpn-split-browser.mjs.map +1 -1
- package/browser-prod/package.json +1 -1
- package/lib/build-info._auto-generated_.d.ts +1 -1
- package/lib/build-info._auto-generated_.js +1 -1
- package/lib/package.json +1 -1
- package/lib-prod/build-info._auto-generated_.js +14 -0
- package/lib-prod/env/env.angular-node-app.js +130 -0
- package/lib-prod/env/env.docs-webapp.js +130 -0
- package/lib-prod/env/env.electron-app.js +130 -0
- package/lib-prod/env/env.mobile-app.js +130 -0
- package/lib-prod/env/env.npm-lib-and-cli-tool.js +130 -0
- package/lib-prod/env/env.vscode-plugin.js +130 -0
- package/lib-prod/env/index.js +6 -0
- package/lib-prod/{hostile.backend.ts → hostile.backend.js} +21 -31
- package/lib-prod/index._auto-generated_.js +0 -0
- package/lib-prod/index.js +2 -0
- package/lib-prod/migrations/index.js +1 -0
- package/lib-prod/migrations/migrations_index._auto-generated_.js +0 -0
- package/lib-prod/models.js +165 -0
- package/lib-prod/package.json +1 -1
- package/lib-prod/start.backend.js +0 -0
- package/lib-prod/vpn-split.backend.js +700 -0
- package/package.json +1 -1
- package/websql/package.json +1 -1
- package/websql-prod/fesm2022/vpn-split-websql.mjs.map +1 -1
- package/websql-prod/package.json +1 -1
- package/lib-prod/build-info._auto-generated_.ts +0 -27
- package/lib-prod/env/env.angular-node-app.ts +0 -66
- package/lib-prod/env/env.docs-webapp.ts +0 -66
- package/lib-prod/env/env.electron-app.ts +0 -66
- package/lib-prod/env/env.mobile-app.ts +0 -66
- package/lib-prod/env/env.npm-lib-and-cli-tool.ts +0 -66
- package/lib-prod/env/env.vscode-plugin.ts +0 -66
- package/lib-prod/env/index.ts +0 -6
- package/lib-prod/index._auto-generated_.ts +0 -5
- package/lib-prod/index.ts +0 -5
- package/lib-prod/lib-info.md +0 -8
- package/lib-prod/migrations/index.ts +0 -2
- package/lib-prod/migrations/migrations-info.md +0 -6
- package/lib-prod/migrations/migrations_index._auto-generated_.ts +0 -5
- package/lib-prod/models.ts +0 -249
- package/lib-prod/start.backend.ts +0 -28
- package/lib-prod/vpn-split.backend.ts +0 -1039
|
@@ -0,0 +1,700 @@
|
|
|
1
|
+
import { execSync } from "child_process";
|
|
2
|
+
import * as dgram from "dgram";
|
|
3
|
+
import * as http from "http";
|
|
4
|
+
import axios from "axios";
|
|
5
|
+
import * as express from "express";
|
|
6
|
+
import * as httpProxy from "http-proxy";
|
|
7
|
+
import { Log, Level } from "ng2-logger/lib-prod";
|
|
8
|
+
import { config } from "tnp-core/lib-prod";
|
|
9
|
+
import { path, fse, https, isElevated, crossPlatformPath, ___NS__includes, ___NS__isArray, ___NS__keys, ___NS__merge, ___NS__values, UtilsNetwork__NS__getEtcHostsPath, UtilsNetwork__NS__getFirstIpV4LocalActiveIpAddress, UtilsNetwork__NS__getLocalIpAddresses, UtilsOs__NS__commandExistsSync, UtilsOs__NS__getRealHomeDir, UtilsOs__NS__isRunningInWindowsPowerShell } from "tnp-core/lib-prod";
|
|
10
|
+
import { CoreModels__NS__localhostDomain, CoreModels__NS__localhostIp127 } from "tnp-core/lib-prod";
|
|
11
|
+
import { Helpers__NS__error, Helpers__NS__exists, Helpers__NS__info, Helpers__NS__killProcessByPort, Helpers__NS__run, Helpers__NS__writeFile, HelpersTaon__NS__arrays__NS__from } from "tnp-helpers/lib-prod";
|
|
12
|
+
import { Hostile } from "./hostile.backend";
|
|
13
|
+
import { HostForServer } from "./models";
|
|
14
|
+
const HOST_FILE_PATH = UtilsNetwork__NS__getEtcHostsPath();
|
|
15
|
+
const log = Log.create("vpn-split", Level.INFO);
|
|
16
|
+
const GENERATED = "#GENERATED_BY_CLI#";
|
|
17
|
+
const EOL = process.platform === "win32" ? "\r\n" : "\n";
|
|
18
|
+
const SERVERS_PATH = "/$$$$servers$$$$";
|
|
19
|
+
const HOST_FILE_PATHUSER = crossPlatformPath([
|
|
20
|
+
UtilsOs__NS__getRealHomeDir(),
|
|
21
|
+
"hosts-file__vpn-split"
|
|
22
|
+
]);
|
|
23
|
+
const from = HostForServer.From;
|
|
24
|
+
const DefaultEtcHosts = {
|
|
25
|
+
"localhost alias": from({
|
|
26
|
+
ipOrDomain: "127.0.0.1",
|
|
27
|
+
aliases: "localhost",
|
|
28
|
+
isDefault: true
|
|
29
|
+
}),
|
|
30
|
+
broadcasthost: from({
|
|
31
|
+
ipOrDomain: "255.255.255.255",
|
|
32
|
+
aliases: "broadcasthost",
|
|
33
|
+
isDefault: true
|
|
34
|
+
}),
|
|
35
|
+
"localhost alias ipv6": from({
|
|
36
|
+
ipOrDomain: "::1",
|
|
37
|
+
aliases: "localhost",
|
|
38
|
+
isDefault: true
|
|
39
|
+
})
|
|
40
|
+
};
|
|
41
|
+
class VpnSplit {
|
|
42
|
+
constructor(portsToPass, hosts, cwd) {
|
|
43
|
+
this.portsToPass = portsToPass;
|
|
44
|
+
this.hosts = hosts;
|
|
45
|
+
this.cwd = cwd;
|
|
46
|
+
this.__hostile = new Hostile();
|
|
47
|
+
}
|
|
48
|
+
//#region getters
|
|
49
|
+
get hostsArr() {
|
|
50
|
+
const hosts = this.hosts;
|
|
51
|
+
return ___NS__keys(hosts).map((hostName) => {
|
|
52
|
+
const v = hosts[hostName];
|
|
53
|
+
v.name = hostName;
|
|
54
|
+
return v;
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
get hostsArrWithoutDefault() {
|
|
58
|
+
return this.hostsArr.filter((f) => !f.isDefault);
|
|
59
|
+
}
|
|
60
|
+
get serveKeyName() {
|
|
61
|
+
return "tmp-" + config.file.server_key;
|
|
62
|
+
}
|
|
63
|
+
get serveKeyPath() {
|
|
64
|
+
return path.join(this.cwd, this.serveKeyName);
|
|
65
|
+
}
|
|
66
|
+
get serveCertName() {
|
|
67
|
+
return "tmp-" + config.file.server_cert;
|
|
68
|
+
}
|
|
69
|
+
get serveCertPath() {
|
|
70
|
+
return path.join(this.cwd, this.serveCertName);
|
|
71
|
+
}
|
|
72
|
+
get serveCertChainName() {
|
|
73
|
+
return "tmp-" + config.file.server_chain_cert;
|
|
74
|
+
}
|
|
75
|
+
get serveCertChainPath() {
|
|
76
|
+
return path.join(this.cwd, this.serveCertChainName);
|
|
77
|
+
}
|
|
78
|
+
//#endregion
|
|
79
|
+
//#region fields
|
|
80
|
+
__hostile;
|
|
81
|
+
//#endregion
|
|
82
|
+
//#region singleton
|
|
83
|
+
static _instances = {};
|
|
84
|
+
static async Instance({
|
|
85
|
+
ports = [80, 443, 4443, 22, 2222, 8180, 8080, 4407, 7999, 9443],
|
|
86
|
+
additionalDefaultHosts = {},
|
|
87
|
+
cwd = process.cwd(),
|
|
88
|
+
allowNotSudo = false
|
|
89
|
+
} = {}) {
|
|
90
|
+
if (!await isElevated() && !allowNotSudo) {
|
|
91
|
+
Helpers__NS__error(
|
|
92
|
+
`[vpn-split] Please run this program as sudo (or admin on windows)`,
|
|
93
|
+
false,
|
|
94
|
+
true
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
if (!VpnSplit._instances[cwd]) {
|
|
98
|
+
VpnSplit._instances[cwd] = new VpnSplit(
|
|
99
|
+
ports,
|
|
100
|
+
___NS__merge(DefaultEtcHosts, additionalDefaultHosts),
|
|
101
|
+
cwd
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
return VpnSplit._instances[cwd];
|
|
105
|
+
}
|
|
106
|
+
//#endregion
|
|
107
|
+
//#region start server
|
|
108
|
+
async startServer(saveHostInUserFolder = false) {
|
|
109
|
+
this.createCertificateIfNotExists();
|
|
110
|
+
const hostsForCert = this.hosts;
|
|
111
|
+
saveHosts(hostsForCert, { saveHostInUserFolder });
|
|
112
|
+
for (const portToPassthrough of this.portsToPass) {
|
|
113
|
+
await this.serverPassthrough(portToPassthrough);
|
|
114
|
+
}
|
|
115
|
+
for (const portToPassthrough of this.portsToPass) {
|
|
116
|
+
await this.serverUdpPassthrough(portToPassthrough);
|
|
117
|
+
}
|
|
118
|
+
Helpers__NS__info(`Activated (server).`);
|
|
119
|
+
}
|
|
120
|
+
//#endregion
|
|
121
|
+
//#region apply hosts
|
|
122
|
+
applyHosts(hosts) {
|
|
123
|
+
saveHosts(hosts);
|
|
124
|
+
}
|
|
125
|
+
applyHostsLocal(hosts) {
|
|
126
|
+
saveHostsLocal(hosts);
|
|
127
|
+
}
|
|
128
|
+
//#endregion
|
|
129
|
+
//#region start client
|
|
130
|
+
async startClient(vpnServerTargets, options) {
|
|
131
|
+
options = options || {};
|
|
132
|
+
const { saveHostInUserFolder } = options;
|
|
133
|
+
if (!Array.isArray(vpnServerTargets)) {
|
|
134
|
+
vpnServerTargets = [vpnServerTargets];
|
|
135
|
+
}
|
|
136
|
+
for (const vpnServerTarget of vpnServerTargets) {
|
|
137
|
+
await this.preventBadTargetForClient(vpnServerTarget);
|
|
138
|
+
}
|
|
139
|
+
this.createCertificateIfNotExists();
|
|
140
|
+
const hosts = [];
|
|
141
|
+
for (const vpnServerTarget of vpnServerTargets) {
|
|
142
|
+
const newHosts = await this.getRemoteHosts(vpnServerTarget);
|
|
143
|
+
for (const host of newHosts) {
|
|
144
|
+
host.originHostname = vpnServerTarget.hostname;
|
|
145
|
+
hosts.push(host);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
const originalHosts = this.hostsArr;
|
|
149
|
+
const combinedHostsObj = ___NS__values(
|
|
150
|
+
[
|
|
151
|
+
...originalHosts,
|
|
152
|
+
...hosts.map(
|
|
153
|
+
(h) => HostForServer.From(
|
|
154
|
+
{
|
|
155
|
+
aliases: h.alias,
|
|
156
|
+
ipOrDomain: h.ip,
|
|
157
|
+
originHostname: h.originHostname
|
|
158
|
+
},
|
|
159
|
+
`external host ${h.alias} ${h.ip}`
|
|
160
|
+
)
|
|
161
|
+
)
|
|
162
|
+
].map((c) => {
|
|
163
|
+
let copy = c.clone();
|
|
164
|
+
if (!copy.isDefault) {
|
|
165
|
+
copy.ip = `127.0.0.1`;
|
|
166
|
+
}
|
|
167
|
+
return copy;
|
|
168
|
+
}).reduce((prev, curr) => {
|
|
169
|
+
return ___NS__merge(prev, {
|
|
170
|
+
[curr.aliases.join(" ")]: curr
|
|
171
|
+
});
|
|
172
|
+
}, {})
|
|
173
|
+
);
|
|
174
|
+
const hostsForCert = options.useHost ? options.useHost : combinedHostsObj;
|
|
175
|
+
saveHosts(hostsForCert, {
|
|
176
|
+
saveHostInUserFolder
|
|
177
|
+
});
|
|
178
|
+
for (const portToPassthrough of this.portsToPass) {
|
|
179
|
+
await this.clientPassthrough(
|
|
180
|
+
portToPassthrough,
|
|
181
|
+
vpnServerTargets,
|
|
182
|
+
combinedHostsObj
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
for (const portToPassthrough of this.portsToPass) {
|
|
186
|
+
await this.clientUdpPassthrough(
|
|
187
|
+
portToPassthrough,
|
|
188
|
+
vpnServerTargets,
|
|
189
|
+
combinedHostsObj
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
Helpers__NS__info(`Client activated`);
|
|
193
|
+
}
|
|
194
|
+
//#endregion
|
|
195
|
+
//#region private methods / get remote hosts
|
|
196
|
+
async getRemoteHosts(vpnServerTarget) {
|
|
197
|
+
try {
|
|
198
|
+
const url = `http://${vpnServerTarget.hostname}${SERVERS_PATH}`;
|
|
199
|
+
const response = await axios({ url, method: "GET" });
|
|
200
|
+
return response.data;
|
|
201
|
+
} catch (err) {
|
|
202
|
+
Helpers__NS__error(
|
|
203
|
+
`Remote server: ${vpnServerTarget.hostname} may be inactive...`,
|
|
204
|
+
true,
|
|
205
|
+
true
|
|
206
|
+
);
|
|
207
|
+
return [];
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
//#endregion
|
|
211
|
+
//#region private methods / create certificate
|
|
212
|
+
createCertificateIfNotExists() {
|
|
213
|
+
if (!Helpers__NS__exists(this.serveKeyPath) || !Helpers__NS__exists(this.serveCertPath)) {
|
|
214
|
+
Helpers__NS__info(
|
|
215
|
+
`[vpn-split] Generating new self-signed certificate for localhost...`
|
|
216
|
+
);
|
|
217
|
+
const commandGen = UtilsOs__NS__isRunningInWindowsPowerShell() ? `powershell -Command "& 'C:\\Program Files\\Git\\usr\\bin\\openssl.exe' req -nodes -new -x509 -keyout ${this.serveKeyName} -out ${this.serveCertName}"` : `openssl req -nodes -new -x509 -keyout ${this.serveKeyName} -out ${this.serveCertName}`;
|
|
218
|
+
Helpers__NS__run(commandGen, { cwd: this.cwd, output: true }).sync();
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
//#endregion
|
|
222
|
+
//#region private methods / TCP & HTTPS passthrough
|
|
223
|
+
getTarget({
|
|
224
|
+
req,
|
|
225
|
+
res,
|
|
226
|
+
port,
|
|
227
|
+
hostname
|
|
228
|
+
}) {
|
|
229
|
+
return `${req.protocol}://${hostname}:${port}`;
|
|
230
|
+
}
|
|
231
|
+
getProxyConfig({
|
|
232
|
+
req,
|
|
233
|
+
res,
|
|
234
|
+
port,
|
|
235
|
+
hostname,
|
|
236
|
+
isHttps
|
|
237
|
+
}) {
|
|
238
|
+
const serverPassthrough = !!hostname;
|
|
239
|
+
const target = this.getTarget({
|
|
240
|
+
req,
|
|
241
|
+
res,
|
|
242
|
+
port,
|
|
243
|
+
hostname: serverPassthrough ? hostname : req.hostname
|
|
244
|
+
});
|
|
245
|
+
return isHttps ? {
|
|
246
|
+
target,
|
|
247
|
+
ssl: {
|
|
248
|
+
key: fse.readFileSync(this.serveKeyPath),
|
|
249
|
+
cert: fse.readFileSync(this.serveCertPath)
|
|
250
|
+
},
|
|
251
|
+
// agent: new https.Agent({
|
|
252
|
+
// secureOptions: crypto.constants.SSL_OP_ALLOW_UNSAFE_LEGACY_RENEGOTIATION,
|
|
253
|
+
// }),
|
|
254
|
+
// changeOrigin: true,
|
|
255
|
+
secure: true
|
|
256
|
+
} : { target };
|
|
257
|
+
}
|
|
258
|
+
getNotFoundMsg(req, res, port, type) {
|
|
259
|
+
return `[vpn-split] You are requesting a URL that is not in proxy reach [${type}]
|
|
260
|
+
Protocol: ${req.protocol}
|
|
261
|
+
Hostname: ${req.hostname}
|
|
262
|
+
OriginalUrl: ${req.originalUrl}
|
|
263
|
+
Req.method: ${req.method}
|
|
264
|
+
Port: ${port}
|
|
265
|
+
SERVERS_PATH: ${SERVERS_PATH}`;
|
|
266
|
+
}
|
|
267
|
+
getMaybeChangeOriginTrueMsg(req, res, port, type) {
|
|
268
|
+
return `[vpn-split] Possibly need changeOrigin: true in your proxy config
|
|
269
|
+
Protocol: ${req.protocol}
|
|
270
|
+
Hostname: ${req.hostname}
|
|
271
|
+
OriginalUrl: ${req.originalUrl}
|
|
272
|
+
Req.method: ${req.method}
|
|
273
|
+
Port: ${port}`;
|
|
274
|
+
}
|
|
275
|
+
filterHeaders(req, res) {
|
|
276
|
+
const headersToRemove = [
|
|
277
|
+
// 'Strict-Transport-Security',
|
|
278
|
+
// 'Content-Security-Policy',
|
|
279
|
+
// ...
|
|
280
|
+
];
|
|
281
|
+
headersToRemove.forEach((headerName) => {
|
|
282
|
+
delete req.headers[headerName];
|
|
283
|
+
res.setHeader(headerName, "");
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
isHttpsPort(port) {
|
|
287
|
+
return [443, 4443, 9443].includes(port);
|
|
288
|
+
}
|
|
289
|
+
//#region server passthrough (TCP/HTTPS)
|
|
290
|
+
async serverPassthrough(portToPassthrough) {
|
|
291
|
+
const isHttps = this.isHttpsPort(portToPassthrough);
|
|
292
|
+
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
|
|
293
|
+
const app = express();
|
|
294
|
+
const proxy = httpProxy.createProxyServer({});
|
|
295
|
+
const localIp = await UtilsNetwork__NS__getLocalIpAddresses();
|
|
296
|
+
const currentLocalIps = [
|
|
297
|
+
CoreModels__NS__localhostDomain,
|
|
298
|
+
CoreModels__NS__localhostIp127,
|
|
299
|
+
...localIp.map((a) => a.address)
|
|
300
|
+
];
|
|
301
|
+
app.use(
|
|
302
|
+
(req, res, next) => {
|
|
303
|
+
this.filterHeaders(req, res);
|
|
304
|
+
if (currentLocalIps.includes(req.hostname)) {
|
|
305
|
+
if (req.method === "GET" && req.originalUrl === SERVERS_PATH) {
|
|
306
|
+
res.send(
|
|
307
|
+
JSON.stringify(
|
|
308
|
+
this.hostsArrWithoutDefault.map((h) => ({
|
|
309
|
+
ip: h.ip,
|
|
310
|
+
alias: HelpersTaon__NS__arrays__NS__from(h.aliases).join(" ")
|
|
311
|
+
}))
|
|
312
|
+
)
|
|
313
|
+
);
|
|
314
|
+
} else {
|
|
315
|
+
const msg = this.getNotFoundMsg(
|
|
316
|
+
req,
|
|
317
|
+
res,
|
|
318
|
+
portToPassthrough,
|
|
319
|
+
"server"
|
|
320
|
+
);
|
|
321
|
+
log.d(msg);
|
|
322
|
+
res.send(msg);
|
|
323
|
+
}
|
|
324
|
+
next();
|
|
325
|
+
} else {
|
|
326
|
+
proxy.web(
|
|
327
|
+
req,
|
|
328
|
+
res,
|
|
329
|
+
this.getProxyConfig({ req, res, port: portToPassthrough, isHttps }),
|
|
330
|
+
next
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
);
|
|
335
|
+
const server = isHttps ? https.createServer(
|
|
336
|
+
{
|
|
337
|
+
key: fse.readFileSync(this.serveKeyPath),
|
|
338
|
+
cert: fse.readFileSync(this.serveCertPath)
|
|
339
|
+
},
|
|
340
|
+
app
|
|
341
|
+
) : http.createServer(app);
|
|
342
|
+
await Helpers__NS__killProcessByPort(portToPassthrough, { silent: true });
|
|
343
|
+
await new Promise((resolve, reject) => {
|
|
344
|
+
server.listen(portToPassthrough, () => {
|
|
345
|
+
console.log(
|
|
346
|
+
`TCP/HTTPS server listening on port ${portToPassthrough} (secure=${isHttps})`
|
|
347
|
+
);
|
|
348
|
+
resolve();
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
//#endregion
|
|
353
|
+
//#region client passthrough (TCP/HTTPS)
|
|
354
|
+
async clientPassthrough(portToPassthrough, vpnServerTargets, hostsArr) {
|
|
355
|
+
const aliasToOriginHostname = {};
|
|
356
|
+
for (const h of hostsArr) {
|
|
357
|
+
for (const alias of h.aliases) {
|
|
358
|
+
aliasToOriginHostname[alias] = h.originHostname;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
delete aliasToOriginHostname["localhost"];
|
|
362
|
+
delete aliasToOriginHostname["broadcasthost"];
|
|
363
|
+
const originToUrlMap = {};
|
|
364
|
+
for (const url of vpnServerTargets) {
|
|
365
|
+
originToUrlMap[url.hostname] = url;
|
|
366
|
+
}
|
|
367
|
+
const isHttps = this.isHttpsPort(portToPassthrough);
|
|
368
|
+
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
|
|
369
|
+
const app = express();
|
|
370
|
+
const proxy = httpProxy.createProxyServer({});
|
|
371
|
+
app.use(
|
|
372
|
+
(req, res, next) => {
|
|
373
|
+
this.filterHeaders(req, res);
|
|
374
|
+
const originHostname = aliasToOriginHostname[req.hostname];
|
|
375
|
+
if (!originHostname) {
|
|
376
|
+
const msg = this.getMaybeChangeOriginTrueMsg(
|
|
377
|
+
req,
|
|
378
|
+
res,
|
|
379
|
+
portToPassthrough,
|
|
380
|
+
"client"
|
|
381
|
+
);
|
|
382
|
+
res.send(msg);
|
|
383
|
+
next();
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
const targetUrlObj = originToUrlMap[originHostname];
|
|
387
|
+
if (!targetUrlObj) {
|
|
388
|
+
const notFoundMsg = this.getNotFoundMsg(
|
|
389
|
+
req,
|
|
390
|
+
res,
|
|
391
|
+
portToPassthrough,
|
|
392
|
+
"client"
|
|
393
|
+
);
|
|
394
|
+
res.send(notFoundMsg);
|
|
395
|
+
next();
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
proxy.web(
|
|
399
|
+
req,
|
|
400
|
+
res,
|
|
401
|
+
this.getProxyConfig({
|
|
402
|
+
req,
|
|
403
|
+
res,
|
|
404
|
+
port: portToPassthrough,
|
|
405
|
+
isHttps,
|
|
406
|
+
hostname: targetUrlObj.hostname
|
|
407
|
+
}),
|
|
408
|
+
next
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
);
|
|
412
|
+
const server = isHttps ? https.createServer(
|
|
413
|
+
{
|
|
414
|
+
key: fse.readFileSync(this.serveKeyPath),
|
|
415
|
+
cert: fse.readFileSync(this.serveCertPath)
|
|
416
|
+
},
|
|
417
|
+
app
|
|
418
|
+
) : http.createServer(app);
|
|
419
|
+
await Helpers__NS__killProcessByPort(portToPassthrough, { silent: true });
|
|
420
|
+
await new Promise((resolve, reject) => {
|
|
421
|
+
server.listen(portToPassthrough, () => {
|
|
422
|
+
log.i(
|
|
423
|
+
`TCP/HTTPS client listening on port ${portToPassthrough} (secure=${isHttps})`
|
|
424
|
+
);
|
|
425
|
+
resolve();
|
|
426
|
+
});
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
//#endregion
|
|
430
|
+
//#region UDP passthrough
|
|
431
|
+
/**
|
|
432
|
+
* Start a UDP socket for “server” mode on a given port.
|
|
433
|
+
* This example forwards inbound messages right back to the sender,
|
|
434
|
+
* or you can forward them to an external IP:port if desired.
|
|
435
|
+
*/
|
|
436
|
+
async serverUdpPassthrough(port) {
|
|
437
|
+
return;
|
|
438
|
+
const socket = dgram.createSocket("udp4");
|
|
439
|
+
socket.on("message", (msg, rinfo) => {
|
|
440
|
+
socket.send(msg, 0, msg.length, rinfo.port, rinfo.address, (err) => {
|
|
441
|
+
if (err) {
|
|
442
|
+
log.er(`UDP server send error: ${err}`);
|
|
443
|
+
}
|
|
444
|
+
});
|
|
445
|
+
});
|
|
446
|
+
socket.on("listening", () => {
|
|
447
|
+
const address = socket.address();
|
|
448
|
+
log.i(`UDP server listening at ${address.address}:${address.port}`);
|
|
449
|
+
});
|
|
450
|
+
socket.bind(port);
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* Start a UDP socket for “client” mode on a given port.
|
|
454
|
+
* This example also just does a trivial pass-through or echo,
|
|
455
|
+
* but you can adapt to forward to a remote server.
|
|
456
|
+
*/
|
|
457
|
+
async clientUdpPassthrough(port, vpnServerTargets, hostsArr) {
|
|
458
|
+
return;
|
|
459
|
+
const socket = dgram.createSocket("udp4");
|
|
460
|
+
const primaryTarget = vpnServerTargets[0];
|
|
461
|
+
const targetHost = primaryTarget.hostname;
|
|
462
|
+
const targetPort = port;
|
|
463
|
+
const clientMap = /* @__PURE__ */ new Map();
|
|
464
|
+
socket.on("message", (msg, rinfo) => {
|
|
465
|
+
const isFromLocal = !___NS__includes(
|
|
466
|
+
hostsArr.map((h) => h.ipOrDomain),
|
|
467
|
+
rinfo.address
|
|
468
|
+
);
|
|
469
|
+
if (isFromLocal) {
|
|
470
|
+
const key = `local-${rinfo.address}:${rinfo.port}`;
|
|
471
|
+
clientMap.set(key, rinfo);
|
|
472
|
+
socket.send(msg, 0, msg.length, targetPort, targetHost, (err) => {
|
|
473
|
+
if (err) {
|
|
474
|
+
log.er(`UDP client forward error: ${err}`);
|
|
475
|
+
}
|
|
476
|
+
});
|
|
477
|
+
} else {
|
|
478
|
+
clientMap.forEach((localRinfo, key) => {
|
|
479
|
+
socket.send(
|
|
480
|
+
msg,
|
|
481
|
+
0,
|
|
482
|
+
msg.length,
|
|
483
|
+
localRinfo.port,
|
|
484
|
+
localRinfo.address,
|
|
485
|
+
(err) => {
|
|
486
|
+
if (err) {
|
|
487
|
+
log.er(`UDP client re-send error: ${err}`);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
);
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
});
|
|
494
|
+
socket.on("listening", () => {
|
|
495
|
+
const address = socket.address();
|
|
496
|
+
log.i(
|
|
497
|
+
`UDP client listening at ${address.address}:${address.port}, forwarding to ${targetHost}:${targetPort}`
|
|
498
|
+
);
|
|
499
|
+
});
|
|
500
|
+
socket.bind(port);
|
|
501
|
+
}
|
|
502
|
+
//#endregion
|
|
503
|
+
//#region private methods / get port from request
|
|
504
|
+
getPortFromRequest(req) {
|
|
505
|
+
const host = req.headers.host;
|
|
506
|
+
const protocol = req.protocol;
|
|
507
|
+
if (host) {
|
|
508
|
+
const hostParts = host.split(":");
|
|
509
|
+
if (hostParts.length === 2) {
|
|
510
|
+
return Number(hostParts[1]);
|
|
511
|
+
} else {
|
|
512
|
+
return protocol === "https" ? 443 : 80;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
return 80;
|
|
516
|
+
}
|
|
517
|
+
//#endregion
|
|
518
|
+
// Put this method in your VpnSplit class (or outside as a static helper)
|
|
519
|
+
ensureMkcertCertificate(domains) {
|
|
520
|
+
if (!UtilsOs__NS__commandExistsSync("mkcert")) {
|
|
521
|
+
Helpers__NS__error("[vpn-split] mkcert is not installed.", false, true);
|
|
522
|
+
}
|
|
523
|
+
const certPath = this.serveCertPath;
|
|
524
|
+
const keyPath = this.serveKeyPath;
|
|
525
|
+
const allNames = [
|
|
526
|
+
"localhost",
|
|
527
|
+
"127.0.0.1",
|
|
528
|
+
// '::1',
|
|
529
|
+
// optional: common local networks so the cert also works for raw IP access
|
|
530
|
+
// '10.0.0.0/8',
|
|
531
|
+
// '172.16.0.0/12',
|
|
532
|
+
// '192.168.0.0/16',
|
|
533
|
+
...domains
|
|
534
|
+
// <-- your 120+ domains here
|
|
535
|
+
];
|
|
536
|
+
const runMkcert = (args) => {
|
|
537
|
+
try {
|
|
538
|
+
return execSync(`mkcert ${args}`, { stdio: "pipe" }).toString().trim();
|
|
539
|
+
} catch (err) {
|
|
540
|
+
const stderr = err.stderr?.toString() || "";
|
|
541
|
+
if (stderr.includes("command not found")) {
|
|
542
|
+
Helpers__NS__error(
|
|
543
|
+
"[vpn-split] mkcert is not installed. Install it first: https://github.com/FiloSottile/mkcert",
|
|
544
|
+
false,
|
|
545
|
+
true
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
throw err;
|
|
549
|
+
}
|
|
550
|
+
};
|
|
551
|
+
let caInstalled = false;
|
|
552
|
+
try {
|
|
553
|
+
const output = runMkcert("-CAROOT");
|
|
554
|
+
const rootDir = crossPlatformPath(output.trim());
|
|
555
|
+
if (rootDir && fse.pathExistsSync(crossPlatformPath([rootDir, "rootCA.pem"]))) {
|
|
556
|
+
Helpers__NS__info("[vpn-split] mkcert local CA is already installed");
|
|
557
|
+
caInstalled = true;
|
|
558
|
+
} else {
|
|
559
|
+
Helpers__NS__info("[vpn-split] mkcert local CA is NOT installed");
|
|
560
|
+
}
|
|
561
|
+
} catch {
|
|
562
|
+
Helpers__NS__info("[vpn-split] mkcert error checking local CA installation");
|
|
563
|
+
}
|
|
564
|
+
if (!caInstalled) {
|
|
565
|
+
Helpers__NS__info(
|
|
566
|
+
"[vpn-split] Installing mkcert local CA (requires sudo once)..."
|
|
567
|
+
);
|
|
568
|
+
try {
|
|
569
|
+
execSync("mkcert -install", { stdio: "inherit" });
|
|
570
|
+
Helpers__NS__info("[vpn-split] Local CA installed and trusted system-wide");
|
|
571
|
+
} catch (err) {
|
|
572
|
+
Helpers__NS__error(
|
|
573
|
+
'[vpn-split] Failed to run "mkcert -install". Run it manually with sudo.',
|
|
574
|
+
false,
|
|
575
|
+
true
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
const isWindows = process.platform === "win32";
|
|
580
|
+
let tempCertFiles = [];
|
|
581
|
+
let tempKeyFiles = [];
|
|
582
|
+
if (allNames.length <= 20 || !isWindows) {
|
|
583
|
+
const namesArg = allNames.map((d) => `"${d}"`).join(" ");
|
|
584
|
+
const mkcertCmd = `-key-file "${keyPath}" -cert-file "${certPath}" ${namesArg}`;
|
|
585
|
+
fse.ensureDirSync(path.dirname(certPath));
|
|
586
|
+
runMkcert(mkcertCmd);
|
|
587
|
+
} else {
|
|
588
|
+
const chunkSize = 20;
|
|
589
|
+
fse.ensureDirSync(path.dirname(certPath));
|
|
590
|
+
for (let i = 0; i < allNames.length; i += chunkSize) {
|
|
591
|
+
const chunk = allNames.slice(i, i + chunkSize);
|
|
592
|
+
const tempCert = `${certPath}.${i}.pem`;
|
|
593
|
+
const tempKey = `${keyPath}.${i}.pem`;
|
|
594
|
+
const namesArg = chunk.map((d) => `"${d}"`).join(" ");
|
|
595
|
+
const mkcertCmd = `-key-file "${tempKey}" -cert-file "${tempCert}" ${namesArg}`;
|
|
596
|
+
Helpers__NS__info(
|
|
597
|
+
`[vpn-split] Generating batch ${Math.floor(i / chunkSize) + 1}/${Math.ceil(allNames.length / chunkSize)}...`
|
|
598
|
+
);
|
|
599
|
+
runMkcert(mkcertCmd);
|
|
600
|
+
tempCertFiles.push(tempCert);
|
|
601
|
+
tempKeyFiles.push(tempKey);
|
|
602
|
+
}
|
|
603
|
+
Helpers__NS__info("[vpn-split] Merging batches into final cert...");
|
|
604
|
+
const firstKey = tempKeyFiles[0];
|
|
605
|
+
fse.copySync(firstKey, keyPath);
|
|
606
|
+
const mergedCertContent = tempCertFiles.map((f) => fse.readFileSync(f, "utf8")).join("\n");
|
|
607
|
+
fse.writeFileSync(certPath, mergedCertContent);
|
|
608
|
+
tempCertFiles.forEach((f) => fse.removeSync(f));
|
|
609
|
+
tempKeyFiles.slice(1).forEach((f) => fse.removeSync(f));
|
|
610
|
+
Helpers__NS__info("[vpn-split] Merged cert ready!");
|
|
611
|
+
}
|
|
612
|
+
if (!fse.existsSync(certPath) || !fse.existsSync(keyPath)) {
|
|
613
|
+
Helpers__NS__error("[vpn-split] Files missing after generation!", false, true);
|
|
614
|
+
} else {
|
|
615
|
+
Helpers__NS__info(
|
|
616
|
+
`[vpn-split] Trusted cert ready: ${allNames.length} hostnames covered`
|
|
617
|
+
);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
//#region private methods / prevent bad target for client
|
|
621
|
+
async preventBadTargetForClient(vpnServerTarget) {
|
|
622
|
+
if (!vpnServerTarget) {
|
|
623
|
+
const currentLocalIp = await UtilsNetwork__NS__getFirstIpV4LocalActiveIpAddress();
|
|
624
|
+
Helpers__NS__error(
|
|
625
|
+
`[vpn-server] Please provide a correct target server.
|
|
626
|
+
Example:
|
|
627
|
+
vpn-server ${currentLocalIp}
|
|
628
|
+
|
|
629
|
+
Your local IP is: ${currentLocalIp}`,
|
|
630
|
+
false,
|
|
631
|
+
true
|
|
632
|
+
);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
//#endregion
|
|
636
|
+
}
|
|
637
|
+
const genMsg = `
|
|
638
|
+
################################################
|
|
639
|
+
## This file is generated #####################
|
|
640
|
+
################################################
|
|
641
|
+
`.trim() + EOL;
|
|
642
|
+
function saveHosts(hosts, options) {
|
|
643
|
+
const { saveHostInUserFolder } = options || {};
|
|
644
|
+
if (___NS__isArray(hosts)) {
|
|
645
|
+
hosts = hosts.reduce((prev, curr) => {
|
|
646
|
+
return ___NS__merge(prev, {
|
|
647
|
+
[curr.name]: curr
|
|
648
|
+
});
|
|
649
|
+
}, {});
|
|
650
|
+
}
|
|
651
|
+
const toSave = parseHost(hosts, !!saveHostInUserFolder);
|
|
652
|
+
if (saveHostInUserFolder) {
|
|
653
|
+
Helpers__NS__writeFile(HOST_FILE_PATHUSER, toSave);
|
|
654
|
+
} else {
|
|
655
|
+
Helpers__NS__writeFile(HOST_FILE_PATH, toSave);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
function saveHostsLocal(hosts, options) {
|
|
659
|
+
const { saveHostInUserFolder } = options || {};
|
|
660
|
+
if (___NS__isArray(hosts)) {
|
|
661
|
+
hosts = hosts.reduce((prev, curr) => {
|
|
662
|
+
return ___NS__merge(prev, {
|
|
663
|
+
[curr.name]: curr
|
|
664
|
+
});
|
|
665
|
+
}, {});
|
|
666
|
+
}
|
|
667
|
+
const toSave = parseHost(hosts, !!saveHostInUserFolder, true);
|
|
668
|
+
if (saveHostInUserFolder) {
|
|
669
|
+
Helpers__NS__writeFile(HOST_FILE_PATHUSER, toSave);
|
|
670
|
+
} else {
|
|
671
|
+
Helpers__NS__writeFile(HOST_FILE_PATH, toSave);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
function parseHost(hosts, saveHostInUserFolder, useLocal = false) {
|
|
675
|
+
hosts = {
|
|
676
|
+
...DefaultEtcHosts,
|
|
677
|
+
...hosts
|
|
678
|
+
};
|
|
679
|
+
___NS__keys(hosts).forEach((hostName) => {
|
|
680
|
+
const v = hosts[hostName];
|
|
681
|
+
v.name = hostName;
|
|
682
|
+
});
|
|
683
|
+
return genMsg + EOL + ___NS__keys(hosts).map((hostName) => {
|
|
684
|
+
const v = hosts[hostName];
|
|
685
|
+
if (v.skipUpdateOfServerEtcHosts) {
|
|
686
|
+
console.warn(
|
|
687
|
+
`[vpn-split] Skip saving host: ${v.name} (${v.ipOrDomain})`
|
|
688
|
+
);
|
|
689
|
+
return `# SKIPPING HOST ${v.ipOrDomain} ${v.aliases.join(" ")} ${GENERATED}`;
|
|
690
|
+
}
|
|
691
|
+
const aliasesStr = v.aliases.join(" ");
|
|
692
|
+
if (saveHostInUserFolder) {
|
|
693
|
+
return useLocal ? `127.0.0.1 ${aliasesStr}` : `${v.disabled ? "#" : ""}${v.ipOrDomain} ${aliasesStr}`;
|
|
694
|
+
}
|
|
695
|
+
return useLocal ? `127.0.0.1 ${aliasesStr}` : `${v.disabled ? "#" : ""}${v.ipOrDomain} ${aliasesStr} # ${v.name} ${GENERATED}`;
|
|
696
|
+
}).join(EOL) + EOL + EOL + genMsg;
|
|
697
|
+
}
|
|
698
|
+
export {
|
|
699
|
+
VpnSplit
|
|
700
|
+
};
|
package/package.json
CHANGED