From 17ba25f9cbb5501f079745020615c4243aba9fde Mon Sep 17 00:00:00 2001 From: Russell Ballestrini Date: Fri, 29 May 2026 17:43:33 -0400 Subject: [PATCH] =?UTF-8?q?zebra-audio:=20stop=20displaying=20peer=20IP/po?= =?UTF-8?q?rt=20in=20path=20status=20=E2=80=94=20no=20dox?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit reportPath used to print 'you A:B ↔ peer C:D' in path-status and the log, exposing both participants' real IP addresses to anyone glancing at the screen. Show DIRECT vs RELAYED and the candidate types (host/srflx/relay) only; addresses are gone. CLAUDE.md gains a 'Web UI privacy — never display peer IPs' rule so this does not regress. --- CLAUDE.md | 10 ++++++++++ web/zebra-audio.html | 19 +++++++++++-------- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index cf4997e..556f08e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -120,6 +120,16 @@ Handshake frame: `[0x5A 0x42 0x01 baud_lo baud_hi xor_cksum]` — 6 bytes at 50 - userland only — survives without root - **Operation Voyeur**: all terminal output is public — never pass secrets through these channels unencrypted. The web page does ECDH key exchange + AES-256-GCM before TX. +### Web UI privacy — never display peer IPs + +`chat.html` and `zebra-audio.html` must never print peer IP addresses or ports +in the page UI or in any visible log. Our users do not run Wireshark — if it +is not on the screen, peers cannot dox each other. Candidate types +(`host`/`srflx`/`relay`) from `pc.getStats()` are abstract and fine to show +(they tell you direct vs relayed); `loc.address` / `loc.port` / +`rem.address` / `rem.port` are not. The WebRTC stack already obfuscates host +candidates via mDNS by default — do not undo that work in the UI. + ### Web page integrity stamping Each deployed page (`web/chat.html`, `web/zebra-audio.html`, `web/how-it-works.html`, diff --git a/web/zebra-audio.html b/web/zebra-audio.html index 7d9518a..a1ea82b 100644 --- a/web/zebra-audio.html +++ b/web/zebra-audio.html @@ -317,13 +317,16 @@ async function reportPath(){ if (!sel) stats.forEach(r=>{ if(r.type==='candidate-pair'&&r.state==='succeeded'&&(r.nominated||r.selected)) sel=r; }); if (!sel){ setPath('path: connecting…', null); return; } const loc=stats.get(sel.localCandidateId)||{}, rem=stats.get(sel.remoteCandidateId)||{}; - const la=(loc.address||loc.ip||'?')+':'+(loc.port||'?'), ra=(rem.address||rem.ip||'?')+':'+(rem.port||'?'); const relayed = loc.candidateType==='relay'||rem.candidateType==='relay'; - /* show the real endpoints so it's verifiable: host = the two devices' IPs; - * relay = the TURN server's IP (142.93.73.64) appears on at least one side. */ - setPath('path: '+(relayed?'RELAYED via TURN':'DIRECT P2P')+' — you '+la - +' ↔ peer '+ra+' ('+(loc.candidateType||'?')+'/'+(rem.candidateType||'?')+')', relayed?'err':'ok'); - logLine('', 'active pair: '+la+' ['+(loc.candidateType||'?')+'] ↔ '+ra+' ['+(rem.candidateType||'?')+']'); + /* deliberately do NOT surface loc.address/loc.port/rem.address/rem.port — + * printing them would dox the peer to anyone who glances at the screen. + * candidate types (host/srflx/relay) are abstract enough to show: they + * tell you whether you're direct or relayed without leaking an address. */ + setPath('path: '+(relayed + ? 'RELAYED via TURN — a server forwards your (encrypted) audio' + : 'DIRECT peer-to-peer — nobody between you') + +' ('+(loc.candidateType||'?')+'/'+(rem.candidateType||'?')+')', relayed?'err':'ok'); + logLine('', 'transport: '+(relayed?'relayed via TURN':'direct peer-to-peer')+' ('+(loc.candidateType||'?')+'/'+(rem.candidateType||'?')+')'); } catch(_){} } @@ -641,8 +644,8 @@ else wirePuppet();