fix: make WebRTC privacy mode explicit

This commit is contained in:
lockbitchat
2026-05-17 17:57:11 -04:00
parent ce48e8a851
commit f71ff62417
2 changed files with 72 additions and 19 deletions
+37 -13
View File
@@ -102,6 +102,13 @@ class EnhancedSecureWebRTCManager {
static PROTOCOL_VERSION = '4.1'; static PROTOCOL_VERSION = '4.1';
static MAX_SAS_ATTEMPTS = 3; 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 flag instead of this._debugMode
static DEBUG_MODE = true; // Set to true during development, false in production static DEBUG_MODE = true; // Set to true during development, false in production
@@ -146,14 +153,14 @@ class EnhancedSecureWebRTCManager {
useRandomHeaders: config.antiFingerprinting?.useRandomHeaders ?? false useRandomHeaders: config.antiFingerprinting?.useRandomHeaders ?? false
}, },
webrtc: { webrtc: {
relayOnly: config.webrtc?.relayOnly ?? false, // `privacyMode` is the explicit operator-facing setting.
iceServers: config.webrtc?.iceServers ?? [ // Keep `relayOnly` as a backward-compatible alias.
{ urls: 'stun:stun.l.google.com:19302' }, privacyMode: config.webrtc?.privacyMode
{ urls: 'stun:stun1.l.google.com:19302' }, ?? (config.webrtc?.relayOnly ? 'relay-only' : 'standard'),
{ urls: 'stun:stun2.l.google.com:19302' }, relayOnly: config.webrtc?.relayOnly
{ urls: 'stun:stun3.l.google.com:19302' }, ?? config.webrtc?.privacyMode === 'relay-only',
{ urls: 'stun:stun4.l.google.com:19302' } iceServers: config.webrtc?.iceServers
] ?? EnhancedSecureWebRTCManager.DEFAULT_ICE_SERVERS.map(server => ({ ...server }))
} }
}; };
this._ipLeakWarningShown = false; this._ipLeakWarningShown = false;
@@ -7193,23 +7200,40 @@ async processMessage(data) {
} }
_buildPeerConnectionConfig() { _buildPeerConnectionConfig() {
const relayOnly = this._isRelayOnlyMode();
const config = { const config = {
iceServers: this._config.webrtc.iceServers, iceServers: this._config.webrtc.iceServers,
iceCandidatePoolSize: 10, iceCandidatePoolSize: 10,
bundlePolicy: 'balanced' bundlePolicy: 'balanced'
}; };
if (this._config.webrtc.relayOnly) { if (relayOnly) {
config.iceTransportPolicy = 'relay'; config.iceTransportPolicy = 'relay';
} }
return config; return config;
} }
_isRelayOnlyMode() {
return this._config.webrtc.privacyMode === 'relay-only'
|| this._config.webrtc.relayOnly === true;
}
_warnIfTurnMissing() { _warnIfTurnMissing() {
if (this._hasTurnServer() || this._ipLeakWarningShown) return; if (this._ipLeakWarningShown) return;
this._ipLeakWarningShown = true; 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.' const relayOnly = this._isRelayOnlyMode();
: 'Privacy warning: no TURN server is configured. Direct WebRTC connections may expose IP addresses; STUN alone does not provide IP protection.'; 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'); this.deliverMessageToUI(message, 'system');
} }
+35 -6
View File
@@ -14,6 +14,7 @@ function fake(config = {}) {
return { return {
_config: { _config: {
webrtc: { webrtc: {
privacyMode: config.privacyMode ?? (config.relayOnly ? 'relay-only' : 'standard'),
relayOnly: config.relayOnly ?? false, relayOnly: config.relayOnly ?? false,
iceServers: config.iceServers ?? [{ urls: 'stun:stun.example.test:3478' }] iceServers: config.iceServers ?? [{ urls: 'stun:stun.example.test:3478' }]
} }
@@ -23,11 +24,12 @@ function fake(config = {}) {
deliverMessageToUI(message, type) { deliverMessageToUI(message, type) {
this.delivered.push({ 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 manager = fake();
const config = EnhancedSecureWebRTCManager.prototype._buildPeerConnectionConfig.call(manager); 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'); 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 manager = fake({ relayOnly: true, iceServers: [{ urls: 'turn:turn.example.test:3478' }] });
const config = EnhancedSecureWebRTCManager.prototype._buildPeerConnectionConfig.call(manager); const config = EnhancedSecureWebRTCManager.prototype._buildPeerConnectionConfig.call(manager);
assert.equal(config.iceTransportPolicy, 'relay'); assert.equal(config.iceTransportPolicy, 'relay');
} }
// Missing TURN warns clearly. // Missing TURN in standard mode warns clearly and visibly.
{ {
const manager = fake(); const manager = fake();
EnhancedSecureWebRTCManager.prototype._warnIfTurnMissing.call(manager); 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. // 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); assert.equal(EnhancedSecureWebRTCManager.prototype._hasTurnServer.call(manager), false);
EnhancedSecureWebRTCManager.prototype._warnIfTurnMissing.call(manager); EnhancedSecureWebRTCManager.prototype._warnIfTurnMissing.call(manager);
assert.match(manager.delivered[0].message, /STUN alone does not hide IP addresses/i); 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'); console.log('WebRTC privacy mode tests passed');