From 3f61aab032fd0076486430435243611528bd4c91 Mon Sep 17 00:00:00 2001 From: SteinerBBQ <155865077+SteinerBBQ@users.noreply.github.com> Date: Sun, 26 Jul 2026 12:59:51 -0500 Subject: [PATCH 1/2] feat(relay): allow scoped health and metrics listeners Signed-off-by: SteinerBBQ <155865077+SteinerBBQ@users.noreply.github.com> --- .env.example | 4 ++ Justfile | 6 ++- TESTING.md | 2 + crates/buzz-relay/src/config.rs | 83 ++++++++++++++++++++++++++------ crates/buzz-relay/src/main.rs | 20 ++++---- crates/buzz-relay/src/metrics.rs | 11 +++-- 6 files changed, 96 insertions(+), 30 deletions(-) diff --git a/.env.example b/.env.example index db5a7ea25c..944ebf90f2 100644 --- a/.env.example +++ b/.env.example @@ -45,6 +45,10 @@ TYPESENSE_URL=http://localhost:8108 # ----------------------------------------------------------------------------- # Bind address for the relay (host:port) BUZZ_BIND_ADDR=0.0.0.0:3000 +# Optional full listener addresses. These override the legacy port-only +# BUZZ_HEALTH_PORT and BUZZ_METRICS_PORT settings when present. +# BUZZ_HEALTH_ADDR=127.0.0.1:8080 +# BUZZ_METRICS_ADDR=127.0.0.1:9102 # Public WebSocket URL — used in NIP-42 auth challenges RELAY_URL=ws://localhost:3000 # Stable relay signing key. Set this in dev if you want REST-created forum posts diff --git a/Justfile b/Justfile index 701a1f06b2..919efcfbac 100644 --- a/Justfile +++ b/Justfile @@ -415,8 +415,10 @@ dev *ARGS: bootstrap _ensure-sidecar-stubs _ensure-migrations export PATH="{{justfile_directory()}}/bin:$PATH" bind_addr="${BUZZ_BIND_ADDR:-0.0.0.0:3000}" relay_port="${bind_addr##*:}"; [[ -n "$relay_port" ]] || relay_port=3000 - health_port="${BUZZ_HEALTH_PORT:-8080}" - metrics_port="${BUZZ_METRICS_PORT:-9102}" + health_addr="${BUZZ_HEALTH_ADDR:-0.0.0.0:${BUZZ_HEALTH_PORT:-8080}}" + health_port="${health_addr##*:}"; [[ -n "$health_port" ]] || health_port=8080 + metrics_addr="${BUZZ_METRICS_ADDR:-0.0.0.0:${BUZZ_METRICS_PORT:-9102}}" + metrics_port="${metrics_addr##*:}"; [[ -n "$metrics_port" ]] || metrics_port=9102 if command -v lsof >/dev/null 2>&1; then for spec in "relay:$relay_port" "health:$health_port" "metrics:$metrics_port"; do name="${spec%%:*}"; port="${spec##*:}" diff --git a/TESTING.md b/TESTING.md index 51a5eb44c1..f755aad85b 100644 --- a/TESTING.md +++ b/TESTING.md @@ -270,7 +270,9 @@ out of the box with `just setup` or `just relay`. Common overrides: | Variable | Default | Notes | |-----------------------------------|-----------------------------|-------| | `BUZZ_BIND_ADDR` | `0.0.0.0:3000` | Main app port | +| `BUZZ_HEALTH_ADDR` | unset | Full health listener address; overrides `BUZZ_HEALTH_PORT` | | `BUZZ_HEALTH_PORT` | `8080` | `/_liveness`, `/_readiness` | +| `BUZZ_METRICS_ADDR` | unset | Full metrics listener address; overrides `BUZZ_METRICS_PORT` | | `BUZZ_METRICS_PORT` | `9102` | Prometheus `/metrics` | | `RELAY_URL` | `ws://localhost:3000` | Advertised in NIP-11 / NIP-42 challenges. **Note: no `BUZZ_` prefix.** | | `DATABASE_URL` | `postgres://buzz:buzz_dev@localhost:5432/buzz` | | diff --git a/crates/buzz-relay/src/config.rs b/crates/buzz-relay/src/config.rs index 47030dcf3f..590bcd12ce 100644 --- a/crates/buzz-relay/src/config.rs +++ b/crates/buzz-relay/src/config.rs @@ -93,11 +93,11 @@ pub struct Config { /// Optional Unix Domain Socket path. When set, the relay also listens on this /// UDS for traffic (e.g. service mesh sidecar). Health probes still use TCP. pub uds_path: Option, - /// TCP port for the health-only router (`/_liveness`, `/_readiness`, `/_status`). + /// Address for the health-only router (`/_liveness`, `/_readiness`, `/_status`). /// Separate from the app router so K8s probes bypass Istio and auth middleware. - pub health_port: u16, - /// TCP port for the Prometheus metrics exporter (`GET /metrics`). - pub metrics_port: u16, + pub health_addr: SocketAddr, + /// Address for the Prometheus metrics exporter (`GET /metrics`). + pub metrics_addr: SocketAddr, /// When true, NIP-42 pubkey-only authentication (no API token) is /// restricted to pubkeys in the `pubkey_allowlist` table. Users with valid @@ -267,6 +267,24 @@ fn parse_bind_addr(raw: &str) -> Result { .map_err(|e| ConfigError::InvalidBindAddr(e.to_string())) } +fn listener_addr( + address_setting: &str, + port_setting: &str, + default_port: u16, +) -> Result { + if let Ok(raw) = std::env::var(address_setting) { + return raw.parse().map_err(|_| { + ConfigError::InvalidValue(format!("{address_setting} must be a valid socket address")) + }); + } + + let port = std::env::var(port_setting) + .ok() + .and_then(|value| value.parse().ok()) + .unwrap_or(default_port); + Ok(SocketAddr::from(([0, 0, 0, 0], port))) +} + fn positive_u64_from_env(name: &str, default: u64) -> Result { match std::env::var(name) { Ok(raw) => raw @@ -606,15 +624,8 @@ impl Config { .map(|s| s.trim().to_string()) .filter(|s| !s.is_empty()); - let health_port = std::env::var("BUZZ_HEALTH_PORT") - .ok() - .and_then(|v| v.parse().ok()) - .unwrap_or(8080); - - let metrics_port = std::env::var("BUZZ_METRICS_PORT") - .ok() - .and_then(|v| v.parse().ok()) - .unwrap_or(9102); + let health_addr = listener_addr("BUZZ_HEALTH_ADDR", "BUZZ_HEALTH_PORT", 8080)?; + let metrics_addr = listener_addr("BUZZ_METRICS_ADDR", "BUZZ_METRICS_PORT", 9102)?; let media = buzz_media::MediaConfig { s3_endpoint: std::env::var("BUZZ_S3_ENDPOINT") @@ -887,8 +898,8 @@ impl Config { cors_origins, relay_private_key, uds_path, - health_port, - metrics_port, + health_addr, + metrics_addr, pubkey_allowlist_enabled, require_relay_membership, huddle_audio_available, @@ -1273,6 +1284,48 @@ mod tests { )); } + #[test] + fn listener_addresses_can_be_constrained_to_loopback() { + let _guard = ENV_MUTEX.lock().unwrap(); + std::env::set_var("BUZZ_HEALTH_ADDR", "127.0.0.1:8180"); + std::env::set_var("BUZZ_METRICS_ADDR", "127.0.0.1:9202"); + let config = Config::from_env().expect("config"); + std::env::remove_var("BUZZ_HEALTH_ADDR"); + std::env::remove_var("BUZZ_METRICS_ADDR"); + assert_eq!( + config.health_addr, + "127.0.0.1:8180".parse::().unwrap() + ); + assert_eq!( + config.metrics_addr, + "127.0.0.1:9202".parse::().unwrap() + ); + } + + #[test] + fn listener_address_overrides_legacy_port_setting() { + let _guard = ENV_MUTEX.lock().unwrap(); + std::env::set_var("BUZZ_HEALTH_ADDR", "127.0.0.1:8180"); + std::env::set_var("BUZZ_HEALTH_PORT", "8088"); + let config = Config::from_env().expect("config"); + std::env::remove_var("BUZZ_HEALTH_ADDR"); + std::env::remove_var("BUZZ_HEALTH_PORT"); + assert_eq!(config.health_addr.port(), 8180); + } + + #[test] + fn invalid_listener_address_is_rejected() { + let _guard = ENV_MUTEX.lock().unwrap(); + std::env::set_var("BUZZ_HEALTH_ADDR", "not-an-address"); + let result = Config::from_env(); + std::env::remove_var("BUZZ_HEALTH_ADDR"); + assert!(matches!( + result, + Err(ConfigError::InvalidValue(ref message)) + if message.contains("BUZZ_HEALTH_ADDR") + )); + } + #[test] fn pairing_relay_url_accepts_websocket_urls_and_rejects_http() { let _guard = ENV_MUTEX.lock().unwrap(); diff --git a/crates/buzz-relay/src/main.rs b/crates/buzz-relay/src/main.rs index 22101219eb..8a2c9d2b0d 100644 --- a/crates/buzz-relay/src/main.rs +++ b/crates/buzz-relay/src/main.rs @@ -126,8 +126,8 @@ async fn main() -> anyhow::Result<()> { info!( bind_addr = %config.bind_addr, relay_url = %config.relay_url, - health_port = config.health_port, - metrics_port = config.metrics_port, + health_addr = %config.health_addr, + metrics_addr = %config.metrics_addr, max_frame_bytes = config.max_frame_bytes, audit_enabled = config.audit_enabled, "Config loaded" @@ -135,10 +135,10 @@ async fn main() -> anyhow::Result<()> { let usage_interval_secs = usage_metrics_interval_secs(); let usage_idle_timeout_secs = usage_metrics_idle_timeout_secs(usage_interval_secs); - relay_metrics::install(config.metrics_port, usage_idle_timeout_secs); + relay_metrics::install(config.metrics_addr, usage_idle_timeout_secs); metrics::gauge!("buzz_audit_enabled").set(if config.audit_enabled { 1.0 } else { 0.0 }); info!( - port = config.metrics_port, + address = %config.metrics_addr, idle_timeout_secs = usage_idle_timeout_secs, "Prometheus metrics exporter started" ); @@ -1102,8 +1102,8 @@ async fn run_periodic_until_cancelled( /// ┌─────────────────────────────────────────────────────────┐ /// │ Listener 1: TCP BUZZ_BIND_ADDR:3000 (app router) │ /// │ Listener 2: UDS BUZZ_UDS_PATH (app, optional)│ -/// │ Listener 3: TCP 0.0.0.0:8080 (health only) │ -/// │ Listener 4: TCP 0.0.0.0:9102 (metrics, via │ +/// │ Listener 3: TCP BUZZ_HEALTH_ADDR (health only) │ +/// │ Listener 4: TCP BUZZ_METRICS_ADDR (metrics, via │ /// │ PrometheusBuilder — already bound) │ /// │ │ /// │ SIGTERM → shutting_down=true → readiness 503 │ @@ -1117,10 +1117,12 @@ async fn serve( ) -> anyhow::Result<()> { let config = &state.config; - let health_listener = tokio::net::TcpListener::bind(("0.0.0.0", config.health_port)) + let health_listener = tokio::net::TcpListener::bind(config.health_addr) .await - .map_err(|e| anyhow::anyhow!("Failed to bind health port {}: {e}", config.health_port))?; - info!(port = config.health_port, "Health probe listener started"); + .map_err(|e| { + anyhow::anyhow!("Failed to bind health address {}: {e}", config.health_addr) + })?; + info!(address = %config.health_addr, "Health probe listener started"); tokio::spawn(async move { axum::serve(health_listener, health_router).await.ok(); }); diff --git a/crates/buzz-relay/src/metrics.rs b/crates/buzz-relay/src/metrics.rs index 16e521a44e..d301b7aa1e 100644 --- a/crates/buzz-relay/src/metrics.rs +++ b/crates/buzz-relay/src/metrics.rs @@ -14,7 +14,10 @@ //! recorded by [`track_metrics`] middleware on the app router. Buzz-specific //! metrics are recorded inline at their call sites. -use std::time::{Duration, Instant}; +use std::{ + net::SocketAddr, + time::{Duration, Instant}, +}; use axum::{ extract::{MatchedPath, Request}, @@ -62,10 +65,10 @@ const FANOUT_BUCKETS: [f64; 9] = [0.0, 1.0, 5.0, 10.0, 25.0, 50.0, 100.0, 500.0, /// the upkeep task, so no separate upkeep call is needed. /// /// Must be called from within a Tokio runtime. -/// Panics if a recorder is already installed or the port is in use. -pub fn install(port: u16, gauge_idle_timeout_secs: u64) { +/// Panics if a recorder is already installed or the address is in use. +pub fn install(address: SocketAddr, gauge_idle_timeout_secs: u64) { let (recorder, exporter) = PrometheusBuilder::new() - .with_http_listener(([0, 0, 0, 0], port)) + .with_http_listener(address) // Remove gauge series that the relay intentionally stops emitting. .idle_timeout( MetricKindMask::GAUGE, From 171b4fd0e0c40de288114d6d88d29aa91f90b8d3 Mon Sep 17 00:00:00 2001 From: SteinerBBQ <155865077+SteinerBBQ@users.noreply.github.com> Date: Sun, 26 Jul 2026 13:00:43 -0500 Subject: [PATCH 2/2] chore(dev): bind support services to loopback Signed-off-by: SteinerBBQ <155865077+SteinerBBQ@users.noreply.github.com> --- docker-compose.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 0056ea6a42..e7dc09fafc 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,7 +10,7 @@ services: POSTGRES_DB: buzz PGDATA: /var/lib/postgresql/data ports: - - "5432:5432" + - "127.0.0.1:5432:5432" volumes: - postgres-data:/var/lib/postgresql/data networks: @@ -34,7 +34,7 @@ services: image: redis:7-alpine container_name: buzz-redis ports: - - "6379:6379" + - "127.0.0.1:6379:6379" networks: - buzz-net healthcheck: @@ -56,7 +56,7 @@ services: image: adminer:latest container_name: buzz-adminer ports: - - "8082:8080" + - "127.0.0.1:8082:8080" networks: - buzz-net depends_on: @@ -82,7 +82,7 @@ services: KEYCLOAK_ADMIN: admin KEYCLOAK_ADMIN_PASSWORD: admin ports: - - "8180:8080" + - "127.0.0.1:8180:8080" networks: - buzz-net healthcheck: @@ -108,8 +108,8 @@ services: MINIO_ROOT_USER: buzz_dev MINIO_ROOT_PASSWORD: buzz_dev_secret ports: - - "9000:9000" - - "9001:9001" + - "127.0.0.1:9000:9000" + - "127.0.0.1:9001:9001" volumes: - minio-data:/data networks: @@ -152,7 +152,7 @@ services: image: prom/prometheus:latest container_name: buzz-prometheus ports: - - "9090:9090" + - "127.0.0.1:9090:9090" volumes: - ./prometheus.yml:/etc/prometheus/prometheus.yml:ro - prometheus-data:/prometheus