summaryrefslogtreecommitdiffstats
path: root/packages
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-04 01:47:45 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-04 01:47:45 +0200
commit5022f2be149e40ac5bdd5fc362e926f3edcf44f0 (patch)
treed202f7a557e6d5ac5a6e8409ab1c8cadac39be05 /packages
parent2e819f7c5e0fb6352908ca88ff09b3465023767c (diff)
downloadmeshbay-5022f2be149e40ac5bdd5fc362e926f3edcf44f0.tar.gz
fix(client): resolve STUN hostnames in the main process
Chromium's P2P socket manager failed every STUN hostname with ERR_NAME_NOT_RESOLVED in a restricted-resolver environment (a Windows KVM guest), even though its own general network stack, the OS resolver and Node's resolver all resolved the same names -- and mapping the names to IPs with --host-resolver-rules changed nothing, so it is not ordinary resolution. WebRTC was left with no server-reflexive candidate. The desktop client now resolves the STUN hostnames in the main process (`ice:resolve-stun`, Node's dns.resolve4) and hands `transport.js` the IP form; a name that will not resolve (the decommissioned Mozilla host) is dropped. In a browser there is no `meshbay` bridge and the hostnames are used unchanged -- a browser resolves them fine, so that path is untouched. Falls back to the hostname form if the bridge call throws. Also, scoped to win32: disable WebRtcHideLocalIpsWithMdns, so the client publishes its real local IP instead of a `.local` name the node's ICE stack cannot resolve across the KVM bridge. Changes nothing on Linux/macOS. Test-env workaround, revisit before release. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Diffstat (limited to 'packages')
-rw-r--r--packages/meshbay-client/src/main.js28
-rw-r--r--packages/meshbay-client/src/preload.js6
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/transport.js39
3 files changed, 65 insertions, 8 deletions
diff --git a/packages/meshbay-client/src/main.js b/packages/meshbay-client/src/main.js
index 2a97246..9c27cfe 100644
--- a/packages/meshbay-client/src/main.js
+++ b/packages/meshbay-client/src/main.js
@@ -40,6 +40,17 @@ const { pathToFileURL } = require('node:url');
// runs (`electron .`) consistent with it.
app.commandLine.appendSwitch('class', 'MeshBay');
+// TEST-ENV WORKAROUND (Windows libvirt/KVM guest) — REVISIT BEFORE RELEASE.
+// In that guest Chromium hides the host candidate behind a random `.local`
+// mDNS name that the node's ICE stack cannot resolve across the KVM bridge,
+// so the one working candidate pair is present on some attempts and missing
+// on others (60 s ICE timeouts, "2nd connection hangs"). Publishing the real
+// local IP removes the dependency. Scoped to win32 so it changes nothing on
+// Linux/macOS, where mDNS concealment works and should stay on.
+if (process.platform === 'win32') {
+ app.commandLine.appendSwitch('disable-features', 'WebRtcHideLocalIpsWithMdns');
+}
+
const UI_DIR = path.join(__dirname, '..', 'ui');
const SCHEME = 'app';
@@ -497,6 +508,23 @@ function registerBridge() {
};
});
+ ipcMain.handle('ice:resolve-stun', async (_e, urls) => {
+ const dns = require('node:dns').promises;
+ const list = Array.isArray(urls) ? urls : [];
+ const out = [];
+ for (const u of list) {
+ const m = /^(stuns?):(\[?[^\]]+\]?|[^:]+):(\d+)$/.exec(String(u));
+ if (!m) { out.push(String(u)); continue; }
+ const [, scheme, host, port] = m;
+ if (/^[\d.]+$/.test(host) || host.includes(':')) { out.push(String(u)); continue; }
+ try {
+ const [ip] = await dns.resolve4(host);
+ if (ip) out.push(`${scheme}:${ip}:${port}`);
+ } catch { /* unresolvable (e.g. a decommissioned host) — drop it */ }
+ }
+ return out;
+ });
+
ipcMain.handle('hub:probe', async (_e, url) => {
const target = String(url || config.hubBase || '').replace(/\/+$/, '');
if (!target) return null;
diff --git a/packages/meshbay-client/src/preload.js b/packages/meshbay-client/src/preload.js
index 7ffdf66..96d6790 100644
--- a/packages/meshbay-client/src/preload.js
+++ b/packages/meshbay-client/src/preload.js
@@ -47,6 +47,12 @@ contextBridge.exposeInMainWorld('meshbay', {
// which CORS refuses and which is not a credential anyway.
fetch: (url, init) => ipcRenderer.invoke('hub:fetch', url, init),
+ // Resolve STUN `stun:host:port` URLs to `stun:ip:port` using Node's resolver.
+ // Chromium's P2P socket manager fails STUN hostnames outright in some
+ // restricted-resolver environments; the renderer cannot do DNS, so it asks
+ // here. Unresolvable entries are dropped.
+ resolveStun: (urls) => ipcRenderer.invoke('ice:resolve-stun', urls),
+
// The device's hub key. Generated, held and used entirely in the main
// process: the interface asks for a signature and never sees a key, because
// it is the part of this application that parses hostile input.
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/transport.js b/packages/meshbay-hub/src/meshbay_hub/static/transport.js
index 329baf1..809bb83 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/transport.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/transport.js
@@ -136,6 +136,36 @@ window.MeshBayTrace = {
clear() { try { localStorage.removeItem(TRACE_LOG_KEY); } catch { /* ignore */ } },
};
+// STUN, two providers deep. On the desktop client the hostnames are resolved in
+// the main process (Node's resolver) and handed back as IPs: Chromium's P2P
+// socket manager fails every STUN hostname with ERR_NAME_NOT_RESOLVED in some
+// restricted-resolver environments (a libvirt/KVM guest was where this surfaced)
+// even though every other resolver on the box works. In a browser there is no
+// `meshbay` bridge and the hostnames are used directly — a browser resolves them
+// fine. Resolved once per run; a provider changing IPs is picked up on restart.
+const STUN_URLS = [
+ 'stun:stun.l.google.com:19302',
+ 'stun:stun1.l.google.com:19302',
+ 'stun:stun.cloudflare.com:3478',
+ 'stun:stun.services.mozilla.com:3478',
+];
+let _iceServersPromise = null;
+function iceServers() {
+ if (!_iceServersPromise) {
+ _iceServersPromise = (async () => {
+ let urls = STUN_URLS;
+ if (window.meshbay && typeof window.meshbay.resolveStun === 'function') {
+ try {
+ const r = await window.meshbay.resolveStun(STUN_URLS);
+ if (Array.isArray(r) && r.length) urls = r;
+ } catch { /* keep the hostname form */ }
+ }
+ return urls.map((u) => ({ urls: u }));
+ })();
+ }
+ return _iceServersPromise;
+}
+
function _showTraceView() {
{
const renderTraceView = () => {
@@ -355,14 +385,7 @@ class MeshBayTransport {
this._newNodeBundle = null;
this._newNodeBundleRecovery = null;
this._joinError = null;
- this._pc = new RTCPeerConnection({
- iceServers: [
- { urls: 'stun:stun.l.google.com:19302' },
- { urls: 'stun:stun1.l.google.com:19302' },
- { urls: 'stun:stun.cloudflare.com:3478' },
- { urls: 'stun:stun.services.mozilla.com:3478' },
- ],
- });
+ this._pc = new RTCPeerConnection({ iceServers: await iceServers() });
this._channel = this._pc.createDataChannel('mnp', { ordered: true });
this._channel.binaryType = 'arraybuffer';