fix(fips): ship a second, reachable default anchor (once-and-for-all)
Nodes depended on a single public anchor (185.18.221.160) that is unreachable from most real networks — confirmed on the thinkpad WiFi and a fresh Framework node, both of which islanded (is_root=true, depth=0). Its DNS even resolves IPv6-first while the daemon is IPv4-only. Stand up an Archipelago-operated FIPS anchor on vps2 (146.59.87.168, the OTA host every node already reaches) and ship it as an additional default anchor. default_public_anchor() -> default_public_anchors() -> Vec; load() returns the set; the apply loop already dials each, so whichever the node's network can reach wins. Verified end-to-end: a fresh node joined the tree via vps2 in 3s (depth 1). anchors tests: 7/7 pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
7eb4d6a7bf
commit
0f13545375
@ -49,9 +49,7 @@ pub const DEFAULT_PUBLIC_ANCHOR_NPUB: &str =
|
||||
pub const DEFAULT_PUBLIC_ANCHOR_ADDR: &str = "185.18.221.160:8443";
|
||||
pub const DEFAULT_PUBLIC_ANCHOR_TRANSPORT: &str = "tcp";
|
||||
|
||||
/// The default public anchor as a ready-to-apply `SeedAnchor`. Carried
|
||||
/// implicitly by `load()` on nodes that have never edited their anchor
|
||||
/// list, so every node dials it without operator action.
|
||||
/// The upstream public anchor as a ready-to-apply `SeedAnchor`.
|
||||
pub fn default_public_anchor() -> SeedAnchor {
|
||||
SeedAnchor {
|
||||
npub: DEFAULT_PUBLIC_ANCHOR_NPUB.to_string(),
|
||||
@ -61,6 +59,38 @@ pub fn default_public_anchor() -> SeedAnchor {
|
||||
}
|
||||
}
|
||||
|
||||
// Archipelago-operated anchor on vps2 (the OTA/registry host, 146.59.87.168).
|
||||
// Every node already reaches this host for updates, so it is reachable from
|
||||
// networks that the upstream anchor is not — which is most of them (the
|
||||
// upstream anchor answers on one IPv4 that many home/office networks can't
|
||||
// reach, and its DNS resolves IPv6-first while the daemon is IPv4-only).
|
||||
// TCP because that traverses NAT/firewalls best; 8444 because 8443 on that
|
||||
// host is already taken by a container.
|
||||
pub const ARCHY_ANCHOR_NPUB: &str =
|
||||
"npub1dptaktwxv0mm245g2lqjykwm5ll0jpc6m3r4242ydfa9z7qe6urs3jvrak";
|
||||
pub const ARCHY_ANCHOR_ADDR: &str = "146.59.87.168:8444";
|
||||
pub const ARCHY_ANCHOR_TRANSPORT: &str = "tcp";
|
||||
|
||||
/// The Archipelago-operated anchor as a ready-to-apply `SeedAnchor`.
|
||||
pub fn archy_anchor() -> SeedAnchor {
|
||||
SeedAnchor {
|
||||
npub: ARCHY_ANCHOR_NPUB.to_string(),
|
||||
address: ARCHY_ANCHOR_ADDR.to_string(),
|
||||
transport: ARCHY_ANCHOR_TRANSPORT.to_string(),
|
||||
label: "Archipelago anchor (vps2)".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// The default anchor set carried implicitly by `load()` on nodes that have
|
||||
/// never edited their anchor list, so every node dials them without operator
|
||||
/// action. Multiple anchors so one unreachable rendezvous host can't strand a
|
||||
/// node: `fipsctl connect` is attempted for each, and whichever the node's
|
||||
/// network can reach wins. The Archipelago-operated anchor is listed first
|
||||
/// because it is reachable from the widest set of networks.
|
||||
pub fn default_public_anchors() -> Vec<SeedAnchor> {
|
||||
vec![archy_anchor(), default_public_anchor()]
|
||||
}
|
||||
|
||||
/// One seed-anchor entry. `address` must be directly dialable (IP or
|
||||
/// resolvable hostname + UDP port); `transport` is one of "udp", "tcp",
|
||||
/// "tor", "ethernet" (the values upstream `fipsctl connect` accepts).
|
||||
@ -94,7 +124,7 @@ fn anchors_path(data_dir: &Path) -> PathBuf {
|
||||
pub async fn load(data_dir: &Path) -> Result<Vec<SeedAnchor>> {
|
||||
let path = anchors_path(data_dir);
|
||||
if !path.exists() {
|
||||
return Ok(vec![default_public_anchor()]);
|
||||
return Ok(default_public_anchors());
|
||||
}
|
||||
let bytes = tokio::fs::read(&path)
|
||||
.await
|
||||
@ -268,28 +298,46 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn load_missing_seeds_default_public_anchor() {
|
||||
// A node that has never edited its anchor list should still get
|
||||
// the public anchor so it can bootstrap the mesh out of the box.
|
||||
async fn load_missing_seeds_default_public_anchors() {
|
||||
// A node that has never edited its anchor list should still get the
|
||||
// full default anchor set so it can bootstrap the mesh out of the box.
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let got = load(dir.path()).await.unwrap();
|
||||
assert_eq!(got, vec![default_public_anchor()]);
|
||||
// ...and the default must be the TCP/8443 form, not the dead udp:8668.
|
||||
assert_eq!(got[0].transport, "tcp");
|
||||
assert!(got[0].address.ends_with(":8443"));
|
||||
assert_eq!(got, default_public_anchors());
|
||||
// The Archipelago-operated anchor must come first (widest reachability)
|
||||
// and the upstream anchor must remain present as a fallback.
|
||||
assert_eq!(got[0], archy_anchor());
|
||||
assert!(got.contains(&default_public_anchor()));
|
||||
// Every default must be a TCP form (traverses NAT/firewalls), never the
|
||||
// dead udp:8668 the upstream anchor never answers on.
|
||||
assert!(got.iter().all(|a| a.transport == "tcp"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn removing_default_persists_as_empty() {
|
||||
// Once the operator removes the default, a file exists and is
|
||||
// authoritative — we must not silently re-seed it on next load.
|
||||
async fn removing_one_default_persists_and_keeps_the_other() {
|
||||
// Editing the anchor list (here removing one default) makes the file
|
||||
// authoritative: the removed anchor must not be silently re-seeded on
|
||||
// next load, and the remaining default must stay.
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let list = remove(dir.path(), ARCHY_ANCHOR_NPUB).await.unwrap();
|
||||
assert!(!list.iter().any(|a| a.npub == ARCHY_ANCHOR_NPUB));
|
||||
assert!(list.contains(&default_public_anchor()));
|
||||
let got = load(dir.path()).await.unwrap();
|
||||
assert_eq!(got, list, "edited list is authoritative; no re-seed");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn removing_all_defaults_persists_as_empty() {
|
||||
// Removing every default leaves an empty authoritative list that must
|
||||
// not be re-seeded on next load.
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
remove(dir.path(), ARCHY_ANCHOR_NPUB).await.unwrap();
|
||||
let list = remove(dir.path(), DEFAULT_PUBLIC_ANCHOR_NPUB)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(list.is_empty());
|
||||
let got = load(dir.path()).await.unwrap();
|
||||
assert!(got.is_empty(), "default must stay removed once edited");
|
||||
assert!(got.is_empty(), "defaults must stay removed once edited");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user