Skip to content

feat: replace STUN servers with sovereign executor-based NAT traversal#557

Draft
HexaField wants to merge 2 commits into
devfrom
feat/iroh-ice-v2
Draft

feat: replace STUN servers with sovereign executor-based NAT traversal#557
HexaField wants to merge 2 commits into
devfrom
feat/iroh-ice-v2

Conversation

@HexaField

@HexaField HexaField commented Mar 11, 2026

Copy link
Copy Markdown
Contributor

Summary

Removes all external STUN/TURN server dependencies and replaces them with dynamic ICE candidate fetching from the local AD4M executor.

Changes

Phase 4a — STUN removal:

  • Deleted getDefaultIceServers.ts, stun-servers.ts and all references
  • Removed STUN server configuration from Connection.vue settings
  • ~935 lines deleted across 15 files

Phase 4b — Executor STUN integration:

  • webrtcStore.ts: Fetches runtimeIceCandidates from executor GraphQL, formats as stun: URLs, 60s refresh cycle tied to room join/leave
  • WebRTCManager.ts: Accepts dynamic iceServers, passes to RTCPeerConnection
  • Graceful fallback to empty iceServers when no addresses available (LAN-only mode)

How it works

  1. When joining a room, Flux queries the local executor's runtimeIceCandidates GraphQL endpoint
  2. Executor returns its Iroh-discovered transport addresses as ICE candidates
  3. Flux formats these as STUN server URLs and passes to RTCPeerConnection
  4. Browsers use peer executors as decentralized STUN servers
  5. Refresh every 60s to pick up address changes

Dependencies

Part of: coasys/ad4m#719

Summary by CodeRabbit

Release Notes

  • New Features

    • Connection now uses dynamic Iroh transport for establishing peer-to-peer communication.
  • UI Changes

    • Removed manual STUN server configuration interface; simplified WebRTC settings by eliminating the Connection management tab.

depends on d9011e34-8219-4ba1-bdd1-7d104a25cd1f/91215135-9143-46eb-8f5d-b38504a40392#742
depends on d9011e34-8219-4ba1-bdd1-7d104a25cd1f/91215135-9143-46eb-8f5d-b38504a40392#719

Removes all hardcoded STUN server lists, ICE server management UI,
and related configuration. ICE candidates will be provided by the
AD4M executor via ad4mClient.runtime.iceCandidates() using addresses
discovered by the Iroh transport layer.

Part of: coasys/ad4m#719
- webrtcStore: fetch ICE candidates from executor via ad4mClient.runtime.iceCandidates()
- Format executor addresses as stun:{address}:{port} URLs for RTCPeerConnection
- Refresh STUN servers every 60s while in a call (start on join, stop on leave)
- Fallback to empty iceServers when executor unavailable (LAN-only mode)
- WebRTCManager: accept iceServers prop, add updateIceServers() for dynamic updates
@netlify

netlify Bot commented Mar 11, 2026

Copy link
Copy Markdown

Deploy Preview for fluxsocial-dev failed. Why did it fail? →

Name Link
🔨 Latest commit 93b3728
🔍 Latest deploy log https://app.netlify.com/projects/fluxsocial-dev/deploys/69b1e3f91479220008d6dbe3

@coderabbitai

coderabbitai Bot commented Mar 11, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This PR replaces static ICE/STUN server management with dynamic Iroh-based transport by removing hardcoded server defaults, eliminating manual server configuration UI, and introducing automatic STUN candidate fetching from an executor with periodic 60-second refresh cycles integrated into room join/leave lifecycle events.

Changes

Cohort / File(s) Summary
Vue Settings UI Removal
app/src/views/main/modals/webrtc-settings/Connection.vue, app/src/views/main/modals/webrtc-settings/WebrtcSettingsModal.vue, views/webrtc-view/src/components/Settings/Connection/*
Removed entire Connection component and associated menu tabs from WebRTC settings modals, eliminating UI for manual ICE/STUN server management, including add/remove/reset server controls.
WebRTC Store Dynamic ICE Servers
app/src/stores/webrtcStore.ts
Replaced static iceServers with dynamic executorIceServers, added executor-based STUN candidate fetching with 60-second periodic refresh, integrated ICE refresh lifecycle (startIceRefresh/stopIceRefresh) into joinRoom/leaveRoom, removed public ICE server mutation helpers.
WebRTC Manager Type System
packages/webrtc/src/WebRTCManager.ts
Removed custom IceServer type in favor of native RTCIceServer, replaced setIceServers() with updateIceServers(), made iceServers a private field initialized from props, removed getDefaultIceServers dependency.
Constants & Utilities Cleanup
packages/constants/src/videoSettings.ts, packages/utils/src/getDefaultIceServers.ts, packages/utils/src/index.ts
Removed defaultIceServers constant export and getDefaultIceServers() utility function, eliminating hardcoded ICE server defaults from public APIs.
React Hook ICE Server Support
packages/react-web/src/useWebrtc.ts
Removed iceServers and onChangeIceServers from WebRTC type and hook return value, eliminating ICE server state management from React hook API.
Call Window Disclaimer
app/src/components/call/window/CallWindow.vue
Updated footer disclaimer text from STUN servers reference to Iroh transport messaging: "Connection is established via Iroh transport. All further communication is peer-to-peer."
STUN Server Lists Cleanup
views/webrtc-debug/src/stun-servers.ts, views/webrtc-view/src/stun-servers.ts
Removed hardcoded STUN server URL arrays (257 lines each), eliminating static fallback server lists.
CSS Styling Removal
views/webrtc-view/src/components/Settings/Connection/Connection.module.css
Removed all Connection component styling rules (card, contents, credentials, footer classes).

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • lucksus

Poem

🐰 Hops excitedly

Static servers fade away,
Iroh's transport saves the day!
Dynamic STUN with periodic cheer,
Connection flows so crystal clear! 🚀

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately reflects the main objective of the PR: replacing external STUN servers with a new executor-based (Iroh) NAT traversal system. It is specific and clear about the primary change.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/iroh-ice-v2

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/webrtc/src/WebRTCManager.ts (1)

524-527: ⚠️ Potential issue | 🔴 Critical

Syntax error: extra closing brace.

Line 526 has an extraneous closing brace that will cause a compilation error. The sendTestBroadcast method closes on line 524, and the class should close directly with the brace on line 527.

🐛 Proposed fix
   async sendTestBroadcast() {
     // console.log("⚙️ Sending TEST_BROADCAST to room");
     this.neighbourhood.sendBroadcastU({
       links: [
         {
           source: this.source,
           predicate: TEST_BROADCAST,
           target: Literal.from('test broadcast').toUrl(),
         },
       ],
     });
   }
-
-  }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/webrtc/src/WebRTCManager.ts` around lines 524 - 527, There’s an
extra closing brace after the sendTestBroadcast method causing a syntax error;
remove the redundant closing brace so the sendTestBroadcast method (in
WebRTCManager) ends with its single closing brace and the class closes only with
the final closing brace for the WebRTCManager class.
🧹 Nitpick comments (1)
app/src/stores/webrtcStore.ts (1)

74-91: Error handling is acceptable but could be more robust.

The current approach of keeping existing servers on failure and falling back to empty on first failure is reasonable. However, consider logging the response status on non-JSON errors for debugging.

♻️ Optional enhancement for better debugging
       const response = await fetch(httpUrl, {
         method: 'POST',
         headers: { 'Content-Type': 'application/json', ...(token ? { authorization: token } : {}) },
         body: JSON.stringify({ query: '{ runtimeIceCandidates { address port } }' }),
       });
+      if (!response.ok) {
+        console.warn(`iroh-ice: executor returned status ${response.status}`);
+        return;
+      }
       const json = await response.json();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/stores/webrtcStore.ts` around lines 74 - 91, The fetch in
refreshIceServers should log HTTP response status/body when the server returns
non-OK or non-JSON to improve debugging: after calling fetch(httpUrl, ...),
check response.ok and if false call response.text() and log response.status and
the returned text before returning/throwing; also wrap response.json() in its
own try/catch so JSON parse failures are caught and logged (include
response.status and any body text), then only proceed to call formatStunServers
and assign executorIceServers.value when a valid JSON payload with
data.runtimeIceCandidates exists.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/src/stores/webrtcStore.ts`:
- Around line 93-98: The initial ICE fetch is started but not awaited, causing
executorIceServers.value to be empty when peers are created; modify
startIceRefresh/refreshIceServers so the initial fetch is awaited (e.g., have
startIceRefresh return a Promise that awaits refreshIceServers for the first
call and then starts the interval) and update joinRoom to await startIceRefresh
(or directly await refreshIceServers) before calling createPeerConnection so
executorIceServers.value is populated when peer connections are constructed.

In `@packages/webrtc/src/WebRTCManager.ts`:
- Around line 110-114: The React package is not providing ICE servers to
WebRTCManager (iceServers defaults to [] in the WebRTCManager constructor) and
updateIceServers() is never invoked; fix by wiring ICE configuration into the
hook that instantiates WebRTCManager (useWebrtc.ts): fetch or accept STUN/TURN
server config in the hook, then call
webRTCManager.updateIceServers(fetchedIceServers) (or pass props.iceServers into
new WebRTCManager({...}) when constructing) so RTCPeerConnections get proper
RTCIceServer entries; alternatively, if you intend LAN-only, add clear docs and
a runtime warning in useWebrtc.ts referencing WebRTCManager and
updateIceServers.

---

Outside diff comments:
In `@packages/webrtc/src/WebRTCManager.ts`:
- Around line 524-527: There’s an extra closing brace after the
sendTestBroadcast method causing a syntax error; remove the redundant closing
brace so the sendTestBroadcast method (in WebRTCManager) ends with its single
closing brace and the class closes only with the final closing brace for the
WebRTCManager class.

---

Nitpick comments:
In `@app/src/stores/webrtcStore.ts`:
- Around line 74-91: The fetch in refreshIceServers should log HTTP response
status/body when the server returns non-OK or non-JSON to improve debugging:
after calling fetch(httpUrl, ...), check response.ok and if false call
response.text() and log response.status and the returned text before
returning/throwing; also wrap response.json() in its own try/catch so JSON parse
failures are caught and logged (include response.status and any body text), then
only proceed to call formatStunServers and assign executorIceServers.value when
a valid JSON payload with data.runtimeIceCandidates exists.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 85c440ec-1448-4796-bee5-26a7b763d653

📥 Commits

Reviewing files that changed from the base of the PR and between 0c1ae90 and 93b3728.

📒 Files selected for processing (15)
  • app/src/components/call/window/CallWindow.vue
  • app/src/stores/webrtcStore.ts
  • app/src/views/main/modals/webrtc-settings/Connection.vue
  • app/src/views/main/modals/webrtc-settings/WebrtcSettingsModal.vue
  • packages/constants/src/videoSettings.ts
  • packages/react-web/src/useWebrtc.ts
  • packages/utils/src/getDefaultIceServers.ts
  • packages/utils/src/index.ts
  • packages/webrtc/src/WebRTCManager.ts
  • views/webrtc-debug/src/stun-servers.ts
  • views/webrtc-view/src/components/Settings/Connection/Connection.module.css
  • views/webrtc-view/src/components/Settings/Connection/Connection.tsx
  • views/webrtc-view/src/components/Settings/Connection/index.ts
  • views/webrtc-view/src/components/Settings/Settings.tsx
  • views/webrtc-view/src/stun-servers.ts
💤 Files with no reviewable changes (11)
  • views/webrtc-view/src/components/Settings/Settings.tsx
  • views/webrtc-view/src/components/Settings/Connection/index.ts
  • views/webrtc-view/src/stun-servers.ts
  • packages/utils/src/getDefaultIceServers.ts
  • app/src/views/main/modals/webrtc-settings/WebrtcSettingsModal.vue
  • packages/utils/src/index.ts
  • packages/constants/src/videoSettings.ts
  • app/src/views/main/modals/webrtc-settings/Connection.vue
  • views/webrtc-debug/src/stun-servers.ts
  • views/webrtc-view/src/components/Settings/Connection/Connection.module.css
  • views/webrtc-view/src/components/Settings/Connection/Connection.tsx

Comment on lines +93 to +98
/** Start periodic ICE server refresh (every 60s). */
function startIceRefresh() {
if (iceRefreshInterval) return;
refreshIceServers(); // immediate first fetch
iceRefreshInterval = setInterval(refreshIceServers, 60_000);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Race condition: ICE servers may not be available when peers connect.

startIceRefresh() calls refreshIceServers() without awaiting it. In joinRoom(), peer connections are created immediately after calling startIceRefresh() (lines 629-636). Since refreshIceServers() is async and involves a network fetch, executorIceServers.value will likely still be empty when createPeerConnection() runs.

Consider awaiting the initial fetch before creating peer connections:

🔧 Proposed fix
-    /** Start periodic ICE server refresh (every 60s). */
-    function startIceRefresh() {
+    /** Start periodic ICE server refresh (every 60s). Returns promise for initial fetch. */
+    async function startIceRefresh() {
       if (iceRefreshInterval) return;
-      refreshIceServers(); // immediate first fetch
+      await refreshIceServers(); // await initial fetch before connections
       iceRefreshInterval = setInterval(refreshIceServers, 60_000);
     }

Then in joinRoom():

       // Start fetching STUN servers from executor
-      startIceRefresh();
+      await startIceRefresh();

       try {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/stores/webrtcStore.ts` around lines 93 - 98, The initial ICE fetch is
started but not awaited, causing executorIceServers.value to be empty when peers
are created; modify startIceRefresh/refreshIceServers so the initial fetch is
awaited (e.g., have startIceRefresh return a Promise that awaits
refreshIceServers for the first call and then starts the interval) and update
joinRoom to await startIceRefresh (or directly await refreshIceServers) before
calling createPeerConnection so executorIceServers.value is populated when peer
connections are constructed.

Comment on lines +110 to 114
private iceServers: RTCIceServer[];

constructor(props: Props) {
this.iceServers = props.iceServers || [];
this.init(props);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if updateIceServers is called anywhere in the React package
rg -n 'updateIceServers' packages/react-web/

Repository: coasys/flux

Length of output: 37


🏁 Script executed:

# Check how WebRTCManager is instantiated in useWebrtc.ts
rg -B 5 -A 5 'new WebRTCManager' packages/react-web/src/useWebrtc.ts

Repository: coasys/flux

Length of output: 300


🏁 Script executed:

# Check the Props interface and iceServers field
rg -B 3 -A 10 'interface Props' packages/webrtc/src/WebRTCManager.ts

Repository: coasys/flux

Length of output: 37


🏁 Script executed:

# Check webrtcStore.ts for ICE fetching mechanism
head -50 packages/webrtc/src/webrtcStore.ts

Repository: coasys/flux

Length of output: 148


🏁 Script executed:

# List all files in webrtc/src to understand structure
find packages/webrtc/src -type f -name "*.ts" | head -20

Repository: coasys/flux

Length of output: 152


🏁 Script executed:

# Search for Props type definition more broadly in WebRTCManager.ts
rg -A 15 'Props' packages/webrtc/src/WebRTCManager.ts | head -40

Repository: coasys/flux

Length of output: 1194


🏁 Script executed:

# Search for ICE server initialization or fetching anywhere in webrtc package
rg -n 'iceServers' packages/webrtc/src/ | head -20

Repository: coasys/flux

Length of output: 557


🏁 Script executed:

# Search for updateIceServers calls across the entire codebase
rg -n 'updateIceServers' . --type ts --type js

Repository: coasys/flux

Length of output: 146


🏁 Script executed:

# Search for any ICE server initialization in other packages
rg -n 'iceServers' packages/ --type ts -B 2 -A 2 | head -80

Repository: coasys/flux

Length of output: 1656


🏁 Script executed:

# Check if there are other instantiations of WebRTCManager outside react-web
rg -n 'new WebRTCManager' . --type ts

Repository: coasys/flux

Length of output: 140


React package lacks ICE server integration.

The constructor defaults iceServers to an empty array, and useWebrtc.ts instantiates WebRTCManager without passing iceServers. This means React web package users will have no STUN/TURN servers configured, breaking NAT traversal for non-LAN connections.

While updateIceServers() is defined in WebRTCManager, it is never called anywhere in the codebase. Either:

  1. Implement ICE server configuration in the React hook (e.g., fetch STUN servers during initialization and call updateIceServers()), or
  2. Document that the React package only supports LAN-only operation.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/webrtc/src/WebRTCManager.ts` around lines 110 - 114, The React
package is not providing ICE servers to WebRTCManager (iceServers defaults to []
in the WebRTCManager constructor) and updateIceServers() is never invoked; fix
by wiring ICE configuration into the hook that instantiates WebRTCManager
(useWebrtc.ts): fetch or accept STUN/TURN server config in the hook, then call
webRTCManager.updateIceServers(fetchedIceServers) (or pass props.iceServers into
new WebRTCManager({...}) when constructing) so RTCPeerConnections get proper
RTCIceServer entries; alternatively, if you intend LAN-only, add clear docs and
a runtime warning in useWebrtc.ts referencing WebRTCManager and
updateIceServers.

@HexaField HexaField marked this pull request as draft March 11, 2026 22:12
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