fix: cap peer browse, containerise the cert section, serve LAN HTTPS
content.browse-all-peers had a per-peer timeout but no OVERALL budget. On this node that meant >45s with no answer, which the assistant reported to the operator as "having trouble accessing the peer content list". Measured cause: 16 federated peers, 1 reachable. Now bounded to 20s total, returning partial results with peers_reached / peers_total / peers_unreachable / partial, so the assistant can say "1 of 16 peers answered" instead of implying the rest have nothing. Verified on the node: 20.015s, was >45s. NodeCertificateSection had no container — I copied a section that sits INSIDE a card rather than one that provides its own. Now uses the same `glass-card px-6 py-6 mb-6` shell and heading level as every other settings section, so it matches on desktop and mobile. setup-node-ca.sh now also ensures the nginx HTTPS listener, because a CA is useless if nothing serves TLS. It binds LAN addresses ONLY: tailscaled already owns :443 on the tailnet addresses with its own Let's Encrypt cert, so a plain `listen 443 default_server` binds 0.0.0.0 and fails EADDRINUSE — and nginx then keeps running the OLD config while the reload reports success. Hit exactly that on archi-dev-box. Port 80 keeps serving: nodes are reached by IP on LANs where forcing a redirect would strand anyone who has not installed the CA. Live now: https://192.168.63.240/ and https://<host>.local/ both 200 with verify=0 against the node CA, http still 200, tailscaled's 443 untouched. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
0a23c99463
commit
75919a2071
@@ -1261,16 +1261,31 @@ impl RpcHandler {
|
||||
let mut reached = 0usize;
|
||||
let mut unreachable = 0usize;
|
||||
|
||||
// OVERALL budget, not just per-peer. With a dozen federated peers an
|
||||
// 8s per-peer timeout still adds up past any usable answer — measured
|
||||
// on the node: this call returned nothing after 45 seconds, which the
|
||||
// assistant reports to the user as "having trouble accessing the peer
|
||||
// content list". Partial results beat a timeout: whatever answered
|
||||
// inside the budget is returned, and the counts say what was missed.
|
||||
let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(20);
|
||||
|
||||
// Sequential with a per-peer timeout rather than an unbounded fan-out:
|
||||
// 02-08 traced a real UI stall to content.browse-peer starving the
|
||||
// connection pool, and the assistant is not latency-critical.
|
||||
for onion in &onions {
|
||||
if tokio::time::Instant::now() >= deadline {
|
||||
// Everything not yet tried counts as unreachable for this call
|
||||
// rather than being silently omitted.
|
||||
unreachable += onions.len() - reached - unreachable;
|
||||
break;
|
||||
}
|
||||
let params = Some(serde_json::json!({ "onion": onion }));
|
||||
match tokio::time::timeout(
|
||||
// Never wait past the overall deadline for a single peer.
|
||||
let per_peer = std::cmp::min(
|
||||
std::time::Duration::from_secs(8),
|
||||
self.handle_content_browse_peer(params),
|
||||
)
|
||||
.await
|
||||
deadline.saturating_duration_since(tokio::time::Instant::now()),
|
||||
);
|
||||
match tokio::time::timeout(per_peer, self.handle_content_browse_peer(params)).await
|
||||
{
|
||||
Ok(Ok(v)) => {
|
||||
reached += 1;
|
||||
@@ -1292,6 +1307,10 @@ impl RpcHandler {
|
||||
"items": items,
|
||||
"peers_reached": reached,
|
||||
"peers_unreachable": unreachable,
|
||||
"peers_total": onions.len(),
|
||||
// Explicit so the assistant can say "3 of 12 peers answered"
|
||||
// instead of implying the empty ones have nothing to share.
|
||||
"partial": unreachable > 0,
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
@@ -52,9 +52,12 @@ onMounted(async () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mb-6">
|
||||
<h3 class="text-base font-medium text-white/90 mb-1">Node certificate</h3>
|
||||
<p class="text-sm text-white/60 mb-4">
|
||||
<!-- Node Certificate Section -->
|
||||
<div class="glass-card px-6 py-6 mb-6">
|
||||
<div class="mb-2">
|
||||
<h2 class="text-xl font-semibold text-white/96">Node certificate</h2>
|
||||
</div>
|
||||
<p class="text-sm text-white/60 mb-6">
|
||||
Install this node's certificate on a device and it stops warning you about
|
||||
this node — on every port, not just the dashboard. Apps that open inside
|
||||
the dashboard need this: a certificate warning cannot be accepted inside an
|
||||
|
||||
@@ -154,6 +154,59 @@ install -m 644 "$CA_CRT" "$SSL_DIR/ca-download.crt"
|
||||
FP="$(openssl x509 -in "$CA_CRT" -noout -fingerprint -sha256 | cut -d= -f2)"
|
||||
log "CA fingerprint (SHA-256): $FP"
|
||||
|
||||
# --- nginx HTTPS listener -----------------------------------------------------
|
||||
# The CA is only useful if something actually serves TLS. Bind the dashboard's
|
||||
# HTTPS on this host's LAN addresses ONLY: tailscaled already owns :443 on the
|
||||
# tailnet addresses (with its own Let's Encrypt cert), so a plain
|
||||
# `listen 443 default_server` binds 0.0.0.0 and fails with EADDRINUSE — nginx
|
||||
# then keeps running the OLD config and the reload looks like it worked.
|
||||
# Observed exactly that on archi-dev-box.
|
||||
#
|
||||
# Port 80 keeps serving: nodes are reached by IP on LANs where forcing a
|
||||
# redirect would strand anyone who has not installed the CA yet.
|
||||
ensure_nginx_https() {
|
||||
local site="${ARCHY_NGINX_SITE:-/etc/nginx/sites-enabled/archipelago}"
|
||||
[ -f "$site" ] || { log "No nginx site at $site — skipping HTTPS listener"; return; }
|
||||
|
||||
local addrs
|
||||
addrs="$(ip -o -4 addr show scope global 2>/dev/null | awk '{print $4}' | cut -d/ -f1 \
|
||||
| grep -vE '^100\.(6[4-9]|[7-9][0-9]|1[01][0-9]|12[0-7])\.' | sort -u)"
|
||||
[ -z "$addrs" ] && { log "No LAN address — skipping HTTPS listener"; return; }
|
||||
|
||||
if grep -q 'listen .*:443 ssl' "$site"; then
|
||||
log "nginx HTTPS listener already present"
|
||||
return
|
||||
fi
|
||||
log "Adding nginx HTTPS listener on: $(echo "$addrs" | tr '\n' ' ')"
|
||||
python3 - "$site" "$addrs" <<'PYEOF'
|
||||
import sys, re, pathlib
|
||||
site, addrs = pathlib.Path(sys.argv[1]), sys.argv[2].split()
|
||||
s = site.read_text(); lines = s.split('\n')
|
||||
start = next(i for i,l in enumerate(lines) if l.strip() == 'server {')
|
||||
depth = 0; end = None
|
||||
for i in range(start, len(lines)):
|
||||
depth += lines[i].count('{') - lines[i].count('}')
|
||||
if depth == 0 and i > start:
|
||||
end = i; break
|
||||
block = lines[start:end+1]
|
||||
https = []
|
||||
for l in block:
|
||||
if re.match(r'\s*listen 80 default_server;', l):
|
||||
https += [f' listen {a}:443 ssl;' for a in addrs]
|
||||
https += [' ssl_certificate /etc/archipelago/ssl/archipelago.crt;',
|
||||
' ssl_certificate_key /etc/archipelago/ssl/archipelago.key;',
|
||||
' ssl_protocols TLSv1.2 TLSv1.3;']
|
||||
continue
|
||||
if re.match(r'\s*listen \[::\]:80 default_server;', l):
|
||||
continue
|
||||
https.append(l)
|
||||
lines = lines[:end+1] + [''] + https + lines[end+1:]
|
||||
site.write_text('\n'.join(lines))
|
||||
PYEOF
|
||||
}
|
||||
|
||||
ensure_nginx_https
|
||||
|
||||
if command -v systemctl >/dev/null 2>&1 && systemctl is-active --quiet nginx; then
|
||||
if nginx -t >/dev/null 2>&1; then
|
||||
systemctl reload nginx && log "nginx reloaded"
|
||||
|
||||
Reference in New Issue
Block a user