@bitcall/webrtc-sip-gateway 0.2.5 → 0.2.6

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,7 +5,7 @@ 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.5
8
+ sudo npm i -g @bitcall/webrtc-sip-gateway@0.2.6
9
9
  ```
10
10
 
11
11
  ## Main workflow
@@ -14,12 +14,13 @@ sudo npm i -g @bitcall/webrtc-sip-gateway@0.2.5
14
14
  sudo bitcall-gateway init
15
15
  sudo bitcall-gateway status
16
16
  sudo bitcall-gateway logs -f
17
+ sudo bitcall-gateway media status
17
18
  ```
18
19
 
19
- Default media policy is IPv4-only (`MEDIA_IPV6=0` and `MEDIA_FORCE_IPV4=1`),
20
- including IPv6 candidate stripping on SIP->WebRTC SDP. Set `MEDIA_IPV6=1`
21
- and `MEDIA_FORCE_IPV4=0` in `/opt/bitcall-gateway/.env` only if you want
22
- IPv6 candidates.
20
+ Default media policy is IPv4-only via IPv6 media firewall drops on RTP/TURN
21
+ ports only. Host IPv6 remains enabled for signaling and non-media traffic.
22
+ Backend selection prefers nftables on non-UFW hosts and uses ip6tables when UFW
23
+ is active.
23
24
 
24
25
  ## Commands
25
26
 
@@ -33,6 +34,9 @@ IPv6 candidates.
33
34
  - `sudo bitcall-gateway cert renew`
34
35
  - `sudo bitcall-gateway cert install --cert /path/cert.pem --key /path/key.pem`
35
36
  - `sudo bitcall-gateway update`
37
+ - `sudo bitcall-gateway media status`
38
+ - `sudo bitcall-gateway media ipv4-only on`
39
+ - `sudo bitcall-gateway media ipv4-only off`
36
40
  - `sudo bitcall-gateway uninstall`
37
41
 
38
42
  ## Files created by init
package/lib/constants.js CHANGED
@@ -14,7 +14,7 @@ module.exports = {
14
14
  SSL_DIR: path.join(GATEWAY_DIR, "ssl"),
15
15
  ENV_PATH: path.join(GATEWAY_DIR, ".env"),
16
16
  COMPOSE_PATH: path.join(GATEWAY_DIR, "docker-compose.yml"),
17
- DEFAULT_GATEWAY_IMAGE: "ghcr.io/bitcallio/webrtc-sip-gateway:0.2.5",
17
+ DEFAULT_GATEWAY_IMAGE: "ghcr.io/bitcallio/webrtc-sip-gateway:0.2.6",
18
18
  DEFAULT_PROVIDER_HOST: "sip.example.com",
19
19
  DEFAULT_WEBPHONE_ORIGIN: "*",
20
20
  RENEW_HOOK_PATH: "/etc/letsencrypt/renewal-hooks/deploy/bitcall-gateway.sh",
@@ -0,0 +1,357 @@
1
+ "use strict";
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+
6
+ const { run, runShell, output, commandExists } = require("./shell");
7
+
8
+ const MARKER = "bitcall-gateway media ipv6 block";
9
+ const NFT_TABLE = "bitcall_gateway_media_ipv6";
10
+ const NFT_RULE_FILE = "/etc/nftables.d/bitcall-gateway.nft";
11
+ const NFT_MAIN_CONF = "/etc/nftables.conf";
12
+ const NFT_INCLUDE_LINE = `include "${NFT_RULE_FILE}"`;
13
+
14
+ function withDeps(deps = {}) {
15
+ return {
16
+ fs,
17
+ run,
18
+ runShell,
19
+ output,
20
+ commandExists,
21
+ ...deps,
22
+ };
23
+ }
24
+
25
+ function toInt(value, fallback) {
26
+ const parsed = Number.parseInt(String(value), 10);
27
+ return Number.isFinite(parsed) ? parsed : fallback;
28
+ }
29
+
30
+ function normalizeOptions(options = {}) {
31
+ const rtpMin = toInt(options.rtpMin, 10000);
32
+ const rtpMax = toInt(options.rtpMax, 20000);
33
+ const turnEnabled = Boolean(options.turnEnabled);
34
+ const turnUdpPort = toInt(options.turnUdpPort, 3478);
35
+ const turnsTcpPort = toInt(options.turnsTcpPort, 5349);
36
+ const turnRelayMin = toInt(options.turnRelayMin, 49152);
37
+ const turnRelayMax = toInt(options.turnRelayMax, 49252);
38
+
39
+ return {
40
+ rtpMin,
41
+ rtpMax,
42
+ turnEnabled,
43
+ turnUdpPort,
44
+ turnsTcpPort,
45
+ turnRelayMin,
46
+ turnRelayMax,
47
+ };
48
+ }
49
+
50
+ function detectFirewallBackend(deps = {}) {
51
+ const d = withDeps(deps);
52
+ let ufwActive = false;
53
+ if (d.commandExists("ufw")) {
54
+ const ufwStatus = d.run("ufw", ["status"], { check: false, stdio: "pipe" });
55
+ ufwActive = ufwStatus.status === 0 && ((ufwStatus.stdout || "").toLowerCase().includes("status: active"));
56
+ }
57
+
58
+ if (ufwActive && d.commandExists("ip6tables")) {
59
+ return "ip6tables";
60
+ }
61
+
62
+ if (d.commandExists("nft")) {
63
+ return "nft";
64
+ }
65
+ if (d.commandExists("ip6tables")) {
66
+ return "ip6tables";
67
+ }
68
+ throw new Error(
69
+ "No IPv6 firewall backend found. Install nftables or ip6tables/netfilter-persistent."
70
+ );
71
+ }
72
+
73
+ function buildIp6tablesRules(options = {}) {
74
+ const cfg = normalizeOptions(options);
75
+ const rules = [
76
+ { proto: "udp", dport: `${cfg.rtpMin}:${cfg.rtpMax}` },
77
+ ];
78
+
79
+ if (cfg.turnEnabled) {
80
+ rules.push({ proto: "udp", dport: String(cfg.turnUdpPort) });
81
+ rules.push({ proto: "tcp", dport: String(cfg.turnsTcpPort) });
82
+ rules.push({ proto: "udp", dport: `${cfg.turnRelayMin}:${cfg.turnRelayMax}` });
83
+ }
84
+
85
+ return rules;
86
+ }
87
+
88
+ function buildNftRuleset(options = {}) {
89
+ const rules = buildIp6tablesRules(options);
90
+ const lines = [
91
+ `table inet ${NFT_TABLE} {`,
92
+ " chain input {",
93
+ " type filter hook input priority 0; policy accept;",
94
+ ];
95
+
96
+ for (const rule of rules) {
97
+ lines.push(
98
+ ` meta nfproto ipv6 ${rule.proto} dport ${rule.dport} comment \"${MARKER}\" drop`
99
+ );
100
+ }
101
+
102
+ lines.push(" }");
103
+ lines.push("}");
104
+ lines.push("");
105
+
106
+ return lines.join("\n");
107
+ }
108
+
109
+ function persistIp6tables(d) {
110
+ if (!d.commandExists("netfilter-persistent")) {
111
+ d.run("apt-get", ["update"], { stdio: "inherit" });
112
+ d.run("apt-get", ["install", "-y", "netfilter-persistent", "iptables-persistent"], {
113
+ check: false,
114
+ stdio: "inherit",
115
+ });
116
+ }
117
+
118
+ if (!d.commandExists("netfilter-persistent")) {
119
+ throw new Error("Unable to persist ip6tables rules: netfilter-persistent not available.");
120
+ }
121
+
122
+ const saved = d.run("netfilter-persistent", ["save"], {
123
+ check: false,
124
+ stdio: "inherit",
125
+ });
126
+ if (saved.status !== 0) {
127
+ throw new Error("Failed to persist ip6tables rules with netfilter-persistent save.");
128
+ }
129
+ }
130
+
131
+ function ensureNftInclude(d) {
132
+ let content = "";
133
+ if (d.fs.existsSync(NFT_MAIN_CONF)) {
134
+ content = d.fs.readFileSync(NFT_MAIN_CONF, "utf8");
135
+ } else {
136
+ content = "#!/usr/sbin/nft -f\n\n";
137
+ }
138
+
139
+ if (!content.includes(NFT_INCLUDE_LINE)) {
140
+ const suffix = content.endsWith("\n") ? "" : "\n";
141
+ content = `${content}${suffix}${NFT_INCLUDE_LINE}\n`;
142
+ d.fs.writeFileSync(NFT_MAIN_CONF, content, { mode: 0o644 });
143
+ d.fs.chmodSync(NFT_MAIN_CONF, 0o644);
144
+ }
145
+ }
146
+
147
+ function removeNftInclude(d) {
148
+ if (!d.fs.existsSync(NFT_MAIN_CONF)) {
149
+ return;
150
+ }
151
+
152
+ const content = d.fs.readFileSync(NFT_MAIN_CONF, "utf8");
153
+ const lines = content.split("\n").filter((line) => line.trim() !== NFT_INCLUDE_LINE);
154
+ const next = `${lines.join("\n").replace(/\n+$/g, "")}\n`;
155
+ d.fs.writeFileSync(NFT_MAIN_CONF, next, { mode: 0o644 });
156
+ d.fs.chmodSync(NFT_MAIN_CONF, 0o644);
157
+ }
158
+
159
+ function applyNftRules(options, d) {
160
+ d.fs.mkdirSync(path.dirname(NFT_RULE_FILE), { recursive: true, mode: 0o755 });
161
+ d.fs.writeFileSync(NFT_RULE_FILE, buildNftRuleset(options), { mode: 0o644 });
162
+ d.fs.chmodSync(NFT_RULE_FILE, 0o644);
163
+
164
+ d.run("nft", ["delete", "table", "inet", NFT_TABLE], {
165
+ check: false,
166
+ stdio: "ignore",
167
+ });
168
+ d.run("nft", ["-f", NFT_RULE_FILE], { stdio: "inherit" });
169
+
170
+ ensureNftInclude(d);
171
+
172
+ const enabled = d.run("systemctl", ["enable", "--now", "nftables"], {
173
+ check: false,
174
+ stdio: "inherit",
175
+ });
176
+ if (enabled.status !== 0) {
177
+ throw new Error("Failed to enable nftables service for persistent IPv6 media block rules.");
178
+ }
179
+ }
180
+
181
+ function removeNftRules(d) {
182
+ d.run("nft", ["delete", "table", "inet", NFT_TABLE], {
183
+ check: false,
184
+ stdio: "ignore",
185
+ });
186
+
187
+ if (d.fs.existsSync(NFT_RULE_FILE)) {
188
+ d.fs.rmSync(NFT_RULE_FILE, { force: true });
189
+ }
190
+ removeNftInclude(d);
191
+
192
+ d.run("systemctl", ["reload", "nftables"], {
193
+ check: false,
194
+ stdio: "ignore",
195
+ });
196
+ }
197
+
198
+ function ip6tableRuleArgs(rule) {
199
+ return [
200
+ "-p",
201
+ rule.proto,
202
+ "--dport",
203
+ rule.dport,
204
+ "-m",
205
+ "comment",
206
+ "--comment",
207
+ MARKER,
208
+ "-j",
209
+ "DROP",
210
+ ];
211
+ }
212
+
213
+ function applyIp6tablesRules(options, d) {
214
+ const rules = buildIp6tablesRules(options);
215
+
216
+ for (const rule of rules) {
217
+ const args = ip6tableRuleArgs(rule);
218
+ const exists = d.run("ip6tables", ["-C", "INPUT", ...args], {
219
+ check: false,
220
+ stdio: "ignore",
221
+ }).status === 0;
222
+
223
+ if (!exists) {
224
+ d.run("ip6tables", ["-I", "INPUT", "1", ...args], {
225
+ stdio: "inherit",
226
+ });
227
+ }
228
+ }
229
+
230
+ persistIp6tables(d);
231
+ }
232
+
233
+ function removeIp6tablesRules(options, d) {
234
+ const rules = buildIp6tablesRules(options);
235
+
236
+ for (const rule of rules) {
237
+ const args = ip6tableRuleArgs(rule);
238
+ for (;;) {
239
+ const exists = d.run("ip6tables", ["-C", "INPUT", ...args], {
240
+ check: false,
241
+ stdio: "ignore",
242
+ }).status === 0;
243
+
244
+ if (!exists) {
245
+ break;
246
+ }
247
+
248
+ d.run("ip6tables", ["-D", "INPUT", ...args], {
249
+ stdio: "inherit",
250
+ });
251
+ }
252
+ }
253
+
254
+ persistIp6tables(d);
255
+ }
256
+
257
+ function isNftPresent(d) {
258
+ const res = d.run("nft", ["list", "table", "inet", NFT_TABLE], {
259
+ check: false,
260
+ stdio: "pipe",
261
+ });
262
+ if (res.status !== 0) {
263
+ return false;
264
+ }
265
+ return (res.stdout || "").includes(MARKER);
266
+ }
267
+
268
+ function isIp6tablesPresent(d) {
269
+ const rules = d.output("sh", ["-lc", "ip6tables-save 2>/dev/null || true"]);
270
+ return rules.includes(MARKER);
271
+ }
272
+
273
+ function applyMediaIpv4OnlyRules(options = {}, runtime = {}) {
274
+ const d = withDeps(runtime.deps);
275
+ const backend = runtime.backend || detectFirewallBackend(d);
276
+
277
+ if (backend === "nft") {
278
+ applyNftRules(options, d);
279
+ return { backend };
280
+ }
281
+
282
+ if (backend === "ip6tables") {
283
+ applyIp6tablesRules(options, d);
284
+ return { backend };
285
+ }
286
+
287
+ throw new Error(`Unsupported firewall backend: ${backend}`);
288
+ }
289
+
290
+ function removeMediaIpv4OnlyRules(options = {}, runtime = {}) {
291
+ const d = withDeps(runtime.deps);
292
+ const backend = runtime.backend || detectFirewallBackend(d);
293
+
294
+ if (backend === "nft") {
295
+ removeNftRules(d);
296
+ return { backend };
297
+ }
298
+
299
+ if (backend === "ip6tables") {
300
+ removeIp6tablesRules(options, d);
301
+ return { backend };
302
+ }
303
+
304
+ throw new Error(`Unsupported firewall backend: ${backend}`);
305
+ }
306
+
307
+ function isMediaIpv4OnlyRulesPresent(runtime = {}) {
308
+ const d = withDeps(runtime.deps);
309
+ let backend = runtime.backend;
310
+
311
+ try {
312
+ backend = backend || detectFirewallBackend(d);
313
+ } catch (error) {
314
+ return {
315
+ enabled: false,
316
+ backend: null,
317
+ error: error.message,
318
+ };
319
+ }
320
+
321
+ if (backend === "nft") {
322
+ return {
323
+ enabled: isNftPresent(d),
324
+ backend,
325
+ marker: MARKER,
326
+ };
327
+ }
328
+
329
+ if (backend === "ip6tables") {
330
+ return {
331
+ enabled: isIp6tablesPresent(d),
332
+ backend,
333
+ marker: MARKER,
334
+ };
335
+ }
336
+
337
+ return {
338
+ enabled: false,
339
+ backend,
340
+ marker: MARKER,
341
+ };
342
+ }
343
+
344
+ module.exports = {
345
+ MARKER,
346
+ NFT_TABLE,
347
+ NFT_RULE_FILE,
348
+ NFT_MAIN_CONF,
349
+ NFT_INCLUDE_LINE,
350
+ normalizeOptions,
351
+ detectFirewallBackend,
352
+ buildIp6tablesRules,
353
+ buildNftRuleset,
354
+ applyMediaIpv4OnlyRules,
355
+ removeMediaIpv4OnlyRules,
356
+ isMediaIpv4OnlyRulesPresent,
357
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bitcall/webrtc-sip-gateway",
3
- "version": "0.2.5",
3
+ "version": "0.2.6",
4
4
  "description": "Linux CLI for bootstrapping and managing the Bitcall WebRTC-to-SIP Gateway",
5
5
  "repository": {
6
6
  "type": "git",
@@ -22,7 +22,7 @@
22
22
  },
23
23
  "scripts": {
24
24
  "lint": "node --check src/index.js && node --check bin/bitcall-gateway.js && for f in lib/*.js; do node --check \"$f\"; done",
25
- "test": "node test/smoke.test.js",
25
+ "test": "node test/smoke.test.js && node test/firewall.test.js",
26
26
  "pack:dry": "npm pack --dry-run"
27
27
  },
28
28
  "dependencies": {
package/src/index.js CHANGED
@@ -33,8 +33,14 @@ const {
33
33
  ensureComposePlugin,
34
34
  } = require("../lib/system");
35
35
  const { readTemplate, renderTemplate } = require("../lib/template");
36
+ const {
37
+ detectFirewallBackend,
38
+ applyMediaIpv4OnlyRules,
39
+ removeMediaIpv4OnlyRules,
40
+ isMediaIpv4OnlyRulesPresent,
41
+ } = require("../lib/firewall");
36
42
 
37
- const PACKAGE_VERSION = "0.2.5";
43
+ const PACKAGE_VERSION = "0.2.6";
38
44
 
39
45
  function detectComposeCommand() {
40
46
  if (run("docker", ["compose", "version"], { check: false }).status === 0) {
@@ -81,9 +87,11 @@ function envOrder() {
81
87
  return [
82
88
  "GATEWAY_VERSION",
83
89
  "BITCALL_GATEWAY_IMAGE",
90
+ "BITCALL_ENV",
84
91
  "DOMAIN",
85
92
  "PUBLIC_IP",
86
93
  "DEPLOY_MODE",
94
+ "ROUTING_MODE",
87
95
  "SIP_PROVIDER_URI",
88
96
  "SIP_UPSTREAM_TRANSPORT",
89
97
  "SIP_UPSTREAM_PORT",
@@ -103,8 +111,11 @@ function envOrder() {
103
111
  "WSS_LISTEN_PORT",
104
112
  "INTERNAL_WSS_PORT",
105
113
  "INTERNAL_WS_PORT",
106
- "MEDIA_IPV6",
107
- "MEDIA_FORCE_IPV4",
114
+ "MEDIA_IPV4_ONLY",
115
+ "TURN_UDP_PORT",
116
+ "TURNS_TCP_PORT",
117
+ "TURN_RELAY_MIN_PORT",
118
+ "TURN_RELAY_MAX_PORT",
108
119
  "RTPENGINE_MIN_PORT",
109
120
  "RTPENGINE_MAX_PORT",
110
121
  "WITH_REVERSE_PROXY",
@@ -202,6 +213,52 @@ function configureFirewall(config) {
202
213
  run("ufw", ["reload"], { check: false, stdio: "ignore" });
203
214
  }
204
215
 
216
+ function countAllowedDomains(raw) {
217
+ if (!raw) {
218
+ return 0;
219
+ }
220
+ return raw
221
+ .split(",")
222
+ .map((item) => item.trim())
223
+ .filter(Boolean).length;
224
+ }
225
+
226
+ function mediaFirewallOptionsFromConfig(config) {
227
+ return {
228
+ rtpMin: Number.parseInt(config.rtpMin || "10000", 10),
229
+ rtpMax: Number.parseInt(config.rtpMax || "20000", 10),
230
+ turnEnabled: config.turnMode === "coturn",
231
+ turnUdpPort: Number.parseInt(config.turnUdpPort || "3478", 10),
232
+ turnsTcpPort: Number.parseInt(config.turnsTcpPort || "5349", 10),
233
+ turnRelayMin: Number.parseInt(config.turnRelayMinPort || "49152", 10),
234
+ turnRelayMax: Number.parseInt(config.turnRelayMaxPort || "49252", 10),
235
+ };
236
+ }
237
+
238
+ function mediaFirewallOptionsFromEnv(envMap) {
239
+ return {
240
+ rtpMin: Number.parseInt(envMap.RTPENGINE_MIN_PORT || "10000", 10),
241
+ rtpMax: Number.parseInt(envMap.RTPENGINE_MAX_PORT || "20000", 10),
242
+ turnEnabled: (envMap.TURN_MODE || "none") === "coturn",
243
+ turnUdpPort: Number.parseInt(envMap.TURN_UDP_PORT || "3478", 10),
244
+ turnsTcpPort: Number.parseInt(envMap.TURNS_TCP_PORT || "5349", 10),
245
+ turnRelayMin: Number.parseInt(envMap.TURN_RELAY_MIN_PORT || "49152", 10),
246
+ turnRelayMax: Number.parseInt(envMap.TURN_RELAY_MAX_PORT || "49252", 10),
247
+ };
248
+ }
249
+
250
+ async function confirmContinueWithoutMediaBlock() {
251
+ const prompt = new Prompter();
252
+ try {
253
+ return await prompt.askYesNo(
254
+ "Continue without IPv6 media block (may cause no-audio on some networks)?",
255
+ false
256
+ );
257
+ } finally {
258
+ prompt.close();
259
+ }
260
+ }
261
+
205
262
  function generateSelfSigned(certPath, keyPath, domain, days = 1) {
206
263
  ensureDir(path.dirname(certPath), 0o700);
207
264
  run(
@@ -346,143 +403,179 @@ async function runWizard(existing = {}, preflight = {}) {
346
403
  try {
347
404
  const detectedIp = detectPublicIp();
348
405
  const domain = await prompt.askText("Gateway domain", existing.DOMAIN || "", { required: true });
349
- const publicIp = await prompt.askText("Public IP", existing.PUBLIC_IP || detectedIp, {
350
- required: true,
351
- });
406
+ let publicIp = existing.PUBLIC_IP || detectedIp || "";
407
+ if (!publicIp) {
408
+ publicIp = await prompt.askText("Public IPv4 (auto-detect failed)", "", { required: true });
409
+ }
352
410
 
353
411
  const resolved = resolveDomainIpv4(domain);
354
412
  if (resolved.length > 0 && !resolved.includes(publicIp)) {
355
413
  console.log(`Warning: DNS for ${domain} resolves to ${resolved.join(", ")}, not ${publicIp}.`);
356
414
  }
357
415
 
358
- const conflictModeDefault =
416
+ const autoDeployMode =
359
417
  (preflight.p80 && preflight.p80.inUse) || (preflight.p443 && preflight.p443.inUse)
360
- ? 1
361
- : 0;
362
- const deployMode = await prompt.askChoice(
363
- "Deployment mode",
364
- ["standalone", "reverse-proxy"],
365
- conflictModeDefault
366
- );
418
+ ? "reverse-proxy"
419
+ : "standalone";
367
420
 
368
- if (deployMode === "reverse-proxy") {
369
- console.log("Note: forward /turn-credentials and websocket upgrade to 127.0.0.1:8443, and ACME path to 127.0.0.1:8080.");
370
- }
371
-
372
- const tlsMode = await prompt.askChoice(
373
- "TLS certificate mode",
374
- ["letsencrypt", "custom", "dev-self-signed"],
375
- 0
376
- );
421
+ const advanced = await prompt.askYesNo("Advanced setup", false);
377
422
 
423
+ let deployMode = autoDeployMode;
424
+ let tlsMode = "letsencrypt";
378
425
  let acmeEmail = existing.ACME_EMAIL || "";
379
426
  let customCertPath = "";
380
427
  let customKeyPath = "";
428
+ let bitcallEnv = existing.BITCALL_ENV || "production";
429
+ let routingMode = existing.ROUTING_MODE || "universal";
430
+ let sipProviderHost = DEFAULT_PROVIDER_HOST;
431
+ let sipTransport = "udp";
432
+ let sipPort = "5060";
433
+ let sipProviderUri = "";
434
+ let allowedDomains = existing.ALLOWED_SIP_DOMAINS || "";
435
+ let sipTrustedIps = existing.SIP_TRUSTED_IPS || "";
436
+ let turnMode = await prompt.askYesNo("Enable built-in TURN (coturn)?", true) ? "coturn" : "none";
437
+ let turnSecret = "";
438
+ let turnTtl = existing.TURN_TTL || "86400";
439
+ let turnApiToken = existing.TURN_API_TOKEN || "";
440
+ let turnExternalUrls = "";
441
+ let turnExternalUsername = "";
442
+ let turnExternalCredential = "";
443
+ let webphoneOrigin = existing.WEBPHONE_ORIGIN || DEFAULT_WEBPHONE_ORIGIN;
444
+ let configureUfw = true;
445
+ let mediaIpv4Only = existing.MEDIA_IPV4_ONLY ? existing.MEDIA_IPV4_ONLY === "1" : true;
381
446
 
382
- if (tlsMode === "letsencrypt") {
383
- acmeEmail = await prompt.askText("Let's Encrypt email", acmeEmail, { required: true });
384
- } else if (tlsMode === "custom") {
385
- customCertPath = await prompt.askText("Path to TLS certificate (PEM)", existing.TLS_CERT || "", {
386
- required: true,
387
- });
388
- customKeyPath = await prompt.askText("Path to TLS private key (PEM)", existing.TLS_KEY || "", {
389
- required: true,
390
- });
447
+ if (turnMode === "coturn") {
448
+ turnSecret = crypto.randomBytes(32).toString("hex");
391
449
  }
392
450
 
393
- const sipProviderHost = await prompt.askText(
394
- "SIP provider host",
395
- existing.SIP_PROVIDER_URI
396
- ? existing.SIP_PROVIDER_URI.replace(/^sip:/, "").split(":")[0]
397
- : DEFAULT_PROVIDER_HOST,
398
- { required: true }
399
- );
400
-
401
- const sipTransportChoice = await prompt.askChoice(
402
- "SIP provider transport",
403
- ["udp", "tcp", "tls", "custom"],
404
- 0
405
- );
451
+ if (advanced) {
452
+ deployMode = await prompt.askChoice(
453
+ "Deployment mode",
454
+ ["standalone", "reverse-proxy"],
455
+ autoDeployMode === "reverse-proxy" ? 1 : 0
456
+ );
406
457
 
407
- let sipTransport = sipTransportChoice;
408
- let sipPort = "5060";
458
+ tlsMode = await prompt.askChoice(
459
+ "TLS certificate mode",
460
+ ["letsencrypt", "custom", "dev-self-signed"],
461
+ 0
462
+ );
409
463
 
410
- if (sipTransportChoice === "tcp" || sipTransportChoice === "udp") {
411
- sipPort = "5060";
412
- } else if (sipTransportChoice === "tls") {
413
- sipPort = "5061";
414
- } else {
415
- sipTransport = await prompt.askChoice("Custom transport", ["udp", "tcp", "tls"], 0);
416
- const defaultPort = sipTransport === "tls" ? "5061" : "5060";
417
- sipPort = await prompt.askText("Custom SIP port", defaultPort, { required: true });
418
- }
464
+ if (tlsMode === "custom") {
465
+ customCertPath = await prompt.askText("Path to TLS certificate (PEM)", existing.TLS_CERT || "", {
466
+ required: true,
467
+ });
468
+ customKeyPath = await prompt.askText("Path to TLS private key (PEM)", existing.TLS_KEY || "", {
469
+ required: true,
470
+ });
471
+ }
419
472
 
420
- const sipProviderUri = `sip:${sipProviderHost}:${sipPort};transport=${sipTransport}`;
473
+ if (tlsMode === "letsencrypt") {
474
+ acmeEmail = await prompt.askText("Let's Encrypt email", acmeEmail, { required: true });
475
+ } else {
476
+ acmeEmail = "";
477
+ }
421
478
 
422
- const allowedDomains = await prompt.askText(
423
- "Allowed SIP domains (comma-separated; blank enables dev mode)",
424
- existing.ALLOWED_SIP_DOMAINS || sipProviderHost
425
- );
479
+ bitcallEnv = await prompt.askChoice("Environment", ["production", "dev"], bitcallEnv === "dev" ? 1 : 0);
480
+ allowedDomains = await prompt.askText(
481
+ "Allowed SIP domains (comma-separated; required in production)",
482
+ allowedDomains
483
+ );
426
484
 
427
- if (!allowedDomains.trim()) {
428
- console.log("Warning: ALLOWED_SIP_DOMAINS is empty (dev mode, open relay risk).");
429
- }
485
+ routingMode = await prompt.askChoice(
486
+ "Routing mode",
487
+ ["universal", "single-provider"],
488
+ routingMode === "single-provider" ? 1 : 0
489
+ );
430
490
 
431
- const sipTrustedIps = await prompt.askText(
432
- "Trusted SIP source IPs (optional, comma-separated)",
433
- existing.SIP_TRUSTED_IPS || ""
434
- );
491
+ if (routingMode === "single-provider") {
492
+ sipProviderHost = await prompt.askText(
493
+ "SIP provider host",
494
+ existing.SIP_PROVIDER_URI
495
+ ? existing.SIP_PROVIDER_URI.replace(/^sip:/, "").split(":")[0]
496
+ : DEFAULT_PROVIDER_HOST,
497
+ { required: true }
498
+ );
499
+
500
+ const sipTransportChoice = await prompt.askChoice(
501
+ "SIP provider transport",
502
+ ["udp", "tcp", "tls", "custom"],
503
+ 0
504
+ );
505
+
506
+ sipTransport = sipTransportChoice;
507
+ if (sipTransportChoice === "tcp" || sipTransportChoice === "udp") {
508
+ sipPort = "5060";
509
+ } else if (sipTransportChoice === "tls") {
510
+ sipPort = "5061";
511
+ } else {
512
+ sipTransport = await prompt.askChoice("Custom transport", ["udp", "tcp", "tls"], 0);
513
+ sipPort = await prompt.askText(
514
+ "Custom SIP port",
515
+ sipTransport === "tls" ? "5061" : "5060",
516
+ { required: true }
517
+ );
518
+ }
519
+
520
+ sipProviderUri = `sip:${sipProviderHost}:${sipPort};transport=${sipTransport}`;
521
+ }
435
522
 
436
- const turnMode = await prompt.askChoice("TURN mode", ["none", "external", "coturn"], 0);
523
+ sipTrustedIps = await prompt.askText(
524
+ "Trusted SIP source IPs (optional, comma-separated)",
525
+ sipTrustedIps
526
+ );
437
527
 
438
- let turnSecret = existing.TURN_SECRET || "";
439
- let turnTtl = existing.TURN_TTL || "86400";
440
- let turnApiToken = existing.TURN_API_TOKEN || "";
441
- let turnExternalUrls = "";
442
- let turnExternalUsername = "";
443
- let turnExternalCredential = "";
528
+ const turnDefaultIndex = turnMode === "external" ? 2 : turnMode === "coturn" ? 1 : 0;
529
+ turnMode = await prompt.askChoice("TURN mode", ["none", "coturn", "external"], turnDefaultIndex);
530
+ if (turnMode === "coturn") {
531
+ turnSecret = crypto.randomBytes(32).toString("hex");
532
+ turnTtl = await prompt.askText("TURN credential TTL seconds", turnTtl, { required: true });
533
+ turnApiToken = await prompt.askText("TURN API token (optional)", turnApiToken);
534
+ } else if (turnMode === "external") {
535
+ turnSecret = "";
536
+ turnApiToken = "";
537
+ turnExternalUrls = await prompt.askText("External TURN urls", existing.TURN_EXTERNAL_URLS || "", {
538
+ required: true,
539
+ });
540
+ turnExternalUsername = await prompt.askText(
541
+ "External TURN username",
542
+ existing.TURN_EXTERNAL_USERNAME || ""
543
+ );
544
+ turnExternalCredential = await prompt.askText(
545
+ "External TURN credential",
546
+ existing.TURN_EXTERNAL_CREDENTIAL || ""
547
+ );
548
+ } else {
549
+ turnSecret = "";
550
+ turnApiToken = "";
551
+ }
444
552
 
445
- if (turnMode === "external") {
446
- turnExternalUrls = await prompt.askText("External TURN urls", existing.TURN_EXTERNAL_URLS || "", {
447
- required: true,
448
- });
449
- turnExternalUsername = await prompt.askText(
450
- "External TURN username",
451
- existing.TURN_EXTERNAL_USERNAME || ""
553
+ webphoneOrigin = await prompt.askText(
554
+ "Allowed webphone origin (* for any)",
555
+ webphoneOrigin
452
556
  );
453
- turnExternalCredential = await prompt.askText(
454
- "External TURN credential",
455
- existing.TURN_EXTERNAL_CREDENTIAL || ""
557
+
558
+ mediaIpv4Only = await prompt.askYesNo(
559
+ "Media IPv4-only mode (block IPv6 RTP/TURN on host firewall)",
560
+ mediaIpv4Only
456
561
  );
457
- }
458
562
 
459
- if (turnMode === "coturn") {
460
- turnSecret = crypto.randomBytes(32).toString("hex");
461
- turnTtl = await prompt.askText("TURN credential TTL seconds", turnTtl, { required: true });
462
- turnApiToken = await prompt.askText("TURN API token (optional)", turnApiToken);
563
+ configureUfw = await prompt.askYesNo("Configure ufw firewall rules now", true);
463
564
  } else {
464
- turnSecret = "";
465
- turnApiToken = "";
565
+ acmeEmail = await prompt.askText("Let's Encrypt email", acmeEmail, { required: true });
466
566
  }
467
567
 
468
- const webphoneOrigin = await prompt.askText(
469
- "Allowed webphone origin (* for any)",
470
- existing.WEBPHONE_ORIGIN || DEFAULT_WEBPHONE_ORIGIN
471
- );
472
-
473
- const mediaIpv6Enabled = await prompt.askYesNo(
474
- "Enable IPv6 media candidates? (default: No)",
475
- existing.MEDIA_IPV6 === "1"
476
- );
477
-
478
- const mediaForceIpv4Enabled = await prompt.askYesNo(
479
- "Force IPv4-only SDP candidates? (default: Yes)",
480
- existing.MEDIA_FORCE_IPV4
481
- ? existing.MEDIA_FORCE_IPV4 === "1"
482
- : !mediaIpv6Enabled
483
- );
568
+ if (bitcallEnv === "production" && !allowedDomains.trim()) {
569
+ throw new Error(
570
+ "Production mode requires ALLOWED_SIP_DOMAINS. Re-run init with Advanced=Yes and provide allowlisted SIP domains, or switch Environment to dev."
571
+ );
572
+ }
484
573
 
485
- const configureUfw = await prompt.askYesNo("Configure ufw firewall rules now", true);
574
+ if (deployMode === "reverse-proxy") {
575
+ console.log(
576
+ "Note: forward /turn-credentials and websocket upgrade to 127.0.0.1:8443, and ACME path to 127.0.0.1:8080."
577
+ );
578
+ }
486
579
 
487
580
  const config = {
488
581
  domain,
@@ -492,6 +585,8 @@ async function runWizard(existing = {}, preflight = {}) {
492
585
  acmeEmail,
493
586
  customCertPath,
494
587
  customKeyPath,
588
+ bitcallEnv,
589
+ routingMode,
495
590
  sipProviderHost,
496
591
  sipTransport,
497
592
  sipPort,
@@ -506,10 +601,13 @@ async function runWizard(existing = {}, preflight = {}) {
506
601
  turnExternalUsername,
507
602
  turnExternalCredential,
508
603
  webphoneOrigin,
509
- mediaIpv6: mediaIpv6Enabled ? "1" : "0",
510
- mediaForceIpv4: mediaForceIpv4Enabled ? "1" : "0",
511
- rtpMin: "10000",
512
- rtpMax: "20000",
604
+ mediaIpv4Only: mediaIpv4Only ? "1" : "0",
605
+ rtpMin: existing.RTPENGINE_MIN_PORT || "10000",
606
+ rtpMax: existing.RTPENGINE_MAX_PORT || "20000",
607
+ turnUdpPort: existing.TURN_UDP_PORT || "3478",
608
+ turnsTcpPort: existing.TURNS_TCP_PORT || "5349",
609
+ turnRelayMinPort: existing.TURN_RELAY_MIN_PORT || "49152",
610
+ turnRelayMaxPort: existing.TURN_RELAY_MAX_PORT || "49252",
513
611
  acmeListenPort: deployMode === "reverse-proxy" ? "8080" : "80",
514
612
  wssListenPort: deployMode === "reverse-proxy" ? "8443" : "443",
515
613
  internalWssPort: "8443",
@@ -517,16 +615,19 @@ async function runWizard(existing = {}, preflight = {}) {
517
615
  configureUfw,
518
616
  };
519
617
 
618
+ const allowedCount = countAllowedDomains(config.allowedDomains);
520
619
  console.log("\nSummary:");
521
620
  console.log(` Domain: ${config.domain}`);
522
621
  console.log(` Public IP: ${config.publicIp}`);
523
- console.log(` Deploy mode: ${config.deployMode}`);
524
- console.log(` TLS mode: ${config.tlsMode}`);
525
- console.log(` SIP provider URI: ${config.sipProviderUri}`);
526
- console.log(` Allowed SIP domains: ${config.allowedDomains || "(empty/dev-mode)"}`);
527
- console.log(` TURN mode: ${config.turnMode}`);
528
- console.log(` IPv6 media candidates: ${config.mediaIpv6 === "1" ? "enabled" : "disabled (IPv4-only)"}`);
529
- console.log(` Force IPv4 SDP strip: ${config.mediaForceIpv4 === "1" ? "enabled" : "disabled"}`);
622
+ console.log(` TLS: ${config.tlsMode === "letsencrypt" ? "Let's Encrypt" : config.tlsMode}`);
623
+ console.log(` Deploy mode: ${config.deployMode}${advanced ? "" : " (auto)"}`);
624
+ console.log(` TURN: ${config.turnMode}`);
625
+ console.log(
626
+ ` Media: ${config.mediaIpv4Only === "1" ? "IPv4-only (IPv6 signaling allowed; IPv6 media blocked)" : "dual-stack"}`
627
+ );
628
+ console.log(
629
+ ` Security: Allowed SIP domains ${allowedCount > 0 ? `${allowedCount} entries` : "not set"}`
630
+ );
530
631
 
531
632
  const proceed = await prompt.askYesNo("Proceed with provisioning", true);
532
633
  if (!proceed) {
@@ -553,9 +654,11 @@ function renderEnvContent(config, tlsCert, tlsKey) {
553
654
  const content = renderTemplate(".env.template", {
554
655
  GATEWAY_VERSION: PACKAGE_VERSION,
555
656
  BITCALL_GATEWAY_IMAGE: DEFAULT_GATEWAY_IMAGE,
657
+ BITCALL_ENV: config.bitcallEnv,
556
658
  DOMAIN: config.domain,
557
659
  PUBLIC_IP: config.publicIp,
558
660
  DEPLOY_MODE: config.deployMode,
661
+ ROUTING_MODE: config.routingMode,
559
662
  SIP_PROVIDER_URI: config.sipProviderUri,
560
663
  SIP_UPSTREAM_TRANSPORT: config.sipTransport,
561
664
  SIP_UPSTREAM_PORT: config.sipPort,
@@ -575,8 +678,11 @@ function renderEnvContent(config, tlsCert, tlsKey) {
575
678
  WSS_LISTEN_PORT: config.wssListenPort,
576
679
  INTERNAL_WSS_PORT: config.internalWssPort,
577
680
  INTERNAL_WS_PORT: config.internalWsPort,
578
- MEDIA_IPV6: config.mediaIpv6,
579
- MEDIA_FORCE_IPV4: config.mediaForceIpv4,
681
+ MEDIA_IPV4_ONLY: config.mediaIpv4Only,
682
+ TURN_UDP_PORT: config.turnUdpPort,
683
+ TURNS_TCP_PORT: config.turnsTcpPort,
684
+ TURN_RELAY_MIN_PORT: config.turnRelayMinPort,
685
+ TURN_RELAY_MAX_PORT: config.turnRelayMaxPort,
580
686
  });
581
687
 
582
688
  let extra = "";
@@ -696,6 +802,22 @@ async function initCommand() {
696
802
  configureFirewall(config);
697
803
  }
698
804
 
805
+ if (config.mediaIpv4Only === "1") {
806
+ try {
807
+ const applied = applyMediaIpv4OnlyRules(mediaFirewallOptionsFromConfig(config));
808
+ console.log(`Applied IPv6 media block rules (${applied.backend}).`);
809
+ } catch (error) {
810
+ console.error(`IPv6 media block setup failed: ${error.message}`);
811
+ const proceed = await confirmContinueWithoutMediaBlock();
812
+ if (!proceed) {
813
+ throw new Error("Initialization stopped because media IPv4-only firewall rules were not applied.");
814
+ }
815
+ updateGatewayEnv({ MEDIA_IPV4_ONLY: "0" });
816
+ config.mediaIpv4Only = "0";
817
+ console.log("Continuing without IPv6 media block rules.");
818
+ }
819
+ }
820
+
699
821
  startGatewayStack();
700
822
 
701
823
  if (config.tlsMode === "letsencrypt") {
@@ -783,6 +905,8 @@ function statusCommand() {
783
905
  }).status === 0;
784
906
  }
785
907
 
908
+ const mediaStatus = isMediaIpv4OnlyRulesPresent();
909
+
786
910
  console.log("Bitcall Gateway Status\n");
787
911
  console.log(`Gateway service: ${formatMark(active)} ${active ? "running" : "stopped"}`);
788
912
  console.log(`Auto-start: ${formatMark(enabled)} ${enabled ? "enabled" : "disabled"}`);
@@ -792,8 +916,17 @@ function statusCommand() {
792
916
  console.log(`Port ${envMap.WSS_LISTEN_PORT || "443"}: ${formatMark(p443.inUse)} listening`);
793
917
  console.log(`Port 5060: ${formatMark(p5060.inUse)} listening`);
794
918
  console.log(`rtpengine control: ${formatMark(rtpReady)} reachable`);
795
- console.log(`IPv6 media candidates: ${(envMap.MEDIA_IPV6 || "0") === "1" ? "enabled" : "disabled (IPv4-only)"}`);
796
- console.log(`Force IPv4 SDP strip: ${(envMap.MEDIA_FORCE_IPV4 || "1") === "1" ? "enabled" : "disabled"}`);
919
+ if (mediaStatus.backend) {
920
+ console.log(`Media IPv4-only: ${mediaStatus.enabled ? "enabled" : "disabled"}`);
921
+ console.log(`Media firewall backend: ${mediaStatus.backend}`);
922
+ } else {
923
+ console.log(`Media IPv4-only: disabled (backend unavailable)`);
924
+ }
925
+ if (!mediaStatus.enabled) {
926
+ console.log(
927
+ "Warning: IPv6 media may cause no-audio on some clients; enable via `bitcall-gateway media ipv4-only on`"
928
+ );
929
+ }
797
930
  if (envMap.TURN_MODE && envMap.TURN_MODE !== "none") {
798
931
  console.log(`/turn-credentials: ${formatMark(turnReady)} reachable`);
799
932
  }
@@ -807,12 +940,13 @@ function statusCommand() {
807
940
 
808
941
  console.log("\nConfig summary:");
809
942
  console.log(`DOMAIN=${envMap.DOMAIN || ""}`);
943
+ console.log(`BITCALL_ENV=${envMap.BITCALL_ENV || "production"}`);
944
+ console.log(`ROUTING_MODE=${envMap.ROUTING_MODE || "universal"}`);
810
945
  console.log(`SIP_PROVIDER_URI=${envMap.SIP_PROVIDER_URI || ""}`);
811
946
  console.log(`DEPLOY_MODE=${envMap.DEPLOY_MODE || ""}`);
812
947
  console.log(`ALLOWED_SIP_DOMAINS=${envMap.ALLOWED_SIP_DOMAINS || ""}`);
813
948
  console.log(`TURN_MODE=${envMap.TURN_MODE || "none"}`);
814
- console.log(`MEDIA_IPV6=${envMap.MEDIA_IPV6 || "0"}`);
815
- console.log(`MEDIA_FORCE_IPV4=${envMap.MEDIA_FORCE_IPV4 || "1"}`);
949
+ console.log(`MEDIA_IPV4_ONLY=${envMap.MEDIA_IPV4_ONLY || "1"}`);
816
950
  }
817
951
 
818
952
  function certStatusCommand() {
@@ -890,6 +1024,42 @@ function updateCommand() {
890
1024
  runSystemctl(["reload", SERVICE_NAME], ["up", "-d", "--remove-orphans"]);
891
1025
  }
892
1026
 
1027
+ function mediaStatusCommand() {
1028
+ ensureInitialized();
1029
+ const envMap = loadEnvFile(ENV_PATH);
1030
+ const desired = (envMap.MEDIA_IPV4_ONLY || "1") === "1";
1031
+ const state = isMediaIpv4OnlyRulesPresent();
1032
+
1033
+ console.log("Media firewall status\n");
1034
+ console.log(`Configured mode: ${desired ? "IPv4-only" : "dual-stack"}`);
1035
+ if (!state.backend) {
1036
+ console.log(`Backend: unavailable (${state.error || "not detected"})`);
1037
+ console.log("Rules active: no");
1038
+ return;
1039
+ }
1040
+
1041
+ console.log(`Backend: ${state.backend}`);
1042
+ console.log(`Rules active: ${state.enabled ? "yes" : "no"}`);
1043
+ }
1044
+
1045
+ function mediaIpv4OnlyOnCommand() {
1046
+ ensureInitialized();
1047
+ const envMap = loadEnvFile(ENV_PATH);
1048
+ const options = mediaFirewallOptionsFromEnv(envMap);
1049
+ const applied = applyMediaIpv4OnlyRules(options);
1050
+ updateGatewayEnv({ MEDIA_IPV4_ONLY: "1" });
1051
+ console.log(`Enabled IPv6 media block rules (${applied.backend}).`);
1052
+ }
1053
+
1054
+ function mediaIpv4OnlyOffCommand() {
1055
+ ensureInitialized();
1056
+ const envMap = loadEnvFile(ENV_PATH);
1057
+ const options = mediaFirewallOptionsFromEnv(envMap);
1058
+ const removed = removeMediaIpv4OnlyRules(options);
1059
+ updateGatewayEnv({ MEDIA_IPV4_ONLY: "0" });
1060
+ console.log(`Disabled IPv6 media block rules (${removed.backend}).`);
1061
+ }
1062
+
893
1063
  async function uninstallCommand(options) {
894
1064
  if (!options.yes) {
895
1065
  const prompt = new Prompter();
@@ -903,6 +1073,18 @@ async function uninstallCommand(options) {
903
1073
  }
904
1074
  }
905
1075
 
1076
+ let uninstallEnv = {};
1077
+ if (fs.existsSync(ENV_PATH)) {
1078
+ uninstallEnv = loadEnvFile(ENV_PATH);
1079
+ }
1080
+
1081
+ try {
1082
+ const backend = detectFirewallBackend();
1083
+ removeMediaIpv4OnlyRules(mediaFirewallOptionsFromEnv(uninstallEnv), { backend });
1084
+ } catch (error) {
1085
+ console.log(`Warning: failed to remove IPv6 media firewall rules automatically: ${error.message}`);
1086
+ }
1087
+
906
1088
  run("systemctl", ["stop", SERVICE_NAME], { check: false, stdio: "inherit" });
907
1089
  run("systemctl", ["disable", SERVICE_NAME], { check: false, stdio: "inherit" });
908
1090
 
@@ -974,6 +1156,13 @@ function buildProgram() {
974
1156
 
975
1157
  program.command("update").description("Pull latest image and restart service").action(updateCommand);
976
1158
  program.command("config").description("Print active configuration (secrets hidden)").action(configCommand);
1159
+
1160
+ const media = program.command("media").description("Media firewall operations");
1161
+ media.command("status").description("Show media IPv4-only firewall state").action(mediaStatusCommand);
1162
+ const mediaIpv4Only = media.command("ipv4-only").description("Toggle IPv6 media block");
1163
+ mediaIpv4Only.command("on").description("Enable IPv4-only media mode").action(mediaIpv4OnlyOnCommand);
1164
+ mediaIpv4Only.command("off").description("Disable IPv4-only media mode").action(mediaIpv4OnlyOffCommand);
1165
+
977
1166
  program
978
1167
  .command("uninstall")
979
1168
  .description("Remove gateway service and files")
@@ -1,9 +1,11 @@
1
1
  # Generated by bitcall-gateway init
2
2
  GATEWAY_VERSION=__GATEWAY_VERSION__
3
3
  BITCALL_GATEWAY_IMAGE=__BITCALL_GATEWAY_IMAGE__
4
+ BITCALL_ENV=__BITCALL_ENV__
4
5
  DOMAIN=__DOMAIN__
5
6
  PUBLIC_IP=__PUBLIC_IP__
6
7
  DEPLOY_MODE=__DEPLOY_MODE__
8
+ ROUTING_MODE=__ROUTING_MODE__
7
9
  SIP_PROVIDER_URI=__SIP_PROVIDER_URI__
8
10
  SIP_UPSTREAM_TRANSPORT=__SIP_UPSTREAM_TRANSPORT__
9
11
  SIP_UPSTREAM_PORT=__SIP_UPSTREAM_PORT__
@@ -23,5 +25,8 @@ ACME_LISTEN_PORT=__ACME_LISTEN_PORT__
23
25
  WSS_LISTEN_PORT=__WSS_LISTEN_PORT__
24
26
  INTERNAL_WSS_PORT=__INTERNAL_WSS_PORT__
25
27
  INTERNAL_WS_PORT=__INTERNAL_WS_PORT__
26
- MEDIA_IPV6=__MEDIA_IPV6__
27
- MEDIA_FORCE_IPV4=__MEDIA_FORCE_IPV4__
28
+ MEDIA_IPV4_ONLY=__MEDIA_IPV4_ONLY__
29
+ TURN_UDP_PORT=__TURN_UDP_PORT__
30
+ TURNS_TCP_PORT=__TURNS_TCP_PORT__
31
+ TURN_RELAY_MIN_PORT=__TURN_RELAY_MIN_PORT__
32
+ TURN_RELAY_MAX_PORT=__TURN_RELAY_MAX_PORT__
@@ -10,9 +10,6 @@ services:
10
10
  security_opt:
11
11
  - no-new-privileges:true
12
12
  env_file: .env
13
- environment:
14
- - MEDIA_IPV6=${MEDIA_IPV6:-0}
15
- - MEDIA_FORCE_IPV4=${MEDIA_FORCE_IPV4:-1}
16
13
  volumes:
17
14
  - ${TLS_CERT}:/etc/ssl/cert.pem:ro
18
15
  - ${TLS_KEY}:/etc/ssl/key.pem:ro