use std::net::{IpAddr, SocketAddr, ToSocketAddrs};
use minip2p_core::{Multiaddr, PeerAddr, Protocol};
use minip2p_transport::TransportError;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum Family {
V4,
V6,
}
impl Family {
fn of(addr: SocketAddr) -> Self {
match addr {
SocketAddr::V4(_) => Self::V4,
SocketAddr::V6(_) => Self::V6,
}
}
}
pub(crate) fn targets(addr: &PeerAddr) -> Result<Vec<(Family, PeerAddr)>, TransportError> {
let protocols = addr.transport().protocols();
let Some(host) = protocols.first() else {
return Err(invalid("dial target has no host component"));
};
let rest = &protocols[1..];
let (name, filter) = match host {
Protocol::Ip4(_) | Protocol::Ip6(_) => {
let family = match host {
Protocol::Ip4(_) => Family::V4,
_ => Family::V6,
};
return Ok(vec![(family, addr.clone())]);
}
Protocol::Dns(name) => (name, None),
Protocol::Dns4(name) => (name, Some(Family::V4)),
Protocol::Dns6(name) => (name, Some(Family::V6)),
_ => return Err(invalid("dial target has no host component")),
};
let literal = name
.strip_prefix('[')
.and_then(|name| name.strip_suffix(']'))
.unwrap_or(name.as_str());
if literal.parse::<IpAddr>().is_ok() {
return Err(invalid(
"a /dns component must contain a DNS name, not an IP address",
));
}
let port = rest
.iter()
.find_map(|protocol| match protocol {
Protocol::Tcp(port) | Protocol::Udp(port) => Some(*port),
_ => None,
})
.ok_or_else(|| invalid("a dns dial target needs a /tcp or /udp port to resolve"))?;
let resolved = (name.as_str(), port)
.to_socket_addrs()
.map_err(|error| invalid(format!("dns resolution failed for {name}: {error}")))?;
let targets = rebuild(addr, rest, resolved, filter)?;
if targets.is_empty() {
return Err(invalid(format!(
"dns resolution returned no usable address for {name}"
)));
}
Ok(targets)
}
fn rebuild(
addr: &PeerAddr,
rest: &[Protocol],
resolved: impl IntoIterator<Item = SocketAddr>,
filter: Option<Family>,
) -> Result<Vec<(Family, PeerAddr)>, TransportError> {
let mut targets: Vec<(Family, PeerAddr)> = Vec::new();
for socket_addr in resolved {
let family = Family::of(socket_addr);
if filter.is_some_and(|wanted| wanted != family) {
continue;
}
if targets.iter().any(|(seen, _)| *seen == family) {
continue;
}
let mut expanded = vec![host_protocol(socket_addr.ip())];
expanded.extend_from_slice(rest);
let transport = Multiaddr::from_protocols(expanded);
let target = PeerAddr::new(transport, addr.peer_id().clone())
.map_err(|error| invalid(format!("resolved address was not a peer addr: {error}")))?;
targets.push((family, target));
}
Ok(targets)
}
fn host_protocol(ip: IpAddr) -> Protocol {
match ip {
IpAddr::V4(v4) => Protocol::Ip4(v4.octets()),
IpAddr::V6(v6) => Protocol::Ip6(v6.octets()),
}
}
fn invalid(reason: impl Into<String>) -> TransportError {
TransportError::InvalidAddress {
context: "dial target",
reason: reason.into(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use minip2p_identity::Ed25519Keypair;
fn peer_addr(text: &str) -> PeerAddr {
let transport: Multiaddr = text.parse().expect("test address parses");
PeerAddr::new(transport, Ed25519Keypair::generate().peer_id()).expect("peer addr")
}
#[test]
fn a_concrete_address_is_its_own_only_target() {
let addr = peer_addr("/ip4/198.51.100.7/udp/4001/quic-v1");
assert_eq!(targets(&addr).expect("targets"), vec![(Family::V4, addr)]);
let addr = peer_addr("/ip6/2001:db8::1/tcp/4001");
assert_eq!(targets(&addr).expect("targets"), vec![(Family::V6, addr)]);
}
#[test]
fn a_name_expands_to_one_target_per_family_keeping_the_shape() {
let addr = peer_addr("/dns/localhost/tcp/4001");
let expanded = targets(&addr).expect("targets");
assert!(
!expanded.is_empty(),
"localhost has to resolve to something"
);
for (family, target) in &expanded {
let protocols = target.transport().protocols();
assert!(
matches!(
(family, &protocols[0]),
(Family::V4, Protocol::Ip4(_)) | (Family::V6, Protocol::Ip6(_))
),
"the host is concrete and matches its family: {protocols:?}"
);
assert_eq!(
&protocols[1..],
&[Protocol::Tcp(4001)],
"everything after the host is the caller's, not the resolver's"
);
assert_eq!(target.peer_id(), addr.peer_id());
}
}
#[test]
fn a_dns_component_is_resolved_as_a_name_and_not_as_an_address() {
let addr = peer_addr("/dns/[::1]/tcp/4001");
assert!(
matches!(targets(&addr), Err(TransportError::InvalidAddress { .. })),
"an IP literal is not a name to look up"
);
let addr = peer_addr("/dns/127.0.0.1/tcp/4001");
assert!(
matches!(targets(&addr), Err(TransportError::InvalidAddress { .. })),
"an IPv4 literal is not a name to look up"
);
}
#[test]
fn a_family_is_dialed_once_however_many_addresses_it_answered_with() {
let addr = peer_addr("/dns/example.invalid/udp/4001/quic-v1");
let answers = vec![
"198.51.100.7:4001".parse().expect("v4"),
"198.51.100.8:4001".parse().expect("v4"),
"[2001:db8::1]:4001".parse().expect("v6"),
"[2001:db8::2]:4001".parse().expect("v6"),
];
let targets = rebuild(
&addr,
&[Protocol::Udp(4001), Protocol::QuicV1],
answers,
None,
)
.expect("rebuild");
assert_eq!(
targets
.iter()
.map(|(family, target)| (*family, target.transport().to_string()))
.collect::<Vec<_>>(),
vec![
(Family::V4, "/ip4/198.51.100.7/udp/4001/quic-v1".to_string()),
(Family::V6, "/ip6/2001:db8::1/udp/4001/quic-v1".to_string()),
],
"the first answer of each family, in the order they arrived"
);
}
#[test]
fn a_family_specific_name_stays_in_its_family() {
let addr = peer_addr("/dns4/localhost/udp/4001/quic-v1");
for (family, target) in targets(&addr).expect("targets") {
assert_eq!(family, Family::V4, "/dns4 must not produce an ipv6 dial");
assert!(matches!(
target.transport().protocols()[0],
Protocol::Ip4(_)
));
}
}
#[test]
fn a_name_without_a_port_cannot_be_resolved() {
let addr = peer_addr("/dns/localhost");
let error = targets(&addr).expect_err("no port");
assert!(
matches!(&error, TransportError::InvalidAddress { reason, .. } if reason.contains("port")),
"got {error:?}"
);
}
}