fix: make WebRTC privacy mode explicit
This commit is contained in:
@@ -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');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
Reference in New Issue
Block a user