Files
archived-vdo.ninja/stun.html
steveseguin 45f80890fa .
2025-08-26 00:49:40 -04:00

540 lines
22 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ICE Candidates Viewer</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
background: #f5f5f5;
}
.container {
background: white;
border-radius: 8px;
padding: 30px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
h1 {
color: #333;
margin-bottom: 30px;
}
.controls {
margin-bottom: 20px;
display: flex;
gap: 10px;
flex-wrap: wrap;
}
button {
background: #4CAF50;
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
transition: background 0.3s;
}
button:hover {
background: #45a049;
}
button:disabled {
background: #ccc;
cursor: not-allowed;
}
.status {
margin: 20px 0;
padding: 10px;
border-radius: 4px;
font-size: 14px;
}
.status.info {
background: #e3f2fd;
color: #1976d2;
}
.status.error {
background: #ffebee;
color: #c62828;
}
.status.success {
background: #e8f5e9;
color: #2e7d32;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
th, td {
text-align: left;
padding: 12px;
border-bottom: 1px solid #e0e0e0;
}
th {
background: #f8f9fa;
font-weight: 600;
color: #333;
}
tr:hover {
background: #f5f5f5;
}
.type-badge {
display: inline-block;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
}
.type-host { background: #e8f5e9; color: #2e7d32; }
.type-srflx { background: #e3f2fd; color: #1976d2; }
.type-relay { background: #fff3e0; color: #f57c00; }
.type-prflx { background: #f3e5f5; color: #7b1fa2; }
.server-select {
margin-bottom: 20px;
}
select {
padding: 8px 12px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 16px;
}
</style>
</head>
<body>
<div class="container">
<h1>WebRTC ICE Candidates Viewer</h1>
<div class="server-select">
<label for="stunServer">STUN Server: </label>
<select id="stunServer">
<option value="stun:stun.l.google.com:19302">Google STUN (stun.l.google.com:19302)</option>
<option value="stun:stun1.l.google.com:19302">Google STUN 1 (stun1.l.google.com:19302)</option>
<option value="stun:stun2.l.google.com:19302">Google STUN 2 (stun2.l.google.com:19302)</option>
<option value="stun:stun3.l.google.com:19302">Google STUN 3 (stun3.l.google.com:19302)</option>
<option value="stun:stun4.l.google.com:19302">Google STUN 4 (stun4.l.google.com:19302)</option>
<option value="stun:stun.cloudflare.com:3478">Cloudflare STUN (stun.cloudflare.com:3478)</option>
</select>
</div>
<div class="controls">
<button id="gatherBtn">Gather ICE Candidates</button>
<button id="clearBtn">Clear Results</button>
<button id="checkBtn">Check Browser Settings</button>
</div>
<div id="status" class="status" style="display: none;"></div>
<div id="checkResults" class="check-results" style="display: none;"></div>
<div id="results" style="display: none;">
<h2>ICE Candidates</h2>
<table id="candidatesTable">
<thead>
<tr>
<th>Type</th>
<th>Protocol</th>
<th>IP Address</th>
<th>Port</th>
<th>Priority</th>
<th>Foundation</th>
</tr>
</thead>
<tbody id="candidatesBody"></tbody>
</table>
</div>
</div>
<script>
let pc = null;
const candidates = [];
const gatherBtn = document.getElementById('gatherBtn');
const clearBtn = document.getElementById('clearBtn');
const checkBtn = document.getElementById('checkBtn');
const statusDiv = document.getElementById('status');
const resultsDiv = document.getElementById('results');
const candidatesBody = document.getElementById('candidatesBody');
const stunServerSelect = document.getElementById('stunServer');
const checkResultsDiv = document.getElementById('checkResults');
// OBS Mode - hide unnecessary UI and auto-run
if (window.obsstudio !== undefined) {
document.querySelector('h1').style.display = 'none';
document.querySelector('.server-select').style.display = 'none';
document.querySelector('.controls').style.display = 'none';
document.querySelector('.container').style.padding = '10px';
document.body.style.padding = '10px';
// Compact styles for OBS
const style = document.createElement('style');
style.textContent = `
.check-item { margin: 5px 0; padding: 8px; }
.check-description { font-size: 13px; }
.settings-list li { font-size: 13px; margin: 3px 0; }
table { font-size: 13px; }
th, td { padding: 6px; }
h2 { margin: 10px 0 10px 0; font-size: 18px; }
.status { margin: 10px 0; padding: 8px; }
`;
document.head.appendChild(style);
}
function showStatus(message, type = 'info') {
statusDiv.textContent = message;
statusDiv.className = `status ${type}`;
statusDiv.style.display = 'block';
}
function parseCandidate(candidateString) {
const parts = candidateString.split(' ');
return {
foundation: parts[0],
component: parts[1],
protocol: parts[2],
priority: parts[3],
ip: parts[4],
port: parts[5],
type: parts[7]
};
}
function addCandidateToTable(candidate) {
const parsed = parseCandidate(candidate.candidate);
const row = document.createElement('tr');
row.innerHTML = `
<td><span class="type-badge type-${parsed.type}">${parsed.type.toUpperCase()}</span></td>
<td>${parsed.protocol.toUpperCase()}</td>
<td>${parsed.ip}</td>
<td>${parsed.port}</td>
<td>${parsed.priority}</td>
<td>${parsed.foundation}</td>
`;
candidatesBody.appendChild(row);
}
async function detectWebRTCRestrictions() {
const restrictions = [];
// Test for mDNS candidates (privacy mode)
const testConfig = {
iceServers: [{urls: 'stun:stun.l.google.com:19302'}]
};
try {
const testPc = new RTCPeerConnection(testConfig);
const testDc = testPc.createDataChannel('test');
let foundHost = false;
let foundSrflx = false;
let foundMdns = false;
let candidateCount = 0;
return new Promise((resolve) => {
const timeout = setTimeout(() => {
testPc.close();
if (candidateCount === 0) {
restrictions.push('WebRTC might be completely disabled');
}
if (!foundHost && !foundMdns) {
restrictions.push('Local IP addresses are hidden (privacy mode enabled)');
}
if (foundMdns) {
restrictions.push('mDNS candidates detected (local IPs anonymized)');
}
if (!foundSrflx) {
restrictions.push('STUN server connection might be blocked');
}
resolve(restrictions);
}, 3000);
testPc.onicecandidate = (event) => {
if (event.candidate) {
candidateCount++;
const parsed = parseCandidate(event.candidate.candidate);
if (parsed.type === 'host') {
if (parsed.ip.endsWith('.local')) {
foundMdns = true;
} else {
foundHost = true;
}
} else if (parsed.type === 'srflx') {
foundSrflx = true;
}
}
};
testPc.createOffer().then(offer => {
return testPc.setLocalDescription(offer);
}).catch(err => {
clearTimeout(timeout);
testPc.close();
restrictions.push('WebRTC API error: ' + err.message);
resolve(restrictions);
});
});
} catch (error) {
restrictions.push('WebRTC not available: ' + error.message);
return restrictions;
}
}
async function gatherCandidates() {
if (pc) {
pc.close();
pc = null;
}
candidates.length = 0;
candidatesBody.innerHTML = '';
gatherBtn.disabled = true;
showStatus('Detecting WebRTC restrictions...', 'info');
// Check for restrictions first
const restrictions = await detectWebRTCRestrictions();
if (restrictions.length > 0) {
showStatus('Detected restrictions: ' + restrictions.join('; '), 'error');
await new Promise(resolve => setTimeout(resolve, 3000));
}
showStatus('Gathering ICE candidates...', 'info');
const stunServer = stunServerSelect.value;
const config = {
iceServers: [{ urls: stunServer }],
iceCandidatePoolSize: 10
};
try {
pc = new RTCPeerConnection(config);
// Create a data channel to trigger ICE gathering
const dc = pc.createDataChannel('test');
pc.onicecandidate = (event) => {
if (event.candidate) {
candidates.push(event.candidate);
addCandidateToTable(event.candidate);
resultsDiv.style.display = 'block';
// Count candidate types
const types = candidates.reduce((acc, c) => {
const type = parseCandidate(c.candidate).type;
acc[type] = (acc[type] || 0) + 1;
return acc;
}, {});
const typeStr = Object.entries(types).map(([k,v]) => `${k}: ${v}`).join(', ');
showStatus(`Gathering candidates... Found ${candidates.length} (${typeStr})`, 'info');
} else {
showStatus(`Gathering complete. Found ${candidates.length} candidates.`, 'success');
gatherBtn.disabled = false;
}
};
pc.onicegatheringstatechange = () => {
console.log('ICE gathering state:', pc.iceGatheringState);
if (pc.iceGatheringState === 'complete') {
showStatus(`Gathering complete. Found ${candidates.length} candidates.`, 'success');
gatherBtn.disabled = false;
}
};
pc.oniceconnectionstatechange = () => {
console.log('ICE connection state:', pc.iceConnectionState);
};
// Create offer with ICE restart
const offer = await pc.createOffer({
offerToReceiveAudio: true,
offerToReceiveVideo: true,
iceRestart: true
});
await pc.setLocalDescription(offer);
// Set a timeout in case gathering takes too long
setTimeout(() => {
if (gatherBtn.disabled) {
showStatus(`Gathering timed out. Found ${candidates.length} candidates.`, 'success');
gatherBtn.disabled = false;
}
}, 10000);
} catch (error) {
showStatus(`Error: ${error.message}`, 'error');
gatherBtn.disabled = false;
console.error('WebRTC error:', error);
}
}
function clearResults() {
if (pc) {
pc.close();
pc = null;
}
candidates.length = 0;
candidatesBody.innerHTML = '';
resultsDiv.style.display = 'none';
statusDiv.style.display = 'none';
checkResultsDiv.style.display = 'none';
}
async function checkBrowserSettings() {
clearResults();
checkResultsDiv.innerHTML = '';
checkResultsDiv.style.display = 'block';
statusDiv.style.display = 'none';
// Check basic WebRTC support
if (typeof RTCPeerConnection === 'undefined') {
addCheckItem('error', 'WebRTC Not Supported',
'WebRTC is not available in this browser. This could mean you\'re using an older browser or WebRTC has been disabled.');
return;
}
addCheckItem('success', 'WebRTC API Available',
'The browser supports WebRTC and the API is accessible.');
// Test restriction detection
showStatus('Testing WebRTC restrictions...', 'info');
const restrictions = await detectWebRTCRestrictions();
statusDiv.style.display = 'none';
// Analyze restrictions
if (restrictions.length === 0) {
addCheckItem('success', 'No WebRTC Restrictions',
'Your browser is configured to allow full WebRTC functionality. You should see all types of ICE candidates including local IP addresses.');
}
if (restrictions.includes('mDNS candidates detected (local IPs anonymized)')) {
addCheckItem('warning', 'mDNS Anonymization Active',
'Your browser is hiding local IP addresses by converting them to .local addresses (like abc123.local). ' +
'This is a privacy feature that prevents websites from seeing your real local network addresses. ' +
'Expected: 192.168.1.100, but seeing: abc123def.local',
[
'In Chrome: Go to chrome://flags/#enable-webrtc-hide-local-ips-with-mdns and set to "Disabled"',
'This is often enabled by default in newer Chrome versions for privacy'
]);
}
if (restrictions.includes('Local IP addresses are hidden (privacy mode enabled)')) {
addCheckItem('warning', 'Local IPs Completely Hidden',
'Your browser is not exposing ANY local IP addresses. This is a strict privacy setting that prevents WebRTC from revealing network information.',
[
'Check browser privacy settings',
'Disable VPN browser extensions',
'Check privacy-focused extensions like uBlock Origin settings'
]);
}
if (restrictions.includes('STUN server connection might be blocked')) {
addCheckItem('error', 'STUN Server Blocked',
'Cannot connect to STUN servers to determine your public IP address. This means only local candidates will be available.',
[
'Check firewall settings',
'Verify network allows UDP traffic on port 3478/19302',
'Try a different network connection'
]);
}
// Environment checks
if (window.self !== window.top) {
addCheckItem('info', 'Running in iFrame',
'This page is running inside an iframe (like Claude.ai\'s artifact viewer). ' +
'iFrames often have additional security restrictions that limit WebRTC functionality.');
}
if (location.protocol === 'https:') {
addCheckItem('success', 'Secure Context (HTTPS)',
'Running on HTTPS. WebRTC has full functionality in secure contexts.');
} else if (location.protocol === 'http:') {
addCheckItem('warning', 'Insecure Context (HTTP)',
'Running on HTTP. Some browsers restrict WebRTC features on non-secure origins.',
['Consider moving to HTTPS for full WebRTC support']);
}
// Browser-specific guidance
const ua = navigator.userAgent;
if (ua.includes('Chrome')) {
addCheckItem('info', 'Chrome Browser Detected',
'Chrome has several flags and settings that control WebRTC behavior:',
[
'<b>chrome://flags/#enable-webrtc-hide-local-ips-with-mdns</b> - Controls mDNS anonymization',
'<b>chrome://settings/content/location</b> - Location permissions can affect WebRTC',
'<b>chrome://settings/privacy</b> - Check "WebRTC IP handling policy"',
'Extensions like WebRTC Leak Prevent, uBlock Origin, or Privacy Badger may block WebRTC'
]);
} else if (ua.includes('Firefox')) {
addCheckItem('info', 'Firefox Browser Detected',
'Firefox has about:config settings that control WebRTC:',
[
'<b>media.peerconnection.enabled</b> - Must be true (enables WebRTC)',
'<b>media.peerconnection.ice.default_address_only</b> - If true, only shows default network interface',
'<b>media.peerconnection.ice.no_host</b> - If true, hides all local IPs',
'<b>media.peerconnection.ice.proxy_only_if_behind_proxy</b> - Forces proxy use'
]);
}
// Check for OBS Studio
if (window.obsstudio !== undefined) {
addCheckItem('warning', 'OBS Browser Source',
'OBS browser sources hide local IPs by default. Restart OBS with:',
[
'<code style="word-break: break-all;">obs64.exe --disable-web-security --allow-running-insecure-content --ignore-certificate-errors --use-fake-ui-for-media-stream</code>'
]);
}
}
function addCheckItem(type, title, description, solutions = []) {
const item = document.createElement('div');
item.className = `check-item ${type}`;
let icon = '';
switch(type) {
case 'success': icon = '✅'; break;
case 'warning': icon = '⚠️'; break;
case 'error': icon = '❌'; break;
case 'info': icon = ''; break;
}
let html = `<div class="check-title">${icon} ${title}</div>`;
html += `<div class="check-description">${description}</div>`;
if (solutions.length > 0) {
html += '<ul class="settings-list">';
solutions.forEach(solution => {
html += `<li>${solution}</li>`;
});
html += '</ul>';
}
item.innerHTML = html;
checkResultsDiv.appendChild(item);
}
gatherBtn.addEventListener('click', gatherCandidates);
clearBtn.addEventListener('click', clearResults);
checkBtn.addEventListener('click', checkBrowserSettings);
// Auto-run if in OBS
if (window.obsstudio !== undefined) {
setTimeout(async () => {
// First check browser settings
await checkBrowserSettings();
// Then gather candidates
setTimeout(() => {
gatherCandidates();
}, 100);
}, 500);
}
</script>
</body>
</html>