opentunnel-cli 1.0.15 → 1.0.17
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/README.md +106 -0
- package/dist/cli/index.js +137 -22
- package/dist/cli/index.js.map +1 -1
- package/dist/server/CertManager.d.ts +5 -4
- package/dist/server/CertManager.d.ts.map +1 -1
- package/dist/server/CertManager.js +28 -15
- package/dist/server/CertManager.js.map +1 -1
- package/dist/server/TunnelServer.d.ts +1 -0
- package/dist/server/TunnelServer.d.ts.map +1 -1
- package/dist/server/TunnelServer.js +38 -16
- package/dist/server/TunnelServer.js.map +1 -1
- package/dist/shared/types.d.ts +5 -0
- package/dist/shared/types.d.ts.map +1 -1
- package/dist/shared/utils.d.ts +9 -0
- package/dist/shared/utils.d.ts.map +1 -1
- package/dist/shared/utils.js +26 -0
- package/dist/shared/utils.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
- [As a Client](#-as-a-client) - Expose your local ports
|
|
10
10
|
- [As a Server](#-as-a-server) - Host your own tunnel server
|
|
11
11
|
- [Home Use](#-home-use-behind-routernat) - Run from home network
|
|
12
|
+
- [Multi-Domain Support](#-multi-domain-support) - Handle multiple domains on one server
|
|
12
13
|
- [Authentication](#-authentication) - Secure your server
|
|
13
14
|
- [IP Access Control](#-ip-access-control) - Allow/deny IPs and CIDR ranges
|
|
14
15
|
- [Configuration File](#-configuration-file) - opentunnel.yml reference
|
|
@@ -352,6 +353,111 @@ opentunnel server -d --domain yourdomain.com -p 8443 --public-port 8443
|
|
|
352
353
|
|
|
353
354
|
---
|
|
354
355
|
|
|
356
|
+
# 🌐 Multi-Domain Support
|
|
357
|
+
|
|
358
|
+
OpenTunnel can handle **multiple domains** on a single server instance. This is useful when you want to:
|
|
359
|
+
|
|
360
|
+
- Serve tunnels under different domain names
|
|
361
|
+
- Use different base paths for different domains
|
|
362
|
+
- Consolidate multiple domains on one server
|
|
363
|
+
|
|
364
|
+
## DNS Configuration
|
|
365
|
+
|
|
366
|
+
For each domain you want to use, create DNS records pointing to your server:
|
|
367
|
+
|
|
368
|
+
| Domain | Type | Name | Value |
|
|
369
|
+
|--------|------|------|-------|
|
|
370
|
+
| `domain1.com` | A | `op` | `YOUR_SERVER_IP` |
|
|
371
|
+
| `domain1.com` | A | `*.op` | `YOUR_SERVER_IP` |
|
|
372
|
+
| `domain2.com` | A | `op` | `YOUR_SERVER_IP` |
|
|
373
|
+
| `domain2.com` | A | `*.op` | `YOUR_SERVER_IP` |
|
|
374
|
+
|
|
375
|
+
## Configuration
|
|
376
|
+
|
|
377
|
+
Configure multiple domains in `opentunnel.yml`:
|
|
378
|
+
|
|
379
|
+
```yaml
|
|
380
|
+
server:
|
|
381
|
+
domains:
|
|
382
|
+
- domain: domain1.com
|
|
383
|
+
basePath: op # Tunnels at: *.op.domain1.com
|
|
384
|
+
- domain: domain2.com
|
|
385
|
+
basePath: op # Tunnels at: *.op.domain2.com
|
|
386
|
+
port: 443
|
|
387
|
+
|
|
388
|
+
tunnels:
|
|
389
|
+
- name: app
|
|
390
|
+
protocol: http
|
|
391
|
+
port: 3000
|
|
392
|
+
subdomain: myapp # Accessible on ALL configured domains
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
With this configuration, the tunnel `myapp` is accessible at:
|
|
396
|
+
- `https://myapp.op.domain1.com`
|
|
397
|
+
- `https://myapp.op.domain2.com`
|
|
398
|
+
|
|
399
|
+
## Single Domain (Backward Compatible)
|
|
400
|
+
|
|
401
|
+
If you only need one domain, use the simple configuration:
|
|
402
|
+
|
|
403
|
+
```yaml
|
|
404
|
+
server:
|
|
405
|
+
domain: example.com # Single domain
|
|
406
|
+
basePath: op # Default: op
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
This is equivalent to:
|
|
410
|
+
|
|
411
|
+
```yaml
|
|
412
|
+
server:
|
|
413
|
+
domains:
|
|
414
|
+
- domain: example.com
|
|
415
|
+
basePath: op
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
## SSL Certificates
|
|
419
|
+
|
|
420
|
+
When using self-signed certificates with multiple domains, OpenTunnel automatically generates a **SAN (Subject Alternative Name) certificate** that covers all configured domains and their wildcards.
|
|
421
|
+
|
|
422
|
+
For Let's Encrypt, you'll need separate certificates or a multi-domain certificate with all your domains listed.
|
|
423
|
+
|
|
424
|
+
## Use Cases
|
|
425
|
+
|
|
426
|
+
### Different Teams/Projects
|
|
427
|
+
|
|
428
|
+
```yaml
|
|
429
|
+
server:
|
|
430
|
+
domains:
|
|
431
|
+
- domain: dev.company.com
|
|
432
|
+
basePath: op # Dev team: *.op.dev.company.com
|
|
433
|
+
- domain: staging.company.com
|
|
434
|
+
basePath: op # Staging: *.op.staging.company.com
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
### White-Label Service
|
|
438
|
+
|
|
439
|
+
```yaml
|
|
440
|
+
server:
|
|
441
|
+
domains:
|
|
442
|
+
- domain: client1.com
|
|
443
|
+
basePath: op # Client 1: *.op.client1.com
|
|
444
|
+
- domain: client2.com
|
|
445
|
+
basePath: op # Client 2: *.op.client2.com
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
### Migration Between Domains
|
|
449
|
+
|
|
450
|
+
```yaml
|
|
451
|
+
server:
|
|
452
|
+
domains:
|
|
453
|
+
- domain: newdomain.com
|
|
454
|
+
basePath: op # New domain (primary)
|
|
455
|
+
- domain: olddomain.com
|
|
456
|
+
basePath: op # Old domain (still supported)
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
---
|
|
460
|
+
|
|
355
461
|
# 🔐 Authentication
|
|
356
462
|
|
|
357
463
|
OpenTunnel uses a **shared secret** system for authentication. The server defines a list of valid tokens, and clients must provide one to connect.
|
package/dist/cli/index.js
CHANGED
|
@@ -156,7 +156,7 @@ program
|
|
|
156
156
|
.name("opentunnel")
|
|
157
157
|
.alias("ot")
|
|
158
158
|
.description("Expose local ports to the internet via custom domains or ngrok")
|
|
159
|
-
.version("1.0.
|
|
159
|
+
.version("1.0.17");
|
|
160
160
|
// Helper function to build WebSocket URL from domain
|
|
161
161
|
// User only provides base domain (e.g., fjrg2007.com), system handles the rest
|
|
162
162
|
// Note: --insecure flag only affects certificate verification, not the protocol
|
|
@@ -1188,15 +1188,18 @@ program
|
|
|
1188
1188
|
});
|
|
1189
1189
|
child.unref();
|
|
1190
1190
|
fsModule.writeFileSync(pidFile, String(child.pid));
|
|
1191
|
-
// Wait
|
|
1192
|
-
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
1191
|
+
// Wait and verify process is still running (check multiple times)
|
|
1193
1192
|
let isRunning = false;
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1193
|
+
for (let i = 0; i < 5; i++) {
|
|
1194
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
1195
|
+
try {
|
|
1196
|
+
process.kill(child.pid, 0);
|
|
1197
|
+
isRunning = true;
|
|
1198
|
+
}
|
|
1199
|
+
catch {
|
|
1200
|
+
isRunning = false;
|
|
1201
|
+
break;
|
|
1202
|
+
}
|
|
1200
1203
|
}
|
|
1201
1204
|
if (isRunning) {
|
|
1202
1205
|
// Register in global registry
|
|
@@ -1217,24 +1220,47 @@ program
|
|
|
1217
1220
|
console.log(chalk_1.default.gray(`Stop with: opentunnel down ${instanceName}`));
|
|
1218
1221
|
console.log(chalk_1.default.gray(`Stop all: opentunnel down --all`));
|
|
1219
1222
|
console.log(chalk_1.default.gray(`List: opentunnel ps`));
|
|
1220
|
-
console.log(chalk_1.default.gray(`Logs:
|
|
1223
|
+
console.log(chalk_1.default.gray(`Logs: opentunnel logs ${instanceName}`));
|
|
1221
1224
|
}
|
|
1222
1225
|
else {
|
|
1223
|
-
// Process died
|
|
1224
|
-
console.log(chalk_1.default.red(
|
|
1225
|
-
|
|
1226
|
-
// Read last few lines of log
|
|
1226
|
+
// Process died - show error from log
|
|
1227
|
+
console.log(chalk_1.default.red(`\n✗ OpenTunnel "${instanceName}" failed to start\n`));
|
|
1228
|
+
// Read log and find error
|
|
1227
1229
|
try {
|
|
1228
1230
|
const logContent = fsModule.readFileSync(logFile, "utf-8");
|
|
1229
|
-
const lines = logContent.trim().split("\n")
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1231
|
+
const lines = logContent.trim().split("\n");
|
|
1232
|
+
// Look for common errors
|
|
1233
|
+
const errorLine = lines.find(l => l.includes("EADDRINUSE") ||
|
|
1234
|
+
l.includes("EACCES") ||
|
|
1235
|
+
l.includes("Error:") ||
|
|
1236
|
+
l.includes("error:"));
|
|
1237
|
+
if (errorLine) {
|
|
1238
|
+
if (errorLine.includes("EADDRINUSE")) {
|
|
1239
|
+
console.log(chalk_1.default.red(" Error: Port already in use"));
|
|
1240
|
+
console.log(chalk_1.default.gray(" Another process is using the port. Try:"));
|
|
1241
|
+
console.log(chalk_1.default.cyan(" - Stop other OpenTunnel: opentunnel down --all"));
|
|
1242
|
+
console.log(chalk_1.default.cyan(" - Use different port in config: server.port: 8443"));
|
|
1243
|
+
}
|
|
1244
|
+
else if (errorLine.includes("EACCES")) {
|
|
1245
|
+
console.log(chalk_1.default.red(" Error: Permission denied"));
|
|
1246
|
+
console.log(chalk_1.default.gray(" Port 443 requires admin/root. Try:"));
|
|
1247
|
+
console.log(chalk_1.default.cyan(" - Run as administrator"));
|
|
1248
|
+
console.log(chalk_1.default.cyan(" - Use port > 1024 in config"));
|
|
1249
|
+
}
|
|
1250
|
+
else {
|
|
1251
|
+
console.log(chalk_1.default.red(` ${errorLine}`));
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
// Show last few lines
|
|
1255
|
+
console.log(chalk_1.default.gray("\n Last log lines:"));
|
|
1256
|
+
console.log(chalk_1.default.gray(" " + "─".repeat(56)));
|
|
1257
|
+
for (const line of lines.slice(-8)) {
|
|
1258
|
+
console.log(chalk_1.default.gray(` ${line}`));
|
|
1233
1259
|
}
|
|
1234
|
-
console.log(chalk_1.default.gray("─".repeat(
|
|
1260
|
+
console.log(chalk_1.default.gray(" " + "─".repeat(56)));
|
|
1235
1261
|
}
|
|
1236
1262
|
catch {
|
|
1237
|
-
console.log(chalk_1.default.gray(`
|
|
1263
|
+
console.log(chalk_1.default.gray(`Check log: cat ${logFile}`));
|
|
1238
1264
|
}
|
|
1239
1265
|
// Clean up pid file
|
|
1240
1266
|
try {
|
|
@@ -1271,6 +1297,16 @@ program
|
|
|
1271
1297
|
const port = config.server?.port || 443;
|
|
1272
1298
|
const useHttps = config.server?.https !== false;
|
|
1273
1299
|
const hasTunnels = tunnelsToStart.length > 0;
|
|
1300
|
+
// Parse multiple domains from config
|
|
1301
|
+
let serverDomains;
|
|
1302
|
+
if (config.server?.domains && config.server.domains.length > 0) {
|
|
1303
|
+
serverDomains = config.server.domains.map(d => {
|
|
1304
|
+
if (typeof d === "string") {
|
|
1305
|
+
return { domain: d, basePath: basePath };
|
|
1306
|
+
}
|
|
1307
|
+
return { domain: d.domain, basePath: d.basePath || basePath };
|
|
1308
|
+
});
|
|
1309
|
+
}
|
|
1274
1310
|
// Mode detection:
|
|
1275
1311
|
// 1. Explicit mode in config takes priority
|
|
1276
1312
|
// 2. Auto-detect based on config:
|
|
@@ -1338,8 +1374,9 @@ program
|
|
|
1338
1374
|
const server = new TunnelServer({
|
|
1339
1375
|
port,
|
|
1340
1376
|
host: "0.0.0.0",
|
|
1341
|
-
domain,
|
|
1377
|
+
domain: domain || serverDomains?.[0]?.domain || "localhost",
|
|
1342
1378
|
basePath,
|
|
1379
|
+
domains: serverDomains, // Pass multiple domains if configured
|
|
1343
1380
|
tunnelPortRange: {
|
|
1344
1381
|
min: tcpMin,
|
|
1345
1382
|
max: tcpMax,
|
|
@@ -1348,7 +1385,16 @@ program
|
|
|
1348
1385
|
});
|
|
1349
1386
|
try {
|
|
1350
1387
|
await server.start();
|
|
1351
|
-
|
|
1388
|
+
const primaryDomain = serverDomains?.[0]?.domain || domain;
|
|
1389
|
+
const primaryBasePath = serverDomains?.[0]?.basePath || basePath;
|
|
1390
|
+
spinner.succeed(`Server running on https://${primaryBasePath}.${primaryDomain}:${port}`);
|
|
1391
|
+
// Show all domains if multiple configured
|
|
1392
|
+
if (serverDomains && serverDomains.length > 1) {
|
|
1393
|
+
console.log(chalk_1.default.cyan("\nConfigured domains:"));
|
|
1394
|
+
for (const d of serverDomains) {
|
|
1395
|
+
console.log(chalk_1.default.white(` *.${d.basePath}.${d.domain}`));
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1352
1398
|
// Start tunnels if in hybrid mode or if tunnels are defined
|
|
1353
1399
|
if (isHybridMode && hasTunnels) {
|
|
1354
1400
|
console.log(chalk_1.default.cyan(`\nStarting ${tunnelsToStart.length} tunnel(s)...\n`));
|
|
@@ -1512,6 +1558,75 @@ program
|
|
|
1512
1558
|
console.log(chalk_1.default.gray(`Stop all: opentunnel down --all`));
|
|
1513
1559
|
}
|
|
1514
1560
|
});
|
|
1561
|
+
// Logs command - view logs for an instance
|
|
1562
|
+
program
|
|
1563
|
+
.command("logs [name]")
|
|
1564
|
+
.description("View logs for an instance")
|
|
1565
|
+
.option("-f, --follow", "Follow log output (like tail -f)")
|
|
1566
|
+
.option("-n, --lines <n>", "Number of lines to show", "50")
|
|
1567
|
+
.action(async (name, options) => {
|
|
1568
|
+
const fs = await Promise.resolve().then(() => __importStar(require("fs")));
|
|
1569
|
+
const { spawn } = await Promise.resolve().then(() => __importStar(require("child_process")));
|
|
1570
|
+
const registry = loadRegistry();
|
|
1571
|
+
// Find instance
|
|
1572
|
+
let instance;
|
|
1573
|
+
if (name) {
|
|
1574
|
+
instance = registry.instances.find(i => i.name === name);
|
|
1575
|
+
}
|
|
1576
|
+
else {
|
|
1577
|
+
// Use first instance from current directory
|
|
1578
|
+
instance = registry.instances.find(i => i.cwd === process.cwd());
|
|
1579
|
+
}
|
|
1580
|
+
if (!instance) {
|
|
1581
|
+
console.log(chalk_1.default.yellow(name ? `Instance "${name}" not found` : "No instance found in current directory"));
|
|
1582
|
+
console.log(chalk_1.default.gray("Use 'opentunnel ps' to list instances"));
|
|
1583
|
+
return;
|
|
1584
|
+
}
|
|
1585
|
+
if (!fs.existsSync(instance.logFile)) {
|
|
1586
|
+
console.log(chalk_1.default.yellow(`Log file not found: ${instance.logFile}`));
|
|
1587
|
+
return;
|
|
1588
|
+
}
|
|
1589
|
+
if (options.follow) {
|
|
1590
|
+
// Use tail -f equivalent
|
|
1591
|
+
console.log(chalk_1.default.gray(`Following logs for ${instance.name}... (Ctrl+C to stop)\n`));
|
|
1592
|
+
// Read existing content first
|
|
1593
|
+
const content = fs.readFileSync(instance.logFile, "utf-8");
|
|
1594
|
+
const lines = content.split("\n").slice(-parseInt(options.lines));
|
|
1595
|
+
console.log(lines.join("\n"));
|
|
1596
|
+
// Watch for changes
|
|
1597
|
+
let lastSize = fs.statSync(instance.logFile).size;
|
|
1598
|
+
const watcher = setInterval(() => {
|
|
1599
|
+
try {
|
|
1600
|
+
const stat = fs.statSync(instance.logFile);
|
|
1601
|
+
if (stat.size > lastSize) {
|
|
1602
|
+
const fd = fs.openSync(instance.logFile, "r");
|
|
1603
|
+
const buffer = Buffer.alloc(stat.size - lastSize);
|
|
1604
|
+
fs.readSync(fd, buffer, 0, buffer.length, lastSize);
|
|
1605
|
+
fs.closeSync(fd);
|
|
1606
|
+
process.stdout.write(buffer.toString());
|
|
1607
|
+
lastSize = stat.size;
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
catch {
|
|
1611
|
+
clearInterval(watcher);
|
|
1612
|
+
}
|
|
1613
|
+
}, 100);
|
|
1614
|
+
// Handle Ctrl+C
|
|
1615
|
+
process.on("SIGINT", () => {
|
|
1616
|
+
clearInterval(watcher);
|
|
1617
|
+
process.exit(0);
|
|
1618
|
+
});
|
|
1619
|
+
// Keep running
|
|
1620
|
+
await new Promise(() => { });
|
|
1621
|
+
}
|
|
1622
|
+
else {
|
|
1623
|
+
// Just show last N lines
|
|
1624
|
+
const content = fs.readFileSync(instance.logFile, "utf-8");
|
|
1625
|
+
const lines = content.split("\n").slice(-parseInt(options.lines));
|
|
1626
|
+
console.log(chalk_1.default.gray(`Last ${options.lines} lines of ${instance.name}:\n`));
|
|
1627
|
+
console.log(lines.join("\n"));
|
|
1628
|
+
}
|
|
1629
|
+
});
|
|
1515
1630
|
// Test server command - simple HTTP server for testing tunnels
|
|
1516
1631
|
program
|
|
1517
1632
|
.command("test-server")
|