diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-08-30 22:17:30 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-08-30 22:17:30 +0200 |
| commit | 07e6e4271ea3a45e9cd364eb6ec05801a653d9e8 (patch) | |
| tree | 5d2dea4251ee2ecea32aeef80d09167d08085dd0 /packages/meshbay-node/src/meshbay_node/ops.py | |
| parent | bf56b60b95c5413d592f0199320d637978792756 (diff) | |
| download | meshbay-07e6e4271ea3a45e9cd364eb6ec05801a653d9e8.tar.gz | |
feat: configurable STUN server fallbacks for WebRTC ICE
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 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-node/src/meshbay_node/ops.py')
| -rw-r--r-- | packages/meshbay-node/src/meshbay_node/ops.py | 55 |
1 files changed, 46 insertions, 9 deletions
diff --git a/packages/meshbay-node/src/meshbay_node/ops.py b/packages/meshbay-node/src/meshbay_node/ops.py index d1314f0..2ab6753 100644 --- a/packages/meshbay-node/src/meshbay_node/ops.py +++ b/packages/meshbay-node/src/meshbay_node/ops.py @@ -332,6 +332,7 @@ async def list_groups(state: dict) -> dict: members = await roster.list_members() has_operator = any(m["role"] == "operator" and m["status"] == "active" for m in members) + from meshbay_node.config import DEFAULT_STUN_SERVERS nd = config.node if config else None defaults = { "invite_ttl_hours": nd.invite_ttl_hours if nd else 168, @@ -339,6 +340,7 @@ async def list_groups(state: dict) -> dict: "device_request_ttl_minutes": nd.device_request_ttl_minutes if nd else 60, "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), } if roster: settings = await roster.node_settings(defaults) @@ -511,6 +513,14 @@ def _update_node_toml(conf_path: Path, updates: dict) -> None: node_start = len(lines) - 1 node_end = len(lines) + def _format_value(key, value): + if isinstance(value, bool): + return f"{key} = {'true' if value else 'false'}" + if isinstance(value, list): + items = ", ".join(f'"{v}"' for v in value) + return f"{key} = [{items}]" + return f"{key} = {value}" + remaining = dict(updates) for i in range(node_start + 1, node_end): for key in list(remaining): @@ -518,18 +528,11 @@ def _update_node_toml(conf_path: Path, updates: dict) -> None: r'^(\s*#?\s*)' + re.escape(key) + r'\s*=\s*.*$') if pattern.match(lines[i]): value = remaining.pop(key) - if isinstance(value, bool): - lines[i] = f"{key} = {'true' if value else 'false'}" - else: - lines[i] = f"{key} = {value}" + lines[i] = _format_value(key, value) break for key, value in remaining.items(): - if isinstance(value, bool): - insert_line = f"{key} = {'true' if value else 'false'}" - else: - insert_line = f"{key} = {value}" - lines.insert(node_end, insert_line) + lines.insert(node_end, _format_value(key, value)) node_end += 1 conf_path.write_text("\n".join(lines)) @@ -783,6 +786,24 @@ async def set_member_upload(state: dict, group_id: str, allowed: bool) -> dict: # ── Node settings ──────────────────────────────────────────────────────────── +async def get_node_settings(state: dict) -> dict: + """Return current effective node settings.""" + from meshbay_node.config import DEFAULT_STUN_SERVERS + roster = _roster(state) + config = _config(state) + nd = config.node + defaults = { + "invite_ttl_hours": nd.invite_ttl_hours, + "pair_ttl_hours": nd.pair_ttl_hours, + "device_request_ttl_minutes": nd.device_request_ttl_minutes, + "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), + } + if roster: + return await roster.node_settings(defaults) + return defaults + async def set_node_settings(state: dict, settings: dict) -> dict: """Update node-level daemon settings. Writes to both roster.db and node.toml.""" roster = _roster(state) @@ -796,6 +817,7 @@ 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), } set_by = state.get("node_user_id", "") @@ -819,6 +841,17 @@ 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": + 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:)") + setattr(nd, key, v) + await roster.set_node_setting(setting_key, _json.dumps(v), set_by) + updated[key] = v if updated: _update_node_toml(conf_path, updated) @@ -826,6 +859,10 @@ async def set_node_settings(state: dict, settings: dict) -> dict: webrtc = state.get("webrtc") if webrtc and hasattr(webrtc, '_stream_sem'): webrtc._stream_sem = asyncio.Semaphore(updated["max_concurrent_streams"]) + if "stun_servers" in updated: + webrtc = state.get("webrtc") + if webrtc and hasattr(webrtc, '_stun'): + webrtc._stun = updated["stun_servers"] log.info("Node settings updated: %s", updated) return {"updated": updated} |