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 image: None,
833 }
834 }
835
836 fn record(ident: &str, mesh_ip: &str, ports: &[u16]) -> DiscoveredRecord {
837 DiscoveredRecord {
838 ident: ident.to_string(),
839 mesh_ip: mesh_ip.to_string(),
840 ports: ports.to_vec(),
841 named_ports: Default::default(),
842 }
843 }
844
845 /// Plan → resolve → collate → verify, the whole pipeline the CLI runs.
846 fn run(
847 mut plans: Vec<(&str, &str, IngressPlan)>,
848 fanout: &ServiceRecordFanout,
849 dial: impl FnMut(&str) -> DialOutcome,
850 ) -> VerifyReport {
851 let mut resolutions = RuleResolutions::new();
852 let mut planned = Vec::new();
853 for (service, env, plan) in &mut plans {
854 for res in resolve_upstreams_reporting(plan, fanout) {
855 resolutions.insert(res.key(), res);
856 }
857 planned.push(PlannedEdge {
858 service: service.to_string(),
859 env: env.to_string(),
860 plan: plan.clone(),
861 });
862 }
863 let collation = collate_front_doors(&planned).unwrap();
864 let note = fanout.unknown_note();
865 verify_collation(&collation, &resolutions, note.as_deref(), dial)
866 }
867
868 fn always_open(_: &str) -> DialOutcome {
869 DialOutcome::Open { millis: 1 }
870 }
871
872 #[test]
873 fn a_resolved_rule_whose_endpoint_answers_is_clean() {
874 let mut fanout = ServiceRecordFanout::default();
875 fanout.push_answer(
876 "us-east-001",
877 vec![record("yah-marketing", "100.64.0.3", &[8080])],
878 );
879
880 let report = run(
881 vec![(
882 "yah-marketing",
883 "cloud",
884 plan(
885 vec![rule("yah.dev", Some(8080), &["us-east-001"])],
886 &["us-east-001"],
887 ),
888 )],
889 &fanout,
890 always_open,
891 );
892
893 assert!(report.is_clean(), "{:#?}", report.verdicts);
894 assert_eq!(report.verdicts.len(), 1);
895 assert_eq!(report.verdicts[0].addresses(), vec!["100.64.0.3:8080"]);
896 assert!(!report.verdicts[0].pinned);
897 }
898
899 #[test]
900 fn a_ready_record_whose_address_refuses_is_a_failure() {
901 // R844-B11 in miniature: the record is Ready and every offline check
902 // passes; only the connect knows.
903 let mut fanout = ServiceRecordFanout::default();
904 fanout.push_answer(
905 "us-west-001",
906 vec![record("yah-marketing", "100.64.0.3", &[4325])],
907 );
908
909 let report = run(
910 vec![(
911 "yah-marketing",
912 "cloud",
913 plan(
914 vec![rule("yah.dev", Some(4325), &["us-west-001"])],
915 &["us-west-001"],
916 ),
917 )],
918 &fanout,
919 |_| DialOutcome::Closed("connection refused".to_string()),
920 );
921
922 assert!(!report.is_clean());
923 let msg = report.verdicts[0].findings[0].message();
924 assert!(msg.contains("100.64.0.3:4325"), "{msg}");
925 assert!(msg.contains("connection refused"), "{msg}");
926 assert!(
927 msg.contains("OPINION"),
928 "a DISCOVERED address that refuses means the record and the world \
929 disagree, and the message has to say which: {msg}"
930 );
931 }
932
933 #[test]
934 fn an_unreachable_pin_blames_the_toml_not_a_service_record() {
935 // Found by running the verb against a mirror pinning a dead address:
936 // the message told the operator a service record disagreed with the
937 // world, when nothing had discovered anything — the address came
938 // straight off the slot, and that is the file to open.
939 let mut fanout = ServiceRecordFanout::default();
940 fanout.push_answer(
941 "us-east-001",
942 vec![record("yah-marketing", "100.64.0.3", &[8080])],
943 );
944
945 let mut pinned_rule = rule("yah.dev", Some(8080), &["us-east-001"]);
946 pinned_rule.upstream_hosts = vec!["100.64.0.99".to_string()];
947
948 let report = run(
949 vec![(
950 "yah-marketing",
951 "cloud",
952 plan(vec![pinned_rule], &["us-east-001"]),
953 )],
954 &fanout,
955 |_| DialOutcome::Closed("connection timed out".to_string()),
956 );
957
958 let msg = report.verdicts[0].findings[0].message();
959 assert!(msg.contains("upstream_host"), "{msg}");
960 assert!(
961 !msg.contains("OPINION"),
962 "no record was consulted, so none can be blamed: {msg}"
963 );
964 }
965
966 #[test]
967 fn resolving_a_subset_of_the_declared_placement_fails() {
968 // The failure class this verb exists to remove: one of two nodes
969 // answers, the rule renders and dials fine, and half the front door is
970 // silently absent.
971 let mut fanout = ServiceRecordFanout::default();
972 fanout.push_answer(
973 "us-east-001",
974 vec![record("yah-marketing", "100.64.0.3", &[8080])],
975 );
976 fanout.push_answer("us-west-001", vec![]);
977
978 let report = run(
979 vec![(
980 "yah-marketing",
981 "cloud",
982 plan(
983 vec![rule("yah.dev", Some(8080), &["us-east-001", "us-west-001"])],
984 &["us-east-001"],
985 ),
986 )],
987 &fanout,
988 always_open,
989 );
990
991 assert!(!report.is_clean(), "a subset must not pass");
992 let msg = report.verdicts[0].findings[0].message();
993 assert!(msg.contains("1 of 2"), "{msg}");
994 assert!(msg.contains("us-west-001"), "{msg}");
995 // The address it DID resolve is still reported — a failure that hides
996 // the working half is a worse report, not a stricter one.
997 assert_eq!(report.verdicts[0].addresses(), vec!["100.64.0.3:8080"]);
998 }
999
1000 #[test]
1001 fn an_unseen_placement_node_fails_as_unknown_not_as_down() {
1002 let mut fanout = ServiceRecordFanout::default();
1003 fanout.push_answer(
1004 "us-east-001",
1005 vec![record("yah-marketing", "100.64.0.3", &[8080])],
1006 );
1007 fanout.push_unknown(
1008 "us-west-001",
1009 UnknownReason::Unreachable("no route to host".to_string()),
1010 );
1011
1012 let report = run(
1013 vec![(
1014 "yah-marketing",
1015 "cloud",
1016 plan(
1017 vec![rule("yah.dev", Some(8080), &["us-east-001", "us-west-001"])],
1018 &["us-east-001"],
1019 ),
1020 )],
1021 &fanout,
1022 always_open,
1023 );
1024
1025 assert!(!report.is_clean());
1026 let msg = report.verdicts[0].findings[0].message();
1027 assert!(msg.contains("us-west-001"), "{msg}");
1028 assert!(msg.contains("no route to host"), "{msg}");
1029 }
1030
1031 #[test]
1032 fn a_rule_with_no_record_anywhere_reports_no_backend_on_a_complete_read() {
1033 let mut fanout = ServiceRecordFanout::default();
1034 fanout.push_answer("us-east-001", vec![]);
1035
1036 let report = run(
1037 vec![(
1038 "yah-marketing",
1039 "cloud",
1040 plan(
1041 vec![rule("yah.dev", Some(8080), &["us-east-001"])],
1042 &["us-east-001"],
1043 ),
1044 )],
1045 &fanout,
1046 always_open,
1047 );
1048
1049 assert!(!report.is_clean());
1050 let msg = report.verdicts[0].findings[0].message();
1051 assert!(msg.contains("not serving"), "{msg}");
1052 assert!(
1053 !msg.contains("PARTIAL"),
1054 "a complete read must not hedge: {msg}"
1055 );
1056 }
1057
1058 #[test]
1059 fn a_rule_with_no_record_on_a_partial_read_says_unknown_rather_than_down() {
1060 let mut fanout = ServiceRecordFanout::default();
1061 fanout.push_unknown(
1062 "us-east-001",
1063 UnknownReason::EndpointAbsent("GET … returned 404".to_string()),
1064 );
1065
1066 let report = run(
1067 vec![(
1068 "yah-marketing",
1069 "cloud",
1070 plan(
1071 vec![rule("yah.dev", Some(8080), &["us-east-001"])],
1072 &["us-east-001"],
1073 ),
1074 )],
1075 &fanout,
1076 always_open,
1077 );
1078
1079 assert!(!report.is_clean());
1080 let msg = report.verdicts[0].findings[0].message();
1081 assert!(msg.contains("UNKNOWN"), "{msg}");
1082 assert!(msg.contains("404"), "{msg}");
1083 }
1084
1085 #[test]
1086 fn a_portless_rule_reports_both_halves_rather_than_only_the_address() {
1087 // The `fronted = true` shape with nothing to match on: it must not read
1088 // as "the address is missing" alone.
1089 let mut fanout = ServiceRecordFanout::default();
1090 fanout.push_answer("us-east-001", vec![]);
1091
1092 let report = run(
1093 vec![(
1094 "yah-marketing",
1095 "cloud",
1096 plan(vec![rule("yah.dev", None, &["us-east-001"])], &["us-east-001"]),
1097 )],
1098 &fanout,
1099 always_open,
1100 );
1101
1102 let findings = &report.verdicts[0].findings;
1103 assert!(
1104 findings
1105 .iter()
1106 .any(|f| matches!(f, VerifyFinding::PortUnresolved)),
1107 "{findings:#?}"
1108 );
1109 assert!(
1110 findings
1111 .iter()
1112 .any(|f| matches!(f, VerifyFinding::NoBackend { .. })),
1113 "{findings:#?}"
1114 );
1115 }
1116
1117 #[test]
1118 fn a_pinned_upstream_is_dialed_and_flagged_as_a_declaration() {
1119 // The 127.0.0.1 case. The pin wins, so the dial is still performed —
1120 // but the verdict has to say the address came from a TOML, and the
1121 // placement gap it papers over has to be visible.
1122 let mut fanout = ServiceRecordFanout::default();
1123 fanout.push_answer("us-east-001", vec![]);
1124
1125 let mut pinned_rule = rule("yah.dev", Some(8080), &["us-east-001"]);
1126 pinned_rule.upstream_hosts = vec!["127.0.0.1".to_string()];
1127
1128 let mut dialed: Vec<String> = Vec::new();
1129 let report = run(
1130 vec![(
1131 "yah-marketing",
1132 "cloud",
1133 plan(vec![pinned_rule], &["us-east-001"]),
1134 )],
1135 &fanout,
1136 |addr| {
1137 dialed.push(addr.to_string());
1138 DialOutcome::Open { millis: 1 }
1139 },
1140 );
1141
1142 assert_eq!(dialed, vec!["127.0.0.1:8080"]);
1143 let v = &report.verdicts[0];
1144 assert!(v.pinned);
1145 assert!(v.is_ok(), "a pin that answers is not a failure: {v:#?}");
1146 assert_eq!(v.notes.len(), 1, "{:#?}", v.notes);
1147 assert!(v.notes[0].contains("DECLARATION"), "{}", v.notes[0]);
1148 assert!(v.notes[0].contains("us-east-001"), "{}", v.notes[0]);
1149 }
1150
1151 #[test]
1152 fn one_backend_published_through_two_front_doors_is_dialed_once() {
1153 let mut fanout = ServiceRecordFanout::default();
1154 fanout.push_answer(
1155 "us-east-001",
1156 vec![record("yah-marketing", "100.64.0.3", &[8080])],
1157 );
1158
1159 let mut dialed: Vec<String> = Vec::new();
1160 let report = run(
1161 vec![(
1162 "yah-marketing",
1163 "cloud",
1164 plan(
1165 vec![rule("yah.dev", Some(8080), &["us-east-001"])],
1166 &["us-east-001", "us-west-001"],
1167 ),
1168 )],
1169 &fanout,
1170 |addr| {
1171 dialed.push(addr.to_string());
1172 DialOutcome::Open { millis: 1 }
1173 },
1174 );
1175
1176 assert_eq!(
1177 report.verdicts.len(),
1178 2,
1179 "both front doors publish the rule, so both are verified"
1180 );
1181 assert_eq!(dialed, vec!["100.64.0.3:8080"], "dialed twice");
1182 assert!(report.is_clean());
1183 }
1184
1185 #[test]
1186 fn every_rule_is_reported_even_after_one_of_them_fails() {
1187 // The reason this does not call `resolve_upstreams_from`: an operator
1188 // fixing one rule at a time is being handed a linked list.
1189 let mut fanout = ServiceRecordFanout::default();
1190 fanout.push_answer(
1191 "us-east-001",
1192 vec![record("yah-marketing", "100.64.0.3", &[8080])],
1193 );
1194
1195 let report = run(
1196 vec![(
1197 "yah-marketing",
1198 "cloud",
1199 plan(
1200 vec![
1201 rule("a.yah.dev", Some(9999), &["us-east-001"]),
1202 rule("b.yah.dev", Some(8080), &["us-east-001"]),
1203 ],
1204 &["us-east-001"],
1205 ),
1206 )],
1207 &fanout,
1208 always_open,
1209 );
1210
1211 assert_eq!(report.verdicts.len(), 2);
1212 let a = report
1213 .verdicts
1214 .iter()
1215 .find(|v| v.hostname == "a.yah.dev")
1216 .unwrap();
1217 let b = report
1218 .verdicts
1219 .iter()
1220 .find(|v| v.hostname == "b.yah.dev")
1221 .unwrap();
1222 assert!(!a.is_ok(), "the unresolvable rule fails");
1223 assert!(
1224 b.is_ok(),
1225 "and the rule after it is still resolved and dialed: {b:#?}"
1226 );
1227 }
1228
1229 // ── the public path (R844-F18) ───────────────────────────────────────────
1230
1231 /// The healthy apex: one hostname, one door, one backend, both sides
1232 /// serving the same publish.
1233 fn healthy_report() -> VerifyReport {
1234 let mut fanout = ServiceRecordFanout::default();
1235 fanout.push_answer(
1236 "us-east-001",
1237 vec![record("yah-marketing", "100.64.0.3", &[8080])],
1238 );
1239 run(
1240 vec![(
1241 "yah-marketing",
1242 "cloud",
1243 plan(
1244 vec![rule("yah.dev", Some(8080), &["us-east-001"])],
1245 &["us-east-001"],
1246 ),
1247 )],
1248 &fanout,
1249 always_open,
1250 )
1251 }
1252
1253 fn served(digest: &str) -> BeaconFetch {
1254 BeaconFetch::Answered {
1255 status: 200,
1256 digest: Some(digest.to_string()),
1257 }
1258 }
1259
1260 #[test]
1261 fn matching_beacons_on_both_sides_leave_the_verdict_clean() {
1262 let mut report = healthy_report();
1263 let readings = PublicReadings {
1264 public: [("yah.dev".to_string(), served("abc123"))]
1265 .into_iter()
1266 .collect(),
1267 backends: [("100.64.0.3:8080".to_string(), served("abc123"))]
1268 .into_iter()
1269 .collect(),
1270 };
1271 apply_public_path(&mut report, &readings);
1272 assert!(report.is_clean(), "{:#?}", report.verdicts);
1273 assert!(
1274 report.verdicts[0].notes.is_empty(),
1275 "a fully compared rule has nothing to caveat: {:?}",
1276 report.verdicts[0].notes
1277 );
1278 }
1279
1280 /// THE 2026-09-03 OUTAGE, reproduced as a unit test. Every mesh signal is
1281 /// green — the record is correct, the address answers, `verify_collation`
1282 /// alone reports the rule clean — and the public gets a 503 because the
1283 /// running front door is still dialing the port the workload left.
1284 #[test]
1285 fn a_503_at_the_apex_fails_a_rule_whose_mesh_side_is_entirely_green() {
1286 let mut report = healthy_report();
1287 assert!(
1288 report.is_clean(),
1289 "precondition: the mesh side is what reported success during the outage"
1290 );
1291
1292 let readings = PublicReadings {
1293 public: [(
1294 "yah.dev".to_string(),
1295 BeaconFetch::Answered {
1296 status: 503,
1297 digest: None,
1298 },
1299 )]
1300 .into_iter()
1301 .collect(),
1302 backends: [("100.64.0.3:8080".to_string(), served("abc123"))]
1303 .into_iter()
1304 .collect(),
1305 };
1306 apply_public_path(&mut report, &readings);
1307
1308 assert!(!report.is_clean());
1309 let msg = report.verdicts[0].findings[0].message();
1310 assert!(msg.contains("503"), "{msg}");
1311 assert!(
1312 msg.contains("yah.dev"),
1313 "the finding names the hostname the public dials: {msg}"
1314 );
1315 }
1316
1317 /// The stale-serve shape: the hostname answers 200 with a real page, so
1318 /// nothing short of comparing publishes can see it. This is the failure
1319 /// that froze the apex for nineteen days.
1320 #[test]
1321 fn a_200_serving_a_different_publish_than_the_backend_is_a_failure() {
1322 let mut report = healthy_report();
1323 let readings = PublicReadings {
1324 public: [("yah.dev".to_string(), served("old-digest"))]
1325 .into_iter()
1326 .collect(),
1327 backends: [("100.64.0.3:8080".to_string(), served("new-digest"))]
1328 .into_iter()
1329 .collect(),
1330 };
1331 apply_public_path(&mut report, &readings);
1332
1333 assert!(!report.is_clean());
1334 let msg = report.verdicts[0].findings[0].message();
1335 assert!(msg.contains("old-digest"), "{msg}");
1336 assert!(msg.contains("new-digest"), "{msg}");
1337 assert!(
1338 msg.contains("100.64.0.3:8080"),
1339 "and it names the backend it compared against: {msg}"
1340 );
1341 }
1342
1343 #[test]
1344 fn a_public_path_that_does_not_answer_at_all_is_a_failure() {
1345 let mut report = healthy_report();
1346 let readings = PublicReadings {
1347 public: [(
1348 "yah.dev".to_string(),
1349 BeaconFetch::Failed("dns error: no record".to_string()),
1350 )]
1351 .into_iter()
1352 .collect(),
1353 backends: [("100.64.0.3:8080".to_string(), served("abc123"))]
1354 .into_iter()
1355 .collect(),
1356 };
1357 apply_public_path(&mut report, &readings);
1358
1359 assert!(!report.is_clean());
1360 assert!(report.verdicts[0].findings[0]
1361 .message()
1362 .contains("dns error"));
1363 }
1364
1365 /// A hostname fronting something that is not a mesofact publish has no
1366 /// beacon to compare. That is a limit on the CHECK, not a fault of the
1367 /// host — so it is a note, and the rule stays clean rather than failing
1368 /// every non-bundle hostname in the fleet.
1369 #[test]
1370 fn a_hostname_with_no_beacon_is_noted_and_not_failed() {
1371 let mut report = healthy_report();
1372 let readings = PublicReadings {
1373 public: [(
1374 "yah.dev".to_string(),
1375 BeaconFetch::Answered {
1376 status: 200,
1377 digest: None,
1378 },
1379 )]
1380 .into_iter()
1381 .collect(),
1382 backends: [("100.64.0.3:8080".to_string(), served("abc123"))]
1383 .into_iter()
1384 .collect(),
1385 };
1386 apply_public_path(&mut report, &readings);
1387
1388 assert!(report.is_clean(), "{:#?}", report.verdicts);
1389 assert!(
1390 report.verdicts[0]
1391 .notes
1392 .iter()
1393 .any(|n| n.contains("no publish beacon")),
1394 "{:?}",
1395 report.verdicts[0].notes
1396 );
1397 }
1398
1399 /// The distinction this whole relay is about: "answers" is not "answers
1400 /// with the backend we just proved". A public 200 with no comparable
1401 /// backend must not read as a full pass.
1402 #[test]
1403 fn a_public_200_with_nothing_to_compare_says_so_rather_than_implying_a_match() {
1404 let mut report = healthy_report();
1405 let readings = PublicReadings {
1406 public: [("yah.dev".to_string(), served("abc123"))]
1407 .into_iter()
1408 .collect(),
1409 backends: BTreeMap::new(),
1410 };
1411 apply_public_path(&mut report, &readings);
1412
1413 assert!(report.is_clean());
1414 assert!(
1415 report.verdicts[0]
1416 .notes
1417 .iter()
1418 .any(|n| n.contains("NOT that it is fronting the discovered backend")),
1419 "{:?}",
1420 report.verdicts[0].notes
1421 );
1422 }
1423
1424 #[test]
1425 fn an_unmeasured_hostname_is_marked_as_unmeasured_not_as_passing() {
1426 let mut report = healthy_report();
1427 apply_public_path(&mut report, &PublicReadings::default());
1428
1429 assert!(report.is_clean(), "not checking is not failing");
1430 assert!(
1431 report.verdicts[0]
1432 .notes
1433 .iter()
1434 .any(|n| n.contains("was not checked")),
1435 "{:?}",
1436 report.verdicts[0].notes
1437 );
1438 }
1439
1440 /// DNS picks one door, so the comparison covers one door. Saying that is
1441 /// the difference between a caveat and a false claim of coverage.
1442 #[test]
1443 fn a_hostname_on_two_front_doors_is_noted_as_measured_at_only_one() {
1444 let mut fanout = ServiceRecordFanout::default();
1445 fanout.push_answer(
1446 "us-east-001",
1447 vec![record("yah-marketing", "100.64.0.3", &[8080])],
1448 );
1449 fanout.push_answer(
1450 "us-south-001",
1451 vec![record("yah-marketing", "100.64.0.2", &[8080])],
1452 );
1453 let mut report = run(
1454 vec![(
1455 "yah-marketing",
1456 "cloud",
1457 plan(
1458 vec![rule(
1459 "yah.dev",
1460 Some(8080),
1461 &["us-east-001", "us-south-001"],
1462 )],
1463 &["us-east-001", "us-south-001"],
1464 ),
1465 )],
1466 &fanout,
1467 always_open,
1468 );
1469 assert_eq!(report.verdicts.len(), 2, "one verdict per front door");
1470
1471 let readings = PublicReadings {
1472 public: [("yah.dev".to_string(), served("abc123"))]
1473 .into_iter()
1474 .collect(),
1475 backends: [
1476 ("100.64.0.3:8080".to_string(), served("abc123")),
1477 ("100.64.0.2:8080".to_string(), served("abc123")),
1478 ]
1479 .into_iter()
1480 .collect(),
1481 };
1482 apply_public_path(&mut report, &readings);
1483
1484 assert!(report.is_clean(), "{:#?}", report.verdicts);
1485 assert!(
1486 report
1487 .verdicts
1488 .iter()
1489 .all(|v| v.notes.iter().any(|n| n.contains("wherever DNS sent it"))),
1490 "{:#?}",
1491 report.verdicts
1492 );
1493 }
1494}