diff --git a/src/network/EnhancedSecureWebRTCManager.js b/src/network/EnhancedSecureWebRTCManager.js index 89588e7..b7df56e 100644 --- a/src/network/EnhancedSecureWebRTCManager.js +++ b/src/network/EnhancedSecureWebRTCManager.js @@ -102,6 +102,13 @@ class EnhancedSecureWebRTCManager { static PROTOCOL_VERSION = '4.1'; static MAX_SAS_ATTEMPTS = 3; + static DEFAULT_ICE_SERVERS = Object.freeze([ + Object.freeze({ urls: 'stun:stun.l.google.com:19302' }), + Object.freeze({ urls: 'stun:stun1.l.google.com:19302' }), + Object.freeze({ urls: 'stun:stun2.l.google.com:19302' }), + Object.freeze({ urls: 'stun:stun3.l.google.com:19302' }), + Object.freeze({ urls: 'stun:stun4.l.google.com:19302' }) + ]); // Static debug flag instead of this._debugMode static DEBUG_MODE = true; // Set to true during development, false in production @@ -146,14 +153,14 @@ class EnhancedSecureWebRTCManager { useRandomHeaders: config.antiFingerprinting?.useRandomHeaders ?? false }, webrtc: { - relayOnly: config.webrtc?.relayOnly ?? false, - iceServers: config.webrtc?.iceServers ?? [ - { urls: 'stun:stun.l.google.com:19302' }, - { urls: 'stun:stun1.l.google.com:19302' }, - { urls: 'stun:stun2.l.google.com:19302' }, - { urls: 'stun:stun3.l.google.com:19302' }, - { urls: 'stun:stun4.l.google.com:19302' } - ] + // `privacyMode` is the explicit operator-facing setting. + // Keep `relayOnly` as a backward-compatible alias. + privacyMode: config.webrtc?.privacyMode + ?? (config.webrtc?.relayOnly ? 'relay-only' : 'standard'), + relayOnly: config.webrtc?.relayOnly + ?? config.webrtc?.privacyMode === 'relay-only', + iceServers: config.webrtc?.iceServers + ?? EnhancedSecureWebRTCManager.DEFAULT_ICE_SERVERS.map(server => ({ ...server })) } }; this._ipLeakWarningShown = false; @@ -7193,23 +7200,40 @@ async processMessage(data) { } _buildPeerConnectionConfig() { + const relayOnly = this._isRelayOnlyMode(); const config = { iceServers: this._config.webrtc.iceServers, iceCandidatePoolSize: 10, bundlePolicy: 'balanced' }; - if (this._config.webrtc.relayOnly) { + if (relayOnly) { config.iceTransportPolicy = 'relay'; } return config; } + _isRelayOnlyMode() { + return this._config.webrtc.privacyMode === 'relay-only' + || this._config.webrtc.relayOnly === true; + } + _warnIfTurnMissing() { - if (this._hasTurnServer() || this._ipLeakWarningShown) return; + if (this._ipLeakWarningShown) return; this._ipLeakWarningShown = true; - const message = this._config.webrtc.relayOnly - ? 'Privacy mode is enabled, but no TURN server is configured. Relay-only mode cannot connect until TURN is configured; STUN alone does not hide IP addresses.' - : 'Privacy warning: no TURN server is configured. Direct WebRTC connections may expose IP addresses; STUN alone does not provide IP protection.'; + + const relayOnly = this._isRelayOnlyMode(); + const hasTurnServer = this._hasTurnServer(); + let message = null; + + if (relayOnly && !hasTurnServer) { + message = 'Privacy mode is relay-only, but no TURN server is configured. Relay-only mode cannot connect until TURN is configured; STUN alone does not hide IP addresses.'; + } else if (!relayOnly && !hasTurnServer) { + message = 'Privacy warning: relay-only mode is disabled and no TURN server is configured. Direct WebRTC connections may expose host or server-reflexive IP addresses; STUN alone does not provide IP protection.'; + } else if (!relayOnly) { + message = 'Privacy warning: relay-only mode is disabled. Direct WebRTC connectivity may expose host or server-reflexive IP addresses even when TURN is available.'; + } + + if (!message) return; this.deliverMessageToUI(message, 'system'); } diff --git a/tests/webrtc-privacy-mode.test.mjs b/tests/webrtc-privacy-mode.test.mjs index 2147802..96dcd40 100644 --- a/tests/webrtc-privacy-mode.test.mjs +++ b/tests/webrtc-privacy-mode.test.mjs @@ -14,6 +14,7 @@ function fake(config = {}) { return { _config: { webrtc: { + privacyMode: config.privacyMode ?? (config.relayOnly ? 'relay-only' : 'standard'), relayOnly: config.relayOnly ?? false, iceServers: config.iceServers ?? [{ urls: 'stun:stun.example.test:3478' }] } @@ -23,11 +24,12 @@ function fake(config = {}) { deliverMessageToUI(message, type) { this.delivered.push({ message, type }); }, - _hasTurnServer: EnhancedSecureWebRTCManager.prototype._hasTurnServer + _hasTurnServer: EnhancedSecureWebRTCManager.prototype._hasTurnServer, + _isRelayOnlyMode: EnhancedSecureWebRTCManager.prototype._isRelayOnlyMode }; } -// Default mode preserves current behavior. +// Standard mode remains usable, but it is not relay-only. { const manager = fake(); const config = EnhancedSecureWebRTCManager.prototype._buildPeerConnectionConfig.call(manager); @@ -35,26 +37,53 @@ function fake(config = {}) { assert.equal(config.iceServers[0].urls, 'stun:stun.example.test:3478'); } -// Privacy mode uses relay-only transport. +// Explicit privacy mode uses relay-only transport, suppressing host/srflx usage. +{ + const manager = fake({ privacyMode: 'relay-only', iceServers: [{ urls: 'turn:turn.example.test:3478' }] }); + const config = EnhancedSecureWebRTCManager.prototype._buildPeerConnectionConfig.call(manager); + assert.equal(config.iceTransportPolicy, 'relay'); +} + +// Backward-compatible relayOnly alias still enables relay transport. { const manager = fake({ relayOnly: true, iceServers: [{ urls: 'turn:turn.example.test:3478' }] }); const config = EnhancedSecureWebRTCManager.prototype._buildPeerConnectionConfig.call(manager); assert.equal(config.iceTransportPolicy, 'relay'); } -// Missing TURN warns clearly. +// Missing TURN in standard mode warns clearly and visibly. { const manager = fake(); EnhancedSecureWebRTCManager.prototype._warnIfTurnMissing.call(manager); - assert.match(manager.delivered[0].message, /may expose IP addresses/i); + assert.equal(manager.delivered[0].type, 'system'); + assert.match(manager.delivered[0].message, /relay-only mode is disabled/i); + assert.match(manager.delivered[0].message, /may expose host or server-reflexive IP addresses/i); } // STUN-only config does not claim IP protection, even with privacy mode selected. { - const manager = fake({ relayOnly: true, iceServers: [{ urls: 'stun:stun.example.test:3478' }] }); + const manager = fake({ privacyMode: 'relay-only', iceServers: [{ urls: 'stun:stun.example.test:3478' }] }); assert.equal(EnhancedSecureWebRTCManager.prototype._hasTurnServer.call(manager), false); EnhancedSecureWebRTCManager.prototype._warnIfTurnMissing.call(manager); assert.match(manager.delivered[0].message, /STUN alone does not hide IP addresses/i); } +// Non-private mode warns even when TURN exists because direct candidates remain allowed. +{ + const manager = fake({ iceServers: [{ urls: 'turn:turn.example.test:3478' }] }); + EnhancedSecureWebRTCManager.prototype._warnIfTurnMissing.call(manager); + assert.equal(manager.delivered[0].type, 'system'); + assert.match(manager.delivered[0].message, /relay-only mode is disabled/i); + assert.match(manager.delivered[0].message, /may expose host or server-reflexive IP addresses/i); +} + +// ICE defaults are centralized and operator overrides remain untouched. +{ + assert.equal(Array.isArray(EnhancedSecureWebRTCManager.DEFAULT_ICE_SERVERS), true); + const overrideServers = [{ urls: ['stun:operator.example.test:3478', 'turn:operator.example.test:3478'] }]; + const manager = fake({ iceServers: overrideServers }); + const config = EnhancedSecureWebRTCManager.prototype._buildPeerConnectionConfig.call(manager); + assert.equal(config.iceServers, overrideServers); +} + console.log('WebRTC privacy mode tests passed');