From 2d6c276734fd19f80edf84cdb95f91a929df6365 Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Sun, 30 Aug 2026 22:36:56 +0200 Subject: feat(ui): configurable ICE interfaces in the Node page The ice_interfaces setting (auto-exclude vs explicit whitelist) is now editable from the Node page, persisted via the settings API and roster, and hot-swapped at runtime by re-installing the aioice filter. Co-Authored-By: Claude Opus 4.6 --- .../src/meshbay_hub/static/locales/de.js | 11 +++ .../src/meshbay_hub/static/locales/en.js | 11 +++ .../src/meshbay_hub/static/locales/es.js | 11 +++ .../src/meshbay_hub/static/locales/fr.js | 11 +++ .../src/meshbay_hub/static/locales/it.js | 11 +++ .../src/meshbay_hub/static/locales/ja.js | 11 +++ .../src/meshbay_hub/static/locales/nl.js | 11 +++ .../src/meshbay_hub/static/locales/pl.js | 11 +++ .../src/meshbay_hub/static/locales/pt-BR.js | 11 +++ .../src/meshbay_hub/static/locales/zh-CN.js | 11 +++ .../src/meshbay_hub/static/node-page.js | 81 ++++++++++++++++++++++ packages/meshbay-node/src/meshbay_node/daemon.py | 1 + packages/meshbay-node/src/meshbay_node/ops.py | 17 +++-- packages/meshbay-node/src/meshbay_node/roster.py | 22 +++--- 14 files changed, 217 insertions(+), 14 deletions(-) (limited to 'packages') diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js index 604be3c..b61d93c 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js @@ -757,4 +757,15 @@ export default { 'node.stun_empty': 'No STUN servers configured. NAT traversal will not work.', 'node.stun_duplicate': 'This server is already in the list.', 'node.stun_invalid': 'STUN URL must start with stun:', + 'node.ice_interfaces': 'ICE Interfaces', + 'node.ice_auto_hint': 'Auto mode — virtual and VPN interfaces (Tailscale, Docker, libvirt, etc.) are excluded automatically.', + 'node.ice_manual_hint': 'Manual mode — only the listed interfaces are used for ICE candidate gathering.', + 'node.ice_add': 'Add', + 'node.ice_add_placeholder': 'e.g. eth0, wlp0s20f3', + 'node.ice_remove': 'Remove', + 'node.ice_save': 'Save', + 'node.ice_saving': 'Saving…', + 'node.ice_saved': 'ICE interfaces saved.', + 'node.ice_reset_auto': 'Switch to auto', + 'node.ice_duplicate': 'This interface is already in the list.', }; diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js index a58619e..851911c 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js @@ -736,4 +736,15 @@ export default { 'node.stun_empty': 'No STUN servers configured. NAT traversal will not work.', 'node.stun_duplicate': 'This server is already in the list.', 'node.stun_invalid': 'STUN URL must start with stun:', + 'node.ice_interfaces': 'ICE Interfaces', + 'node.ice_auto_hint': 'Auto mode — virtual and VPN interfaces (Tailscale, Docker, libvirt, etc.) are excluded automatically.', + 'node.ice_manual_hint': 'Manual mode — only the listed interfaces are used for ICE candidate gathering.', + 'node.ice_add': 'Add', + 'node.ice_add_placeholder': 'e.g. eth0, wlp0s20f3', + 'node.ice_remove': 'Remove', + 'node.ice_save': 'Save', + 'node.ice_saving': 'Saving…', + 'node.ice_saved': 'ICE interfaces saved.', + 'node.ice_reset_auto': 'Switch to auto', + 'node.ice_duplicate': 'This interface is already in the list.', }; diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js index bcb0b93..84af4cd 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js @@ -753,4 +753,15 @@ export default { 'node.stun_empty': 'No STUN servers configured. NAT traversal will not work.', 'node.stun_duplicate': 'This server is already in the list.', 'node.stun_invalid': 'STUN URL must start with stun:', + 'node.ice_interfaces': 'ICE Interfaces', + 'node.ice_auto_hint': 'Auto mode — virtual and VPN interfaces (Tailscale, Docker, libvirt, etc.) are excluded automatically.', + 'node.ice_manual_hint': 'Manual mode — only the listed interfaces are used for ICE candidate gathering.', + 'node.ice_add': 'Add', + 'node.ice_add_placeholder': 'e.g. eth0, wlp0s20f3', + 'node.ice_remove': 'Remove', + 'node.ice_save': 'Save', + 'node.ice_saving': 'Saving…', + 'node.ice_saved': 'ICE interfaces saved.', + 'node.ice_reset_auto': 'Switch to auto', + 'node.ice_duplicate': 'This interface is already in the list.', }; diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js index 2e3e4d6..e540b4b 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js @@ -769,4 +769,15 @@ export default { 'node.stun_empty': 'No STUN servers configured. NAT traversal will not work.', 'node.stun_duplicate': 'This server is already in the list.', 'node.stun_invalid': 'STUN URL must start with stun:', + 'node.ice_interfaces': 'ICE Interfaces', + 'node.ice_auto_hint': 'Auto mode — virtual and VPN interfaces (Tailscale, Docker, libvirt, etc.) are excluded automatically.', + 'node.ice_manual_hint': 'Manual mode — only the listed interfaces are used for ICE candidate gathering.', + 'node.ice_add': 'Add', + 'node.ice_add_placeholder': 'e.g. eth0, wlp0s20f3', + 'node.ice_remove': 'Remove', + 'node.ice_save': 'Save', + 'node.ice_saving': 'Saving…', + 'node.ice_saved': 'ICE interfaces saved.', + 'node.ice_reset_auto': 'Switch to auto', + 'node.ice_duplicate': 'This interface is already in the list.', }; diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js index dc1d16f..b009460 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js @@ -767,4 +767,15 @@ export default { 'node.stun_empty': 'No STUN servers configured. NAT traversal will not work.', 'node.stun_duplicate': 'This server is already in the list.', 'node.stun_invalid': 'STUN URL must start with stun:', + 'node.ice_interfaces': 'ICE Interfaces', + 'node.ice_auto_hint': 'Auto mode — virtual and VPN interfaces (Tailscale, Docker, libvirt, etc.) are excluded automatically.', + 'node.ice_manual_hint': 'Manual mode — only the listed interfaces are used for ICE candidate gathering.', + 'node.ice_add': 'Add', + 'node.ice_add_placeholder': 'e.g. eth0, wlp0s20f3', + 'node.ice_remove': 'Remove', + 'node.ice_save': 'Save', + 'node.ice_saving': 'Saving…', + 'node.ice_saved': 'ICE interfaces saved.', + 'node.ice_reset_auto': 'Switch to auto', + 'node.ice_duplicate': 'This interface is already in the list.', }; diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js index 90a5f24..9b317a1 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js @@ -751,4 +751,15 @@ export default { 'node.stun_empty': 'No STUN servers configured. NAT traversal will not work.', 'node.stun_duplicate': 'This server is already in the list.', 'node.stun_invalid': 'STUN URL must start with stun:', + 'node.ice_interfaces': 'ICE Interfaces', + 'node.ice_auto_hint': 'Auto mode — virtual and VPN interfaces (Tailscale, Docker, libvirt, etc.) are excluded automatically.', + 'node.ice_manual_hint': 'Manual mode — only the listed interfaces are used for ICE candidate gathering.', + 'node.ice_add': 'Add', + 'node.ice_add_placeholder': 'e.g. eth0, wlp0s20f3', + 'node.ice_remove': 'Remove', + 'node.ice_save': 'Save', + 'node.ice_saving': 'Saving…', + 'node.ice_saved': 'ICE interfaces saved.', + 'node.ice_reset_auto': 'Switch to auto', + 'node.ice_duplicate': 'This interface is already in the list.', }; diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js index 3347087..0436a31 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js @@ -769,4 +769,15 @@ export default { 'node.stun_empty': 'No STUN servers configured. NAT traversal will not work.', 'node.stun_duplicate': 'This server is already in the list.', 'node.stun_invalid': 'STUN URL must start with stun:', + 'node.ice_interfaces': 'ICE Interfaces', + 'node.ice_auto_hint': 'Auto mode — virtual and VPN interfaces (Tailscale, Docker, libvirt, etc.) are excluded automatically.', + 'node.ice_manual_hint': 'Manual mode — only the listed interfaces are used for ICE candidate gathering.', + 'node.ice_add': 'Add', + 'node.ice_add_placeholder': 'e.g. eth0, wlp0s20f3', + 'node.ice_remove': 'Remove', + 'node.ice_save': 'Save', + 'node.ice_saving': 'Saving…', + 'node.ice_saved': 'ICE interfaces saved.', + 'node.ice_reset_auto': 'Switch to auto', + 'node.ice_duplicate': 'This interface is already in the list.', }; diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js index 499a54f..391b8cd 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js @@ -796,4 +796,15 @@ export default { 'node.stun_empty': 'No STUN servers configured. NAT traversal will not work.', 'node.stun_duplicate': 'This server is already in the list.', 'node.stun_invalid': 'STUN URL must start with stun:', + 'node.ice_interfaces': 'ICE Interfaces', + 'node.ice_auto_hint': 'Auto mode — virtual and VPN interfaces (Tailscale, Docker, libvirt, etc.) are excluded automatically.', + 'node.ice_manual_hint': 'Manual mode — only the listed interfaces are used for ICE candidate gathering.', + 'node.ice_add': 'Add', + 'node.ice_add_placeholder': 'e.g. eth0, wlp0s20f3', + 'node.ice_remove': 'Remove', + 'node.ice_save': 'Save', + 'node.ice_saving': 'Saving…', + 'node.ice_saved': 'ICE interfaces saved.', + 'node.ice_reset_auto': 'Switch to auto', + 'node.ice_duplicate': 'This interface is already in the list.', }; diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js index 03a9f6e..16e0559 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js @@ -754,4 +754,15 @@ export default { 'node.stun_empty': 'No STUN servers configured. NAT traversal will not work.', 'node.stun_duplicate': 'This server is already in the list.', 'node.stun_invalid': 'STUN URL must start with stun:', + 'node.ice_interfaces': 'ICE Interfaces', + 'node.ice_auto_hint': 'Auto mode — virtual and VPN interfaces (Tailscale, Docker, libvirt, etc.) are excluded automatically.', + 'node.ice_manual_hint': 'Manual mode — only the listed interfaces are used for ICE candidate gathering.', + 'node.ice_add': 'Add', + 'node.ice_add_placeholder': 'e.g. eth0, wlp0s20f3', + 'node.ice_remove': 'Remove', + 'node.ice_save': 'Save', + 'node.ice_saving': 'Saving…', + 'node.ice_saved': 'ICE interfaces saved.', + 'node.ice_reset_auto': 'Switch to auto', + 'node.ice_duplicate': 'This interface is already in the list.', }; diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js index 8004868..edd9edc 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js @@ -738,4 +738,15 @@ export default { 'node.stun_empty': 'No STUN servers configured. NAT traversal will not work.', 'node.stun_duplicate': 'This server is already in the list.', 'node.stun_invalid': 'STUN URL must start with stun:', + 'node.ice_interfaces': 'ICE Interfaces', + 'node.ice_auto_hint': 'Auto mode — virtual and VPN interfaces (Tailscale, Docker, libvirt, etc.) are excluded automatically.', + 'node.ice_manual_hint': 'Manual mode — only the listed interfaces are used for ICE candidate gathering.', + 'node.ice_add': 'Add', + 'node.ice_add_placeholder': 'e.g. eth0, wlp0s20f3', + 'node.ice_remove': 'Remove', + 'node.ice_save': 'Save', + 'node.ice_saving': 'Saving…', + 'node.ice_saved': 'ICE interfaces saved.', + 'node.ice_reset_auto': 'Switch to auto', + 'node.ice_duplicate': 'This interface is already in the list.', }; 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 87a9815..82fc56e 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/node-page.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/node-page.js @@ -105,6 +105,9 @@ export function NodePage({ groups }) { const [editStun, setEditStun] = useState(null); const [savingStun, setSavingStun] = useState(false); const [stunInput, setStunInput] = useState(''); + const [editIce, setEditIce] = useState(null); + const [savingIce, setSavingIce] = useState(false); + const [iceInput, setIceInput] = useState(''); const [operatorPaired, setOperatorPaired] = useState(false); const [pairBusy, setPairBusy] = useState(false); const [pairStatus, setPairStatus] = useState(''); @@ -135,6 +138,7 @@ export function NodePage({ groups }) { useEffect(() => { if (nodeSettings && !editSettings) setEditSettings({ ...nodeSettings }); if (nodeSettings && !editStun) setEditStun([...(nodeSettings.stun_servers || [])]); + if (nodeSettings && !editIce) setEditIce([...(nodeSettings.ice_interfaces || [])]); }, [nodeSettings]); const refresh = useCallback(async () => { @@ -350,6 +354,42 @@ export function NodePage({ groups }) { setActionMsg(''); }, []); + const saveIce = useCallback(async () => { + if (!editIce) return; + setSavingIce(true); + setActionMsg(''); + try { + await nodeCall('PUT', '/api/node-settings', { ice_interfaces: editIce }); + setNodeSettings(s => s ? { ...s, ice_interfaces: [...editIce] } : s); + setActionMsg(t('node.ice_saved')); + } catch (err) { + setActionMsg(platform.bridgeMessage(err)); + } finally { + setSavingIce(false); + } + }, [editIce]); + + const addIceInterface = useCallback(() => { + const name = iceInput.trim(); + if (!name) return; + if (editIce && editIce.includes(name)) { + setActionMsg(t('node.ice_duplicate')); + return; + } + setEditIce(s => [...(s || []), name]); + setIceInput(''); + setActionMsg(''); + }, [iceInput, editIce]); + + const removeIceInterface = useCallback((idx) => { + setEditIce(s => s.filter((_, i) => i !== idx)); + }, []); + + const resetIceAuto = useCallback(() => { + setEditIce([]); + setActionMsg(''); + }, []); + const doPairOperator = useCallback(async () => { setPairBusy(true); setPairStatus(''); @@ -663,6 +703,47 @@ export function NodePage({ groups }) { `} + + ${editIce && html` +
+ ${t('node.ice_interfaces')} +

${editIce.length === 0 + ? t('node.ice_auto_hint') + : t('node.ice_manual_hint')}

+ ${editIce.length > 0 && html` +
+ ${editIce.map((name, idx) => html` +
+ ${name} +
+ +
+
+ `)} +
+ `} +
+ setIceInput(e.target.value)} + onKeyDown=${e => { if (e.key === 'Enter') addIceInterface(); }} /> + +
+
+ + ${editIce.length > 0 && html` + + `} +
+
+ `} `; } diff --git a/packages/meshbay-node/src/meshbay_node/daemon.py b/packages/meshbay-node/src/meshbay_node/daemon.py index 51f78b7..5c6d86c 100644 --- a/packages/meshbay-node/src/meshbay_node/daemon.py +++ b/packages/meshbay-node/src/meshbay_node/daemon.py @@ -278,6 +278,7 @@ class NodeDaemon: "max_concurrent_streams": nd.max_concurrent_streams, "transcode_incompatible_video": nd.transcode_incompatible_video, "stun_servers": nd.stun_servers if nd.stun_servers else list(DEFAULT_STUN_SERVERS), + "ice_interfaces": nd.ice_interfaces, } effective = await self._roster.node_settings(defaults) for k, v in effective.items(): diff --git a/packages/meshbay-node/src/meshbay_node/ops.py b/packages/meshbay-node/src/meshbay_node/ops.py index 2ab6753..07579cd 100644 --- a/packages/meshbay-node/src/meshbay_node/ops.py +++ b/packages/meshbay-node/src/meshbay_node/ops.py @@ -341,6 +341,7 @@ async def list_groups(state: dict) -> dict: "max_concurrent_streams": nd.max_concurrent_streams if nd else 8, "transcode_incompatible_video": nd.transcode_incompatible_video if nd else True, "stun_servers": nd.stun_servers if nd and nd.stun_servers else list(DEFAULT_STUN_SERVERS), + "ice_interfaces": nd.ice_interfaces if nd else [], } if roster: settings = await roster.node_settings(defaults) @@ -799,6 +800,7 @@ async def get_node_settings(state: dict) -> dict: "max_concurrent_streams": nd.max_concurrent_streams, "transcode_incompatible_video": nd.transcode_incompatible_video, "stun_servers": nd.stun_servers if nd.stun_servers else list(DEFAULT_STUN_SERVERS), + "ice_interfaces": nd.ice_interfaces, } if roster: return await roster.node_settings(defaults) @@ -817,7 +819,8 @@ async def set_node_settings(state: dict, settings: dict) -> dict: "device_request_ttl_minutes": ("int", roster.SETTING_DEVICE_TTL), "max_concurrent_streams": ("int", roster.SETTING_MAX_STREAMS), "transcode_incompatible_video": ("bool", roster.SETTING_TRANSCODE), - "stun_servers": ("list", roster.SETTING_STUN_SERVERS), + "stun_servers": ("stun_list", roster.SETTING_STUN_SERVERS), + "ice_interfaces": ("list", roster.SETTING_ICE_INTERFACES), } set_by = state.get("node_user_id", "") @@ -841,14 +844,15 @@ async def set_node_settings(state: dict, settings: dict) -> dict: setattr(nd, key, v) await roster.set_node_setting(setting_key, "1" if v else "0", set_by) updated[key] = v - elif kind == "list": + elif kind in ("list", "stun_list"): import json as _json if not isinstance(value, list): raise OpError(f"{key} must be a list") v = [str(s) for s in value] - for s in v: - if not s.startswith("stun:"): - raise OpError(f"Invalid STUN server: {s} (must start with stun:)") + if kind == "stun_list": + for s in v: + if not s.startswith("stun:"): + raise OpError(f"Invalid STUN server: {s} (must start with stun:)") setattr(nd, key, v) await roster.set_node_setting(setting_key, _json.dumps(v), set_by) updated[key] = v @@ -863,6 +867,9 @@ async def set_node_settings(state: dict, settings: dict) -> dict: webrtc = state.get("webrtc") if webrtc and hasattr(webrtc, '_stun'): webrtc._stun = updated["stun_servers"] + if "ice_interfaces" in updated: + from meshbay_node.transport.ice_filter import install as install_ice_filter + install_ice_filter(updated["ice_interfaces"] or None) log.info("Node settings updated: %s", updated) return {"updated": updated} diff --git a/packages/meshbay-node/src/meshbay_node/roster.py b/packages/meshbay-node/src/meshbay_node/roster.py index 5bdb5ee..5af2b55 100644 --- a/packages/meshbay-node/src/meshbay_node/roster.py +++ b/packages/meshbay-node/src/meshbay_node/roster.py @@ -765,6 +765,7 @@ class Roster: SETTING_MAX_STREAMS = "max_concurrent_streams" SETTING_TRANSCODE = "transcode_incompatible_video" SETTING_STUN_SERVERS = "stun_servers" + SETTING_ICE_INTERFACES = "ice_interfaces" async def node_settings(self, defaults: dict) -> dict: """Current effective settings: roster override if present, else config default.""" @@ -785,15 +786,18 @@ class Roster: result[key] = int(stored) else: result[key] = defaults.get(key) - stored = await self.get_setting(self.NODE_WIDE_GROUP_ID, - self.SETTING_STUN_SERVERS) - if stored is not None: - try: - result["stun_servers"] = _json.loads(stored) - except (ValueError, TypeError): - result["stun_servers"] = defaults.get("stun_servers", []) - else: - result["stun_servers"] = defaults.get("stun_servers", []) + for list_key, setting in [ + ("stun_servers", self.SETTING_STUN_SERVERS), + ("ice_interfaces", self.SETTING_ICE_INTERFACES), + ]: + stored = await self.get_setting(self.NODE_WIDE_GROUP_ID, setting) + if stored is not None: + try: + result[list_key] = _json.loads(stored) + except (ValueError, TypeError): + result[list_key] = defaults.get(list_key, []) + else: + result[list_key] = defaults.get(list_key, []) return result async def set_node_setting(self, key: str, value: str, -- cgit v1.2.3