Capture TLS fingerprints in report context

This commit is contained in:
fawney19
2026-05-06 15:01:31 +08:00
parent 621eb55ae1
commit bfda9b3e78
7 changed files with 818 additions and 111 deletions

View File

@@ -4,7 +4,11 @@ use aether_ai_formats::api::{
ExecutionRuntimeAuthContext, EXECUTION_RUNTIME_STREAM_DECISION_ACTION,
EXECUTION_RUNTIME_SYNC_DECISION_ACTION,
};
use aether_contracts::{ExecutionTimeouts, ProxySnapshot, ResolvedTransportProfile};
use aether_contracts::{
ExecutionTimeouts, ProxySnapshot, ResolvedTransportProfile, TRANSPORT_BACKEND_HYPER_RUSTLS,
TRANSPORT_BACKEND_REQWEST_RUSTLS, TRANSPORT_HTTP_MODE_HTTP1_ONLY,
};
use serde_json::{json, Map, Value};
use crate::{AiExecutionDecision, ConversionMode, ExecutionStrategy};
@@ -43,8 +47,14 @@ pub struct AiExecutionDecisionResponseParts {
}
pub fn build_ai_execution_decision_response(
parts: AiExecutionDecisionResponseParts,
mut parts: AiExecutionDecisionResponseParts,
) -> AiExecutionDecision {
parts.report_context = attach_outgoing_tls_fingerprint(
parts.report_context,
parts.transport_profile.as_ref(),
parts.proxy.as_ref(),
);
AiExecutionDecision {
action: ai_execution_decision_action(parts.decision_is_stream).to_string(),
decision_kind: Some(parts.decision_kind),
@@ -90,3 +100,196 @@ pub const fn ai_execution_decision_action(decision_is_stream: bool) -> &'static
EXECUTION_RUNTIME_SYNC_DECISION_ACTION
}
}
fn attach_outgoing_tls_fingerprint(
report_context: Option<Value>,
transport_profile: Option<&ResolvedTransportProfile>,
proxy: Option<&ProxySnapshot>,
) -> Option<Value> {
let mut report_context = match report_context {
Some(Value::Object(object)) => object,
_ => Map::new(),
};
let tls_fingerprint = report_context
.entry("tls_fingerprint".to_string())
.or_insert_with(|| Value::Object(Map::new()));
if let Value::Object(object) = tls_fingerprint {
object.insert(
"outgoing".to_string(),
outgoing_tls_fingerprint_value(transport_profile, proxy),
);
}
Some(Value::Object(report_context))
}
fn outgoing_tls_fingerprint_value(
transport_profile: Option<&ResolvedTransportProfile>,
proxy: Option<&ProxySnapshot>,
) -> Value {
let via_local_proxy = proxy
.and_then(|value| value.node_id.as_deref())
.map(str::trim)
.is_some_and(|value| !value.is_empty());
let backend = transport_profile
.map(|profile| profile.backend.trim())
.filter(|value| !value.is_empty())
.unwrap_or(if via_local_proxy {
TRANSPORT_BACKEND_HYPER_RUSTLS
} else {
TRANSPORT_BACKEND_REQWEST_RUSTLS
});
let http_mode = transport_profile
.map(|profile| profile.http_mode.trim())
.filter(|value| !value.is_empty())
.unwrap_or("auto");
let alpn_offered = if http_mode.eq_ignore_ascii_case(TRANSPORT_HTTP_MODE_HTTP1_ONLY) {
json!(["http/1.1"])
} else {
json!(["h2", "http/1.1"])
};
let transport_path = if via_local_proxy {
"aether_proxy_tunnel"
} else if proxy.is_some() {
"direct_with_proxy"
} else {
"direct"
};
let mut object = Map::new();
object.insert(
"source".to_string(),
Value::String("aether_transport_config".to_string()),
);
object.insert("observed".to_string(), Value::Bool(false));
object.insert(
"transport_path".to_string(),
Value::String(transport_path.to_string()),
);
object.insert("backend".to_string(), Value::String(backend.to_string()));
object.insert(
"http_mode".to_string(),
Value::String(http_mode.to_string()),
);
object.insert("tls_stack".to_string(), Value::String("rustls".to_string()));
object.insert(
"tls_versions_offered".to_string(),
json!(["TLS1.3", "TLS1.2"]),
);
object.insert("alpn_offered".to_string(), alpn_offered);
if let Some(profile) = transport_profile {
object.insert(
"profile_id".to_string(),
Value::String(profile.profile_id.clone()),
);
object.insert(
"pool_scope".to_string(),
Value::String(profile.pool_scope.clone()),
);
}
Value::Object(object)
}
#[cfg(test)]
mod tests {
use super::*;
use aether_contracts::{TRANSPORT_HTTP_MODE_AUTO, TRANSPORT_POOL_SCOPE_KEY};
fn sample_parts() -> AiExecutionDecisionResponseParts {
AiExecutionDecisionResponseParts {
decision_is_stream: false,
decision_kind: "local_sync".to_string(),
execution_strategy: ExecutionStrategy::LocalSameFormat,
conversion_mode: ConversionMode::None,
request_id: "trace-1".to_string(),
candidate_id: "candidate-1".to_string(),
provider_name: "OpenAI".to_string(),
provider_id: "provider-1".to_string(),
endpoint_id: "endpoint-1".to_string(),
key_id: "key-1".to_string(),
upstream_base_url: "https://api.example.com".to_string(),
upstream_url: "https://api.example.com/v1/chat/completions".to_string(),
provider_request_method: None,
auth_header: None,
auth_value: None,
provider_api_format: "openai:chat".to_string(),
client_api_format: "openai:chat".to_string(),
model_name: "gpt-5".to_string(),
mapped_model: "gpt-5".to_string(),
prompt_cache_key: None,
provider_request_headers: BTreeMap::new(),
provider_request_body: Some(json!({"model": "gpt-5"})),
provider_request_body_base64: None,
content_type: Some("application/json".to_string()),
proxy: None,
transport_profile: None,
timeouts: None,
upstream_is_stream: false,
report_kind: None,
report_context: Some(json!({
"tls_fingerprint": {
"incoming": {
"source": "forwarded_header",
"ja3": "incoming-ja3"
}
}
})),
auth_context: ExecutionRuntimeAuthContext {
user_id: "user-1".to_string(),
api_key_id: "api-key-1".to_string(),
username: None,
api_key_name: None,
balance_remaining: None,
access_allowed: true,
api_key_is_standalone: false,
},
}
}
#[test]
fn decision_response_records_outgoing_tls_fingerprint_without_dropping_incoming() {
let mut parts = sample_parts();
parts.transport_profile = Some(ResolvedTransportProfile {
profile_id: "claude_code_nodejs".to_string(),
backend: TRANSPORT_BACKEND_REQWEST_RUSTLS.to_string(),
http_mode: TRANSPORT_HTTP_MODE_AUTO.to_string(),
pool_scope: TRANSPORT_POOL_SCOPE_KEY.to_string(),
header_fingerprint: None,
extra: None,
});
let decision = build_ai_execution_decision_response(parts);
let tls_fingerprint = &decision.report_context.unwrap()["tls_fingerprint"];
assert_eq!(tls_fingerprint["incoming"]["ja3"], "incoming-ja3");
assert_eq!(
tls_fingerprint["outgoing"]["profile_id"],
"claude_code_nodejs"
);
assert_eq!(
tls_fingerprint["outgoing"]["backend"],
TRANSPORT_BACKEND_REQWEST_RUSTLS
);
assert_eq!(tls_fingerprint["outgoing"]["observed"], false);
assert_eq!(
tls_fingerprint["outgoing"]["alpn_offered"],
json!(["h2", "http/1.1"])
);
}
#[test]
fn decision_response_defaults_outgoing_tls_to_proxy_hyper_backend_for_node_proxy() {
let mut parts = sample_parts();
parts.proxy = Some(ProxySnapshot {
node_id: Some("proxy-node-1".to_string()),
..ProxySnapshot::default()
});
let decision = build_ai_execution_decision_response(parts);
let outgoing = &decision.report_context.unwrap()["tls_fingerprint"]["outgoing"];
assert_eq!(outgoing["transport_path"], "aether_proxy_tunnel");
assert_eq!(outgoing["backend"], TRANSPORT_BACKEND_HYPER_RUSTLS);
}
}

View File

@@ -87,6 +87,7 @@ fn copy_allowed_metadata_fields(source: &Map<String, Value>, target: &mut Map<St
copy_non_null_value(source, target, "dimensions");
copy_non_null_value(source, target, "billing_rule_snapshot");
copy_non_null_value(source, target, "scheduling_audit");
copy_non_null_value(source, target, "tls_fingerprint");
copy_number(source, target, "rate_multiplier");
copy_bool(source, target, "is_free_tier");
copy_number(source, target, "input_price_per_1m");
@@ -119,6 +120,7 @@ fn move_allowed_metadata_fields(mut source: Map<String, Value>, target: &mut Map
remove_non_null_value(&mut source, target, "dimensions");
remove_non_null_value(&mut source, target, "billing_rule_snapshot");
remove_non_null_value(&mut source, target, "scheduling_audit");
remove_non_null_value(&mut source, target, "tls_fingerprint");
remove_number(&mut source, target, "rate_multiplier");
remove_bool(&mut source, target, "is_free_tier");
remove_number(&mut source, target, "input_price_per_1m");
@@ -476,6 +478,46 @@ mod tests {
);
}
#[test]
fn sanitizes_request_metadata_preserves_tls_fingerprint() {
let metadata = sanitize_usage_request_metadata(Some(json!({
"tls_fingerprint": {
"incoming": {
"source": "forwarded_header",
"ja3": "incoming-ja3",
"ja4": "incoming-ja4"
},
"outgoing": {
"source": "aether_transport_config",
"backend": "reqwest_rustls",
"observed": false
}
},
"untrusted_tls_fingerprint": {
"ja3": "spoofed"
}
})))
.expect("metadata should remain");
assert_eq!(
metadata,
json!({
"tls_fingerprint": {
"incoming": {
"source": "forwarded_header",
"ja3": "incoming-ja3",
"ja4": "incoming-ja4"
},
"outgoing": {
"source": "aether_transport_config",
"backend": "reqwest_rustls",
"observed": false
}
}
})
);
}
#[test]
fn builds_seed_from_context_and_allowlisted_metadata_only() {
let metadata = build_usage_request_metadata_seed(