@bitcall/webrtc-sip-gateway 0.2.8 → 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.8
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,14 +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
- Default `init` and `init --dev` run in dev profile:
26
- - `BITCALL_ENV=dev`
25
+ Default `init` runs in production profile with universal routing:
26
+ - `BITCALL_ENV=production`
27
27
  - `ROUTING_MODE=universal`
28
- - provider allowlist/origin/source IPs are permissive by default (with warnings)
28
+ - provider allowlist/origin/source IPs are permissive by default
29
29
 
30
- Use `sudo bitcall-gateway init --production` for strict input validation and
31
- hardening checks. Production universal routing requires explicit
32
- `ALLOWED_SIP_DOMAINS`.
30
+ Use `sudo bitcall-gateway init --advanced` for full security/provider controls.
31
+ Use `sudo bitcall-gateway init --dev` for local testing only.
33
32
  Use `--verbose` to stream apt/docker output during install. Default mode keeps
34
33
  console output concise and writes command details to
35
34
  `/var/log/bitcall-gateway-install.log`.
@@ -44,6 +43,11 @@ console output concise and writes command details to
44
43
  - `sudo bitcall-gateway up`
45
44
  - `sudo bitcall-gateway down`
46
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`
47
51
  - `sudo bitcall-gateway status`
48
52
  - `sudo bitcall-gateway logs [-f] [service]`
49
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.8",
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.8",
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.8";
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
 
@@ -545,7 +547,6 @@ function toOriginPattern(origin) {
545
547
  }
546
548
 
547
549
  function normalizeInitProfile(initOptions = {}, existing = {}) {
548
- void existing;
549
550
  if (initOptions.dev && initOptions.production) {
550
551
  throw new Error("Use only one mode: --dev or --production.");
551
552
  }
@@ -555,7 +556,7 @@ function normalizeInitProfile(initOptions = {}, existing = {}) {
555
556
  if (initOptions.dev) {
556
557
  return "dev";
557
558
  }
558
- return "dev";
559
+ return existing.BITCALL_ENV || "production";
559
560
  }
560
561
 
561
562
  async function runPreflight(ctx) {
@@ -601,27 +602,20 @@ async function runPreflight(ctx) {
601
602
  };
602
603
  }
603
604
 
604
- function printSummary(config, devWarnings) {
605
+ function printSummary(config, securityNotes) {
605
606
  const allowedCount = countAllowedDomains(config.allowedDomains);
606
- const showDevWarnings = config.bitcallEnv === "dev";
607
607
  const providerAllowlistSummary =
608
608
  allowedCount > 0
609
609
  ? config.allowedDomains
610
- : config.bitcallEnv === "production"
611
- ? isSingleProviderConfigured(config)
612
- ? "(single-provider mode)"
613
- : "(missing)"
610
+ : isSingleProviderConfigured(config)
611
+ ? "(single-provider mode)"
614
612
  : "(any)";
615
613
  console.log("\nSummary:");
616
614
  console.log(` Domain: ${config.domain}`);
617
615
  console.log(` Environment: ${config.bitcallEnv}`);
618
616
  console.log(` Routing: ${config.routingMode}`);
619
- console.log(
620
- ` Provider allowlist: ${providerAllowlistSummary}${showDevWarnings && allowedCount === 0 ? " [DEV WARNING]" : ""}`
621
- );
622
- console.log(
623
- ` Webphone origin: ${config.webphoneOrigin === "*" ? `(any)${showDevWarnings ? " [DEV WARNING]" : ""}` : config.webphoneOrigin}`
624
- );
617
+ console.log(` Provider allowlist: ${providerAllowlistSummary}`);
618
+ console.log(` Webphone origin: ${config.webphoneOrigin === "*" ? "(any)" : config.webphoneOrigin}`);
625
619
  console.log(` SIP source IPs: ${(config.sipTrustedIps || "").trim() ? config.sipTrustedIps : "(any)"}`);
626
620
  console.log(` TLS: ${config.tlsMode === "letsencrypt" ? "Let's Encrypt" : config.tlsMode}`);
627
621
  console.log(` TURN: ${config.turnMode === "coturn" ? "enabled (coturn)" : config.turnMode}`);
@@ -630,16 +624,18 @@ function printSummary(config, devWarnings) {
630
624
  );
631
625
  console.log(` Firewall: ${config.configureUfw ? "UFW enabled" : "manual setup"}`);
632
626
 
633
- if (devWarnings.length > 0) {
634
- console.log("\nWarnings:");
635
- for (const warning of devWarnings) {
636
- console.log(` - ${warning}`);
627
+ if (securityNotes.length > 0) {
628
+ console.log("\nInfo:");
629
+ for (const note of securityNotes) {
630
+ console.log(` ${note}`);
637
631
  }
638
632
  }
639
633
  }
640
634
 
641
635
  function shouldRequireAllowlist(bitcallEnv, routingMode) {
642
- return bitcallEnv === "production" && routingMode === "universal";
636
+ void bitcallEnv;
637
+ void routingMode;
638
+ return false;
643
639
  }
644
640
 
645
641
  function parseProviderFromUri(uri = "") {
@@ -662,12 +658,7 @@ async function runWizard(existing = {}, preflight = {}, initOptions = {}) {
662
658
 
663
659
  try {
664
660
  const initProfile = normalizeInitProfile(initOptions, existing);
665
- let advanced = Boolean(initOptions.advanced);
666
- if (initOptions.production && !initOptions.advanced) {
667
- advanced = true;
668
- } else if (!initOptions.dev && !initOptions.production && !initOptions.advanced) {
669
- advanced = await prompt.askYesNo("Advanced setup", false);
670
- }
661
+ const advanced = Boolean(initOptions.advanced);
671
662
 
672
663
  const detectedIp = detectPublicIp();
673
664
  const domainDefault = initOptions.domain || existing.DOMAIN || "";
@@ -680,45 +671,55 @@ async function runWizard(existing = {}, preflight = {}, initOptions = {}) {
680
671
  }
681
672
 
682
673
  const resolved = resolveDomainIpv4(domain);
683
- if (resolved.length > 0 && !resolved.includes(publicIp)) {
674
+ if (initProfile !== "dev" && resolved.length > 0 && !resolved.includes(publicIp)) {
684
675
  console.log(`Warning: DNS for ${domain} resolves to ${resolved.join(", ")}, not ${publicIp}.`);
685
676
  }
686
677
 
687
- const autoDeployMode =
678
+ const detectedDeployMode =
688
679
  (preflight.p80 && preflight.p80.inUse) || (preflight.p443 && preflight.p443.inUse)
689
680
  ? "reverse-proxy"
690
681
  : "standalone";
691
682
 
692
- 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;
693
688
  let tlsMode = "letsencrypt";
694
689
  let acmeEmail = initOptions.email || existing.ACME_EMAIL || "";
695
690
  let customCertPath = "";
696
691
  let customKeyPath = "";
697
- let bitcallEnv = initProfile;
698
- let routingMode = existing.ROUTING_MODE || "universal";
692
+ const quickDefaults = buildQuickFlowDefaults(initProfile, existing);
693
+ let bitcallEnv = quickDefaults.bitcallEnv;
694
+ let routingMode = "universal";
699
695
  const providerFromEnv = parseProviderFromUri(existing.SIP_PROVIDER_URI || "");
700
696
  let sipProviderHost = providerFromEnv.host || DEFAULT_PROVIDER_HOST;
701
- let sipTransport = providerFromEnv.transport || "udp";
702
- let sipPort = providerFromEnv.port || "5060";
697
+ let sipTransport = "udp";
698
+ let sipPort = "5060";
703
699
  let sipProviderUri = "";
704
- let allowedDomains = existing.ALLOWED_SIP_DOMAINS || "";
705
- let sipTrustedIps = existing.SIP_TRUSTED_IPS || "";
700
+ let allowedDomains = "";
701
+ let sipTrustedIps = "";
706
702
  let turnMode = "coturn";
707
703
  let turnSecret = "";
708
704
  let turnTtl = existing.TURN_TTL || "86400";
709
- let turnApiToken = existing.TURN_API_TOKEN || "";
705
+ let turnApiToken = "";
710
706
  let turnExternalUrls = "";
711
707
  let turnExternalUsername = "";
712
708
  let turnExternalCredential = "";
713
- let webphoneOrigin = existing.WEBPHONE_ORIGIN || DEFAULT_WEBPHONE_ORIGIN;
714
- let configureUfw = await prompt.askYesNo("Configure UFW firewall rules now?", true);
709
+ let webphoneOrigin = "*";
710
+ let configureUfw = true;
715
711
  let mediaIpv4Only = existing.MEDIA_IPV4_ONLY ? existing.MEDIA_IPV4_ONLY === "1" : true;
716
-
717
- if (!advanced) {
718
- acmeEmail = acmeEmail || (await prompt.askText("Let's Encrypt email", "", { required: true }));
719
- turnMode = await prompt.askYesNo("Enable built-in TURN (coturn)?", true) ? "coturn" : "none";
720
- const quickDefaults = buildQuickFlowDefaults(initProfile, existing);
721
- 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";
722
723
  routingMode = quickDefaults.routingMode;
723
724
  sipProviderUri = quickDefaults.sipProviderUri;
724
725
  sipTransport = quickDefaults.sipTransport;
@@ -726,17 +727,13 @@ async function runWizard(existing = {}, preflight = {}, initOptions = {}) {
726
727
  allowedDomains = quickDefaults.allowedDomains;
727
728
  webphoneOrigin = quickDefaults.webphoneOrigin;
728
729
  sipTrustedIps = quickDefaults.sipTrustedIps;
730
+ configureUfw = true;
731
+ mediaIpv4Only = true;
729
732
  } else {
730
- deployMode = await prompt.askChoice(
731
- "Deployment mode",
732
- ["standalone", "reverse-proxy"],
733
- autoDeployMode === "reverse-proxy" ? 1 : 0
734
- );
735
-
736
733
  tlsMode = await prompt.askChoice(
737
734
  "TLS certificate mode",
738
- ["letsencrypt", "custom", "dev-self-signed"],
739
- 0
735
+ ["letsencrypt", "custom"],
736
+ existing.TLS_MODE === "custom" ? 1 : 0
740
737
  );
741
738
 
742
739
  if (tlsMode === "custom") {
@@ -746,104 +743,79 @@ async function runWizard(existing = {}, preflight = {}, initOptions = {}) {
746
743
  customKeyPath = await prompt.askText("Path to TLS private key (PEM)", existing.TLS_KEY || "", {
747
744
  required: true,
748
745
  });
749
- }
750
-
751
- if (tlsMode === "letsencrypt") {
752
- acmeEmail = await prompt.askText("Let's Encrypt email", acmeEmail, { required: true });
753
- } else {
754
746
  acmeEmail = "";
747
+ } else {
748
+ acmeEmail = await prompt.askText("Let's Encrypt email", acmeEmail, { required: true });
755
749
  }
756
750
 
757
- if (!initOptions.dev && !initOptions.production) {
758
- bitcallEnv = await prompt.askChoice("Environment", ["production", "dev"], bitcallEnv === "dev" ? 1 : 0);
759
- }
760
- routingMode = await prompt.askChoice(
761
- "Routing mode",
762
- ["universal", "single-provider"],
763
- routingMode === "single-provider" ? 1 : 0
764
- );
765
-
766
- if (routingMode === "single-provider") {
767
- sipProviderHost = await prompt.askText(
768
- "SIP provider host",
769
- existing.SIP_PROVIDER_URI
770
- ? existing.SIP_PROVIDER_URI.replace(/^sip:/, "").split(":")[0]
771
- : DEFAULT_PROVIDER_HOST,
772
- { required: true }
773
- );
774
-
775
- const sipTransportChoice = await prompt.askChoice(
776
- "SIP provider transport",
777
- ["udp", "tcp", "tls", "custom"],
778
- 0
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"
779
765
  );
780
766
 
781
- sipTransport = sipTransportChoice;
782
- if (sipTransportChoice === "tcp" || sipTransportChoice === "udp") {
783
- sipPort = "5060";
784
- } else if (sipTransportChoice === "tls") {
785
- sipPort = "5061";
786
- } else {
787
- sipTransport = await prompt.askChoice("Custom transport", ["udp", "tcp", "tls"], 0);
788
- sipPort = await prompt.askText(
789
- "Custom SIP port",
790
- 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,
791
772
  { required: true }
792
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}`;
793
783
  }
794
784
 
795
- sipProviderUri = `sip:${sipProviderHost}:${sipPort};transport=${sipTransport}`;
796
- }
797
-
798
- const requireAllowlist = shouldRequireAllowlist(bitcallEnv, routingMode);
799
- allowedDomains = await prompt.askText(
800
- requireAllowlist
801
- ? "Allowed SIP domains (comma-separated; required in production universal mode)"
802
- : "Allowed SIP domains (comma-separated)",
803
- allowedDomains,
804
- { required: requireAllowlist }
805
- );
806
-
807
- sipTrustedIps = await prompt.askText(
808
- "Trusted SIP source IPs (optional, comma-separated)",
809
- sipTrustedIps
810
- );
811
-
812
- const existingTurn = existing.TURN_MODE || "coturn";
813
- const turnDefaultIndex = existingTurn === "external" ? 2 : existingTurn === "coturn" ? 1 : 0;
814
- turnMode = await prompt.askChoice("TURN mode", ["none", "coturn", "external"], turnDefaultIndex);
815
- if (turnMode === "coturn") {
816
- turnSecret = crypto.randomBytes(32).toString("hex");
817
- turnTtl = await prompt.askText("TURN credential TTL seconds", turnTtl, { required: true });
818
- turnApiToken = await prompt.askText("TURN API token (optional)", turnApiToken);
819
- } else if (turnMode === "external") {
820
- turnSecret = "";
821
- turnApiToken = "";
822
- turnExternalUrls = await prompt.askText("External TURN urls", existing.TURN_EXTERNAL_URLS || "", {
823
- required: true,
824
- });
825
- turnExternalUsername = await prompt.askText(
826
- "External TURN username",
827
- existing.TURN_EXTERNAL_USERNAME || ""
785
+ allowedDomains = await prompt.askText(
786
+ "SIP domain allowlist (optional, comma-separated)",
787
+ existing.ALLOWED_SIP_DOMAINS || ""
828
788
  );
829
- turnExternalCredential = await prompt.askText(
830
- "External TURN credential",
831
- 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 || ""
832
797
  );
833
- } else {
834
- turnSecret = "";
835
- turnApiToken = "";
836
- }
837
798
 
838
- webphoneOrigin = await prompt.askText(
839
- "Allowed webphone origin (* for any)",
840
- webphoneOrigin
841
- );
799
+ if (turnMode === "coturn") {
800
+ turnApiToken = await prompt.askText("TURN API token (optional)", existing.TURN_API_TOKEN || "");
801
+ }
842
802
 
843
- mediaIpv4Only = await prompt.askYesNo(
844
- "Media IPv4-only mode (block IPv6 RTP/TURN on host firewall)",
845
- mediaIpv4Only
846
- );
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
+ }
847
819
  }
848
820
 
849
821
  if (turnMode === "coturn") {
@@ -881,12 +853,12 @@ async function runWizard(existing = {}, preflight = {}, initOptions = {}) {
881
853
  turnExternalCredential,
882
854
  webphoneOrigin,
883
855
  mediaIpv4Only: mediaIpv4Only ? "1" : "0",
884
- rtpMin: existing.RTPENGINE_MIN_PORT || "10000",
885
- rtpMax: existing.RTPENGINE_MAX_PORT || "20000",
886
- turnUdpPort: existing.TURN_UDP_PORT || "3478",
887
- turnsTcpPort: existing.TURNS_TCP_PORT || "5349",
888
- turnRelayMinPort: existing.TURN_RELAY_MIN_PORT || "49152",
889
- turnRelayMaxPort: existing.TURN_RELAY_MAX_PORT || "49252",
856
+ rtpMin,
857
+ rtpMax,
858
+ turnUdpPort,
859
+ turnsTcpPort,
860
+ turnRelayMinPort,
861
+ turnRelayMaxPort,
890
862
  acmeListenPort: deployMode === "reverse-proxy" ? "8080" : "80",
891
863
  wssListenPort: deployMode === "reverse-proxy" ? "8443" : "443",
892
864
  internalWssPort: "8443",
@@ -898,12 +870,14 @@ async function runWizard(existing = {}, preflight = {}, initOptions = {}) {
898
870
  validateProductionConfig(config);
899
871
  }
900
872
 
901
- const devWarnings = config.bitcallEnv === "dev" ? buildDevWarnings(config) : [];
902
- printSummary(config, devWarnings);
873
+ const securityNotes = buildSecurityNotes(config);
874
+ printSummary(config, securityNotes);
903
875
 
904
- const proceed = await prompt.askYesNo("Proceed with provisioning", true);
905
- if (!proceed) {
906
- throw new Error("Initialization canceled.");
876
+ if (initProfile !== "dev") {
877
+ const proceed = await prompt.askYesNo("Proceed with provisioning", true);
878
+ if (!proceed) {
879
+ throw new Error("Initialization canceled.");
880
+ }
907
881
  }
908
882
 
909
883
  return config;
@@ -1100,6 +1074,9 @@ async function initCommand(initOptions = {}) {
1100
1074
  tlsCertPath = path.join(SSL_DIR, "dev-fullchain.pem");
1101
1075
  tlsKeyPath = path.join(SSL_DIR, "dev-privkey.pem");
1102
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");
1103
1080
  } else {
1104
1081
  tlsCertPath = path.join(SSL_DIR, "bootstrap-fullchain.pem");
1105
1082
  tlsKeyPath = path.join(SSL_DIR, "bootstrap-privkey.pem");
@@ -1131,6 +1108,92 @@ async function initCommand(initOptions = {}) {
1131
1108
  }
1132
1109
  }
1133
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
+
1134
1197
  function runSystemctl(args, fallbackComposeArgs) {
1135
1198
  const result = run("systemctl", args, { check: false, stdio: "inherit" });
1136
1199
  if (result.status !== 0 && fallbackComposeArgs) {
@@ -1154,6 +1217,30 @@ function restartCommand() {
1154
1217
  runSystemctl(["reload", SERVICE_NAME], ["restart"]);
1155
1218
  }
1156
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
+
1157
1244
  function logsCommand(service, options) {
1158
1245
  ensureInitialized();
1159
1246
  const args = ["logs", "--tail", String(options.tail || 200)];
@@ -1238,7 +1325,7 @@ function statusCommand() {
1238
1325
 
1239
1326
  console.log("\nConfig summary:");
1240
1327
  console.log(`DOMAIN=${envMap.DOMAIN || ""}`);
1241
- console.log(`BITCALL_ENV=${envMap.BITCALL_ENV || "dev"}`);
1328
+ console.log(`BITCALL_ENV=${envMap.BITCALL_ENV || "production"}`);
1242
1329
  console.log(`ROUTING_MODE=${envMap.ROUTING_MODE || "universal"}`);
1243
1330
  console.log(`SIP_PROVIDER_URI=${envMap.SIP_PROVIDER_URI || ""}`);
1244
1331
  console.log(`DEPLOY_MODE=${envMap.DEPLOY_MODE || ""}`);
@@ -1318,8 +1405,24 @@ async function certInstallCommand(options) {
1318
1405
 
1319
1406
  function updateCommand() {
1320
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
+
1321
1420
  runCompose(["pull"], { stdio: "inherit" });
1322
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");
1323
1426
  }
1324
1427
 
1325
1428
  function mediaStatusCommand() {
@@ -1402,11 +1505,40 @@ async function uninstallCommand(options) {
1402
1505
  fs.rmSync(RENEW_HOOK_PATH, { force: true });
1403
1506
  }
1404
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
+
1405
1530
  if (fs.existsSync(GATEWAY_DIR)) {
1406
1531
  fs.rmSync(GATEWAY_DIR, { recursive: true, force: true });
1407
1532
  }
1408
1533
 
1409
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
+ }
1410
1542
  }
1411
1543
 
1412
1544
  function configCommand() {
@@ -1432,7 +1564,7 @@ function buildProgram() {
1432
1564
  .description("Run setup wizard and provision gateway")
1433
1565
  .option("--advanced", "Show advanced configuration prompts")
1434
1566
  .option("--dev", "Quick dev setup (minimal prompts, permissive defaults)")
1435
- .option("--production", "Production setup (strict validation enabled)")
1567
+ .option("--production", "Production setup profile")
1436
1568
  .option("--domain <domain>", "Gateway domain")
1437
1569
  .option("--email <email>", "Let's Encrypt email")
1438
1570
  .option("--verbose", "Stream full installer command output")
@@ -1463,6 +1595,18 @@ function buildProgram() {
1463
1595
 
1464
1596
  program.command("update").description("Pull latest image and restart service").action(updateCommand);
1465
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);
1466
1610
 
1467
1611
  const media = program.command("media").description("Media firewall operations");
1468
1612
  media.command("status").description("Show media IPv4-only firewall state").action(mediaStatusCommand);
@@ -1488,7 +1632,7 @@ module.exports = {
1488
1632
  main,
1489
1633
  normalizeInitProfile,
1490
1634
  validateProductionConfig,
1491
- buildDevWarnings,
1635
+ buildSecurityNotes,
1492
1636
  buildQuickFlowDefaults,
1493
1637
  shouldRequireAllowlist,
1494
1638
  isOriginWildcard,