vellaveto_http_proxy/proxy/mod.rs
1// Copyright 2026 Paolo Vella
2// SPDX-License-Identifier: BUSL-1.1
3//
4// Use of this software is governed by the Business Source License
5// included in the LICENSE-BSL-1.1 file at the root of this repository.
6//
7// Change Date: Three years from the date of publication of this version.
8// Change License: MPL-2.0
9
10//! MCP Streamable HTTP reverse proxy.
11//!
12//! Implements the Streamable HTTP transport (MCP spec 2025-11-25) as a
13//! reverse proxy that intercepts tool calls, evaluates policies, and
14//! forwards allowed requests to an upstream MCP server.
15
16mod auth;
17pub mod call_chain;
18pub mod discovery;
19mod fallback;
20pub mod gateway;
21#[cfg(feature = "grpc")]
22pub mod grpc;
23mod handlers;
24mod helpers;
25mod inspection;
26pub mod origin;
27pub mod smart_fallback;
28#[cfg(test)]
29mod tests;
30pub mod trace_propagation;
31pub mod transport_health;
32mod upstream;
33pub mod websocket;
34
35pub use call_chain::PrivilegeEscalationCheck;
36pub use discovery::handle_transport_discovery;
37pub use handlers::{
38 handle_mcp_delete, handle_mcp_get, handle_mcp_post, handle_protected_resource_metadata,
39};
40pub use websocket::{handle_ws_upgrade, WebSocketConfig};
41
42use hmac::Hmac;
43use sha2::Sha256;
44use std::net::SocketAddr;
45use std::sync::Arc;
46use vellaveto_approval::ApprovalStore;
47use vellaveto_audit::AuditLogger;
48use vellaveto_config::ManifestConfig;
49use vellaveto_engine::PolicyEngine;
50use vellaveto_engine::{circuit_breaker::CircuitBreakerManager, deputy::DeputyValidator};
51use vellaveto_mcp::extension_registry::ExtensionRegistry;
52use vellaveto_mcp::inspection::InjectionScanner;
53use vellaveto_mcp::mediation::MediationConfig;
54use vellaveto_mcp::output_validation::OutputSchemaRegistry;
55use vellaveto_mcp::{
56 auth_level::AuthLevelTracker, sampling_detector::SamplingDetector,
57 schema_poisoning::SchemaLineageTracker, shadow_agent::ShadowAgentDetector,
58};
59use vellaveto_types::{Policy, SessionKeyScope, WorkloadIdentity};
60
61use crate::oauth::OAuthValidator;
62use crate::session::SessionStore;
63
64/// HMAC-SHA256 type alias for call chain signing (FIND-015).
65pub type HmacSha256 = Hmac<Sha256>;
66
67/// Query parameters for POST /mcp.
68#[derive(Debug, serde::Deserialize, Default)]
69#[serde(deny_unknown_fields)]
70pub struct McpQueryParams {
71 /// When true, include evaluation trace in the response.
72 #[serde(default)]
73 pub trace: bool,
74}
75
76/// Detached request-signature freshness policy applied before replay checks.
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78pub struct DetachedSignatureFreshnessConfig {
79 pub max_age_secs: u64,
80 pub max_future_skew_secs: u64,
81}
82
83impl Default for DetachedSignatureFreshnessConfig {
84 fn default() -> Self {
85 Self {
86 max_age_secs: 600,
87 max_future_skew_secs: 300,
88 }
89 }
90}
91
92impl From<&vellaveto_config::AcisConfig> for DetachedSignatureFreshnessConfig {
93 fn from(acis: &vellaveto_config::AcisConfig) -> Self {
94 Self {
95 max_age_secs: acis.detached_request_signature_max_age_secs,
96 max_future_skew_secs: acis.detached_request_signature_max_future_skew_secs,
97 }
98 }
99}
100
101/// Trusted detached signer metadata used at verification time.
102#[derive(Debug, Clone, PartialEq, Eq)]
103pub struct TrustedRequestSigner {
104 pub public_key: [u8; 32],
105 pub session_key_scope: SessionKeyScope,
106 pub execution_is_ephemeral: bool,
107 pub workload_identity: Option<WorkloadIdentity>,
108}
109
110/// Shared state for the HTTP proxy handlers.
111#[derive(Clone)]
112pub struct ProxyState {
113 pub engine: Arc<PolicyEngine>,
114 pub policies: Arc<Vec<Policy>>,
115 pub audit: Arc<AuditLogger>,
116 pub sessions: Arc<SessionStore>,
117 pub upstream_url: String,
118 pub http_client: reqwest::Client,
119 /// OAuth 2.1 JWT validator. When `Some`, all MCP requests require a valid Bearer token.
120 pub oauth: Option<Arc<OAuthValidator>>,
121 /// Custom injection scanner. When `Some`, uses configured patterns instead of defaults.
122 pub injection_scanner: Option<Arc<InjectionScanner>>,
123 /// When true, injection scanning is completely disabled.
124 pub injection_disabled: bool,
125 /// When true, injection matches block the response instead of just logging (H4).
126 pub injection_blocking: bool,
127 /// API key for authenticating requests. None disables auth (--allow-anonymous).
128 pub api_key: Option<Arc<String>>,
129 /// Optional approval store for RequireApproval verdicts.
130 /// When set, creates pending approvals with approval_id in error response data.
131 pub approval_store: Option<Arc<ApprovalStore>>,
132 /// Optional manifest verification config. When set, tools/list responses
133 /// are verified against a pinned manifest per session.
134 pub manifest_config: Option<ManifestConfig>,
135 /// Allowed origins for CSRF / DNS rebinding protection. If non-empty,
136 /// Origin must be in the allowlist. If empty and the proxy is bound to a
137 /// loopback address, only localhost origins are accepted. If empty and
138 /// bound to a non-loopback address, falls back to same-origin check
139 /// (Origin host must match Host header).
140 /// Requests without an Origin header are always allowed (non-browser clients).
141 pub allowed_origins: Vec<String>,
142 /// The socket address the proxy is bound to. Used for automatic localhost
143 /// origin validation when `allowed_origins` is empty.
144 pub bind_addr: SocketAddr,
145 /// When true, re-serialize parsed JSON-RPC messages before forwarding to
146 /// upstream. This closes the TOCTOU gap where the proxy evaluates a parsed
147 /// representation but forwards original bytes that could differ (e.g., due to
148 /// duplicate keys or parser-specific handling). Duplicate keys are always
149 /// rejected regardless of this setting.
150 pub canonicalize: bool,
151 /// Output schema registry for structuredContent validation (MCP 2025-06-18).
152 pub output_schema_registry: Arc<OutputSchemaRegistry>,
153 /// When true, scan tool responses for secrets (DLP response scanning).
154 pub response_dlp_enabled: bool,
155 /// When true, block responses that contain detected secrets instead of just logging.
156 /// SECURITY (R18-DLP-BLOCK): Without this, DLP is log-only and secrets still reach the client.
157 pub response_dlp_blocking: bool,
158 /// Strict audit mode (FIND-CREATIVE-003): When true, audit logging failures
159 /// cause requests to be denied instead of proceeding without an audit trail.
160 /// This enforces non-repudiation guarantees — no unaudited security decisions
161 /// can occur. Default: false (backward compatible).
162 pub audit_strict_mode: bool,
163 /// Shared mediation-stage controls for provenance and semantic containment.
164 /// HTTP handlers keep their existing request scanners and use this config
165 /// for ACIS binding, provenance, and sink/lineage enforcement.
166 pub mediation_config: MediationConfig,
167 /// Trusted detached request-signature signers keyed by `RequestSignature.key_id`.
168 /// Used to promote detached per-request signatures from metadata to verified provenance.
169 pub trusted_request_signers: Arc<std::collections::HashMap<String, TrustedRequestSigner>>,
170 /// Freshness limits for verified detached request signatures.
171 pub detached_signature_freshness: DetachedSignatureFreshnessConfig,
172 /// Known legitimate tool names for squatting detection.
173 /// Built from DEFAULT_KNOWN_TOOLS + any config overrides.
174 pub known_tools: std::collections::HashSet<String>,
175 /// Elicitation interception configuration (MCP 2025-06-18).
176 /// Controls whether `elicitation/create` requests are allowed or blocked.
177 pub elicitation_config: vellaveto_config::ElicitationConfig,
178 /// Sampling request policy configuration.
179 /// Controls whether `sampling/createMessage` requests are allowed or blocked.
180 pub sampling_config: vellaveto_config::SamplingConfig,
181 /// Tool registry for tracking tool trust scores (P2.1).
182 /// None when tool registry is disabled.
183 pub tool_registry: Option<Arc<vellaveto_mcp::tool_registry::ToolRegistry>>,
184 /// HMAC-SHA256 key for signing and verifying X-Upstream-Agents call chain entries (FIND-015).
185 /// When `Some`, Vellaveto signs its own chain entries and verifies incoming ones.
186 /// When `None`, chain signing/verification is disabled (backward compatible).
187 pub call_chain_hmac_key: Option<[u8; 32]>,
188 /// When true, the `?trace=true` query parameter is honored and evaluation
189 /// traces are included in responses. When false (the default), trace output
190 /// is silently suppressed regardless of the client query parameter.
191 ///
192 /// SECURITY: Traces expose internal policy names, patterns, and constraint
193 /// configurations. Leaving this disabled prevents information leakage to
194 /// authenticated clients.
195 pub trace_enabled: bool,
196
197 // =========================================================================
198 // Phase 3.1 Security Managers
199 // =========================================================================
200 /// Circuit breaker for cascading failure prevention (OWASP ASI08).
201 /// When a tool fails repeatedly, the circuit opens and subsequent calls are rejected.
202 pub circuit_breaker: Option<Arc<CircuitBreakerManager>>,
203
204 /// Shadow agent detector for agent impersonation detection.
205 /// Tracks known agent fingerprints and alerts on impersonation attempts.
206 pub shadow_agent: Option<Arc<ShadowAgentDetector>>,
207
208 /// Deputy validator for confused deputy attack prevention (OWASP ASI02).
209 /// Tracks delegation chains and validates action permissions.
210 pub deputy: Option<Arc<DeputyValidator>>,
211
212 /// Schema lineage tracker for schema poisoning detection (OWASP ASI05).
213 /// Tracks tool schema changes and alerts on suspicious mutations.
214 pub schema_lineage: Option<Arc<SchemaLineageTracker>>,
215
216 /// Auth level tracker for step-up authentication.
217 /// Tracks session auth levels and enforces step-up requirements.
218 pub auth_level: Option<Arc<AuthLevelTracker>>,
219
220 /// Sampling detector for sampling attack prevention.
221 /// Tracks sampling request patterns and enforces rate limits.
222 pub sampling_detector: Option<Arc<SamplingDetector>>,
223
224 // =========================================================================
225 // Runtime Limits
226 // =========================================================================
227 /// Configurable runtime limits for memory bounds, timeouts, and chain lengths.
228 /// Provides operator control over previously hardcoded security constants.
229 pub limits: vellaveto_config::LimitsConfig,
230
231 // =========================================================================
232 // WebSocket Transport (Phase 17.1 — SEP-1288)
233 // =========================================================================
234 /// WebSocket transport configuration. When `Some`, the `/mcp/ws` endpoint
235 /// is active with the specified message size, idle timeout, and rate limit.
236 /// When `None`, WebSocket requests use default configuration.
237 pub ws_config: Option<WebSocketConfig>,
238
239 // =========================================================================
240 // Protocol Extensions (Phase 17.4)
241 // =========================================================================
242 /// Extension registry for `x-` prefixed protocol extensions.
243 /// When `Some`, extension method calls are routed to registered handlers
244 /// before falling back to upstream forwarding.
245 pub extension_registry: Option<Arc<ExtensionRegistry>>,
246
247 // =========================================================================
248 // Phase 18: Transport Discovery & Negotiation
249 // =========================================================================
250 /// Transport discovery and negotiation configuration.
251 pub transport_config: vellaveto_config::TransportConfig,
252
253 /// gRPC listen port, when gRPC transport is enabled.
254 /// Used by the discovery endpoint to advertise the gRPC endpoint.
255 pub grpc_port: Option<u16>,
256
257 // =========================================================================
258 // Phase 20: MCP Gateway Mode
259 // =========================================================================
260 /// Multi-backend gateway router. When `Some`, tool calls are routed to
261 /// different upstream MCP servers based on tool name prefix matching.
262 /// When `None`, all requests use `upstream_url` (single-server mode).
263 pub gateway: Option<Arc<gateway::GatewayRouter>>,
264
265 // =========================================================================
266 // Phase 21: Advanced Authorization (ABAC)
267 // =========================================================================
268 /// ABAC policy engine for Cedar-style permit/forbid evaluation.
269 /// When `Some`, refines Allow verdicts from the PolicyEngine.
270 /// When `None`, behavior is identical to pre-Phase 21.
271 pub abac_engine: Option<Arc<vellaveto_engine::abac::AbacEngine>>,
272 /// Least-agency tracker for permission usage monitoring.
273 /// When `Some`, records which permissions each agent actually uses.
274 pub least_agency: Option<Arc<vellaveto_engine::least_agency::LeastAgencyTracker>>,
275 /// Continuous authorization config for risk-based deny.
276 pub continuous_auth_config: Option<vellaveto_config::abac::ContinuousAuthConfig>,
277
278 // =========================================================================
279 // Phase 29: Cross-Transport Smart Fallback
280 // =========================================================================
281 /// Per-transport circuit breaker tracker. When `Some` and
282 /// `transport_config.cross_transport_fallback` is true, failed transports
283 /// trigger automatic fallback to the next transport in priority order.
284 pub transport_health: Option<Arc<transport_health::TransportHealthTracker>>,
285
286 // =========================================================================
287 // Phase 30: MCP 2025-11-25 Streamable HTTP
288 // =========================================================================
289 /// Streamable HTTP configuration for SSE resumability, strict tool name
290 /// validation, and retry directives.
291 pub streamable_http: vellaveto_config::StreamableHttpConfig,
292
293 // =========================================================================
294 // Phase 39: Agent Identity Federation
295 // =========================================================================
296 /// Federation resolver for cross-organization agent identity validation.
297 /// When `Some`, incoming JWTs are checked against federation trust anchors
298 /// before falling back to the local OAuth validator.
299 pub federation: Option<Arc<crate::federation::FederationResolver>>,
300
301 // =========================================================================
302 // Phase 34: Tool Discovery Service
303 // =========================================================================
304 /// Tool discovery engine for intent-based tool search.
305 /// When `Some`, the proxy intercepts `vv_discover` tool calls and returns
306 /// discovered tool schemas without forwarding to upstream.
307 #[cfg(feature = "discovery")]
308 pub discovery_engine: Option<std::sync::Arc<vellaveto_mcp::discovery::DiscoveryEngine>>,
309
310 // =========================================================================
311 // Phase 35.3: Model Projector
312 // =========================================================================
313 /// Model projector registry for cross-model tool schema translation.
314 /// When `Some`, tool schemas can be projected to model-specific formats.
315 #[cfg(feature = "projector")]
316 pub projector_registry: Option<std::sync::Arc<vellaveto_mcp::projector::ProjectorRegistry>>,
317}
318
319/// Per-request trust signal for forwarded-header handling.
320///
321/// FIND-R56-HTTP-002: `from_trusted_proxy` is `pub(crate)` to prevent external
322/// code from constructing a `TrustedProxyContext { from_trusted_proxy: true }`
323/// and injecting it as a spoofed trust signal. Use `is_from_trusted_proxy()`
324/// for read access from outside the crate.
325#[derive(Clone, Copy, Debug)]
326pub struct TrustedProxyContext {
327 pub(crate) from_trusted_proxy: bool,
328}
329
330impl TrustedProxyContext {
331 /// Create a new `TrustedProxyContext`.
332 pub fn new(from_trusted_proxy: bool) -> Self {
333 Self { from_trusted_proxy }
334 }
335
336 /// Returns whether the request came from a trusted reverse proxy.
337 pub fn is_from_trusted_proxy(&self) -> bool {
338 self.from_trusted_proxy
339 }
340}
341
342/// MCP Session ID header name.
343const MCP_SESSION_ID: &str = "mcp-session-id";
344
345/// MCP protocol version header (MCP 2025-06-18 spec requirement).
346const MCP_PROTOCOL_VERSION_HEADER: &str = "mcp-protocol-version";
347
348/// The protocol version value this proxy speaks.
349/// FIND-R56-HTTP-006: Renamed from `MCP_PROTOCOL_VERSION` to avoid confusion
350/// with `MCP_PROTOCOL_VERSION_HEADER` (the header name).
351const MCP_PROTOCOL_VERSION_VALUE: &str = "2025-11-25";
352
353/// Supported MCP protocol versions for incoming requests.
354/// The proxy accepts these versions for backwards compatibility.
355const SUPPORTED_PROTOCOL_VERSIONS: &[&str] = &["2025-11-25", "2025-06-18", "2025-03-26"];
356
357/// Header for client transport preference negotiation (MCP June 2026).
358/// Clients may send a comma-separated list of preferred transports.
359/// Used in request handling when transport-preference-aware routing is active.
360const MCP_TRANSPORT_PREFERENCE_HEADER: &str = "mcp-transport-preference";
361
362/// OWASP ASI08: Header for tracking upstream agents in multi-hop MCP scenarios.
363/// Contains a JSON-encoded array of CallChainEntry objects from previous hops.
364/// This header is added by Vellaveto when forwarding requests downstream
365/// and read when receiving requests from upstream.
366pub const X_UPSTREAM_AGENTS: &str = "x-upstream-agents";
367
368/// SECURITY (FIND-R41-001): Allowlist of headers forwarded to upstream.
369/// Only these headers are forwarded to prevent leaking internal/sensitive
370/// headers (e.g., authorization, cookies) to upstream backends.
371/// Shared by `fallback.rs` and `smart_fallback.rs`.
372pub(crate) const FORWARDED_HEADERS: &[&str] = &[
373 "content-type",
374 "accept",
375 "user-agent",
376 "traceparent",
377 "tracestate",
378 "x-request-id",
379];
380
381/// Maximum response body size from upstream (16 MB).
382/// SECURITY (FIND-R41-004, FIND-R42-020): Prevents unbounded memory
383/// allocation from malicious upstream responses.
384/// Shared by `fallback.rs` and `smart_fallback.rs`.
385pub(crate) const MAX_RESPONSE_BODY_BYTES: usize = 16 * 1024 * 1024;
386
387/// SECURITY (R240-PROXY-1/2): Validate that an upstream URL uses HTTPS for non-local hosts.
388/// Only allows plaintext HTTP for localhost/127.0.0.1/[::1] (local development).
389/// Returns `Ok(())` if the URL is safe to use, `Err(reason)` if it should be rejected.
390pub(crate) fn validate_upstream_url_scheme(url: &str) -> Result<(), String> {
391 if let Some(scheme_end) = url.find("://") {
392 let scheme = &url[..scheme_end].to_lowercase();
393 if scheme == "http" || scheme == "ws" {
394 let after_scheme = &url[scheme_end + 3..];
395 let host_port = after_scheme.split('/').next().unwrap_or("");
396 // SECURITY (R240-IMP-008): Handle bracketed IPv6 addresses like [::1]:8080.
397 // split(':') on "[::1]:8080" gives "[" which is wrong.
398 let host_no_port = if host_port.starts_with('[') {
399 // IPv6 bracketed: extract up to and including ']'
400 host_port
401 .split(']')
402 .next()
403 .map(|s| {
404 let mut full = s.to_string();
405 full.push(']');
406 full
407 })
408 .unwrap_or_default()
409 } else {
410 host_port.split(':').next().unwrap_or("").to_string()
411 };
412 let is_local = host_no_port == "localhost"
413 || host_no_port == "127.0.0.1"
414 || host_no_port == "[::1]";
415 if !is_local {
416 return Err("HTTPS required for non-local upstream".to_string());
417 }
418 }
419 }
420 Ok(())
421}
422
423/// OWASP ASI07: Header for cryptographically attested agent identity.
424/// Contains a signed JWT with claims identifying the agent (issuer, subject, custom claims).
425/// Provides stronger identity guarantees than the simple agent_id string derived from OAuth.
426const X_AGENT_IDENTITY: &str = "x-agent-identity";
427
428/// Header carrying authenticated workload claims for transport provenance.
429/// The value is a base64url-encoded JSON object with allowlisted workload fields.
430const X_WORKLOAD_CLAIMS: &str = "x-workload-claims";
431
432/// Header carrying detached per-request signature metadata for provenance.
433/// The value is a base64url-encoded `RequestSignature` JSON object.
434const X_REQUEST_SIGNATURE: &str = "x-request-signature";