Skip to main content

saluki_io/net/
addr.rs

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/// A listen address.
14///
15/// Listen addresses are used to bind listeners to specific local addresses and ports, and multiple address families and
16/// protocols are supported. In textual form, listen addresses are represented as URLs, with the scheme indicating the
17/// protocol and the authority/path representing the address to listen on.
18///
19/// # Examples
20///
21/// - `tcp://127.0.0.1:6789` (listen on IPv4 loopback, TCP port 6789)
22/// - `udp://[::1]:53` (listen on IPv6 loopback, UDP port 53)
23/// - `unixgram:///tmp/app.socket` (listen on a Unix datagram socket at `/tmp/app.socket`)
24/// - `unix:///tmp/app.socket` (listen on a Unix stream socket at `/tmp/app.socket`)
25#[derive(Clone, Debug, Deserialize)]
26#[serde(try_from = "String")]
27pub enum ListenAddress {
28    /// A TCP listen address.
29    Tcp(SocketAddr),
30
31    /// A UDP listen address.
32    Udp(SocketAddr),
33
34    /// A Unix datagram listen address.
35    #[cfg(unix)]
36    Unixgram(PathBuf),
37
38    /// A Unix stream listen address.
39    #[cfg(unix)]
40    Unix(PathBuf),
41}
42
43impl ListenAddress {
44    /// Creates a TCP address for the given port that listens on all interfaces.
45    pub const fn any_tcp(port: u16) -> Self {
46        Self::Tcp(SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, port)))
47    }
48
49    /// Returns the socket type of the listen address.
50    pub const fn listener_type(&self) -> &'static str {
51        match self {
52            Self::Tcp(_) => "tcp",
53            Self::Udp(_) => "udp",
54            #[cfg(unix)]
55            Self::Unixgram(_) => "unixgram",
56            #[cfg(unix)]
57            Self::Unix(_) => "unix",
58        }
59    }
60
61    /// Returns a socket address that can be used to connect to the configured listen address with a bias for local
62    /// clients.
63    ///
64    /// When the listen address is a TCP or UDP address, this method returns a socket address that can be used to
65    /// connect to the listener bound to this listen address, such that if the listen address is unspecified
66    /// (`0.0.0.0`), the client will connect locally using `localhost`. When the listen address isn't unspecified or
67    /// already uses `localhost`, this method returns the listen address as-is.
68    ///
69    /// If the address is a Unix domain socket, this method returns `None`.
70    pub fn as_local_connect_addr(&self) -> Option<SocketAddr> {
71        match self {
72            Self::Tcp(addr) | Self::Udp(addr) => {
73                let mut connect_addr = *addr;
74                if connect_addr.ip().is_unspecified() {
75                    let localhost_ip = match connect_addr.is_ipv4() {
76                        true => IpAddr::V4(Ipv4Addr::LOCALHOST),
77                        false => IpAddr::V6(Ipv6Addr::LOCALHOST),
78                    };
79
80                    connect_addr.set_ip(localhost_ip);
81                }
82
83                Some(connect_addr)
84            }
85            // TODO: why did i do this? it's totally possible to connect to a unix domain socket locally...
86            // in fact, it's kind of the only way to connect to a unix domain socket :thonk:
87            #[cfg(unix)]
88            Self::Unixgram(_) => None,
89            #[cfg(unix)]
90            Self::Unix(_) => None,
91        }
92    }
93
94    /// Returns the Unix domain socket path if the address is a Unix domain socket in SOCK_STREAM mode.
95    ///
96    /// Returns `None` otherwise.
97    pub fn as_unix_stream_path(&self) -> Option<&Path> {
98        match self {
99            #[cfg(unix)]
100            Self::Unix(path) => Some(path),
101            _ => None,
102        }
103    }
104}
105
106impl fmt::Display for ListenAddress {
107    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
108        match self {
109            Self::Tcp(addr) => write!(f, "tcp://{}", addr),
110            Self::Udp(addr) => write!(f, "udp://{}", addr),
111            #[cfg(unix)]
112            Self::Unixgram(path) => write!(f, "unixgram://{}", path.display()),
113            #[cfg(unix)]
114            Self::Unix(path) => write!(f, "unix://{}", path.display()),
115        }
116    }
117}
118
119impl TryFrom<String> for ListenAddress {
120    type Error = String;
121
122    fn try_from(value: String) -> Result<Self, Self::Error> {
123        Self::try_from(value.as_str())
124    }
125}
126
127impl<'a> TryFrom<&'a str> for ListenAddress {
128    type Error = String;
129
130    fn try_from(value: &'a str) -> Result<Self, Self::Error> {
131        let url = match Url::parse(value) {
132            Ok(url) => url,
133            Err(e) => match e {
134                url::ParseError::RelativeUrlWithoutBase => {
135                    Url::parse(&format!("unixgram://{}", value)).map_err(|e| e.to_string())?
136                }
137                _ => return Err(e.to_string()),
138            },
139        };
140
141        match url.scheme() {
142            "tcp" => {
143                let mut socket_addresses = url.socket_addrs(|| None).map_err(|e| e.to_string())?;
144                if socket_addresses.is_empty() {
145                    Err("listen address must resolve to at least one valid IP address/port pair".to_string())
146                } else {
147                    Ok(Self::Tcp(socket_addresses.swap_remove(0)))
148                }
149            }
150            "udp" => {
151                let mut socket_addresses = url.socket_addrs(|| None).map_err(|e| e.to_string())?;
152                if socket_addresses.is_empty() {
153                    Err("listen address must resolve to at least one valid IP address/port pair".to_string())
154                } else {
155                    Ok(Self::Udp(socket_addresses.swap_remove(0)))
156                }
157            }
158            #[cfg(unix)]
159            "unixgram" => {
160                let path = url.path();
161                if path.is_empty() {
162                    return Err("socket path cannot be empty".to_string());
163                }
164
165                let path_buf = PathBuf::from(path);
166                if !path_buf.is_absolute() {
167                    return Err("socket path must be absolute".to_string());
168                }
169
170                Ok(Self::Unixgram(path_buf))
171            }
172            #[cfg(unix)]
173            "unix" => {
174                let path = url.path();
175                if path.is_empty() {
176                    return Err("socket path cannot be empty".to_string());
177                }
178
179                let path_buf = PathBuf::from(path);
180                if !path_buf.is_absolute() {
181                    return Err("socket path must be absolute".to_string());
182                }
183
184                Ok(Self::Unix(path_buf))
185            }
186            scheme => Err(format!("unknown/unsupported address scheme '{}'", scheme)),
187        }
188    }
189}
190
191/// Process credentials for a Unix domain socket connection.
192///
193/// When dealing with Unix domain sockets, they can be configured such that the "process credentials" of the remote peer
194/// are sent as part of each received message. These "credentials" are the process ID of the remote peer, and the user
195/// ID and group ID that the process is running as.
196///
197/// In some cases, this information can be useful for identifying the remote peer and enriching the received data in an
198/// automatic way.
199#[cfg(unix)]
200#[derive(Clone)]
201pub struct ProcessCredentials {
202    /// Process ID of the remote peer.
203    pub pid: i32,
204
205    /// User ID of the remote peer process.
206    pub uid: u32,
207
208    /// Group ID of the remote peer process.
209    pub gid: u32,
210}
211
212/// Reason UDS process credential detection failed.
213#[cfg(unix)]
214#[derive(Clone, Copy)]
215pub enum ProcessCredentialsError {
216    /// Ancillary data was present but didn't contain usable process credentials.
217    InvalidCredentials,
218
219    /// Process credentials were present, but the PID was zero.
220    ZeroPid,
221
222    /// UDS process credential detection isn't supported on this platform.
223    UnsupportedPlatform,
224}
225
226#[cfg(unix)]
227impl ProcessCredentialsError {
228    /// Returns a concise identifier for the failure reason.
229    pub const fn identifier(&self) -> &'static str {
230        match self {
231            Self::InvalidCredentials => "invalid-credentials",
232            Self::ZeroPid => "zero-pid",
233            Self::UnsupportedPlatform => "unsupported-platform",
234        }
235    }
236}
237
238#[cfg(unix)]
239impl fmt::Display for ProcessCredentialsError {
240    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
241        match self {
242            Self::InvalidCredentials => write!(f, "invalid process credentials"),
243            Self::ZeroPid => write!(f, "process credential PID is zero"),
244            Self::UnsupportedPlatform => write!(f, "process credentials are unsupported on this platform"),
245        }
246    }
247}
248
249/// Process identity associated with a Unix domain socket peer.
250#[cfg(unix)]
251#[derive(Clone)]
252pub enum ProcessIdentity {
253    /// Process credentials were detected.
254    Credentials(ProcessCredentials),
255
256    /// Process credential detection failed.
257    Error(ProcessCredentialsError),
258
259    /// Process identity isn't available for this peer.
260    Unavailable,
261}
262
263#[cfg(unix)]
264impl ProcessIdentity {
265    /// Returns process credentials, if they were detected.
266    pub fn credentials(&self) -> Option<&ProcessCredentials> {
267        match self {
268            Self::Credentials(creds) => Some(creds),
269            Self::Error(_) | Self::Unavailable => None,
270        }
271    }
272
273    /// Returns `true` if process credential detection failed.
274    pub const fn is_error(&self) -> bool {
275        matches!(self, Self::Error(_))
276    }
277}
278
279/// Connection address.
280///
281/// A generic representation of the address of a remote peer. This can either be a typical socket address (used for
282/// IPv4/IPv6), or potentially the process credentials of a Unix domain socket connection.
283#[derive(Clone)]
284pub enum ConnectionAddress {
285    /// A socket-like address.
286    SocketLike(SocketAddr),
287
288    /// A process-like address.
289    #[cfg(unix)]
290    ProcessLike(ProcessIdentity),
291}
292
293impl fmt::Display for ConnectionAddress {
294    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
295        match self {
296            Self::SocketLike(addr) => write!(f, "{}", addr),
297            #[cfg(unix)]
298            Self::ProcessLike(identity) => match identity {
299                ProcessIdentity::Credentials(creds) => {
300                    write!(f, "<pid={} uid={} gid={}>", creds.pid, creds.uid, creds.gid)
301                }
302                ProcessIdentity::Error(error) => write!(f, "<origin-detection-error: {}>", error.identifier()),
303                ProcessIdentity::Unavailable => write!(f, "<no-origin>"),
304            },
305        }
306    }
307}
308
309impl ConnectionAddress {
310    /// Returns process credentials for a Unix domain socket peer, if available.
311    #[cfg(unix)]
312    pub fn process_credentials(&self) -> Option<&ProcessCredentials> {
313        match self {
314            Self::ProcessLike(identity) => identity.credentials(),
315            Self::SocketLike(_) => None,
316        }
317    }
318
319    /// Returns `true` if Unix domain socket process credential detection failed.
320    #[cfg(unix)]
321    pub const fn has_process_credential_error(&self) -> bool {
322        match self {
323            Self::ProcessLike(identity) => identity.is_error(),
324            Self::SocketLike(_) => false,
325        }
326    }
327}
328
329impl From<SocketAddr> for ConnectionAddress {
330    fn from(value: SocketAddr) -> Self {
331        Self::SocketLike(value)
332    }
333}
334
335#[cfg(unix)]
336impl From<ProcessCredentials> for ConnectionAddress {
337    fn from(creds: ProcessCredentials) -> Self {
338        Self::ProcessLike(ProcessIdentity::Credentials(creds))
339    }
340}
341
342impl<'a> Connected<&'a Connection> for ConnectionAddress {
343    fn connect_info(target: &'a Connection) -> Self {
344        target.remote_addr()
345    }
346}
347
348/// A gRPC target address.
349///
350/// This represents the address of a gRPC server that can be connected to. `GrpcTargetAddress` exposes a `Display`
351/// implementation that emits the target address following the rules of the [gRPC Name
352/// Resolution][grpc_name_resolution_docs] documentation.
353///
354/// Only connection-oriented transports are supported: TCP and Unix domain sockets in SOCK_STREAM mode.
355///
356/// [grpc_name_resolution_docs]: https://github.com/grpc/grpc/blob/master/doc/naming.md
357pub enum GrpcTargetAddress {
358    Tcp(SocketAddr),
359    Unix(PathBuf),
360}
361
362impl GrpcTargetAddress {
363    /// Creates a new `GrpcTargetAddress` from the given `ListenAddress`.
364    ///
365    /// For TCP addresses, this method converts unspecified addresses (`0.0.0.0` or `::`) to localhost
366    /// (`127.0.0.1` or `::1`) to ensure the advertised address matches TLS certificates.
367    ///
368    /// Returns `None` if the listen address isn't a connection-oriented transport.
369    pub fn try_from_listen_addr(listen_address: &ListenAddress) -> Option<Self> {
370        match listen_address {
371            ListenAddress::Tcp(_) => {
372                // For TCP, convert 0.0.0.0 to 127.0.0.1 to match TLS certificate
373                listen_address.as_local_connect_addr().map(GrpcTargetAddress::Tcp)
374            }
375            ListenAddress::Unix(path) => Some(GrpcTargetAddress::Unix(path.clone())),
376            _ => None,
377        }
378    }
379}
380
381impl fmt::Display for GrpcTargetAddress {
382    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
383        match self {
384            GrpcTargetAddress::Tcp(addr) => write!(f, "{}", addr),
385            GrpcTargetAddress::Unix(path) => write!(f, "unix://{}", path.display()),
386        }
387    }
388}
389
390#[cfg(test)]
391mod tests {
392    use super::*;
393
394    #[test]
395    fn test_as_local_connect_addr() {
396        let tcp_any_addr = ListenAddress::try_from("tcp://0.0.0.0:1234").unwrap();
397        assert_eq!(
398            tcp_any_addr.as_local_connect_addr(),
399            Some(SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 1234)))
400        );
401
402        let tcp_localhost_addr = ListenAddress::try_from("tcp://127.0.0.1:2345").unwrap();
403        assert_eq!(
404            tcp_localhost_addr.as_local_connect_addr(),
405            Some(SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 2345)))
406        );
407
408        let tcp_private_addr = ListenAddress::try_from("tcp://192.168.10.2:3456").unwrap();
409        assert_eq!(
410            tcp_private_addr.as_local_connect_addr(),
411            Some(SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(192, 168, 10, 2), 3456)))
412        );
413
414        let udp_any_addr = ListenAddress::try_from("udp://0.0.0.0:4567").unwrap();
415        assert_eq!(
416            udp_any_addr.as_local_connect_addr(),
417            Some(SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 4567)))
418        );
419
420        let udp_localhost_addr = ListenAddress::try_from("udp://127.0.0.1:5678").unwrap();
421        assert_eq!(
422            udp_localhost_addr.as_local_connect_addr(),
423            Some(SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 5678)))
424        );
425
426        let udp_private_addr = ListenAddress::try_from("udp://192.168.10.2:6789").unwrap();
427        assert_eq!(
428            udp_private_addr.as_local_connect_addr(),
429            Some(SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(192, 168, 10, 2), 6789)))
430        );
431    }
432}