fix(control-surface): reserve loopback URL ACL at install time
The control surface uses HttpListener (http.sys), which denies prefix registration to standard users -- on every non-Administrator install the REST/WebSocket surface died at startup with Access is denied (5), taking the /ui phone panel and Companion integration with it. The MSI runs elevated, so it now reserves http://127.0.0.1:9755/ on install and deletes the reservation on uninstall. Deliberately NOT the strong wildcard: verified empirically that an idle http://+:9755/ RESERVATION makes http.sys answer 503 for every request to the loopback-bound listener without the request ever reaching the app. The in-app failure guidance suggested exactly that reservation; corrected to recommend the bound prefix and warn about the stale-wildcard trap. Verified on a clean install: urlacl present, GET / and /ui and /participants all 200 as a standard user; reservation removed on uninstall. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
e1b3339cc2
commit
daa4068784
4 changed files with 82 additions and 8 deletions
|
|
@ -27,6 +27,13 @@ Beta pre-release for the framerate and reliability work since 1.1.0.
|
|||
- **MSI ProductVersion now tracks the build version** — it was hardcoded to
|
||||
1.0.0.0, so in-place upgrades (MajorUpgrade) never saw a newer version.
|
||||
Upgrading from an earlier install may require a manual uninstall once.
|
||||
- **Control surface works out of the box for standard users** — the installer
|
||||
now reserves the http.sys URL namespace for `http://127.0.0.1:9755/`
|
||||
(removed again on uninstall). Previously the REST/WebSocket control surface
|
||||
failed with "Access is denied" on every non-Administrator launch. Note for
|
||||
LAN-reachable mode: it still needs its own `http://+:9755/` reservation
|
||||
(the app logs the exact command), and that reservation must be deleted when
|
||||
reverting to loopback-only — while idle it makes http.sys 503 every request.
|
||||
- **CAB embedded in the MSI** — the installer is a single self-contained
|
||||
file again; distributing only the MSI no longer fails with error 1302.
|
||||
|
||||
|
|
|
|||
|
|
@ -39,6 +39,8 @@
|
|||
-->
|
||||
<ItemGroup>
|
||||
<PackageReference Include="WixToolset.UI.wixext" Version="5.0.2" />
|
||||
<!-- WixQuietExec for the URL ACL custom actions (control surface, port 9755). -->
|
||||
<PackageReference Include="WixToolset.Util.wixext" Version="5.0.2" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -224,5 +224,62 @@
|
|||
</Component>
|
||||
</ComponentGroup>
|
||||
|
||||
<!--
|
||||
URL ACL reservation for the control surface (REST/WebSocket, default
|
||||
port 9755). The server uses HttpListener (http.sys), and http.sys
|
||||
denies prefix registration to non-elevated processes unless the URL
|
||||
namespace is reserved — without this, the control surface fails with
|
||||
"Access is denied (5)" on every standard-user launch and the operator
|
||||
has to run the app elevated or run netsh by hand. The MSI already runs
|
||||
elevated, so reserve the namespace here.
|
||||
|
||||
IMPORTANT — reserve ONLY the explicit loopback prefix, never
|
||||
http://+:9755/. Verified empirically 2026-07-06: an idle strong-
|
||||
wildcard RESERVATION poisons http.sys routing for the app's IP-bound
|
||||
registration — every request 503s inside http.sys without ever
|
||||
reaching the listener. The LAN-reachable mode (which binds
|
||||
http://+:9755/) remains an opt-in that needs its own reservation;
|
||||
the app logs the exact netsh command when that bind fails, and the
|
||||
operator must delete that + reservation again if they revert to
|
||||
loopback-only.
|
||||
|
||||
SDDL D:(A;;GX;;;WD) = allow GENERIC_EXECUTE (listen) to Everyone by
|
||||
SID, which survives non-English locales where "Everyone" isn't a
|
||||
valid account name. Operators who change the port in settings still
|
||||
need the app's documented netsh one-liner for the new port.
|
||||
Return="ignore": an already-existing reservation (repair/reinstall)
|
||||
exits non-zero and must not fail the install.
|
||||
-->
|
||||
<SetProperty Id="AddControlSurfaceUrlAcl"
|
||||
Value=""[System64Folder]netsh.exe" http add urlacl url=http://127.0.0.1:9755/ sddl=D:(A;;GX;;;WD)"
|
||||
Sequence="execute"
|
||||
Before="AddControlSurfaceUrlAcl" />
|
||||
<CustomAction Id="AddControlSurfaceUrlAcl"
|
||||
BinaryRef="Wix4UtilCA_X64"
|
||||
DllEntry="WixQuietExec64"
|
||||
Execute="deferred"
|
||||
Impersonate="no"
|
||||
Return="ignore" />
|
||||
|
||||
<SetProperty Id="RemoveControlSurfaceUrlAcl"
|
||||
Value=""[System64Folder]netsh.exe" http delete urlacl url=http://127.0.0.1:9755/"
|
||||
Sequence="execute"
|
||||
Before="RemoveControlSurfaceUrlAcl" />
|
||||
<CustomAction Id="RemoveControlSurfaceUrlAcl"
|
||||
BinaryRef="Wix4UtilCA_X64"
|
||||
DllEntry="WixQuietExec64"
|
||||
Execute="deferred"
|
||||
Impersonate="no"
|
||||
Return="ignore" />
|
||||
|
||||
<InstallExecuteSequence>
|
||||
<Custom Action="AddControlSurfaceUrlAcl"
|
||||
After="InstallFiles"
|
||||
Condition="NOT Installed OR REINSTALL" />
|
||||
<Custom Action="RemoveControlSurfaceUrlAcl"
|
||||
After="InstallInitialize"
|
||||
Condition="REMOVE="ALL"" />
|
||||
</InstallExecuteSequence>
|
||||
|
||||
</Package>
|
||||
</Wix>
|
||||
|
|
@ -97,11 +97,17 @@ public sealed partial class ControlSurfaceServer : IAsyncDisposable
|
|||
/// "headless show machine + thin client controller" setups. When false
|
||||
/// (default), binds to <c>127.0.0.1</c> only.
|
||||
///
|
||||
/// LAN binding requires either running Dragon-ISO as Administrator OR a
|
||||
/// one-time URL ACL reservation at the OS level:
|
||||
/// <code>netsh http add urlacl url=http://+:9755/ user=Everyone</code>
|
||||
/// If neither is in place the listener throws AccessDeniedException
|
||||
/// which we catch and surface as a logger warning.
|
||||
/// Binding requires either running Dragon-ISO as Administrator OR a
|
||||
/// one-time URL ACL reservation at the OS level for the prefix actually
|
||||
/// being bound (the installer reserves the loopback default,
|
||||
/// <c>http://127.0.0.1:9755/</c>):
|
||||
/// <code>netsh http add urlacl url=http://127.0.0.1:9755/ sddl=D:(A;;GX;;;WD)</code>
|
||||
/// or <c>url=http://+:9755/</c> for LAN mode. CAUTION: an idle
|
||||
/// <c>http://+:port/</c> reservation makes http.sys 503 every request to
|
||||
/// the loopback-bound listener — only add it while LAN mode is in use,
|
||||
/// and delete it when reverting to loopback-only. If no reservation is
|
||||
/// in place the listener throws AccessDeniedException which we catch and
|
||||
/// surface as a logger warning.
|
||||
/// </summary>
|
||||
public void Start(int port, bool bindToLan = false)
|
||||
{
|
||||
|
|
@ -123,9 +129,11 @@ public sealed partial class ControlSurfaceServer : IAsyncDisposable
|
|||
{
|
||||
_logger?.LogWarning(ex,
|
||||
"Could not start control surface on {Prefix}. " +
|
||||
"If binding to LAN, run as Administrator once OR run: " +
|
||||
"netsh http add urlacl url=http://+:{Port}/ user=Everyone",
|
||||
prefix, port);
|
||||
"Run as Administrator once OR reserve the URL namespace: " +
|
||||
"netsh http add urlacl url={UrlAclUrl} sddl=D:(A;;GX;;;WD) " +
|
||||
"(if switching back from LAN mode, also delete the stale " +
|
||||
"http://+:{Port}/ reservation — while idle it 503s the loopback listener)",
|
||||
prefix, prefix, port);
|
||||
_listener = null;
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue