From b7e57ca9cffcfd39ca96b41700b5a0ae1a04f89c Mon Sep 17 00:00:00 2001 From: archipelago Date: Sat, 8 Aug 2026 07:08:38 -0400 Subject: [PATCH] fix(update): stop presenting the same server as two mirrors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both default update mirrors resolve to the SAME host — the primary by name over HTTPS, the fallback by IP over plain HTTP — while SystemUpdate.vue told the operator "Servers this node checks for updates. The primary is tried first; if it's slow or unreachable, the next one in the list is tried automatically." That promises availability redundancy the pair cannot provide: if the origin is down, both entries are down. Reported by the operator, who read the list and correctly concluded the fallback made no sense. The mechanism is fine and deliberate — it recovers a node whose DNS is broken or whose clock is wrong, both of which fail TLS while plain HTTP still works, and it is safe because the manifest carries an Ed25519 signature verified against the pinned release-root anchor, so transport integrity is not what protects the update. (That last part only became true once Workstream B pinned the anchor; before then this fallback would have been a real hole.) So the bug was the labelling, not the design: - Backend label "Direct (fallback)" -> "Same server, no DNS/TLS", and the comment now states plainly that it is the same host, what it recovers, and that real redundancy needs a different one. - UI copy now scopes the redundancy sentence to genuine mirrors and adds a paragraph saying the two built-in entries are one server, what the second actually recovers, that it does not help if the server is down, why an unencrypted fetch is acceptable, and how to get real redundancy. The relabel reaches existing nodes: force_ovh_update_primary rewrites labels for the two default URLs on every load, while the merge matches on URL and never on label — without that rewrite path a renamed default would have sat in the code and never propagated to a single deployed node. Noted inline so it is not re-broken. Verified: 40/40 update tests pass (including the mirror load/merge/strip ones), vue-tsc clean, build green, and the new copy is present in the freshly built SystemUpdate chunk. Nothing in the tree pinned the old label string. Co-Authored-By: Claude Opus 5 (1M context) --- core/archipelago/src/update.rs | 27 +++++++++++++++++++-------- neode-ui/src/views/SystemUpdate.vue | 5 ++++- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/core/archipelago/src/update.rs b/core/archipelago/src/update.rs index 69a3345d..bb7ce127 100644 --- a/core/archipelago/src/update.rs +++ b/core/archipelago/src/update.rs @@ -131,12 +131,19 @@ fn default_mirrors() -> Vec { url: DEFAULT_UPDATE_MANIFEST_URL.to_string(), label: "Archipelago Foundation".to_string(), }, - // Fallback, tried only if the named origin fails: a node whose DNS - // or clock is wrong (both break TLS) must still be able to update - // itself, and the signature check is what makes either source safe. + // NOT a second server — the SAME host as the entry above, reached by + // IP over plain HTTP instead of by name over TLS. It buys nothing if + // the origin is down; what it recovers is a node whose **DNS is + // broken or whose clock is wrong**, either of which fails TLS while + // plain HTTP still works. Safe because the manifest carries an Ed25519 + // signature verified against the pinned release-root anchor, so + // transport integrity is not what protects the update. + // + // Labelled explicitly so the UI cannot imply redundancy it doesn't + // provide. Real redundancy needs a mirror on a different host. UpdateMirror { url: LEGACY_UPDATE_MANIFEST_URL.to_string(), - label: "Direct (fallback)".to_string(), + label: "Same server, no DNS/TLS".to_string(), }, ] } @@ -210,12 +217,16 @@ fn force_ovh_update_primary(list: &mut Vec) { if mirror.url == DEFAULT_UPDATE_MANIFEST_URL { mirror.label = "Archipelago Foundation".to_string(); } else if mirror.url == LEGACY_UPDATE_MANIFEST_URL { - mirror.label = "Direct (fallback)".to_string(); + // Rewritten on every load, so relabelling here reaches nodes that + // already have the old "Direct (fallback)" text saved in their + // update-mirrors.json — the merge below matches on URL, never on + // label, so without this a renamed default would never propagate. + mirror.label = "Same server, no DNS/TLS".to_string(); } } - // Named origin first, its IP fallback second, anything the operator - // added after that. Ordering matters: the list is tried in order, so a - // stale entry sitting first costs a timeout on every check. + // Named origin first, its same-host IP fallback second, anything the + // operator added after that. Ordering matters: the list is tried in order, + // so a stale entry sitting first costs a timeout on every check. list.sort_by_key(|m| match m.url.as_str() { u if u == DEFAULT_UPDATE_MANIFEST_URL => 0, u if u == LEGACY_UPDATE_MANIFEST_URL => 1, diff --git a/neode-ui/src/views/SystemUpdate.vue b/neode-ui/src/views/SystemUpdate.vue index f9f9436c..09c608d8 100644 --- a/neode-ui/src/views/SystemUpdate.vue +++ b/neode-ui/src/views/SystemUpdate.vue @@ -254,7 +254,10 @@ >+ Add mirror

- Servers this node checks for updates. The primary is tried first; if it's slow or unreachable, the next one in the list is tried automatically. Downloads always come from the mirror that served the manifest — switching primary switches where files come from. + Sources this node checks for updates. The primary is tried first; if it's slow or unreachable, the next one is tried automatically. Downloads always come from the source that served the manifest — switching primary switches where files come from. +

+

+ The two built-in entries are the same server, not two servers: the second reaches it by IP without DNS or TLS, which recovers a node whose DNS is broken or whose clock is wrong. It does not help if the server itself is down. Every update is signature-checked whichever source serves it, so an unencrypted fetch can't substitute a tampered build. For real redundancy, add a mirror on a different host.