1use std::{
2 fmt,
3 net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4},
4 path::{Path, PathBuf},
5};
6
7use axum::extract::connect_info::Connected;
8use serde::Deserialize;
9use url::Url;
10
11use super::Connection;
12
13#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
26#[serde(try_from = "String")]
27pub enum ListenAddress {
28 Tcp(SocketAddr),
30
31 Udp(SocketAddr),
33
34 Unixgram(PathBuf),
36
37 Unix(PathBuf),
39
40 NamedPipe {
42 name: String,
44
45 security_descriptor: String,
47
48 input_buffer_size: Option<u32>,
50 },
51}
52
53impl ListenAddress {
54 pub const fn tcp_any(port: u16) -> Self {
56 Self::Tcp(SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, port)))
57 }
58
59 pub const fn tcp_loopback(port: u16) -> Self {
61 Self::Tcp(SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), port)))
62 }
63
64 pub fn named_pipe(name: impl Into<String>, security_descriptor: impl Into<String>) -> Self {
66 Self::named_pipe_with_input_buffer_size(name, security_descriptor, None)
67 }
68
69 pub fn named_pipe_with_input_buffer_size(
71 name: impl Into<String>, security_descriptor: impl Into<String>, input_buffer_size: impl Into<Option<u32>>,
72 ) -> Self {
73 Self::NamedPipe {
74 name: normalize_windows_named_pipe_name(name.into()),
75 security_descriptor: security_descriptor.into(),
76 input_buffer_size: input_buffer_size.into(),
77 }
78 }
79
80 pub const fn listener_type(&self) -> &'static str {
82 match self {
83 Self::Tcp(_) => "tcp",
84 Self::Udp(_) => "udp",
85 Self::Unixgram(_) => "unixgram",
86 Self::Unix(_) => "unix",
87 Self::NamedPipe { .. } => "named_pipe",
88 }
89 }
90
91 pub fn as_local_connect_addr(&self) -> Option<SocketAddr> {
101 match self {
102 Self::Tcp(addr) | Self::Udp(addr) => {
103 let mut connect_addr = *addr;
104 if connect_addr.ip().is_unspecified() {
105 let localhost_ip = match connect_addr.is_ipv4() {
106 true => IpAddr::V4(Ipv4Addr::LOCALHOST),
107 false => IpAddr::V6(Ipv6Addr::LOCALHOST),
108 };
109
110 connect_addr.set_ip(localhost_ip);
111 }
112
113 Some(connect_addr)
114 }
115 Self::Unixgram(_) => None,
118 Self::Unix(_) => None,
119 Self::NamedPipe { .. } => None,
120 }
121 }
122
123 pub fn as_windows_named_pipe_path(&self) -> Option<String> {
125 match self {
126 Self::NamedPipe { name, .. } => Some(format!(r"\\.\pipe\{name}")),
127 _ => None,
128 }
129 }
130
131 pub fn as_windows_named_pipe_security_descriptor(&self) -> Option<&str> {
133 match self {
134 Self::NamedPipe {
135 security_descriptor, ..
136 } => Some(security_descriptor.as_str()),
137 _ => None,
138 }
139 }
140
141 pub fn as_unix_stream_path(&self) -> Option<&Path> {
145 match self {
146 Self::Unix(path) => Some(path),
147 _ => None,
148 }
149 }
150}
151
152fn normalize_windows_named_pipe_name(name: String) -> String {
153 name.strip_prefix(r"\\.\pipe\")
154 .or_else(|| name.strip_prefix(r"//./pipe/"))
155 .or_else(|| name.strip_prefix("pipe/"))
156 .unwrap_or(&name)
157 .to_string()
158}
159
160impl fmt::Display for ListenAddress {
161 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
162 match self {
163 Self::Tcp(addr) => write!(f, "tcp://{}", addr),
164 Self::Udp(addr) => write!(f, "udp://{}", addr),
165 Self::Unixgram(path) => write!(f, "unixgram://{}", path.display()),
166 Self::Unix(path) => write!(f, "unix://{}", path.display()),
167 Self::NamedPipe { name, .. } => write!(f, "npipe://{}", name),
168 }
169 }
170}
171
172impl TryFrom<String> for ListenAddress {
173 type Error = String;
174
175 fn try_from(value: String) -> Result<Self, Self::Error> {
176 Self::try_from(value.as_str())
177 }
178}
179
180impl<'a> TryFrom<&'a str> for ListenAddress {
181 type Error = String;
182
183 fn try_from(value: &'a str) -> Result<Self, Self::Error> {
184 let url = match Url::parse(value) {
185 Ok(url) => url,
186 Err(e) => match e {
187 url::ParseError::RelativeUrlWithoutBase => {
188 Url::parse(&format!("unixgram://{}", value)).map_err(|e| e.to_string())?
189 }
190 _ => return Err(e.to_string()),
191 },
192 };
193
194 match url.scheme() {
195 "tcp" => {
196 let mut socket_addresses = url.socket_addrs(|| None).map_err(|e| e.to_string())?;
197 if socket_addresses.is_empty() {
198 Err("listen address must resolve to at least one valid IP address/port pair".to_string())
199 } else {
200 Ok(Self::Tcp(socket_addresses.swap_remove(0)))
201 }
202 }
203 "udp" => {
204 let mut socket_addresses = url.socket_addrs(|| None).map_err(|e| e.to_string())?;
205 if socket_addresses.is_empty() {
206 Err("listen address must resolve to at least one valid IP address/port pair".to_string())
207 } else {
208 Ok(Self::Udp(socket_addresses.swap_remove(0)))
209 }
210 }
211 "unixgram" => {
212 let path = url.path();
213 if path.is_empty() {
214 return Err("socket path cannot be empty".to_string());
215 }
216
217 let path_buf = PathBuf::from(path);
218 if !path_buf.is_absolute() {
219 return Err("socket path must be absolute".to_string());
220 }
221
222 Ok(Self::Unixgram(path_buf))
223 }
224 "unix" => {
225 let path = url.path();
226 if path.is_empty() {
227 return Err("socket path cannot be empty".to_string());
228 }
229
230 let path_buf = PathBuf::from(path);
231 if !path_buf.is_absolute() {
232 return Err("socket path must be absolute".to_string());
233 }
234
235 Ok(Self::Unix(path_buf))
236 }
237 "npipe" => {
238 let name = url.host_str().unwrap_or_else(|| url.path().trim_start_matches('/'));
239 if name.is_empty() {
240 return Err("named pipe name cannot be empty".to_string());
241 }
242
243 Ok(Self::named_pipe(name, String::new()))
244 }
245 scheme => Err(format!("unknown/unsupported address scheme '{}'", scheme)),
246 }
247 }
248}
249
250#[derive(Clone, Debug, PartialEq, Eq)]
258pub enum BoundListenAddress {
259 Tcp(SocketAddr),
261
262 Udp(SocketAddr),
264
265 Unixgram(PathBuf),
267
268 Unix(PathBuf),
270
271 NamedPipe(String),
273}
274
275impl fmt::Display for BoundListenAddress {
276 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
277 match self {
278 Self::Tcp(addr) => write!(f, "tcp://{}", addr),
279 Self::Udp(addr) => write!(f, "udp://{}", addr),
280 Self::Unixgram(path) => write!(f, "unixgram://{}", path.display()),
281 Self::Unix(path) => write!(f, "unix://{}", path.display()),
282 Self::NamedPipe(name) => write!(f, "npipe://{}", name),
283 }
284 }
285}
286
287#[derive(Clone)]
296pub struct ProcessCredentials {
297 pub pid: i32,
299
300 pub uid: u32,
302
303 pub gid: u32,
305}
306
307#[derive(Clone, Copy)]
309pub enum ProcessCredentialsError {
310 InvalidCredentials,
312
313 ZeroPid,
315
316 UnsupportedPlatform,
318}
319
320impl ProcessCredentialsError {
321 pub const fn identifier(&self) -> &'static str {
323 match self {
324 Self::InvalidCredentials => "invalid-credentials",
325 Self::ZeroPid => "zero-pid",
326 Self::UnsupportedPlatform => "unsupported-platform",
327 }
328 }
329}
330
331impl fmt::Display for ProcessCredentialsError {
332 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
333 match self {
334 Self::InvalidCredentials => write!(f, "invalid process credentials"),
335 Self::ZeroPid => write!(f, "process credential PID is zero"),
336 Self::UnsupportedPlatform => write!(f, "process credentials are unsupported on this platform"),
337 }
338 }
339}
340
341#[derive(Clone)]
343pub enum ProcessIdentity {
344 Credentials(ProcessCredentials),
346
347 Error(ProcessCredentialsError),
349
350 Unavailable,
352}
353
354impl ProcessIdentity {
355 pub fn credentials(&self) -> Option<&ProcessCredentials> {
357 match self {
358 Self::Credentials(creds) => Some(creds),
359 Self::Error(_) | Self::Unavailable => None,
360 }
361 }
362
363 pub const fn is_error(&self) -> bool {
365 matches!(self, Self::Error(_))
366 }
367
368 pub const fn is_telemetry_error(&self) -> bool {
370 matches!(
371 self,
372 Self::Error(ProcessCredentialsError::InvalidCredentials | ProcessCredentialsError::ZeroPid)
373 )
374 }
375}
376
377#[derive(Clone)]
382pub enum ConnectionAddress {
383 SocketLike(SocketAddr),
385
386 ProcessLike(ProcessIdentity),
388}
389
390impl fmt::Display for ConnectionAddress {
391 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
392 match self {
393 Self::SocketLike(addr) => write!(f, "{}", addr),
394 Self::ProcessLike(identity) => match identity {
395 ProcessIdentity::Credentials(creds) => {
396 write!(f, "<pid={} uid={} gid={}>", creds.pid, creds.uid, creds.gid)
397 }
398 ProcessIdentity::Error(error) => write!(f, "<origin-detection-error: {}>", error.identifier()),
399 ProcessIdentity::Unavailable => write!(f, "<no-origin>"),
400 },
401 }
402 }
403}
404
405impl ConnectionAddress {
406 pub fn process_credentials(&self) -> Option<&ProcessCredentials> {
408 match self {
409 Self::ProcessLike(identity) => identity.credentials(),
410 Self::SocketLike(_) => None,
411 }
412 }
413
414 pub const fn has_process_credential_error(&self) -> bool {
416 match self {
417 Self::ProcessLike(identity) => identity.is_error(),
418 Self::SocketLike(_) => false,
419 }
420 }
421
422 pub const fn has_process_credential_telemetry_error(&self) -> bool {
424 match self {
425 Self::ProcessLike(identity) => identity.is_telemetry_error(),
426 Self::SocketLike(_) => false,
427 }
428 }
429}
430
431impl From<SocketAddr> for ConnectionAddress {
432 fn from(value: SocketAddr) -> Self {
433 Self::SocketLike(value)
434 }
435}
436
437impl From<ProcessCredentials> for ConnectionAddress {
438 fn from(creds: ProcessCredentials) -> Self {
439 Self::ProcessLike(ProcessIdentity::Credentials(creds))
440 }
441}
442
443impl<'a> Connected<&'a Connection> for ConnectionAddress {
444 fn connect_info(target: &'a Connection) -> Self {
445 target.remote_addr()
446 }
447}
448
449pub enum GrpcTargetAddress {
459 Tcp(SocketAddr),
460 Unix(PathBuf),
461}
462
463impl GrpcTargetAddress {
464 pub fn try_from_listen_addr(listen_address: &ListenAddress) -> Option<Self> {
471 match listen_address {
472 ListenAddress::Tcp(_) => {
473 listen_address.as_local_connect_addr().map(GrpcTargetAddress::Tcp)
475 }
476 ListenAddress::Unix(path) => Some(GrpcTargetAddress::Unix(path.clone())),
477 _ => None,
478 }
479 }
480}
481
482impl fmt::Display for GrpcTargetAddress {
483 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
484 match self {
485 GrpcTargetAddress::Tcp(addr) => write!(f, "{}", addr),
486 GrpcTargetAddress::Unix(path) => write!(f, "unix://{}", path.display()),
487 }
488 }
489}
490
491#[cfg(test)]
492mod tests {
493 use super::*;
494
495 #[test]
496 fn named_pipe_listen_address_formats_with_windows_pipe_prefix() {
497 let address = ListenAddress::named_pipe("datadog-dogstatsd", "D:AI(A;;GA;;;WD)");
498
499 assert_eq!(address.listener_type(), "named_pipe");
500 assert_eq!(address.to_string(), r"npipe://datadog-dogstatsd");
501 assert_eq!(
502 address.as_windows_named_pipe_path().as_deref(),
503 Some(r"\\.\pipe\datadog-dogstatsd")
504 );
505 }
506
507 #[test]
508 fn named_pipe_listen_address_accepts_full_windows_pipe_path() {
509 let address = ListenAddress::named_pipe(r"\\.\pipe\datadog-dogstatsd", "D:AI(A;;GA;;;WD)");
510
511 assert_eq!(address.to_string(), r"npipe://datadog-dogstatsd");
512 assert_eq!(
513 address.as_windows_named_pipe_path().as_deref(),
514 Some(r"\\.\pipe\datadog-dogstatsd")
515 );
516 }
517
518 #[test]
519 fn npipe_url_parses_full_windows_pipe_path() {
520 let address = ListenAddress::try_from("npipe:////./pipe/datadog-dogstatsd").unwrap();
521
522 assert_eq!(address.to_string(), r"npipe://datadog-dogstatsd");
523 assert_eq!(
524 address.as_windows_named_pipe_path().as_deref(),
525 Some(r"\\.\pipe\datadog-dogstatsd")
526 );
527 }
528
529 #[test]
530 fn unsupported_platform_process_identity_is_not_a_telemetry_error() {
531 let peer_addr =
532 ConnectionAddress::ProcessLike(ProcessIdentity::Error(ProcessCredentialsError::UnsupportedPlatform));
533
534 assert!(peer_addr.has_process_credential_error());
535 assert!(!peer_addr.has_process_credential_telemetry_error());
536 }
537
538 #[test]
539 fn invalid_process_credentials_are_telemetry_errors() {
540 let peer_addr =
541 ConnectionAddress::ProcessLike(ProcessIdentity::Error(ProcessCredentialsError::InvalidCredentials));
542
543 assert!(peer_addr.has_process_credential_error());
544 assert!(peer_addr.has_process_credential_telemetry_error());
545 }
546
547 #[test]
548 fn zero_pid_process_credentials_are_telemetry_errors() {
549 let peer_addr = ConnectionAddress::ProcessLike(ProcessIdentity::Error(ProcessCredentialsError::ZeroPid));
550
551 assert!(peer_addr.has_process_credential_error());
552 assert!(peer_addr.has_process_credential_telemetry_error());
553 }
554
555 #[test]
556 fn as_local_connect_addr() {
557 let tcp_any_addr = ListenAddress::try_from("tcp://0.0.0.0:1234").unwrap();
558 assert_eq!(
559 tcp_any_addr.as_local_connect_addr(),
560 Some(SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 1234)))
561 );
562
563 let tcp_localhost_addr = ListenAddress::try_from("tcp://127.0.0.1:2345").unwrap();
564 assert_eq!(
565 tcp_localhost_addr.as_local_connect_addr(),
566 Some(SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 2345)))
567 );
568
569 let tcp_private_addr = ListenAddress::try_from("tcp://192.168.10.2:3456").unwrap();
570 assert_eq!(
571 tcp_private_addr.as_local_connect_addr(),
572 Some(SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(192, 168, 10, 2), 3456)))
573 );
574
575 let udp_any_addr = ListenAddress::try_from("udp://0.0.0.0:4567").unwrap();
576 assert_eq!(
577 udp_any_addr.as_local_connect_addr(),
578 Some(SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 4567)))
579 );
580
581 let udp_localhost_addr = ListenAddress::try_from("udp://127.0.0.1:5678").unwrap();
582 assert_eq!(
583 udp_localhost_addr.as_local_connect_addr(),
584 Some(SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 5678)))
585 );
586
587 let udp_private_addr = ListenAddress::try_from("udp://192.168.10.2:6789").unwrap();
588 assert_eq!(
589 udp_private_addr.as_local_connect_addr(),
590 Some(SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(192, 168, 10, 2), 6789)))
591 );
592 }
593
594 #[cfg(unix)]
595 #[test]
596 fn unix_scheme_parses_as_unix() {
597 let address = ListenAddress::try_from("unix:///tmp/otlp.sock").unwrap();
598 assert_eq!(address.listener_type(), "unix");
599 assert_eq!(
600 address.as_unix_stream_path(),
601 Some(std::path::Path::new("/tmp/otlp.sock"))
602 );
603 }
604}