1use std::net::{IpAddr, SocketAddr};
7use std::sync::{Arc, LazyLock};
8
9use agent_data_plane_config::{ConfigValue, SalukiConfiguration};
10use async_trait::async_trait;
11use axum::extract::{ConnectInfo, Request as AxumRequest, State};
12use axum::response::IntoResponse;
13use axum::Router;
14use http::{HeaderName, HeaderValue};
15use metrics::Counter;
16use saluki_api::APIHandler;
17use saluki_core::accounting::{MemoryBounds, MemoryBoundsBuilder};
18use saluki_core::components::relays::{Relay, RelayBuilder, RelayContext};
19use saluki_core::components::BuildContext;
20use saluki_core::data_model::payload::PayloadType;
21use saluki_core::runtime;
22use saluki_core::topology::OutputDefinition;
23use saluki_env::workload::{CaptureEntityResolver, EntityId};
24use saluki_error::{generic_error, ErrorContext as _, GenericError};
25use saluki_io::net::dns::resolve_host;
26use saluki_io::net::server::http::HttpServer;
27use saluki_io::net::{ConnectionAddress, ListenAddress};
28use saluki_metrics::static_metrics;
29use tokio::{pin, select};
30use tracing::debug;
31
32mod proxy;
33use self::proxy::{Destination, TraceAgentProxy};
34
35const CONTAINER_ID_HEADER: HeaderName = HeaderName::from_static("datadog-container-id");
37
38const ENTITY_ID_HEADER: HeaderName = HeaderName::from_static("datadog-entity-id");
41
42#[derive(Clone)]
50pub struct ApmRelayConfiguration {
51 receiver_socket: String,
55
56 proxy_destination: String,
59
60 capture_entity_resolver: Option<Arc<dyn CaptureEntityResolver + Send + Sync>>,
66
67 bind_host: Option<String>,
72
73 non_local_traffic: bool,
77
78 receiver_port: u16,
82}
83
84#[static_metrics(prefix = component, labels(component_id, component_type))]
85#[derive(Clone)]
86struct Metrics {
87 requests_relayed_total: Counter,
88 requests_failed_total: Counter,
89}
90
91impl ApmRelayConfiguration {
92 pub fn from_configuration(config: &SalukiConfiguration, containerized: bool) -> Self {
97 let (bind_host, non_local_traffic) = resolve_listen_host(
98 &config.shared.bind_host,
99 config.domains.apm.non_local_traffic,
100 containerized,
101 );
102
103 Self {
105 receiver_socket: config.domains.apm.receiver_socket.clone().unwrap_or_default(),
106 proxy_destination: config.domains.apm.proxy_destination.to_owned(),
107 capture_entity_resolver: None,
108 bind_host,
109 non_local_traffic,
110 receiver_port: config.domains.apm.receiver_port,
111 }
112 }
113
114 pub fn with_capture_entity_resolver(mut self, resolver: Arc<dyn CaptureEntityResolver + Send + Sync>) -> Self {
116 self.capture_entity_resolver = Some(resolver);
117 self
118 }
119
120 async fn tcp_listen_address(&self) -> Result<Option<ListenAddress>, GenericError> {
121 if self.receiver_port == 0 {
122 return Ok(None);
123 };
124
125 let listen_address = if self.non_local_traffic {
126 [0, 0, 0, 0].into()
127 } else if let Some(bind_host) = &self.bind_host {
128 resolve_host(bind_host)
129 .await
130 .error_context("Could not resolve bind_host for the APM relay.")?
131 } else {
132 [127, 0, 0, 1].into()
133 };
134
135 Ok(Some(ListenAddress::Tcp(SocketAddr::new(
136 listen_address,
137 self.receiver_port,
138 ))))
139 }
140
141 fn uds_listen_address(&self) -> Result<Option<ListenAddress>, GenericError> {
142 if self.receiver_socket.is_empty() {
143 return Ok(None);
144 }
145
146 ListenAddress::try_from(format!("unix://{}", self.receiver_socket))
147 .map(Some)
148 .map_err(|e| {
149 generic_error!(
150 "Invalid APM relay Unix domain socket path `{}`: {e}",
151 self.receiver_socket
152 )
153 })
154 }
155}
156
157fn resolve_listen_host(
163 bind_host: &ConfigValue<Option<String>>, non_local_traffic: ConfigValue<bool>, containerized: bool,
164) -> (Option<String>, bool) {
165 if bind_host.is_explicit() || non_local_traffic.is_explicit() {
166 let host = if bind_host.is_explicit() {
167 bind_host.value.clone()
168 } else {
169 None
170 };
171 (host, non_local_traffic.value)
172 } else {
173 (None, containerized)
174 }
175}
176
177async fn listener_reached_by<'a>(
182 destination: &Destination, tcp_endpoint: Option<&'a ListenAddress>, uds_endpoint: Option<&'a ListenAddress>,
183) -> Option<&'a ListenAddress> {
184 #[cfg(not(unix))]
186 let _ = uds_endpoint;
187
188 match destination {
189 Destination::Tcp { host, port } => {
190 let endpoint = tcp_endpoint?;
191 let ListenAddress::Tcp(listener) = endpoint else {
192 return None;
193 };
194 let mut destinations = tokio::net::lookup_host((host.as_str(), *port)).await.ok()?;
196 destinations
197 .any(|destination| tcp_reaches(destination, *listener))
198 .then_some(endpoint)
199 }
200 #[cfg(unix)]
201 Destination::Unix(path) => {
202 uds_endpoint.filter(|endpoint| matches!(endpoint, ListenAddress::Unix(listener) if listener == path))
203 }
204 }
205}
206
207fn tcp_reaches(destination: SocketAddr, listener: SocketAddr) -> bool {
212 let is_local = |ip: IpAddr| ip.is_loopback() || ip.is_unspecified();
213 let (destination_ip, listener_ip) = (destination.ip(), listener.ip());
214
215 destination.port() == listener.port()
216 && (destination_ip == listener_ip
217 || ((destination_ip.is_unspecified() || listener_ip.is_unspecified())
218 && destination_ip.is_ipv4() == listener_ip.is_ipv4()
219 && is_local(destination_ip)
220 && is_local(listener_ip)))
221}
222
223impl MemoryBounds for ApmRelayConfiguration {
224 fn specify_bounds(&self, builder: &mut MemoryBoundsBuilder) {
225 builder
226 .minimum()
227 .with_single_value::<ApmRelay>("component struct")
228 .with_single_value::<RelayState>("component struct");
229 }
230}
231
232#[async_trait]
233impl RelayBuilder for ApmRelayConfiguration {
234 fn outputs(&self) -> &[OutputDefinition<PayloadType>] {
235 static OUTPUTS: LazyLock<Vec<OutputDefinition<PayloadType>>> =
236 LazyLock::new(|| vec![OutputDefinition::named_output("traces", PayloadType::Http)]);
237 &OUTPUTS
238 }
239
240 async fn build(&self, context: BuildContext) -> Result<Box<dyn Relay + Send>, GenericError> {
241 let tcp_endpoint = self.tcp_listen_address().await?;
242 let uds_endpoint = self.uds_listen_address()?;
243
244 if tcp_endpoint.is_none() && uds_endpoint.is_none() {
245 return Err(generic_error!(
246 "APM relay requires at least one of `apm_config.receiver_port` or `apm_config.receiver_socket` to be configured."
247 ));
248 }
249
250 let proxy = TraceAgentProxy::from_destination(&self.proxy_destination)?;
251 if let Some(listener) =
252 listener_reached_by(proxy.destination(), tcp_endpoint.as_ref(), uds_endpoint.as_ref()).await
253 {
254 return Err(generic_error!(
255 "APM proxy destination `{}` is the relay's own listener `{listener}`, so every request would be proxied back to the relay. Set `data_plane.experimental.apm.proxy_destination` to the trace-agent's address.",
256 self.proxy_destination
257 ));
258 }
259
260 Ok(Box::new(ApmRelay {
261 tcp_endpoint,
262 uds_endpoint,
263 proxy,
264 capture_entity_resolver: self.capture_entity_resolver.clone(),
265 metrics: Metrics::new(context.component_id(), context.component_type().as_category_str()),
266 }))
267 }
268}
269
270pub struct ApmRelay {
274 tcp_endpoint: Option<ListenAddress>,
275 uds_endpoint: Option<ListenAddress>,
276 proxy: TraceAgentProxy,
277 capture_entity_resolver: Option<Arc<dyn CaptureEntityResolver + Send + Sync>>,
278 metrics: Metrics,
279}
280
281#[async_trait]
282impl Relay for ApmRelay {
283 async fn run(self: Box<Self>, mut context: RelayContext) -> Result<(), GenericError> {
284 let Self {
285 tcp_endpoint,
286 uds_endpoint,
287 proxy,
288 capture_entity_resolver,
289 metrics,
290 } = *self;
291
292 let global_shutdown = context.take_shutdown_handle();
293 pin!(global_shutdown);
294
295 let mut health = context.take_health_handle();
296 let worker_pool = context.topology_context().global_thread_pool().clone();
297 let api_handler = ApmRelayAPIHandler::new(RelayState {
298 metrics,
299 proxy,
300 capture_entity_resolver,
301 });
302 let router = api_handler
303 .generate_routes()
304 .with_state(api_handler.generate_initial_state());
305
306 if let Some(endpoint) = tcp_endpoint {
309 debug!(%endpoint, "Binding APM relay TCP listener.");
310 let server = HttpServer::from_listen_address(endpoint)
311 .with_routes(router.clone())
312 .with_worker_pool(worker_pool.clone());
313 runtime::nested_supervisor(server.into_supervisor())
314 .needed()
315 .stops_with_owner()
316 .spawn();
317 }
318 if let Some(endpoint) = uds_endpoint {
319 debug!(%endpoint, "Binding APM relay Unix domain socket listener.");
320 let server = HttpServer::from_listen_address(endpoint)
321 .with_routes(router.clone())
322 .with_worker_pool(worker_pool);
323 runtime::nested_supervisor(server.into_supervisor())
324 .needed()
325 .stops_with_owner()
326 .spawn();
327 }
328
329 health.mark_ready();
330 debug!("APM relay started.");
331
332 loop {
333 select! {
334 _ = &mut global_shutdown => {
335 debug!("Received shutdown signal.");
336 break
337 },
338 _ = health.live() => continue,
339 }
340 }
341
342 debug!("APM relay stopped.");
343
344 Ok(())
345 }
346}
347
348#[derive(Clone)]
350struct RelayState {
351 proxy: TraceAgentProxy,
352 capture_entity_resolver: Option<Arc<dyn CaptureEntityResolver + Send + Sync>>,
353 metrics: Metrics,
354}
355
356struct ApmRelayAPIHandler {
357 state: RelayState,
358}
359
360impl ApmRelayAPIHandler {
361 fn new(state: RelayState) -> Self {
362 Self { state }
363 }
364
365 async fn proxy_request(
366 State(RelayState {
367 proxy,
368 capture_entity_resolver,
369 metrics,
370 }): State<RelayState>,
371 mut request: AxumRequest,
372 ) -> impl IntoResponse {
373 if let Some(resolver) = capture_entity_resolver.as_deref() {
374 add_peer_container_id(&mut request, resolver);
375 }
376 let result = proxy.forward(request).await;
377 match &result {
378 Ok(_) => metrics.requests_relayed_total().increment(1),
379 Err(_) => metrics.requests_failed_total().increment(1),
380 }
381 result
382 }
383}
384
385fn add_peer_container_id(request: &mut AxumRequest, resolver: &(dyn CaptureEntityResolver + Send + Sync)) {
391 let headers = request.headers();
392 if headers.contains_key(CONTAINER_ID_HEADER) || headers.contains_key(ENTITY_ID_HEADER) {
393 return;
394 }
395
396 let Some(process_id) = request
397 .extensions()
398 .get::<ConnectInfo<ConnectionAddress>>()
399 .and_then(|ConnectInfo(addr)| addr.process_credentials())
400 .and_then(|creds| u32::try_from(creds.pid).ok())
401 else {
402 return;
403 };
404
405 match resolver.resolve_container_entity_for_live_pid(process_id) {
406 Some(EntityId::Container(container_id)) => match HeaderValue::from_str(&container_id) {
407 Ok(value) => {
408 request.headers_mut().insert(CONTAINER_ID_HEADER, value);
409 }
410 Err(_) => debug!(process_id, %container_id, "Resolved container ID is not a valid header value."),
411 },
412 Some(entity_id) => debug!(process_id, %entity_id, "Tracer process resolved to a non-container entity."),
413 None => debug!(process_id, "Could not resolve the container of the tracer process."),
414 }
415}
416
417impl APIHandler for ApmRelayAPIHandler {
418 type State = RelayState;
419
420 fn generate_initial_state(&self) -> Self::State {
421 self.state.clone()
422 }
423
424 fn generate_routes(&self) -> Router<Self::State> {
426 Router::new().fallback(Self::proxy_request)
427 }
428}
429
430#[cfg(test)]
431mod tests {
432 use std::net::SocketAddr;
433 use std::time::Duration;
434
435 use http::{Request, StatusCode};
436 use saluki_core::components::test_util::TestComponentDriver;
437 use saluki_core::runtime::state::{DataspaceUpdate, IdentifierFilter};
438 use saluki_core::runtime::test_util::TestSupervisor;
439 use saluki_io::net::{BoundListenAddress, ProcessCredentials};
440 #[cfg(unix)]
441 use tempfile::tempdir;
442 use tokio::io::{AsyncReadExt, AsyncWriteExt};
443 use tokio::net::TcpStream;
444 #[cfg(unix)]
445 use tokio::net::UnixStream;
446 use tower::ServiceExt;
447
448 use super::*;
449
450 fn unreachable_proxy() -> TraceAgentProxy {
453 TraceAgentProxy::from_destination("http://127.0.0.1:1").expect("destination should parse")
454 }
455
456 fn build_router(state: RelayState) -> Router {
457 let api_handler = ApmRelayAPIHandler::new(state);
458 api_handler
459 .generate_routes()
460 .with_state(api_handler.generate_initial_state())
461 }
462
463 fn test_router() -> Router {
464 build_router(RelayState {
465 proxy: unreachable_proxy(),
466 capture_entity_resolver: None,
467 metrics: Metrics::new("dd_traces_relay", "relays"),
468 })
469 }
470
471 struct FixedContainerResolver;
473
474 impl CaptureEntityResolver for FixedContainerResolver {
475 fn resolve_container_entity_for_live_pid(&self, _process_id: u32) -> Option<EntityId> {
476 Some(EntityId::Container("tracer-container".into()))
477 }
478 }
479
480 fn request_from(peer: ConnectionAddress, headers: &[(&str, &str)]) -> AxumRequest {
482 let mut builder = Request::builder().method("POST").uri("/v0.4/traces");
483 for (name, value) in headers {
484 builder = builder.header(*name, *value);
485 }
486 let mut request = builder.body(axum::body::Body::empty()).unwrap();
487 request.extensions_mut().insert(ConnectInfo(peer));
488 request
489 }
490
491 fn unix_peer() -> ConnectionAddress {
492 ProcessCredentials {
493 pid: 1234,
494 uid: 0,
495 gid: 0,
496 }
497 .into()
498 }
499
500 #[test]
501 fn sets_the_container_id_of_a_unix_socket_peer() {
502 let mut request = request_from(unix_peer(), &[]);
503
504 add_peer_container_id(&mut request, &FixedContainerResolver);
505
506 assert_eq!(request.headers()[CONTAINER_ID_HEADER], "tracer-container");
507 }
508
509 #[test]
510 fn keeps_the_container_headers_a_tracer_sent() {
511 let mut request = request_from(unix_peer(), &[("Datadog-Container-ID", "from-tracer")]);
512 add_peer_container_id(&mut request, &FixedContainerResolver);
513 assert_eq!(request.headers()[CONTAINER_ID_HEADER], "from-tracer");
514
515 let mut request = request_from(unix_peer(), &[("Datadog-Entity-ID", "in-42")]);
516 add_peer_container_id(&mut request, &FixedContainerResolver);
517 assert!(!request.headers().contains_key(CONTAINER_ID_HEADER));
518 }
519
520 #[test]
521 fn leaves_tcp_requests_alone() {
522 let mut request = request_from(ConnectionAddress::from(SocketAddr::from(([127, 0, 0, 1], 4242))), &[]);
523
524 add_peer_container_id(&mut request, &FixedContainerResolver);
525
526 assert!(!request.headers().contains_key(CONTAINER_ID_HEADER));
527 }
528
529 #[tokio::test]
530 async fn counts_a_request_the_trace_agent_never_received_as_failed() {
531 let recorder = saluki_metrics::test::TestRecorder::default();
532 let _recorder_guard = metrics::set_default_local_recorder(&recorder);
533 let router = test_router();
534
535 let response = router
536 .oneshot(
537 Request::builder()
538 .uri("/v0.4/traces")
539 .body(axum::body::Body::empty())
540 .unwrap(),
541 )
542 .await
543 .unwrap();
544 assert_eq!(response.status(), StatusCode::BAD_GATEWAY);
545
546 let metric_key = |name| {
547 metrics::Key::from_parts(
548 name,
549 vec![
550 metrics::Label::new("component_id", "dd_traces_relay"),
551 metrics::Label::new("component_type", "relays"),
552 ],
553 )
554 };
555 assert_eq!(recorder.counter(metric_key("component_requests_failed_total")), Some(1));
556 assert_eq!(
557 recorder.counter(metric_key("component_requests_relayed_total")),
558 Some(0)
559 );
560 }
561
562 #[tokio::test]
563 async fn proxies_requests_to_the_trace_agent() {
564 let router = test_router();
565
566 let response = router
567 .oneshot(
568 Request::builder()
569 .method("POST")
570 .uri("/not-a-real-path")
571 .body(axum::body::Body::empty())
572 .unwrap(),
573 )
574 .await
575 .unwrap();
576
577 assert_eq!(response.status(), StatusCode::BAD_GATEWAY);
578 }
579
580 #[tokio::test]
581 async fn binds_and_serves_over_tcp() {
582 const BOUND_ADDRESS_ID: &str = "apm-relay-tcp-test-bound-address";
583
584 let supervisor = TestSupervisor::start("apm-relay-tcp-test").await;
585
586 let tcp_endpoint = ListenAddress::try_from("tcp://127.0.0.1:0").expect("TCP APM endpoint should parse");
588
589 let router = test_router();
590 let server = HttpServer::from_listen_address(tcp_endpoint)
591 .with_routes(router)
592 .with_server_id(BOUND_ADDRESS_ID)
593 .with_worker_pool(tokio::runtime::Handle::current());
594
595 let mut bound_address_subscription = supervisor
596 .dataspace()
597 .subscribe::<BoundListenAddress>(IdentifierFilter::exact(format!("http-server-{BOUND_ADDRESS_ID}")));
598
599 supervisor
600 .scope(async {
601 runtime::nested_supervisor(server.into_supervisor()).spawn();
602 })
603 .await;
604
605 let bound_addr = match tokio::time::timeout(Duration::from_secs(5), bound_address_subscription.recv()).await {
606 Ok(Some(DataspaceUpdate::Asserted(_, BoundListenAddress::Tcp(address)))) => address,
607 update => panic!("expected a bound address assertion for `{BOUND_ADDRESS_ID}`, got {update:?}"),
608 };
609
610 let mut stream = tokio::time::timeout(Duration::from_secs(5), TcpStream::connect(bound_addr))
611 .await
612 .expect("should connect to TCP listener within the timeout")
613 .expect("should connect to TCP listener");
614
615 let body = b"proxied request over tcp";
616 let request = format!(
617 "POST /info HTTP/1.1\r\nHost: localhost\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
618 body.len()
619 );
620 stream
621 .write_all(request.as_bytes())
622 .await
623 .expect("should write request head");
624 stream.write_all(body).await.expect("should write request body");
625
626 let mut response = Vec::new();
627 tokio::time::timeout(Duration::from_secs(5), stream.read_to_end(&mut response))
628 .await
629 .expect("server should respond over TCP")
630 .expect("should read response");
631
632 assert!(
633 response.starts_with(b"HTTP/1.1 502"),
634 "expected a 502 Bad Gateway response, got: {}",
635 String::from_utf8_lossy(&response)
636 );
637 }
638
639 #[tokio::test]
640 async fn answers_a_request_proxied_back_to_itself_with_loop_detected() {
641 const BOUND_ADDRESS_ID: &str = "apm-relay-loop-test-bound-address";
642
643 let supervisor = TestSupervisor::start("apm-relay-loop-test").await;
644
645 let port = std::net::TcpListener::bind("127.0.0.1:0")
648 .and_then(|listener| listener.local_addr())
649 .expect("should find a free port")
650 .port();
651 let tcp_endpoint =
652 ListenAddress::try_from(format!("tcp://127.0.0.1:{port}")).expect("TCP APM endpoint should parse");
653 let router = build_router(RelayState {
654 proxy: TraceAgentProxy::from_destination(&format!("http://127.0.0.1:{port}"))
655 .expect("destination should parse"),
656 capture_entity_resolver: None,
657 metrics: Metrics::new("dd_traces_relay", "relays"),
658 });
659 let server = HttpServer::from_listen_address(tcp_endpoint)
660 .with_routes(router)
661 .with_server_id(BOUND_ADDRESS_ID)
662 .with_worker_pool(tokio::runtime::Handle::current());
663
664 let mut bound_address_subscription = supervisor
665 .dataspace()
666 .subscribe::<BoundListenAddress>(IdentifierFilter::exact(format!("http-server-{BOUND_ADDRESS_ID}")));
667
668 supervisor
669 .scope(async {
670 runtime::nested_supervisor(server.into_supervisor()).spawn();
671 })
672 .await;
673
674 let bound_addr = match tokio::time::timeout(Duration::from_secs(5), bound_address_subscription.recv()).await {
675 Ok(Some(DataspaceUpdate::Asserted(_, BoundListenAddress::Tcp(address)))) => address,
676 update => panic!("expected a bound address assertion for `{BOUND_ADDRESS_ID}`, got {update:?}"),
677 };
678
679 let mut stream = TcpStream::connect(bound_addr)
680 .await
681 .expect("should connect to TCP listener");
682 stream
683 .write_all(b"GET /info HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n")
684 .await
685 .expect("should write request");
686
687 let mut response = Vec::new();
688 tokio::time::timeout(Duration::from_secs(5), stream.read_to_end(&mut response))
689 .await
690 .expect("the relay should answer rather than proxy the request to itself forever")
691 .expect("should read response");
692
693 assert!(
694 response.starts_with(b"HTTP/1.1 508"),
695 "expected a 508 Loop Detected response, got: {}",
696 String::from_utf8_lossy(&response)
697 );
698 }
699
700 #[cfg(unix)]
701 #[tokio::test]
702 async fn binds_and_serves_over_unix_domain_socket() {
703 const BOUND_ADDRESS_ID: &str = "apm-relay-uds-test-bound-address";
704
705 let supervisor = TestSupervisor::start("apm-relay-uds-test").await;
706
707 let dir = tempdir().expect("temp dir should be created");
708 let socket_path = dir.path().join("apm.sock");
709 let uds_endpoint = ListenAddress::try_from(format!("unix://{}", socket_path.display()).as_str())
710 .expect("Unix APM endpoint should parse");
711
712 let router = test_router();
713 let server = HttpServer::from_listen_address(uds_endpoint)
714 .with_routes(router)
715 .with_server_id(BOUND_ADDRESS_ID)
716 .with_worker_pool(tokio::runtime::Handle::current());
717
718 let mut bound_address_subscription = supervisor
720 .dataspace()
721 .subscribe::<BoundListenAddress>(IdentifierFilter::exact(format!("http-server-{BOUND_ADDRESS_ID}")));
722
723 supervisor
724 .scope(async {
725 runtime::nested_supervisor(server.into_supervisor()).spawn();
726 })
727 .await;
728
729 let bound_path = match tokio::time::timeout(Duration::from_secs(5), bound_address_subscription.recv()).await {
730 Ok(Some(DataspaceUpdate::Asserted(_, BoundListenAddress::Unix(path)))) => path,
731 update => panic!("expected a bound address assertion for `{BOUND_ADDRESS_ID}`, got {update:?}"),
732 };
733 assert_eq!(bound_path, socket_path);
734
735 let mut stream = UnixStream::connect(&socket_path)
736 .await
737 .expect("should connect to Unix domain socket");
738
739 let body = b"proxied request over uds";
740 let request = format!(
741 "POST /info HTTP/1.1\r\nHost: localhost\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
742 body.len()
743 );
744 stream
745 .write_all(request.as_bytes())
746 .await
747 .expect("should write request head");
748 stream.write_all(body).await.expect("should write request body");
749
750 let mut response = Vec::new();
751 tokio::time::timeout(Duration::from_secs(5), stream.read_to_end(&mut response))
752 .await
753 .expect("server should respond over the Unix domain socket")
754 .expect("should read response");
755
756 assert!(
757 response.starts_with(b"HTTP/1.1 502"),
758 "expected a 502 Bad Gateway response, got: {}",
759 String::from_utf8_lossy(&response)
760 );
761 }
762
763 fn test_config(bind_host: Option<&str>, non_local_traffic: bool, receiver_port: u16) -> ApmRelayConfiguration {
764 ApmRelayConfiguration {
765 receiver_socket: String::new(),
766 proxy_destination: "http://127.0.0.1:8127".to_string(),
767 capture_entity_resolver: None,
768 bind_host: bind_host.map(str::to_string),
769 non_local_traffic,
770 receiver_port,
771 }
772 }
773
774 #[tokio::test]
775 async fn a_listener_that_fails_to_bind_fails_the_relay() {
776 let occupied = std::net::TcpListener::bind("127.0.0.1:0").expect("should bind an ephemeral port");
778 let port = occupied.local_addr().expect("should have a local address").port();
779
780 let (control, _outputs) = TestComponentDriver::relay(test_config(None, false, port))
781 .await
782 .expect("the relay should build");
783
784 let result = control.wait().await;
785 assert!(result.is_err(), "the relay should fail when its listener can't bind");
786 }
787
788 async fn build_error(config: ApmRelayConfiguration) -> GenericError {
789 match config.build(BuildContext::test_relay("apm_relay_build_error")).await {
790 Ok(_) => panic!("build should fail"),
791 Err(error) => error,
792 }
793 }
794
795 #[tokio::test]
796 async fn build_fails_when_the_destination_is_the_tcp_listener() {
797 let config = test_config(None, false, 8127);
799 assert!(build_error(config).await.to_string().contains("relay's own listener"));
800
801 let config = test_config(None, true, 8127);
803 assert!(build_error(config).await.to_string().contains("relay's own listener"));
804
805 let mut config = test_config(None, false, 8127);
806 config.proxy_destination = "http://localhost:8127".to_string();
807 assert!(build_error(config).await.to_string().contains("relay's own listener"));
808 }
809
810 #[cfg(unix)]
811 #[tokio::test]
812 async fn build_fails_when_the_destination_is_the_unix_domain_socket_listener() {
813 let mut config = test_config(None, false, 0);
814 config.receiver_socket = "/var/run/datadog/apm.socket".to_string();
815 config.proxy_destination = "unix:///var/run/datadog/apm.socket".to_string();
816
817 assert!(build_error(config).await.to_string().contains("relay's own listener"));
818 }
819
820 #[test]
821 fn tcp_reaches_only_a_listener_on_the_same_port_and_address() {
822 let addr = |s: &str| s.parse::<SocketAddr>().unwrap();
823
824 assert!(tcp_reaches(addr("127.0.0.1:8126"), addr("127.0.0.1:8126")));
825 assert!(tcp_reaches(addr("127.0.0.1:8126"), addr("0.0.0.0:8126")));
826 assert!(tcp_reaches(addr("0.0.0.0:8126"), addr("127.0.0.1:8126")));
827 assert!(tcp_reaches(addr("[::1]:8126"), addr("[::]:8126")));
828
829 assert!(!tcp_reaches(addr("127.0.0.1:8127"), addr("127.0.0.1:8126")));
830 assert!(!tcp_reaches(addr("10.0.0.2:8126"), addr("10.0.0.1:8126")));
831 assert!(!tcp_reaches(addr("[::1]:8126"), addr("0.0.0.0:8126")));
832 assert!(!tcp_reaches(addr("127.0.0.1:8126"), addr("10.0.0.1:8126")));
833 }
834
835 #[test]
836 fn listen_host_follows_explicit_settings() {
837 let host = Some("10.0.0.1".to_string());
838
839 for containerized in [false, true] {
841 assert_eq!(
842 resolve_listen_host(
843 &ConfigValue::explicit(host.clone()),
844 ConfigValue::defaulted(false),
845 containerized
846 ),
847 (host.clone(), false)
848 );
849 }
850
851 assert_eq!(
854 resolve_listen_host(
855 &ConfigValue::explicit(host.clone()),
856 ConfigValue::defaulted(true),
857 false
858 ),
859 (host.clone(), true)
860 );
861
862 assert_eq!(
864 resolve_listen_host(
865 &ConfigValue::defaulted(Some("localhost".to_string())),
866 ConfigValue::explicit(false),
867 true
868 ),
869 (None, false)
870 );
871 }
872
873 #[test]
874 fn listen_host_binds_every_interface_in_a_container_when_unset() {
875 let defaulted_host = ConfigValue::defaulted(Some("localhost".to_string()));
876
877 assert_eq!(
878 resolve_listen_host(&defaulted_host, ConfigValue::defaulted(false), true),
879 (None, true)
880 );
881 assert_eq!(
882 resolve_listen_host(&defaulted_host, ConfigValue::defaulted(false), false),
883 (None, false)
884 );
885 }
886
887 #[tokio::test]
888 async fn build_fails_without_any_configured_endpoint() {
889 let config = test_config(None, false, 0);
891
892 let error = match config.build(BuildContext::test_relay("apm_relay_no_endpoints")).await {
893 Ok(_) => panic!("build should fail when no endpoint is configured"),
894 Err(error) => error,
895 };
896 assert!(error.to_string().contains("at least one"));
897 }
898
899 #[tokio::test]
900 async fn build_succeeds_with_only_tcp_endpoint_configured() {
901 let config = test_config(None, true, 8126);
902
903 assert!(config
904 .build(BuildContext::test_relay("apm_relay_tcp_only"))
905 .await
906 .is_ok());
907 }
908
909 #[tokio::test]
910 async fn build_fails_with_invalid_proxy_destination() {
911 let mut config = test_config(None, false, 8126);
912 config.proxy_destination = "ftp://127.0.0.1:8127".to_string();
913
914 assert!(config
915 .build(BuildContext::test_relay("apm_relay_bad_destination"))
916 .await
917 .is_err());
918 }
919
920 #[tokio::test]
921 async fn build_fails_with_unresolvable_bind_host() {
922 let config = test_config(Some("not a valid host"), false, 8126);
923
924 let error = match config
925 .build(BuildContext::test_relay("apm_relay_unresolvable_bind_host"))
926 .await
927 {
928 Ok(_) => panic!("build should fail for an unresolvable bind host"),
929 Err(error) => error,
930 };
931 assert!(error.to_string().contains("Could not resolve bind_host"));
932 }
933
934 #[tokio::test]
935 async fn tcp_listen_address_defaults_to_loopback() {
936 let config = test_config(None, false, 8127);
937 assert_eq!(
938 config.tcp_listen_address().await.unwrap().unwrap().to_string(),
939 "tcp://127.0.0.1:8127"
940 );
941 }
942
943 #[tokio::test]
944 async fn tcp_listen_address_uses_bind_host() {
945 let config = test_config(Some("192.168.1.50"), false, 8127);
946 assert_eq!(
947 config.tcp_listen_address().await.unwrap().unwrap().to_string(),
948 "tcp://192.168.1.50:8127"
949 );
950 }
951
952 #[tokio::test]
953 async fn tcp_listen_address_non_local_traffic_overrides_bind_host() {
954 let config = test_config(Some("192.168.1.50"), true, 8127);
955 assert_eq!(
956 config.tcp_listen_address().await.unwrap().unwrap().to_string(),
957 "tcp://0.0.0.0:8127"
958 );
959 }
960
961 #[cfg(unix)]
962 #[test]
963 fn uds_listen_address_parses_path() {
964 let mut config = test_config(None, false, 0);
965 config.receiver_socket = "/tmp/apm.sock".to_string();
966 assert_eq!(
967 config.uds_listen_address().unwrap().unwrap().to_string(),
968 "unix:///tmp/apm.sock"
969 );
970 }
971}