Repository navigation
The firewall verbs name the rule from darling.json's port while the endpoint binds the store's #2414
Description
Found while working #2389, which fixed the diagnostic for the same split. This one is a functional consequence and deserves its own lane.
mcp.port / web.port are control-plane-authoritative once the store is seeded — the supervisor resolves config_service.mcp_port and binds Kestrel there. But every firewall surface derives the rule name and the port from darling.json:
DarlingCliCommands.ReconcileEndpointFirewallAsync(the firewall half of--enable-mcp/--disable-mcp/--enable-web/--disable-web) —McpFirewallRuleName(config.Mcp.Port),config.Mcp.Port--configure-firewall's plan builder — same two, for MCP and web alike
The runtime side does not. DarlingMcpHostService calls CheckMcpFirewallAsync(effectivePort, …), where effectivePort is the store's port.
The failure
Toggle the MCP port in the Viewer's Settings (5152 → 5199) on a box with an mcp.network block:
- the endpoint rebinds to 5199 within one poll interval, correctly, no restart
- the service then looks for
PerformanceMonitor Darling MCP (port 5199)and reports it missing - an admin runs
--configure-firewallas instructed, and it creates/keepsPerformanceMonitor Darling MCP (port 5152)for port 5152 — the wrong rule, from the stale file value, and it will keep creating that one however many times it is run - the exposed endpoint has no firewall path; LAN clients are blocked, loopback still works, so it reads as a client or CIDR problem
The stale rule for 5152 also stays open on a port nothing is listening on, which is the opposite of what the scoping is for.
Same shape for the web dashboard.
Why it survived
Nothing agrees or disagrees until someone changes the port in the store, which is rare — and when it happens the symptom (LAN clients can't connect, loopback can) points at the CIDR or the token, not at a rule name. #2389 now warns when the two ports disagree, so the precondition is at least visible, but the elevated verb still writes the wrong rule.
Options
- Give the firewall verbs the store's port.
ToggleEndpointAsyncalready builds an owner connection from the DPAPI credential a few lines earlier, so for--enable-mcp/--disable-mcpthe read is nearly free — resolve the effective port the same way the supervisor does and name the rule for it.--configure-firewallwould need the same connection added, and a defined answer for an unreachable store (fall back to the file value and say so, rather than silently). - Reconcile by prefix rather than by exact name — enumerate
PerformanceMonitor Darling MCP (port *)and remove the ones that do not match the effective port. Fixes the stale-rule half regardless of which port the verb was told about. - Refuse and explain when the file port and the store port disagree, pointing at the store value — cheapest, and consistent with the fail-closed posture elsewhere, but it leaves the operator to run something else.
What should decide
Whether --configure-firewall is allowed to require a reachable store. It runs elevated at install time, possibly before the store exists, so option 1 alone may not be available to it — which argues for 1 + 2 together (authoritative port when the store answers, prefix cleanup either way), or 3 as the floor.
Not a security hole: it fails closed (the port is unreachable rather than over-exposed), and the token + in-app CIDR are the boundary, not the firewall. It is a correctness and wasted-afternoon problem.
Activity
Merge pull request #2432 from erikdarlingdata/fix/2414-firewall-port-…
erikdarlingdata commentedon Aug 21, 2026
Fixed on
devby #2432. Both firewall surfaces now resolve their port throughDarlingHostBinding.ResolveEndpointToggle— the same call both supervisors bind on — rather than a second copy ofpublished?.Port ?? filePort. No new resolution path, which was the point: a second path is how these drifted apart.The store-unreachable case got two different answers, and the reasoning is the good part.
The four toggle verbs effectively refuse: they take the port back from a
RETURNING mcp_port/web_portclause on theconfig_serviceUPDATE they already perform — free, atomic with the toggle, under the same row lock, and doubling as the unseeded check. A store that cannot be written has already failed the verb with exit 1 before any firewall command is built, so those verbs structurally cannot open a port on a guessed number.--configure-firewallfalls back, loudly, because it cannot require a store:install-darling.ps1calls it elevated at line 521, beforeStart-Serviceat line 534, and it is correct there —config_service's row is seeded FROM darling.json's ports, so at that moment the file is the control plane's future answer. Requiring a store would fail every fresh networked install to close a window in which the file cannot be wrong. So it reads best-effort with a 10 s bound and prints the port it used, the specific reason, the one state that would make that port wrong, and the command to re-run.Stale-rule cleanup went in on the half that lacked it.
--configure-firewallhas swept the per-surface wildcard since #1771; the toggle verbs reconciled one exact DisplayName, which by construction cannot reach a rule left on a port they no longer know about — and those are exactly the verbs an operator reaches for after moving a port. They sweep the same wildcard now, through a helper that returns an unrecognized name unchanged rather than inventing a pattern, so the widening cannot touch a rule this product did not create.--disable-mcpnow closes every MCP port rather than only the one it resolved.webhad the same defect on the same two lines and is fixed in the same shape. A third surface turned up:--configure-network'sPrintNextStepsprinted a paste-readyNew-NetFirewallRulebuilt from the file port; that wizard holds no store connection and honestly cannot do better, so it now names which port the printed rule is for and points at--configure-firewall.Fifteen tests, four of them expressible against dev and all four red there — the two source drift guards and the two
RETURNINGshape pins. The other eleven cannot compile against dev at all, which is its own kind of evidence.Two residues split out as #2435.
Name the firewall rule for the port the endpoint binds (erikdarlingda…
Say that the MCP endpoint is what an unreadable settings.json cost
Close the two firewall residues erikdarlingdata#2432 disclosed (erikd…
Reprint a lost endpoint token, and say when a network block can change (