feat(security): harden gateway boundaries and usage policies

Consolidate subscription usage policy enforcement, privacy-safe persistence, and gateway security hardening into one reviewable change.

Includes bounded HTTP and execution envelopes, header and protocol guards, DNS and relay validation, authentication and secret projection hardening, secure backup/install paths, and regression coverage.
This commit is contained in:
elky
2026-09-04 03:45:52 +08:00
parent ddcbeb3ae9
commit 579f2c7cc1
1019 changed files with 190437 additions and 26080 deletions
+91
View File
@@ -0,0 +1,91 @@
use std::io;
use std::net::SocketAddr;
use std::time::Duration;
/// Maximum number of addresses accepted from one hostname lookup.
///
/// A DNS response is attacker-controlled at the resolver boundary. Keeping
/// the result bounded prevents a pathological answer from forcing an
/// unbounded vector allocation or an unbounded connect fan-out. Callers still
/// validate every returned address for their own network policy.
pub const MAX_DNS_RESOLVED_ADDRESSES: usize = 32;
/// Upper bound used by callers that do not have a tighter request deadline.
pub const DEFAULT_DNS_LOOKUP_TIMEOUT: Duration = Duration::from_secs(10);
/// Resolve a host while bounding both resolver wait time and answer count.
///
/// The iterator is consumed one item past the allowed count so an answer set
/// larger than the policy is rejected rather than silently truncated. This
/// keeps validation and connection pinning based on the complete, bounded
/// answer set.
pub async fn lookup_host_with_limits(
host: &str,
port: u16,
timeout: Duration,
) -> io::Result<Vec<SocketAddr>> {
if timeout.is_zero() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"DNS lookup timeout must be non-zero",
));
}
let mut resolved = tokio::time::timeout(timeout, tokio::net::lookup_host((host, port)))
.await
.map_err(|_| io::Error::new(io::ErrorKind::TimedOut, "DNS lookup timed out"))??;
collect_resolved_addresses_with_limit(&mut resolved)
}
fn collect_resolved_addresses_with_limit(
resolved: &mut impl Iterator<Item = SocketAddr>,
) -> io::Result<Vec<SocketAddr>> {
let mut addresses = Vec::with_capacity(MAX_DNS_RESOLVED_ADDRESSES.min(8));
while let Some(address) = resolved.next() {
if addresses.len() >= MAX_DNS_RESOLVED_ADDRESSES {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"DNS lookup returned too many addresses",
));
}
addresses.push(address);
}
Ok(addresses)
}
#[cfg(test)]
mod tests {
use std::net::SocketAddr;
use super::{
collect_resolved_addresses_with_limit, lookup_host_with_limits, DEFAULT_DNS_LOOKUP_TIMEOUT,
MAX_DNS_RESOLVED_ADDRESSES,
};
#[tokio::test]
async fn rejects_zero_dns_timeout_before_resolving() {
let error = lookup_host_with_limits("localhost", 80, std::time::Duration::ZERO)
.await
.expect_err("zero timeout must be rejected");
assert_eq!(error.kind(), std::io::ErrorKind::InvalidInput);
}
#[tokio::test]
async fn resolves_within_shared_address_bound() {
let addresses = lookup_host_with_limits("localhost", 80, DEFAULT_DNS_LOOKUP_TIMEOUT)
.await
.expect("localhost should resolve in the test environment");
assert!(!addresses.is_empty());
assert!(addresses.len() <= MAX_DNS_RESOLVED_ADDRESSES);
}
#[test]
fn rejects_an_answer_set_larger_than_the_shared_bound() {
let mut resolved = (0..=MAX_DNS_RESOLVED_ADDRESSES)
.map(|index| SocketAddr::from(([192, 0, 2, (index % 254 + 1) as u8], 443)));
let error = collect_resolved_addresses_with_limit(&mut resolved)
.expect_err("more than 32 DNS answers must be rejected");
assert_eq!(error.kind(), std::io::ErrorKind::InvalidData);
}
}