use std::{
borrow::Cow,
collections::{BTreeSet, HashSet},
fmt::{self, Display},
hash::Hash,
net::SocketAddr,
str::FromStr,
sync::Arc,
};
use iroh_base::{EndpointAddr, EndpointId, RelayUrl, SecretKey, TransportAddr};
pub use iroh_dns::attrs::{EncodingError, IROH_TXT_NAME, ParseError};
pub(crate) use iroh_dns::attrs::{IrohAttr, TxtAttrs};
use iroh_dns::pkarr;
use n0_error::{ensure, stack_error};
use url::Url;
#[derive(Debug, Clone, Default, Eq, PartialEq)]
pub struct EndpointData {
addrs: Vec<TransportAddr>,
user_data: Option<UserData>,
}
fn dedup<T: Eq + Hash + Clone>(items: &mut Vec<T>) -> HashSet<T> {
let mut seen = HashSet::new();
items.retain(|item| seen.insert(item.clone()));
seen
}
impl EndpointData {
pub fn new(mut addrs: Vec<TransportAddr>) -> Self {
dedup(&mut addrs);
Self {
addrs,
user_data: None,
}
}
pub fn with_user_data(mut self, user_data: UserData) -> Self {
self.user_data = Some(user_data);
self
}
pub fn add_relay_url(&mut self, relay_url: RelayUrl) {
let addr = TransportAddr::Relay(relay_url);
if !self.addrs.contains(&addr) {
self.addrs.push(addr);
}
}
pub fn add_ip_addrs(&mut self, addresses: Vec<SocketAddr>) {
self.add_addrs(addresses.into_iter().map(TransportAddr::Ip))
}
pub fn add_addrs(&mut self, addrs: impl IntoIterator<Item = TransportAddr>) {
let mut addr_set = dedup(&mut self.addrs);
for addr in addrs.into_iter() {
if !addr_set.contains(&addr) {
self.addrs.push(addr.clone());
addr_set.insert(addr);
}
}
}
pub fn set_user_data(&mut self, user_data: Option<UserData>) {
self.user_data = user_data;
}
pub fn clear_ip_addrs(&mut self) {
self.addrs
.retain(|addr| !matches!(addr, TransportAddr::Ip(_)));
}
pub fn clear_relay_urls(&mut self) {
self.addrs
.retain(|addr| !matches!(addr, TransportAddr::Relay(_)));
}
pub fn relay_urls(&self) -> impl Iterator<Item = &RelayUrl> {
self.addrs.iter().filter_map(|addr| match addr {
TransportAddr::Relay(url) => Some(url),
_ => None,
})
}
pub fn user_data(&self) -> Option<&UserData> {
self.user_data.as_ref()
}
pub fn ip_addrs(&self) -> impl Iterator<Item = &SocketAddr> {
self.addrs.iter().filter_map(|addr| match addr {
TransportAddr::Ip(addr) => Some(addr),
_ => None,
})
}
pub fn addrs(&self) -> impl Iterator<Item = &TransportAddr> {
self.addrs.iter()
}
pub fn has_addrs(&self) -> bool {
!self.addrs.is_empty()
}
pub fn filtered_addrs(&self, filter: &AddrFilter) -> Cow<'_, Vec<TransportAddr>> {
filter.apply(&self.addrs)
}
pub fn apply_filter(&self, filter: &AddrFilter) -> Cow<'_, Self> {
match self.filtered_addrs(filter) {
Cow::Borrowed(_) => Cow::Borrowed(self),
Cow::Owned(addrs) => {
let mut data = EndpointData::new(addrs);
data.set_user_data(self.user_data.clone());
Cow::Owned(data)
}
}
}
}
impl From<BTreeSet<TransportAddr>> for EndpointData {
fn from(addrs: BTreeSet<TransportAddr>) -> Self {
Self {
addrs: addrs.into_iter().collect(),
user_data: None,
}
}
}
impl From<BTreeSet<SocketAddr>> for EndpointData {
fn from(addrs: BTreeSet<SocketAddr>) -> Self {
Self {
addrs: addrs.into_iter().map(TransportAddr::Ip).collect(),
user_data: None,
}
}
}
impl FromIterator<TransportAddr> for EndpointData {
fn from_iter<T: IntoIterator<Item = TransportAddr>>(iter: T) -> Self {
Self::new(iter.into_iter().collect())
}
}
type AddrFilterFn =
dyn Fn(&Vec<TransportAddr>) -> Cow<'_, Vec<TransportAddr>> + Send + Sync + 'static;
#[derive(Clone, Default)]
pub struct AddrFilter(Option<Arc<AddrFilterFn>>);
impl std::fmt::Debug for AddrFilter {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.0.is_some() {
f.debug_struct("AddrFilter").finish_non_exhaustive()
} else {
write!(f, "identity")
}
}
}
impl AddrFilter {
pub fn new(
f: impl Fn(&Vec<TransportAddr>) -> Cow<'_, Vec<TransportAddr>> + Send + Sync + 'static,
) -> Self {
Self(Some(Arc::new(f)))
}
pub fn unfiltered() -> Self {
Self::new(|addrs| Cow::Borrowed(addrs))
}
pub fn relay_only() -> Self {
Self::new(|addrs| Cow::Owned(addrs.iter().filter(|a| a.is_relay()).cloned().collect()))
}
pub fn ip_only() -> Self {
Self::new(|addrs| Cow::Owned(addrs.iter().filter(|a| !a.is_relay()).cloned().collect()))
}
pub fn apply<'a>(&self, addrs: &'a Vec<TransportAddr>) -> Cow<'a, Vec<TransportAddr>> {
match &self.0 {
Some(f) => f(addrs),
None => Cow::Borrowed(addrs),
}
}
}
impl From<EndpointAddr> for EndpointData {
fn from(endpoint_addr: EndpointAddr) -> Self {
Self {
addrs: endpoint_addr.addrs.into_iter().collect(),
user_data: None,
}
}
}
#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct UserData(String);
impl UserData {
pub const MAX_LENGTH: usize = 245;
}
#[allow(missing_docs)]
#[stack_error(derive, add_meta)]
#[error("max length exceeded")]
pub struct MaxLengthExceededError {}
impl TryFrom<String> for UserData {
type Error = MaxLengthExceededError;
fn try_from(value: String) -> Result<Self, Self::Error> {
ensure!(value.len() <= Self::MAX_LENGTH, MaxLengthExceededError);
Ok(Self(value))
}
}
impl FromStr for UserData {
type Err = MaxLengthExceededError;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
ensure!(s.len() <= Self::MAX_LENGTH, MaxLengthExceededError);
Ok(Self(s.to_string()))
}
}
impl fmt::Display for UserData {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl AsRef<str> for UserData {
fn as_ref(&self) -> &str {
&self.0
}
}
#[derive(derive_more::Debug, Clone, Eq, PartialEq)]
pub struct EndpointInfo {
pub endpoint_id: EndpointId,
pub data: EndpointData,
}
impl From<EndpointInfo> for EndpointAddr {
fn from(value: EndpointInfo) -> Self {
value.into_endpoint_addr()
}
}
impl From<EndpointAddr> for EndpointInfo {
fn from(addr: EndpointAddr) -> Self {
Self {
endpoint_id: addr.id,
data: EndpointData::from(addr.addrs),
}
}
}
impl EndpointInfo {
pub fn new(endpoint_id: EndpointId) -> Self {
Self::from_parts(endpoint_id, Default::default())
}
pub fn from_parts(endpoint_id: EndpointId, data: EndpointData) -> Self {
Self { endpoint_id, data }
}
pub fn with_relay_url(mut self, relay_url: RelayUrl) -> Self {
self.data.add_relay_url(relay_url);
self
}
pub fn with_ip_addrs(mut self, addrs: Vec<SocketAddr>) -> Self {
self.data.add_ip_addrs(addrs);
self
}
pub fn with_user_data(mut self, user_data: Option<UserData>) -> Self {
self.data.set_user_data(user_data);
self
}
pub fn to_endpoint_addr(&self) -> EndpointAddr {
EndpointAddr {
id: self.endpoint_id,
addrs: self.data.addrs.iter().cloned().collect(),
}
}
pub fn into_endpoint_addr(self) -> EndpointAddr {
let Self { endpoint_id, data } = self;
EndpointAddr {
id: endpoint_id,
addrs: data.addrs.into_iter().collect(),
}
}
pub(crate) fn to_attrs(&self) -> TxtAttrs<IrohAttr> {
endpoint_info_to_attrs(self)
}
pub fn addrs(&self) -> impl Iterator<Item = &TransportAddr> {
self.data.addrs()
}
pub fn relay_urls(&self) -> impl Iterator<Item = &RelayUrl> {
self.data.relay_urls()
}
pub fn user_data(&self) -> Option<&UserData> {
self.data.user_data()
}
pub fn ip_addrs(&self) -> impl Iterator<Item = &SocketAddr> {
self.data.ip_addrs()
}
pub fn from_txt_lookup(
domain_name: String,
lookup: impl Iterator<Item = impl Display>,
) -> Result<Self, ParseError> {
let attrs: TxtAttrs<IrohAttr> = TxtAttrs::from_txt_lookup(domain_name, lookup)?;
Ok(endpoint_info_from_attrs(&attrs))
}
pub fn from_pkarr_signed_packet(packet: &pkarr::SignedPacket) -> Result<Self, ParseError> {
let attrs: TxtAttrs<IrohAttr> = TxtAttrs::from_pkarr_signed_packet(packet)?;
Ok(endpoint_info_from_attrs(&attrs))
}
pub fn to_pkarr_signed_packet(
&self,
secret_key: &SecretKey,
ttl: u32,
) -> Result<pkarr::SignedPacket, EncodingError> {
self.to_attrs().to_pkarr_signed_packet(secret_key, ttl)
}
pub fn to_txt_strings(&self) -> Vec<String> {
self.to_attrs().to_txt_strings().collect()
}
}
fn endpoint_info_to_attrs(info: &EndpointInfo) -> TxtAttrs<IrohAttr> {
let mut attrs = vec![];
for addr in &info.data.addrs {
match addr {
TransportAddr::Relay(url) => attrs.push((IrohAttr::Relay, url.to_string())),
TransportAddr::Ip(addr) => attrs.push((IrohAttr::Addr, addr.to_string())),
TransportAddr::Custom(addr) => attrs.push((IrohAttr::Addr, addr.to_string())),
_ => {}
}
}
if let Some(user_data) = &info.data.user_data {
attrs.push((IrohAttr::UserData, user_data.to_string()));
}
TxtAttrs::from_parts(info.endpoint_id, attrs.into_iter())
}
fn endpoint_info_from_attrs(attrs: &TxtAttrs<IrohAttr>) -> EndpointInfo {
use iroh_base::CustomAddr;
let endpoint_id = attrs.endpoint_id();
let a = attrs.attrs();
let relay_urls = a
.get(&IrohAttr::Relay)
.into_iter()
.flatten()
.filter_map(|s| Url::parse(s).ok())
.map(|url| TransportAddr::Relay(url.into()));
let addrs = a
.get(&IrohAttr::Addr)
.into_iter()
.flatten()
.filter_map(|s| {
if let Ok(addr) = SocketAddr::from_str(s) {
Some(TransportAddr::Ip(addr))
} else if let Ok(addr) = CustomAddr::from_str(s) {
Some(TransportAddr::Custom(addr))
} else {
None
}
});
let user_data = a
.get(&IrohAttr::UserData)
.into_iter()
.flatten()
.next()
.and_then(|s| UserData::from_str(s).ok());
let mut data = EndpointData::default();
data.set_user_data(user_data);
data.add_addrs(relay_urls.chain(addrs));
EndpointInfo { endpoint_id, data }
}
#[cfg(test)]
mod tests {
use std::str::FromStr;
use hickory_resolver::{
lookup::Lookup,
proto::{
op::Query,
rr::{
Name, RData, Record, RecordType,
rdata::{A, TXT},
},
},
};
use iroh_base::{EndpointId, SecretKey, TransportAddr};
use n0_error::{Result, StdResultExt};
use super::{EndpointData, EndpointInfo};
use crate::dns::TxtRecordData;
#[test]
fn txt_attr_roundtrip() {
let endpoint_data = EndpointData::from_iter([
TransportAddr::Relay("https://example.com".parse().unwrap()),
TransportAddr::Ip("127.0.0.1:1234".parse().unwrap()),
])
.with_user_data("foobar".parse().unwrap());
let endpoint_id = "vpnk377obfvzlipnsfbqba7ywkkenc4xlpmovt5tsfujoa75zqia"
.parse()
.unwrap();
let expected = EndpointInfo::from_parts(endpoint_id, endpoint_data);
let attrs = expected.to_attrs();
let actual = super::endpoint_info_from_attrs(&attrs);
assert_eq!(expected, actual);
}
#[test]
fn signed_packet_roundtrip() {
let secret_key =
SecretKey::from_str("vpnk377obfvzlipnsfbqba7ywkkenc4xlpmovt5tsfujoa75zqia").unwrap();
let endpoint_data = EndpointData::from_iter([
TransportAddr::Relay("https://example.com".parse().unwrap()),
TransportAddr::Ip("127.0.0.1:1234".parse().unwrap()),
])
.with_user_data("foobar".parse().unwrap());
let expected = EndpointInfo::from_parts(secret_key.public(), endpoint_data);
let packet = expected.to_pkarr_signed_packet(&secret_key, 30).unwrap();
let actual = EndpointInfo::from_pkarr_signed_packet(&packet).unwrap();
assert_eq!(expected, actual);
}
#[test]
fn txt_attr_roundtrip_with_custom_addr() {
use iroh_base::CustomAddr;
let bt_addr = CustomAddr::from_parts(1, &[0xa1, 0xb2, 0xc3, 0xd4, 0xe5, 0xf6]);
let tor_addr = CustomAddr::from_parts(42, &[0xab; 32]);
let endpoint_data = EndpointData::from_iter([
TransportAddr::Relay("https://example.com".parse().unwrap()),
TransportAddr::Ip("127.0.0.1:1234".parse().unwrap()),
TransportAddr::Custom(bt_addr),
TransportAddr::Custom(tor_addr),
]);
let endpoint_id = "vpnk377obfvzlipnsfbqba7ywkkenc4xlpmovt5tsfujoa75zqia"
.parse()
.unwrap();
let expected = EndpointInfo::from_parts(endpoint_id, endpoint_data);
let attrs = expected.to_attrs();
let actual = super::endpoint_info_from_attrs(&attrs);
assert_eq!(expected, actual);
}
#[test]
fn signed_packet_roundtrip_with_custom_addr() {
use iroh_base::CustomAddr;
let secret_key =
SecretKey::from_str("vpnk377obfvzlipnsfbqba7ywkkenc4xlpmovt5tsfujoa75zqia").unwrap();
let bt_addr = CustomAddr::from_parts(1, &[0xa1, 0xb2, 0xc3, 0xd4, 0xe5, 0xf6]);
let tor_addr = CustomAddr::from_parts(42, &[0xab; 32]);
let endpoint_data = EndpointData::from_iter([
TransportAddr::Relay("https://example.com".parse().unwrap()),
TransportAddr::Ip("127.0.0.1:1234".parse().unwrap()),
TransportAddr::Custom(bt_addr),
TransportAddr::Custom(tor_addr),
])
.with_user_data("foobar".parse().unwrap());
let expected = EndpointInfo::from_parts(secret_key.public(), endpoint_data);
let packet = expected.to_pkarr_signed_packet(&secret_key, 30).unwrap();
let actual = EndpointInfo::from_pkarr_signed_packet(&packet).unwrap();
assert_eq!(expected, actual);
}
#[test]
fn test_from_hickory_lookup() -> Result {
let name = Name::from_utf8(
"_iroh.dgjpkxyn3zyrk3zfads5duwdgbqpkwbjxfj4yt7rezidr3fijccy.dns.iroh.link.",
)
.std_context("dns name")?;
let query = Query::query(name.clone(), RecordType::TXT);
let records = [
Record::from_rdata(
name.clone(),
30,
RData::TXT(TXT::new(vec!["addr=192.168.96.145:60165".to_string()])),
),
Record::from_rdata(
name.clone(),
30,
RData::TXT(TXT::new(vec!["addr=213.208.157.87:60165".to_string()])),
),
Record::from_rdata(name.clone(), 30, RData::A(A::new(127, 0, 0, 1))),
Record::from_rdata(
{
let other_id = EndpointId::from_str(
"a55f26132e5e43de834d534332f66a20d480c3e50a13a312a071adea6569981e",
)?;
Name::from_utf8(format!("_iroh.{}.dns.iroh.link.", other_id.to_z32()))
}
.std_context("name")?,
30,
RData::TXT(TXT::new(vec![
"relay=https://euw1-1.relay.iroh.network./".to_string(),
])),
),
Record::from_rdata(
Name::from_utf8("dns.iroh.link.").std_context("name")?,
30,
RData::TXT(TXT::new(vec![
"relay=https://euw1-1.relay.iroh.network./".to_string(),
])),
),
Record::from_rdata(
name.clone(),
30,
RData::TXT(TXT::new(vec![
"relay=https://euw1-1.relay.iroh.network./".to_string(),
])),
),
];
let lookup = Lookup::new_with_max_ttl(query, records);
let lookup = lookup
.answers()
.iter()
.filter_map(|record| match &record.data {
RData::TXT(txt) => Some(TxtRecordData::from(txt.txt_data.to_vec())),
_ => None,
});
let endpoint_info = EndpointInfo::from_txt_lookup(name.to_string(), lookup)?;
let expected_endpoint_info = EndpointInfo::new(EndpointId::from_str(
"1992d53c02cdc04566e5c0edb1ce83305cd550297953a047a445ea3264b54b18",
)?)
.with_relay_url("https://euw1-1.relay.iroh.network./".parse()?)
.with_ip_addrs(vec![
"192.168.96.145:60165".parse().unwrap(),
"213.208.157.87:60165".parse().unwrap(),
]);
assert_eq!(endpoint_info, expected_endpoint_info);
Ok(())
}
}