diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-09-04 14:10:16 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-09-04 14:10:16 +0200 |
| commit | 40abf0979f93771ccfb58eecb8a6fcc5863ec604 (patch) | |
| tree | 9fe1c57408125a306831d09f243df1d75cf64ffd | |
| parent | 8288714853952aca6b3511268b9d772f1b7f489f (diff) | |
| download | meshbay-40abf0979f93771ccfb58eecb8a6fcc5863ec604.tar.gz | |
feat(packaging): ship a node firewall profile for inbound WebRTC
The packages carried a profile for LAN casting and none for the node's own
peer traffic, on the reasoning that the node exposes only a loopback admin
API. That is true of its administration surface and false of its transport.
WebRTC binds an ephemeral UDP port per connection, so there is no fixed port
to open, and a connection succeeds only if one side can initiate. Browsers
publish their host candidate as an mDNS `<uuid>.local` name, which aioice
cannot resolve on any platform and discards — so the node can never call a
browser back, and the browser must call the node. A node that refuses
unsolicited inbound UDP is unreachable from every browser on its own LAN,
leaving reflexive candidates, which fail whenever both peers share one public
IP and the router will not hairpin.
Hit twice in one session on two different hosts: a firewalld zone narrowed to
mdns + 19550-19553/tcp, and a ufw host with default deny-incoming. Both
presented as "the app cannot connect", neither as a firewall message.
Passive like the cast profile: packaged, not activated. The guide says to
scope it to a LAN zone or source, including the libvirt case, where traffic
from a guest to its own hypervisor is not masqueraded and so must be scoped
to the guest subnet rather than the LAN.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtfG7z6wHWj8RKHCvxQtY1
| -rw-r--r-- | docs/PACKAGING-GUIDE.md | 30 | ||||
| -rwxr-xr-x | packaging/build/build-node.sh | 16 | ||||
| -rw-r--r-- | packaging/firewall/firewalld/meshbay-node.xml | 6 | ||||
| -rw-r--r-- | packaging/firewall/ufw/meshbay | 5 | ||||
| -rw-r--r-- | packaging/rpm/meshbay-node.spec | 1 |
5 files changed, 54 insertions, 4 deletions
diff --git a/docs/PACKAGING-GUIDE.md b/docs/PACKAGING-GUIDE.md index 5a5c388..56504d4 100644 --- a/docs/PACKAGING-GUIDE.md +++ b/docs/PACKAGING-GUIDE.md @@ -202,6 +202,36 @@ sudo firewall-cmd --reload sudo ufw allow "MeshBay Cast" ``` +### Peer connections (node) + +Opens inbound UDP 1024-65535. **Scope it to the LAN** — apply the firewalld +service to the zone holding the LAN interface, and give the ufw rule a +`from`. It does not belong in an internet-facing zone. + +```bash +# Fedora (firewalld) — replace FedoraWorkstation with your LAN zone +sudo firewall-cmd --permanent --zone=FedoraWorkstation --add-service=meshbay-node +sudo firewall-cmd --reload + +# Ubuntu (ufw) +sudo ufw allow from 192.168.1.0/24 app "MeshBay Node" + +# a libvirt guest reaching the node on its own hypervisor: scope to the guest +# subnet, since traffic to the host's own address is not masqueraded +sudo ufw allow in on virbr0 from 192.168.200.0/24 app "MeshBay Node" +``` + +**Why a node needs this.** WebRTC binds an ephemeral UDP port per connection, +so there is no fixed port to open. A connection succeeds if *either* side can +initiate. Browsers publish their host candidate as an mDNS `<uuid>.local` +name, which `aioice` cannot resolve on any platform and discards — so the node +can never call a browser back, and the browser must call the node. A node that +refuses unsolicited inbound UDP is unreachable from every browser on its own +LAN, and falls back to reflexive candidates, which fail whenever both peers +share one public IP and the router will not hairpin. + +The node's administration surface is unaffected: loopback only, see below. + The node's own administration surface is a loopback API (127.0.0.1 only, per-run token) reached by the CLI and the desktop client's Node page. It is never network-exposed and ships no firewall profile. diff --git a/packaging/build/build-node.sh b/packaging/build/build-node.sh index 9ad4968..bd81096 100755 --- a/packaging/build/build-node.sh +++ b/packaging/build/build-node.sh @@ -75,12 +75,20 @@ cp "$REPO/packaging/systemd/meshbay-node.service" \ cp "$REPO/packaging/systemd/meshbay-node-user.service" \ "$ROOT/usr/lib/systemd/user/meshbay-node.service" -# --- Firewall profile --------------------------------------------------------- -# UFW (Ubuntu/Debian) — carries the "MeshBay Cast" LAN-casting profile only. -# The node's own admin surface is a loopback API (127.0.0.1, token-gated) and -# is never firewall-exposed, so it ships no profile. +# --- Firewall profiles -------------------------------------------------------- +# The node's admin surface is a loopback API (127.0.0.1, token-gated) and is +# never firewall-exposed. Its *peer* traffic is: WebRTC binds an ephemeral UDP +# port per connection, and a peer that publishes an unroutable address — every +# browser does, as an mDNS .local name aioice cannot resolve — can only be +# reached if it calls the node. A node refusing unsolicited inbound UDP is +# therefore unreachable from browsers on its own LAN. Both profiles are passive: +# packaged, not activated, and meant to be scoped to a LAN zone/source. mkdir -p "$ROOT/etc/ufw/applications.d" cp "$REPO/packaging/firewall/ufw/meshbay" \ "$ROOT/etc/ufw/applications.d/" +mkdir -p "$ROOT/usr/lib/firewalld/services" +cp "$REPO/packaging/firewall/firewalld/meshbay-node.xml" \ + "$ROOT/usr/lib/firewalld/services/" + echo "==> meshbay-node staging ready at $ROOT" diff --git a/packaging/firewall/firewalld/meshbay-node.xml b/packaging/firewall/firewalld/meshbay-node.xml new file mode 100644 index 0000000..2f863ec --- /dev/null +++ b/packaging/firewall/firewalld/meshbay-node.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="utf-8"?> +<service> + <short>MeshBay Node</short> + <description>Inbound WebRTC/ICE for a MeshBay node. Peers connect over a DataChannel negotiated through the hub; there is no fixed listening port, because aiortc binds an ephemeral UDP port per connection. A peer that cannot reach the node directly can only be reached if the node initiates, which requires the peer to publish a routable address — browsers do not, they publish an mDNS .local name the node cannot resolve. So a node that refuses unsolicited inbound UDP is unreachable from any browser it cannot itself call back. Apply this to the zone holding the LAN interface only; it does not belong in an internet-facing zone. The node's administration surface is unaffected: it is a loopback API on 127.0.0.1 and is never exposed here.</description> + <port protocol="udp" port="1024-65535"/> +</service> diff --git a/packaging/firewall/ufw/meshbay b/packaging/firewall/ufw/meshbay index 732e5d8..b87afdb 100644 --- a/packaging/firewall/ufw/meshbay +++ b/packaging/firewall/ufw/meshbay @@ -2,3 +2,8 @@ title=MeshBay Chromecast relay description=HTTP relay for casting decrypted video to LAN devices ports=19550:19553/tcp|5353/udp + +[MeshBay Node] +title=MeshBay node WebRTC/ICE +description=Inbound UDP for peer connections; scope it to your LAN with "from" +ports=1024:65535/udp diff --git a/packaging/rpm/meshbay-node.spec b/packaging/rpm/meshbay-node.spec index 2bebb26..7910a83 100644 --- a/packaging/rpm/meshbay-node.spec +++ b/packaging/rpm/meshbay-node.spec @@ -43,6 +43,7 @@ fi %{_unitdir}/meshbay-node@.service %{_userunitdir}/meshbay-node.service /etc/ufw/applications.d/meshbay +/usr/lib/firewalld/services/meshbay-node.xml %changelog * Sun Aug 31 2026 MeshBay Team <team@meshbay.org> - %{version}-1 |