opentunnel-cli 1.0.9 → 1.0.11

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
  - [Authentication](#-authentication) - Secure your server
12
+ - [IP Access Control](#-ip-access-control) - Allow/deny IPs and CIDR ranges
12
13
  - [Configuration File](#-configuration-file) - opentunnel.yml reference
13
14
  - [Environment Variables](#environment-variables) - Docker-style ${VAR:-default} syntax
14
15
  - [Commands Reference](#-commands-reference)
@@ -232,6 +233,11 @@ SSL/TLS:
232
233
  --email <email> Email for Let's Encrypt
233
234
  --production Use Let's Encrypt production (not staging)
234
235
  --cloudflare-token <token> Cloudflare API token for DNS-01 challenge
236
+
237
+ IP Access Control:
238
+ --ip-mode <mode> Access mode: all, allowlist, denylist (default: all)
239
+ --ip-allow <ips> Comma-separated IPs/CIDRs to allow
240
+ --ip-deny <ips> Comma-separated IPs/CIDRs to deny
235
241
  ```
236
242
 
237
243
  ## Server Modes
@@ -311,6 +317,60 @@ server:
311
317
 
312
318
  ---
313
319
 
320
+ # 🛡️ IP Access Control
321
+
322
+ Control which IP addresses can connect to your server. By default, all IPs are allowed.
323
+
324
+ ## Access Modes
325
+
326
+ | Mode | Description |
327
+ |------|-------------|
328
+ | `all` | Allow all IPs (default) |
329
+ | `allowlist` | Only allow IPs in the allow list |
330
+ | `denylist` | Deny IPs in the deny list, allow others |
331
+
332
+ ## Command Line
333
+
334
+ ```bash
335
+ # Only allow specific IPs/ranges
336
+ opentunnel server -d --domain example.com --ip-mode allowlist --ip-allow "192.168.1.0/24,10.0.0.1"
337
+
338
+ # Deny specific IPs
339
+ opentunnel server -d --domain example.com --ip-mode denylist --ip-deny "1.2.3.4,5.6.7.0/24"
340
+ ```
341
+
342
+ ## Configuration File
343
+
344
+ ```yaml
345
+ server:
346
+ domain: example.com
347
+ token: ${AUTH_TOKEN}
348
+ ipAccess:
349
+ mode: allowlist # all, allowlist, or denylist
350
+ allowList:
351
+ - 192.168.1.0/24 # Allow entire subnet
352
+ - 10.0.0.1 # Allow single IP
353
+ - 172.16.0.0/16 # Allow another range
354
+ ```
355
+
356
+ ```yaml
357
+ server:
358
+ domain: example.com
359
+ ipAccess:
360
+ mode: denylist
361
+ denyList:
362
+ - 1.2.3.4 # Block single IP
363
+ - 5.6.7.0/24 # Block entire subnet
364
+ ```
365
+
366
+ ## Supported Formats
367
+
368
+ - Single IP: `192.168.1.1`
369
+ - CIDR notation: `192.168.1.0/24` (256 addresses)
370
+ - IPv6: `::1`, `2001:db8::/32`
371
+
372
+ ---
373
+
314
374
  # 📄 Configuration File
315
375
 
316
376
  Create `opentunnel.yml` in your project directory.
package/dist/cli/index.js CHANGED
@@ -67,13 +67,13 @@ function loadEnvFile() {
67
67
  value = value.slice(1, -1);
68
68
  }
69
69
  // Only set if not already defined in environment
70
- if (process.env[key] === undefined) {
70
+ if (process.env[key] === undefined)
71
71
  process.env[key] = value;
72
- }
73
72
  }
74
73
  }
75
74
  }
76
75
  }
76
+ ;
77
77
  // Docker-style environment variable substitution
78
78
  // Supports: ${VAR}, ${VAR:-default}, ${VAR:=default}
79
79
  function substituteEnvVars(content) {
@@ -81,19 +81,17 @@ function substituteEnvVars(content) {
81
81
  const pattern = /\$\{([^}:]+)(?:(:[-=])([^}]*))?\}/g;
82
82
  return content.replace(pattern, (match, varName, operator, defaultValue) => {
83
83
  const envValue = process.env[varName];
84
- if (operator === ":-" || operator === ":=") {
85
- // Use default if variable is unset or empty
84
+ if (operator === ":-" || operator === ":=")
86
85
  return (envValue !== undefined && envValue !== "") ? envValue : (defaultValue || "");
87
- }
88
86
  // Just ${VAR} - return value or empty string
89
87
  return envValue || "";
90
88
  });
91
89
  }
90
+ ;
92
91
  // Load and parse config file with environment variable substitution
93
92
  function loadConfig(configPath) {
94
- if (!fs.existsSync(configPath)) {
93
+ if (!fs.existsSync(configPath))
95
94
  return null;
96
- }
97
95
  // Load .env file first
98
96
  loadEnvFile();
99
97
  // Read and substitute environment variables
@@ -101,12 +99,13 @@ function loadConfig(configPath) {
101
99
  content = substituteEnvVars(content);
102
100
  return (0, yaml_1.parse)(content);
103
101
  }
102
+ ;
104
103
  const program = new commander_1.Command();
105
104
  program
106
105
  .name("opentunnel")
107
106
  .alias("ot")
108
107
  .description("Expose local ports to the internet via custom domains or ngrok")
109
- .version("1.0.0");
108
+ .version("1.0.11");
110
109
  // Helper function to build WebSocket URL from domain
111
110
  // User only provides base domain (e.g., fjrg2007.com), system handles the rest
112
111
  // Note: --insecure flag only affects certificate verification, not the protocol
@@ -166,7 +165,7 @@ program
166
165
  token: options.token,
167
166
  reconnect: true,
168
167
  silent: true,
169
- rejectUnauthorized: !insecure,
168
+ rejectUnauthorized: !insecure
170
169
  });
171
170
  await client.connect();
172
171
  spinner.text = "Creating tunnel...";
@@ -200,18 +199,16 @@ program
200
199
  publicUrl = result.publicUrl;
201
200
  usedInsecure = true;
202
201
  }
203
- else {
202
+ else
204
203
  throw firstErr;
205
- }
206
204
  }
207
205
  spinner.succeed("Tunnel established!");
208
206
  console.log("");
209
207
  console.log(chalk_1.default.cyan(` OpenTunnel ${chalk_1.default.gray(`(via ${serverDisplayName})`)}`));
210
208
  console.log(chalk_1.default.gray(" ─────────────────────────────────────────"));
211
209
  console.log(` ${chalk_1.default.white("Status:")} ${chalk_1.default.green("● Online")}`);
212
- if (usedInsecure && !options.insecure) {
210
+ if (usedInsecure && !options.insecure)
213
211
  console.log(` ${chalk_1.default.white("Security:")} ${chalk_1.default.yellow("⚠ Insecure (self-signed cert)")}`);
214
- }
215
212
  console.log(` ${chalk_1.default.white("Protocol:")} ${chalk_1.default.yellow(options.protocol.toUpperCase())}`);
216
213
  console.log(` ${chalk_1.default.white("Local:")} ${chalk_1.default.gray(`${options.host}:${port}`)}`);
217
214
  console.log(` ${chalk_1.default.white("Public:")} ${chalk_1.default.green(publicUrl)}`);
@@ -335,7 +332,7 @@ program
335
332
  subdomain,
336
333
  serverUrl,
337
334
  token: options.token,
338
- insecure: true,
335
+ insecure: true
339
336
  });
340
337
  }
341
338
  catch (error) {
@@ -365,7 +362,7 @@ program
365
362
  localPort: parseInt(port),
366
363
  remotePort: options.remotePort ? parseInt(options.remotePort) : undefined,
367
364
  authtoken: options.token,
368
- region: options.region,
365
+ region: options.region
369
366
  });
370
367
  return;
371
368
  }
@@ -379,7 +376,7 @@ program
379
376
  remotePort: options.remotePort ? parseInt(options.remotePort) : undefined,
380
377
  serverUrl,
381
378
  token: options.token,
382
- insecure: options.insecure,
379
+ insecure: options.insecure
383
380
  });
384
381
  return;
385
382
  }
@@ -402,7 +399,7 @@ program
402
399
  domain,
403
400
  basePath: "op",
404
401
  tunnelPortRange: { min: 10000, max: 20000 },
405
- selfSignedHttps: { enabled: true },
402
+ selfSignedHttps: { enabled: true }
406
403
  });
407
404
  try {
408
405
  await server.start();
@@ -417,7 +414,7 @@ program
417
414
  remotePort: options.remotePort ? parseInt(options.remotePort) : undefined,
418
415
  serverUrl,
419
416
  token: options.token,
420
- insecure: true,
417
+ insecure: true
421
418
  });
422
419
  }
423
420
  catch (error) {
@@ -451,7 +448,7 @@ program
451
448
  localHost: "localhost",
452
449
  localPort: parseInt(port),
453
450
  subdomain: options.subdomain,
454
- authtoken: options.token,
451
+ authtoken: options.token
455
452
  });
456
453
  }
457
454
  else {
@@ -462,7 +459,7 @@ program
462
459
  subdomain: options.subdomain,
463
460
  serverUrl,
464
461
  token: options.token,
465
- insecure: options.insecure,
462
+ insecure: options.insecure
466
463
  });
467
464
  }
468
465
  });
@@ -486,6 +483,9 @@ program
486
483
  .option("--production", "Use Let's Encrypt production (default: staging)")
487
484
  .option("--cloudflare-token <token>", "Cloudflare API token for DNS-01 challenge")
488
485
  .option("--duckdns-token <token>", "DuckDNS token for dynamic DNS updates")
486
+ .option("--ip-mode <mode>", "IP access mode: all, allowlist, denylist (default: all)")
487
+ .option("--ip-allow <ips>", "Comma-separated IPs/CIDRs to allow (e.g., 192.168.1.0/24,10.0.0.1)")
488
+ .option("--ip-deny <ips>", "Comma-separated IPs/CIDRs to deny")
489
489
  .option("-d, --detach", "Run server in background (detached mode)")
490
490
  .action(async (options) => {
491
491
  // Load config from opentunnel.yml if exists (with env variable substitution)
@@ -493,9 +493,8 @@ program
493
493
  let fileConfig = {};
494
494
  try {
495
495
  const parsed = loadConfig(configPath);
496
- if (parsed?.server && parsed.server.domain) {
496
+ if (parsed?.server && parsed.server.domain)
497
497
  fileConfig = parsed.server;
498
- }
499
498
  }
500
499
  catch (err) {
501
500
  // Ignore parse errors, use CLI options
@@ -518,6 +517,9 @@ program
518
517
  production: options.production,
519
518
  cloudflareToken: options.cloudflareToken,
520
519
  duckdnsToken: options.duckdnsToken,
520
+ ipMode: options.ipMode || fileConfig.ipAccess?.mode || "all",
521
+ ipAllow: options.ipAllow || fileConfig.ipAccess?.allowList?.join(","),
522
+ ipDeny: options.ipDeny || fileConfig.ipAccess?.denyList?.join(","),
521
523
  detach: options.detach,
522
524
  };
523
525
  // Detached mode - run in background
@@ -562,6 +564,12 @@ program
562
564
  args.push("--cloudflare-token", mergedOptions.cloudflareToken);
563
565
  if (mergedOptions.duckdnsToken)
564
566
  args.push("--duckdns-token", mergedOptions.duckdnsToken);
567
+ if (mergedOptions.ipMode && mergedOptions.ipMode !== "all")
568
+ args.push("--ip-mode", mergedOptions.ipMode);
569
+ if (mergedOptions.ipAllow)
570
+ args.push("--ip-allow", mergedOptions.ipAllow);
571
+ if (mergedOptions.ipDeny)
572
+ args.push("--ip-deny", mergedOptions.ipDeny);
565
573
  const out = fsAsync.openSync(logFile, "a");
566
574
  const err = fsAsync.openSync(logFile, "a");
567
575
  const child = spawn(process.execPath, [process.argv[1], ...args], {
@@ -611,6 +619,12 @@ program
611
619
  enabled: mergedOptions.https !== false,
612
620
  };
613
621
  }
622
+ // Build IP access config
623
+ const ipAccessConfig = mergedOptions.ipMode !== "all" ? {
624
+ mode: mergedOptions.ipMode,
625
+ allowList: mergedOptions.ipAllow ? mergedOptions.ipAllow.split(",").map((ip) => ip.trim()) : undefined,
626
+ denyList: mergedOptions.ipDeny ? mergedOptions.ipDeny.split(",").map((ip) => ip.trim()) : undefined,
627
+ } : undefined;
614
628
  const server = new TunnelServer({
615
629
  port: parseInt(mergedOptions.port),
616
630
  publicPort: mergedOptions.publicPort ? parseInt(mergedOptions.publicPort) : undefined,
@@ -624,10 +638,11 @@ program
624
638
  auth: mergedOptions.authTokens
625
639
  ? { required: true, tokens: mergedOptions.authTokens.split(",") }
626
640
  : undefined,
641
+ ipAccess: ipAccessConfig,
627
642
  https: httpsConfig,
628
643
  selfSignedHttps: selfSignedHttpsConfig,
629
644
  autoHttps: autoHttpsConfig,
630
- autoDns: detectDnsConfig(mergedOptions),
645
+ autoDns: detectDnsConfig(mergedOptions)
631
646
  });
632
647
  // Helper function to auto-detect DNS provider
633
648
  function detectDnsConfig(opts) {
@@ -642,7 +657,7 @@ program
642
657
  cloudflareToken: opts.cloudflareToken,
643
658
  createRecords: false,
644
659
  deleteOnClose: false,
645
- setupWildcard: true,
660
+ setupWildcard: true
646
661
  };
647
662
  }
648
663
  if (opts.duckdnsToken || isDuckDnsDomain) {
@@ -652,7 +667,7 @@ program
652
667
  duckdnsToken: opts.duckdnsToken,
653
668
  createRecords: false,
654
669
  deleteOnClose: false,
655
- setupWildcard: false,
670
+ setupWildcard: false
656
671
  };
657
672
  }
658
673
  return undefined;
@@ -700,9 +715,8 @@ program
700
715
  fs.unlinkSync(pidFile);
701
716
  console.log(chalk_1.default.yellow(`Server was not running (stale PID file removed)`));
702
717
  }
703
- else {
718
+ else
704
719
  console.log(chalk_1.default.red(`Failed to stop server: ${err.message}`));
705
- }
706
720
  }
707
721
  });
708
722
  // Logs command
@@ -732,9 +746,8 @@ program
732
746
  console.log(fs.readFileSync(logFile, "utf-8"));
733
747
  });
734
748
  }
735
- else {
749
+ else
736
750
  spawn("tail", ["-f", "-n", options.lines, logFile], { stdio: "inherit" });
737
- }
738
751
  }
739
752
  else {
740
753
  const content = fs.readFileSync(logFile, "utf-8");
@@ -1194,9 +1207,8 @@ program
1194
1207
  .filter(f => f.startsWith(".opentunnel-") && f.endsWith(".pid"));
1195
1208
  // Also include the server PID
1196
1209
  const serverPidFile = ".opentunnel.pid";
1197
- if (fs.existsSync(path.join(process.cwd(), serverPidFile))) {
1210
+ if (fs.existsSync(path.join(process.cwd(), serverPidFile)))
1198
1211
  pidFiles.push(serverPidFile);
1199
- }
1200
1212
  if (pidFiles.length === 0) {
1201
1213
  console.log(chalk_1.default.yellow("No tunnels running"));
1202
1214
  return;
@@ -1216,17 +1228,15 @@ program
1216
1228
  fs.unlinkSync(pidPath);
1217
1229
  console.log(chalk_1.default.yellow(` - ${name} was not running (cleaned up)`));
1218
1230
  }
1219
- else {
1231
+ else
1220
1232
  console.log(chalk_1.default.red(` ✗ Failed to stop ${name}: ${err.message}`));
1221
- }
1222
1233
  }
1223
1234
  }
1224
1235
  // Clean up log files
1225
1236
  const logFiles = fs.readdirSync(process.cwd())
1226
1237
  .filter(f => f.startsWith("opentunnel") && f.endsWith(".log"));
1227
- if (logFiles.length > 0) {
1238
+ if (logFiles.length > 0)
1228
1239
  console.log(chalk_1.default.gray(`\nLog files preserved: ${logFiles.join(", ")}`));
1229
- }
1230
1240
  console.log(chalk_1.default.green("\nAll tunnels stopped"));
1231
1241
  });
1232
1242
  // PS command - list running tunnel processes
@@ -1236,8 +1246,7 @@ program
1236
1246
  .action(async () => {
1237
1247
  const fs = await Promise.resolve().then(() => __importStar(require("fs")));
1238
1248
  const path = await Promise.resolve().then(() => __importStar(require("path")));
1239
- const pidFiles = fs.readdirSync(process.cwd())
1240
- .filter(f => f.startsWith(".opentunnel") && f.endsWith(".pid"));
1249
+ const pidFiles = fs.readdirSync(process.cwd()).filter(f => f.startsWith(".opentunnel") && f.endsWith(".pid"));
1241
1250
  if (pidFiles.length === 0) {
1242
1251
  console.log(chalk_1.default.yellow("No tunnels running"));
1243
1252
  console.log(chalk_1.default.gray("Start tunnels with: opentunnel up -d"));
@@ -1327,13 +1336,13 @@ program
1327
1336
  method,
1328
1337
  url,
1329
1338
  headers: req.headers,
1330
- body: body || undefined,
1339
+ body: body || undefined
1331
1340
  },
1332
1341
  server: {
1333
1342
  port,
1334
1343
  timestamp,
1335
- uptime: process.uptime(),
1336
- },
1344
+ uptime: process.uptime()
1345
+ }
1337
1346
  };
1338
1347
  res.writeHead(200, {
1339
1348
  "Content-Type": "application/json",
@@ -1377,10 +1386,8 @@ program
1377
1386
  const specific = `.test-server-${options.port}.pid`;
1378
1387
  pidFiles = fs.existsSync(path.join(process.cwd(), specific)) ? [specific] : [];
1379
1388
  }
1380
- else {
1381
- pidFiles = fs.readdirSync(process.cwd())
1382
- .filter(f => f.startsWith(".test-server-") && f.endsWith(".pid"));
1383
- }
1389
+ else
1390
+ pidFiles = fs.readdirSync(process.cwd()).filter(f => f.startsWith(".test-server-") && f.endsWith(".pid"));
1384
1391
  if (pidFiles.length === 0) {
1385
1392
  console.log(chalk_1.default.yellow("No test servers running"));
1386
1393
  return;
@@ -1443,6 +1450,7 @@ async function runTunnelInBackgroundFromConfig(name, protocol, port, options) {
1443
1450
  fs.writeFileSync(pidFile, String(child.pid));
1444
1451
  console.log(chalk_1.default.green(` ✓ ${name}: started (PID: ${child.pid})`));
1445
1452
  }
1453
+ ;
1446
1454
  async function startTunnelsFromConfig(tunnels, serverUrl, token, insecure) {
1447
1455
  const spinner = (0, ora_1.default)("Connecting to server...").start();
1448
1456
  const client = new TunnelClient_1.TunnelClient({
@@ -1517,6 +1525,7 @@ async function startTunnelsFromConfig(tunnels, serverUrl, token, insecure) {
1517
1525
  process.exit(1);
1518
1526
  }
1519
1527
  }
1528
+ ;
1520
1529
  async function runTunnelInBackground(command, port, options) {
1521
1530
  const { spawn } = await Promise.resolve().then(() => __importStar(require("child_process")));
1522
1531
  const fs = await Promise.resolve().then(() => __importStar(require("fs")));
@@ -1594,6 +1603,7 @@ async function runTunnelInBackground(command, port, options) {
1594
1603
  console.log(chalk_1.default.gray(`Stop with: kill ${child.pid}`));
1595
1604
  console.log(chalk_1.default.gray(`Check: tail -f ${logFile}`));
1596
1605
  }
1606
+ ;
1597
1607
  async function createTunnel(options) {
1598
1608
  const spinner = (0, ora_1.default)("Connecting to server...").start();
1599
1609
  const client = new TunnelClient_1.TunnelClient({
@@ -1653,6 +1663,7 @@ async function createTunnel(options) {
1653
1663
  process.exit(1);
1654
1664
  }
1655
1665
  }
1666
+ ;
1656
1667
  async function createNgrokTunnel(options) {
1657
1668
  const spinner = (0, ora_1.default)("Starting ngrok...").start();
1658
1669
  const client = new NgrokClient_1.NgrokClient({
@@ -1703,6 +1714,7 @@ async function createNgrokTunnel(options) {
1703
1714
  process.exit(1);
1704
1715
  }
1705
1716
  }
1717
+ ;
1706
1718
  function printTunnelInfo(info) {
1707
1719
  console.log("");
1708
1720
  console.log(chalk_1.default.cyan(` OpenTunnel ${chalk_1.default.gray(`(via ${info.provider})`)}`));
@@ -1716,5 +1728,6 @@ function printTunnelInfo(info) {
1716
1728
  console.log(chalk_1.default.gray(" Press Ctrl+C to close the tunnel"));
1717
1729
  console.log("");
1718
1730
  }
1731
+ ;
1719
1732
  program.parse();
1720
1733
  //# sourceMappingURL=index.js.map