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