oa_gateway/addr.rs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90
//! Resolves configured `host:port` strings to [`std::net::SocketAddr`].
//!
//! Adapters take a resolved address, not a string, so a bad name fails at
//! startup instead of when the first connection is attempted.
use std::net::SocketAddr;
use tracing::info;
/// Resolves a configured address string to a socket address.
///
/// Accepts a literal address or a `host:port` that needs a lookup, so
/// `localhost:9000` works as naturally as `127.0.0.1:9000`.
///
/// When a name offers both families, IPv4 wins. `localhost` resolves to
/// `::1` first on some hosts, and binding there alone would refuse the
/// documented `127.0.0.1:9000` clients.
///
/// `key` is the config field name used in error messages and the
/// resolution log line, not part of the lookup.
///
/// # Errors
///
/// Returns an error if `value` is not a usable address, the lookup fails,
/// or the name resolves to no addresses.
pub(crate) async fn resolve_addr(key: &str, value: &str) -> Result<SocketAddr, String> {
if let Ok(addr) = value.parse::<SocketAddr>() {
return Ok(addr);
}
let candidates: Vec<SocketAddr> = tokio::net::lookup_host(value)
.await
.map_err(|err| {
format!("{key} = \"{value}\" is not a usable address ({err}). Expected host:port, such as 127.0.0.1:9000.")
})?
.collect();
let addr = candidates
.iter()
.find(|addr| addr.is_ipv4())
.or_else(|| candidates.first())
.copied()
.ok_or_else(|| format!("{key} = \"{value}\" resolved to no addresses"))?;
info!(%key, value, %addr, "resolved address");
Ok(addr)
}
/// The host part of a configured `host:port`, without the port.
///
/// TLS server-name verification checks the name the operator configured,
/// not whatever [`resolve_addr`] resolved it to — reversing a resolved
/// [`SocketAddr`] back to a name would be both wrong (it is not
/// necessarily the name that was configured) and impossible for a
/// round-robin DNS record. Handles the bracketed IPv6 form: `[::1]:61613`
/// is `::1`.
pub(crate) fn host_part(value: &str) -> &str {
if let Some(rest) = value.strip_prefix('[') {
if let Some(end) = rest.find(']') {
return &rest[..end];
}
}
match value.rfind(':') {
Some(idx) => &value[..idx],
None => value,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_hostname_and_port_split_on_the_last_colon() {
assert_eq!(host_part("broker.example.com:61613"), "broker.example.com");
}
#[test]
fn an_ipv4_literal_and_port_split_the_same_way() {
assert_eq!(host_part("127.0.0.1:61613"), "127.0.0.1");
}
#[test]
fn a_bracketed_ipv6_literal_loses_its_brackets_and_port() {
assert_eq!(host_part("[::1]:61613"), "::1");
assert_eq!(host_part("[2001:db8::1]:61613"), "2001:db8::1");
}
#[test]
fn a_value_with_no_port_is_returned_whole() {
assert_eq!(host_part("broker.example.com"), "broker.example.com");
}
}