From 07e6e4271ea3a45e9cd364eb6ec05801a653d9e8 Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Sun, 30 Aug 2026 22:17:30 +0200 Subject: feat: configurable STUN server fallbacks for WebRTC ICE MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The WebRTC transport relied on a single Google STUN server — if it was unreachable, ICE gathering waited the full 4s timeout. Now four public servers are used by default (Google ×2, Cloudflare, Mozilla), configurable via node.toml, the Node page UI, and the CLI (meshbay-node stun list|add| remove|reset). Changes are hot-swapped on the live transport. Co-Authored-By: Claude Opus 4.6 --- .../src/meshbay_hub/static/node-page.js | 104 +++++++++++++++++++++ 1 file changed, 104 insertions(+) (limited to 'packages/meshbay-hub/src/meshbay_hub/static/node-page.js') diff --git a/packages/meshbay-hub/src/meshbay_hub/static/node-page.js b/packages/meshbay-hub/src/meshbay_hub/static/node-page.js index 9a6db7d..87a9815 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/node-page.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/node-page.js @@ -102,6 +102,9 @@ export function NodePage({ groups }) { const [nodeSettings, setNodeSettings] = useState(null); const [editSettings, setEditSettings] = useState(null); const [savingSettings, setSavingSettings] = useState(false); + const [editStun, setEditStun] = useState(null); + const [savingStun, setSavingStun] = useState(false); + const [stunInput, setStunInput] = useState(''); const [operatorPaired, setOperatorPaired] = useState(false); const [pairBusy, setPairBusy] = useState(false); const [pairStatus, setPairStatus] = useState(''); @@ -131,6 +134,7 @@ export function NodePage({ groups }) { useEffect(() => { if (nodeSettings && !editSettings) setEditSettings({ ...nodeSettings }); + if (nodeSettings && !editStun) setEditStun([...(nodeSettings.stun_servers || [])]); }, [nodeSettings]); const refresh = useCallback(async () => { @@ -291,6 +295,61 @@ export function NodePage({ groups }) { } }, [editSettings]); + const saveStun = useCallback(async () => { + if (!editStun) return; + setSavingStun(true); + setActionMsg(''); + try { + await nodeCall('PUT', '/api/node-settings', { stun_servers: editStun }); + setNodeSettings(s => s ? { ...s, stun_servers: [...editStun] } : s); + setActionMsg(t('node.stun_saved')); + } catch (err) { + setActionMsg(platform.bridgeMessage(err)); + } finally { + setSavingStun(false); + } + }, [editStun]); + + const addStunServer = useCallback(() => { + const url = stunInput.trim(); + if (!url) return; + if (!url.startsWith('stun:')) { + setActionMsg(t('node.stun_invalid')); + return; + } + if (editStun && editStun.includes(url)) { + setActionMsg(t('node.stun_duplicate')); + return; + } + setEditStun(s => [...(s || []), url]); + setStunInput(''); + setActionMsg(''); + }, [stunInput, editStun]); + + const removeStunServer = useCallback((idx) => { + setEditStun(s => s.filter((_, i) => i !== idx)); + }, []); + + const moveStunServer = useCallback((idx, dir) => { + setEditStun(s => { + const a = [...s]; + const target = idx + dir; + if (target < 0 || target >= a.length) return a; + [a[idx], a[target]] = [a[target], a[idx]]; + return a; + }); + }, []); + + const resetStunDefaults = useCallback(() => { + setEditStun([ + 'stun:stun.l.google.com:19302', + 'stun:stun1.l.google.com:19302', + 'stun:stun.cloudflare.com:3478', + 'stun:stun.services.mozilla.com:3478', + ]); + setActionMsg(''); + }, []); + const doPairOperator = useCallback(async () => { setPairBusy(true); setPairStatus(''); @@ -559,6 +618,51 @@ export function NodePage({ groups }) { `} + + ${editStun && html` +
+ ${t('node.stun_servers')} +

${t('node.stun_hint')}

+ ${editStun.length === 0 ? html` +

${t('node.stun_empty')}

+ ` : html` +
+ ${editStun.map((url, idx) => html` +
+ ${url} +
+ + + +
+
+ `)} +
+ `} +
+ setStunInput(e.target.value)} + onKeyDown=${e => { if (e.key === 'Enter') addStunServer(); }} /> + +
+
+ + +
+
+ `} `; } -- cgit v1.2.3