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 clients.
62    ///
63    /// When the listen address is a TCP or UDP address, this method returns a socket address that can be used to
64    /// connect to the listener bound to this listen addresss, such that if the listen address is unspecified
65    /// (`0.0.0.0`), the client will connect locally using "localhost". When the listen address is not "unspecified" or
66    /// already uses "localhost", this method returns the listen address as-is.
67    ///
68    /// If the address is a Unix domain socket, this method returns `None`.
69    pub fn as_local_connect_addr(&self) -> Option<SocketAddr> {
70        match self {
71            Self::Tcp(addr) | Self::Udp(addr) => {
72                let mut connect_addr = *addr;
73                if connect_addr.ip().is_unspecified() {
74                    let localhost_ip = match connect_addr.is_ipv4() {
75                        true => IpAddr::V4(Ipv4Addr::LOCALHOST),
76                        false => IpAddr::V6(Ipv6Addr::LOCALHOST),
77                    };
78
79                    connect_addr.set_ip(localhost_ip);
80                }
81
82                Some(connect_addr)
83            }
84            // TODO: why did i do this? it's totally possible to connect to a unix domain socket locally...
85            // in fact, it's kind of the only way to connect to a unix domain socket :thonk:
86            #[cfg(unix)]
87            Self::Unixgram(_) => None,
88            #[cfg(unix)]
89            Self::Unix(_) => None,
90        }
91    }
92
93    /// Returns the Unix domain socket path if the address is a Unix domain socket in SOCK_STREAM mode.
94    ///
95    /// Returns `None` otherwise.
96    pub fn as_unix_stream_path(&self) -> Option<&Path> {
97        match self {
98            #[cfg(unix)]
99            Self::Unix(path) => Some(path),
100            _ => None,
101        }
102    }
103}
104
105impl fmt::Display for ListenAddress {
106    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
107        match self {
108            Self::Tcp(addr) => write!(f, "tcp://{}", addr),
109            Self::Udp(addr) => write!(f, "udp://{}", addr),
110            #[cfg(unix)]
111            Self::Unixgram(path) => write!(f, "unixgram://{}", path.display()),
112            #[cfg(unix)]
113            Self::Unix(path) => write!(f, "unix://{}", path.display()),
114        }
115    }
116}
117
118impl TryFrom<String> for ListenAddress {
119    type Error = String;
120
121    fn try_from(value: String) -> Result<Self, Self::Error> {
122        Self::try_from(value.as_str())
123    }
124}
125
126impl<'a> TryFrom<&'a str> for ListenAddress {
127    type Error = String;
128
129    fn try_from(value: &'a str) -> Result<Self, Self::Error> {
130        let url = match Url::parse(value) {
131            Ok(url) => url,
132            Err(e) => match e {
133                url::ParseError::RelativeUrlWithoutBase => {
134                    Url::parse(&format!("unixgram://{}", value)).map_err(|e| e.to_string())?
135                }
136                _ => return Err(e.to_string()),
137            },
138        };
139
140        match url.scheme() {
141            "tcp" => {
142                let mut socket_addresses = url.socket_addrs(|| None).map_err(|e| e.to_string())?;
143                if socket_addresses.is_empty() {
144                    Err("listen address must resolve to at least one valid IP address/port pair".to_string())
145                } else {
146                    Ok(Self::Tcp(socket_addresses.swap_remove(0)))
147                }
148            }
149            "udp" => {
150                let mut socket_addresses = url.socket_addrs(|| None).map_err(|e| e.to_string())?;
151                if socket_addresses.is_empty() {
152                    Err("listen address must resolve to at least one valid IP address/port pair".to_string())
153                } else {
154                    Ok(Self::Udp(socket_addresses.swap_remove(0)))
155                }
156            }
157            #[cfg(unix)]
158            "unixgram" => {
159                let path = url.path();
160                if path.is_empty() {
161                    return Err("socket path cannot be empty".to_string());
162                }
163
164                let path_buf = PathBuf::from(path);
165                if !path_buf.is_absolute() {
166                    return Err("socket path must be absolute".to_string());
167                }
168
169                Ok(Self::Unixgram(path_buf))
170            }
171            #[cfg(unix)]
172            "unix" => {
173                let path = url.path();
174                if path.is_empty() {
175                    return Err("socket path cannot be empty".to_string());
176                }
177
178                let path_buf = PathBuf::from(path);
179                if !path_buf.is_absolute() {
180                    return Err("socket path must be absolute".to_string());
181                }
182
183                Ok(Self::Unix(path_buf))
184            }
185            scheme => Err(format!("unknown/unsupported address scheme '{}'", scheme)),
186        }
187    }
188}
189
190/// Process credentials for a Unix domain socket connection.
191///
192/// When dealing with Unix domain sockets, they can be configured such that the "process credentials" of the remote peer
193/// are sent as part of each received message. These "credentials" are the process ID of the remote peer, and the user
194/// ID and group ID that the process is running as.
195///
196/// In some cases, this information can be useful for identifying the remote peer and enriching the received data in an
197/// automatic way.
198#[cfg(unix)]
199#[derive(Clone)]
200pub struct ProcessCredentials {
201    /// Process ID of the remote peer.
202    pub pid: i32,
203
204    /// User ID of the remote peer process.
205    pub uid: u32,
206
207    /// Group ID of the remote peer process.
208    pub gid: u32,
209}
210
211/// Connection address.
212///
213/// A generic representation of the address of a remote peer. This can either be a typical socket address (used for
214/// IPv4/IPv6), or potentially the process credentials of a Unix domain socket connection.
215#[derive(Clone)]
216pub enum ConnectionAddress {
217    /// A socket-like address.
218    SocketLike(SocketAddr),
219
220    /// A process-like address.
221    #[cfg(unix)]
222    ProcessLike(Option<ProcessCredentials>),
223}
224
225impl fmt::Display for ConnectionAddress {
226    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
227        match self {
228            Self::SocketLike(addr) => write!(f, "{}", addr),
229            #[cfg(unix)]
230            Self::ProcessLike(maybe_creds) => match maybe_creds {
231                None => write!(f, "<unbound>"),
232                Some(creds) => write!(f, "<pid={} uid={} gid={}>", creds.pid, creds.uid, creds.gid),
233            },
234        }
235    }
236}
237
238impl From<SocketAddr> for ConnectionAddress {
239    fn from(value: SocketAddr) -> Self {
240        Self::SocketLike(value)
241    }
242}
243
244#[cfg(unix)]
245impl From<ProcessCredentials> for ConnectionAddress {
246    fn from(creds: ProcessCredentials) -> Self {
247        Self::ProcessLike(Some(creds))
248    }
249}
250
251impl<'a> Connected<&'a Connection> for ConnectionAddress {
252    fn connect_info(target: &'a Connection) -> Self {
253        target.remote_addr()
254    }
255}
256
257/// A gRPC target address.
258///
259/// This represents the address of a gRPC server that can be connected to. `GrpcTargetAddress` exposes a `Display`
260/// implementation that emits the target address following the rules of the [gRPC Name
261/// Resolution][grpc_name_resolution_docs] documentation.
262///
263/// Only connection-oriented transports are supported: TCP and Unix domain sockets in SOCK_STREAM mode.
264///
265/// [grpc_name_resolution_docs]: https://github.com/grpc/grpc/blob/master/doc/naming.md
266pub enum GrpcTargetAddress {
267    Tcp(SocketAddr),
268    Unix(PathBuf),
269}
270
271impl GrpcTargetAddress {
272    /// Creates a new `GrpcTargetAddress` from the given `ListenAddress`.
273    ///
274    /// For TCP addresses, this method converts unspecified addresses (`0.0.0.0` or `::`) to localhost
275    /// (`127.0.0.1` or `::1`) to ensure the advertised address matches TLS certificates.
276    ///
277    /// Returns `None` if the listen address is not a connection-oriented transport.
278    pub fn try_from_listen_addr(listen_address: &ListenAddress) -> Option<Self> {
279        match listen_address {
280            ListenAddress::Tcp(_) => {
281                // For TCP, convert 0.0.0.0 to 127.0.0.1 to match TLS certificate
282                listen_address.as_local_connect_addr().map(GrpcTargetAddress::Tcp)
283            }
284            ListenAddress::Unix(path) => Some(GrpcTargetAddress::Unix(path.clone())),
285            _ => None,
286        }
287    }
288}
289
290impl fmt::Display for GrpcTargetAddress {
291    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
292        match self {
293            GrpcTargetAddress::Tcp(addr) => write!(f, "{}", addr),
294            GrpcTargetAddress::Unix(path) => write!(f, "unix://{}", path.display()),
295        }
296    }
297}
298
299#[cfg(test)]
300mod tests {
301    use super::*;
302
303    #[test]
304    fn test_as_local_connect_addr() {
305        let tcp_any_addr = ListenAddress::try_from("tcp://0.0.0.0:1234").unwrap();
306        assert_eq!(
307            tcp_any_addr.as_local_connect_addr(),
308            Some(SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 1234)))
309        );
310
311        let tcp_localhost_addr = ListenAddress::try_from("tcp://127.0.0.1:2345").unwrap();
312        assert_eq!(
313            tcp_localhost_addr.as_local_connect_addr(),
314            Some(SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 2345)))
315        );
316
317        let tcp_private_addr = ListenAddress::try_from("tcp://192.168.10.2:3456").unwrap();
318        assert_eq!(
319            tcp_private_addr.as_local_connect_addr(),
320            Some(SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(192, 168, 10, 2), 3456)))
321        );
322
323        let udp_any_addr = ListenAddress::try_from("udp://0.0.0.0:4567").unwrap();
324        assert_eq!(
325            udp_any_addr.as_local_connect_addr(),
326            Some(SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 4567)))
327        );
328
329        let udp_localhost_addr = ListenAddress::try_from("udp://127.0.0.1:5678").unwrap();
330        assert_eq!(
331            udp_localhost_addr.as_local_connect_addr(),
332            Some(SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 5678)))
333        );
334
335        let udp_private_addr = ListenAddress::try_from("udp://192.168.10.2:6789").unwrap();
336        assert_eq!(
337            udp_private_addr.as_local_connect_addr(),
338            Some(SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(192, 168, 10, 2), 6789)))
339        );
340    }
341}