aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-04 16:08:06 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-04 16:08:06 +0200
commita4aabd1d33770d6199a8cb7bc87f639668617bc9 (patch)
treef1c280790ae4791b587c879b2922567a44290a54
parent7ff675fdc0866ca40bdb703bc95c0cc60e3edd87 (diff)
downloadmeshbay-a4aabd1d33770d6199a8cb7bc87f639668617bc9.tar.gz
fix(packaging): re-running setup no longer re-prompts for firewall access
Every run of customInstall showed the "Allow MeshBay through Windows Firewall?" question and, on Yes, a fresh UAC prompt -- an upgrade or repair install would ask again even with all four rules already in place. customInstall now checks first: firewall.ps1 check, unelevated (Get-NetFirewallRule needs no admin, only New/Remove do), exits 0 if every rule already exists. Only a nonzero result reaches the MessageBox and the elevated add. A second run of setup on an already-configured machine now asks nothing. Verified unelevated: check exits 1 and logs which rules are missing on a machine with none of them (the fresh-install case); electron-builder compiles the nsExec::Exec / Pop $0 / ${If} wiring. Node suite 837 pass / 25 skip. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
-rw-r--r--packages/meshbay-client/build/installer.nsh34
-rw-r--r--packages/meshbay-node/tests/test_packaging_win.py18
-rw-r--r--packaging/win/README.md8
-rw-r--r--packaging/win/firewall.ps122
4 files changed, 63 insertions, 19 deletions
diff --git a/packages/meshbay-client/build/installer.nsh b/packages/meshbay-client/build/installer.nsh
index 67d103d..d2307c8 100644
--- a/packages/meshbay-client/build/installer.nsh
+++ b/packages/meshbay-client/build/installer.nsh
@@ -40,19 +40,27 @@
WriteRegExpandStr HKCU "Environment" "Path" "$1"
SendMessage ${HWND_BROADCAST} ${WM_WININICHANGE} 0 "STR:Environment" /TIMEOUT=5000
- ; Firewall. MeshBay.exe and meshbay-node.exe each bind UDP sockets for WebRTC,
- ; and Windows prompts "Allow access" the first time each does. A per-user
- ; installer cannot pre-create a firewall rule (that needs admin), so offer to
- ; run one elevated helper now: one UAC prompt instead of two dialogs mid-use.
- ; firewall.ps1 is idempotent and does nothing if the exes are missing.
- ${IfNot} ${Silent}
- MessageBox MB_YESNO|MB_ICONQUESTION \
- "Allow MeshBay through Windows Firewall now?$\n$\nMeshBay connects to other devices on your local network. Choosing Yes adds the rules in one step (Windows will ask for administrator confirmation). Choosing No is fine too -- Windows will ask you to allow access the first time MeshBay connects." \
- /SD IDYES IDNO mb_skip_fw
- ExecShellWait "runas" "${MB_PWSH}" \
- '-NoProfile -ExecutionPolicy Bypass -File "$INSTDIR\resources\firewall.ps1" add' \
- SW_HIDE
- mb_skip_fw:
+ ; Firewall. MeshBay.exe / meshbay-node.exe (WebRTC) and MeshBay.exe again
+ ; (LAN cast) each need an inbound allow, and Windows prompts "Allow access"
+ ; the first time each does. A per-user installer cannot pre-create a
+ ; firewall rule (that needs admin), so offer one elevated helper: one UAC
+ ; prompt instead of up to four dialogs spread across first use.
+ ;
+ ; Checked first, UNELEVATED (Get-NetFirewallRule needs no admin, only
+ ; New/Remove do) -- so re-running setup with the rules already in place
+ ; asks nothing and never pops UAC again.
+ nsExec::Exec '"${MB_PWSH}" -NoProfile -ExecutionPolicy Bypass -File "$INSTDIR\resources\firewall.ps1" check'
+ Pop $0
+ ${If} $0 != 0
+ ${IfNot} ${Silent}
+ MessageBox MB_YESNO|MB_ICONQUESTION \
+ "Allow MeshBay through Windows Firewall now?$\n$\nMeshBay connects to other devices on your local network. Choosing Yes adds the rules in one step (Windows will ask for administrator confirmation). Choosing No is fine too -- Windows will ask you to allow access the first time MeshBay connects." \
+ /SD IDYES IDNO mb_skip_fw
+ ExecShellWait "runas" "${MB_PWSH}" \
+ '-NoProfile -ExecutionPolicy Bypass -File "$INSTDIR\resources\firewall.ps1" add' \
+ SW_HIDE
+ mb_skip_fw:
+ ${EndIf}
${EndIf}
!macroend
diff --git a/packages/meshbay-node/tests/test_packaging_win.py b/packages/meshbay-node/tests/test_packaging_win.py
index ba3568c..20a2505 100644
--- a/packages/meshbay-node/tests/test_packaging_win.py
+++ b/packages/meshbay-node/tests/test_packaging_win.py
@@ -199,6 +199,24 @@ def test_the_installer_offers_one_elevated_firewall_step_instead_of_two_dialogs(
assert 'firewall.ps1" add' in install
+def test_reinstalling_with_the_rules_already_in_place_asks_nothing():
+ """
+ Get-NetFirewallRule needs no admin, only New/Remove do — so customInstall
+ checks first, unelevated, and only reaches the MessageBox (and therefore
+ the UAC prompt) when something is actually missing. Without this, running
+ setup a second time — an upgrade, a repair install — would re-ask the
+ question and re-trigger UAC even though nothing needs to change.
+ """
+ nsh = NSH.read_text(encoding="utf-8")
+ install = _macro_body(nsh, "customInstall")
+
+ check_line = 'firewall.ps1" check'
+ assert check_line in install
+ # The check must run, and be evaluated, before the MessageBox — not after.
+ assert install.index(check_line) < install.index("MessageBox MB_YESNO")
+ assert "Pop $0" in install and "${If} $0 != 0" in install
+
+
def test_the_uninstaller_offers_to_remove_the_firewall_rules_default_no():
"""Opt-in on the way out too, and defaulting to No: a stale allow-rule
for a deleted exe is inert, so this should not nag."""
diff --git a/packaging/win/README.md b/packaging/win/README.md
index 5130e60..01c8d1a 100644
--- a/packaging/win/README.md
+++ b/packaging/win/README.md
@@ -109,8 +109,12 @@ Linux packaging solves with a broad `1024-65535/udp` range
since those two are fixed and known — matching
`packaging/firewall/*/meshbay-cast.xml` exactly.
-Say yes and every prompt you'd otherwise hit mid-use — connecting, or the
-first cast — is gone. Say no, or the UAC prompt is dismissed, and Windows
+Setup checks first, **unelevated** (`firewall.ps1 check` — reading rules needs
+no admin, only creating them does), so running the installer again — an
+upgrade, a repair install — asks nothing and never pops UAC a second time once
+the rules are in place. Say yes the first time and every prompt you'd
+otherwise hit mid-use — connecting, or the first cast — is gone. Say no, or
+the UAC prompt is dismissed, and Windows
falls back to its own **"Allow access"** dialog the first time each
program/port combination is used — tick **both Private and Public** then (a
libvirt/VM adapter, and sometimes a plain Ethernet one, registers as Public; a
diff --git a/packaging/win/firewall.ps1 b/packaging/win/firewall.ps1
index bf1b32b..9b3687a 100644
--- a/packaging/win/firewall.ps1
+++ b/packaging/win/firewall.ps1
@@ -29,20 +29,23 @@
Shipped as an extraResource at <install>\resources\firewall.ps1, so it
locates the two executables from its own path and takes no arguments
- beyond the action. Runs elevated and windowless, so it leaves a trace at
- %TEMP%\meshbay-firewall.log.
+ beyond the action. add/remove run elevated and windowless, leaving a
+ trace at %TEMP%\meshbay-firewall.log; check is a plain read (no admin
+ needed -- Get-NetFirewallRule does not require it, only New/Remove do)
+ that the installer runs first, unelevated, so re-running setup with the
+ rules already in place skips the prompt and the UAC entirely.
.PARAMETER Action
add (default) create/replace the rules
remove delete them
+ check exit 0 if all rules already exist, 1 otherwise -- no admin needed
#>
[CmdletBinding()]
param(
- [ValidateSet("add", "remove")]
+ [ValidateSet("add", "remove", "check")]
[string]$Action = "add"
)
-$ErrorActionPreference = "Stop"
$log = Join-Path $env:TEMP "meshbay-firewall.log"
"[{0}] {1}" -f (Get-Date -Format s), $Action | Add-Content $log
@@ -63,6 +66,17 @@ $rules = @(
@{ Name = "MeshBay Cast Discovery"; Path = $client; Protocol = "UDP"; LocalPort = "5353" }
)
+if ($Action -eq "check") {
+ $missing = $rules | Where-Object { -not (Get-NetFirewallRule -DisplayName $_.Name -ErrorAction SilentlyContinue) }
+ if ($missing) {
+ " check: missing $(($missing | ForEach-Object { $_.Name }) -join ', ')" | Add-Content $log
+ exit 1
+ }
+ " check: all rules present" | Add-Content $log
+ exit 0
+}
+
+$ErrorActionPreference = "Stop"
try {
foreach ($r in $rules) {
# Idempotent: clear any existing rule of this name first.