Server: no built-in public host, no trusted proxy by default; OpenAPI server variable host (D-123)
KVITT_PUBLIC_URL is required: every command refuses to start without it (compose always sets it). KVITT_TRUSTED_PROXY defaults to none, which the rate limiter never resolves. contracts/openapi.yaml and the utoipa document: servers[].url https://{host}:8443 with variable host (default kvitt.example.invalid). E-7 and E-12 examples on kvitt.example.invalid. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
This commit is contained in:
@@ -20,8 +20,9 @@
|
||||
//! on restart; there is no table.
|
||||
//!
|
||||
//! The client IP is the TCP peer (axum `ConnectInfo<SocketAddr>`), or the last `X-Forwarded-For` entry when the peer
|
||||
//! is an address of `KVITT_TRUSTED_PROXY` (Caddy, SPEC §8.5). The host name is resolved lazily and re-resolved at most
|
||||
//! once per [`PROXY_RESOLVE_INTERVAL`]; a name that does not resolve means no peer is trusted (D-022). Without
|
||||
//! is an address of `KVITT_TRUSTED_PROXY` (Nginx Proxy Manager, SPEC §8.5). The host name is resolved lazily and
|
||||
//! re-resolved at most once per [`PROXY_RESOLVE_INTERVAL`]; a name that does not resolve means no peer is trusted
|
||||
//! (D-022), and so does the default `none` ([`NO_TRUSTED_PROXY`]), which is never looked up. Without
|
||||
//! `ConnectInfo` (a server not started with `into_make_service_with_connect_info`) all clients share one IP bucket.
|
||||
|
||||
use std::collections::HashMap;
|
||||
@@ -39,6 +40,7 @@ use tower::{Layer, Service};
|
||||
|
||||
use crate::audit::BoxFuture;
|
||||
use crate::auth::{bearer_token, is_token_shaped, sha256_hex};
|
||||
use crate::config::NO_TRUSTED_PROXY;
|
||||
use crate::error::AppError;
|
||||
use crate::time::Clock;
|
||||
use crate::{AppState, lock};
|
||||
@@ -214,7 +216,7 @@ impl Limiter {
|
||||
return addrs.clone();
|
||||
}
|
||||
let name = self.trusted_proxy.trim();
|
||||
let addrs: Vec<IpAddr> = if name.is_empty() {
|
||||
let addrs: Vec<IpAddr> = if name.is_empty() || name.eq_ignore_ascii_case(NO_TRUSTED_PROXY) {
|
||||
Vec::new()
|
||||
} else if let Ok(ip) = name.parse::<IpAddr>() {
|
||||
vec![ip]
|
||||
@@ -463,6 +465,46 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
fn limiter_trusting(trusted_proxy: &str) -> Limiter {
|
||||
let now = parse_ts("2026-10-17T12:00:00.000Z").unwrap();
|
||||
Limiter::new(Arc::new(FixedClock::new(now)), trusted_proxy.to_owned())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn the_default_none_trusts_no_peer() {
|
||||
let peer = IpAddr::V4(Ipv4Addr::new(192, 168, 1, 20));
|
||||
let forwarded = IpAddr::V4(Ipv4Addr::new(203, 0, 113, 5));
|
||||
for value in [crate::config::DEFAULT_TRUSTED_PROXY, " NONE ", ""] {
|
||||
let limiter = limiter_trusting(value);
|
||||
assert_eq!(
|
||||
limiter.proxy_addrs().await,
|
||||
Vec::<IpAddr>::new(),
|
||||
"{value:?}"
|
||||
);
|
||||
assert_eq!(limiter.client_ip(Some(peer), Some(forwarded)).await, peer);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn an_unresolvable_name_trusts_no_peer() {
|
||||
// `.invalid` never resolves (RFC 6761).
|
||||
let limiter = limiter_trusting("proxy.kvitt.invalid");
|
||||
let peer = IpAddr::V4(Ipv4Addr::new(192, 168, 1, 20));
|
||||
let forwarded = IpAddr::V4(Ipv4Addr::new(203, 0, 113, 5));
|
||||
assert_eq!(limiter.client_ip(Some(peer), Some(forwarded)).await, peer);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn an_ip_literal_is_trusted() {
|
||||
let limiter = limiter_trusting("192.168.1.20");
|
||||
let peer = IpAddr::V4(Ipv4Addr::new(192, 168, 1, 20));
|
||||
let forwarded = IpAddr::V4(Ipv4Addr::new(203, 0, 113, 5));
|
||||
assert_eq!(
|
||||
limiter.client_ip(Some(peer), Some(forwarded)).await,
|
||||
forwarded
|
||||
);
|
||||
}
|
||||
|
||||
fn token_key(i: u32) -> Key {
|
||||
Key::Token {
|
||||
ip: IpAddr::V4(Ipv4Addr::LOCALHOST),
|
||||
|
||||
Reference in New Issue
Block a user