feat(fips): connect to public mesh anchor over TCP + wire daemon updates
The whole fleet was silently never reaching the FIPS mesh: the default public anchor was configured as fips.v0l.io:8668/udp, but the anchor only answers on TCP/8443. Fix the default to 185.18.221.160:8443/tcp (IPv4 literal — the hostname resolves IPv6-first and the daemon binds v4-only, which fails the handshake with EAFNOSUPPORT), and auto-seed it in anchors::load() so every node dials it without operator action (removal still persists). Proven live on .116: cold start → anchor_connected in ~400ms, anchor became mesh parent. Wire fips::update::apply() against upstream GitHub releases (stable channel only): resolve /releases/latest → SHA256-verify the .deb against checksums-linux.txt → install → restart. dpkg runs via `systemd-run` to escape archipelago's ProtectSystem=strict sandbox (else /var/lib/dpkg is read-only), with --force-confold (archipelago manages /etc/fips conffiles) and --force-downgrade (dev builds sort newer than the stable tag). Validated live: .116 upgraded 0.3.0-dev -> stable v0.3.0. Also: standalone fips-ui dashboard app (apps/fips-ui + docker/fips-ui, static nginx proxying /rpc/v1 same-origin, copiable own-anchor address); reserve UI port 8336; register fips/fips-ui as platform-managed. Includes the Lightning wallet cross-origin (CORS) + LND proxy auth + nginx self-healer fix so the wallet screen connects instead of "failed to fetch". Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
640dc87a5f
commit
95f9a805b1
@@ -256,6 +256,45 @@ impl ApiHandler {
|
||||
}
|
||||
}
|
||||
|
||||
/// CORS origin to echo for same-node app → backend calls (e.g. the LND
|
||||
/// wallet UI, served on its own APP_PORTS port). Such apps share the node's
|
||||
/// host but use a different port, so the strict allowlist (`host_ip`, no
|
||||
/// port) rejects them and the browser gets no `Access-Control-Allow-Origin`
|
||||
/// header ("blocked by CORS policy"). Reflect the Origin when its host
|
||||
/// matches the request's own `Host` header — i.e. the app lives on the same
|
||||
/// address the node is being reached by, which transparently covers the LAN
|
||||
/// IP, the Tailscale IP, localhost, and the `.onion` address without needing
|
||||
/// to enumerate them. Auth is still enforced by the session cookie; this
|
||||
/// only authorizes the browser to *read* the reply. Returns "" (no echoed
|
||||
/// origin) when there is no match.
|
||||
fn app_cors_origin(&self, headers: &hyper::HeaderMap) -> String {
|
||||
if let Some(origin) = self.validate_origin(headers) {
|
||||
return origin;
|
||||
}
|
||||
let Some(origin) = headers.get("origin").and_then(|v| v.to_str().ok()) else {
|
||||
return String::new();
|
||||
};
|
||||
// host portion (no scheme, no port) of an `scheme://host[:port]` value
|
||||
let host_of = |s: &str| -> Option<String> {
|
||||
let after_scheme = s.split_once("://").map(|(_, r)| r).unwrap_or(s);
|
||||
let host_port = after_scheme.split('/').next().unwrap_or(after_scheme);
|
||||
let host = host_port
|
||||
.rsplit_once(':')
|
||||
.map(|(h, _)| h)
|
||||
.unwrap_or(host_port);
|
||||
(!host.is_empty()).then(|| host.to_string())
|
||||
};
|
||||
let origin_host = host_of(origin);
|
||||
let req_host = headers
|
||||
.get(hyper::header::HOST)
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.and_then(host_of);
|
||||
match (origin_host, req_host) {
|
||||
(Some(o), Some(r)) if o == r => origin.to_string(),
|
||||
_ => String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn handle_request(&self, req: Request<hyper::Body>) -> Result<Response<hyper::Body>> {
|
||||
let path = req.uri().path().to_string();
|
||||
let method = req.method().clone();
|
||||
@@ -265,9 +304,10 @@ impl ApiHandler {
|
||||
let mut builder = Response::builder()
|
||||
.status(StatusCode::NO_CONTENT)
|
||||
.header("Vary", "Origin");
|
||||
if let Some(origin) = self.validate_origin(req.headers()) {
|
||||
let preflight_origin = self.app_cors_origin(req.headers());
|
||||
if !preflight_origin.is_empty() {
|
||||
builder = builder
|
||||
.header("Access-Control-Allow-Origin", &origin)
|
||||
.header("Access-Control-Allow-Origin", &preflight_origin)
|
||||
.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
||||
.header("Access-Control-Allow-Headers", "Content-Type, X-CSRF-Token")
|
||||
.header("Access-Control-Allow-Credentials", "true");
|
||||
@@ -448,7 +488,8 @@ impl ApiHandler {
|
||||
// No backend auth check here because the LND UI iframe fetches this
|
||||
// endpoint and the session cookie flow is validated at the nginx layer.
|
||||
(Method::GET, "/lnd-connect-info") => {
|
||||
Self::handle_lnd_connect_info(self.rpc_handler.clone()).await
|
||||
let origin = self.app_cors_origin(&headers);
|
||||
Self::handle_lnd_connect_info(self.rpc_handler.clone(), &origin).await
|
||||
}
|
||||
|
||||
// Container logs — requires session
|
||||
@@ -465,8 +506,8 @@ impl ApiHandler {
|
||||
if !self.is_authenticated(&headers).await {
|
||||
return Ok(Self::unauthorized());
|
||||
}
|
||||
let origin = self.validate_origin(&headers).unwrap_or_default();
|
||||
Self::handle_lnd_proxy(path, &origin).await
|
||||
let origin = self.app_cors_origin(&headers);
|
||||
Self::handle_lnd_proxy(self.rpc_handler.clone(), path, &origin).await
|
||||
}
|
||||
|
||||
// DWN health — unauthenticated
|
||||
|
||||
Reference in New Issue
Block a user