fix: serve the panel API over TLS, and advertise the right address - #48
Merged
Conversation
Every Supervisor endpoint answers `{"result": ..., "data": {...}}`, but both
calls here read the fields they wanted from the top level of the body.
Registering a panel therefore never found the uuid, and logged the whole
successful reply as an invalid one. The entry was created — the simulator just
did not record what it was called, so `unregister_panel` had nothing to delete
and every entry leaked. `cleanup_stale` read `discovery` off the same wrong
level, always saw an empty list, and could not reap them on the next start
either, so they accumulated in the Supervisor across restarts.
Unwrapped once in a helper rather than at each call site, so the shape is
stated in one place. The tests mocked a flat body the Supervisor never returns,
which is why this shipped; they now mock the real envelope.
The panel minted a server certificate and published the authority that signed it, but nothing ever served that certificate: the per-panel bootstrap server bound plain HTTP only. A consumer that pins the published authority — as the Home Assistant integration does before it will send the panel passphrase — checks that the anchor validates what the panel actually serves, and that check could not pass at any port. The config flow failed at the certificate step no matter what was entered, reporting a leaf mismatch when the truth was that nothing answered. Each panel now binds HTTPS alongside HTTP, off one runner, using the leaf that was already being generated. Both listeners serve the whole route table rather than splitting it: deciding a panel is a SPAN panel at all, and fetching the authority, both happen before a consumer holds an anchor, so neither can sit behind one — and which endpoints a consumer reaches over which listener is its decision, not the panel's to hard-code. The TLS port is allocated per panel from a pool shared with the HTTP one, so one can never be handed out as the other, and it is published by both discovery paths — `https_port` through the Supervisor, `httpsPort` in the mDNS TXT — so a consumer is not left to assume 443, which is never right here. The new tests bind real sockets with real certificates, because what is under test is exactly what an in-process test client bypasses. They verify against a context built the way the consumer builds it, with VERIFY_X509_STRICT cleared: real panels ship a minimal authority with no key identifier, and verifying more strictly here would test a client that does not exist and push this simulator away from the hardware it stands in for.
The detection took `$3` of the default route, which is the next hop. The comment above it explained why that was the host — true of a bridge-networked container, and this add-on sets `host_network: true`, so the container shares the host's network namespace and reads the host's own routing table. The value picked up was the upstream router. That address went into the leaf certificate's SAN and into the mDNS advertisement, so a neighbouring device was named as the panel and there was no address a client could reach the panel by that passed verification. It also broke the simulator-to-panelbench upgrade rehearsal, since the only remaining way in was a Supervisor hostname that differs between the two. Now taken from `ip -4 route get`, which is a routing-table lookup rather than a probe — it sends no packets and needs nothing reachable — and answers the question that matters: the source address the kernel would put on a reply. That also stays correct on an interface holding several addresses, where taking the first one listed would be arbitrary. An address supplied by the environment now wins over detection, which the previous unconditional assignment clobbered. Existing installs need no intervention: the leaf-reuse check already rejects a certificate whose SAN omits the advertised address, so the next start re-signs it and leaves the authority alone. The test executes the function out of the shipped run.sh against a stub `ip` rather than restating the pipeline, since a copy would keep passing after the original drifted — which is precisely how this broke.
Minor rather than patch: the panel gains a TLS listener and a new discovery field, which is added capability, not only repair.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Three separate defects, each of which on its own stopped a simulated panel being added to Home Assistant. Split into one commit per concern.
The panel never served TLS
BootstrapHttpServerbound plain HTTP only. The panel minted a server certificate and published the authority that signed it at/api/v2/certificate/ca, but nothing ever served that certificate — the leaf only ever fed mosquitto.The integration pins the published authority before it sends the panel passphrase, and checks the anchor validates what the panel actually serves. With no TLS listener that check cannot pass at any port, so the config flow ended at "The certificate this panel serves is not signed by the authority it published" whatever was entered. The message is misleading:
async_panel_leaf_hostreturns the same answer for a refused connection as for a genuine mismatch, and the truth here was that nothing answered.Each panel now binds HTTPS alongside HTTP off one runner, using the leaf already being generated. Both listeners serve the whole route table — deciding a panel is a SPAN panel at all, and fetching the authority, both necessarily happen before a consumer holds an anchor, so neither can sit behind one.
Home Assistant had to ask for the TLS port, and could not be told
The port is allocated per panel and reallocated across restarts, so this add-on is the only party that knows it. Neither discovery path published it, so the integration fell back to prompting with a 443 default that is right for real hardware and never right here.
It is now published both ways:
https_portthrough the Supervisor,httpsPortin the mDNS TXT.Supervisor replies were read from the wrong level
Every Supervisor endpoint answers
{"result": ..., "data": {...}}; both calls read from the top level of the body.Registration never found the uuid and logged the whole successful reply as an invalid one. The entry was created — the simulator just did not record what it was called, so
unregister_panelhad nothing to delete and every entry leaked;cleanup_stalereaddiscoveryoff the same wrong level, always saw an empty list, and could not reap them on the next start either. Entries accumulated in the Supervisor across every restart.The advertised address was the default gateway
run.shtook$3of the default route — the next hop. The comment above it explained why that was the host, which holds for a bridge-networked container; this add-on setshost_network: true, so the container reads the host's own routing table and the value picked up was the upstream router.That address went into the certificate SAN and the mDNS advertisement, so a neighbouring device was named as the panel and no address a client could use passed verification. It also broke the simulator-to-panelbench upgrade rehearsal, leaving only a Supervisor hostname that differs between the two.
Now taken from
ip -4 route get, a routing-table lookup rather than a probe — it sends no packets and needs nothing reachable — which yields the source address the kernel would put on a reply, and stays correct on an interface holding several addresses.Existing installs need no intervention: the leaf-reuse check already rejects a certificate whose SAN omits the advertised address, so the next start re-signs it and leaves the authority alone.
Testing
486 passed, up from 473 on this branch point. Three failures remain (test_graph_builder_topology×2,test_main_logging) and are pre-existing onmain, unrelated to this change: the pin isebus-sdk==0.23.1while the vendored emitter targets the 0.1.xDeviceAPI.New coverage, 13 tests:
VERIFY_X509_STRICTcleared — real panels ship a minimal authority with no key identifier, and verifying more strictly would test a client that does not exist.run.sh— the function is extracted from the shipped script and executed against a stubip, rather than restating the pipeline, since a copy would keep passing after the original drifted. That is precisely how this broke. Covers the gateway case, an on-link route wheresrcshifts position, control-character stripping, a failingipnot aborting underset -euo pipefail, and the environment override.Verified by falsification rather than reading: the old pipeline yields
192.168.65.1and the new one192.168.65.19against identical stubbed routing output.Note
Version is bumped minor, not patch: the TLS listener and the new discovery field are added capability.
span_panel_simulator/CHANGELOG.mdis left alone — it has been stale since 1.0.12 and was not updated for 1.1.0 either, so the root changelog is the maintained one. Worth deciding separately whether the add-on changelog should be retired or backfilled.Follow-on: #49 carries two further fixes found by repeating the upgrade rehearsal against this build — the Supervisor discovery host, and matching panelbench's TLS port convention.