Decoy Fasttrack Drafts

Co-Authored-By: brekotis <93345790+brekotis@users.noreply.github.com>
This commit is contained in:
Alexey
2026-09-05 21:18:48 +03:00
parent 9f023ff9c7
commit 106b26a5b7
28 changed files with 920 additions and 59 deletions
+4
View File
@@ -85,6 +85,10 @@ pub(super) fn warn_non_hot_changes(old: &ProxyConfig, new: &ProxyConfig, non_hot
warned = true;
warn!("config reload: server listener settings changed; restart required");
}
if old.web.decoy_fasttrack_mode != new.web.decoy_fasttrack_mode {
warned = true;
warn!("config reload: web.decoy_fasttrack_mode changed; restart required");
}
if old.censorship.tls_domain != new.censorship.tls_domain
|| old.censorship.tls_domains != new.censorship.tls_domains
|| old.censorship.tls_fetch_scope != new.censorship.tls_fetch_scope
+2
View File
@@ -343,8 +343,10 @@ pub(super) fn overlay_hot_fields(old: &ProxyConfig, new: &ProxyConfig) -> ProxyC
cfg.access.user_max_unique_ips_mode = new.access.user_max_unique_ips_mode;
cfg.access.user_max_unique_ips_window_secs = new.access.user_max_unique_ips_window_secs;
let process_limits = cfg.web.limits.clone();
let decoy_fasttrack_mode = cfg.web.decoy_fasttrack_mode;
cfg.web = new.web.clone();
cfg.web.limits = process_limits;
cfg.web.decoy_fasttrack_mode = decoy_fasttrack_mode;
if cfg.web.carrier_negotiation_enabled()
&& cfg.web.carrier_learning
&& cfg.web.limits.max_carrier_learning_entries < WEB_CARRIER_LEARNING_MIN_ENTRIES
+15
View File
@@ -123,6 +123,21 @@ fn web_debug_policy_is_hot_while_debug_capacity_is_process_owned() {
);
}
#[test]
fn decoy_fasttrack_mode_is_deferred_until_restart() {
let old = sample_config();
let mut new = old.clone();
new.web.decoy_fasttrack_mode = crate::config::WebDecoyFastTrackMode::Enforce;
let applied = overlay_hot_fields(&old, &new);
assert_eq!(
applied.web.decoy_fasttrack_mode,
old.web.decoy_fasttrack_mode
);
assert_eq!(HotFields::from_config(&old), HotFields::from_config(&applied));
}
#[test]
fn hot_overlay_defers_learning_that_requires_new_process_capacity() {
let mut old = sample_config();
+4
View File
@@ -36,6 +36,7 @@ pub(super) fn rebuild(config: &mut ProxyConfig) -> Result<()> {
&mut static_bytes,
)?;
let mut profiles = Vec::with_capacity(vhost.profiles.len());
let mut capability_table = Vec::with_capacity(vhost.profiles.len());
let mut capabilities = HashSet::with_capacity(vhost.profiles.len());
for profile in &vhost.profiles {
let user_id = auth.user_id_by_name(&profile.user).ok_or_else(|| {
@@ -84,6 +85,7 @@ pub(super) fn rebuild(config: &mut ProxyConfig) -> Result<()> {
.max_streams_per_session
.unwrap_or(config.web.limits.max_streams_per_session),
});
capability_table.push(capability);
profiles.push(Arc::clone(&runtime_profile));
runtime_profiles.push(runtime_profile);
}
@@ -91,9 +93,11 @@ pub(super) fn rebuild(config: &mut ProxyConfig) -> Result<()> {
vhost.host.clone(),
Arc::new(WebRuntimeVhost {
host: vhost.host.clone(),
decoy_fasttrack_mode: config.web.decoy_fasttrack_mode,
decoy,
decoy_header_secs: config.web.timeouts.decoy_header_secs,
profiles,
capabilities: capability_table.into_boxed_slice(),
}),
);
}
+1
View File
@@ -266,6 +266,7 @@ const WEB_CONFIG_KEYS: &[&str] = &[
"carriers",
"carrier_learning",
"carrier_negotiation_aggressiveness",
"decoy_fasttrack_mode",
"http_connection_capacity_action",
"debug",
"limits",
+18
View File
@@ -6,6 +6,7 @@ const WEB_DEBUG_GROUP_SCRATCH_BYTES: usize = 4 * 1024 * 1024;
const WEB_CARRIER_LEARNING_ENTRY_BYTES: usize = 512;
const WEB_LANE_STATE_BYTES: usize = 512;
const WEB_OVERLOAD_CONNECTION_BYTES: usize = 4 * 1024;
const WEB_CAPABILITY_INDEX_ENTRY_BYTES: usize = 32;
/// Validates process-wide body, header, queue, static, and debug reservations.
pub(super) fn validate(limits: &WebLimitsConfig) -> Result<()> {
@@ -65,6 +66,12 @@ pub(super) fn validate(limits: &WebLimitsConfig) -> Result<()> {
.ok_or_else(|| {
ProxyError::Config("web.carrier learning reservation overflowed usize".to_string())
})?;
let capability_index_reservation = limits
.max_profiles
.checked_mul(WEB_CAPABILITY_INDEX_ENTRY_BYTES)
.ok_or_else(|| {
ProxyError::Config("web capability index reservation overflowed usize".to_string())
})?;
let lane_state_reservation = limits
.max_streams_per_session
.checked_add(limits.max_tombstones_per_session)
@@ -82,6 +89,7 @@ pub(super) fn validate(limits: &WebLimitsConfig) -> Result<()> {
.and_then(|value| value.checked_add(status_pages))
.and_then(|value| value.checked_add(debug_reservation))
.and_then(|value| value.checked_add(carrier_learning_reservation))
.and_then(|value| value.checked_add(capability_index_reservation))
.and_then(|value| value.checked_add(lane_state_reservation))
.and_then(|value| value.checked_add(http_header_reservation))
.and_then(|value| value.checked_add(overload_connection_reservation))
@@ -111,4 +119,14 @@ mod tests {
};
assert!(validate(&previous_envelope).is_err());
}
#[test]
fn capability_index_reservation_rejects_size_overflow() {
let limits = WebLimitsConfig {
max_profiles: usize::MAX,
..WebLimitsConfig::default()
};
let error = validate(&limits).unwrap_err().to_string();
assert!(error.contains("web capability index reservation overflowed usize"));
}
}
@@ -41,6 +41,12 @@ fn web_config_builds_canonical_runtime_snapshot() {
.get("proxy.example.com")
.expect("canonical WEB vhost");
assert_eq!(vhost.profiles.len(), 1);
assert_eq!(vhost.capabilities.len(), vhost.profiles.len());
assert_eq!(vhost.capabilities[0], vhost.profiles[0].capability);
assert_eq!(
vhost.decoy_fasttrack_mode,
WebDecoyFastTrackMode::Off
);
assert_eq!(vhost.profiles[0].user, "alice");
assert_eq!(vhost.profiles[0].secret_mode, WebSecretMode::Dd);
assert_eq!(vhost.profiles[0].carrier, WebCarrier::HttpsLanes);
@@ -56,6 +62,45 @@ fn web_config_builds_canonical_runtime_snapshot() {
);
}
#[test]
fn web_decoy_fasttrack_mode_is_typed_and_defaults_off() {
let defaults = ProxyConfig::default();
assert_eq!(
defaults.web.decoy_fasttrack_mode,
WebDecoyFastTrackMode::Off
);
for (token, expected) in [
("shadow", WebDecoyFastTrackMode::Shadow),
("enforce", WebDecoyFastTrackMode::Enforce),
] {
let configured = WEB_CONFIG.replace(
"carrier = \"https-lanes\"",
&format!(
"carrier = \"https-lanes\"\ndecoy_fasttrack_mode = \"{token}\""
),
);
let config = load_config_from_temp_toml(&configured);
assert_eq!(config.web.decoy_fasttrack_mode, expected);
assert_eq!(
config
.web
.runtime
.as_ref()
.unwrap()
.vhosts["proxy.example.com"]
.decoy_fasttrack_mode,
expected
);
}
let invalid = WEB_CONFIG.replace(
"carrier = \"https-lanes\"",
"carrier = \"https-lanes\"\ndecoy_fasttrack_mode = \"automatic\"",
);
assert!(load_config_error_from_temp_toml(&invalid).contains("decoy_fasttrack_mode"));
}
#[test]
fn web_http_connection_capacity_policy_is_bounded_and_configurable() {
let configured = WEB_CONFIG
+1 -1
View File
@@ -53,7 +53,7 @@ pub use server::{
};
#[allow(unused_imports)]
pub use web::{
WebCarrierNegotiationAggressiveness, WebConfig, WebDecoyConfig,
WebCarrierNegotiationAggressiveness, WebConfig, WebDecoyConfig, WebDecoyFastTrackMode,
WebHttpConnectionCapacityAction, WebLimitsConfig, WebProfileConfig, WebSecretMode,
WebTimeoutsConfig, WebVhostConfig,
};
+7
View File
@@ -12,6 +12,9 @@ use super::web_debug::WebDebugConfig;
// Serialized WEB defaults remain separate from the runtime data model.
mod defaults;
use defaults::*;
// Decoy fast-track policy remains isolated from the bulky WEB data model.
mod fasttrack;
pub use fasttrack::WebDecoyFastTrackMode;
// Accepted-socket overload policy remains separate from the bulky WEB data model.
mod overload;
pub use overload::WebHttpConnectionCapacityAction;
@@ -416,6 +419,9 @@ pub struct WebConfig {
/// Controls the evidence thresholds used by automatic carrier ranking.
#[serde(default)]
pub carrier_negotiation_aggressiveness: WebCarrierNegotiationAggressiveness,
/// Restart-only capability-scan policy for structurally impossible bridge requests.
#[serde(default)]
pub decoy_fasttrack_mode: WebDecoyFastTrackMode,
/// Action applied when accepted HTTP connection capacity is exhausted.
#[serde(default)]
pub http_connection_capacity_action: WebHttpConnectionCapacityAction,
@@ -465,6 +471,7 @@ impl Default for WebConfig {
carriers: WebCarriers::default(),
carrier_learning: default_web_carrier_learning(),
carrier_negotiation_aggressiveness: WebCarrierNegotiationAggressiveness::default(),
decoy_fasttrack_mode: WebDecoyFastTrackMode::default(),
http_connection_capacity_action: WebHttpConnectionCapacityAction::default(),
limits: WebLimitsConfig::default(),
debug: WebDebugConfig::default(),
+28
View File
@@ -0,0 +1,28 @@
use serde::{Deserialize, Serialize};
/// Capability-scan policy for structurally impossible WEB bridge requests.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum WebDecoyFastTrackMode {
/// Preserve the legacy full scan without collecting fast-track decisions.
#[default]
Off,
/// Record eligible requests while preserving the legacy full scan.
Shadow,
/// Skip the scan only when the public request shape cannot open a bridge.
Enforce,
}
impl WebDecoyFastTrackMode {
/// Complete fixed mode set in stable API and metric order.
pub const ALL: [Self; 3] = [Self::Off, Self::Shadow, Self::Enforce];
/// Returns the stable serialized mode token.
pub const fn as_str(self) -> &'static str {
match self {
Self::Off => "off",
Self::Shadow => "shadow",
Self::Enforce => "enforce",
}
}
}
+4
View File
@@ -14,12 +14,16 @@ pub(crate) struct WebRuntimeConfig {
pub(crate) struct WebRuntimeVhost {
/// Canonical lowercase ACE hostname.
pub(crate) host: String,
/// Restart-frozen decoy capability-scan policy.
pub(crate) decoy_fasttrack_mode: WebDecoyFastTrackMode,
/// Immutable ordinary-site fallback snapshot.
pub(crate) decoy: WebRuntimeDecoy,
/// Upstream connect and response-head deadline.
pub(crate) decoy_header_secs: u64,
/// Exact capability profiles accepted by this host.
pub(crate) profiles: Vec<Arc<WebRuntimeProfile>>,
/// Contiguous capability table aligned one-to-one with `profiles`.
pub(crate) capabilities: Box<[[u8; 32]]>,
}
/// Precomputed exact-user capability entry.