Skip to content

ICE: surface bot's local candidates + offer host-network compose mode - #15

Merged
queueeee merged 1 commit into
mainfrom
claude/review-claude-md-iOuUt
May 1, 2026
Merged

queueeee merged 1 commit into
mainfrom
claude/review-claude-md-iOuUt

Conversation

@queueeee

@queueeee queueeee commented May 1, 2026

Copy link
Copy Markdown
Owner

Diagnose aus dem letzten Live-Lauf

ICE-Race-Fix von PR #14 ist drin und hat gefruchtet — keine "addIceCandidate without remote description" Warnings mehr. Aber ICE schlägt trotzdem fehl, jetzt offen sichtbar:

Connection(0) Check CandidatePair(('172.18.0.5', 49510) ->
                                  ('192.168.178.29', 52891)) FAILED
Connection(0) Check CandidatePair(('172.18.0.5', 49510) ->
                                  ('87.79.86.36',   52891)) FAILED
Connection(0) ICE failed

Der Bot hat nur EINE lokale Candidate-IP: 172.18.0.5. Das ist die Docker-Bridge-IP. Kein srflx-Candidate vom STUN, keine Relay-Candidate vom TURN. Ohne externe Adresse kann der Viewer nicht zurückreichen, NAT-Punch ist mangels öffentlicher Bot-Adresse nicht möglich.

Root cause: Die Docker-Bridge-NAT-Schicht frisst STUN-Responses oder verbiegt sie so, dass aiortc keine srflx baut.

Was dieser PR macht

Zwei Sachen:

  1. stream_publisher.local_ice_candidates Log vor dem Senden der Offer-SDP — listet alle Bot-Candidates die aiortc nach Gathering produziert hat. Zeile zeigt kinds=['host'] versus kinds=['host', 'srflx'] versus mit Relay → in einem Log sehen ob STUN/TURN was beigetragen haben.

  2. docker-compose Profiles — bridge (default, alles wie vorher) vs. host:

services:
  bot:        # profiles: ["", "bridge"]   <-- default
  bot-host:   # profiles: ["host"]         <-- network_mode: host

Beide nutzen das gleiche Image. Der host-Modus eliminiert die Docker-Bridge-NAT — Bot sieht die echte Host-LAN/WAN-IP, STUN funktioniert, Viewer können den Bot direkt erreichen.

Trade-off host vs bridge

bridge (default) host
WebRTC ICE scheitert ohne externe IP funktioniert mit Host-IP
Port 8080 nur 127.0.0.1 (lokaler Reverse-Proxy nötig) bindet Host-Port direkt
TS6-Server über ts6-net Docker-Netzwerk über 127.0.0.1 oder LAN-IP
Isolation gut schwächer (Host-Network sichtbar)

Operator commands

Wechsel auf host-mode:

cd /opt/ts6-stream-bot
git pull
docker compose down
# .env anpassen: TS6_HOST muss vom Host aus erreichbar sein, z.B.
#   TS6_HOST=127.0.0.1
# (statt "teamspeak-server" was nur im Docker-Netzwerk auflösbar war)
docker compose --profile host up -d --build
docker compose logs -f

Wenn der host-Mode aus irgendeinem Grund nicht geht (z.B. Host-Port 8080 belegt, TS6-Server nicht via 127.0.0.1 erreichbar): dann TURN aktivieren statt host-mode. In .env:

TURN_URL=turn:openrelay.metered.ca:80
TURN_USERNAME=openrelayproject
TURN_PASSWORD=openrelayproject

Dann docker compose down && docker compose up -d. Im neuen Lauf sollte local_ice_candidates kinds=['host', 'relay'] erscheinen und das Relay durchpeitschen.

Test plan

  • ruff check src tests — clean
  • mypy --strict — clean
  • pytest — 219 passed
  • Live: redeploy in einem der beiden Modi. Erwarteter Log:
    local_ice_candidates kinds=['host', 'srflx'] (mind. host+srflx).
    Wenn nur kinds=['host'] weiterhin → TURN nötig.

https://claude.ai/code/session_016DuCjRJK995Tj9aDhhB9at


Generated by Claude Code

Live trace shows ICE failing because the bot only ever advertised one
local candidate - its docker bridge IP `172.18.0.5`. No srflx, no
relay. Without an externally-reachable candidate the viewer's TS6
client has nowhere to NAT-punch back to:

    Connection(0) Check CandidatePair(('172.18.0.5', 49510) ->
                                      ('192.168.178.29', 52891)) FAILED
    Connection(0) Check CandidatePair(('172.18.0.5', 49510) ->
                                      ('87.79.86.36',   52891)) FAILED

STUN/TURN responses are getting eaten by the docker bridge NAT.

Two changes:

1. New `stream_publisher.local_ice_candidates` log right before
   sending the offer. Lists what aiortc actually produced after
   gathering completed (kind = host / srflx / relay, ip+port). Makes
   "STUN didn't help" diagnosable in one line instead of inferring it
   from absence-of-evidence in the connection-check trace.

2. docker-compose: split into two profiles.
   - "bridge" (default, profile="" too so existing `docker compose up`
     still works unchanged): original ts6-net bridge networking.
   - "host": same image, but `network_mode: host`. Bot sees the host's
     real LAN/WAN interface, STUN actually returns a usable srflx,
     viewers can reach back. Operator picks which one fits their
     deployment via `docker compose --profile host up -d`.

ruff + mypy strict + 219 tests pass.

https://claude.ai/code/session_016DuCjRJK995Tj9aDhhB9at
@queueeee
queueeee merged commit f95a3e0 into main May 1, 2026
1 check passed
queueeee pushed a commit that referenced this pull request May 1, 2026
PR #15's compose-profiles approach broke `docker compose up -d`: putting
profiles=["", "bridge"] on the default service made the empty-string
profile NOT match no-profile invocations, so the user got no logs after
running their normal commands.

Cleaner pattern: docker-compose.yml stays single-service (default
bridge, identical to pre-#15 behaviour), and docker-compose.host.yml
is an override layered on top via -f when the operator needs host
networking for ICE.

    docker compose up -d                                          # bridge
    docker compose -f docker-compose.yml \
                   -f docker-compose.host.yml up -d                # host

The overlay uses Compose v2's `!reset null` to drop the bridge
attachment, the ts6-net network section, and the port mapping; then
`network_mode: host` puts the bot in the host namespace.

ruff + 219 tests still green.

https://claude.ai/code/session_016DuCjRJK995Tj9aDhhB9at
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants