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 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.15");
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 a moment and verify process is still running
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
- try {
1195
- process.kill(child.pid, 0);
1196
- isRunning = true;
1197
- }
1198
- catch {
1199
- isRunning = false;
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: tail -f ${logFile}`));
1223
+ console.log(chalk_1.default.gray(`Logs: opentunnel logs ${instanceName}`));
1221
1224
  }
1222
1225
  else {
1223
- // Process died immediately - show error
1224
- console.log(chalk_1.default.red(`OpenTunnel "${instanceName}" failed to start`));
1225
- console.log(chalk_1.default.gray(`\nCheck the log for errors:`));
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").slice(-10);
1230
- console.log(chalk_1.default.gray("─".repeat(60)));
1231
- for (const line of lines) {
1232
- console.log(chalk_1.default.gray(line));
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(60)));
1260
+ console.log(chalk_1.default.gray(" " + "─".repeat(56)));
1235
1261
  }
1236
1262
  catch {
1237
- console.log(chalk_1.default.gray(` cat ${logFile}`));
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
- spinner.succeed(`Server running on https://${basePath}.${domain}:${port}`);
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")