vpn-split 21.0.19 → 21.0.21

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