diff --git a/pubky-homeserver/src/config.rs b/pubky-homeserver/src/config.rs index 55f015c..b855111 100644 --- a/pubky-homeserver/src/config.rs +++ b/pubky-homeserver/src/config.rs @@ -21,7 +21,7 @@ pub struct Config { testnet: bool, port: Option, bootstrap: Option>, - domain: String, + domain: Option, /// Path to the storage directory /// /// Defaults to a directory in the OS data directory @@ -98,8 +98,8 @@ impl Config { self.bootstrap.to_owned() } - pub fn domain(&self) -> &str { - &self.domain + pub fn domain(&self) -> Option<&String> { + self.domain.as_ref() } /// Get the path to the storage directory @@ -131,7 +131,7 @@ impl Default for Config { testnet: false, port: Some(0), bootstrap: None, - domain: "localhost".to_string(), + domain: None, storage: None, secret_key: None, dht_request_timeout: None, @@ -168,6 +168,7 @@ impl Debug for Config { .entry(&"port", &self.port()) .entry(&"storage", &self.storage()) .entry(&"public_key", &self.keypair().public_key()) + .entry(&"domain", &self.domain()) .finish() } } diff --git a/pubky-homeserver/src/config.toml b/pubky-homeserver/src/config.toml index 2012efc..cb47003 100644 --- a/pubky-homeserver/src/config.toml +++ b/pubky-homeserver/src/config.toml @@ -1,10 +1,14 @@ # Use testnet network (local DHT) for testing. -testnet = false +# testnet = false + # Secret key (in hex) to generate the Homeserver's Keypair -secret_key = "0000000000000000000000000000000000000000000000000000000000000000" -# Domain to be published in Pkarr records for this server to be accessible by. -domain = "localhost" +# secret_key = "0000000000000000000000000000000000000000000000000000000000000000" + +# ICANN domain pointing to this server to allow browsers to connect to it. +# domain = "example.com" + # Port for the Homeserver to listen on. port = 6287 + # Storage directory Defaults to # storage = "" diff --git a/pubky-homeserver/src/pkarr.rs b/pubky-homeserver/src/pkarr.rs index 1a680cd..a4dac80 100644 --- a/pubky-homeserver/src/pkarr.rs +++ b/pubky-homeserver/src/pkarr.rs @@ -1,40 +1,59 @@ //! Pkarr related task +use std::net::Ipv4Addr; + use pkarr::{ - dns::{rdata::SVCB, Packet}, + dns::{ + rdata::{RData, A, SVCB}, + Packet, + }, Keypair, PkarrClientAsync, SignedPacket, }; pub async fn publish_server_packet( pkarr_client: PkarrClientAsync, keypair: &Keypair, - domain: &str, + domain: Option<&String>, port: u16, ) -> anyhow::Result<()> { // TODO: Try to resolve first before publishing. let mut packet = Packet::new_reply(0); - let mut svcb = SVCB::new(0, domain.try_into()?); + let default = ".".to_string(); + let target = domain.unwrap_or(&default); + let mut svcb = SVCB::new(0, target.as_str().try_into()?); - // Publishing port only for localhost domain, - // assuming any other domain will point to a reverse proxy - // at the conventional ports. - if domain == "localhost" { - svcb.priority = 1; - svcb.set_port(port); - - // TODO: Add more parameteres like the signer key! - // svcb.set_param(key, value) - }; + svcb.priority = 1; + svcb.set_port(port); packet.answers.push(pkarr::dns::ResourceRecord::new( "@".try_into().unwrap(), pkarr::dns::CLASS::IN, 60 * 60, - pkarr::dns::rdata::RData::HTTPS(svcb.clone().into()), + RData::HTTPS(svcb.clone().into()), )); + if domain.is_none() { + // TODO: remove after remvoing Pubky shared/public + // and add local host IP address instead. + svcb.target = "localhost".try_into().unwrap(); + + packet.answers.push(pkarr::dns::ResourceRecord::new( + "@".try_into().unwrap(), + pkarr::dns::CLASS::IN, + 60 * 60, + RData::HTTPS(svcb.clone().into()), + )); + + packet.answers.push(pkarr::dns::ResourceRecord::new( + "@".try_into().unwrap(), + pkarr::dns::CLASS::IN, + 60 * 60, + RData::A(A::from(Ipv4Addr::from([127, 0, 0, 1]))), + )); + } + // TODO: announce A/AAAA records as well for TLS connections? let signed_packet = SignedPacket::from_packet(keypair, &packet)?; diff --git a/pubky/src/native.rs b/pubky/src/native.rs index b6b1522..e5406de 100644 --- a/pubky/src/native.rs +++ b/pubky/src/native.rs @@ -8,7 +8,7 @@ use crate::PubkyClient; mod api; mod internals; -use internals::endpoints::PkarrResolver; +use internals::resolver::PkarrResolver; static DEFAULT_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),); @@ -55,7 +55,7 @@ impl PubkyClientBuilder { // TODO: convert to Result let pkarr = PkarrClient::new(self.pkarr_settings).unwrap().as_async(); - let dns_resolver = PkarrResolver::new(pkarr.clone()); + let dns_resolver: PkarrResolver = pkarr.clone().into(); PubkyClient { http: reqwest::Client::builder() diff --git a/pubky/src/native/internals.rs b/pubky/src/native/internals.rs index 822ecbb..9f3ae40 100644 --- a/pubky/src/native/internals.rs +++ b/pubky/src/native/internals.rs @@ -6,7 +6,8 @@ use url::Url; use crate::error::Result; use crate::PubkyClient; -pub mod endpoints; +mod endpoints; +pub mod resolver; impl PubkyClient { // === Pkarr === diff --git a/pubky/src/native/internals/endpoints.rs b/pubky/src/native/internals/endpoints.rs index b650e75..8d30d30 100644 --- a/pubky/src/native/internals/endpoints.rs +++ b/pubky/src/native/internals/endpoints.rs @@ -1,162 +1,130 @@ -use std::net::ToSocketAddrs; +use std::net::{IpAddr, SocketAddr, ToSocketAddrs}; use pkarr::dns::rdata::{RData, SVCB}; -use pkarr::{PkarrClientAsync, PublicKey, SignedPacket}; -use reqwest::dns::{Addrs, Resolve}; - -use crate::error::{Error, Result}; - -const MAX_CHAIN_LENGTH: u8 = 3; +use pkarr::dns::ResourceRecord; +use pkarr::SignedPacket; +use pubky_common::timestamp::Timestamp; #[derive(Debug, Clone)] -pub struct PkarrResolver { - pkarr: PkarrClientAsync, -} - -impl PkarrResolver { - pub fn new(pkarr: PkarrClientAsync) -> Self { - Self { pkarr } - } - - /// Resolve a `qname` to an alternative [Endpoint] as defined in [RFC9460](https://www.rfc-editor.org/rfc/rfc9460#name-terminology). - /// - /// A `qname` is can be either a regular domain name for HTTPS endpoints, - /// or it could use Attrleaf naming pattern for cusotm protcol. For example: - /// `_foo.example.com` for `foo://example.com`. - async fn resolve_endpoint(&self, qname: &str) -> Result { - let target = qname; - // TODO: cache the result of this function? - - let is_svcb = target.starts_with('_'); - - let mut step = 0; - let mut svcb: Option = None; - - loop { - let current = svcb.clone().map_or(target.to_string(), |s| s.target); - if let Ok(tld) = PublicKey::try_from(current.clone()) { - if let Ok(Some(signed_packet)) = self.pkarr.resolve(&tld).await { - if step >= MAX_CHAIN_LENGTH { - break; - }; - step += 1; - - // Choose most prior SVCB record - svcb = get_endpoint(&signed_packet, ¤t, is_svcb); - - // TODO: support wildcard? - } else { - break; - } - } else { - break; - } - } - - if let Some(svcb) = svcb { - if PublicKey::try_from(svcb.target.as_str()).is_err() { - return Ok(svcb); - } - } - - Err(Error::ResolveEndpoint(target.into())) - } -} - -impl Resolve for PkarrResolver { - fn resolve(&self, name: reqwest::dns::Name) -> reqwest::dns::Resolving { - let client = self.clone(); - - Box::pin(async move { - let name = name.as_str(); - - if PublicKey::try_from(name).is_ok() { - let x = client.resolve_endpoint(name).await?; - - let addrs = format!("{}:{}", x.target, x.port).to_socket_addrs()?; - - let addrs: Addrs = Box::new(addrs); - - return Ok(addrs); - }; - - Ok(Box::new(format!("{name}:0").to_socket_addrs().unwrap())) - }) - } -} - -#[derive(Debug, Clone)] -struct Endpoint { - target: String, +pub struct Endpoint { + pub(crate) target: String, // public_key: PublicKey, - port: u16, + pub(crate) port: u16, + pub(crate) addrs: Vec, } -fn get_endpoint(signed_packet: &SignedPacket, target: &str, is_svcb: bool) -> Option { - signed_packet - .resource_records(target) - .fold(None, |prev: Option, answer| { - if let Some(svcb) = match &answer.rdata { - RData::SVCB(svcb) => { - if is_svcb { - Some(svcb) - } else { - None - } - } - RData::HTTPS(curr) => { - if is_svcb { - None - } else { - Some(&curr.0) - } - } - _ => None, - } { - let curr = svcb.clone(); +impl Endpoint { + /// 1. Find the SVCB or HTTPS records with the lowest priority + /// 2. Choose a random one of the list of the above + /// 3. If the target is `.`, check A and AAAA records (https://www.rfc-editor.org/rfc/rfc9460#name-special-handling-of-in-targ) + pub(crate) fn find( + signed_packet: &SignedPacket, + target: &str, + is_svcb: bool, + ) -> Option { + let mut lowest_priority = u16::MAX; + let mut lowest_priority_index = 0; + let mut records = vec![]; - if curr.priority == 0 { - return Some(curr); - } - if let Some(prev) = &prev { - if curr.priority >= prev.priority { - return Some(curr); + for record in signed_packet.resource_records(target) { + if let Some(svcb) = get_svcb(record, is_svcb) { + match svcb.priority.cmp(&lowest_priority) { + std::cmp::Ordering::Equal => records.push(svcb), + std::cmp::Ordering::Less => { + lowest_priority_index = records.len(); + lowest_priority = svcb.priority; + records.push(svcb) + } + _ => {} + } + } + } + + // Good enough random selection + let now = Timestamp::now(); + let slice = &records[lowest_priority_index..]; + let index = if slice.is_empty() { + 0 + } else { + (now.into_inner() as usize) % slice.len() + }; + + slice.get(index).map(|s| { + let target = s.target.to_string(); + + let mut addrs: Vec = vec![]; + + if &target == "." { + for record in signed_packet.resource_records("@") { + match &record.rdata { + RData::A(ip) => addrs.push(IpAddr::V4(ip.address.into())), + RData::AAAA(ip) => addrs.push(IpAddr::V6(ip.address.into())), + _ => {} } - } else { - return Some(curr); } } - prev - }) - .map(|s| Endpoint { - target: s.target.to_string(), - // public_key: signed_packet.public_key(), - port: u16::from_be_bytes( - s.get_param(SVCB::PORT) - .unwrap_or_default() - .try_into() - .unwrap_or([0, 0]), - ), + Endpoint { + target, + // public_key: signed_packet.public_key(), + port: u16::from_be_bytes( + s.get_param(SVCB::PORT) + .unwrap_or_default() + .try_into() + .unwrap_or([0, 0]), + ), + addrs, + } }) + } + + pub fn to_socket_addrs(&self) -> std::io::Result> { + if self.target == "." { + let port = self.port; + return Ok(self + .addrs + .iter() + .map(|addr| SocketAddr::from((*addr, port))) + .collect::>() + .into_iter()); + } + + format!("{}:{}", self.target, self.port).to_socket_addrs() + } +} + +fn get_svcb<'a>(record: &'a ResourceRecord, is_svcb: bool) -> Option<&'a SVCB<'a>> { + match &record.rdata { + RData::SVCB(svcb) => { + if is_svcb { + Some(svcb) + } else { + None + } + } + + RData::HTTPS(curr) => { + if is_svcb { + None + } else { + Some(&curr.0) + } + } + _ => None, + } } #[cfg(test)] mod tests { + use std::net::{Ipv4Addr, Ipv6Addr}; + use std::str::FromStr; + use super::*; - use pkarr::dns::{self, rdata::RData}; - use pkarr::PkarrClient; - use pkarr::{mainline::Testnet, Keypair}; + + use pkarr::{dns, Keypair}; #[tokio::test] - async fn resolve_direct_endpoint() { - let testnet = Testnet::new(3); - let pkarr = PkarrClient::builder() - .testnet(&testnet) - .build() - .unwrap() - .as_async(); - + async fn endpoint_target() { let mut packet = dns::Packet::new_reply(0); packet.answers.push(dns::ResourceRecord::new( dns::Name::new("foo").unwrap(), @@ -185,83 +153,61 @@ mod tests { RData::SVCB(SVCB::new(0, "protocol.example.com".try_into().unwrap())), )); let keypair = Keypair::random(); - let inter_signed_packet = SignedPacket::from_packet(&keypair, &packet).unwrap(); - pkarr.publish(&inter_signed_packet).await.unwrap(); - - let resolver = PkarrResolver { pkarr }; + let signed_packet = SignedPacket::from_packet(&keypair, &packet).unwrap(); let tld = keypair.public_key(); // Follow foo.tld HTTPS records - let endpoint = resolver - .resolve_endpoint(&format!("foo.{tld}")) - .await - .unwrap(); + let endpoint = Endpoint::find(&signed_packet, &format!("foo.{tld}"), false).unwrap(); assert_eq!(endpoint.target, "https.example.com"); // Follow _foo.tld SVCB records - let endpoint = resolver - .resolve_endpoint(&format!("_foo.{tld}")) - .await - .unwrap(); + let endpoint = Endpoint::find(&signed_packet, &format!("_foo.{tld}"), true).unwrap(); assert_eq!(endpoint.target, "protocol.example.com"); } - #[tokio::test] - async fn resolve_endpoint_with_intermediate_pubky() { - let testnet = Testnet::new(3); - let pkarr = PkarrClient::builder() - .testnet(&testnet) - .build() - .unwrap() - .as_async(); - - // USER => Server Owner => Server - // pubky. => pubky-homeserver. => @. - + #[test] + fn endpoint_to_socket_addrs() { let mut packet = dns::Packet::new_reply(0); packet.answers.push(dns::ResourceRecord::new( dns::Name::new("@").unwrap(), dns::CLASS::IN, 3600, - RData::HTTPS(SVCB::new(0, "example.com".try_into().unwrap()).into()), + RData::A(Ipv4Addr::from_str("209.151.148.15").unwrap().into()), )); - let keypair = Keypair::random(); - let inter_signed_packet = SignedPacket::from_packet(&keypair, &packet).unwrap(); - pkarr.publish(&inter_signed_packet).await.unwrap(); - - let end_target = format!("{}", keypair.public_key()); - let mut packet = dns::Packet::new_reply(0); packet.answers.push(dns::ResourceRecord::new( - dns::Name::new("pubky-homeserver.").unwrap(), + dns::Name::new("@").unwrap(), dns::CLASS::IN, 3600, - RData::HTTPS(SVCB::new(0, end_target.as_str().try_into().unwrap()).into()), + RData::AAAA(Ipv6Addr::from_str("2a05:d014:275:6201::64").unwrap().into()), )); - let keypair = Keypair::random(); - let inter_signed_packet = SignedPacket::from_packet(&keypair, &packet).unwrap(); - pkarr.publish(&inter_signed_packet).await.unwrap(); - let inter_target = format!("pubky-homeserver.{}", keypair.public_key()); - let mut packet = dns::Packet::new_reply(0); + let mut svcb = SVCB::new(1, ".".try_into().unwrap()); + svcb.set_port(6881); + packet.answers.push(dns::ResourceRecord::new( - dns::Name::new("pubky.").unwrap(), + dns::Name::new("@").unwrap(), dns::CLASS::IN, 3600, - RData::HTTPS(SVCB::new(0, inter_target.as_str().try_into().unwrap()).into()), + RData::HTTPS(svcb.into()), )); let keypair = Keypair::random(); - let inter_signed_packet = SignedPacket::from_packet(&keypair, &packet).unwrap(); - pkarr.publish(&inter_signed_packet).await.unwrap(); + let signed_packet = SignedPacket::from_packet(&keypair, &packet).unwrap(); - let resolver = PkarrResolver { pkarr }; + // Follow foo.tld HTTPS records + let endpoint = Endpoint::find( + &signed_packet, + &signed_packet.public_key().to_string(), + false, + ) + .unwrap(); - let tld = keypair.public_key(); + assert_eq!(endpoint.target, "."); - let endpoint = resolver - .resolve_endpoint(&format!("pubky.{tld}")) - .await - .unwrap(); - assert_eq!(endpoint.target, "example.com"); + let addrs = endpoint.to_socket_addrs().unwrap(); + assert_eq!( + addrs.map(|s| s.to_string()).collect::>(), + vec!["209.151.148.15:6881", "[2a05:d014:275:6201::64]:6881"] + ) } } diff --git a/pubky/src/native/internals/resolver.rs b/pubky/src/native/internals/resolver.rs new file mode 100644 index 0000000..ff2b2bb --- /dev/null +++ b/pubky/src/native/internals/resolver.rs @@ -0,0 +1,246 @@ +use std::net::ToSocketAddrs; + +use pkarr::{PkarrClientAsync, PublicKey}; +use reqwest::dns::{Addrs, Resolve}; + +use crate::error::{Error, Result}; + +use super::endpoints::Endpoint; + +const DEFAULT_MAX_CHAIN_LENGTH: u8 = 3; + +#[derive(Debug, Clone)] +pub struct PkarrResolver { + pkarr: PkarrClientAsync, + max_chain_length: u8, +} + +impl PkarrResolver { + pub fn new(pkarr: PkarrClientAsync, max_chain_length: u8) -> Self { + PkarrResolver { + pkarr, + max_chain_length, + } + } + + /// Resolve a `qname` to an alternative [Endpoint] as defined in [RFC9460](https://www.rfc-editor.org/rfc/rfc9460#name-terminology). + /// + /// A `qname` is can be either a regular domain name for HTTPS endpoints, + /// or it could use Attrleaf naming pattern for cusotm protcol. For example: + /// `_foo.example.com` for `foo://example.com`. + async fn resolve_endpoint(&self, qname: &str) -> Result { + let target = qname; + // TODO: cache the result of this function? + + let is_svcb = target.starts_with('_'); + + let mut step = 0; + let mut svcb: Option = None; + + loop { + let current = svcb.clone().map_or(target.to_string(), |s| s.target); + if let Ok(tld) = PublicKey::try_from(current.clone()) { + if let Ok(Some(signed_packet)) = self.pkarr.resolve(&tld).await { + if step >= self.max_chain_length { + break; + }; + step += 1; + + // Choose most prior SVCB record + svcb = Endpoint::find(&signed_packet, ¤t, is_svcb); + + // TODO: support wildcard? + } else { + break; + } + } else { + break; + } + } + + if let Some(svcb) = svcb { + if PublicKey::try_from(svcb.target.as_str()).is_err() { + return Ok(svcb); + } + } + + Err(Error::ResolveEndpoint(target.into())) + } +} + +impl Resolve for PkarrResolver { + fn resolve(&self, name: reqwest::dns::Name) -> reqwest::dns::Resolving { + let client = self.clone(); + + Box::pin(async move { + let name = name.as_str(); + + if PublicKey::try_from(name).is_ok() { + let endpoint = client.resolve_endpoint(name).await?; + + // let addrs = format!("{}:{}", x.target, x.port).to_socket_addrs()?; + + let addrs: Addrs = Box::new(endpoint.to_socket_addrs()?); + + return Ok(addrs); + }; + + Ok(Box::new(format!("{name}:0").to_socket_addrs().unwrap())) + }) + } +} + +impl From<&PkarrClientAsync> for PkarrResolver { + fn from(pkarr: &PkarrClientAsync) -> Self { + pkarr.clone().into() + } +} + +impl From for PkarrResolver { + fn from(pkarr: PkarrClientAsync) -> Self { + Self::new(pkarr, DEFAULT_MAX_CHAIN_LENGTH) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use pkarr::dns::rdata::{A, SVCB}; + use pkarr::dns::{self, rdata::RData}; + use pkarr::{mainline::Testnet, Keypair}; + use pkarr::{PkarrClient, SignedPacket}; + + use std::future::Future; + use std::pin::Pin; + + fn generate_subtree( + client: PkarrClientAsync, + depth: u8, + branching: u8, + domain: Option, + ) -> Pin>> { + Box::pin(async move { + let keypair = Keypair::random(); + + let mut packet = dns::Packet::new_reply(0); + + for _ in 0..branching { + let mut svcb = SVCB::new(0, ".".try_into().unwrap()); + + if depth == 0 { + svcb.priority = 1; + svcb.set_port((branching) as u16 * 1000); + + if let Some(target) = &domain { + let target: &'static str = Box::leak(target.clone().into_boxed_str()); + svcb.target = target.try_into().unwrap() + } + } else { + let target = + generate_subtree(client.clone(), depth - 1, branching, domain.clone()) + .await + .to_string(); + let target: &'static str = Box::leak(target.into_boxed_str()); + svcb.target = target.try_into().unwrap(); + }; + + packet.answers.push(dns::ResourceRecord::new( + dns::Name::new("@").unwrap(), + dns::CLASS::IN, + 3600, + RData::HTTPS(svcb.into()), + )); + } + + if depth == 0 { + packet.answers.push(dns::ResourceRecord::new( + dns::Name::new("@").unwrap(), + dns::CLASS::IN, + 3600, + RData::A(A { address: 10 }), + )); + } + + let signed_packet = SignedPacket::from_packet(&keypair, &packet).unwrap(); + client.publish(&signed_packet).await.unwrap(); + + keypair.public_key() + }) + } + + fn generate( + client: PkarrClientAsync, + depth: u8, + branching: u8, + domain: Option, + ) -> Pin>> { + generate_subtree(client, depth - 1, branching, domain) + } + + #[tokio::test] + async fn resolve_endpoints() { + let testnet = Testnet::new(3); + let pkarr = PkarrClient::builder() + .testnet(&testnet) + .build() + .unwrap() + .as_async(); + + let resolver: PkarrResolver = (&pkarr).into(); + let tld = generate(pkarr, 3, 3, Some("example.com".to_string())).await; + + let endpoint = resolver.resolve_endpoint(&tld.to_string()).await.unwrap(); + assert_eq!(endpoint.target, "example.com"); + } + + #[tokio::test] + async fn max_chain_exceeded() { + let testnet = Testnet::new(3); + let pkarr = PkarrClient::builder() + .testnet(&testnet) + .build() + .unwrap() + .as_async(); + + let resolver: PkarrResolver = (&pkarr).into(); + + let tld = generate(pkarr, 4, 3, Some("example.com".to_string())).await; + + let endpoint = resolver.resolve_endpoint(&tld.to_string()).await; + + assert_eq!( + match endpoint { + Err(error) => error.to_string(), + _ => "".to_string(), + }, + crate::Error::ResolveEndpoint(tld.to_string()).to_string() + ) + } + + #[tokio::test] + async fn resolve_addresses() { + let testnet = Testnet::new(3); + let pkarr = PkarrClient::builder() + .testnet(&testnet) + .build() + .unwrap() + .as_async(); + + let resolver: PkarrResolver = (&pkarr).into(); + let tld = generate(pkarr, 3, 3, None).await; + + let endpoint = resolver.resolve_endpoint(&tld.to_string()).await.unwrap(); + assert_eq!(endpoint.target, "."); + assert_eq!(endpoint.port, 3000); + assert_eq!( + endpoint + .to_socket_addrs() + .unwrap() + .into_iter() + .map(|s| s.to_string()) + .collect::>(), + vec!["0.0.0.10:3000"] + ); + dbg!(&endpoint); + } +} diff --git a/pubky/src/shared/public.rs b/pubky/src/shared/public.rs index 366f78c..62f8144 100644 --- a/pubky/src/shared/public.rs +++ b/pubky/src/shared/public.rs @@ -762,8 +762,9 @@ mod tests { ); } - let get = client.get(url.as_str()).await.unwrap(); - dbg!(get); + let get = client.get(url.as_str()).await.unwrap().unwrap(); + + assert_eq!(get.as_ref(), &[0]); } #[tokio::test]