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
+48 -6
View File
@@ -16,15 +16,18 @@ use ipnetwork::IpNetwork;
use tokio::net::TcpStream;
use tokio_util::sync::CancellationToken;
use crate::config::{WebClientIpSource, WebRuntimeVhost};
use crate::config::{WebClientIpSource, WebDecoyFastTrackMode, WebRuntimeVhost};
use crate::maestro::generation::RuntimeGeneration;
use crate::web::bridge;
use crate::web::manager::{ManagerError, WebProcessRuntime};
use crate::web::telemetry::WebDecoyFastTrackDisposition;
// Response-body activity keeps connection idle accounting lifecycle-correct.
mod activity;
// Body collection retains allocation permits through request processing.
mod body;
// Canonical capability parsing and complete scans remain isolated from HTTP routing.
mod capability;
// Decoy routing and upstream proxying are isolated from carrier authentication.
mod decoy;
// Downlink long-poll handling remains isolated from request routing.
@@ -49,11 +52,12 @@ mod trace_tests;
use crate::web::trace::{HttpTraceExchange, TraceDirection, TraceLifecycleEvent, TraceRoute};
use activity::{ActivityBody, ConnectionActivity, RequestActivity, RequestDeadlineHandle};
use body::{CollectBodyError, CollectedBody, RequestBody, collect_body};
use capability::bridge_candidate;
use decoy::serve_decoy;
use down::handle_down;
use request::{
bearer_token_hash, binary_content_type, bridge_candidate, canonical_request_host,
canonical_u64_header, client_ip, compatible_cookie_header, match_profile,
bearer_token_hash, binary_content_type, canonical_request_host, canonical_u64_header,
client_ip, compatible_cookie_header, match_profile,
};
use response::{
bad_gateway, carrier_empty, carrier_headers, carrier_lane, full_response, generic_not_found,
@@ -230,10 +234,48 @@ async fn handle_root(
strip_query(&mut request);
return serve_decoy(request, vhost, true, &runtime).await;
}
let (candidate, canonical) = bridge_candidate(request.uri().query());
let profile = match_profile(&vhost, &candidate);
let candidate = bridge_candidate(request.uri().query());
let canonical = candidate.is_canonical();
let plausible_candidate = canonical && request.method() == Method::GET;
let fasttrack_mode = vhost.decoy_fasttrack_mode;
match fasttrack_mode {
WebDecoyFastTrackMode::Off => {}
WebDecoyFastTrackMode::Shadow => {
runtime.telemetry().record_decoy_fasttrack(if plausible_candidate {
WebDecoyFastTrackDisposition::ShadowCandidateFullScan
} else {
WebDecoyFastTrackDisposition::ShadowWouldFastTrack
});
}
WebDecoyFastTrackMode::Enforce if !plausible_candidate => {
runtime
.telemetry()
.record_decoy_fasttrack(WebDecoyFastTrackDisposition::EnforceFastTrack);
let recovery_requested =
matches!(representation, recovery::RootRepresentation::Recovery(_));
if recovery_requested {
strip_query(&mut request);
}
return serve_decoy(request, vhost, recovery_requested, &runtime).await;
}
WebDecoyFastTrackMode::Enforce => {
runtime.telemetry().record_decoy_fasttrack(
WebDecoyFastTrackDisposition::EnforceCandidateFullScan,
);
}
}
let matched_profile = match_profile(&vhost, candidate.scan_bytes());
let recovery_requested = matches!(representation, recovery::RootRepresentation::Recovery(_));
let Some(profile) = profile.filter(|_| canonical && request.method() == Method::GET) else {
let profile = matched_profile.filter(|_| canonical && request.method() == Method::GET);
if fasttrack_mode == WebDecoyFastTrackMode::Shadow
&& !plausible_candidate
&& profile.is_some()
{
runtime
.telemetry()
.record_decoy_fasttrack_shadow_mismatch();
}
let Some(profile) = profile else {
if recovery_requested {
strip_query(&mut request);
}
+94
View File
@@ -0,0 +1,94 @@
use base64::Engine as _;
use subtle::{Choice, ConditionallySelectable, ConstantTimeEq};
const NON_CANONICAL_BRIDGE_CANDIDATE: [u8; 32] = [0; 32];
/// Parsed public bridge query without an allocated credential string.
#[derive(Clone, Copy)]
pub(super) enum BridgeCandidate {
/// The query cannot authenticate a bridge under the public request grammar.
NonCanonical,
/// Exact canonical base64url capability bytes.
Canonical([u8; 32]),
}
impl BridgeCandidate {
/// Returns whether this query can authenticate a bridge.
pub(super) const fn is_canonical(self) -> bool {
matches!(self, Self::Canonical(_))
}
/// Returns the candidate used by the legacy full-scan path.
pub(super) fn scan_bytes(&self) -> &[u8; 32] {
match self {
Self::NonCanonical => &NON_CANONICAL_BRIDGE_CANDIDATE,
Self::Canonical(candidate) => candidate,
}
}
}
/// Decodes an exact canonical bridge query without allocating credential strings.
pub(super) fn bridge_candidate(query: Option<&str>) -> BridgeCandidate {
let Some(value) = query.and_then(|query| query.strip_prefix("bridge=")) else {
return BridgeCandidate::NonCanonical;
};
if value.len() != 43 {
return BridgeCandidate::NonCanonical;
}
let mut decoded = [0u8; 32];
let Ok(decoded_len) =
base64::engine::general_purpose::URL_SAFE_NO_PAD.decode_slice(value, &mut decoded)
else {
return BridgeCandidate::NonCanonical;
};
let mut canonical = [0u8; 43];
let Ok(encoded_len) =
base64::engine::general_purpose::URL_SAFE_NO_PAD.encode_slice(decoded, &mut canonical)
else {
return BridgeCandidate::NonCanonical;
};
if decoded_len != decoded.len()
|| encoded_len != canonical.len()
|| !bool::from(canonical.ct_eq(value.as_bytes()))
{
return BridgeCandidate::NonCanonical;
}
BridgeCandidate::Canonical(decoded)
}
/// Internal result of one complete capability-table scan.
pub(super) struct CapabilityScan {
/// Whether any capability matched.
pub(super) matched: Choice,
/// Matching table position selected without a data-dependent branch.
pub(super) matched_index: u64,
#[cfg(test)]
/// Exact comparison count exposed only to deterministic unit tests.
pub(super) comparisons: usize,
}
/// Scans every configured capability without candidate-dependent control flow.
pub(super) fn scan_capabilities(
capabilities: &[[u8; 32]],
candidate: &[u8; 32],
) -> CapabilityScan {
let mut matched = Choice::from(0);
let mut matched_index = 0u64;
#[cfg(test)]
let mut comparisons = 0usize;
for (index, capability) in capabilities.iter().enumerate() {
let equal = capability.ct_eq(candidate);
matched_index = u64::conditional_select(&matched_index, &(index as u64), equal);
matched |= equal;
#[cfg(test)]
{
comparisons += 1;
}
}
CapabilityScan {
matched,
matched_index,
#[cfg(test)]
comparisons,
}
}
+293
View File
@@ -0,0 +1,293 @@
use super::*;
use crate::config::WebDecoyFastTrackMode;
use crate::web::telemetry::WebDecoyFastTrackDisposition;
const RECOVERY_TYPE: &str = "application/vnd.telemt.web-recovery+json";
struct Observation {
response: Vec<u8>,
counters: [u64; WebDecoyFastTrackDisposition::ALL.len()],
shadow_mismatches: u64,
}
async fn capture_origin_request(listener: &TcpListener) -> Vec<u8> {
let (mut stream, _) = listener.accept().await.unwrap();
let mut captured = Vec::new();
let mut buffer = [0u8; 4096];
loop {
let read = stream.read(&mut buffer).await.unwrap();
assert_ne!(read, 0, "decoy origin connection closed before the request completed");
captured.extend_from_slice(&buffer[..read]);
let Some(header_end) = captured.windows(4).position(|window| window == b"\r\n\r\n")
else {
continue;
};
let headers = std::str::from_utf8(&captured[..header_end]).unwrap();
let content_length = headers
.lines()
.filter_map(|line| line.split_once(':'))
.find_map(|(name, value)| {
name.eq_ignore_ascii_case("content-length")
.then(|| value.trim().parse::<usize>().unwrap())
})
.unwrap_or(0);
if captured.len() >= header_end + 4 + content_length {
break;
}
}
stream
.write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 6\r\nConnection: close\r\n\r\norigin")
.await
.unwrap();
captured
}
async fn observe_http_origin(
mode: WebDecoyFastTrackMode,
capability: [u8; 32],
request_bytes: Vec<u8>,
origin: &TcpListener,
) -> (Observation, Vec<u8>) {
let mut config = runtime_config_with_fasttrack(capability, WebCarrier::Https, mode);
let runtime_config = Arc::get_mut(config.web.runtime.as_mut().unwrap()).unwrap();
let vhost = Arc::get_mut(
runtime_config
.vhosts
.get_mut("proxy.example.com")
.unwrap(),
)
.unwrap();
vhost.decoy = WebRuntimeDecoy::HttpUpstream {
addr: origin.local_addr().unwrap(),
authority: "decoy.example".to_string(),
};
let generation = test_runtime_generation(1, config);
let active_runtime = Arc::new(ArcSwap::from(Arc::clone(&generation)));
let runtime = WebProcessRuntime::start(active_runtime);
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let (response, captured) = tokio::join!(
request(&listener, &runtime, request_bytes),
capture_origin_request(origin)
);
let counters = WebDecoyFastTrackDisposition::ALL
.map(|disposition| runtime.telemetry().decoy_fasttrack_total(disposition));
let shadow_mismatches = runtime.telemetry().decoy_fasttrack_shadow_mismatches();
runtime.shutdown().await;
generation.stop_sessions().await;
generation.stop_background_tasks().await;
(
Observation {
response,
counters,
shadow_mismatches,
},
captured,
)
}
async fn observe(mode: WebDecoyFastTrackMode, capability: [u8; 32], request_bytes: Vec<u8>) -> Observation {
let generation = test_runtime_generation(
1,
runtime_config_with_fasttrack(capability, WebCarrier::Https, mode),
);
let active_runtime = Arc::new(ArcSwap::from(Arc::clone(&generation)));
let runtime = WebProcessRuntime::start(active_runtime);
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let response = request(&listener, &runtime, request_bytes).await;
let counters = WebDecoyFastTrackDisposition::ALL
.map(|disposition| runtime.telemetry().decoy_fasttrack_total(disposition));
let shadow_mismatches = runtime.telemetry().decoy_fasttrack_shadow_mismatches();
runtime.shutdown().await;
generation.stop_sessions().await;
generation.stop_background_tasks().await;
Observation {
response,
counters,
shadow_mismatches,
}
}
fn root_request(method: &str, query: &str) -> Vec<u8> {
format!(
"{method} /{query} HTTP/1.1\r\nHost: proxy.example.com\r\nX-Forwarded-For: 192.0.2.90\r\nConnection: close\r\n\r\n"
)
.into_bytes()
}
#[tokio::test]
async fn impossible_root_shapes_preserve_decoy_bytes_and_follow_the_selected_mode() {
let capability = [70u8; 32];
let encoded = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(capability);
for request_bytes in [
root_request("GET", ""),
root_request("GET", "?bridge=not-canonical"),
root_request("HEAD", &format!("?bridge={encoded}")),
] {
let off = observe(WebDecoyFastTrackMode::Off, capability, request_bytes.clone()).await;
let shadow = observe(
WebDecoyFastTrackMode::Shadow,
capability,
request_bytes.clone(),
)
.await;
let enforce = observe(WebDecoyFastTrackMode::Enforce, capability, request_bytes).await;
assert_eq!(shadow.response, off.response);
assert_eq!(enforce.response, off.response);
assert_eq!(off.counters, [0, 0, 0, 0]);
assert_eq!(shadow.counters, [1, 0, 0, 0]);
assert_eq!(enforce.counters, [0, 0, 1, 0]);
assert_eq!(shadow.shadow_mismatches, 0);
}
}
#[tokio::test]
async fn canonical_hit_and_miss_always_retain_the_full_scan() {
let capability = [71u8; 32];
for candidate in [capability, [72u8; 32]] {
let encoded = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(candidate);
let request_bytes = root_request("GET", &format!("?bridge={encoded}"));
for (mode, expected_counters) in [
(WebDecoyFastTrackMode::Off, [0, 0, 0, 0]),
(WebDecoyFastTrackMode::Shadow, [0, 1, 0, 0]),
(WebDecoyFastTrackMode::Enforce, [0, 0, 0, 1]),
] {
let observation = observe(mode, capability, request_bytes.clone()).await;
assert!(observation.response.starts_with(b"HTTP/1.1 200"));
assert_eq!(observation.counters, expected_counters);
assert_eq!(observation.shadow_mismatches, 0);
if candidate == capability {
assert!(
observation
.response
.windows(b"bootstrap=\"".len())
.any(|window| window == b"bootstrap=\"")
);
} else {
let (_, body) = split_response(&observation.response);
assert_eq!(body, b"<!doctype html><title>decoy</title>");
}
}
}
}
#[tokio::test]
async fn recovery_sanitization_precedes_fasttrack_classification() {
let capability = [73u8; 32];
let request_bytes = format!(
"GET /?bridge=not-canonical HTTP/1.1\r\nHost: proxy.example.com\r\nX-Forwarded-For: 192.0.2.90\r\nAccept: {RECOVERY_TYPE}\r\nAuthorization: Bearer {}\r\nContent-Length: 4\r\nConnection: close\r\n\r\nbody",
"U".repeat(43)
)
.into_bytes();
let off = observe(WebDecoyFastTrackMode::Off, capability, request_bytes.clone()).await;
let shadow = observe(
WebDecoyFastTrackMode::Shadow,
capability,
request_bytes.clone(),
)
.await;
let enforce = observe(WebDecoyFastTrackMode::Enforce, capability, request_bytes).await;
assert_eq!(shadow.response, off.response);
assert_eq!(enforce.response, off.response);
assert_eq!(response_header(split_response(&off.response).0, "cache-control"), "no-store");
assert_eq!(off.counters, [0, 0, 0, 0]);
assert_eq!(shadow.counters, [1, 0, 0, 0]);
assert_eq!(enforce.counters, [0, 0, 1, 0]);
assert_eq!(shadow.shadow_mismatches, 0);
}
#[tokio::test]
async fn http_origin_forwarding_is_identical_across_modes() {
let capability = [74u8; 32];
let origin = TcpListener::bind("127.0.0.1:0").await.unwrap();
let mut request_bytes = b"GET /?bridge=not-canonical HTTP/1.1\r\nHost: proxy.example.com\r\nX-Forwarded-For: 192.0.2.90\r\nX-Ordinary: preserved\r\nContent-Length: 4\r\nConnection: close\r\n\r\n".to_vec();
request_bytes.extend_from_slice(b"body");
let (off, off_upstream) = observe_http_origin(
WebDecoyFastTrackMode::Off,
capability,
request_bytes.clone(),
&origin,
)
.await;
let (shadow, shadow_upstream) = observe_http_origin(
WebDecoyFastTrackMode::Shadow,
capability,
request_bytes.clone(),
&origin,
)
.await;
let (enforce, enforce_upstream) = observe_http_origin(
WebDecoyFastTrackMode::Enforce,
capability,
request_bytes,
&origin,
)
.await;
assert_eq!(shadow.response, off.response);
assert_eq!(enforce.response, off.response);
assert_eq!(shadow_upstream, off_upstream);
assert_eq!(enforce_upstream, off_upstream);
assert!(off_upstream.starts_with(b"GET /?bridge=not-canonical HTTP/1.1\r\n"));
assert!(off_upstream.windows(21).any(|window| window == b"x-ordinary: preserved"));
assert!(off_upstream.ends_with(b"\r\n\r\nbody"));
}
#[tokio::test]
async fn http_origin_recovery_sanitization_is_identical_across_modes() {
let capability = [75u8; 32];
let origin = TcpListener::bind("127.0.0.1:0").await.unwrap();
let request_bytes = format!(
"GET /?bridge=not-canonical HTTP/1.1\r\nHost: proxy.example.com\r\nX-Forwarded-For: 192.0.2.90\r\nAccept: {RECOVERY_TYPE}\r\nAuthorization: Bearer {}\r\nContent-Type: application/octet-stream\r\nContent-Length: 4\r\nX-Up-Seq: 9\r\nConnection: close\r\n\r\nbody",
"U".repeat(43)
)
.into_bytes();
let (off, off_upstream) = observe_http_origin(
WebDecoyFastTrackMode::Off,
capability,
request_bytes.clone(),
&origin,
)
.await;
let (shadow, shadow_upstream) = observe_http_origin(
WebDecoyFastTrackMode::Shadow,
capability,
request_bytes.clone(),
&origin,
)
.await;
let (enforce, enforce_upstream) = observe_http_origin(
WebDecoyFastTrackMode::Enforce,
capability,
request_bytes,
&origin,
)
.await;
assert_eq!(shadow.response, off.response);
assert_eq!(enforce.response, off.response);
assert_eq!(shadow_upstream, off_upstream);
assert_eq!(enforce_upstream, off_upstream);
assert!(off_upstream.starts_with(b"GET / HTTP/1.1\r\n"));
let lowercase = String::from_utf8_lossy(&off_upstream).to_ascii_lowercase();
for forbidden in [
"authorization:",
"accept:",
"content-type:",
"content-length:",
"x-up-seq:",
] {
assert!(!lowercase.contains(forbidden));
}
assert!(off_upstream.ends_with(b"\r\n\r\n"));
}
+12 -38
View File
@@ -13,53 +13,27 @@ use crate::web::manager::{
const USER_AGENT_CONTEXT: &[u8] = b"telemt-web-carrier-user-agent-v1\0";
use super::capability::scan_capabilities;
// Canonical host and forwarded-address provenance remain isolated from credentials.
mod identity;
pub(super) use identity::{canonical_request_host, carrier_ip_learning_eligible, client_ip};
/// Decodes an exact canonical bridge query without allocating credential strings.
pub(super) fn bridge_candidate(query: Option<&str>) -> ([u8; 32], bool) {
let mut candidate = [0u8; 32];
let Some(value) = query.and_then(|query| query.strip_prefix("bridge=")) else {
return (candidate, false);
};
if value.len() != 43 {
return (candidate, false);
}
let mut decoded = [0u8; 32];
let Ok(decoded_len) =
base64::engine::general_purpose::URL_SAFE_NO_PAD.decode_slice(value, &mut decoded)
else {
return (candidate, false);
};
let mut canonical = [0u8; 43];
let Ok(encoded_len) =
base64::engine::general_purpose::URL_SAFE_NO_PAD.encode_slice(decoded, &mut canonical)
else {
return (candidate, false);
};
if decoded_len != decoded.len()
|| encoded_len != canonical.len()
|| !bool::from(canonical.ct_eq(value.as_bytes()))
{
return (candidate, false);
}
candidate = decoded;
(candidate, true)
}
/// Matches a capability in constant time across every profile of one virtual host.
/// Matches one capability after a complete branchless scan of the virtual host table.
pub(super) fn match_profile(
vhost: &WebRuntimeVhost,
candidate: &[u8; 32],
) -> Option<Arc<WebRuntimeProfile>> {
let mut matched = None;
for profile in &vhost.profiles {
if bool::from(profile.capability.ct_eq(candidate)) {
matched = Some(Arc::clone(profile));
}
debug_assert_eq!(vhost.capabilities.len(), vhost.profiles.len());
let scan = scan_capabilities(&vhost.capabilities, candidate);
if bool::from(scan.matched) {
usize::try_from(scan.matched_index)
.ok()
.and_then(|index| vhost.profiles.get(index))
.map(Arc::clone)
} else {
None
}
matched
}
/// Validates and hashes one canonical bearer credential for map lookup.
+34 -3
View File
@@ -1,14 +1,45 @@
use super::*;
use ipnetwork::IpNetwork;
use proptest::prelude::*;
use crate::config::{WebCarrier, WebClientIpSource};
use crate::web::http::capability::{bridge_candidate, scan_capabilities};
#[test]
fn canonical_bridge_query_rejects_aliases() {
let token = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode([7u8; 32]);
assert!(bridge_candidate(Some(&format!("bridge={token}"))).1);
assert!(!bridge_candidate(Some(&format!("x=1&bridge={token}"))).1);
assert!(!bridge_candidate(Some(&format!("bridge={token}="))).1);
assert!(bridge_candidate(Some(&format!("bridge={token}"))).is_canonical());
assert!(!bridge_candidate(Some(&format!("x=1&bridge={token}"))).is_canonical());
assert!(!bridge_candidate(Some(&format!("bridge={token}="))).is_canonical());
}
#[test]
fn capability_scan_checks_every_entry_independent_of_match_position() {
let capabilities = [[1u8; 32], [2u8; 32], [3u8; 32], [4u8; 32]];
for (expected_index, candidate) in capabilities.iter().enumerate() {
let scan = scan_capabilities(&capabilities, candidate);
assert!(bool::from(scan.matched));
assert_eq!(scan.matched_index as usize, expected_index);
assert_eq!(scan.comparisons, capabilities.len());
}
let miss = scan_capabilities(&capabilities, &[99u8; 32]);
assert!(!bool::from(miss.matched));
assert_eq!(miss.comparisons, capabilities.len());
let empty = scan_capabilities(&[], &[99u8; 32]);
assert!(!bool::from(empty.matched));
assert_eq!(empty.comparisons, 0);
}
proptest! {
#[test]
fn every_capability_has_one_canonical_bridge_query(capability in any::<[u8; 32]>()) {
let token = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(capability);
let candidate = bridge_candidate(Some(&format!("bridge={token}")));
prop_assert!(candidate.is_canonical());
prop_assert_eq!(candidate.scan_bytes(), &capability);
}
}
#[test]
+24 -1
View File
@@ -11,7 +11,8 @@ use tokio_util::sync::CancellationToken;
use super::serve_connection;
use crate::config::{
ProxyConfig, WebCarrier, WebCarriers, WebClientIpSource, WebRuntimeConfig, WebRuntimeDecoy,
WebRuntimeProfile, WebRuntimeVhost, WebSecretMode, WebStaticAsset, WebStaticSite,
WebDecoyFastTrackMode, WebRuntimeProfile, WebRuntimeVhost, WebSecretMode, WebStaticAsset,
WebStaticSite,
};
use crate::maestro::generation::test_runtime_generation;
use crate::web::frame::{self, FrameType};
@@ -39,6 +40,9 @@ mod operator_lifecycle_tests;
// Positive-only recovery representation coverage remains isolated from ordinary root routing.
#[path = "recovery_tests.rs"]
mod recovery_tests;
// Decoy fast-track routing and telemetry remain isolated from carrier protocol scenarios.
#[path = "decoy_fasttrack_tests.rs"]
mod decoy_fasttrack_tests;
const TEST_CARRIER_DEADLINES_SECS: [u64; 4] = [3, 5, 8, 12];
@@ -46,6 +50,21 @@ pub(super) fn runtime_config(capability: [u8; 32], carrier: WebCarrier) -> Proxy
runtime_config_with_carriers(capability, carrier, false, true, Arc::from([carrier]))
}
/// Builds a static-decoy runtime with one restart-frozen fast-track mode.
pub(super) fn runtime_config_with_fasttrack(
capability: [u8; 32],
carrier: WebCarrier,
mode: WebDecoyFastTrackMode,
) -> ProxyConfig {
let mut config = runtime_config(capability, carrier);
config.web.decoy_fasttrack_mode = mode;
let runtime = Arc::get_mut(config.web.runtime.as_mut().unwrap()).unwrap();
for vhost in runtime.vhosts.values_mut() {
Arc::get_mut(vhost).unwrap().decoy_fasttrack_mode = mode;
}
config
}
pub(super) fn negotiation_runtime_config(
capability: [u8; 32],
carrier: WebCarrier,
@@ -128,9 +147,11 @@ fn runtime_config_with_carriers_and_deadlines(
});
let vhost = Arc::new(WebRuntimeVhost {
host: "proxy.example.com".to_string(),
decoy_fasttrack_mode: WebDecoyFastTrackMode::Off,
decoy: WebRuntimeDecoy::StaticDirectory(Arc::clone(&site)),
decoy_header_secs: 1,
profiles: vec![Arc::clone(&profile)],
capabilities: vec![capability].into_boxed_slice(),
});
let mut vhosts = BTreeMap::new();
vhosts.insert("proxy.example.com".to_string(), vhost);
@@ -138,9 +159,11 @@ fn runtime_config_with_carriers_and_deadlines(
"other.example.com".to_string(),
Arc::new(WebRuntimeVhost {
host: "other.example.com".to_string(),
decoy_fasttrack_mode: WebDecoyFastTrackMode::Off,
decoy: WebRuntimeDecoy::StaticDirectory(site),
decoy_header_secs: 1,
profiles: Vec::new(),
capabilities: Vec::new().into_boxed_slice(),
}),
);
let mut config = ProxyConfig::default();
+7
View File
@@ -10,6 +10,9 @@ pub(crate) use carrier::{
WebCarrierFailureCounter, WebCarrierFailurePhase, WebCarrierLearningCounter,
WebCarrierLearningOutcome, WebCarrierSelectionCounter, WebCarrierSelectionDisposition,
};
mod fasttrack;
use fasttrack::DECOY_FASTTRACK_SLOTS;
pub(crate) use fasttrack::{WebDecoyFastTrackCounter, WebDecoyFastTrackDisposition};
mod lifecycle;
use lifecycle::{SESSION_CLOSE_SLOTS, SESSION_OBSERVATION_SLOTS};
pub(crate) use lifecycle::{
@@ -316,6 +319,8 @@ pub(crate) struct WebTelemetry {
carrier_selections: [AtomicU64; CARRIER_SELECTION_SLOTS],
carrier_failures: [AtomicU64; CARRIER_FAILURE_SLOTS],
carrier_learning_outcomes: [AtomicU64; CARRIER_LEARNING_SLOTS],
decoy_fasttrack_requests: [AtomicU64; DECOY_FASTTRACK_SLOTS],
decoy_fasttrack_shadow_mismatches: AtomicU64,
session_closures: [AtomicU64; SESSION_CLOSE_SLOTS],
session_observations: [AtomicU64; SESSION_OBSERVATION_SLOTS],
bridge_recovery_events: [AtomicU64; WebBridgeRecoveryEvent::ALL.len()],
@@ -343,6 +348,8 @@ impl WebTelemetry {
carrier_selections: std::array::from_fn(|_| AtomicU64::new(0)),
carrier_failures: std::array::from_fn(|_| AtomicU64::new(0)),
carrier_learning_outcomes: std::array::from_fn(|_| AtomicU64::new(0)),
decoy_fasttrack_requests: std::array::from_fn(|_| AtomicU64::new(0)),
decoy_fasttrack_shadow_mismatches: AtomicU64::new(0),
session_closures: std::array::from_fn(|_| AtomicU64::new(0)),
session_observations: std::array::from_fn(|_| AtomicU64::new(0)),
bridge_recovery_events: std::array::from_fn(|_| AtomicU64::new(0)),
+85
View File
@@ -0,0 +1,85 @@
use std::sync::atomic::Ordering;
use serde::Serialize;
use super::WebTelemetry;
/// Terminal capability-routing work selected for one WEB root request.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[repr(usize)]
pub(crate) enum WebDecoyFastTrackDisposition {
/// Shadow mode identified a request that enforce mode would bypass.
ShadowWouldFastTrack,
/// Shadow mode retained a full scan for a plausible capability request.
ShadowCandidateFullScan,
/// Enforce mode bypassed a structurally impossible capability request.
EnforceFastTrack,
/// Enforce mode retained a full scan for a plausible capability request.
EnforceCandidateFullScan,
}
impl WebDecoyFastTrackDisposition {
/// Complete fixed disposition set in stable API and metric order.
pub(crate) const ALL: [Self; 4] = [
Self::ShadowWouldFastTrack,
Self::ShadowCandidateFullScan,
Self::EnforceFastTrack,
Self::EnforceCandidateFullScan,
];
/// Returns the stable API and Prometheus label token.
pub(crate) const fn as_str(self) -> &'static str {
match self {
Self::ShadowWouldFastTrack => "shadow_would_fasttrack",
Self::ShadowCandidateFullScan => "shadow_candidate_full_scan",
Self::EnforceFastTrack => "enforce_fasttrack",
Self::EnforceCandidateFullScan => "enforce_candidate_full_scan",
}
}
}
pub(super) const DECOY_FASTTRACK_SLOTS: usize = WebDecoyFastTrackDisposition::ALL.len();
/// API-safe fixed decoy fast-track counter.
#[derive(Clone, Serialize)]
pub(crate) struct WebDecoyFastTrackCounter {
/// Stable capability-routing disposition token.
pub(crate) disposition: &'static str,
/// Process-lifetime event count.
pub(crate) total: u64,
}
impl WebTelemetry {
/// Records one shadow or enforce capability-routing disposition.
pub(crate) fn record_decoy_fasttrack(&self, disposition: WebDecoyFastTrackDisposition) {
self.decoy_fasttrack_requests[disposition as usize].fetch_add(1, Ordering::Relaxed);
}
/// Returns one fixed decoy fast-track counter.
pub(crate) fn decoy_fasttrack_total(&self, disposition: WebDecoyFastTrackDisposition) -> u64 {
self.decoy_fasttrack_requests[disposition as usize].load(Ordering::Relaxed)
}
/// Captures the complete decoy fast-track counter set.
pub(crate) fn decoy_fasttrack_counters(&self) -> Vec<WebDecoyFastTrackCounter> {
WebDecoyFastTrackDisposition::ALL
.into_iter()
.map(|disposition| WebDecoyFastTrackCounter {
disposition: disposition.as_str(),
total: self.decoy_fasttrack_total(disposition),
})
.collect()
}
/// Records a shadow decision that disagreed with legacy bridge eligibility.
pub(crate) fn record_decoy_fasttrack_shadow_mismatch(&self) {
self.decoy_fasttrack_shadow_mismatches
.fetch_add(1, Ordering::Relaxed);
}
/// Returns shadow decisions that disagreed with legacy bridge eligibility.
pub(crate) fn decoy_fasttrack_shadow_mismatches(&self) -> u64 {
self.decoy_fasttrack_shadow_mismatches
.load(Ordering::Relaxed)
}
}
+10
View File
@@ -18,6 +18,7 @@ fn fixed_counter_sets_and_acceptor_guard_are_exact() {
CarrierFailure::Network,
);
telemetry.record_carrier_learning(WebCarrier::Https, WebCarrierLearningOutcome::Recorded);
telemetry.record_decoy_fasttrack(WebDecoyFastTrackDisposition::ShadowWouldFastTrack);
telemetry.record_session_closed(WebCarrier::Https, SessionCloseReason::ApiClose);
telemetry.record_session_observation(
WebCarrier::Https,
@@ -48,6 +49,15 @@ fn fixed_counter_sets_and_acceptor_guard_are_exact() {
telemetry.carrier_learning_counters().len(),
WebCarrier::ALL.len() * WebCarrierLearningOutcome::ALL.len()
);
assert_eq!(
telemetry.decoy_fasttrack_counters().len(),
WebDecoyFastTrackDisposition::ALL.len()
);
assert_eq!(
telemetry.decoy_fasttrack_total(WebDecoyFastTrackDisposition::ShadowWouldFastTrack),
1
);
assert_eq!(telemetry.decoy_fasttrack_shadow_mismatches(), 0);
assert_eq!(
telemetry.session_close_counters().len(),
WebCarrier::ALL.len() * SessionCloseReason::ALL.len()