mirror of
https://github.com/aljazceru/pubky-core.git
synced 2026-01-08 08:44:24 +01:00
feat(pubky): endpoints return A and AAAA records
This commit is contained in:
@@ -21,7 +21,7 @@ pub struct Config {
|
||||
testnet: bool,
|
||||
port: Option<u16>,
|
||||
bootstrap: Option<Vec<String>>,
|
||||
domain: String,
|
||||
domain: Option<String>,
|
||||
/// 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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 <System's Data Directory>
|
||||
# storage = ""
|
||||
|
||||
@@ -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)?;
|
||||
|
||||
@@ -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<PubkyClient>
|
||||
|
||||
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()
|
||||
|
||||
@@ -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 ===
|
||||
|
||||
@@ -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<Endpoint> {
|
||||
let target = qname;
|
||||
// TODO: cache the result of this function?
|
||||
|
||||
let is_svcb = target.starts_with('_');
|
||||
|
||||
let mut step = 0;
|
||||
let mut svcb: Option<Endpoint> = 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<IpAddr>,
|
||||
}
|
||||
|
||||
fn get_endpoint(signed_packet: &SignedPacket, target: &str, is_svcb: bool) -> Option<Endpoint> {
|
||||
signed_packet
|
||||
.resource_records(target)
|
||||
.fold(None, |prev: Option<SVCB>, 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<Endpoint> {
|
||||
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<IpAddr> = 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<std::vec::IntoIter<SocketAddr>> {
|
||||
if self.target == "." {
|
||||
let port = self.port;
|
||||
return Ok(self
|
||||
.addrs
|
||||
.iter()
|
||||
.map(|addr| SocketAddr::from((*addr, port)))
|
||||
.collect::<Vec<_>>()
|
||||
.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.<tld> => pubky-homeserver.<tld> => @.<tld>
|
||||
|
||||
#[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<_>>(),
|
||||
vec!["209.151.148.15:6881", "[2a05:d014:275:6201::64]:6881"]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
246
pubky/src/native/internals/resolver.rs
Normal file
246
pubky/src/native/internals/resolver.rs
Normal file
@@ -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<Endpoint> {
|
||||
let target = qname;
|
||||
// TODO: cache the result of this function?
|
||||
|
||||
let is_svcb = target.starts_with('_');
|
||||
|
||||
let mut step = 0;
|
||||
let mut svcb: Option<Endpoint> = 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<PkarrClientAsync> 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<String>,
|
||||
) -> Pin<Box<dyn Future<Output = PublicKey>>> {
|
||||
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<String>,
|
||||
) -> Pin<Box<dyn Future<Output = PublicKey>>> {
|
||||
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<String>>(),
|
||||
vec!["0.0.0.10:3000"]
|
||||
);
|
||||
dbg!(&endpoint);
|
||||
}
|
||||
}
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user