cloud/reconciler/ingress_verify.rs
1//! Is the backend reachable at the port its service record advertises? — the
2//! half [`collate_workspace_ingress`](crate::validate::collate_workspace_ingress)
3//! deliberately cannot answer (R844-F16).
4//!
5//! **Read [§What this does not prove](#what-this-does-not-prove) before
6//! treating a green result as a deploy gate.** That question is narrower than
7//! "does the front door work", and on 2026-09-03 the difference took yah.dev
8//! down for four minutes.
9//!
10//! `yah cloud ingress collate` is pure: no network, no credentials. That purity
11//! is load-bearing — it is what lets `xtask/tests/mirror_ingress.rs` plan this
12//! camp's real `.yah/services/` tree as a unit test — but it means the
13//! `100.64.0.3:8080` it prints is an **echo of declarations**: the mirror's
14//! `upstream_host` pin, or since R844-F12 the placement machine's declared
15//! `[registration].mesh_ipv4`. Collate attests that the mirrors *cohere*. It
16//! has never attested that anything answers.
17//!
18//! That distinction is not academic in this repo. The apex's `upstream_host`
19//! was once left at `127.0.0.1` after a second front door existed, and collate
20//! rendered it exactly as confidently as it renders a correct one — for the
21//! nineteen days the site was frozen.
22//!
23//! ## Four claims, and this module measures only the third
24//!
25//! | claim | evidence | who says it |
26//! |---|---|---|
27//! | "the mirrors agree on what fronts what" | the declaration tree | `collate` |
28//! | "a ready record exists for it" | `GET /service-records?ready=true` | [`ServiceRecordFanout`] |
29//! | "that record's address answers" | a TCP connect | **this module** |
30//! | "the front door is configured to dial it" | an HTTPS GET of the hostname, compared against the backend | **this module**, [`apply_public_path`] (R844-F18) |
31//!
32//! The second is *yubaba's opinion*, and R844-B11 is the proof it can be wrong
33//! while looking right: us-west-001 advertised `100.64.0.3:4325` as `Ready`
34//! while the workload answered on `100.64.0.1:4325`. The record was healthy,
35//! the record's own address refused connections, and every layer above it
36//! reported success. A connect is the only step that can catch that, because it
37//! is the only step that asks the world instead of asking a declaration.
38//!
39//! ## What this does not prove
40//!
41//! **Updated by R844-F18 — the gap this section describes is now closed by
42//! [`apply_public_path`], and the history below is why that check exists and
43//! what it is still not.** [`verify_collation`] alone proves **the backend is
44//! reachable at the port the record advertises**; on its own it never proves
45//! **the front door is configured to dial that port**. Those two coincide only
46//! while a pin forces them to — so that pass is *weakest in exactly the
47//! portless configuration it was built to certify*.
48//!
49//! That is not a theoretical gap. R844-T10 deleted the apex's `port` pin on the
50//! strength of a green run from this verb, on 2026-09-03:
51//!
52//! * kamaji allocated a **new** port for the redeployed workload — 34759;
53//! * the service record correctly advertised 34759;
54//! * this verb dialed 34759, found it open, and printed
55//! *"2 rule(s) … 2 proven to serve, 0 not"*;
56//! * the public got **HTTP 503** for four minutes, because the running passway
57//! was still configured for 8080 and **nothing reconfigures it**.
58//!
59//! So the verb reported success during the live outage it was built to prevent.
60//! The prior measurement that authorised the edit was green for a reason that
61//! did not survive a real deploy: it stripped the pins from a *copy* of the
62//! config while the old workload was still bound to 8080, which is the one
63//! arrangement in which the record and the front door cannot disagree.
64//!
65//! This is [`collate`]'s own limitation one level up — collate attests
66//! coherence and not reachability; a dial attests reachability of a *record*,
67//! and not that the front door agrees with that record.
68//!
69//! [`apply_public_path`] closes it by traversing the **public path** and
70//! comparing: it fetches the publish beacon
71//! ([`publish_beacon`](crate::reconciler::publish_beacon)) from
72//! `https://<hostname>/` and from each discovered backend, and fails the rule
73//! when the two serve different publishes. A bare `GET /` would not have done —
74//! a door pointed at the wrong backend answers 200 with a plausible page, which
75//! is how the apex stayed frozen for nineteen days. The beacon is the only
76//! object on either side that says *which publish this is*.
77//!
78//! **Two things that check is still not.** It is a **detector of the current
79//! state**, not a simulation of a pending edit — run it before and after an
80//! apply and require both green. And the public fetch goes wherever **DNS**
81//! sends it, so a hostname on two front doors is measured at one of them; the
82//! verdict says so in a note rather than implying it covered both.
83//!
84//! **So: a green [`verify_collation`] alone is necessary, not sufficient. Do
85//! not use it without the public-path pass as the gate on removing a pin.**
86//!
87//! [`collate`]: crate::validate::collate_workspace_ingress
88//!
89//! ## Why this is a separate verb and not a flag on `collate`
90//!
91//! Because the purity above is the feature. A `--live` flag would put a network
92//! read inside the function eleven offline tests call, and the pressure to make
93//! those tests pass would then push the network read towards being optional in
94//! a way that silently degrades. A sibling verb costs nothing that flag would
95//! not cost more.
96//!
97//! ## Pure, like everything else on this seam
98//!
99//! Nothing here opens a socket. The caller does the fanout read and the dial,
100//! and hands both in as data — the fifth instance of the shape
101//! [`resolve_ingress_placements`](crate::reconciler::resolve_ingress_placements),
102//! [`IngressPlan::resolve_upstreams`], [`IngressPlan::resolve_ports`] and
103//! [`IngressPlan::resolve_upstreams_from_config`] already use. So the verdict
104//! logic — which is where the interesting mistakes live — is unit-testable
105//! against a fake fleet with no network at all.
106//!
107//! ## A subset renders like a success, so a subset is a failure
108//!
109//! The failure class this whole relay exists to remove is a partial answer that
110//! looks complete. A rule placed on two nodes that resolves one address is
111//! *half a front door*: it renders, it dials, it serves — and half the fleet's
112//! traffic capacity is silently absent. [`RuleVerdict`] therefore fails a rule
113//! whose resolved backends do not cover its whole declared placement, and names
114//! the node that went missing along with why.
115//!
116//! @yah:ticket(R844-F18, "Verify the PUBLIC path — an HTTPS GET of the hostname through the real front door, compared against what the record claims")
117//! @yah:status(review)
118//! @yah:assignee(agent:bundle-anthropic-ashguard)
119//! @yah:at(2026-09-04T01:55:44Z)
120//! @yah:parent(R844)
121//! @yah:next("KEEP `collate` PURE (R772, R844-F5, R844-F12, R844-F16 each fought for this) and prefer a third verb or a flag on `verify` over touching it. `cargo test -p xtask --test main mirror_ingress` planning the camp's REAL .yah/services tree with no network is the property being protected; it is currently 11 green.")
122//! @yah:verify("And it must still be green on the healthy fleet: https://yah.dev/ through both declared front doors, agreeing with what `yah cloud ingress verify` resolves.")
123//! @yah:gotcha("A LIVE-OUTAGE-DETECTOR IS NOT AUTOMATICALLY A PRE-FLIGHT GATE, and this ticket should be honest about which it is building. An HTTPS GET proves the CURRENT front door serves; it cannot tell you what a config change is ABOUT to do, because the front door has not been reconfigured yet. That may still be enough — run it before and after an apply and require both green — but say so explicitly rather than letting a future reader assume it gates the edit. The failure that started this was precisely someone (twice) treating a green from the wrong vantage point as authorisation.")
124//! @yah:next("A SHAPE FOR THIS, from @Ashguard:griffin (session:75f87e36, the session that caused the outage behind it) — offered as a starting point, not a settled design. The open question on this ticket is whether an HTTPS GET is a pre-flight GATE or only an outage DETECTOR. I think it is only ever a detector, and that the ticket is really TWO checks answering two different questions:\n\n 1. PRE-FLIGHT, and it is not a probe at all — it is a COMPARISON. Read what the front door is actually configured to dial, FROM THE DOOR, and compare it against what the service record says the backend is. That is the check that would have caught tonight's outage BEFORE it happened, because the two disagreed (passway held 8080, the record advertised 34759) at a moment when every probe of either side in isolation was green. Note what makes it different from R844-F16's verify: verify reads the record and dials the port the record names, so both of its inputs come from the same side of the disagreement. The door's own configured value is the input nobody currently reads, and it is the only one that makes the comparison possible.\n\n 2. POST-CONDITION, which is where the HTTPS GET belongs — an unauthenticated GET of the public hostname through the real front door, asserted AFTER an apply, once R844-B19 guarantees the door has actually been repointed. Today that assertion cannot be trusted to mean anything, because B19's ordering bug means the door may never have been updated at all; the GET would just be re-measuring the old configuration and calling it a pass.\n\nWHY THIS PAIRS F18 WITH B19 RATHER THAN DUPLICATING IT: B19 makes the front-door update reliably HAPPEN; (2) is the assertion that it DID; (1) is the only one of the three that can speak before a change is applied. Sequencing follows from that — do not land (2) before B19, or it encodes today's broken ordering as the expected one.\n\nTHE CAVEAT I CANNOT RESOLVE AND WHOEVER TAKES THIS SHOULD NOT ASSUME AWAY: even (1) compares two CURRENT states. It does not simulate what a config change is about to do, which is what we actually wanted to know tonight. It catches an existing divergence, and it would catch this specific class because the divergence appears the moment the workload is redeployed — but it is not a general \"is this edit safe\" oracle, and nothing in this design is. If someone needs that, it is a different and much larger ticket, and it should be filed as one rather than smuggled in here.")
125//! @yah:handoff("LANDED. `yah cloud ingress verify` now takes the fourth step, on by default. For every hostname in the collation it fetches `https://<hostname>/.well-known/yah-publish.json`, for every discovered backend it fetches the same object over the mesh, and it FAILS the rule when the two name different publishes. Verdict logic is `apply_public_path` in oss/yubaba/crates/cloud/src/reconciler/ingress_verify.rs, pure like the rest of that seam — the CLI does both fetches and hands them in as `PublicReadings`, so the interesting mistakes are unit-testable against a fake fleet. New surface: `BeaconFetch`, `PublicReadings`, `apply_public_path`, three `VerifyFinding` arms, `fetch_beacon` and `--skip-public` in app/yah/cli/src/cloud.rs.")
126//! @yah:handoff("THE COMPARISON IS THE CONTENT, NOT THE GET — this is the design decision, and the ticket title's \\\"HTTPS GET\\\" understates it. A bare `GET /` proves only that something answered: a door pointed at the wrong backend returns 200 with a plausible page, which is exactly how the apex stayed frozen for nineteen days with every signal green. The publish beacon (`publish_beacon.rs`, `BEACON_KEY = .well-known/yah-publish.json`, R703-B4) is the one object on either side of the door that says WHICH PUBLISH THIS IS, so fetching it from both and comparing digests is what turns \\\"something answered\\\" into \\\"the front door is serving the backend the records name\\\". Reused rather than invented — the object already exists, `mesofact serve` already answers it out of the bundle, and R2 static publishes already write it.")
127//! @yah:handoff("THE TICKET'S OWN OPEN QUESTION — GATE OR DETECTOR — ANSWERED, AND ANSWERED THE WAY ITS FILER EXPECTED: **detector**, and the code says so in its own output rather than leaving a reader to infer it. `apply_public_path`'s doc, the CLI `--help`, the summary line printed on every run, W267 and the service-toml guide all now carry the same sentence: it compares two CURRENT states, cannot simulate an edit you have not applied, and the protocol is run-before-and-after-and-require-both-green. That is enough for the failure it was built for — the divergence appears the moment the workload is redeployed onto a new port — and it is deliberately NOT sold as an \\\"is this edit safe\\\" oracle. @Ashguard:griffin's caveat on this ticket was right and is preserved as the design, not assumed away.")
128//! @yah:handoff("GRIFFIN'S PART (1), THE PRE-FLIGHT \\\"READ THE DOOR'S OWN CONFIG AND COMPARE\\\", IS NOT WHAT SHIPPED — say so plainly rather than letting the ticket read as fully covered. Their shape proposed reading what passway is CONFIGURED to dial, from the door, and comparing that against the record. This ships the equivalent comparison one layer out: what the door ACTUALLY SERVES versus what the backend serves. Why that substitution rather than the config read: the running passway holds its upstreams in container env (`PASSWAY_UPSTREAMS`, `PASSWAY_UPSTREAM_SOURCE=static` — oss/passway/crates/passway/src/main.rs), so reading it means an SSH or a docker inspect per door, i.e. credentials and a shell on a production box inside a read-only verb. The served comparison needs neither, catches the same divergence class (it is true exactly when the door is dialing something else), and additionally catches a stale edge cache, which a config read cannot see. What the config read would still buy is naming WHY they diverge; that is a genuinely separable ticket and is not smuggled in here.")
129//! @yah:handoff("A FAILURE IS ONLY A FAILURE WHEN SOMETHING WAS ACTUALLY COMPARED — the design care, and the thing a careless version of this gets wrong in the direction that matters. Three arms produce a NOTE and leave the rule clean rather than a finding: a hostname that answers 200 with no beacon (it fronts something that is not a mesofact publish — a limit on the check, not a fault of the host, and failing it would red every non-bundle hostname in the fleet); a public 200 with no comparable backend (the note says verbatim that this proves the hostname is up and NOT that it is fronting the discovered backend); and a hostname nobody measured. Two arms fail: a transport failure, and a non-2xx. One arm is the point: `PublicBackendDivergence`, which names both digests and the backend address it compared against. And a backend that answers without a beacon adds nothing — `verify_collation` already dialed it and said what it found, so a second opinion phrased as an error would double-count one fact.")
130//! @yah:gotcha("THE LIMIT I COULD NOT DESIGN AWAY, AND DID NOT HIDE: the public fetch goes wherever DNS sends it, so a hostname published through two front doors is measured at ONE of them and this pass cannot say which. A divergence affecting only the other door reads as clean. Every verdict for such a hostname carries a note saying so — but only once a comparison actually happened, since on a rule where nothing could be compared that note is noise stacked on the finding. Closing it needs a fetch pinned to each door's public address with a `Host` override, which needs a public IP per door that nothing in the `Collation` carries today. Pinned by `a_hostname_on_two_front_doors_is_noted_as_measured_at_only_one`.")
131//! @yah:verify("UNIT: `cargo test --manifest-path oss/yubaba/Cargo.toml -p yah-cloud --lib ingress_verify` = 19 passed / 0 failed (11 before, +8 new). THE ONE THAT MATTERS IS `a_503_at_the_apex_fails_a_rule_whose_mesh_side_is_entirely_green` — it reproduces the 2026-09-03 outage as a unit test, asserting FIRST that `verify_collation` alone reports the rule clean (that assertion is the precondition, because a green mesh side is what reported success during the outage) and THEN that the public leg fails it. The other seven cover the stale-serve shape (200 serving a different digest than the backend), a transport failure, a hostname with no beacon, a public 200 with nothing to compare, an unmeasured hostname, a hostname on two doors, and the fully-clean case where the two digests match and NOTHING is caveated. Wider: `-p yah-cloud --lib ingress` = 94 passed / 0 failed; `-p yah-cloud --lib` = 1019 passed / 0 failed / 4 ignored (1011 before, so +8 and nothing lost).")
132//! @yah:verify("THE PURITY CANARY, which this ticket's own `next` named as the property to protect: `cargo test -p xtask --test main mirror_ingress` = 11 passed / 0 failed. `collate` was not touched — the public leg is a fourth step on `verify`, per the same reasoning R844-F16 used to make `verify` a sibling verb rather than a flag. `cargo check --workspace --all-targets` cargo-exit=0, zero `^error` lines. Installed and re-installed with `cargo xtask install` (sha256 b751f470e329253765075e5c050256f4e848ae1e9265f55fd6965f024bafbf24, `PATH resolves here`), per this relay's standing gotcha that `cargo build` does not update the binary an operator runs.")
133//! @yah:verify("THIS TICKET'S STATED ACCEPTANCE TEST — \\\"green on the healthy fleet, https://yah.dev/ through both declared front doors, agreeing with what verify resolves\\\" — WAS **NOT** MET, AND NOT BECAUSE OF THIS CHANGE. The fleet is not healthy right now: the mesh coordination server is down (`cloud.mesh.yah.dev` -> 15.204.89.240 REFUSES :443 and :80 while :22 answers, and `tailscale status` reports this machine logged out with \\\"fetch control key ... connection refused\\\"), so NO 100.64.0.0/10 address is reachable from here and the mesh half of the check cannot run at all. Filed as R858 with the full measurement chain. I am recording this as unmet rather than reporting a partial green.")
134//! @yah:verify("WHAT THE LIVE RUN DID PROVE, and it is more than nothing: `yah cloud ingress verify --path .` from the freshly installed binary fetched `https://yah.dev/.well-known/yah-publish.json` over the real public internet, parsed it, and — because no backend beacon could be read across the dead mesh — printed exactly the right sentence instead of a pass: \\\"https://yah.dev answers with a publish beacon, but no discovered backend served one to compare it against, so this proves the hostname is up and NOT that it is fronting the discovered backend\\\". Independently confirmed by hand: `curl https://yah.dev/` = HTTP 200 in 0.81s and the beacon is `{\\\"prefix\\\":\\\"bundle/yah-marketing\\\",\\\"digest\\\":\\\"bfb47cd42468b080c474193fa6091ec273b6c99ab5a8a3b91d9c847cd7278551\\\",\\\"files\\\":39}`. So the public leg ran end to end against production and, on a real unplanned failure it was never designed for, refused to overclaim — which is the behaviour this ticket exists to install. `--skip-public` also exercised live: every verdict then reads \\\"the public path was not checked for yah.dev — this verdict speaks only for the mesh side, which is necessary and not sufficient\\\", and the summary names the dropped claim.")
135//! @yah:next("RE-RUN THE ACCEPTANCE TEST ONCE R858 CLEARS — it is one command and it is the only thing outstanding on this ticket: `yah cloud ingress verify --path .` must exit 0 with both front doors' rules reporting the mesh dial open AND the public beacon matching the backend's. Until the mesh is reachable that run measures nothing about the fourth claim.")
136//! @yah:notify_on(R858, "The mesh is reachable again — run this ticket's outstanding acceptance test: `yah cloud ingress verify --path .` must exit 0 with BOTH front doors reporting the mesh dial open AND the public beacon digest matching the backend's. It could not be run at landing time because no 100.64.0.0/10 address answered. If it passes, that closes the last open item here; if it fails, read which of the two legs failed before touching the code — the mesh leg is R844-F16's and predates this change.")
137
138use std::collections::BTreeMap;
139use std::fmt;
140
141use crate::reconciler::ingress::{Collation, IngressPlan, IngressRule};
142use crate::reconciler::service_discovery::ServiceRecordFanout;
143
144/// What a TCP connect to one resolved `host:port` actually did.
145///
146/// Two arms, no `Unknown`: unlike a discovery read — which can fail to *ask* a
147/// node, and whose whole vocabulary
148/// ([`RecordVisibility`](crate::reconciler::RecordVisibility)) exists to keep
149/// that apart from an empty answer — a dial either completed or it did not.
150/// The attempt is the evidence.
151#[derive(Debug, Clone, PartialEq, Eq)]
152pub enum DialOutcome {
153 /// The connect completed. This is the only positive evidence in the whole
154 /// ingress stack that anything is listening.
155 Open {
156 /// How long the connect took, for an operator eyeballing a slow path.
157 millis: u128,
158 },
159 /// The connect did not complete, in the transport's own words — refused,
160 /// timed out, no route.
161 Closed(String),
162}
163
164impl DialOutcome {
165 /// One short label for a summary line.
166 pub fn as_str(&self) -> &'static str {
167 match self {
168 Self::Open { .. } => "open",
169 Self::Closed(_) => "CLOSED",
170 }
171 }
172
173 /// Did anything answer?
174 pub fn is_open(&self) -> bool {
175 matches!(self, Self::Open { .. })
176 }
177}
178
179impl fmt::Display for DialOutcome {
180 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
181 match self {
182 Self::Open { millis } => write!(f, "open ({millis}ms)"),
183 Self::Closed(why) => write!(f, "CLOSED — {why}"),
184 }
185 }
186}
187
188/// One dialed address and what came back.
189#[derive(Debug, Clone, PartialEq, Eq)]
190pub struct EndpointCheck {
191 /// The `host:port` dialed — exactly what the front door would dial.
192 pub address: String,
193 pub outcome: DialOutcome,
194}
195
196/// How one rule's placement resolved against a live discovery read.
197///
198/// Recorded *during* resolution rather than reconstructed after it, because two
199/// of these three facts are unrecoverable from the resolved plan: whether the
200/// address came from a pin or from the fleet, and which placement node supplied
201/// it. Both are exactly what an operator needs to act on a failure.
202#[derive(Debug, Clone, PartialEq, Eq)]
203pub struct RuleResolution {
204 pub hostname: String,
205 pub slot: String,
206 /// The rule already carried an address before the read — the slot pins
207 /// `upstream_host`, and the pin wins everywhere (R844-F5/F12).
208 ///
209 /// Reported because it changes what the dial *means*: a pinned rule's
210 /// connect measures whether a **declaration** answers, and the fleet has no
211 /// say in what was dialed. That is the 127.0.0.1 case above.
212 pub pinned: bool,
213 /// Placement nodes that answered with a ready record on this rule's port.
214 pub covered: Vec<String>,
215 /// Placement nodes that did not, each with the reason — an `Unknown` node's
216 /// own words, or "answered, and has no such record".
217 pub missing: Vec<(String, String)>,
218}
219
220impl RuleResolution {
221 /// Key under which a resolution is looked up once collation has grouped the
222 /// rules by node.
223 ///
224 /// `(hostname, slot)` rather than hostname alone: one hostname is fronted
225 /// through exactly one provider (`collate_front_doors` rejects otherwise),
226 /// but the slot is what an operator opens to fix a finding, so carrying it
227 /// costs nothing and a message without it points at no file.
228 pub fn key(&self) -> (String, String) {
229 (self.hostname.clone(), self.slot.clone())
230 }
231}
232
233/// Everything the resolution pass learned, keyed for the verify pass.
234pub type RuleResolutions = BTreeMap<(String, String), RuleResolution>;
235
236/// Why one rule is not proven to serve.
237#[derive(Debug, Clone, PartialEq, Eq)]
238pub enum VerifyFinding {
239 /// No port resolved — the slot declares `fronted = true`, pins no number,
240 /// and no ready record supplied one.
241 PortUnresolved,
242 /// No address resolved. The detail carries whether the read was complete,
243 /// because "the workload is not up" and "the node that has it could not be
244 /// seen" are different facts (R844-F4).
245 NoBackend { detail: String },
246 /// Fewer backends than the rule's declared placement. Fatal on purpose —
247 /// see the module doc.
248 PartialPlacement {
249 covered: Vec<String>,
250 missing: Vec<(String, String)>,
251 },
252 /// A resolved address did not answer. The one finding no offline pass could
253 /// ever produce.
254 Unreachable {
255 address: String,
256 why: String,
257 /// The address came from the slot's `upstream_host` rather than from a
258 /// service record. Carried because it changes *which* claim just got
259 /// falsified — a discovered address that refuses means the record and
260 /// the world disagree, a pinned one means the TOML is wrong — and a
261 /// message that names the wrong one sends the operator to the wrong
262 /// file. Caught by running this verb against a mirror with a bogus pin.
263 from_pin: bool,
264 },
265 /// No resolution was recorded for this rule at all — a bug in the caller's
266 /// wiring rather than a fact about the fleet, reported instead of silently
267 /// rendering the rule as fine.
268 Unresolved,
269 /// The public hostname did not answer at all — DNS, TLS, connect, timeout
270 /// (R844-F18). The one finding that speaks for the public rather than for
271 /// the mesh.
272 PublicPathFailed { hostname: String, why: String },
273 /// The public hostname answered with a status the public would read as
274 /// broken. `503` here is the 2026-09-03 outage exactly: every mesh dial
275 /// open, every record correct, and this the only signal that disagreed.
276 PublicPathStatus { hostname: String, status: u16 },
277 /// The public path and the backend the service record names are serving
278 /// **different publishes** (R844-F18). The finding this ticket exists for:
279 /// it is true precisely when the front door is dialing something other than
280 /// the backend the rest of this report just proved reachable.
281 PublicBackendDivergence {
282 hostname: String,
283 public_digest: String,
284 address: String,
285 backend_digest: String,
286 },
287}
288
289impl VerifyFinding {
290 /// Human-readable finding, in the imperative where there is something to do.
291 pub fn message(&self) -> String {
292 match self {
293 Self::PortUnresolved => "no port resolved — the slot declares `fronted = true` \
294 without `port`, and no in-scope node reported a ready record naming one. \
295 Either the workload is not up, or its yubaba predates named ports and the \
296 record is ambiguous; pin `port = <n>` on the slot to publish it anyway."
297 .to_string(),
298 Self::NoBackend { detail } => {
299 format!("no backend resolved — {detail}")
300 }
301 Self::PartialPlacement { covered, missing } => format!(
302 "resolves {} of {} declared placement node(s) — a SUBSET that renders like a \
303 whole front door. Serving: {}. Missing: {}.",
304 covered.len(),
305 covered.len() + missing.len(),
306 covered.join(", "),
307 missing
308 .iter()
309 .map(|(m, why)| format!("{m} ({why})"))
310 .collect::<Vec<_>>()
311 .join("; ")
312 ),
313 Self::Unreachable {
314 address,
315 why,
316 from_pin,
317 } => {
318 let origin = if *from_pin {
319 "The slot PINS this address in `upstream_host`, so nothing discovered it and \
320 nothing but this connect could have contradicted it — fix the pin, or drop \
321 it and let the fleet answer."
322 } else {
323 "A ready service record is yubaba's OPINION; this connect is the measurement, \
324 and they disagree (R844-B11)."
325 };
326 format!("{address} did not answer — {why}. {origin}")
327 }
328 Self::Unresolved => "no discovery resolution was recorded for this rule — the \
329 verify pass planned it but never resolved it, which is a wiring bug in the \
330 caller, not a fact about the fleet."
331 .to_string(),
332 Self::PublicPathFailed { hostname, why } => format!(
333 "https://{hostname}/ did not answer — {why}. Every line above measures the \
334 MESH side; this is the only one that measures what the public gets, so a \
335 report that is otherwise clean means the front door is not reaching the \
336 backend the records name."
337 ),
338 Self::PublicPathStatus { hostname, status } => format!(
339 "https://{hostname}/ answered HTTP {status}. The backend above is reachable at \
340 the port its service record advertises, so the front door is configured to \
341 dial something else — that pair of facts is exactly the 2026-09-03 outage \
342 (R844-T10), where a redeploy moved the port and nothing reconfigured the door."
343 ),
344 Self::PublicBackendDivergence {
345 hostname,
346 public_digest,
347 address,
348 backend_digest,
349 } => format!(
350 "https://{hostname}/ and the backend its service record names are serving \
351 DIFFERENT publishes: the public path returns digest {public_digest}, {address} \
352 returns {backend_digest}. The hostname answers, so nothing else in this report \
353 can see it — the front door is dialing a backend other than the discovered one, \
354 or an edge cache is still holding the previous publish."
355 ),
356 }
357 }
358}
359
360/// One rule's verdict on one front door.
361#[derive(Debug, Clone, PartialEq, Eq)]
362pub struct RuleVerdict {
363 /// Node whose front door publishes this rule.
364 pub machine: String,
365 /// Front-door provider, as `IngressProvider::as_str`.
366 pub provider: String,
367 pub hostname: String,
368 pub slot: String,
369 pub port: Option<u16>,
370 /// The address came from the slot's `upstream_host` pin, so the dial below
371 /// measured a declaration rather than a discovered fact.
372 pub pinned: bool,
373 /// Every resolved backend, dialed.
374 pub endpoints: Vec<EndpointCheck>,
375 /// Empty means proven: every declared placement node resolved and every
376 /// resolved address answered.
377 pub findings: Vec<VerifyFinding>,
378 /// Non-fatal context — today, the placement gaps of a *pinned* rule, where
379 /// the pin overrides placement so a gap is worth saying and not worth
380 /// failing.
381 pub notes: Vec<String>,
382}
383
384impl RuleVerdict {
385 /// Proven to serve.
386 pub fn is_ok(&self) -> bool {
387 self.findings.is_empty()
388 }
389
390 /// Every address this verdict dialed, in resolution order.
391 pub fn addresses(&self) -> Vec<String> {
392 self.endpoints
393 .iter()
394 .map(|e| e.address.clone())
395 .collect()
396 }
397
398 /// This rule's port for a message, or `<unresolved>` — the same spelling
399 /// [`IngressRule::port_label`] uses, so a verify line and a collate line
400 /// describing one unresolved rule read identically.
401 pub fn port_label(&self) -> String {
402 self.port
403 .map(|p| p.to_string())
404 .unwrap_or_else(|| "<unresolved>".to_string())
405 }
406}
407
408/// Every rule on every collated front door, verified.
409#[derive(Debug, Clone, Default, PartialEq, Eq)]
410pub struct VerifyReport {
411 pub verdicts: Vec<RuleVerdict>,
412}
413
414impl VerifyReport {
415 /// Rules that are not proven to serve.
416 pub fn failures(&self) -> impl Iterator<Item = &RuleVerdict> {
417 self.verdicts.iter().filter(|v| !v.is_ok())
418 }
419
420 /// Nothing to fix.
421 pub fn is_clean(&self) -> bool {
422 self.failures().next().is_none()
423 }
424}
425
426// ── The public path (R844-F18) ───────────────────────────────────────────────
427
428/// What one HTTP GET of a publish beacon returned — the caller's measurement,
429/// handed in as data like every other input on this seam.
430///
431/// The URL is always
432/// `<base>/.well-known/yah-publish.json` ([`publish_beacon::BEACON_KEY`]),
433/// because that object is the only thing on either side of the comparison that
434/// *identifies which publish is being served*. A bare `GET /` cannot do this
435/// job: a front door pointed at the wrong backend still answers 200 with a
436/// plausible page, which is how `yah.dev` stayed frozen for nineteen days with
437/// every signal green.
438///
439/// [`publish_beacon::BEACON_KEY`]: crate::reconciler::publish_beacon::BEACON_KEY
440#[derive(Debug, Clone, PartialEq, Eq)]
441pub enum BeaconFetch {
442 /// The request completed.
443 Answered {
444 status: u16,
445 /// The `digest` field of the beacon, when the body parsed as one.
446 /// `None` when it did not — a 404, or a hostname fronting something
447 /// that is not a mesofact publish. That is a limit on the comparison,
448 /// not a failure of the host, and is reported as a note.
449 digest: Option<String>,
450 },
451 /// The request did not complete — DNS, TLS, connect, timeout.
452 Failed(String),
453}
454
455/// Everything the caller measured on the public path, keyed for the pure pass.
456///
457/// Two maps rather than one because the two sides are addressed differently and
458/// deduplicated differently: a hostname is fetched once however many front doors
459/// publish it, and a backend address is fetched once however many hostnames
460/// resolve to it.
461#[derive(Debug, Clone, Default, PartialEq, Eq)]
462pub struct PublicReadings {
463 /// `hostname` → what `https://<hostname>/.well-known/yah-publish.json`
464 /// returned. A hostname absent from this map was not measured, and
465 /// [`apply_public_path`] says so rather than passing it.
466 pub public: BTreeMap<String, BeaconFetch>,
467 /// `host:port` → what `http://<host:port>/.well-known/yah-publish.json`
468 /// returned, read over the mesh. This is "what the record claims", made
469 /// comparable.
470 pub backends: BTreeMap<String, BeaconFetch>,
471}
472
473/// Add the public-path verdict to a report that so far only knows the mesh
474/// side (R844-F18).
475///
476/// ## The claim this closes
477///
478/// [`verify_collation`] answers *"is the backend reachable at the port its
479/// record advertises"*. This answers *"and does the front door actually reach
480/// it"* — the fourth row of the table in the module docs, which read `nothing
481/// yet` until this landed. It closes it by comparing two publish beacons: the
482/// one the **public** gets through the real front door, and the one the
483/// **backend the record names** serves. They agree only if the door is dialing
484/// that backend.
485///
486/// ## Detector, not oracle — read this before using it as a gate
487///
488/// It compares two *current* states. It cannot tell you what a config change is
489/// about to do, because the front door has not been reconfigured yet: run it
490/// before and after an apply and require both green. That is enough for the
491/// failure it was built for — the divergence appears the moment the workload is
492/// redeployed onto a new port — and it is not a general "is this edit safe"
493/// oracle. Nothing in this module is.
494///
495/// ## The limit that cannot be designed away here
496///
497/// The public fetch goes wherever **DNS** sends it. A hostname published
498/// through two front doors is measured at one of them, and this pass cannot say
499/// which — so a divergence that affects only the other door reads as clean. The
500/// verdicts for such a hostname carry a note saying so; closing it needs a fetch
501/// pinned to each door's public address with a `Host` override, which needs a
502/// public IP per door that nothing in the collation carries today.
503pub fn apply_public_path(report: &mut VerifyReport, readings: &PublicReadings) {
504 let doors_per_hostname = report.verdicts.iter().fold(
505 BTreeMap::<String, usize>::new(),
506 |mut acc, v| {
507 *acc.entry(v.hostname.clone()).or_default() += 1;
508 acc
509 },
510 );
511
512 for verdict in &mut report.verdicts {
513 let Some(fetched) = readings.public.get(&verdict.hostname) else {
514 verdict.notes.push(format!(
515 "the public path was not checked for {} — this verdict speaks only for the mesh \
516 side, which is necessary and not sufficient",
517 verdict.hostname
518 ));
519 continue;
520 };
521
522 let public_digest = match fetched {
523 BeaconFetch::Failed(why) => {
524 verdict.findings.push(VerifyFinding::PublicPathFailed {
525 hostname: verdict.hostname.clone(),
526 why: why.clone(),
527 });
528 continue;
529 }
530 BeaconFetch::Answered { status, .. } if !(200..300).contains(status) => {
531 verdict.findings.push(VerifyFinding::PublicPathStatus {
532 hostname: verdict.hostname.clone(),
533 status: *status,
534 });
535 continue;
536 }
537 BeaconFetch::Answered { digest, .. } => digest,
538 };
539
540 let Some(public_digest) = public_digest else {
541 verdict.notes.push(format!(
542 "https://{} answers, but serves no publish beacon, so the public path could not \
543 be COMPARED against the backend — only that something is there",
544 verdict.hostname
545 ));
546 continue;
547 };
548
549 let mut compared = false;
550 for endpoint in &verdict.endpoints {
551 match readings.backends.get(&endpoint.address) {
552 Some(BeaconFetch::Answered {
553 digest: Some(backend_digest),
554 ..
555 }) => {
556 compared = true;
557 if backend_digest != public_digest {
558 verdict.findings.push(VerifyFinding::PublicBackendDivergence {
559 hostname: verdict.hostname.clone(),
560 public_digest: public_digest.clone(),
561 address: endpoint.address.clone(),
562 backend_digest: backend_digest.clone(),
563 });
564 }
565 }
566 // A backend that answers without a beacon, or does not answer
567 // at all, leaves nothing to compare against. Not a finding of
568 // its own: `verify_collation` already dialed it and said what
569 // it found, and a second opinion phrased as an error would
570 // double-count one fact.
571 _ => {}
572 }
573 }
574
575 if !compared {
576 verdict.notes.push(format!(
577 "https://{} answers with a publish beacon, but no discovered backend served one \
578 to compare it against, so this proves the hostname is up and NOT that it is \
579 fronting the discovered backend",
580 verdict.hostname
581 ));
582 } else if doors_per_hostname
583 .get(&verdict.hostname)
584 .copied()
585 .unwrap_or(0)
586 > 1
587 {
588 // Only worth saying once a comparison actually happened: on a rule
589 // where nothing could be compared, the note above is the finding
590 // and this one is noise stacked on top of it.
591 verdict.notes.push(format!(
592 "{} is published through more than one front door and the public fetch went \
593 wherever DNS sent it, so this compares ONE of them",
594 verdict.hostname
595 ));
596 }
597 }
598}
599
600/// Fill one plan's rules from a live fanout **without stopping at the first
601/// rule that cannot be dialed**, recording how each one resolved.
602///
603/// Deliberately not [`IngressPlan::resolve_upstreams_from`], though it applies
604/// the same precedence (a rule that already has an address keeps it — the pin
605/// always wins) and reads the same `upstreams_for`. That method `bail!`s on the
606/// first undialable rule, which is right for an *apply*: publishing a front door
607/// that is 80% correct is worse than publishing none. It is wrong for a
608/// *verifier*, whose entire job is the complete picture — an operator who fixes
609/// one rule and re-runs only to be told about the next one has been handed a
610/// linked list instead of a report.
611///
612/// Ports must already be resolved ([`IngressPlan::resolve_ports_from`]):
613/// discovery matches records by port, so a portless rule resolves no address
614/// either and is reported as both.
615pub fn resolve_upstreams_reporting(
616 plan: &mut IngressPlan,
617 fanout: &ServiceRecordFanout,
618) -> Vec<RuleResolution> {
619 let mut out = Vec::new();
620 for rule in &mut plan.rules {
621 let pinned = !rule.upstream_hosts.is_empty();
622 if !pinned {
623 rule.upstream_hosts = fanout.upstreams_for(rule);
624 }
625 let (covered, missing) = placement_coverage(rule, fanout);
626 out.push(RuleResolution {
627 hostname: rule.hostname.clone(),
628 slot: rule.slot.clone(),
629 pinned,
630 covered,
631 missing,
632 });
633 }
634 out
635}
636
637/// Which of a rule's declared placement nodes actually supplied a backend, and
638/// why each of the others did not.
639///
640/// Mirrors [`ServiceRecordFanout::upstreams_for`]'s matching (by port, scoped to
641/// the rule's placement) so the two cannot disagree about what "covered" means —
642/// it answers *which nodes* produced that method's addresses, one level of
643/// detail below what it returns.
644fn placement_coverage(
645 rule: &IngressRule,
646 fanout: &ServiceRecordFanout,
647) -> (Vec<String>, Vec<(String, String)>) {
648 let mut covered = Vec::new();
649 let mut missing = Vec::new();
650 for machine in &rule.machines {
651 match fanout.nodes.get(machine.as_str()) {
652 None => missing.push((
653 machine.clone(),
654 "was never asked — it is not in the set this read fanned out over".to_string(),
655 )),
656 Some(visibility) => match visibility.unknown_reason() {
657 Some(reason) => missing.push((machine.clone(), reason.to_string())),
658 None => {
659 let serving = rule.port.is_some_and(|port| {
660 visibility
661 .records()
662 .iter()
663 .any(|r| r.ports.contains(&port))
664 });
665 if serving {
666 covered.push(machine.clone());
667 } else {
668 missing.push((
669 machine.clone(),
670 match rule.port {
671 Some(port) => format!(
672 "answered, and reports no ready record on port {port}"
673 ),
674 None => "the rule has no resolved port to match a record on"
675 .to_string(),
676 },
677 ));
678 }
679 }
680 },
681 }
682 }
683 (covered, missing)
684}
685
686/// Verify every rule on every collated front door: check the resolution, then
687/// dial what it produced.
688///
689/// `dial` is the caller's TCP connect. Each distinct address is dialed **once**
690/// — a rule published through two front doors is the same backend twice, and
691/// two connects would be two chances to disagree about one fact.
692///
693/// `read_note` is [`ServiceRecordFanout::unknown_note`]: when the fanout could
694/// not see part of the fleet, an empty resolution is `UNKNOWN`, not `absent`,
695/// and every [`VerifyFinding::NoBackend`] says so rather than asserting the
696/// workload is down.
697///
698/// Verdicts come back in front-door order, then rule order — the same walk the
699/// collation itself renders in, so a caller may stream the two side by side
700/// rather than looking each verdict up.
701pub fn verify_collation<D>(
702 collation: &Collation,
703 resolutions: &RuleResolutions,
704 read_note: Option<&str>,
705 mut dial: D,
706) -> VerifyReport
707where
708 D: FnMut(&str) -> DialOutcome,
709{
710 let mut dialed: BTreeMap<String, DialOutcome> = BTreeMap::new();
711 let mut verdicts = Vec::new();
712
713 for door in &collation.front_doors {
714 for rule in &door.rules {
715 let mut findings = Vec::new();
716 let mut notes = Vec::new();
717 let key = (rule.hostname.clone(), rule.slot.clone());
718 let resolution = resolutions.get(&key);
719
720 let pinned = resolution.is_some_and(|r| r.pinned);
721 if resolution.is_none() {
722 findings.push(VerifyFinding::Unresolved);
723 }
724
725 if rule.port.is_none() {
726 findings.push(VerifyFinding::PortUnresolved);
727 }
728
729 if rule.upstream_hosts.is_empty() {
730 findings.push(VerifyFinding::NoBackend {
731 detail: match read_note {
732 Some(note) => format!(
733 "and the discovery read was PARTIAL, so this is UNKNOWN rather than \
734 empty: {note}"
735 ),
736 None => "every node in scope answered and none reports a ready record \
737 for it, so the workload is not serving"
738 .to_string(),
739 },
740 });
741 } else if let Some(res) = resolution {
742 if !res.missing.is_empty() {
743 if pinned {
744 // The pin overrides placement, so a gap is not a
745 // shortfall in what gets published — but it IS the
746 // shape that hid the 127.0.0.1 drift, so it is said
747 // out loud rather than dropped.
748 notes.push(format!(
749 "slot pins `upstream_host`, so this dialed a DECLARATION, not a \
750 discovered address; {} placement node(s) report no ready record \
751 for it: {}",
752 res.missing.len(),
753 res.missing
754 .iter()
755 .map(|(m, why)| format!("{m} ({why})"))
756 .collect::<Vec<_>>()
757 .join("; ")
758 ));
759 } else {
760 findings.push(VerifyFinding::PartialPlacement {
761 covered: res.covered.clone(),
762 missing: res.missing.clone(),
763 });
764 }
765 }
766 }
767
768 let mut endpoints = Vec::new();
769 if let Ok(addrs) = rule.upstreams() {
770 for address in addrs {
771 let outcome = match dialed.get(&address) {
772 Some(prior) => prior.clone(),
773 None => {
774 let outcome = dial(&address);
775 dialed.insert(address.clone(), outcome.clone());
776 outcome
777 }
778 };
779 if let DialOutcome::Closed(why) = &outcome {
780 findings.push(VerifyFinding::Unreachable {
781 address: address.clone(),
782 why: why.clone(),
783 from_pin: pinned,
784 });
785 }
786 endpoints.push(EndpointCheck { address, outcome });
787 }
788 }
789
790 verdicts.push(RuleVerdict {
791 machine: door.machine.clone(),
792 provider: door.provider.as_str().to_string(),
793 hostname: rule.hostname.clone(),
794 slot: rule.slot.clone(),
795 port: rule.port,
796 pinned,
797 endpoints,
798 findings,
799 notes,
800 });
801 }
802 }
803
804 VerifyReport { verdicts }
805}
806
807#[cfg(test)]
808mod tests {
809 use super::*;
810 use crate::config::IngressProvider;
811 use crate::reconciler::ingress::{collate_front_doors, PlannedEdge};
812 use crate::reconciler::service_discovery::{DiscoveredRecord, UnknownReason};
813
814 fn rule(hostname: &str, port: Option<u16>, machines: &[&str]) -> IngressRule {
815 IngressRule {
816 hostname: hostname.to_string(),
817 port,
818 slot: "bundle".to_string(),
819 provider_id: None,
820 machines: machines.iter().map(|m| m.to_string()).collect(),
821 upstream_hosts: vec![],
822 }
823 }
824
825 fn plan(rules: Vec<IngressRule>, front_doors: &[&str]) -> IngressPlan {
826 IngressPlan {
827 provider: IngressProvider::Passway,
828 rules,
829 front_doors: front_doors.iter().map(|m| m.to_string()).collect(),
830 tunnel_id: None,
831 edge_provider_id: None,
832 }
833 }
834
835 fn record(ident: &str, mesh_ip: &str, ports: &[u16]) -> DiscoveredRecord {
836 DiscoveredRecord {
837 ident: ident.to_string(),
838 mesh_ip: mesh_ip.to_string(),
839 ports: ports.to_vec(),
840 named_ports: Default::default(),
841 }
842 }
843
844 /// Plan → resolve → collate → verify, the whole pipeline the CLI runs.
845 fn run(
846 mut plans: Vec<(&str, &str, IngressPlan)>,
847 fanout: &ServiceRecordFanout,
848 dial: impl FnMut(&str) -> DialOutcome,
849 ) -> VerifyReport {
850 let mut resolutions = RuleResolutions::new();
851 let mut planned = Vec::new();
852 for (service, env, plan) in &mut plans {
853 for res in resolve_upstreams_reporting(plan, fanout) {
854 resolutions.insert(res.key(), res);
855 }
856 planned.push(PlannedEdge {
857 service: service.to_string(),
858 env: env.to_string(),
859 plan: plan.clone(),
860 });
861 }
862 let collation = collate_front_doors(&planned).unwrap();
863 let note = fanout.unknown_note();
864 verify_collation(&collation, &resolutions, note.as_deref(), dial)
865 }
866
867 fn always_open(_: &str) -> DialOutcome {
868 DialOutcome::Open { millis: 1 }
869 }
870
871 #[test]
872 fn a_resolved_rule_whose_endpoint_answers_is_clean() {
873 let mut fanout = ServiceRecordFanout::default();
874 fanout.push_answer(
875 "us-east-001",
876 vec![record("yah-marketing", "100.64.0.3", &[8080])],
877 );
878
879 let report = run(
880 vec![(
881 "yah-marketing",
882 "cloud",
883 plan(
884 vec![rule("yah.dev", Some(8080), &["us-east-001"])],
885 &["us-east-001"],
886 ),
887 )],
888 &fanout,
889 always_open,
890 );
891
892 assert!(report.is_clean(), "{:#?}", report.verdicts);
893 assert_eq!(report.verdicts.len(), 1);
894 assert_eq!(report.verdicts[0].addresses(), vec!["100.64.0.3:8080"]);
895 assert!(!report.verdicts[0].pinned);
896 }
897
898 #[test]
899 fn a_ready_record_whose_address_refuses_is_a_failure() {
900 // R844-B11 in miniature: the record is Ready and every offline check
901 // passes; only the connect knows.
902 let mut fanout = ServiceRecordFanout::default();
903 fanout.push_answer(
904 "us-west-001",
905 vec![record("yah-marketing", "100.64.0.3", &[4325])],
906 );
907
908 let report = run(
909 vec![(
910 "yah-marketing",
911 "cloud",
912 plan(
913 vec![rule("yah.dev", Some(4325), &["us-west-001"])],
914 &["us-west-001"],
915 ),
916 )],
917 &fanout,
918 |_| DialOutcome::Closed("connection refused".to_string()),
919 );
920
921 assert!(!report.is_clean());
922 let msg = report.verdicts[0].findings[0].message();
923 assert!(msg.contains("100.64.0.3:4325"), "{msg}");
924 assert!(msg.contains("connection refused"), "{msg}");
925 assert!(
926 msg.contains("OPINION"),
927 "a DISCOVERED address that refuses means the record and the world \
928 disagree, and the message has to say which: {msg}"
929 );
930 }
931
932 #[test]
933 fn an_unreachable_pin_blames_the_toml_not_a_service_record() {
934 // Found by running the verb against a mirror pinning a dead address:
935 // the message told the operator a service record disagreed with the
936 // world, when nothing had discovered anything — the address came
937 // straight off the slot, and that is the file to open.
938 let mut fanout = ServiceRecordFanout::default();
939 fanout.push_answer(
940 "us-east-001",
941 vec![record("yah-marketing", "100.64.0.3", &[8080])],
942 );
943
944 let mut pinned_rule = rule("yah.dev", Some(8080), &["us-east-001"]);
945 pinned_rule.upstream_hosts = vec!["100.64.0.99".to_string()];
946
947 let report = run(
948 vec![(
949 "yah-marketing",
950 "cloud",
951 plan(vec![pinned_rule], &["us-east-001"]),
952 )],
953 &fanout,
954 |_| DialOutcome::Closed("connection timed out".to_string()),
955 );
956
957 let msg = report.verdicts[0].findings[0].message();
958 assert!(msg.contains("upstream_host"), "{msg}");
959 assert!(
960 !msg.contains("OPINION"),
961 "no record was consulted, so none can be blamed: {msg}"
962 );
963 }
964
965 #[test]
966 fn resolving_a_subset_of_the_declared_placement_fails() {
967 // The failure class this verb exists to remove: one of two nodes
968 // answers, the rule renders and dials fine, and half the front door is
969 // silently absent.
970 let mut fanout = ServiceRecordFanout::default();
971 fanout.push_answer(
972 "us-east-001",
973 vec![record("yah-marketing", "100.64.0.3", &[8080])],
974 );
975 fanout.push_answer("us-west-001", vec![]);
976
977 let report = run(
978 vec![(
979 "yah-marketing",
980 "cloud",
981 plan(
982 vec![rule("yah.dev", Some(8080), &["us-east-001", "us-west-001"])],
983 &["us-east-001"],
984 ),
985 )],
986 &fanout,
987 always_open,
988 );
989
990 assert!(!report.is_clean(), "a subset must not pass");
991 let msg = report.verdicts[0].findings[0].message();
992 assert!(msg.contains("1 of 2"), "{msg}");
993 assert!(msg.contains("us-west-001"), "{msg}");
994 // The address it DID resolve is still reported — a failure that hides
995 // the working half is a worse report, not a stricter one.
996 assert_eq!(report.verdicts[0].addresses(), vec!["100.64.0.3:8080"]);
997 }
998
999 #[test]
1000 fn an_unseen_placement_node_fails_as_unknown_not_as_down() {
1001 let mut fanout = ServiceRecordFanout::default();
1002 fanout.push_answer(
1003 "us-east-001",
1004 vec![record("yah-marketing", "100.64.0.3", &[8080])],
1005 );
1006 fanout.push_unknown(
1007 "us-west-001",
1008 UnknownReason::Unreachable("no route to host".to_string()),
1009 );
1010
1011 let report = run(
1012 vec![(
1013 "yah-marketing",
1014 "cloud",
1015 plan(
1016 vec![rule("yah.dev", Some(8080), &["us-east-001", "us-west-001"])],
1017 &["us-east-001"],
1018 ),
1019 )],
1020 &fanout,
1021 always_open,
1022 );
1023
1024 assert!(!report.is_clean());
1025 let msg = report.verdicts[0].findings[0].message();
1026 assert!(msg.contains("us-west-001"), "{msg}");
1027 assert!(msg.contains("no route to host"), "{msg}");
1028 }
1029
1030 #[test]
1031 fn a_rule_with_no_record_anywhere_reports_no_backend_on_a_complete_read() {
1032 let mut fanout = ServiceRecordFanout::default();
1033 fanout.push_answer("us-east-001", vec![]);
1034
1035 let report = run(
1036 vec![(
1037 "yah-marketing",
1038 "cloud",
1039 plan(
1040 vec![rule("yah.dev", Some(8080), &["us-east-001"])],
1041 &["us-east-001"],
1042 ),
1043 )],
1044 &fanout,
1045 always_open,
1046 );
1047
1048 assert!(!report.is_clean());
1049 let msg = report.verdicts[0].findings[0].message();
1050 assert!(msg.contains("not serving"), "{msg}");
1051 assert!(
1052 !msg.contains("PARTIAL"),
1053 "a complete read must not hedge: {msg}"
1054 );
1055 }
1056
1057 #[test]
1058 fn a_rule_with_no_record_on_a_partial_read_says_unknown_rather_than_down() {
1059 let mut fanout = ServiceRecordFanout::default();
1060 fanout.push_unknown(
1061 "us-east-001",
1062 UnknownReason::EndpointAbsent("GET … returned 404".to_string()),
1063 );
1064
1065 let report = run(
1066 vec![(
1067 "yah-marketing",
1068 "cloud",
1069 plan(
1070 vec![rule("yah.dev", Some(8080), &["us-east-001"])],
1071 &["us-east-001"],
1072 ),
1073 )],
1074 &fanout,
1075 always_open,
1076 );
1077
1078 assert!(!report.is_clean());
1079 let msg = report.verdicts[0].findings[0].message();
1080 assert!(msg.contains("UNKNOWN"), "{msg}");
1081 assert!(msg.contains("404"), "{msg}");
1082 }
1083
1084 #[test]
1085 fn a_portless_rule_reports_both_halves_rather_than_only_the_address() {
1086 // The `fronted = true` shape with nothing to match on: it must not read
1087 // as "the address is missing" alone.
1088 let mut fanout = ServiceRecordFanout::default();
1089 fanout.push_answer("us-east-001", vec![]);
1090
1091 let report = run(
1092 vec![(
1093 "yah-marketing",
1094 "cloud",
1095 plan(vec![rule("yah.dev", None, &["us-east-001"])], &["us-east-001"]),
1096 )],
1097 &fanout,
1098 always_open,
1099 );
1100
1101 let findings = &report.verdicts[0].findings;
1102 assert!(
1103 findings
1104 .iter()
1105 .any(|f| matches!(f, VerifyFinding::PortUnresolved)),
1106 "{findings:#?}"
1107 );
1108 assert!(
1109 findings
1110 .iter()
1111 .any(|f| matches!(f, VerifyFinding::NoBackend { .. })),
1112 "{findings:#?}"
1113 );
1114 }
1115
1116 #[test]
1117 fn a_pinned_upstream_is_dialed_and_flagged_as_a_declaration() {
1118 // The 127.0.0.1 case. The pin wins, so the dial is still performed —
1119 // but the verdict has to say the address came from a TOML, and the
1120 // placement gap it papers over has to be visible.
1121 let mut fanout = ServiceRecordFanout::default();
1122 fanout.push_answer("us-east-001", vec![]);
1123
1124 let mut pinned_rule = rule("yah.dev", Some(8080), &["us-east-001"]);
1125 pinned_rule.upstream_hosts = vec!["127.0.0.1".to_string()];
1126
1127 let mut dialed: Vec<String> = Vec::new();
1128 let report = run(
1129 vec![(
1130 "yah-marketing",
1131 "cloud",
1132 plan(vec![pinned_rule], &["us-east-001"]),
1133 )],
1134 &fanout,
1135 |addr| {
1136 dialed.push(addr.to_string());
1137 DialOutcome::Open { millis: 1 }
1138 },
1139 );
1140
1141 assert_eq!(dialed, vec!["127.0.0.1:8080"]);
1142 let v = &report.verdicts[0];
1143 assert!(v.pinned);
1144 assert!(v.is_ok(), "a pin that answers is not a failure: {v:#?}");
1145 assert_eq!(v.notes.len(), 1, "{:#?}", v.notes);
1146 assert!(v.notes[0].contains("DECLARATION"), "{}", v.notes[0]);
1147 assert!(v.notes[0].contains("us-east-001"), "{}", v.notes[0]);
1148 }
1149
1150 #[test]
1151 fn one_backend_published_through_two_front_doors_is_dialed_once() {
1152 let mut fanout = ServiceRecordFanout::default();
1153 fanout.push_answer(
1154 "us-east-001",
1155 vec![record("yah-marketing", "100.64.0.3", &[8080])],
1156 );
1157
1158 let mut dialed: Vec<String> = Vec::new();
1159 let report = run(
1160 vec![(
1161 "yah-marketing",
1162 "cloud",
1163 plan(
1164 vec![rule("yah.dev", Some(8080), &["us-east-001"])],
1165 &["us-east-001", "us-west-001"],
1166 ),
1167 )],
1168 &fanout,
1169 |addr| {
1170 dialed.push(addr.to_string());
1171 DialOutcome::Open { millis: 1 }
1172 },
1173 );
1174
1175 assert_eq!(
1176 report.verdicts.len(),
1177 2,
1178 "both front doors publish the rule, so both are verified"
1179 );
1180 assert_eq!(dialed, vec!["100.64.0.3:8080"], "dialed twice");
1181 assert!(report.is_clean());
1182 }
1183
1184 #[test]
1185 fn every_rule_is_reported_even_after_one_of_them_fails() {
1186 // The reason this does not call `resolve_upstreams_from`: an operator
1187 // fixing one rule at a time is being handed a linked list.
1188 let mut fanout = ServiceRecordFanout::default();
1189 fanout.push_answer(
1190 "us-east-001",
1191 vec![record("yah-marketing", "100.64.0.3", &[8080])],
1192 );
1193
1194 let report = run(
1195 vec![(
1196 "yah-marketing",
1197 "cloud",
1198 plan(
1199 vec![
1200 rule("a.yah.dev", Some(9999), &["us-east-001"]),
1201 rule("b.yah.dev", Some(8080), &["us-east-001"]),
1202 ],
1203 &["us-east-001"],
1204 ),
1205 )],
1206 &fanout,
1207 always_open,
1208 );
1209
1210 assert_eq!(report.verdicts.len(), 2);
1211 let a = report
1212 .verdicts
1213 .iter()
1214 .find(|v| v.hostname == "a.yah.dev")
1215 .unwrap();
1216 let b = report
1217 .verdicts
1218 .iter()
1219 .find(|v| v.hostname == "b.yah.dev")
1220 .unwrap();
1221 assert!(!a.is_ok(), "the unresolvable rule fails");
1222 assert!(
1223 b.is_ok(),
1224 "and the rule after it is still resolved and dialed: {b:#?}"
1225 );
1226 }
1227
1228 // ── the public path (R844-F18) ───────────────────────────────────────────
1229
1230 /// The healthy apex: one hostname, one door, one backend, both sides
1231 /// serving the same publish.
1232 fn healthy_report() -> VerifyReport {
1233 let mut fanout = ServiceRecordFanout::default();
1234 fanout.push_answer(
1235 "us-east-001",
1236 vec![record("yah-marketing", "100.64.0.3", &[8080])],
1237 );
1238 run(
1239 vec![(
1240 "yah-marketing",
1241 "cloud",
1242 plan(
1243 vec![rule("yah.dev", Some(8080), &["us-east-001"])],
1244 &["us-east-001"],
1245 ),
1246 )],
1247 &fanout,
1248 always_open,
1249 )
1250 }
1251
1252 fn served(digest: &str) -> BeaconFetch {
1253 BeaconFetch::Answered {
1254 status: 200,
1255 digest: Some(digest.to_string()),
1256 }
1257 }
1258
1259 #[test]
1260 fn matching_beacons_on_both_sides_leave_the_verdict_clean() {
1261 let mut report = healthy_report();
1262 let readings = PublicReadings {
1263 public: [("yah.dev".to_string(), served("abc123"))]
1264 .into_iter()
1265 .collect(),
1266 backends: [("100.64.0.3:8080".to_string(), served("abc123"))]
1267 .into_iter()
1268 .collect(),
1269 };
1270 apply_public_path(&mut report, &readings);
1271 assert!(report.is_clean(), "{:#?}", report.verdicts);
1272 assert!(
1273 report.verdicts[0].notes.is_empty(),
1274 "a fully compared rule has nothing to caveat: {:?}",
1275 report.verdicts[0].notes
1276 );
1277 }
1278
1279 /// THE 2026-09-03 OUTAGE, reproduced as a unit test. Every mesh signal is
1280 /// green — the record is correct, the address answers, `verify_collation`
1281 /// alone reports the rule clean — and the public gets a 503 because the
1282 /// running front door is still dialing the port the workload left.
1283 #[test]
1284 fn a_503_at_the_apex_fails_a_rule_whose_mesh_side_is_entirely_green() {
1285 let mut report = healthy_report();
1286 assert!(
1287 report.is_clean(),
1288 "precondition: the mesh side is what reported success during the outage"
1289 );
1290
1291 let readings = PublicReadings {
1292 public: [(
1293 "yah.dev".to_string(),
1294 BeaconFetch::Answered {
1295 status: 503,
1296 digest: None,
1297 },
1298 )]
1299 .into_iter()
1300 .collect(),
1301 backends: [("100.64.0.3:8080".to_string(), served("abc123"))]
1302 .into_iter()
1303 .collect(),
1304 };
1305 apply_public_path(&mut report, &readings);
1306
1307 assert!(!report.is_clean());
1308 let msg = report.verdicts[0].findings[0].message();
1309 assert!(msg.contains("503"), "{msg}");
1310 assert!(
1311 msg.contains("yah.dev"),
1312 "the finding names the hostname the public dials: {msg}"
1313 );
1314 }
1315
1316 /// The stale-serve shape: the hostname answers 200 with a real page, so
1317 /// nothing short of comparing publishes can see it. This is the failure
1318 /// that froze the apex for nineteen days.
1319 #[test]
1320 fn a_200_serving_a_different_publish_than_the_backend_is_a_failure() {
1321 let mut report = healthy_report();
1322 let readings = PublicReadings {
1323 public: [("yah.dev".to_string(), served("old-digest"))]
1324 .into_iter()
1325 .collect(),
1326 backends: [("100.64.0.3:8080".to_string(), served("new-digest"))]
1327 .into_iter()
1328 .collect(),
1329 };
1330 apply_public_path(&mut report, &readings);
1331
1332 assert!(!report.is_clean());
1333 let msg = report.verdicts[0].findings[0].message();
1334 assert!(msg.contains("old-digest"), "{msg}");
1335 assert!(msg.contains("new-digest"), "{msg}");
1336 assert!(
1337 msg.contains("100.64.0.3:8080"),
1338 "and it names the backend it compared against: {msg}"
1339 );
1340 }
1341
1342 #[test]
1343 fn a_public_path_that_does_not_answer_at_all_is_a_failure() {
1344 let mut report = healthy_report();
1345 let readings = PublicReadings {
1346 public: [(
1347 "yah.dev".to_string(),
1348 BeaconFetch::Failed("dns error: no record".to_string()),
1349 )]
1350 .into_iter()
1351 .collect(),
1352 backends: [("100.64.0.3:8080".to_string(), served("abc123"))]
1353 .into_iter()
1354 .collect(),
1355 };
1356 apply_public_path(&mut report, &readings);
1357
1358 assert!(!report.is_clean());
1359 assert!(report.verdicts[0].findings[0]
1360 .message()
1361 .contains("dns error"));
1362 }
1363
1364 /// A hostname fronting something that is not a mesofact publish has no
1365 /// beacon to compare. That is a limit on the CHECK, not a fault of the
1366 /// host — so it is a note, and the rule stays clean rather than failing
1367 /// every non-bundle hostname in the fleet.
1368 #[test]
1369 fn a_hostname_with_no_beacon_is_noted_and_not_failed() {
1370 let mut report = healthy_report();
1371 let readings = PublicReadings {
1372 public: [(
1373 "yah.dev".to_string(),
1374 BeaconFetch::Answered {
1375 status: 200,
1376 digest: None,
1377 },
1378 )]
1379 .into_iter()
1380 .collect(),
1381 backends: [("100.64.0.3:8080".to_string(), served("abc123"))]
1382 .into_iter()
1383 .collect(),
1384 };
1385 apply_public_path(&mut report, &readings);
1386
1387 assert!(report.is_clean(), "{:#?}", report.verdicts);
1388 assert!(
1389 report.verdicts[0]
1390 .notes
1391 .iter()
1392 .any(|n| n.contains("no publish beacon")),
1393 "{:?}",
1394 report.verdicts[0].notes
1395 );
1396 }
1397
1398 /// The distinction this whole relay is about: "answers" is not "answers
1399 /// with the backend we just proved". A public 200 with no comparable
1400 /// backend must not read as a full pass.
1401 #[test]
1402 fn a_public_200_with_nothing_to_compare_says_so_rather_than_implying_a_match() {
1403 let mut report = healthy_report();
1404 let readings = PublicReadings {
1405 public: [("yah.dev".to_string(), served("abc123"))]
1406 .into_iter()
1407 .collect(),
1408 backends: BTreeMap::new(),
1409 };
1410 apply_public_path(&mut report, &readings);
1411
1412 assert!(report.is_clean());
1413 assert!(
1414 report.verdicts[0]
1415 .notes
1416 .iter()
1417 .any(|n| n.contains("NOT that it is fronting the discovered backend")),
1418 "{:?}",
1419 report.verdicts[0].notes
1420 );
1421 }
1422
1423 #[test]
1424 fn an_unmeasured_hostname_is_marked_as_unmeasured_not_as_passing() {
1425 let mut report = healthy_report();
1426 apply_public_path(&mut report, &PublicReadings::default());
1427
1428 assert!(report.is_clean(), "not checking is not failing");
1429 assert!(
1430 report.verdicts[0]
1431 .notes
1432 .iter()
1433 .any(|n| n.contains("was not checked")),
1434 "{:?}",
1435 report.verdicts[0].notes
1436 );
1437 }
1438
1439 /// DNS picks one door, so the comparison covers one door. Saying that is
1440 /// the difference between a caveat and a false claim of coverage.
1441 #[test]
1442 fn a_hostname_on_two_front_doors_is_noted_as_measured_at_only_one() {
1443 let mut fanout = ServiceRecordFanout::default();
1444 fanout.push_answer(
1445 "us-east-001",
1446 vec![record("yah-marketing", "100.64.0.3", &[8080])],
1447 );
1448 fanout.push_answer(
1449 "us-south-001",
1450 vec![record("yah-marketing", "100.64.0.2", &[8080])],
1451 );
1452 let mut report = run(
1453 vec![(
1454 "yah-marketing",
1455 "cloud",
1456 plan(
1457 vec![rule(
1458 "yah.dev",
1459 Some(8080),
1460 &["us-east-001", "us-south-001"],
1461 )],
1462 &["us-east-001", "us-south-001"],
1463 ),
1464 )],
1465 &fanout,
1466 always_open,
1467 );
1468 assert_eq!(report.verdicts.len(), 2, "one verdict per front door");
1469
1470 let readings = PublicReadings {
1471 public: [("yah.dev".to_string(), served("abc123"))]
1472 .into_iter()
1473 .collect(),
1474 backends: [
1475 ("100.64.0.3:8080".to_string(), served("abc123")),
1476 ("100.64.0.2:8080".to_string(), served("abc123")),
1477 ]
1478 .into_iter()
1479 .collect(),
1480 };
1481 apply_public_path(&mut report, &readings);
1482
1483 assert!(report.is_clean(), "{:#?}", report.verdicts);
1484 assert!(
1485 report
1486 .verdicts
1487 .iter()
1488 .all(|v| v.notes.iter().any(|n| n.contains("wherever DNS sent it"))),
1489 "{:#?}",
1490 report.verdicts
1491 );
1492 }
1493}