From 0456206e5213b17f5c0aca0fb46115694b15b80b Mon Sep 17 00:00:00 2001 From: atlas Date: Sat, 4 Jul 2026 22:01:25 +0200 Subject: [PATCH] fix(#2245): drop TCP fallback from gateway nginx upstream Agents run in private netns (always-on isolation). The TCP fallback to 127.0.0.1: was unreachable from the gateway's host netns regardless of whether the per-agent socket marker existed. Remove the conditional entirely: always use the unix socket path. If the socket is not yet bound, nginx returns 502 which is already handled by the error_page 502 503 504 = /__hive_agent_unreachable directive in every location block. Also removes the unused lifecycle::agent_web_port call and the now-misleading '.bound state' mention from the render doc comment. --- hive-c0re/src/gateway_nginx.rs | 51 +++++++++++++++++----------------- 1 file changed, 25 insertions(+), 26 deletions(-) diff --git a/hive-c0re/src/gateway_nginx.rs b/hive-c0re/src/gateway_nginx.rs index e84f7a22..c6e709d0 100644 --- a/hive-c0re/src/gateway_nginx.rs +++ b/hive-c0re/src/gateway_nginx.rs @@ -1,7 +1,7 @@ //! Runtime nginx include-file generator for the gateway's per-agent //! `/agent//` location blocks. Writes //! `/var/lib/hyperhive/gateway/agents.conf` on every topology change. -//! UDS vs TCP upstream selection, reload trigger (`systemd-run +//! UDS upstream selection, reload trigger (`systemd-run //! --machine=hive-gateway`), and idempotency: //! `docs/gateway.md::Per-agent unix-socket upstream`. @@ -14,7 +14,6 @@ use std::time::{SystemTime, UNIX_EPOCH}; use crate::priv_client; use crate::agent_sockets; -use crate::lifecycle; /// Set when `write` publishes a new agents.conf; cleared when /// `reload_gateway_nginx` submits the reload command successfully. @@ -74,12 +73,11 @@ const PROXY_HEADER_BLOCK: &str = " proxy_http_version 1.1; /// When `frontend_dir` is `None` (legacy) each agent gets a single /// `location /agent//` proxy block. /// -/// Output is deterministic for the same (sorted) set of names + -/// `.bound` state: no timestamps, no UUIDs. `BTreeMap` would give -/// alphabetical order; we rely on the caller to pass sorted names if -/// they care about diff stability, but the gateway treats the blocks -/// as unordered by nginx's longest-prefix-match rules so ordering only -/// affects human readability. +/// Output is deterministic for the same (sorted) set of names: +/// no timestamps, no UUIDs. `BTreeMap` would give alphabetical order; +/// we rely on the caller to pass sorted names if they care about diff +/// stability, but the gateway treats the blocks as unordered by nginx's +/// longest-prefix-match rules so ordering only affects human readability. fn render(names: &[String], frontend_dir: Option<&str>) -> String { let mut out = String::from( "# Generated by hive-c0re \u{2014} do not edit.\ @@ -87,24 +85,20 @@ fn render(names: &[String], frontend_dir: Option<&str>) -> String { \n# Reload triggered by hive-c0re via systemd-run --machine=hive-gateway.\n", ); for name in names { - let port = lifecycle::agent_web_port(name); // Two upstream forms because named locations (split mode's // `@_dynamic`) forbid a URI part on `proxy_pass`. The // legacy prefix-location path keeps the trailing `/` so nginx // strips `/agent//` automatically; the named-location // path strips the prefix via `rewrite` and uses a bare upstream. - let (upstream_prefix, upstream_bare) = if agent_sockets::ready_marker_for(name).exists() { - let sock = agent_sockets::socket_path_for(name).display().to_string(); - ( - format!("http://unix:{sock}:/"), - format!("http://unix:{sock}:"), - ) - } else { - ( - format!("http://127.0.0.1:{port}/"), - format!("http://127.0.0.1:{port}"), - ) - }; + // + // Always use the unix socket path. The old TCP fallback + // (`127.0.0.1:`) was removed: agents run in private netns + // (always-on isolation) so the host loopback is unreachable from + // the gateway's netns. When the socket is not yet bound, nginx + // returns 502 which is handled by the error_page directive below. + let sock = agent_sockets::socket_path_for(name).display().to_string(); + let upstream_prefix = format!("http://unix:{sock}:/"); + let upstream_bare = format!("http://unix:{sock}:"); if let Some(frontend) = frontend_dir { // Split mode: try to serve files from the nix-store dist first; @@ -303,14 +297,19 @@ mod tests { } #[test] - fn render_tcp_upstream_when_no_bound_marker() { - // No .bound file on disk → falls back to TCP loopback. + fn render_uds_upstream_unconditional() { + // TCP fallback removed — always UDS regardless of socket marker. let names = vec!["iris".to_owned()]; let body = render(&names, None); - let port = lifecycle::agent_web_port("iris"); + // UDS form present assert!( - body.contains(&format!("proxy_pass http://127.0.0.1:{port}/")), - "expected TCP upstream for iris, got:\n{body}" + body.contains("proxy_pass http://unix:"), + "expected UDS upstream for iris, got:\n{body}" + ); + // No TCP loopback fallback + assert!( + !body.contains("127.0.0.1"), + "unexpected TCP loopback in output:\n{body}" ); }