@bitcall/webrtc-sip-gateway 0.2.7 → 0.3.0

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
@@ -5,13 +5,13 @@ Linux-only CLI to install and operate the Bitcall WebRTC-to-SIP gateway.
5
5
  ## Install
6
6
 
7
7
  ```bash
8
- sudo npm i -g @bitcall/webrtc-sip-gateway@0.2.7
8
+ sudo npm i -g @bitcall/webrtc-sip-gateway
9
9
  ```
10
10
 
11
11
  ## Main workflow
12
12
 
13
13
  ```bash
14
- sudo bitcall-gateway init --dev
14
+ sudo bitcall-gateway init
15
15
  sudo bitcall-gateway status
16
16
  sudo bitcall-gateway logs -f
17
17
  sudo bitcall-gateway media status
@@ -22,8 +22,13 @@ ports only. Host IPv6 remains enabled for signaling and non-media traffic.
22
22
  Backend selection prefers nftables on non-UFW hosts and uses ip6tables when UFW
23
23
  is active.
24
24
 
25
- Use `sudo bitcall-gateway init --production` for strict input validation and
26
- hardening checks.
25
+ Default `init` runs in production profile with universal routing:
26
+ - `BITCALL_ENV=production`
27
+ - `ROUTING_MODE=universal`
28
+ - provider allowlist/origin/source IPs are permissive by default
29
+
30
+ Use `sudo bitcall-gateway init --advanced` for full security/provider controls.
31
+ Use `sudo bitcall-gateway init --dev` for local testing only.
27
32
  Use `--verbose` to stream apt/docker output during install. Default mode keeps
28
33
  console output concise and writes command details to
29
34
  `/var/log/bitcall-gateway-install.log`.
@@ -38,6 +43,11 @@ console output concise and writes command details to
38
43
  - `sudo bitcall-gateway up`
39
44
  - `sudo bitcall-gateway down`
40
45
  - `sudo bitcall-gateway restart`
46
+ - `sudo bitcall-gateway pause`
47
+ - `sudo bitcall-gateway resume`
48
+ - `sudo bitcall-gateway enable`
49
+ - `sudo bitcall-gateway disable`
50
+ - `sudo bitcall-gateway reconfigure`
41
51
  - `sudo bitcall-gateway status`
42
52
  - `sudo bitcall-gateway logs [-f] [service]`
43
53
  - `sudo bitcall-gateway cert status`
package/lib/constants.js CHANGED
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
 
3
3
  const path = require("path");
4
+ const PKG_VERSION = require("../package.json").version;
4
5
 
5
6
  const GATEWAY_DIR = "/opt/bitcall-gateway";
6
7
  const SERVICE_NAME = "bitcall-gateway";
@@ -14,7 +15,7 @@ module.exports = {
14
15
  SSL_DIR: path.join(GATEWAY_DIR, "ssl"),
15
16
  ENV_PATH: path.join(GATEWAY_DIR, ".env"),
16
17
  COMPOSE_PATH: path.join(GATEWAY_DIR, "docker-compose.yml"),
17
- DEFAULT_GATEWAY_IMAGE: "ghcr.io/bitcallio/webrtc-sip-gateway:0.2.7",
18
+ DEFAULT_GATEWAY_IMAGE: `ghcr.io/bitcallio/webrtc-sip-gateway:${PKG_VERSION}`,
18
19
  DEFAULT_PROVIDER_HOST: "sip.example.com",
19
20
  DEFAULT_WEBPHONE_ORIGIN: "*",
20
21
  RENEW_HOOK_PATH: "/etc/letsencrypt/renewal-hooks/deploy/bitcall-gateway.sh",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bitcall/webrtc-sip-gateway",
3
- "version": "0.2.7",
3
+ "version": "0.3.0",
4
4
  "description": "Linux CLI for bootstrapping and managing the Bitcall WebRTC-to-SIP Gateway",
5
5
  "repository": {
6
6
  "type": "git",
package/src/index.js CHANGED
@@ -40,7 +40,7 @@ const {
40
40
  isMediaIpv4OnlyRulesPresent,
41
41
  } = require("../lib/firewall");
42
42
 
43
- const PACKAGE_VERSION = "0.2.7";
43
+ const PACKAGE_VERSION = require("../package.json").version;
44
44
  const INSTALL_LOG_PATH = "/var/log/bitcall-gateway-install.log";
45
45
 
46
46
  function printBanner() {
@@ -327,36 +327,35 @@ function isOriginWildcard(originPattern) {
327
327
  }
328
328
 
329
329
  function validateProductionConfig(config) {
330
- const hasAllowedDomains = countAllowedDomains(config.allowedDomains) > 0;
331
- const hasSingleProvider = isSingleProviderConfigured(config);
332
- if (!hasAllowedDomains && !hasSingleProvider) {
333
- throw new Error(
334
- "Production requires ALLOWED_SIP_DOMAINS or single-provider routing with SIP_PROVIDER_URI. Re-run: bitcall-gateway init --advanced --production"
335
- );
330
+ // In production, validate that the config is internally consistent.
331
+ // Universal mode (open domains, open origin) is a valid production config.
332
+
333
+ // Single-provider mode requires a valid SIP_PROVIDER_URI
334
+ if (config.routingMode === "single-provider") {
335
+ if (!(config.sipProviderUri || "").trim()) {
336
+ throw new Error(
337
+ "Single-provider mode requires SIP_PROVIDER_URI. " +
338
+ "Re-run: bitcall-gateway init --advanced --production"
339
+ );
340
+ }
336
341
  }
337
342
 
338
- const originPattern = toOriginPattern(config.webphoneOrigin);
339
- const hasStrictOrigin = !isOriginWildcard(originPattern);
340
- const hasTurnToken = Boolean((config.turnApiToken || "").trim());
341
- if (!hasStrictOrigin && !hasTurnToken) {
342
- throw new Error(
343
- "Production requires a strict WEBPHONE_ORIGIN_PATTERN or TURN_API_TOKEN. Re-run: bitcall-gateway init --advanced --production"
344
- );
345
- }
343
+ // If custom cert mode, paths are required (already validated elsewhere)
344
+ // No other hard requirements for production.
346
345
  }
347
346
 
348
- function buildDevWarnings(config) {
349
- const warnings = [];
347
+ function buildSecurityNotes(config) {
348
+ const notes = [];
350
349
  if (countAllowedDomains(config.allowedDomains) === 0) {
351
- warnings.push("Provider allowlist: any (DEV mode)");
350
+ notes.push("SIP domains: unrestricted (universal mode)");
352
351
  }
353
352
  if (isOriginWildcard(toOriginPattern(config.webphoneOrigin))) {
354
- warnings.push("Webphone origin: any (DEV mode)");
353
+ notes.push("Webphone origin: unrestricted");
355
354
  }
356
355
  if (!(config.sipTrustedIps || "").trim()) {
357
- warnings.push("Trusted SIP source IPs: any (DEV mode)");
356
+ notes.push("SIP source IPs: unrestricted");
358
357
  }
359
- return warnings;
358
+ return notes;
360
359
  }
361
360
 
362
361
  function buildQuickFlowDefaults(initProfile, existing = {}) {
@@ -371,12 +370,15 @@ function buildQuickFlowDefaults(initProfile, existing = {}) {
371
370
  sipTrustedIps: existing.SIP_TRUSTED_IPS || "",
372
371
  };
373
372
 
373
+ // Dev mode: explicitly open everything (ignore existing restrictions)
374
374
  if (initProfile === "dev") {
375
375
  defaults.allowedDomains = "";
376
376
  defaults.webphoneOrigin = "*";
377
377
  defaults.sipTrustedIps = "";
378
378
  }
379
379
 
380
+ // Production mode: use existing values or defaults (already universal)
381
+ // Do not force restrictions here.
380
382
  return defaults;
381
383
  }
382
384
 
@@ -554,7 +556,7 @@ function normalizeInitProfile(initOptions = {}, existing = {}) {
554
556
  if (initOptions.dev) {
555
557
  return "dev";
556
558
  }
557
- return existing.BITCALL_ENV || "dev";
559
+ return existing.BITCALL_ENV || "production";
558
560
  }
559
561
 
560
562
  async function runPreflight(ctx) {
@@ -600,19 +602,20 @@ async function runPreflight(ctx) {
600
602
  };
601
603
  }
602
604
 
603
- function printSummary(config, devWarnings) {
605
+ function printSummary(config, securityNotes) {
604
606
  const allowedCount = countAllowedDomains(config.allowedDomains);
605
- const showDevWarnings = config.bitcallEnv === "dev";
607
+ const providerAllowlistSummary =
608
+ allowedCount > 0
609
+ ? config.allowedDomains
610
+ : isSingleProviderConfigured(config)
611
+ ? "(single-provider mode)"
612
+ : "(any)";
606
613
  console.log("\nSummary:");
607
614
  console.log(` Domain: ${config.domain}`);
608
615
  console.log(` Environment: ${config.bitcallEnv}`);
609
616
  console.log(` Routing: ${config.routingMode}`);
610
- console.log(
611
- ` Provider allowlist: ${allowedCount > 0 ? config.allowedDomains : "(any)"}${showDevWarnings && allowedCount === 0 ? " [DEV WARNING]" : ""}`
612
- );
613
- console.log(
614
- ` Webphone origin: ${config.webphoneOrigin === "*" ? `(any)${showDevWarnings ? " [DEV WARNING]" : ""}` : config.webphoneOrigin}`
615
- );
617
+ console.log(` Provider allowlist: ${providerAllowlistSummary}`);
618
+ console.log(` Webphone origin: ${config.webphoneOrigin === "*" ? "(any)" : config.webphoneOrigin}`);
616
619
  console.log(` SIP source IPs: ${(config.sipTrustedIps || "").trim() ? config.sipTrustedIps : "(any)"}`);
617
620
  console.log(` TLS: ${config.tlsMode === "letsencrypt" ? "Let's Encrypt" : config.tlsMode}`);
618
621
  console.log(` TURN: ${config.turnMode === "coturn" ? "enabled (coturn)" : config.turnMode}`);
@@ -621,14 +624,20 @@ function printSummary(config, devWarnings) {
621
624
  );
622
625
  console.log(` Firewall: ${config.configureUfw ? "UFW enabled" : "manual setup"}`);
623
626
 
624
- if (devWarnings.length > 0) {
625
- console.log("\nWarnings:");
626
- for (const warning of devWarnings) {
627
- console.log(` - ${warning}`);
627
+ if (securityNotes.length > 0) {
628
+ console.log("\nInfo:");
629
+ for (const note of securityNotes) {
630
+ console.log(` ${note}`);
628
631
  }
629
632
  }
630
633
  }
631
634
 
635
+ function shouldRequireAllowlist(bitcallEnv, routingMode) {
636
+ void bitcallEnv;
637
+ void routingMode;
638
+ return false;
639
+ }
640
+
632
641
  function parseProviderFromUri(uri = "") {
633
642
  const clean = uri.replace(/^sip:/, "");
634
643
  const [hostPort, transportPart] = clean.split(";");
@@ -649,12 +658,7 @@ async function runWizard(existing = {}, preflight = {}, initOptions = {}) {
649
658
 
650
659
  try {
651
660
  const initProfile = normalizeInitProfile(initOptions, existing);
652
- let advanced = Boolean(initOptions.advanced);
653
- if (initOptions.production && !initOptions.advanced) {
654
- advanced = true;
655
- } else if (!initOptions.dev && !initOptions.production && !initOptions.advanced) {
656
- advanced = await prompt.askYesNo("Advanced setup", false);
657
- }
661
+ const advanced = Boolean(initOptions.advanced);
658
662
 
659
663
  const detectedIp = detectPublicIp();
660
664
  const domainDefault = initOptions.domain || existing.DOMAIN || "";
@@ -667,49 +671,55 @@ async function runWizard(existing = {}, preflight = {}, initOptions = {}) {
667
671
  }
668
672
 
669
673
  const resolved = resolveDomainIpv4(domain);
670
- if (resolved.length > 0 && !resolved.includes(publicIp)) {
674
+ if (initProfile !== "dev" && resolved.length > 0 && !resolved.includes(publicIp)) {
671
675
  console.log(`Warning: DNS for ${domain} resolves to ${resolved.join(", ")}, not ${publicIp}.`);
672
676
  }
673
677
 
674
- const autoDeployMode =
678
+ const detectedDeployMode =
675
679
  (preflight.p80 && preflight.p80.inUse) || (preflight.p443 && preflight.p443.inUse)
676
680
  ? "reverse-proxy"
677
681
  : "standalone";
678
682
 
679
- let deployMode = autoDeployMode;
683
+ const existingDeployMode =
684
+ existing.DEPLOY_MODE === "standalone" || existing.DEPLOY_MODE === "reverse-proxy"
685
+ ? existing.DEPLOY_MODE
686
+ : "";
687
+ let deployMode = existingDeployMode || detectedDeployMode;
680
688
  let tlsMode = "letsencrypt";
681
689
  let acmeEmail = initOptions.email || existing.ACME_EMAIL || "";
682
690
  let customCertPath = "";
683
691
  let customKeyPath = "";
684
- let bitcallEnv = initProfile;
685
- let routingMode = existing.ROUTING_MODE || "universal";
692
+ const quickDefaults = buildQuickFlowDefaults(initProfile, existing);
693
+ let bitcallEnv = quickDefaults.bitcallEnv;
694
+ let routingMode = "universal";
686
695
  const providerFromEnv = parseProviderFromUri(existing.SIP_PROVIDER_URI || "");
687
696
  let sipProviderHost = providerFromEnv.host || DEFAULT_PROVIDER_HOST;
688
- let sipTransport = providerFromEnv.transport || "udp";
689
- let sipPort = providerFromEnv.port || "5060";
697
+ let sipTransport = "udp";
698
+ let sipPort = "5060";
690
699
  let sipProviderUri = "";
691
- let allowedDomains = existing.ALLOWED_SIP_DOMAINS || "";
692
- let sipTrustedIps = existing.SIP_TRUSTED_IPS || "";
700
+ let allowedDomains = "";
701
+ let sipTrustedIps = "";
693
702
  let turnMode = "coturn";
694
703
  let turnSecret = "";
695
704
  let turnTtl = existing.TURN_TTL || "86400";
696
- let turnApiToken = existing.TURN_API_TOKEN || "";
705
+ let turnApiToken = "";
697
706
  let turnExternalUrls = "";
698
707
  let turnExternalUsername = "";
699
708
  let turnExternalCredential = "";
700
- let webphoneOrigin = existing.WEBPHONE_ORIGIN || DEFAULT_WEBPHONE_ORIGIN;
701
- let configureUfw = initOptions.dev ? true : await prompt.askYesNo("Configure UFW firewall rules now?", true);
709
+ let webphoneOrigin = "*";
710
+ let configureUfw = true;
702
711
  let mediaIpv4Only = existing.MEDIA_IPV4_ONLY ? existing.MEDIA_IPV4_ONLY === "1" : true;
703
-
704
- if (!advanced) {
705
- acmeEmail = acmeEmail || (await prompt.askText("Let's Encrypt email", "", { required: true }));
706
- if (!initOptions.dev) {
707
- turnMode = await prompt.askYesNo("Enable built-in TURN (coturn)?", true) ? "coturn" : "none";
708
- } else {
709
- turnMode = "coturn";
710
- }
711
- const quickDefaults = buildQuickFlowDefaults(initProfile, existing);
712
- bitcallEnv = quickDefaults.bitcallEnv;
712
+ let rtpMin = existing.RTPENGINE_MIN_PORT || "10000";
713
+ let rtpMax = existing.RTPENGINE_MAX_PORT || "20000";
714
+ let turnUdpPort = existing.TURN_UDP_PORT || "3478";
715
+ let turnsTcpPort = existing.TURNS_TCP_PORT || "5349";
716
+ let turnRelayMinPort = existing.TURN_RELAY_MIN_PORT || "49152";
717
+ let turnRelayMaxPort = existing.TURN_RELAY_MAX_PORT || "49252";
718
+
719
+ if (initProfile === "dev") {
720
+ tlsMode = "dev-self-signed";
721
+ acmeEmail = "";
722
+ turnMode = "coturn";
713
723
  routingMode = quickDefaults.routingMode;
714
724
  sipProviderUri = quickDefaults.sipProviderUri;
715
725
  sipTransport = quickDefaults.sipTransport;
@@ -717,17 +727,13 @@ async function runWizard(existing = {}, preflight = {}, initOptions = {}) {
717
727
  allowedDomains = quickDefaults.allowedDomains;
718
728
  webphoneOrigin = quickDefaults.webphoneOrigin;
719
729
  sipTrustedIps = quickDefaults.sipTrustedIps;
730
+ configureUfw = true;
731
+ mediaIpv4Only = true;
720
732
  } else {
721
- deployMode = await prompt.askChoice(
722
- "Deployment mode",
723
- ["standalone", "reverse-proxy"],
724
- autoDeployMode === "reverse-proxy" ? 1 : 0
725
- );
726
-
727
733
  tlsMode = await prompt.askChoice(
728
734
  "TLS certificate mode",
729
- ["letsencrypt", "custom", "dev-self-signed"],
730
- 0
735
+ ["letsencrypt", "custom"],
736
+ existing.TLS_MODE === "custom" ? 1 : 0
731
737
  );
732
738
 
733
739
  if (tlsMode === "custom") {
@@ -737,100 +743,79 @@ async function runWizard(existing = {}, preflight = {}, initOptions = {}) {
737
743
  customKeyPath = await prompt.askText("Path to TLS private key (PEM)", existing.TLS_KEY || "", {
738
744
  required: true,
739
745
  });
740
- }
741
-
742
- if (tlsMode === "letsencrypt") {
743
- acmeEmail = await prompt.askText("Let's Encrypt email", acmeEmail, { required: true });
744
- } else {
745
746
  acmeEmail = "";
747
+ } else {
748
+ acmeEmail = await prompt.askText("Let's Encrypt email", acmeEmail, { required: true });
746
749
  }
747
750
 
748
- if (!initOptions.dev && !initOptions.production) {
749
- bitcallEnv = await prompt.askChoice("Environment", ["production", "dev"], bitcallEnv === "dev" ? 1 : 0);
750
- }
751
- allowedDomains = await prompt.askText(
752
- "Allowed SIP domains (comma-separated)",
753
- allowedDomains
754
- );
755
-
756
- routingMode = await prompt.askChoice(
757
- "Routing mode",
758
- ["universal", "single-provider"],
759
- routingMode === "single-provider" ? 1 : 0
760
- );
761
-
762
- if (routingMode === "single-provider") {
763
- sipProviderHost = await prompt.askText(
764
- "SIP provider host",
765
- existing.SIP_PROVIDER_URI
766
- ? existing.SIP_PROVIDER_URI.replace(/^sip:/, "").split(":")[0]
767
- : DEFAULT_PROVIDER_HOST,
768
- { required: true }
751
+ turnMode = await prompt.askYesNo("Enable built-in TURN (coturn)?", true) ? "coturn" : "none";
752
+ routingMode = "universal";
753
+ sipProviderUri = "";
754
+ sipTransport = "udp";
755
+ sipPort = "5060";
756
+ allowedDomains = "";
757
+ webphoneOrigin = "*";
758
+ sipTrustedIps = "";
759
+ turnApiToken = "";
760
+
761
+ if (advanced) {
762
+ const restrictToSingleProvider = await prompt.askYesNo(
763
+ "Restrict to single SIP provider?",
764
+ existing.ROUTING_MODE === "single-provider"
769
765
  );
770
766
 
771
- const sipTransportChoice = await prompt.askChoice(
772
- "SIP provider transport",
773
- ["udp", "tcp", "tls", "custom"],
774
- 0
775
- );
776
-
777
- sipTransport = sipTransportChoice;
778
- if (sipTransportChoice === "tcp" || sipTransportChoice === "udp") {
779
- sipPort = "5060";
780
- } else if (sipTransportChoice === "tls") {
781
- sipPort = "5061";
782
- } else {
783
- sipTransport = await prompt.askChoice("Custom transport", ["udp", "tcp", "tls"], 0);
784
- sipPort = await prompt.askText(
785
- "Custom SIP port",
786
- sipTransport === "tls" ? "5061" : "5060",
767
+ if (restrictToSingleProvider) {
768
+ routingMode = "single-provider";
769
+ sipProviderHost = await prompt.askText(
770
+ "SIP provider host",
771
+ providerFromEnv.host || DEFAULT_PROVIDER_HOST,
787
772
  { required: true }
788
773
  );
774
+ sipTransport = await prompt.askChoice(
775
+ "SIP provider transport",
776
+ ["udp", "tcp", "tls"],
777
+ providerFromEnv.transport === "tcp" ? 1 : providerFromEnv.transport === "tls" ? 2 : 0
778
+ );
779
+ const defaultProviderPort =
780
+ providerFromEnv.port || (sipTransport === "tls" ? "5061" : "5060");
781
+ sipPort = await prompt.askText("SIP provider port", defaultProviderPort, { required: true });
782
+ sipProviderUri = `sip:${sipProviderHost}:${sipPort};transport=${sipTransport}`;
789
783
  }
790
784
 
791
- sipProviderUri = `sip:${sipProviderHost}:${sipPort};transport=${sipTransport}`;
792
- }
793
-
794
- sipTrustedIps = await prompt.askText(
795
- "Trusted SIP source IPs (optional, comma-separated)",
796
- sipTrustedIps
797
- );
798
-
799
- const existingTurn = existing.TURN_MODE || "coturn";
800
- const turnDefaultIndex = existingTurn === "external" ? 2 : existingTurn === "coturn" ? 1 : 0;
801
- turnMode = await prompt.askChoice("TURN mode", ["none", "coturn", "external"], turnDefaultIndex);
802
- if (turnMode === "coturn") {
803
- turnSecret = crypto.randomBytes(32).toString("hex");
804
- turnTtl = await prompt.askText("TURN credential TTL seconds", turnTtl, { required: true });
805
- turnApiToken = await prompt.askText("TURN API token (optional)", turnApiToken);
806
- } else if (turnMode === "external") {
807
- turnSecret = "";
808
- turnApiToken = "";
809
- turnExternalUrls = await prompt.askText("External TURN urls", existing.TURN_EXTERNAL_URLS || "", {
810
- required: true,
811
- });
812
- turnExternalUsername = await prompt.askText(
813
- "External TURN username",
814
- existing.TURN_EXTERNAL_USERNAME || ""
785
+ allowedDomains = await prompt.askText(
786
+ "SIP domain allowlist (optional, comma-separated)",
787
+ existing.ALLOWED_SIP_DOMAINS || ""
815
788
  );
816
- turnExternalCredential = await prompt.askText(
817
- "External TURN credential",
818
- existing.TURN_EXTERNAL_CREDENTIAL || ""
789
+ webphoneOrigin = await prompt.askText(
790
+ "Webphone origin restriction (optional, * for any)",
791
+ existing.WEBPHONE_ORIGIN || "*"
792
+ );
793
+ webphoneOrigin = webphoneOrigin.trim() ? webphoneOrigin : "*";
794
+ sipTrustedIps = await prompt.askText(
795
+ "SIP trusted source IPs (optional, comma-separated)",
796
+ existing.SIP_TRUSTED_IPS || ""
819
797
  );
820
- } else {
821
- turnSecret = "";
822
- turnApiToken = "";
823
- }
824
798
 
825
- webphoneOrigin = await prompt.askText(
826
- "Allowed webphone origin (* for any)",
827
- webphoneOrigin
828
- );
799
+ if (turnMode === "coturn") {
800
+ turnApiToken = await prompt.askText("TURN API token (optional)", existing.TURN_API_TOKEN || "");
801
+ }
829
802
 
830
- mediaIpv4Only = await prompt.askYesNo(
831
- "Media IPv4-only mode (block IPv6 RTP/TURN on host firewall)",
832
- mediaIpv4Only
833
- );
803
+ const customizePorts = await prompt.askYesNo("Customize RTP/TURN ports?", false);
804
+ if (customizePorts) {
805
+ rtpMin = await prompt.askText("RTP minimum port", rtpMin, { required: true });
806
+ rtpMax = await prompt.askText("RTP maximum port", rtpMax, { required: true });
807
+ if (turnMode === "coturn") {
808
+ turnUdpPort = await prompt.askText("TURN UDP/TCP port", turnUdpPort, { required: true });
809
+ turnsTcpPort = await prompt.askText("TURNS TCP port", turnsTcpPort, { required: true });
810
+ turnRelayMinPort = await prompt.askText("TURN relay minimum port", turnRelayMinPort, {
811
+ required: true,
812
+ });
813
+ turnRelayMaxPort = await prompt.askText("TURN relay maximum port", turnRelayMaxPort, {
814
+ required: true,
815
+ });
816
+ }
817
+ }
818
+ }
834
819
  }
835
820
 
836
821
  if (turnMode === "coturn") {
@@ -868,12 +853,12 @@ async function runWizard(existing = {}, preflight = {}, initOptions = {}) {
868
853
  turnExternalCredential,
869
854
  webphoneOrigin,
870
855
  mediaIpv4Only: mediaIpv4Only ? "1" : "0",
871
- rtpMin: existing.RTPENGINE_MIN_PORT || "10000",
872
- rtpMax: existing.RTPENGINE_MAX_PORT || "20000",
873
- turnUdpPort: existing.TURN_UDP_PORT || "3478",
874
- turnsTcpPort: existing.TURNS_TCP_PORT || "5349",
875
- turnRelayMinPort: existing.TURN_RELAY_MIN_PORT || "49152",
876
- turnRelayMaxPort: existing.TURN_RELAY_MAX_PORT || "49252",
856
+ rtpMin,
857
+ rtpMax,
858
+ turnUdpPort,
859
+ turnsTcpPort,
860
+ turnRelayMinPort,
861
+ turnRelayMaxPort,
877
862
  acmeListenPort: deployMode === "reverse-proxy" ? "8080" : "80",
878
863
  wssListenPort: deployMode === "reverse-proxy" ? "8443" : "443",
879
864
  internalWssPort: "8443",
@@ -885,10 +870,10 @@ async function runWizard(existing = {}, preflight = {}, initOptions = {}) {
885
870
  validateProductionConfig(config);
886
871
  }
887
872
 
888
- const devWarnings = config.bitcallEnv === "dev" ? buildDevWarnings(config) : [];
889
- printSummary(config, devWarnings);
873
+ const securityNotes = buildSecurityNotes(config);
874
+ printSummary(config, securityNotes);
890
875
 
891
- if (!(initOptions.dev && !advanced)) {
876
+ if (initProfile !== "dev") {
892
877
  const proceed = await prompt.askYesNo("Proceed with provisioning", true);
893
878
  if (!proceed) {
894
879
  throw new Error("Initialization canceled.");
@@ -1089,6 +1074,9 @@ async function initCommand(initOptions = {}) {
1089
1074
  tlsCertPath = path.join(SSL_DIR, "dev-fullchain.pem");
1090
1075
  tlsKeyPath = path.join(SSL_DIR, "dev-privkey.pem");
1091
1076
  generateSelfSigned(tlsCertPath, tlsKeyPath, config.domain, 30, ctx.exec);
1077
+ console.log("WARNING: Self-signed certificates do not work with browsers.");
1078
+ console.log("WebSocket connections will be rejected. Use --dev for local testing only.");
1079
+ console.log("For browser access, re-run with Let's Encrypt: sudo bitcall-gateway init");
1092
1080
  } else {
1093
1081
  tlsCertPath = path.join(SSL_DIR, "bootstrap-fullchain.pem");
1094
1082
  tlsKeyPath = path.join(SSL_DIR, "bootstrap-privkey.pem");
@@ -1120,6 +1108,92 @@ async function initCommand(initOptions = {}) {
1120
1108
  }
1121
1109
  }
1122
1110
 
1111
+ async function reconfigureCommand(options) {
1112
+ ensureInitialized();
1113
+ printBanner();
1114
+ const ctx = createInstallContext(options);
1115
+
1116
+ let preflight;
1117
+ await ctx.step("Checks", async () => {
1118
+ preflight = await runPreflight(ctx);
1119
+ });
1120
+
1121
+ const existingEnv = fs.existsSync(ENV_PATH) ? loadEnvFile(ENV_PATH) : {};
1122
+
1123
+ let config;
1124
+ await ctx.step("Config", async () => {
1125
+ config = await runWizard(existingEnv, preflight, options);
1126
+ });
1127
+
1128
+ ensureInstallLayout();
1129
+
1130
+ let tlsCertPath;
1131
+ let tlsKeyPath;
1132
+
1133
+ await ctx.step("Firewall", async () => {
1134
+ if (config.configureUfw) {
1135
+ const ufwReady = await ensureUfwAvailable(ctx);
1136
+ if (ufwReady) {
1137
+ configureFirewall(config, ctx.exec);
1138
+ } else {
1139
+ config.configureUfw = false;
1140
+ printRequiredPorts(config);
1141
+ }
1142
+ } else {
1143
+ printRequiredPorts(config);
1144
+ }
1145
+
1146
+ if (config.mediaIpv4Only === "1") {
1147
+ try {
1148
+ const applied = applyMediaIpv4OnlyRules(mediaFirewallOptionsFromConfig(config));
1149
+ console.log(`Applied IPv6 media block rules (${applied.backend}).`);
1150
+ } catch (error) {
1151
+ console.error(`IPv6 media block setup failed: ${error.message}`);
1152
+ config.mediaIpv4Only = "0";
1153
+ }
1154
+ }
1155
+ });
1156
+
1157
+ await ctx.step("TLS", async () => {
1158
+ if (config.tlsMode === "custom") {
1159
+ const custom = provisionCustomCert(config);
1160
+ tlsCertPath = custom.certPath;
1161
+ tlsKeyPath = custom.keyPath;
1162
+ } else if (config.tlsMode === "dev-self-signed") {
1163
+ tlsCertPath = path.join(SSL_DIR, "dev-fullchain.pem");
1164
+ tlsKeyPath = path.join(SSL_DIR, "dev-privkey.pem");
1165
+ generateSelfSigned(tlsCertPath, tlsKeyPath, config.domain, 30, ctx.exec);
1166
+ } else {
1167
+ // Keep existing LE cert if domain matches, otherwise re-provision.
1168
+ const envMap = loadEnvFile(ENV_PATH);
1169
+ if (envMap.TLS_MODE === "letsencrypt" && envMap.TLS_CERT && fs.existsSync(envMap.TLS_CERT)) {
1170
+ tlsCertPath = envMap.TLS_CERT;
1171
+ tlsKeyPath = envMap.TLS_KEY;
1172
+ } else {
1173
+ tlsCertPath = path.join(SSL_DIR, "bootstrap-fullchain.pem");
1174
+ tlsKeyPath = path.join(SSL_DIR, "bootstrap-privkey.pem");
1175
+ generateSelfSigned(tlsCertPath, tlsKeyPath, config.domain, 1, ctx.exec);
1176
+ }
1177
+ }
1178
+
1179
+ const envContent = renderEnvContent(config, tlsCertPath, tlsKeyPath);
1180
+ writeFileWithMode(ENV_PATH, envContent, 0o600);
1181
+ writeComposeTemplate();
1182
+ });
1183
+
1184
+ await ctx.step("Start", async () => {
1185
+ startGatewayStack(ctx.exec);
1186
+ if (config.tlsMode === "letsencrypt" && (!tlsCertPath || !tlsCertPath.includes("letsencrypt"))) {
1187
+ runLetsEncrypt(config, ctx.exec);
1188
+ }
1189
+ });
1190
+
1191
+ await ctx.step("Done", async () => {});
1192
+
1193
+ console.log("\nGateway reconfigured.");
1194
+ console.log(`WSS URL: wss://${config.domain}`);
1195
+ }
1196
+
1123
1197
  function runSystemctl(args, fallbackComposeArgs) {
1124
1198
  const result = run("systemctl", args, { check: false, stdio: "inherit" });
1125
1199
  if (result.status !== 0 && fallbackComposeArgs) {
@@ -1143,6 +1217,30 @@ function restartCommand() {
1143
1217
  runSystemctl(["reload", SERVICE_NAME], ["restart"]);
1144
1218
  }
1145
1219
 
1220
+ function pauseCommand() {
1221
+ ensureInitialized();
1222
+ runCompose(["pause"], { stdio: "inherit" });
1223
+ console.log("Gateway paused.");
1224
+ }
1225
+
1226
+ function resumeCommand() {
1227
+ ensureInitialized();
1228
+ runCompose(["unpause"], { stdio: "inherit" });
1229
+ console.log("Gateway resumed.");
1230
+ }
1231
+
1232
+ function enableCommand() {
1233
+ ensureInitialized();
1234
+ run("systemctl", ["enable", SERVICE_NAME], { stdio: "inherit" });
1235
+ console.log("Gateway will start on boot.");
1236
+ }
1237
+
1238
+ function disableCommand() {
1239
+ ensureInitialized();
1240
+ run("systemctl", ["disable", SERVICE_NAME], { stdio: "inherit" });
1241
+ console.log("Gateway will NOT start on boot.");
1242
+ }
1243
+
1146
1244
  function logsCommand(service, options) {
1147
1245
  ensureInitialized();
1148
1246
  const args = ["logs", "--tail", String(options.tail || 200)];
@@ -1227,7 +1325,7 @@ function statusCommand() {
1227
1325
 
1228
1326
  console.log("\nConfig summary:");
1229
1327
  console.log(`DOMAIN=${envMap.DOMAIN || ""}`);
1230
- console.log(`BITCALL_ENV=${envMap.BITCALL_ENV || "dev"}`);
1328
+ console.log(`BITCALL_ENV=${envMap.BITCALL_ENV || "production"}`);
1231
1329
  console.log(`ROUTING_MODE=${envMap.ROUTING_MODE || "universal"}`);
1232
1330
  console.log(`SIP_PROVIDER_URI=${envMap.SIP_PROVIDER_URI || ""}`);
1233
1331
  console.log(`DEPLOY_MODE=${envMap.DEPLOY_MODE || ""}`);
@@ -1307,8 +1405,24 @@ async function certInstallCommand(options) {
1307
1405
 
1308
1406
  function updateCommand() {
1309
1407
  ensureInitialized();
1408
+ const envMap = loadEnvFile(ENV_PATH);
1409
+ const currentImage = envMap.BITCALL_GATEWAY_IMAGE || "";
1410
+ const targetImage = DEFAULT_GATEWAY_IMAGE;
1411
+
1412
+ if (currentImage === targetImage) {
1413
+ console.log(`Image is current: ${targetImage}`);
1414
+ console.log("Checking for newer image layers...");
1415
+ } else {
1416
+ console.log(`Updating image: ${currentImage || "(none)"} → ${targetImage}`);
1417
+ updateGatewayEnv({ BITCALL_GATEWAY_IMAGE: targetImage });
1418
+ }
1419
+
1310
1420
  runCompose(["pull"], { stdio: "inherit" });
1311
1421
  runSystemctl(["reload", SERVICE_NAME], ["up", "-d", "--remove-orphans"]);
1422
+
1423
+ console.log(`\nGateway updated to ${PACKAGE_VERSION}.`);
1424
+ console.log("To update the CLI itself:");
1425
+ console.log(" sudo npm i -g @bitcall/webrtc-sip-gateway@latest");
1312
1426
  }
1313
1427
 
1314
1428
  function mediaStatusCommand() {
@@ -1391,11 +1505,40 @@ async function uninstallCommand(options) {
1391
1505
  fs.rmSync(RENEW_HOOK_PATH, { force: true });
1392
1506
  }
1393
1507
 
1508
+ // Remove Bitcall-tagged UFW rules.
1509
+ if (commandExists("ufw")) {
1510
+ const ufwOutput = output("ufw", ["status", "numbered"]);
1511
+ const lines = (ufwOutput || "").split("\n");
1512
+ const ruleNumbers = [];
1513
+ for (const line of lines) {
1514
+ const match = line.match(/^\[\s*(\d+)\]/);
1515
+ if (match && line.includes("Bitcall")) {
1516
+ ruleNumbers.push(match[1]);
1517
+ }
1518
+ }
1519
+
1520
+ // Delete in reverse order so indices do not shift.
1521
+ for (const num of ruleNumbers.reverse()) {
1522
+ run("ufw", ["--force", "delete", num], { check: false, stdio: "inherit" });
1523
+ }
1524
+ if (ruleNumbers.length > 0) {
1525
+ run("ufw", ["reload"], { check: false, stdio: "inherit" });
1526
+ console.log(`Removed ${ruleNumbers.length} UFW rule(s).`);
1527
+ }
1528
+ }
1529
+
1394
1530
  if (fs.existsSync(GATEWAY_DIR)) {
1395
1531
  fs.rmSync(GATEWAY_DIR, { recursive: true, force: true });
1396
1532
  }
1397
1533
 
1398
1534
  console.log("Gateway uninstalled.");
1535
+ const domain = uninstallEnv.DOMAIN || "";
1536
+ console.log("\nTo remove the CLI:");
1537
+ console.log(" sudo npm uninstall -g @bitcall/webrtc-sip-gateway");
1538
+ if (domain && uninstallEnv.TLS_MODE === "letsencrypt") {
1539
+ console.log("\nTo remove Let's Encrypt certificate:");
1540
+ console.log(` sudo certbot delete --cert-name ${domain}`);
1541
+ }
1399
1542
  }
1400
1543
 
1401
1544
  function configCommand() {
@@ -1421,7 +1564,7 @@ function buildProgram() {
1421
1564
  .description("Run setup wizard and provision gateway")
1422
1565
  .option("--advanced", "Show advanced configuration prompts")
1423
1566
  .option("--dev", "Quick dev setup (minimal prompts, permissive defaults)")
1424
- .option("--production", "Production setup (strict validation enabled)")
1567
+ .option("--production", "Production setup profile")
1425
1568
  .option("--domain <domain>", "Gateway domain")
1426
1569
  .option("--email <email>", "Let's Encrypt email")
1427
1570
  .option("--verbose", "Stream full installer command output")
@@ -1452,6 +1595,18 @@ function buildProgram() {
1452
1595
 
1453
1596
  program.command("update").description("Pull latest image and restart service").action(updateCommand);
1454
1597
  program.command("config").description("Print active configuration (secrets hidden)").action(configCommand);
1598
+ program.command("pause").description("Pause gateway containers").action(pauseCommand);
1599
+ program.command("resume").description("Resume paused gateway containers").action(resumeCommand);
1600
+ program.command("enable").description("Enable auto-start on boot").action(enableCommand);
1601
+ program.command("disable").description("Disable auto-start on boot").action(disableCommand);
1602
+ program
1603
+ .command("reconfigure")
1604
+ .description("Re-run wizard with current values as defaults")
1605
+ .option("--advanced", "Show advanced configuration prompts")
1606
+ .option("--dev", "Dev mode")
1607
+ .option("--production", "Production mode")
1608
+ .option("--verbose", "Verbose output")
1609
+ .action(reconfigureCommand);
1455
1610
 
1456
1611
  const media = program.command("media").description("Media firewall operations");
1457
1612
  media.command("status").description("Show media IPv4-only firewall state").action(mediaStatusCommand);
@@ -1477,8 +1632,9 @@ module.exports = {
1477
1632
  main,
1478
1633
  normalizeInitProfile,
1479
1634
  validateProductionConfig,
1480
- buildDevWarnings,
1635
+ buildSecurityNotes,
1481
1636
  buildQuickFlowDefaults,
1637
+ shouldRequireAllowlist,
1482
1638
  isOriginWildcard,
1483
1639
  isSingleProviderConfigured,
1484
1640
  printRequiredPorts,