zebrad reads @DEFAULT_MONITOR@ which mixes every PA stream on
a machine, so each browser tab hears its own carrier echoed
back. without a filter, every outbound message would render
twice in the chat log (once as 'me' at send time, once as
'peer' on echo arrival).
filter: compare frame.sid (carried in DATA + HELLO) against
our own senderIdBytes(). if equal, drop frame before any UI
work. loopback mode unaffected since BroadcastChannel never
echoes to sender by spec.
senderIdBytes returns a stable per-tab id:
* passphrase mode: random 4 bytes cached for tab lifetime
* pubkey mode: first 4 of SHA-256(pubkey), stable across reloads
without zebrad running, the page is TX-only: tabs see their own
outbound frames but nothing crosses. for UX validation while the
introspector daemon doesn't exist yet, gate a BroadcastChannel
fallback on the ?loopback=1 query param.
behavior:
* with ?loopback=1: after each txFrame completes its modulation,
the raw frame bytes are postMessage'd on a same-origin
BroadcastChannel('zebra-report-loopback'). other tabs of the
same browser subscribe & feed received bytes into onRxFrame
exactly as zebrad would. BroadcastChannel suppresses echo to
sender by spec, so the originating tab does not see itself.
* without ?loopback=1: behavior unchanged. real PA transport
stays the production path.
a yellow banner at the top of the page warns when loopback is
active: "frames cross via BroadcastChannel between same-browser
tabs, NOT via PulseAudio". no risk of mistaking dev mode for
production.
single self-contained page implementing the recovered zebra-report
protocol as a peer-to-peer chatroom whose data path is PulseAudio
sink-input state — chat content never enters an IP packet.
architecture:
page (TX) modulates audio output via Web Audio GainNode
between MARK (0.80) and SPACE (0.20). silent stereo
carrier 440/441 Hz published to PulseAudio.
zebrad separate program (companion, not in this commit):
polls PA sink-input volumes, decodes frames,
forwards to ws://127.0.0.1:7777 — the page connects
and renders received messages.
RX path the page is TX-only without zebrad running; with
it, full bidirectional chat.
crypto modes (toggle in UI):
passphrase PBKDF2-SHA256 (600k iter) → AES-GCM-256 group key.
everyone with the same phrase joins the room.
pubkey ECDH P-256 keypair per browser → AES-GCM-256
pairwise. peers added by pasting their pubkey.
both modes use Web Crypto API. no external crypto deps.
frame protocol (matches include/zebra.h):
OFFER magic(ZB) + 0x01 + baud_le(2) + xor(1) — 6B
READY magic(ZB) + 0x02 + baud_le(2) + xor(1) — 6B
HELLO magic + 0x04 + sid(4) + maxBaud_le(2)
+ handleLen(1) + handle(n) + crc32_le(4)
DATA magic + 0x03 + sid(4) + len_le(2)
+ payload(n) + crc32_le(4)
handshake:
1. user clicks "benchmark self" — page schedules 200 gain
events at 0.5ms intervals, measures avg, divides by 2 for
safety, clamps to [ZEBRA_BAUD_MIN, ZEBRA_BAUD_MAX].
2. user clicks "send OFFER" — sends OFFER frame at the
fixed handshake baud (50). also broadcasts HELLO.
3. peer's introspector decodes; their page replies with READY
at their max baud, plus their HELLO.
4. negotiated baud = min(my max, every peer's max).
5. data frames at negotiated baud.
UI sections:
identity handle + mode toggle
room passphrase OR pubkey, depending on mode
carrier start audio, live gain meter
handshake benchmark, OFFER button, baud display
receive ws://127.0.0.1:7777 status + connect
chat peer list, log, message box
about threat model, mitigation, whitepaper link
defensive framing prominent: the protocol is documented as a
research artifact demonstrating PA-IPC's open same-UID volume
read access. "what this does not protect against" enumerated.
mitigation panel cites foxhop.net/linux-audio-ipc-attack-surface.
known v1 tradeoffs:
- no CSMA: simultaneous TX from two peers will collide.
- pubkey mode broadcasts one frame per peer per message.
- GainNode scheduling has OS-level jitter — actual sustained
baud will be lower than the optimistic benchmark.
- base carrier is audible at 440/441 Hz; can be lowered or
moved ultrasonic in a follow-up.
reuses visual identity from web/index.html (chunkfive font,
monospace, black/white, no framework, no build step).
file: 1062 lines, 40 KB, JS syntax-clean (node --check), HTML
balanced.