feat(cluster): wire cluster into proxy lifecycle and request pipeline

Spawn cluster on dedicated thread in main.rs with graceful fallback to
standalone on failure. Add cluster field to SunbeamProxy, record
bandwidth in logging(), and enforce cluster-wide bandwidth cap in
request_filter with 429 JSON response.

Signed-off-by: Sienna Meridian Satterwhite <sienna@sunbeam.pt>
This commit is contained in:
2026-03-10 23:38:21 +00:00
parent 5d279f992b
commit 65516404e1
4 changed files with 63 additions and 3 deletions

View File

@@ -1,4 +1,5 @@
use crate::acme::AcmeRoutes;
use crate::cluster::ClusterHandle;
use crate::config::RouteConfig;
use crate::ddos::detector::DDoSDetector;
use crate::ddos::model::DDoSAction;
@@ -45,6 +46,8 @@ pub struct SunbeamProxy {
pub http_client: reqwest::Client,
/// Parsed bypass CIDRs — IPs in these ranges skip the detection pipeline.
pub pipeline_bypass_cidrs: Vec<crate::rate_limit::cidr::CidrBlock>,
/// Optional cluster handle for multi-node bandwidth tracking.
pub cluster: Option<Arc<ClusterHandle>>,
}
pub struct RequestCtx {
@@ -479,6 +482,24 @@ impl ProxyHttp for SunbeamProxy {
}
}
// Cluster-wide bandwidth cap enforcement.
if let Some(c) = &self.cluster {
use crate::cluster::bandwidth::BandwidthLimitResult;
let bw_result = c.limiter.check();
let decision = if bw_result == BandwidthLimitResult::Reject { "block" } else { "allow" };
metrics::BANDWIDTH_LIMIT_DECISIONS.with_label_values(&[decision]).inc();
if bw_result == BandwidthLimitResult::Reject {
let body = b"{\"error\":\"bandwidth_limit_exceeded\",\"message\":\"Request rate-limited: aggregate bandwidth capacity exceeded. Please try again shortly.\"}";
let mut resp = ResponseHeader::build(429, None)?;
resp.insert_header("Retry-After", "5")?;
resp.insert_header("Content-Type", "application/json")?;
resp.insert_header("Content-Length", body.len().to_string())?;
session.write_response_header(Box::new(resp), false).await?;
session.write_response_body(Some(Bytes::from_static(body)), true).await?;
return Ok(true);
}
}
// Reject unknown host prefixes with 404.
let host = extract_host(session);
let prefix = host.split('.').next().unwrap_or("");
@@ -1074,6 +1095,18 @@ impl ProxyHttp for SunbeamProxy {
.inc();
metrics::REQUEST_DURATION.observe(duration_secs);
// Record bandwidth for cluster aggregation.
if let Some(c) = &self.cluster {
let req_bytes: u64 = session
.req_header()
.headers
.get("content-length")
.and_then(|v| v.to_str().ok())
.and_then(|s| s.parse().ok())
.unwrap_or(0);
c.bandwidth.record(req_bytes, session.body_bytes_sent() as u64);
}
let content_length: u64 = session
.req_header()
.headers