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
|
- **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.
|
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.
|
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
|
- **CAB embedded in the MSI** — the installer is a single self-contained
|
||||||
file again; distributing only the MSI no longer fails with error 1302.
|
file again; distributing only the MSI no longer fails with error 1302.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,8 @@
|
||||||
-->
|
-->
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="WixToolset.UI.wixext" Version="5.0.2" />
|
<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>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
@ -224,5 +224,62 @@
|
||||||
</Component>
|
</Component>
|
||||||
</ComponentGroup>
|
</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>
|
</Package>
|
||||||
</Wix>
|
</Wix>
|
||||||
|
|
@ -97,11 +97,17 @@ public sealed partial class ControlSurfaceServer : IAsyncDisposable
|
||||||
/// "headless show machine + thin client controller" setups. When false
|
/// "headless show machine + thin client controller" setups. When false
|
||||||
/// (default), binds to <c>127.0.0.1</c> only.
|
/// (default), binds to <c>127.0.0.1</c> only.
|
||||||
///
|
///
|
||||||
/// LAN binding requires either running Dragon-ISO as Administrator OR a
|
/// Binding requires either running Dragon-ISO as Administrator OR a
|
||||||
/// one-time URL ACL reservation at the OS level:
|
/// one-time URL ACL reservation at the OS level for the prefix actually
|
||||||
/// <code>netsh http add urlacl url=http://+:9755/ user=Everyone</code>
|
/// being bound (the installer reserves the loopback default,
|
||||||
/// If neither is in place the listener throws AccessDeniedException
|
/// <c>http://127.0.0.1:9755/</c>):
|
||||||
/// which we catch and surface as a logger warning.
|
/// <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>
|
/// </summary>
|
||||||
public void Start(int port, bool bindToLan = false)
|
public void Start(int port, bool bindToLan = false)
|
||||||
{
|
{
|
||||||
|
|
@ -123,9 +129,11 @@ public sealed partial class ControlSurfaceServer : IAsyncDisposable
|
||||||
{
|
{
|
||||||
_logger?.LogWarning(ex,
|
_logger?.LogWarning(ex,
|
||||||
"Could not start control surface on {Prefix}. " +
|
"Could not start control surface on {Prefix}. " +
|
||||||
"If binding to LAN, run as Administrator once OR run: " +
|
"Run as Administrator once OR reserve the URL namespace: " +
|
||||||
"netsh http add urlacl url=http://+:{Port}/ user=Everyone",
|
"netsh http add urlacl url={UrlAclUrl} sddl=D:(A;;GX;;;WD) " +
|
||||||
prefix, port);
|
"(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;
|
_listener = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue