From d04b915580c8ba05beb0943a6fab9b04e12294e4 Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Fri, 4 Sep 2026 14:51:08 +0200 Subject: feat(packaging): stamp the frozen node exe, document Windows networking MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit meshbay-node.spec now builds a VSVersionInfo (ProductName "MeshBay Node", version read from the installed package so it tracks pyproject) and passes it to EXE(version=...). The Windows Firewall prompt, Task Manager and the file's Properties then show "MeshBay Node" instead of a bare exe name — the prompt the operator has to answer on first run. packaging/win/README.md gains a Networking section: the first-run firewall prompt (allow Private AND Public — a VM adapter is Public), that a flat LAN needs nothing else, what a routed/multi-subnet LAN additionally needs, the libvirt-NAT caveat, and a pointer to the packaged Linux firewall profiles. test_packaging_win.py covers the version resource. Co-Authored-By: Claude Sonnet 5 --- packages/meshbay-node/tests/test_packaging_win.py | 13 +++++++ packaging/win/README.md | 41 +++++++++++++++++++++++ packaging/win/meshbay-node.spec | 39 +++++++++++++++++++++ 3 files changed, 93 insertions(+) diff --git a/packages/meshbay-node/tests/test_packaging_win.py b/packages/meshbay-node/tests/test_packaging_win.py index 8c8e272..77fb1e5 100644 --- a/packages/meshbay-node/tests/test_packaging_win.py +++ b/packages/meshbay-node/tests/test_packaging_win.py @@ -113,6 +113,19 @@ def test_the_spec_pulls_in_the_awkward_dependencies_whole(): f"{pkg} dropped from the PyInstaller spec's collect list") +def test_the_frozen_exe_carries_a_version_resource(): + """ + Without it the Windows Firewall prompt, Task Manager and Properties show a + bare "meshbay-node". The version is read from the installed package so it + tracks pyproject rather than being a second copy to update. + """ + spec = (WIN / "meshbay-node.spec").read_text(encoding="utf-8") + assert "VSVersionInfo" in spec + assert "version=_version_info" in spec, "EXE() is not given the version resource" + assert 'StringStruct("ProductName", "MeshBay Node")' in spec + assert '_pkg_version("meshbay-node")' in spec, "version is hardcoded, not read from the package" + + # ── the artifact never gets committed ────────────────────────────────────── def test_the_node_runtime_output_is_gitignored(): diff --git a/packaging/win/README.md b/packaging/win/README.md index 0ba18ca..8c29785 100644 --- a/packaging/win/README.md +++ b/packaging/win/README.md @@ -80,6 +80,47 @@ embeddable zip would need pip to make a `meshbay-node.exe` wrapper, and that wrapper bakes in an **absolute** interpreter path — it stops working the moment the tree is installed somewhere other than where it was built. +## Networking (running the node) + +The node's admin API is loopback-only, but its **transport is not**. WebRTC +binds an ephemeral UDP port per connection and — because browsers/Electron +publish their host candidate as an unresolvable `.local` mDNS name that +`aioice` discards — the browser always dials the node, never the reverse. So the +node has to accept unsolicited inbound UDP from its peers. + +**Windows Defender Firewall.** On the daemon's first run Windows pops a prompt +for `meshbay-node.exe`. Tick **both Private and Public** — a libvirt/VM adapter, +and sometimes a plain Ethernet one, registers as Public, and a Private-only rule +then silently drops every peer. The installer cannot pre-create this rule (it is +per-user and never elevates); the prompt is the mechanism. If you dismissed it, +add the rule by hand: *Windows Defender Firewall → Advanced → Inbound Rules → +New Rule → Program →* the bundled `…\resources\node-runtime\meshbay-node.exe` *→ +Allow → all profiles*. + +**A flat LAN needs nothing else.** The node offers a routable `192.168.x.y` host +candidate and browsers on the same subnet connect straight to it — same as the +residential-NAT cases that work without TURN. + +**A routed or multi-subnet LAN** (peers on `192.168.1.x` reaching a node on +`192.168.2.x`, or a VM) additionally needs: + +- routing between the subnets (the two ends must have a path to each other's + host-candidate address); +- inbound UDP on the ephemeral range allowed for `meshbay-node.exe` **and** for + any Linux host in the path — see the packaged `packaging/firewall/` profiles + and the "inbound WebRTC" section of `docs/PACKAGING-GUIDE.md`; +- `MESHBAY_WEBRTC_EXPOSE_LOCAL_IPS` is not relevant to the node — that switch is + the *client's*, and it is already on by default (`src/main.js`). + +**libvirt NAT is the awkward case.** A guest on the default NAT network is +reachable from its own hypervisor but not from other LAN machines, and its only +host candidate is the `192.168.122.x`/`192.168.200.x` address no third machine +can route to. Give the guest bridged (or macvtap) networking so it gets a real +LAN address, or run the node on the host. The node log now prints the host +addresses it offered — `WebRTC answer ready for peer=… (… host: 192.168.200.173, +1 srflx)` — so "did the node even offer something routable" is answerable from +the journal. + ## Dependency surface that needs watching `meshbay-node.spec` pulls the awkward packages in whole (`collect_all`) because diff --git a/packaging/win/meshbay-node.spec b/packaging/win/meshbay-node.spec index f1f994d..324d8c1 100644 --- a/packaging/win/meshbay-node.spec +++ b/packaging/win/meshbay-node.spec @@ -12,6 +12,44 @@ # frozen run raises ModuleNotFoundError — that is the expected way this list grows. from PyInstaller.utils.hooks import collect_all, copy_metadata +from PyInstaller.utils.win32.versioninfo import ( + FixedFileInfo, + StringFileInfo, + StringStruct, + StringTable, + VarFileInfo, + VarStruct, + VSVersionInfo, +) + +# Version resource stamped into meshbay-node.exe so the Windows Firewall prompt, +# Task Manager and the file's Properties read "MeshBay Node" rather than a bare +# exe name. Read from the installed package so it cannot drift from pyproject. +try: + from importlib.metadata import version as _pkg_version + + _ver = _pkg_version("meshbay-node") +except Exception: + _ver = "0.0.0" +_vtuple = tuple( + int(p) for p in (_ver.split("+")[0].split(".") + ["0", "0", "0", "0"])[:4] +) + +_version_info = VSVersionInfo( + ffi=FixedFileInfo(filevers=_vtuple, prodvers=_vtuple), + kids=[ + StringFileInfo([StringTable("040904B0", [ + StringStruct("CompanyName", "MeshBay"), + StringStruct("FileDescription", "MeshBay Node"), + StringStruct("FileVersion", _ver), + StringStruct("InternalName", "meshbay-node"), + StringStruct("OriginalFilename", "meshbay-node.exe"), + StringStruct("ProductName", "MeshBay Node"), + StringStruct("ProductVersion", _ver), + ])]), + VarFileInfo([VarStruct("Translation", [0x0409, 1200])]), + ], +) # Packages taken in full: data files + shared libs + every submodule. COLLECT_ALL = [ @@ -114,6 +152,7 @@ exe = EXE( codesign_identity=None, entitlements_file=None, icon="../../packages/meshbay-client/build/icon.ico", + version=_version_info, ) coll = COLLECT( exe, -- cgit v1.2.3