on
Hardening a browser-VPN architecture requires intercepting WebRTC candidate generation, filtering non-compliant addresses, and validating the resulting topology against expected routing policies. The implementation below demonstrates a TypeScript-based detection and mitigation module that operates within the browser runtime.
Architecture Decisions and Rationale
- Direct
RTCPeerConnection Usage: We bypass third-party libraries to interact directly with the WebRTC API. This eliminates extension overhead and ensures compatibility across Chromium, Gecko, and WebKit engines.
- Candidate Parsing via SDP: ICE candidates are transmitted as SDP strings. We parse these strings to extract IP addresses, port numbers, and candidate types (host, srflx, relay).
- RFC Compliance Filtering: Private ranges (RFC 1918 for IPv4, RFC 4193 for IPv6) are explicitly filtered. Only public or VPN-routed addresses are permitted to surface.
- STUN Server Isolation: We route STUN queries through a controlled endpoint to prevent third-party servers from logging raw candidate data.
- Validation Layer: A post-capture validation step cross-references discovered addresses against known VPN exit nodes and local interface lists, flagging mismatches before they reach the application logic.
Implementation: WebRTC Candidate Inspector & Validator
interface ICECandidateData {
address: string;
type: 'host' | 'srflx' | 'relay' | 'prflx';
protocol: 'udp' | 'tcp';
port: number;
foundation: string;
}
interface NetworkValidationResult {
safe: boolean;
leakedCandidates: ICECandidateData[];
vpnExitMatch: boolean;
timestamp: number;
}
class WebRTCLeakInspector {
private readonly PRIVATE_IPV4_REGEX = /^(10\.|172\.(1[6-9]|2\d|3[01])\.|192\.168\.)/;
private readonly PRIVATE_IPV6_REGEX = /^([fF][cCdD])/;
private readonly STUN_ENDPOINT = 'stun:stun.l.google.com:19302';
public async auditConnection(expectedExitIP?: string): Promise<NetworkValidationResult> {
const candidates: ICECandidateData[] = [];
const peerConnection = new RTCPeerConnection({
iceServers: [{ urls: this.STUN_ENDPOINT }]
});
return new Promise((resolve) => {
peerConnection.onicecandidate = (event) => {
if (!event.candidate) {
peerConnection.close();
const validation = this.validateCandidates(candidates, expectedExitIP);
resolve(validation);
return;
}
const parsed = this.parseCandidate(event.candidate.candidate);
if (parsed) {
candidates.push(parsed);
}
};
// Create a dummy data channel to trigger ICE gathering
peerConnection.createDataChannel('audit-channel');
peerConnection.createOffer().then(offer => peerConnection.setLocalDescription(offer));
});
}
private parseCandidate(rawCandidate: string): ICECandidateData | null {
const parts = rawCandidate.split(' ');
if (parts.length < 8) return null;
const address = parts[4];
const type = parts[7] as ICECandidateData['type'];
const protocol = parts[2] as ICECandidateData['protocol'];
const port = parseInt(parts[5], 10);
const foundation = parts[0];
return { address, type, protocol, port, foundation };
}
private validateCandidates(
candidates: ICECandidateData[],
expectedExitIP?: string
): NetworkValidationResult {
const leakedCandidates = candidates.filter(c =>
this.PRIVATE_IPV4_REGEX.test(c.address) ||
this.PRIVATE_IPV6_REGEX.test(c.address) ||
c.type === 'host'
);
const vpnExitMatch = expectedExitIP
? candidates.some(c => c.address === expectedExitIP && c.type === 'srflx')
: true;
return {
safe: leakedCandidates.length === 0,
leakedCandidates,
vpnExitMatch,
timestamp: Date.now()
};
}
}
// Usage example in a security audit pipeline
async function runNetworkAudit() {
const inspector = new WebRTCLeakInspector();
const result = await inspector.auditConnection('203.0.113.45'); // Expected VPN exit
if (!result.safe) {
console.warn('[AUDIT] WebRTC leak detected:', result.leakedCandidates);
// Trigger mitigation: disable WebRTC, enforce policy, or alert user
} else {
console.log('[AUDIT] Network topology consistent with VPN routing.');
}
}
Why This Architecture Works
The dummy data channel forces the browser to initiate ICE gathering without establishing an actual media stream. This keeps the audit lightweight and non-disruptive. Parsing the raw candidate string avoids reliance on deprecated or browser-specific properties. The regex filters target RFC-defined private ranges, ensuring that only routable addresses are evaluated. By decoupling candidate extraction from validation, the module remains extensible for enterprise policy engines that need to enforce strict routing compliance.
Pitfall Guide
1. The Single-Metric Validation Trap
Explanation: Engineers verify VPN functionality by checking only the HTTP source IP. This ignores browser-level telemetry that leaks before the request leaves the client.
Fix: Implement multi-layer validation. Audit WebRTC candidates, DNS resolver geography, and ASN reputation alongside the public IP. Use automated audit scripts in CI/CD pipelines to catch regressions.
2. IPv6 Routing Bypass
Explanation: Many VPN clients only route IPv4 traffic through the tunnel. If the OS has IPv6 enabled, WebRTC and native fetch requests may bypass the tunnel entirely, exposing the real ISP gateway.
Fix: Force IPv6 kill-switch behavior at the OS or VPN client level. Disable IPv6 in network interfaces if the provider does not support dual-stack tunneling. Validate with ping6 and WebRTC IPv6 candidate checks.
3. STUN Server Trust Assumption
Explanation: Default browser configurations query public STUN servers that log candidate data. Third-party STUN endpoints can be used to fingerprint users or correlate sessions across different browsing contexts.
Fix: Override iceServers in RTCPeerConnection configurations to point to internal or privacy-respecting STUN relays. Audit STUN traffic via browser devtools or network proxies to ensure no external leakage.
4. DNS Resolver Geography Mismatch
Explanation: The VPN changes the public IP, but DNS queries may still resolve through the ISP's local recursive resolvers. This creates a geographic and ASN mismatch that anti-fraud systems flag immediately.
Fix: Enforce DNS-over-HTTPS (DoH) or DNS-over-TLS (DoT) routing through the VPN tunnel. Configure the OS to use the VPN provider's private DNS resolvers. Validate with dig or nslookup against known geo-located resolvers.
5. Post-Configuration Drift
Explanation: Browser updates, OS network changes, or VPN client patches can silently revert privacy settings. WebRTC policies, IPv6 states, and DNS routing often reset after system updates.
Fix: Implement runtime health checks that re-audit network topology on page load or connection state changes. Use service workers or background scripts to monitor routing consistency and trigger alerts on drift.
6. Over-Restricting WebRTC in Production Apps
Explanation: Disabling WebRTC globally breaks legitimate P2P features like video conferencing, file sharing, and real-time collaboration. A blanket block is not a sustainable production strategy.
Fix: Apply granular WebRTC policies. Allow WebRTC only on trusted domains, enforce relay-only candidates (turn: instead of host:/srflx:), and use Content Security Policy (CSP) headers to restrict WebRTC initialization to specific origins.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Development/Testing | Full WebRTC audit + IPv6 disable | Rapid validation of routing consistency without production constraints | Low (developer time) |
| Production P2P Application | Relay-only candidates + internal STUN | Preserves functionality while preventing local IP exposure | Medium (TURN server infrastructure) |
| High-Security Remote Access | OS-level kill switch + forced DoH + browser policy | Eliminates all routing bypass vectors and DNS mismatches | High (enterprise VPN + policy management) |
| Geo-Restricted Content Access | Standard VPN + WebRTC restriction extension | Balances accessibility with anti-detection requirements | Low-Medium (extension licensing) |
Configuration Template
{
"webrtc_policy": {
"ice_candidate_policy": "relay_only",
"stun_servers": ["stun:internal-relay.example.com:3478"],
"allowed_origins": ["https://app.example.com", "https://collab.example.com"]
},
"network_routing": {
"ipv6_enabled": false,
"dns_over_https": true,
"dns_resolvers": ["https://dns.vpnprovider.com/dns-query"],
"kill_switch": true,
"expected_exit_asn": "AS64496"
},
"browser_hardening": {
"timezone_override": "auto",
"language_locale": "en-US",
"fingerprint_randomization": false,
"webRTC_api_exposure": "restricted"
}
}
Quick Start Guide
- Initialize the Audit Module: Import the
WebRTCLeakInspector class into your security validation script or browser extension background page.
- Execute Candidate Capture: Call
auditConnection(expectedExitIP) with your known VPN exit address. The module will trigger ICE gathering, parse candidates, and return a validation result.
- Apply Routing Fixes: If
leakedCandidates contains host or private addresses, disable IPv6 in your OS network settings and enforce relay_only WebRTC policies via browser flags or CSP headers.
- Validate DNS Consistency: Run a DNS resolution test against a geo-located domain. Confirm the resolver IP matches your VPN provider's private DNS range, not your ISP's local gateway.
- Deploy Runtime Monitoring: Integrate the audit module into your application's connection lifecycle. Trigger re-audits on
online/offline events and VPN state changes to catch configuration drift before it impacts users.