fix: advertise the host's own address, and unwrap the Supervisor envelope - #20
Merged
Merged
Conversation
cayossarian
force-pushed
the
fix/advertise-address-and-supervisor-envelope
branch
from
August 29, 2026 01:02
c0ba1fb to
239cab6
Compare
`ip route | awk '/default/ { print $3 }'` takes the gateway. That was right
for a bridge-networked container, where the default gateway is the host, and
became wrong when the add-on moved to `host_network: true`: the container then
shares the host's network namespace and reads the host's routing table, so the
gateway is a neighbouring device rather than us.
The address is what goes into the leaf certificate's SAN and into the mDNS
advertisement, so the effect was that no address a client can reach the panel
at was one it could verify the panel by. Observed on a real install: both
carried the VLAN gateway 192.168.65.1 while the host was 192.168.65.19.
`ip -4 route get 1.1.1.1` answers the question that actually matters --
which source address the kernel would put on a reply -- as a routing-table
lookup rather than a probe, so it sends no packets and needs nothing at the
far address to be reachable.
A stored leaf naming the wrong address is already unfit by `_leaf_is_fit`, so
an existing /data/certs install re-signs on its next start.
The test extracts the function from the shipped run.sh and runs it against a
stubbed `ip`, rather than restating the pipeline: a copy would keep passing
after the original drifted, and drift is the failure being guarded.
…s stored
Every Supervisor endpoint answers `{"result": "ok", "data": {...}}`, and both
readers went straight to the top level. `data.get("uuid")` on a registration
response was therefore always None, which the code correctly reported as an
invalid uuid and refused to store:
register sim-40t-001 returned invalid uuid: {'result': 'ok', 'data': {'uuid': ...}}
With no uuid tracked, `unregister_panel` had nothing to delete on stop, so
discovery entries accumulated across restarts. `cleanup_stale` read
`data.get("discovery")` the same way and saw an empty list, so the entries the
previous run left behind were never collected either -- the two paths that
could have removed a stale entry failed for one reason.
One `_payload` helper for both, so the envelope is unwrapped in a single place
and a third reader cannot reintroduce the assumption. It answers {} for a body
that is not a dict or carries no dict `data`, and the caller guards that
`discovery` is a list, so a malformed response degrades to doing nothing
rather than raising into startup.
The existing tests now serve enveloped bodies. They fail without this change:
a top-level read of the enveloped fixture yields None for the uuid and [] for
the entries, which is exactly the production symptom.
Version synced across pyproject, config.yaml, Dockerfile and __init__.py by scripts/sync-version.sh from pyproject as the source of truth, and uv.lock regenerated so the recorded project version matches.
cayossarian
force-pushed
the
fix/advertise-address-and-supervisor-envelope
branch
from
August 29, 2026 01:14
f911c62 to
21554e2
Compare
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.
Two independent defects, both of which made the add-on look healthy in its own
log while a consumer could not use what it published. Released as 2.5.1.
The advertised address was the upstream router
run.shderived the address fromip route | awk '/default/ { print $3 }',which is the gateway. That was correct for a bridge-networked container,
where the default gateway is the host, and became wrong when the add-on moved
to
host_network: true: the container then shares the host's networknamespace and reads the host's routing table, so
$3names a neighbouringdevice rather than us.
That address is what goes into the leaf certificate's SAN and into the mDNS
advertisement, so the effect was that no address a client could reach the
panel at was one it could verify the panel by. Observed on a real install:
both carried the VLAN gateway
192.168.65.1while the host was192.168.65.19.Replaced with
ip -4 route get 1.1.1.1, taking the token aftersrc. That isa routing-table lookup rather than a probe, so it sends no packets and needs
nothing at the far address to be reachable, and it answers the question that
actually matters -- which source address the kernel would put on a reply --
which also keeps it right on an interface holding several addresses. An
ADVERTISE_ADDRESSfrom the environment still wins, forrun-local.shandfor an operator on a multi-homed host.
Existing installs self-correct. A stored leaf that does not name the
advertised address is already unfit by
certs.py::_leaf_is_fit, so/data/certsis re-signed on the next start with no manual clearing.The Supervisor response envelope was never unwrapped
Every Supervisor endpoint answers
{"result": "ok", "data": {...}}, and bothreaders in
supervisor_discovery.pywent straight to the top level. Sodata.get("uuid")on a registration response was alwaysNone, which thecode correctly reported as invalid and refused to store:
With no uuid tracked,
unregister_panelhad nothing to delete on stop, andcleanup_stalereaddata.get("discovery")the same way and saw an emptylist -- so both paths that could have removed a stale entry failed for the one
reason, and entries accumulated across restarts.
Added a single
_payloadhelper used by both readers, so the envelope isunwrapped in one place and a third reader cannot reintroduce the assumption.
It answers
{}for a body that is not a dict or carries no dictdata, andthe caller guards that
discoveryis a list, so a malformed response degradesto doing nothing rather than raising into startup.
Release
2.5.1 is also the first release to ship the ebus-sdk ceiling fix merged
separately in #19, so the changelog entry lists it; this branch has been
rebased onto that merge and carries no copy of the commit.
Testing
tests/test_run_sh_advertise_address.pyextractsdetect_advertise_addressfrom the shipped
run.shand runs it against a stubbedipon PATH, ratherthan restating the pipeline -- a copy would keep passing after the original
drifted, and drift is precisely the failure being guarded. Six cases: the
host's address is taken, the gateway specifically is not, an on-link route
with no
viastill yields the source, control characters are stripped, afailing
ipleaves the address empty rather than aborting underset -euo pipefail, and an explicitADVERTISE_ADDRESSis not overridden.The Supervisor tests now serve enveloped bodies and fail without this change:
a top-level read of the enveloped fixture yields
Nonefor the uuid and[]for the entries, which is the production symptom exactly.
Full suite 390 passed after the rebase;
pre-commit run --all-filesclean.