Skip to content

fix(sfu): only advertise the primary public IP in ICE candidates#19

Merged
ralyodio merged 2 commits into
masterfrom
fix/sfu-single-ip-ice-candidates
Jun 14, 2026
Merged

fix(sfu): only advertise the primary public IP in ICE candidates#19
ralyodio merged 2 commits into
masterfrom
fix/sfu-single-ip-ice-candidates

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

Summary

Fixes the recurring "Streaming error: could not establish pc connection" that hits desktop clients a minute or two into a stream (while YouTube/Twitch egress keeps working).

Root cause (from prod logs on sfu.pairux.com)

LiveKit advertises host candidates for every local IP mapping on the droplet: the real public IP (146.190.163.128), the DO reserved IP (134.199.140.185, NAT'd through the anchor IP 10.48.x — not bound to any interface), the VPC IP, and the docker bridge. Prod logs showed ICE flapping between the real-IP and reserved-IP candidate pairs (10 ice reconnected or switched pair events in 30 minutes), followed by publisher dtls timeout errors when the reserved-IP NAT dropped the UDP flow mid-session. The client then surfaces could not establish pc connection on its rejoin.

Nothing resolves to the reserved IP (sfu.pairux.com → 146.190.163.128), so those candidates are pure liability.

Fix

Pin candidate gathering to the primary IP via rtc.ips.includes: [${EXTERNAL_IP}] in the generated livekit.yaml. The setup script is idempotent — re-run it on the droplet (or apply the same block by hand + docker compose restart livekit) to take effect.

Test plan

  • bash -n syntax check; pre-commit lint/build green
  • Apply on sfu.pairux.com, stream >10 min, confirm no ice reconnected or switched pair flapping to 134.199.140.185 and no client pc-connection errors

🤖 Generated with Claude Code

LiveKit was offering host candidates for every local mapping: the real
public IP, the DO reserved IP (NAT'd via the anchor IP 10.48.x), the VPC
IP, and the docker bridge. ICE flapped between the real-IP and
reserved-IP pairs (10 switches in 30 min in prod logs), and the reserved
IP's NAT drops UDP mid-session — publishers died with dtls timeouts and
clients surfaced "could not establish pc connection" a minute or two
into streaming. Pin candidate gathering to ${EXTERNAL_IP} via
rtc.ips.includes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

vu1nz Security Review

0 finding(s) in PR #?

No security issues found.

livekit-server rejects bare IPs in rtc.ips.includes with 'invalid CIDR
address' and crash-loops. Verified on sfu.pairux.com: with /32 the server
starts and advertises only the primary IP.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@ralyodio ralyodio merged commit ae771b1 into master Jun 14, 2026
12 checks passed
@ralyodio ralyodio deleted the fix/sfu-single-ip-ice-candidates branch June 14, 2026 05:41
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.

1 participant