Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 4 additions & 2 deletions Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -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##*:}"
Expand Down
2 changes: 2 additions & 0 deletions TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` | |
Expand Down
83 changes: 68 additions & 15 deletions crates/buzz-relay/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>,
/// 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
Expand Down Expand Up @@ -267,6 +267,24 @@ fn parse_bind_addr(raw: &str) -> Result<SocketAddr, ConfigError> {
.map_err(|e| ConfigError::InvalidBindAddr(e.to_string()))
}

fn listener_addr(
address_setting: &str,
port_setting: &str,
default_port: u16,
) -> Result<SocketAddr, ConfigError> {
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<u64, ConfigError> {
match std::env::var(name) {
Ok(raw) => raw
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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::<SocketAddr>().unwrap()
);
assert_eq!(
config.metrics_addr,
"127.0.0.1:9202".parse::<SocketAddr>().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();
Expand Down
20 changes: 11 additions & 9 deletions crates/buzz-relay/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,19 +126,19 @@ 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"
);

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"
);
Expand Down Expand Up @@ -1102,8 +1102,8 @@ async fn run_periodic_until_cancelled<Tick, TickFuture>(
/// ┌─────────────────────────────────────────────────────────┐
/// │ 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 │
Expand All @@ -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();
});
Expand Down
11 changes: 7 additions & 4 deletions crates/buzz-relay/src/metrics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down Expand Up @@ -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,
Expand Down
14 changes: 7 additions & 7 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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:
Expand All @@ -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:
Expand All @@ -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:
Expand All @@ -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:
Expand Down Expand Up @@ -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
Expand Down