use std::net::IpAddr;
use super::error::{DnsError, TcpEndpointError};
use super::runtime::{install_from_env, lookup_ip, source};
pub fn parse_tcp_host_port(endpoint: &str) -> Result<(String, u16), TcpEndpointError> {
let rest = endpoint
.strip_prefix("tcp://")
.ok_or_else(|| TcpEndpointError::NotTcp(endpoint.to_string()))?;
if rest.is_empty() {
return Err(TcpEndpointError::Syntax(endpoint.to_string()));
}
if rest.starts_with('[') {
let close = rest
.find(']')
.ok_or_else(|| TcpEndpointError::Syntax(endpoint.to_string()))?;
let host = rest[1..close].to_string();
let port_str = rest[close + 1..]
.strip_prefix(':')
.ok_or_else(|| TcpEndpointError::Syntax(endpoint.to_string()))?;
let port = parse_port(port_str, endpoint)?;
return Ok((host, port));
}
let colon = rest
.rfind(':')
.ok_or_else(|| TcpEndpointError::Syntax(endpoint.to_string()))?;
if colon == 0 {
return Err(TcpEndpointError::Syntax(endpoint.to_string()));
}
let host = rest[..colon].to_string();
let port = parse_port(&rest[colon + 1..], endpoint)?;
Ok((host, port))
}
fn parse_port(raw: &str, endpoint: &str) -> Result<u16, TcpEndpointError> {
raw.parse::<u16>()
.map_err(|_| TcpEndpointError::Syntax(endpoint.to_string()))
}
pub fn host_is_literal(host: &str) -> bool {
if host.is_empty() || host == "*" {
return true;
}
host.parse::<IpAddr>().is_ok()
}
pub fn format_tcp_endpoint(host: &str, port: u16) -> String {
if host.parse::<IpAddr>().is_ok_and(|ip| ip.is_ipv6()) {
format!("tcp://[{host}]:{port}")
} else {
format!("tcp://{host}:{port}")
}
}
pub async fn resolve_tcp_connect(endpoint: &str) -> Result<String, DnsError> {
let (host, port) = parse_tcp_host_port(endpoint)?;
if host_is_literal(&host) {
return Ok(format_tcp_endpoint(&host, port));
}
install_from_env()?;
let fqdn = if host.ends_with('.') {
host.clone()
} else {
format!("{host}.")
};
let ips = lookup_ip(&fqdn).await?;
let ip = ips.first().copied().ok_or(TcpEndpointError::NoAddresses)?;
Ok(format_tcp_endpoint(&ip.to_string(), port))
}
pub async fn resolve_tcp_connect_all(endpoint: &str) -> Result<Vec<String>, DnsError> {
let (host, port) = parse_tcp_host_port(endpoint)?;
if host_is_literal(&host) {
return Ok(vec![format_tcp_endpoint(&host, port)]);
}
install_from_env()?;
let fqdn = if host.ends_with('.') {
host.clone()
} else {
format!("{host}.")
};
let ips = lookup_ip(&fqdn).await?;
if ips.is_empty() {
return Err(TcpEndpointError::NoAddresses.into());
}
Ok(ips
.into_iter()
.map(|ip| format_tcp_endpoint(&ip.to_string(), port))
.collect())
}
pub async fn log_aether_connect_resolution() {
let Some(raw) = std::env::var("NAUTALID_AETHER_CONNECT")
.ok()
.filter(|s| !s.trim().is_empty())
else {
return;
};
match resolve_tcp_connect(raw.trim()).await {
Ok(resolved) => {
tracing::info!(
configured = %raw.trim(),
%resolved,
source = ?source(),
"hickory resolved Aether wire endpoint"
);
}
Err(e) => {
tracing::warn!(configured = %raw.trim(), %e, "Aether wire DNS lookup failed (non-fatal)")
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_ipv4_tcp() {
let (host, port) = parse_tcp_host_port("tcp://10.0.0.5:5555").unwrap();
assert_eq!(host, "10.0.0.5");
assert_eq!(port, 5555);
}
#[test]
fn parses_bracket_ipv6_tcp() {
let (host, port) = parse_tcp_host_port("tcp://[2001:db8::1]:5555").unwrap();
assert_eq!(host, "2001:db8::1");
assert_eq!(port, 5555);
}
#[test]
fn parses_domain_tcp() {
let (host, port) = parse_tcp_host_port("tcp://rustis.internal:5555").unwrap();
assert_eq!(host, "rustis.internal");
assert_eq!(port, 5555);
}
#[test]
fn literal_host_skips_lookup() {
assert!(host_is_literal("10.0.0.5"));
assert!(host_is_literal("*"));
assert!(!host_is_literal("rustis.internal"));
}
#[tokio::test]
async fn literal_endpoint_unchanged() {
let out = resolve_tcp_connect("tcp://127.0.0.1:5555").await.unwrap();
assert_eq!(out, "tcp://127.0.0.1:5555");
}
}