diff --git a/src/proxy.rs b/src/proxy.rs index b780769858..cb6461decc 100644 --- a/src/proxy.rs +++ b/src/proxy.rs @@ -558,6 +558,7 @@ pub struct TraceParent { pub const BAGGAGE_HEADER: &str = "baggage"; pub const TRACEPARENT_HEADER: &str = "traceparent"; +pub const X_ORIGIN_SOURCE_HEADER: &str = "x-istio-origin-network"; impl TraceParent { pub fn header(&self) -> hyper::header::HeaderValue { diff --git a/src/proxy/outbound.rs b/src/proxy/outbound.rs index c1f045f13a..df3ef74cc2 100644 --- a/src/proxy/outbound.rs +++ b/src/proxy/outbound.rs @@ -25,10 +25,12 @@ use tokio::sync::watch; use tracing::{Instrument, debug, error, info, info_span, trace_span}; use crate::identity::Identity; +use crate::strng::Strng; use crate::proxy::metrics::Reporter; use crate::proxy::{ - BAGGAGE_HEADER, Error, HboneAddress, ProxyInputs, TRACEPARENT_HEADER, TraceParent, util, + BAGGAGE_HEADER, Error, HboneAddress, ProxyInputs, TRACEPARENT_HEADER, TraceParent, + X_ORIGIN_SOURCE_HEADER, util, }; use crate::proxy::{ConnectionOpen, ConnectionResult, DerivedWorkload, metrics}; @@ -266,7 +268,8 @@ impl OutboundConnection { let mut sender = super::h2::client::spawn_connection(self.pi.cfg.clone(), tls_stream, drain_rx, wl_key) .await?; - let http_request = self.create_hbone_request(remote_addr, req); + let origin_network = &self.pi.cfg.network; + let http_request = self.create_hbone_request(remote_addr, req, Some(origin_network)); let inner_upgraded = sender.send_request(http_request).await?; // Proxy @@ -294,11 +297,12 @@ impl OutboundConnection { } fn create_hbone_request( - &mut self, + &self, remote_addr: SocketAddr, req: &Request, + origin_network: Option<&Strng>, ) -> http::Request<()> { - http::Request::builder() + let mut builder = http::Request::builder() .uri( req.hbone_target_destination .as_ref() @@ -312,7 +316,14 @@ impl OutboundConnection { FORWARDED, build_forwarded(remote_addr, &req.intended_destination_service), ) - .header(TRACEPARENT_HEADER, self.id.header()) + .header(TRACEPARENT_HEADER, self.id.header()); + + // Add x-istio-origin-network header for inner CONNECT requests in double HBONE + if let Some(network) = origin_network { + builder = builder.header(X_ORIGIN_SOURCE_HEADER, network.as_str()); + } + + builder .body(()) .expect("builder with known status code should not fail") } @@ -322,7 +333,11 @@ impl OutboundConnection { remote_addr: SocketAddr, req: &Request, ) -> Result { - let request = self.create_hbone_request(remote_addr, req); + // This is the single cluster/single-HBONE codepath (and also the outer tunnel + // for double HBONE). We don't need the x-istio-origin-network header here because: + // - For single HBONE: both source and destination are in the same network + // - For double HBONE outer: the gateway doesn't need origin network info + let request = self.create_hbone_request(remote_addr, req, None); let pool_key = Box::new(WorkloadKey { src_id: req.source.identity(), // Clone here shouldn't be needed ideally, we could just take ownership of Request. @@ -1876,6 +1891,110 @@ mod tests { ); } + #[tokio::test] + async fn test_x_origin_source_header() { + initialize_telemetry(); + + // Create a test config with a specific network + let cfg = Arc::new(Config { + network: "test-network".into(), + local_node: Some("local-node".to_string()), + ..crate::config::parse_config().unwrap() + }); + + // Create a source workload and add it to state + let source = XdsWorkload { + uid: "cluster1//v1/Pod/ns/source-workload".to_string(), + name: "source-workload".to_string(), + namespace: "ns".to_string(), + addresses: vec![Bytes::copy_from_slice(&[127, 0, 0, 1])], + node: "local-node".to_string(), + ..Default::default() + }; + + let state = new_proxy_state(&[source], &[], &[]); + let sock_fact = Arc::new(crate::proxy::DefaultSocketFactory::default()); + + let wi = WorkloadInfo { + name: "source-workload".to_string(), + namespace: "ns".to_string(), + service_account: "default".to_string(), + }; + let local_workload_information = Arc::new(LocalWorkloadInformation::new( + Arc::new(wi.clone()), + state.clone(), + identity::mock::new_secret_manager(Duration::from_secs(10)), + )); + + let outbound = OutboundConnection { + pi: Arc::new(ProxyInputs { + state: state.clone(), + cfg: cfg.clone(), + metrics: test_proxy_metrics(), + socket_factory: sock_fact.clone(), + local_workload_information: local_workload_information.clone(), + connection_manager: ConnectionManager::default(), + resolver: None, + disable_inbound_freebind: false, + crl_manager: None, + }), + id: TraceParent::new(), + pool: WorkloadHBONEPool::new( + cfg.clone(), + sock_fact, + local_workload_information.clone(), + ), + hbone_port: cfg.inbound_addr.port(), + }; + + // Get the source workload from state + let source_workload = outbound + .pi + .local_workload_information + .get_workload() + .await + .unwrap(); + + // Create a minimal test request with required fields + let req = Request { + protocol: OutboundProtocol::HBONE, + source: source_workload, + hbone_target_destination: Some(HboneAddress::SocketAddr( + "10.0.0.1:8080".parse().unwrap(), + )), + actual_destination_workload: None, + intended_destination_service: None, + actual_destination: "10.0.0.1:8080".parse().unwrap(), + upstream_sans: vec![], + final_sans: vec![], + }; + + let remote_addr = "127.0.0.1:12345".parse().unwrap(); + + // Test the single HBONE case - header should NOT be added when origin_network is None + let http_request_no_header = outbound.create_hbone_request(remote_addr, &req, None); + assert!( + http_request_no_header + .headers() + .get(X_ORIGIN_SOURCE_HEADER) + .is_none(), + "x-istio-origin-network header should not be present when origin_network is None (single HBONE)" + ); + + // Test the double HBONE inner request case - header should be added when network is specified + let network = crate::strng::Strng::from("test-network"); + let http_request_with_header = + outbound.create_hbone_request(remote_addr, &req, Some(&network)); + assert_eq!( + http_request_with_header + .headers() + .get(X_ORIGIN_SOURCE_HEADER) + .unwrap(), + "test-network", + "x-istio-origin-network header should contain the network name for double HBONE inner request" + ); + } + #[derive(PartialEq, Debug)] struct ExpectedRequest<'a> { protocol: OutboundProtocol,