From dea1f5e8628ff41d6b4b0d4f7e3fd09921a5cb75 Mon Sep 17 00:00:00 2001
From: nazeh <ar.nazeh@gmail.com>
Date: Thu, 5 Dec 2024 14:30:38 +0300
Subject: [PATCH 1/8] Revert "feat(pkarr): remove features that are not ready
 yet"

This reverts commit e3a417c09f9e1c6bb164de570259f2e628af68af.
---
 CHANGELOG.md                          |   4 +
 pkarr/Cargo.toml                      |  12 +-
 pkarr/examples/README.md              |  16 ++
 pkarr/examples/http-get.rs            |  42 +++++
 pkarr/examples/http-serve.rs          |  76 ++++++++
 pkarr/src/base/keys.rs                | 105 ++++++++++-
 pkarr/src/extra/endpoints/endpoint.rs | 256 ++++++++++++++++++++++++++
 pkarr/src/extra/endpoints/mod.rs      | 201 ++++++++++++++++++++
 pkarr/src/extra/endpoints/resolver.rs | 139 ++++++++++++++
 pkarr/src/extra/mod.rs                | 103 +++++++++++
 pkarr/src/extra/reqwest.rs            |  45 +++++
 pkarr/src/extra/tls.rs                | 167 +++++++++++++++++
 12 files changed, 1163 insertions(+), 3 deletions(-)
 create mode 100644 pkarr/examples/http-get.rs
 create mode 100644 pkarr/examples/http-serve.rs
 create mode 100644 pkarr/src/extra/endpoints/endpoint.rs
 create mode 100644 pkarr/src/extra/endpoints/mod.rs
 create mode 100644 pkarr/src/extra/endpoints/resolver.rs
 create mode 100644 pkarr/src/extra/reqwest.rs
 create mode 100644 pkarr/src/extra/tls.rs

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3179840..bdf4daa 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -15,6 +15,10 @@ All notable changes to pkarr client and server will be documented in this file.
 - Derive `serde::Serialize` and `serde::Deserialize` for `SignedPacket`.
 - Add `pkarr::LmdbCache` for persistent cache using lmdb.
 - Add `pkarr.pubky.org` as an extra default Relay and Resolver.
+- Add feature `endpoints` to resolve `HTTPS` and `SVCB` endpoints over Pkarr
+- Add feature `reqwest-resolve` to create a custom `reqwest::dns::Resolve` implementation from `Client` and `relay::client::Client`
+- Add feature `tls` to create `rustls::ClientConfig` from `Client` and `relay::client::Client` and create `rustls::ServerCongif` from `KeyPair`.
+- Add feature `reqwest-builder` to create a `reqwest::ClientBuilder` from `Client` and `relay::client::Client` using custom dns resolver and preconfigured rustls client config.
 
 ### Changed
 
diff --git a/pkarr/Cargo.toml b/pkarr/Cargo.toml
index e36e905..4450f35 100644
--- a/pkarr/Cargo.toml
+++ b/pkarr/Cargo.toml
@@ -92,11 +92,19 @@ serde = ["dep:serde", "pubky-timestamp/serde", "pubky-timestamp/httpdate"]
 # Extra
 ## Use [crate::extra::lmdb-cache::LmdbCache]
 lmdb-cache = ["dep:heed", "dep:byteorder", "dep:page_size"]
+## Use [extra::endpoints::EndpointsResolver] trait implementation for [Client] and [client::relay::Client]
+endpoints = ["dep:futures-lite", "dep:genawaiter"]
+## Use [reqwest::dns::Resolve] trait implementation for [Client] and [client::relay::Client]
+reqwest-resolve = ["dep:reqwest", "endpoints"]
+## Use [rustls::ClientConfig] from [Client] and [client::relay::Client] for e2ee transport to Pkarr endpoints
+tls = ["rustls", "ed25519-dalek/pkcs8", "dep:webpki"]
+## Create a [reqwest::ClientBuilder] from [Client] or [client::relay::Client]
+reqwest-builder = ["tls", "reqwest-resolve"]
 
 ## Use all features
-full = ["dht", "relay", "serde","lmdb-cache"]
+full = ["dht", "relay", "serde", "endpoints", "lmdb-cache", "reqwest-resolve", "tls", "reqwest-builder"]
 
-default = ["dht"]
+default = ["full"]
 
 [package.metadata.docs.rs]
 all-features = true
diff --git a/pkarr/examples/README.md b/pkarr/examples/README.md
index f3ddf7f..d56f4a2 100644
--- a/pkarr/examples/README.md
+++ b/pkarr/examples/README.md
@@ -23,3 +23,19 @@ or to use a Relay client:
 ```sh
 cargo run --features relay --example resolve <zbase32 public key from Publish step>
 ```
+
+## HTTP
+
+Run an HTTP server listening on a Pkarr key
+
+```sh
+cargo run --features endpoints --example http-serve <ip address> <port number>
+```
+
+An HTTPs url will be printend with the Pkarr key as the TLD, paste in another terminal window:
+
+```sh
+cargo run --features reqwest-resolve --example http-get <url>
+```
+
+And you should see a `Hello, World!` response.
diff --git a/pkarr/examples/http-get.rs b/pkarr/examples/http-get.rs
new file mode 100644
index 0000000..b806d86
--- /dev/null
+++ b/pkarr/examples/http-get.rs
@@ -0,0 +1,42 @@
+//! Make an HTTP request over to a Pkarr address using Reqwest
+
+use reqwest::Method;
+use tracing::Level;
+use tracing_subscriber;
+
+use clap::Parser;
+
+use pkarr::{Client, PublicKey};
+
+#[derive(Parser)]
+#[command(author, version, about, long_about = None)]
+struct Cli {
+    /// Url to GET from
+    url: String,
+}
+
+#[tokio::main]
+async fn main() -> anyhow::Result<()> {
+    tracing_subscriber::fmt().with_max_level(Level::INFO).init();
+
+    let cli = Cli::parse();
+    let url = cli.url;
+
+    let reqwest = if PublicKey::try_from(url.as_str()).is_err() {
+        // If it is not a Pkarr domain, use normal Reqwest
+        reqwest::Client::new()
+    } else {
+        let client = Client::builder().build()?;
+
+        reqwest::ClientBuilder::from(client).build()?
+    };
+
+    println!("GET {url}..");
+    let response = reqwest.request(Method::GET, &url).send().await?;
+
+    let body = response.text().await?;
+
+    println!("{body}");
+
+    Ok(())
+}
diff --git a/pkarr/examples/http-serve.rs b/pkarr/examples/http-serve.rs
new file mode 100644
index 0000000..facb8b1
--- /dev/null
+++ b/pkarr/examples/http-serve.rs
@@ -0,0 +1,76 @@
+//! Run an HTTP server listening on a Pkarr domain
+//!
+//! This server will _not_ be accessible from other networks
+//! unless the provided IP is public and the port number is forwarded.
+
+use tracing::Level;
+use tracing_subscriber;
+
+use axum::{routing::get, Router};
+use axum_server::tls_rustls::RustlsConfig;
+
+use std::net::{SocketAddr, ToSocketAddrs};
+use std::sync::Arc;
+
+use clap::Parser;
+
+use pkarr::{dns::rdata::SVCB, Client, Keypair, SignedPacket};
+
+#[derive(Parser)]
+#[command(author, version, about, long_about = None)]
+struct Cli {
+    /// IP address to listen on
+    ip: String,
+    /// Port number to listen no
+    port: u16,
+}
+
+#[tokio::main]
+async fn main() -> anyhow::Result<()> {
+    tracing_subscriber::fmt().with_max_level(Level::INFO).init();
+
+    let cli = Cli::parse();
+
+    let addr = format!("{}:{}", cli.ip, cli.port)
+        .to_socket_addrs()?
+        .next()
+        .ok_or(anyhow::anyhow!(
+            "Could not convert IP and port to socket addresses"
+        ))?;
+
+    let keypair = Keypair::random();
+
+    let client = Client::builder().build()?;
+
+    // Run a server on Pkarr
+    println!("Server listening on {addr}");
+
+    // You should republish this every time the socket address change
+    // and once an hour otherwise.
+    publish_server_pkarr(&client, &keypair, &addr).await;
+
+    println!("Server running on https://{}", keypair.public_key());
+
+    let server = axum_server::bind_rustls(
+        addr,
+        RustlsConfig::from_config(Arc::new(keypair.to_rpk_rustls_server_config())),
+    );
+
+    let app = Router::new().route("/", get(|| async { "Hello, world!" }));
+    server.serve(app.into_make_service()).await?;
+
+    Ok(())
+}
+
+async fn publish_server_pkarr(client: &Client, keypair: &Keypair, socket_addr: &SocketAddr) {
+    let mut svcb = SVCB::new(0, ".".try_into().expect("infallible"));
+    svcb.set_port(socket_addr.port());
+
+    let signed_packet = SignedPacket::builder()
+        .https(".".try_into().unwrap(), svcb, 60 * 60)
+        .address(".".try_into().unwrap(), socket_addr.ip(), 60 * 60)
+        .sign(&keypair)
+        .unwrap();
+
+    client.publish(&signed_packet).await.unwrap();
+}
diff --git a/pkarr/src/base/keys.rs b/pkarr/src/base/keys.rs
index 89a9fa2..6e95b3a 100644
--- a/pkarr/src/base/keys.rs
+++ b/pkarr/src/base/keys.rs
@@ -1,10 +1,16 @@
 //! Utility structs for Ed25519 keys.
 
+#[cfg(all(not(target_arch = "wasm32"), feature = "tls"))]
+use ed25519_dalek::pkcs8::{Document, EncodePrivateKey, EncodePublicKey};
 use ed25519_dalek::{
     SecretKey, Signature, SignatureError, Signer, SigningKey, Verifier, VerifyingKey,
 };
 use rand::rngs::OsRng;
-
+#[cfg(all(not(target_arch = "wasm32"), feature = "tls"))]
+use rustls::{
+    crypto::ring::sign::any_eddsa_type, pki_types::CertificateDer,
+    server::AlwaysResolvesServerRawPublicKeys, sign::CertifiedKey, ServerConfig,
+};
 use std::{
     fmt::{self, Debug, Display, Formatter},
     hash::Hash,
@@ -52,6 +58,57 @@ impl Keypair {
     pub fn to_uri_string(&self) -> String {
         self.public_key().to_uri_string()
     }
+
+    #[cfg(all(not(target_arch = "wasm32"), feature = "tls"))]
+    /// Return a RawPublicKey certified key according to [RFC 7250](https://tools.ietf.org/html/rfc7250)
+    /// useful to use with [rustls::ConfigBuilder::with_cert_resolver] and [rustls::server::AlwaysResolvesServerRawPublicKeys]
+    pub fn to_rpk_certified_key(&self) -> CertifiedKey {
+        let client_private_key = any_eddsa_type(
+            &self
+                .0
+                .to_pkcs8_der()
+                .expect("Keypair::to_rpk_certificate: convert secret key to pkcs8 der")
+                .as_bytes()
+                .into(),
+        )
+        .expect("Keypair::to_rpk_certificate: convert KeyPair to rustls SigningKey");
+
+        let client_public_key = client_private_key
+            .public_key()
+            .expect("Keypair::to_rpk_certificate: load SPKI");
+        let client_public_key_as_cert = CertificateDer::from(client_public_key.to_vec());
+
+        CertifiedKey::new(vec![client_public_key_as_cert], client_private_key)
+    }
+
+    #[cfg(all(not(target_arch = "wasm32"), feature = "tls"))]
+    /// Create a [rustls::ServerConfig] using this keypair as a RawPublicKey certificate according to [RFC 7250](https://tools.ietf.org/html/rfc7250)
+    pub fn to_rpk_rustls_server_config(&self) -> ServerConfig {
+        let cert_resolver =
+            AlwaysResolvesServerRawPublicKeys::new(self.to_rpk_certified_key().into());
+
+        ServerConfig::builder_with_provider(rustls::crypto::ring::default_provider().into())
+            .with_safe_default_protocol_versions()
+            .expect("version supported by ring")
+            .with_no_client_auth()
+            .with_cert_resolver(std::sync::Arc::new(cert_resolver))
+    }
+}
+
+#[cfg(all(not(target_arch = "wasm32"), feature = "tls"))]
+impl From<Keypair> for ServerConfig {
+    /// calls [Keypair::to_rpk_rustls_server_config]
+    fn from(keypair: Keypair) -> Self {
+        keypair.to_rpk_rustls_server_config()
+    }
+}
+
+#[cfg(all(not(target_arch = "wasm32"), feature = "tls"))]
+impl From<&Keypair> for ServerConfig {
+    /// calls [Keypair::to_rpk_rustls_server_config]
+    fn from(keypair: &Keypair) -> Self {
+        keypair.to_rpk_rustls_server_config()
+    }
 }
 
 /// Ed25519 public key to verify a signature over dns [Packet](crate::SignedPacket)s.
@@ -90,6 +147,11 @@ impl PublicKey {
     pub fn as_bytes(&self) -> &[u8; 32] {
         self.0.as_bytes()
     }
+
+    #[cfg(all(not(target_arch = "wasm32"), feature = "tls"))]
+    pub fn to_public_key_der(&self) -> Document {
+        self.0.to_public_key_der().expect("to_public_key_der")
+    }
 }
 
 impl AsRef<Keypair> for Keypair {
@@ -482,4 +544,45 @@ mod tests {
         let public_key: PublicKey = str.try_into().unwrap();
         assert_eq!(public_key.verifying_key().as_bytes(), &expected);
     }
+
+    #[cfg(all(not(target_arch = "wasm32"), feature = "tls"))]
+    #[test]
+    fn pkcs8() {
+        let str = "yg4gxe7z1r7mr6orids9fh95y7gxhdsxjqi6nngsxxtakqaxr5no";
+        let public_key: PublicKey = str.try_into().unwrap();
+
+        let der = public_key.to_public_key_der();
+
+        assert_eq!(
+            der.as_bytes(),
+            [
+                // Algorithm and other stuff.
+                48, 42, 48, 5, 6, 3, 43, 101, 112, 3, 33, 0, //
+                // Key
+                1, 180, 103, 163, 183, 145, 58, 178, 122, 4, 168, 237, 242, 243, 251, 7, 76, 254,
+                14, 207, 75, 171, 225, 8, 214, 123, 227, 133, 59, 15, 38, 197,
+            ]
+        )
+    }
+
+    #[cfg(all(not(target_arch = "wasm32"), feature = "tls"))]
+    #[test]
+    fn certificate() {
+        use rustls::SignatureAlgorithm;
+
+        let keypair = Keypair::from_secret_key(&[0; 32]);
+
+        let certified_key = keypair.to_rpk_certified_key();
+
+        assert_eq!(certified_key.key.algorithm(), SignatureAlgorithm::ED25519);
+
+        assert_eq!(
+            certified_key.end_entity_cert().unwrap().as_ref(),
+            [
+                48, 42, 48, 5, 6, 3, 43, 101, 112, 3, 33, 0, 59, 106, 39, 188, 206, 182, 164, 45,
+                98, 163, 168, 208, 42, 111, 13, 115, 101, 50, 21, 119, 29, 226, 67, 166, 58, 192,
+                72, 161, 139, 89, 218, 41,
+            ]
+        )
+    }
 }
diff --git a/pkarr/src/extra/endpoints/endpoint.rs b/pkarr/src/extra/endpoints/endpoint.rs
new file mode 100644
index 0000000..3e72cf5
--- /dev/null
+++ b/pkarr/src/extra/endpoints/endpoint.rs
@@ -0,0 +1,256 @@
+use crate::{
+    dns::{
+        rdata::{RData, SVCB},
+        ResourceRecord,
+    },
+    PublicKey, SignedPacket,
+};
+use std::{
+    collections::HashSet,
+    net::{IpAddr, SocketAddr, ToSocketAddrs},
+};
+
+use rand::{seq::SliceRandom, thread_rng};
+
+#[derive(Debug, Clone)]
+/// An alternative Endpoint for a `qname`, from either [RData::SVCB] or [RData::HTTPS] dns records
+pub struct Endpoint {
+    target: String,
+    public_key: PublicKey,
+    port: u16,
+    /// SocketAddrs from the [SignedPacket]
+    addrs: Vec<IpAddr>,
+}
+
+impl Endpoint {
+    /// Returns a stack of endpoints from a SignedPacket
+    ///
+    /// 1. Find the SVCB or HTTPS records
+    /// 2. Sort them by priority (reverse)
+    /// 3. Shuffle records within each priority
+    /// 3. If the target is `.`, keep track of A and AAAA records see [rfc9460](https://www.rfc-editor.org/rfc/rfc9460#name-special-handling-of-in-targ)
+    pub(crate) fn parse(
+        signed_packet: &SignedPacket,
+        target: &str,
+        // TODO: change is_svcb to a better name
+        is_svcb: bool,
+    ) -> Vec<Endpoint> {
+        let mut records = signed_packet
+            .resource_records(target)
+            .filter_map(|record| get_svcb(record, is_svcb))
+            .collect::<Vec<_>>();
+
+        // TODO: support wildcard?
+
+        // Shuffle the vector first
+        let mut rng = thread_rng();
+        records.shuffle(&mut rng);
+        // Sort by priority
+        records.sort_by(|a, b| b.priority.cmp(&a.priority));
+
+        let mut addrs = HashSet::new();
+        for record in signed_packet.resource_records("@") {
+            match &record.rdata {
+                RData::A(ip) => {
+                    addrs.insert(IpAddr::V4(ip.address.into()));
+                }
+                RData::AAAA(ip) => {
+                    addrs.insert(IpAddr::V6(ip.address.into()));
+                }
+                _ => {}
+            }
+        }
+        let addrs = addrs.into_iter().collect::<Vec<_>>();
+
+        records
+            .into_iter()
+            .map(|s| {
+                let target = s.target.to_string();
+
+                let target = if target == "." || target.is_empty() {
+                    ".".to_string()
+                } else {
+                    target
+                };
+
+                let port = s
+                    .get_param(SVCB::PORT)
+                    .map(|bytes| {
+                        let mut arr = [0_u8; 2];
+                        arr[0] = bytes[0];
+                        arr[1] = bytes[1];
+
+                        u16::from_be_bytes(arr)
+                    })
+                    .unwrap_or_default();
+
+                let addrs = if &target == "." {
+                    addrs.clone()
+                } else {
+                    Vec::with_capacity(0)
+                };
+
+                Endpoint {
+                    target,
+                    port,
+                    public_key: signed_packet.public_key(),
+                    addrs,
+                }
+            })
+            .collect::<Vec<_>>()
+    }
+
+    /// Returns the [SVCB] record's `target` value.
+    ///
+    /// Useful in web browsers where we can't use [Self::to_socket_addrs]
+    pub fn domain(&self) -> &str {
+        &self.target
+    }
+
+    pub fn port(&self) -> u16 {
+        self.port
+    }
+
+    /// Return the [PublicKey] of the [SignedPacket] this endpoint was found at.
+    ///
+    /// This is useful as the [PublicKey] of the endpoint (server), and could be
+    /// used for TLS.
+    pub fn public_key(&self) -> &PublicKey {
+        &self.public_key
+    }
+
+    /// Return an iterator of [SocketAddr], either by resolving the [Endpoint::domain] using normal DNS,
+    /// or, if the target is ".", return the [RData::A] or [RData::AAAA] records
+    /// from the endpoint's [SignedPacket], if available.
+    pub fn to_socket_addrs(&self) -> Vec<SocketAddr> {
+        if self.target == "." {
+            let port = self.port;
+
+            return self
+                .addrs
+                .iter()
+                .map(|addr| SocketAddr::from((*addr, port)))
+                .collect::<Vec<_>>();
+        }
+
+        if cfg!(target_arch = "wasm32") {
+            vec![]
+        } else {
+            format!("{}:{}", self.target, self.port)
+                .to_socket_addrs()
+                .map_or(vec![], |v| v.collect::<Vec<_>>())
+        }
+    }
+}
+
+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 super::*;
+
+    use crate::Keypair;
+
+    #[tokio::test]
+    async fn endpoint_domain() {
+        let keypair = Keypair::random();
+        let signed_packet = SignedPacket::builder()
+            .https(
+                "foo".try_into().unwrap(),
+                SVCB::new(0, "https.example.com".try_into().unwrap()),
+                3600,
+            )
+            .svcb(
+                "foo".try_into().unwrap(),
+                SVCB::new(0, "protocol.example.com".try_into().unwrap()),
+                3600,
+            )
+            // Make sure SVCB only follows SVCB
+            .https(
+                "foo".try_into().unwrap(),
+                SVCB::new(0, "https.example.com".try_into().unwrap()),
+                3600,
+            )
+            .svcb(
+                "_foo".try_into().unwrap(),
+                SVCB::new(0, "protocol.example.com".try_into().unwrap()),
+                3600,
+            )
+            .sign(&keypair)
+            .unwrap();
+
+        let tld = keypair.public_key();
+
+        // Follow foo.tld HTTPS records
+        let endpoint = Endpoint::parse(&signed_packet, &format!("foo.{tld}"), false)
+            .pop()
+            .unwrap();
+        assert_eq!(endpoint.domain(), "https.example.com");
+
+        // Follow _foo.tld SVCB records
+        let endpoint = Endpoint::parse(&signed_packet, &format!("_foo.{tld}"), true)
+            .pop()
+            .unwrap();
+        assert_eq!(endpoint.domain(), "protocol.example.com");
+    }
+
+    #[test]
+    fn endpoint_to_socket_addrs() {
+        let mut svcb = SVCB::new(1, ".".try_into().unwrap());
+        svcb.set_port(6881);
+
+        let keypair = Keypair::random();
+        let signed_packet = SignedPacket::builder()
+            .address(
+                ".".try_into().unwrap(),
+                "209.151.148.15".parse().unwrap(),
+                3600,
+            )
+            .address(
+                ".".try_into().unwrap(),
+                "2a05:d014:275:6201::64".parse().unwrap(),
+                3600,
+            )
+            .https(".".try_into().unwrap(), svcb, 3600)
+            .sign(&keypair)
+            .unwrap();
+
+        // Follow foo.tld HTTPS records
+        let endpoint = Endpoint::parse(
+            &signed_packet,
+            &signed_packet.public_key().to_string(),
+            false,
+        )
+        .pop()
+        .unwrap();
+
+        assert_eq!(endpoint.domain(), ".");
+
+        let mut addrs = endpoint.to_socket_addrs();
+        addrs.sort();
+
+        assert_eq!(
+            addrs.into_iter().map(|s| s.to_string()).collect::<Vec<_>>(),
+            vec!["209.151.148.15:6881", "[2a05:d014:275:6201::64]:6881"]
+        )
+    }
+}
diff --git a/pkarr/src/extra/endpoints/mod.rs b/pkarr/src/extra/endpoints/mod.rs
new file mode 100644
index 0000000..c63a785
--- /dev/null
+++ b/pkarr/src/extra/endpoints/mod.rs
@@ -0,0 +1,201 @@
+//! implementation of EndpointResolver trait for different clients
+
+mod endpoint;
+mod resolver;
+
+pub use endpoint::Endpoint;
+pub use resolver::EndpointsResolver;
+use resolver::ResolveError;
+
+use crate::{PublicKey, SignedPacket};
+
+#[cfg(all(not(target_arch = "wasm32"), feature = "dht"))]
+impl EndpointsResolver for crate::client::dht::Client {
+    async fn resolve(&self, public_key: &PublicKey) -> Result<Option<SignedPacket>, ResolveError> {
+        self.resolve(public_key).await.map_err(|error| match error {
+            crate::client::dht::ClientWasShutdown => ResolveError::ClientWasShutdown,
+        })
+    }
+}
+
+#[cfg(any(target_arch = "wasm32", feature = "relay"))]
+impl EndpointsResolver for crate::client::relay::Client {
+    async fn resolve(&self, public_key: &PublicKey) -> Result<Option<SignedPacket>, ResolveError> {
+        self.resolve(public_key)
+            .await
+            .map_err(ResolveError::Reqwest)
+    }
+}
+
+#[cfg(test)]
+mod tests {
+
+    use super::*;
+    use crate::dns::rdata::SVCB;
+    use crate::{mainline::Testnet, Client, Keypair};
+    use crate::{PublicKey, SignedPacket};
+
+    use std::future::Future;
+    use std::net::IpAddr;
+    use std::pin::Pin;
+    use std::str::FromStr;
+
+    // TODO: test SVCB too.
+
+    fn generate_subtree(
+        client: Client,
+        depth: u8,
+        branching: u8,
+        domain: Option<String>,
+        ips: Vec<IpAddr>,
+        port: Option<u16>,
+    ) -> Pin<Box<dyn Future<Output = PublicKey>>> {
+        Box::pin(async move {
+            let keypair = Keypair::random();
+
+            let mut builder = SignedPacket::builder();
+
+            for _ in 0..branching {
+                let mut svcb = SVCB::new(0, ".".try_into().unwrap());
+
+                if depth == 0 {
+                    svcb.priority = 1;
+
+                    if let Some(port) = port {
+                        svcb.set_port(port);
+                    }
+
+                    if let Some(target) = &domain {
+                        let target: &'static str = Box::leak(target.clone().into_boxed_str());
+                        svcb.target = target.try_into().unwrap()
+                    }
+
+                    for ip in ips.clone() {
+                        builder = builder.address(".".try_into().unwrap(), ip, 3600);
+                    }
+                } else {
+                    let target = generate_subtree(
+                        client.clone(),
+                        depth - 1,
+                        branching,
+                        domain.clone(),
+                        ips.clone(),
+                        port,
+                    )
+                    .await
+                    .to_string();
+                    let target: &'static str = Box::leak(target.into_boxed_str());
+                    svcb.target = target.try_into().unwrap();
+                };
+
+                builder = builder.https(".".try_into().unwrap(), svcb, 3600);
+            }
+
+            let signed_packet = builder.sign(&keypair).unwrap();
+
+            client.publish(&signed_packet).await.unwrap();
+
+            keypair.public_key()
+        })
+    }
+
+    /// depth of (3): A -> B -> C
+    /// branch of (2): A -> B0,  A ->  B1
+    /// domain, ips, and port are all at the end (C, or B1)
+    fn generate(
+        client: &Client,
+        depth: u8,
+        branching: u8,
+        domain: Option<String>,
+        ips: Vec<IpAddr>,
+        port: Option<u16>,
+    ) -> Pin<Box<dyn Future<Output = PublicKey>>> {
+        generate_subtree(client.clone(), depth - 1, branching, domain, ips, port)
+    }
+
+    #[tokio::test]
+    async fn direct_endpoint_resolution() {
+        let testnet = Testnet::new(3).unwrap();
+        let client = Client::builder().testnet(&testnet).build().unwrap();
+
+        let tld = generate(&client, 1, 1, Some("example.com".to_string()), vec![], None).await;
+
+        let endpoint = client
+            .resolve_https_endpoint(&tld.to_string())
+            .await
+            .unwrap();
+
+        assert_eq!(endpoint.domain(), "example.com");
+        assert_eq!(endpoint.public_key(), &tld);
+    }
+
+    #[tokio::test]
+    async fn resolve_endpoints() {
+        let testnet = Testnet::new(3).unwrap();
+        let client = Client::builder().testnet(&testnet).build().unwrap();
+
+        let tld = generate(&client, 3, 3, Some("example.com".to_string()), vec![], None).await;
+
+        let endpoint = client
+            .resolve_https_endpoint(&tld.to_string())
+            .await
+            .unwrap();
+
+        assert_eq!(endpoint.domain(), "example.com");
+    }
+
+    #[tokio::test]
+    async fn empty() {
+        let testnet = Testnet::new(3).unwrap();
+        let client = Client::builder().testnet(&testnet).build().unwrap();
+
+        let pubky = Keypair::random().public_key();
+
+        let endpoint = client.resolve_https_endpoint(&pubky.to_string()).await;
+
+        assert!(endpoint.is_err());
+    }
+
+    #[tokio::test]
+    async fn max_chain_exceeded() {
+        let testnet = Testnet::new(3).unwrap();
+        let client = Client::builder().testnet(&testnet).build().unwrap();
+
+        let tld = generate(&client, 4, 3, Some("example.com".to_string()), vec![], None).await;
+
+        let endpoint = client.resolve_https_endpoint(&tld.to_string()).await;
+
+        assert!(endpoint.is_err());
+    }
+
+    #[tokio::test]
+    async fn resolve_addresses() {
+        let testnet = Testnet::new(3).unwrap();
+        let client = Client::builder().testnet(&testnet).build().unwrap();
+
+        let tld = generate(
+            &client,
+            3,
+            3,
+            None,
+            vec![IpAddr::from_str("0.0.0.10").unwrap()],
+            Some(3000),
+        )
+        .await;
+
+        let endpoint = client
+            .resolve_https_endpoint(&tld.to_string())
+            .await
+            .unwrap();
+
+        assert_eq!(endpoint.domain(), ".");
+        assert_eq!(
+            endpoint
+                .to_socket_addrs()
+                .into_iter()
+                .map(|s| s.to_string())
+                .collect::<Vec<String>>(),
+            vec!["0.0.0.10:3000"]
+        );
+    }
+}
diff --git a/pkarr/src/extra/endpoints/resolver.rs b/pkarr/src/extra/endpoints/resolver.rs
new file mode 100644
index 0000000..d0e2d76
--- /dev/null
+++ b/pkarr/src/extra/endpoints/resolver.rs
@@ -0,0 +1,139 @@
+//! EndpointResolver trait
+
+use futures_lite::{pin, Stream, StreamExt};
+use genawaiter::sync::Gen;
+
+use crate::{PublicKey, SignedPacket};
+
+use super::Endpoint;
+
+const DEFAULT_MAX_CHAIN_LENGTH: u8 = 3;
+
+pub trait EndpointsResolver {
+    /// Returns an async stream of [HTTPS][crate::dns::rdata::RData::HTTPS] [Endpoint]s
+    fn resolve_https_endpoints(&self, qname: &str) -> impl Stream<Item = Endpoint> {
+        self.resolve_endpoints(qname, false)
+    }
+
+    /// Returns an async stream of [SVCB][crate::dns::rdata::RData::SVCB] [Endpoint]s
+    fn resolve_svcb_endpoints(&self, qname: &str) -> impl Stream<Item = Endpoint> {
+        self.resolve_endpoints(qname, true)
+    }
+
+    /// Helper method that returns the first [HTTPS][crate::dns::rdata::RData::HTTPS] [Endpoint] in the Async stream from [EndpointsResolver::resolve_https_endpoints]
+    fn resolve_https_endpoint(
+        &self,
+        qname: &str,
+    ) -> impl std::future::Future<Output = Result<Endpoint, FailedToResolveEndpoint>> {
+        async move {
+            let stream = self.resolve_https_endpoints(qname);
+
+            pin!(stream);
+
+            match stream.next().await {
+                Some(endpoint) => Ok(endpoint),
+                None => {
+                    tracing::trace!(?qname, "failed to resolve endpoint");
+                    Err(FailedToResolveEndpoint)
+                }
+            }
+        }
+    }
+
+    /// Helper method that returns the first [SVCB][crate::dns::rdata::RData::SVCB] [Endpoint] in the Async stream from [EndpointsResolver::resolve_svcb_endpoints]
+    fn resolve_svcb_endpoint(
+        &self,
+        qname: &str,
+    ) -> impl std::future::Future<Output = Result<Endpoint, FailedToResolveEndpoint>> {
+        async move {
+            let stream = self.resolve_https_endpoints(qname);
+
+            pin!(stream);
+
+            match stream.next().await {
+                Some(endpoint) => Ok(endpoint),
+                None => Err(FailedToResolveEndpoint),
+            }
+        }
+    }
+
+    /// A wrapper around the specific Pkarr client's resolve method.
+    fn resolve(
+        &self,
+        public_key: &PublicKey,
+    ) -> impl std::future::Future<Output = Result<Option<SignedPacket>, ResolveError>>;
+
+    /// Returns an async stream of either [HTTPS][crate::dns::rdata::RData::HTTPS] or [SVCB][crate::dns::rdata::RData::SVCB] [Endpoint]s
+    fn resolve_endpoints(&self, qname: &str, is_svcb: bool) -> impl Stream<Item = Endpoint> {
+        Gen::new(|co| async move {
+            // TODO: cache the result of this function?
+            // TODO: test load balancing
+            // TODO: test failover
+            // TODO: custom max_chain_length
+
+            let mut depth = 0;
+            let mut stack: Vec<Endpoint> = Vec::new();
+
+            // Initialize the stack with endpoints from the starting domain.
+            if let Ok(tld) = PublicKey::try_from(qname) {
+                if let Ok(Some(signed_packet)) = self.resolve(&tld).await {
+                    depth += 1;
+                    stack.extend(Endpoint::parse(&signed_packet, qname, is_svcb));
+                }
+            }
+
+            while let Some(next) = stack.pop() {
+                let current = next.domain();
+
+                // Attempt to resolve the domain as a public key.
+                match PublicKey::try_from(current) {
+                    Ok(tld) => match self.resolve(&tld).await {
+                        Ok(Some(signed_packet)) if depth < DEFAULT_MAX_CHAIN_LENGTH => {
+                            depth += 1;
+                            let endpoints = Endpoint::parse(&signed_packet, current, is_svcb);
+
+                            tracing::trace!(?qname, ?depth, ?endpoints, "resolved endpoints");
+
+                            stack.extend(endpoints);
+                        }
+                        _ => break, // Stop on resolution failure or chain length exceeded.
+                    },
+                    // Yield if the domain is not pointing to another Pkarr TLD domain.
+                    Err(_) => co.yield_(next).await,
+                }
+            }
+        })
+    }
+}
+
+#[derive(thiserror::Error, Debug)]
+/// Resolve Error from a client
+pub enum ResolveError {
+    ClientWasShutdown,
+    #[cfg(any(target_arch = "wasm32", feature = "relay"))]
+    Reqwest(reqwest::Error),
+}
+
+impl std::fmt::Display for ResolveError {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(
+            f,
+            "Resolve endpoint error from the client::resolve {:?}",
+            self
+        )
+    }
+}
+
+#[derive(Debug)]
+pub struct FailedToResolveEndpoint;
+
+impl std::error::Error for FailedToResolveEndpoint {}
+
+impl std::fmt::Display for FailedToResolveEndpoint {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(
+            f,
+            "Could not resolve clear net endpoint for the Pkarr domain"
+        )
+    }
+}
diff --git a/pkarr/src/extra/mod.rs b/pkarr/src/extra/mod.rs
index d67481d..3ec4dff 100644
--- a/pkarr/src/extra/mod.rs
+++ b/pkarr/src/extra/mod.rs
@@ -1,2 +1,105 @@
+#[cfg(feature = "endpoints")]
+pub mod endpoints;
+
+#[cfg(all(not(target_arch = "wasm32"), feature = "reqwest-resolve"))]
+pub mod reqwest;
+
+#[cfg(all(not(target_arch = "wasm32"), feature = "tls"))]
+pub mod tls;
+
 #[cfg(all(not(target_arch = "wasm32"), feature = "lmdb-cache"))]
 pub mod lmdb_cache;
+
+#[cfg(all(not(target_arch = "wasm32"), feature = "reqwest-builder"))]
+impl From<crate::Client> for ::reqwest::ClientBuilder {
+    /// Create a [reqwest::ClientBuilder][::reqwest::ClientBuilder] from this Pkarr client,
+    /// using it as a [dns_resolver][::reqwest::ClientBuilder::dns_resolver],
+    /// and a [preconfigured_tls][::reqwest::ClientBuilder::use_preconfigured_tls] client
+    /// config that uses [rustls::crypto::ring::default_provider()] and follows the
+    /// [tls for pkarr domains](https://pkarr.org/tls) spec.
+    fn from(client: crate::Client) -> Self {
+        ::reqwest::ClientBuilder::new()
+            .dns_resolver(std::sync::Arc::new(client.clone()))
+            .use_preconfigured_tls(rustls::ClientConfig::from(client))
+    }
+}
+
+#[cfg(all(not(target_arch = "wasm32"), feature = "reqwest-builder"))]
+impl From<crate::client::relay::Client> for ::reqwest::ClientBuilder {
+    /// Create a [reqwest::ClientBuilder][::reqwest::ClientBuilder] from this Pkarr client,
+    /// using it as a [dns_resolver][::reqwest::ClientBuilder::dns_resolver],
+    /// and a [preconfigured_tls][::reqwest::ClientBuilder::use_preconfigured_tls] client
+    /// config that uses [rustls::crypto::ring::default_provider()] and follows the
+    /// [tls for pkarr domains](https://pkarr.org/tls) spec.
+    fn from(client: crate::client::relay::Client) -> Self {
+        ::reqwest::ClientBuilder::new()
+            .dns_resolver(std::sync::Arc::new(client.clone()))
+            .use_preconfigured_tls(rustls::ClientConfig::from(client))
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use mainline::Testnet;
+    use std::net::SocketAddr;
+    use std::net::TcpListener;
+    use std::sync::Arc;
+
+    use axum::{routing::get, Router};
+    use axum_server::tls_rustls::RustlsConfig;
+
+    use crate::{dns::rdata::SVCB, Client, Keypair, SignedPacket};
+
+    async fn publish_server_pkarr(client: &Client, keypair: &Keypair, socket_addr: &SocketAddr) {
+        let mut svcb = SVCB::new(0, ".".try_into().unwrap());
+        svcb.set_port(socket_addr.port());
+
+        let signed_packet = SignedPacket::builder()
+            .https(".".try_into().unwrap(), svcb, 60 * 60)
+            .address(".".try_into().unwrap(), socket_addr.ip(), 60 * 60)
+            .sign(&keypair)
+            .unwrap();
+
+        client.publish(&signed_packet).await.unwrap();
+    }
+
+    #[tokio::test]
+    async fn reqwest_pkarr_domain() {
+        let testnet = Testnet::new(3).unwrap();
+
+        let keypair = Keypair::random();
+
+        {
+            // Run a server on Pkarr
+            let app = Router::new().route("/", get(|| async { "Hello, world!" }));
+            let listener = TcpListener::bind("127.0.0.1:0").unwrap(); // Bind to any available port
+            let address = listener.local_addr().unwrap();
+
+            let client = Client::builder().testnet(&testnet).build().unwrap();
+            publish_server_pkarr(&client, &keypair, &address).await;
+
+            println!("Server running on https://{}", keypair.public_key());
+
+            let server = axum_server::from_tcp_rustls(
+                listener,
+                RustlsConfig::from_config(Arc::new((&keypair).into())),
+            );
+
+            tokio::spawn(server.serve(app.into_make_service()));
+        }
+
+        // Client setup
+        let pkarr_client = Client::builder().testnet(&testnet).build().unwrap();
+        let reqwest = reqwest::ClientBuilder::from(pkarr_client).build().unwrap();
+
+        // Make a request
+        let response = reqwest
+            .get(format!("https://{}", keypair.public_key()))
+            .send()
+            .await
+            .unwrap();
+
+        assert_eq!(response.status(), reqwest::StatusCode::OK);
+        assert_eq!(response.text().await.unwrap(), "Hello, world!");
+    }
+}
diff --git a/pkarr/src/extra/reqwest.rs b/pkarr/src/extra/reqwest.rs
new file mode 100644
index 0000000..9fb3583
--- /dev/null
+++ b/pkarr/src/extra/reqwest.rs
@@ -0,0 +1,45 @@
+use reqwest::dns::{Addrs, Resolve};
+
+use crate::{Client, PublicKey};
+
+use super::endpoints::EndpointsResolver;
+
+use std::net::ToSocketAddrs;
+
+impl Resolve for Client {
+    fn resolve(&self, name: reqwest::dns::Name) -> reqwest::dns::Resolving {
+        let client = self.clone();
+        Box::pin(resolve(client, name))
+    }
+}
+
+#[cfg(all(not(target_arch = "wasm32"), feature = "relay"))]
+impl Resolve for crate::client::relay::Client {
+    fn resolve(&self, name: reqwest::dns::Name) -> reqwest::dns::Resolving {
+        let client = self.clone();
+        Box::pin(resolve(client, name))
+    }
+}
+
+async fn resolve(
+    client: impl EndpointsResolver,
+    name: reqwest::dns::Name,
+) -> Result<Addrs, Box<dyn std::error::Error + Send + Sync>> {
+    let name = name.as_str();
+
+    if PublicKey::try_from(name).is_ok() {
+        let endpoint = client.resolve_https_endpoint(name).await?;
+
+        let addrs = endpoint.to_socket_addrs().into_iter();
+
+        tracing::trace!(?name, ?endpoint, ?addrs, "Resolved an endpoint");
+
+        return Ok(Box::new(addrs.into_iter()));
+    };
+
+    Ok(Box::new(
+        format!("{name}:0")
+            .to_socket_addrs()
+            .expect("formatting a name and port to socket address"),
+    ))
+}
diff --git a/pkarr/src/extra/tls.rs b/pkarr/src/extra/tls.rs
new file mode 100644
index 0000000..3300134
--- /dev/null
+++ b/pkarr/src/extra/tls.rs
@@ -0,0 +1,167 @@
+use std::{fmt::Debug, sync::Arc};
+
+use futures_lite::{pin, stream::block_on};
+use rustls::{
+    client::danger::{DangerousClientConfigBuilder, ServerCertVerified, ServerCertVerifier},
+    crypto::{verify_tls13_signature_with_raw_key, WebPkiSupportedAlgorithms},
+    pki_types::SubjectPublicKeyInfoDer,
+    CertificateError, SignatureScheme,
+};
+use tracing::{instrument, Level};
+
+use crate::Client;
+
+use crate::extra::endpoints::EndpointsResolver;
+
+#[derive(Debug)]
+pub struct CertVerifier<T: EndpointsResolver + Send + Sync + Debug + Clone>(T);
+
+static SUPPORTED_ALGORITHMS: WebPkiSupportedAlgorithms = WebPkiSupportedAlgorithms {
+    all: &[webpki::ring::ED25519],
+    mapping: &[(SignatureScheme::ED25519, &[webpki::ring::ED25519])],
+};
+
+impl<T: EndpointsResolver + Send + Sync + Debug + Clone> ServerCertVerifier for CertVerifier<T> {
+    #[instrument(ret(level = Level::TRACE), err(level = Level::TRACE))]
+    /// Verify Pkarr public keys
+    fn verify_server_cert(
+        &self,
+        endpoint_certificate: &rustls::pki_types::CertificateDer<'_>,
+        intermediates: &[rustls::pki_types::CertificateDer<'_>],
+        host_name: &rustls::pki_types::ServerName<'_>,
+        _ocsp_response: &[u8],
+        _now: rustls::pki_types::UnixTime,
+    ) -> Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
+        if !intermediates.is_empty() {
+            return Err(rustls::Error::InvalidCertificate(
+                CertificateError::UnknownIssuer,
+            ));
+        }
+
+        let end_entity_as_spki = SubjectPublicKeyInfoDer::from(endpoint_certificate.as_ref());
+        let expected_spki = end_entity_as_spki.as_ref();
+
+        let qname = host_name.to_str();
+
+        // Resolve HTTPS endpoints and hope that the cached SignedPackets didn't chance
+        // since the last time we resolved endpoints to establish the connection in the
+        // first place.
+        let stream = self.0.resolve_https_endpoints(&qname);
+        pin!(stream);
+        for endpoint in block_on(stream) {
+            if endpoint.public_key().to_public_key_der().as_bytes() == expected_spki {
+                return Ok(ServerCertVerified::assertion());
+            }
+        }
+
+        // Repeat for SVCB endpoints
+        let stream = self.0.resolve_svcb_endpoints(&qname);
+        pin!(stream);
+        for endpoint in block_on(stream) {
+            if endpoint.public_key().to_public_key_der().as_bytes() == expected_spki {
+                return Ok(ServerCertVerified::assertion());
+            }
+        }
+
+        Err(rustls::Error::InvalidCertificate(
+            CertificateError::UnknownIssuer,
+        ))
+    }
+
+    #[instrument(ret(level = Level::DEBUG), err(level = Level::DEBUG))]
+    /// Verify a message signature using a raw public key and the first TLS 1.3 compatible
+    /// supported scheme.
+    fn verify_tls12_signature(
+        &self,
+        message: &[u8],
+        cert: &rustls::pki_types::CertificateDer<'_>,
+        dss: &rustls::DigitallySignedStruct,
+    ) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
+        verify_tls13_signature_with_raw_key(
+            message,
+            &SubjectPublicKeyInfoDer::from(cert.as_ref()),
+            dss,
+            &SUPPORTED_ALGORITHMS,
+        )
+    }
+
+    #[instrument(ret(level = Level::DEBUG), err(level = Level::DEBUG))]
+    /// Verify a message signature using a raw public key and the first TLS 1.3 compatible
+    /// supported scheme.
+    fn verify_tls13_signature(
+        &self,
+        message: &[u8],
+        cert: &rustls::pki_types::CertificateDer<'_>,
+        dss: &rustls::DigitallySignedStruct,
+    ) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
+        verify_tls13_signature_with_raw_key(
+            message,
+            &SubjectPublicKeyInfoDer::from(cert.as_ref()),
+            dss,
+            &SUPPORTED_ALGORITHMS,
+        )
+    }
+
+    fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
+        vec![SignatureScheme::ED25519]
+    }
+
+    fn requires_raw_public_keys(&self) -> bool {
+        true
+    }
+}
+
+impl<T: EndpointsResolver + Send + Sync + Debug + Clone> CertVerifier<T> {
+    pub(crate) fn new(pkarr_client: T) -> Self {
+        CertVerifier(pkarr_client)
+    }
+}
+
+impl From<Client> for CertVerifier<Client> {
+    fn from(pkarr_client: Client) -> Self {
+        CertVerifier::new(pkarr_client)
+    }
+}
+
+#[cfg(all(not(target_arch = "wasm32"), feature = "relay"))]
+impl From<crate::client::relay::Client> for CertVerifier<crate::client::relay::Client> {
+    fn from(pkarr_client: crate::client::relay::Client) -> Self {
+        CertVerifier::new(pkarr_client)
+    }
+}
+
+impl From<Client> for rustls::ClientConfig {
+    /// Creates a [rustls::ClientConfig] that uses [rustls::crypto::ring::default_provider()]
+    /// and no client auth and follows the [tls for pkarr domains](https://pkarr.org/tls) spec.
+    ///
+    /// If you want more control, create a [CertVerifier] from this [Client] to use as a [custom certificate verifier][DangerousClientConfigBuilder::with_custom_certificate_verifier].
+    fn from(client: Client) -> Self {
+        let verifier: CertVerifier<Client> = client.into();
+
+        create_client_config_with_ring()
+            .with_custom_certificate_verifier(Arc::new(verifier))
+            .with_no_client_auth()
+    }
+}
+
+#[cfg(all(not(target_arch = "wasm32"), feature = "relay"))]
+impl From<crate::client::relay::Client> for rustls::ClientConfig {
+    /// Creates a [rustls::ClientConfig] that uses [rustls::crypto::ring::default_provider()]
+    /// and no client auth and follows the [tls for pkarr domains](https://pkarr.org/tls) spec.
+    ///
+    /// If you want more control, create a [CertVerifier] from this [Client] to use as a [custom certificate verifier][DangerousClientConfigBuilder::with_custom_certificate_verifier].
+    fn from(client: crate::client::relay::Client) -> Self {
+        let verifier: CertVerifier<crate::client::relay::Client> = client.into();
+
+        create_client_config_with_ring()
+            .with_custom_certificate_verifier(Arc::new(verifier))
+            .with_no_client_auth()
+    }
+}
+
+fn create_client_config_with_ring() -> DangerousClientConfigBuilder {
+    rustls::ClientConfig::builder_with_provider(rustls::crypto::ring::default_provider().into())
+        .with_safe_default_protocol_versions()
+        .expect("version supported by ring")
+        .dangerous()
+}

From d50f1c0806be3b27e81f0d1cbf4e464e9f4cc53e Mon Sep 17 00:00:00 2001
From: nazeh <ar.nazeh@gmail.com>
Date: Thu, 5 Dec 2024 14:35:17 +0300
Subject: [PATCH 2/8] docs(pkarr): update changelog.md

---
 CHANGELOG.md | 13 +++++++++----
 1 file changed, 9 insertions(+), 4 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index bdf4daa..dbc2b3e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,15 @@ All notable changes to pkarr client and server will be documented in this file.
 
 ### Added
 
+- Add feature `endpoints` to resolve `HTTPS` and `SVCB` endpoints over Pkarr
+- Add feature `reqwest-resolve` to create a custom `reqwest::dns::Resolve` implementation from `Client` and `relay::client::Client`
+- Add feature `tls` to create `rustls::ClientConfig` from `Client` and `relay::client::Client` and create `rustls::ServerCongif` from `KeyPair`.
+- Add feature `reqwest-builder` to create a `reqwest::ClientBuilder` from `Client` and `relay::client::Client` using custom dns resolver and preconfigured rustls client config.
+
+##  [3.0.0](https://github.com/pubky/mainline/compare/v2.2.0...v3.0.0) - 2024-12-05
+
+### Added
+
 - Add `SignedPacket::builder()` and convenient methods to create `A`, `AAAA`, `CNAME`, `TXT`, `SVCB`, and `HTTPS` records.
 - Add `SignedPacket::all_resource_records()` to access all resource records without accessing the dns packet.
 - Use `pubky_timestamp::Timestamp` 
@@ -15,10 +24,6 @@ All notable changes to pkarr client and server will be documented in this file.
 - Derive `serde::Serialize` and `serde::Deserialize` for `SignedPacket`.
 - Add `pkarr::LmdbCache` for persistent cache using lmdb.
 - Add `pkarr.pubky.org` as an extra default Relay and Resolver.
-- Add feature `endpoints` to resolve `HTTPS` and `SVCB` endpoints over Pkarr
-- Add feature `reqwest-resolve` to create a custom `reqwest::dns::Resolve` implementation from `Client` and `relay::client::Client`
-- Add feature `tls` to create `rustls::ClientConfig` from `Client` and `relay::client::Client` and create `rustls::ServerCongif` from `KeyPair`.
-- Add feature `reqwest-builder` to create a `reqwest::ClientBuilder` from `Client` and `relay::client::Client` using custom dns resolver and preconfigured rustls client config.
 
 ### Changed
 

From 163918a07f6d48cae426a1aa9d97b4c54104bd4c Mon Sep 17 00:00:00 2001
From: nazeh <ar.nazeh@gmail.com>
Date: Thu, 5 Dec 2024 16:15:24 +0300
Subject: [PATCH 3/8] fix(pkarr): LmdbCache timestamp keys should be BE encoded

---
 pkarr/src/extra/lmdb_cache.rs | 10 ++++++----
 1 file changed, 6 insertions(+), 4 deletions(-)

diff --git a/pkarr/src/extra/lmdb_cache.rs b/pkarr/src/extra/lmdb_cache.rs
index 3334ec9..60ebf6d 100644
--- a/pkarr/src/extra/lmdb_cache.rs
+++ b/pkarr/src/extra/lmdb_cache.rs
@@ -8,7 +8,7 @@ use std::{
     time::Duration,
 };
 
-use byteorder::LittleEndian;
+use byteorder::BigEndian;
 use heed::{
     types::U64, BoxedError, BytesDecode, BytesEncode, Database, Env, EnvOpenOptions, RwTxn,
 };
@@ -30,8 +30,8 @@ const KEY_TO_TIME_TABLE: &str = "pkarrcache:key_to_time";
 const TIME_TO_KEY_TABLE: &str = "pkarrcache:time_to_key";
 
 type SignedPacketsTable = Database<CacheKeyCodec, SignedPacketCodec>;
-type KeyToTimeTable = Database<CacheKeyCodec, U64<LittleEndian>>;
-type TimeToKeyTable = Database<U64<LittleEndian>, CacheKeyCodec>;
+type KeyToTimeTable = Database<CacheKeyCodec, U64<BigEndian>>;
+type TimeToKeyTable = Database<U64<BigEndian>, CacheKeyCodec>;
 
 pub struct CacheKeyCodec;
 
@@ -169,7 +169,7 @@ impl LmdbCache {
         let key_to_time = self.key_to_time_table;
         let time_to_key = self.time_to_key_table;
 
-        let batch = self.batch.read().expect("LmdbCache::batch.read()");
+        let mut batch = self.batch.write().expect("LmdbCache::batch.write()");
         update_lru(&mut wtxn, packets, key_to_time, time_to_key, &batch)?;
 
         let len = packets.len(&wtxn)? as usize;
@@ -188,6 +188,8 @@ impl LmdbCache {
             };
         }
 
+        batch.clear();
+
         if let Some(old_time) = key_to_time.get(&wtxn, key)? {
             time_to_key.delete(&mut wtxn, &old_time)?;
         }

From 9466aa877798219fe89566acbc22f6a36537de06 Mon Sep 17 00:00:00 2001
From: nazeh <ar.nazeh@gmail.com>
Date: Fri, 6 Dec 2024 16:51:34 +0300
Subject: [PATCH 4/8] feat(pkarr): more granular errors for SignedPacket

---
 pkarr/src/base/keys.rs          |  4 ---
 pkarr/src/base/signed_packet.rs | 46 +++++++++++++++++++--------------
 pkarr/src/lib.rs                |  3 ++-
 3 files changed, 29 insertions(+), 24 deletions(-)

diff --git a/pkarr/src/base/keys.rs b/pkarr/src/base/keys.rs
index 6e95b3a..12ca9c7 100644
--- a/pkarr/src/base/keys.rs
+++ b/pkarr/src/base/keys.rs
@@ -339,10 +339,6 @@ pub enum PublicKeyError {
 
     #[error("Invalid PublicKey encoding")]
     InvalidPublicKeyEncoding,
-
-    #[error("DNS Packet is too large, expected max 1000 bytes but got: {0}")]
-    // DNS packet endocded and compressed is larger than 1000 bytes
-    PacketTooLarge(usize),
 }
 
 #[cfg(test)]
diff --git a/pkarr/src/base/signed_packet.rs b/pkarr/src/base/signed_packet.rs
index 4ba36c9..3b1c6a3 100644
--- a/pkarr/src/base/signed_packet.rs
+++ b/pkarr/src/base/signed_packet.rs
@@ -119,7 +119,7 @@ impl SignedPacketBuilder {
     }
 
     /// Alias to [Self::sign]
-    pub fn build(self, keypair: &Keypair) -> Result<SignedPacket, SignedPacketError> {
+    pub fn build(self, keypair: &Keypair) -> Result<SignedPacket, SignedPacketBuildError> {
         self.sign(keypair)
     }
 
@@ -127,7 +127,7 @@ impl SignedPacketBuilder {
     /// it with the given [Keypair].
     ///
     /// Read more about how names will be normalized in [SignedPacket::new].
-    pub fn sign(self, keypair: &Keypair) -> Result<SignedPacket, SignedPacketError> {
+    pub fn sign(self, keypair: &Keypair) -> Result<SignedPacket, SignedPacketBuildError> {
         SignedPacket::new(
             keypair,
             &self.records,
@@ -245,7 +245,7 @@ impl SignedPacket {
         keypair: &Keypair,
         answers: &[ResourceRecord<'_>],
         timestamp: Timestamp,
-    ) -> Result<SignedPacket, SignedPacketError> {
+    ) -> Result<SignedPacket, SignedPacketBuildError> {
         let mut packet = Packet::new_reply(0);
 
         let origin = keypair.public_key().to_z32();
@@ -269,7 +269,7 @@ impl SignedPacket {
         let encoded_packet: Bytes = packet.build_bytes_vec_compressed()?.into();
 
         if encoded_packet.len() > 1000 {
-            return Err(SignedPacketError::PacketTooLarge(encoded_packet.len()));
+            return Err(SignedPacketBuildError::PacketTooLarge(encoded_packet.len()));
         }
 
         let signature = keypair.sign(&signable(timestamp.into(), &encoded_packet));
@@ -280,7 +280,8 @@ impl SignedPacket {
                 &signature,
                 timestamp.into(),
                 &encoded_packet,
-            )?,
+            )
+            .expect("SignedPacket::new() try_from_parts should not fail"),
             last_seen: Timestamp::now(),
         })
     }
@@ -289,7 +290,7 @@ impl SignedPacket {
     pub fn from_relay_payload(
         public_key: &PublicKey,
         payload: &Bytes,
-    ) -> Result<SignedPacket, SignedPacketError> {
+    ) -> Result<SignedPacket, SignedPacketVerifyError> {
         let mut bytes = BytesMut::with_capacity(payload.len() + 32);
 
         bytes.extend_from_slice(public_key.as_bytes());
@@ -498,14 +499,14 @@ impl SignedPacket {
     /// You can skip all these validations by using [Self::from_bytes_unchecked] instead.
     ///
     /// You can use [Self::from_relay_payload] instead if you are receiving a response from an HTTP relay.
-    fn from_bytes(bytes: &Bytes) -> Result<SignedPacket, SignedPacketError> {
+    fn from_bytes(bytes: &Bytes) -> Result<SignedPacket, SignedPacketVerifyError> {
         if bytes.len() < 104 {
-            return Err(SignedPacketError::InvalidSignedPacketBytesLength(
+            return Err(SignedPacketVerifyError::InvalidSignedPacketBytesLength(
                 bytes.len(),
             ));
         }
         if (bytes.len() as u64) > SignedPacket::MAX_BYTES {
-            return Err(SignedPacketError::PacketTooLarge(bytes.len()));
+            return Err(SignedPacketVerifyError::PacketTooLarge(bytes.len()));
         }
         let public_key = PublicKey::try_from(&bytes[..32])?;
         let signature = Signature::from_bytes(
@@ -592,9 +593,9 @@ impl From<&SignedPacket> for MutableItem {
 
 #[cfg(all(not(target_arch = "wasm32"), feature = "dht"))]
 impl TryFrom<&MutableItem> for SignedPacket {
-    type Error = SignedPacketError;
+    type Error = SignedPacketVerifyError;
 
-    fn try_from(i: &MutableItem) -> Result<Self, SignedPacketError> {
+    fn try_from(i: &MutableItem) -> Result<Self, SignedPacketVerifyError> {
         let public_key = PublicKey::try_from(i.key())?;
         let seq = *i.seq() as u64;
         let signature: Signature = i.signature().into();
@@ -689,13 +690,10 @@ impl<'de> Deserialize<'de> for SignedPacket {
 
 #[derive(thiserror::Error, Debug)]
 /// Errors trying to parse or create a [SignedPacket]
-pub enum SignedPacketError {
+pub enum SignedPacketVerifyError {
     #[error(transparent)]
     SignatureError(#[from] SignatureError),
 
-    #[error(transparent)]
-    PublicKeyError(#[from] PublicKeyError),
-
     #[error(transparent)]
     /// Transparent [simple_dns::SimpleDnsError]
     DnsError(#[from] simple_dns::SimpleDnsError),
@@ -705,14 +703,24 @@ pub enum SignedPacketError {
     /// timestamp><less than or equal to 1000 bytes encoded dns packet>`.
     InvalidSignedPacketBytesLength(usize),
 
-    #[error("Invalid relay payload size, expected at least 72 bytes but got: {0}")]
-    /// Relay api http-body should be `<64 bytes signature><8 bytes timestamp>
-    /// <less than or equal to 1000 bytes encoded dns packet>`.
-    InvalidRelayPayloadSize(usize),
+    #[error("DNS Packet is too large, expected max 1000 bytes but got: {0}")]
+    // DNS packet endocded and compressed is larger than 1000 bytes
+    PacketTooLarge(usize),
 
+    #[error(transparent)]
+    PublicKeyError(#[from] PublicKeyError),
+}
+
+#[derive(thiserror::Error, Debug)]
+/// Errors trying to create a new [SignedPacket]
+pub enum SignedPacketBuildError {
     #[error("DNS Packet is too large, expected max 1000 bytes but got: {0}")]
     // DNS packet endocded and compressed is larger than 1000 bytes
     PacketTooLarge(usize),
+
+    #[error("Failed to write encoded DNS packet due to I/O error: {0}")]
+    // Failed to write encoded DNS packet due to I/O error
+    FailedToWrite(#[from] SimpleDnsError),
 }
 
 #[cfg(all(test, not(target_arch = "wasm32")))]
diff --git a/pkarr/src/lib.rs b/pkarr/src/lib.rs
index 87f69dd..900d278 100644
--- a/pkarr/src/lib.rs
+++ b/pkarr/src/lib.rs
@@ -46,5 +46,6 @@ pub mod errors {
     pub use super::client::relay::{EmptyListOfRelays, PublishToRelayError};
 
     pub use super::base::keys::PublicKeyError;
-    pub use super::base::signed_packet::SignedPacketError;
+    pub use super::base::signed_packet::SignedPacketBuildError;
+    pub use super::base::signed_packet::SignedPacketVerifyError;
 }

From caaf12b437b952cae3e189afc035e35815933eae Mon Sep 17 00:00:00 2001
From: nazeh <ar.nazeh@gmail.com>
Date: Wed, 11 Dec 2024 12:20:33 +0300
Subject: [PATCH 5/8] feat(pkarr): rename LmdbCache::new to LmdbCache::start

---
 pkarr/src/extra/lmdb_cache.rs | 16 ++++++++--------
 server/src/lib.rs             | 16 ++++++++--------
 server/src/main.rs            |  2 +-
 3 files changed, 17 insertions(+), 17 deletions(-)

diff --git a/pkarr/src/extra/lmdb_cache.rs b/pkarr/src/extra/lmdb_cache.rs
index 60ebf6d..2eddfb8 100644
--- a/pkarr/src/extra/lmdb_cache.rs
+++ b/pkarr/src/extra/lmdb_cache.rs
@@ -89,7 +89,7 @@ impl LmdbCache {
     /// # Safety
     /// LmdbCache uses LMDB, [opening][heed::EnvOpenOptions::open] which is marked unsafe,
     /// because the possible Undefined Behavior (UB) if the lock file is broken.
-    pub unsafe fn new(env_path: &Path, capacity: usize) -> Result<Self, Error> {
+    pub unsafe fn open(env_path: &Path, capacity: usize) -> Result<Self, Error> {
         let page_size = page_size::get();
 
         // Page aligned but more than enough bytes for `capacity` many SignedPacket
@@ -139,11 +139,11 @@ impl LmdbCache {
         Ok(instance)
     }
 
-    /// Convenient wrapper around [Self::new].
+    /// Convenient wrapper around [Self::open].
     ///
-    /// Make sure to read the safety section in [Self::new]
-    pub fn new_unsafe(env_path: &Path, capacity: usize) -> Result<Self, Error> {
-        unsafe { Self::new(env_path, capacity) }
+    /// Make sure to read the safety section in [Self::open]
+    pub fn open_unsafe(env_path: &Path, capacity: usize) -> Result<Self, Error> {
+        unsafe { Self::open(env_path, capacity) }
     }
 
     pub fn internal_len(&self) -> Result<usize, heed::Error> {
@@ -316,14 +316,14 @@ mod tests {
     fn max_map_size() {
         let env_path = std::env::temp_dir().join(Timestamp::now().to_string());
 
-        LmdbCache::new_unsafe(&env_path, usize::MAX).unwrap();
+        LmdbCache::open_unsafe(&env_path, usize::MAX).unwrap();
     }
 
     #[test]
     fn lru_capacity() {
         let env_path = std::env::temp_dir().join(Timestamp::now().to_string());
 
-        let cache = LmdbCache::new_unsafe(&env_path, 2).unwrap();
+        let cache = LmdbCache::open_unsafe(&env_path, 2).unwrap();
 
         let mut keys = vec![];
 
@@ -382,7 +382,7 @@ mod tests {
     fn lru_capacity_refresh_oldest() {
         let env_path = std::env::temp_dir().join(Timestamp::now().to_string());
 
-        let cache = LmdbCache::new_unsafe(&env_path, 2).unwrap();
+        let cache = LmdbCache::open_unsafe(&env_path, 2).unwrap();
 
         let mut keys = vec![];
 
diff --git a/server/src/lib.rs b/server/src/lib.rs
index 02b6825..d8dd8d5 100644
--- a/server/src/lib.rs
+++ b/server/src/lib.rs
@@ -23,13 +23,13 @@ pub struct Relay {
 }
 
 impl Relay {
-    /// Create a Pkarr [relay](https://pkarr.org/relays) http server as well as dht [resolver](https://pkarr.org/resolvers).
+    /// Start a Pkarr [relay](https://pkarr.org/relays) http server as well as dht [resolver](https://pkarr.org/resolvers).
     ///
     /// # Safety
-    /// Relay uses LMDB, [opening][heed::EnvOpenOptions::open] which is marked unsafe,
+    /// Relay uses LmdbCache, [opening][pkarr::extra::lmdb_cache::LmdbCache::open] which is marked unsafe,
     /// because the possible Undefined Behavior (UB) if the lock file is broken.
-    pub async unsafe fn new(config: Config) -> anyhow::Result<Self> {
-        let cache = Box::new(LmdbCache::new(&config.cache_path()?, config.cache_size())?);
+    pub async unsafe fn start(config: Config) -> anyhow::Result<Self> {
+        let cache = Box::new(LmdbCache::open(&config.cache_path()?, config.cache_size())?);
 
         let rate_limiter = rate_limiting::IpRateLimiter::new(config.rate_limiter());
 
@@ -76,11 +76,11 @@ impl Relay {
         })
     }
 
-    /// Convenient wrapper around [Self::new].
+    /// Convenient wrapper around [Self::start].
     ///
-    /// Make sure to read the safety section in [Self::new]
-    pub async fn new_unsafe(config: Config) -> anyhow::Result<Self> {
-        unsafe { Self::new(config).await }
+    /// Make sure to read the safety section in [Self::start]
+    pub async fn start_unsafe(config: Config) -> anyhow::Result<Self> {
+        unsafe { Self::start(config).await }
     }
 
     pub fn resolver_address(&self) -> SocketAddr {
diff --git a/server/src/main.rs b/server/src/main.rs
index 309490e..786b6cc 100644
--- a/server/src/main.rs
+++ b/server/src/main.rs
@@ -34,7 +34,7 @@ async fn main() -> Result<()> {
 
     debug!(?config, "Pkarr server config");
 
-    let relay = unsafe { Relay::new(config).await? };
+    let relay = unsafe { Relay::start(config).await? };
 
     tokio::signal::ctrl_c().await?;
 

From 70da82a8796fdff89595a897df466935619ec642 Mon Sep 17 00:00:00 2001
From: nazeh <ar.nazeh@gmail.com>
Date: Thu, 12 Dec 2024 11:34:12 +0300
Subject: [PATCH 6/8] feat(relay): add a configurable pkarr relay

---
 Cargo.lock                            |   5 +-
 CHANGELOG.md => pkarr/CHANGELOG.md    |   6 +-
 pkarr/Cargo.toml                      |   2 +-
 pkarr/src/base/mod.rs                 |   5 -
 pkarr/src/{base => client}/cache.rs   |   0
 pkarr/src/client/dht.rs               | 104 ++++++++++---------
 pkarr/src/client/mod.rs               |   6 +-
 pkarr/src/client/relay.rs             |  38 +++----
 pkarr/src/extra/lmdb_cache.rs         |   7 +-
 pkarr/src/{base => }/keys.rs          |   0
 pkarr/src/lib.rs                      |  23 +++--
 pkarr/src/{base => }/signed_packet.rs |   0
 server/CHANGELOG.md                   |   9 ++
 server/Cargo.toml                     |   2 +-
 server/src/config.example.toml        |  37 +++++--
 server/src/config.rs                  | 137 +++++++++++++-------------
 server/src/dht_server.rs              |   2 +-
 server/src/lib.rs                     | 121 ++++++++++++++++++-----
 server/src/main.rs                    |   2 +-
 19 files changed, 311 insertions(+), 195 deletions(-)
 rename CHANGELOG.md => pkarr/CHANGELOG.md (94%)
 delete mode 100644 pkarr/src/base/mod.rs
 rename pkarr/src/{base => client}/cache.rs (100%)
 rename pkarr/src/{base => }/keys.rs (100%)
 rename pkarr/src/{base => }/signed_packet.rs (100%)
 create mode 100644 server/CHANGELOG.md

diff --git a/Cargo.lock b/Cargo.lock
index 6f38da7..7195a14 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1417,8 +1417,7 @@ checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38"
 [[package]]
 name = "mainline"
 version = "4.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0e18c8b0210572062a02c4de8c448865f4ca89824c4ac7da64a0c2669ea2c405"
+source = "git+https://github.com/pubky/mainline?branch=v5#2948896ac074cd5bf9728966dcd076de35e7e763"
 dependencies = [
  "bytes",
  "crc",
@@ -1758,7 +1757,7 @@ dependencies = [
 ]
 
 [[package]]
-name = "pkarr-server"
+name = "pkarr-relay"
 version = "0.1.0"
 dependencies = [
  "anyhow",
diff --git a/CHANGELOG.md b/pkarr/CHANGELOG.md
similarity index 94%
rename from CHANGELOG.md
rename to pkarr/CHANGELOG.md
index dbc2b3e..4c5d07b 100644
--- a/CHANGELOG.md
+++ b/pkarr/CHANGELOG.md
@@ -1,6 +1,6 @@
 # Changelog
 
-All notable changes to pkarr client and server will be documented in this file.
+All notable changes to pkarr will be documented in this file.
 
 ## [Unreleased]
 
@@ -11,6 +11,10 @@ All notable changes to pkarr client and server will be documented in this file.
 - Add feature `tls` to create `rustls::ClientConfig` from `Client` and `relay::client::Client` and create `rustls::ServerCongif` from `KeyPair`.
 - Add feature `reqwest-builder` to create a `reqwest::ClientBuilder` from `Client` and `relay::client::Client` using custom dns resolver and preconfigured rustls client config.
 
+### Changed
+
+- Replace `Settings` with `Config` with public fields.
+
 ##  [3.0.0](https://github.com/pubky/mainline/compare/v2.2.0...v3.0.0) - 2024-12-05
 
 ### Added
diff --git a/pkarr/Cargo.toml b/pkarr/Cargo.toml
index 4450f35..73e93e1 100644
--- a/pkarr/Cargo.toml
+++ b/pkarr/Cargo.toml
@@ -43,7 +43,7 @@ webpki-roots = { version = "0.26.7", optional = true }
 pubky-timestamp = { version = "0.2.0", default-features = false }
 
 # feat: dht dependencies
-mainline = { version = "4.1.0", optional = true }
+mainline = { git = "https://github.com/pubky/mainline", branch="v5", optional = true }
 
 # feat: relay dependencies
 reqwest = { version = "0.12.9", default-features = false, features = ["rustls-tls"], optional = true }
diff --git a/pkarr/src/base/mod.rs b/pkarr/src/base/mod.rs
deleted file mode 100644
index ad1894c..0000000
--- a/pkarr/src/base/mod.rs
+++ /dev/null
@@ -1,5 +0,0 @@
-//! Basic typest, traits and utilities.
-
-pub mod cache;
-pub mod keys;
-pub mod signed_packet;
diff --git a/pkarr/src/base/cache.rs b/pkarr/src/client/cache.rs
similarity index 100%
rename from pkarr/src/base/cache.rs
rename to pkarr/src/client/cache.rs
diff --git a/pkarr/src/client/dht.rs b/pkarr/src/client/dht.rs
index 6c59561..0038ee5 100644
--- a/pkarr/src/client/dht.rs
+++ b/pkarr/src/client/dht.rs
@@ -24,34 +24,34 @@ use crate::{
 use crate::{PublicKey, SignedPacket};
 
 #[derive(Debug)]
-/// [Client]'s settings
-pub struct Settings {
-    pub(crate) dht_settings: mainline::Settings,
+/// [Client]'s Config
+pub struct Config {
+    pub dht_config: mainline::Config,
     /// A set of [resolver](https://pkarr.org/resolvers)s
     /// to be queried alongside the Dht routing table, to
     /// lower the latency on cold starts, and help if the
     /// Dht is missing values not't republished often enough.
     ///
     /// Defaults to [DEFAULT_RESOLVERS]
-    pub(crate) resolvers: Option<Vec<SocketAddr>>,
+    pub resolvers: Option<Vec<SocketAddr>>,
     /// Defaults to [DEFAULT_CACHE_SIZE]
-    pub(crate) cache_size: NonZeroUsize,
+    pub cache_size: NonZeroUsize,
     /// Used in the `min` parameter in [SignedPacket::expires_in].
     ///
     /// Defaults to [DEFAULT_MINIMUM_TTL]
-    pub(crate) minimum_ttl: u32,
+    pub minimum_ttl: u32,
     /// Used in the `max` parameter in [SignedPacket::expires_in].
     ///
     /// Defaults to [DEFAULT_MAXIMUM_TTL]
-    pub(crate) maximum_ttl: u32,
+    pub maximum_ttl: u32,
     /// Custom [Cache] implementation, defaults to [InMemoryCache]
-    pub(crate) cache: Option<Box<dyn Cache>>,
+    pub cache: Option<Box<dyn Cache>>,
 }
 
-impl Default for Settings {
+impl Default for Config {
     fn default() -> Self {
         Self {
-            dht_settings: mainline::Dht::builder(),
+            dht_config: mainline::Config::default(),
             cache_size: NonZeroUsize::new(DEFAULT_CACHE_SIZE)
                 .expect("NonZeroUsize from DEFAULT_CACHE_SIZE"),
             resolvers: Some(
@@ -68,10 +68,13 @@ impl Default for Settings {
     }
 }
 
-impl Settings {
-    /// Set custom set of [resolvers](Settings::resolvers).
+#[derive(Debug, Default)]
+pub struct ClientBuilder(Config);
+
+impl ClientBuilder {
+    /// Set custom set of [resolvers](Config::resolvers).
     pub fn resolvers(mut self, resolvers: Option<Vec<String>>) -> Self {
-        self.resolvers = resolvers.map(|resolvers| {
+        self.0.resolvers = resolvers.map(|resolvers| {
             resolvers
                 .iter()
                 .flat_map(|resolver| resolver.to_socket_addrs())
@@ -81,53 +84,53 @@ impl Settings {
         self
     }
 
-    /// Set the [Settings::cache_size].
+    /// Set the [Config::cache_size].
     ///
     /// Controls the capacity of [Cache].
     pub fn cache_size(mut self, cache_size: NonZeroUsize) -> Self {
-        self.cache_size = cache_size;
+        self.0.cache_size = cache_size;
         self
     }
 
-    /// Set the [Settings::minimum_ttl] value.
+    /// Set the [Config::minimum_ttl] value.
     ///
     /// Limits how soon a [SignedPacket] is considered expired.
     pub fn minimum_ttl(mut self, ttl: u32) -> Self {
-        self.minimum_ttl = ttl;
-        self.maximum_ttl = self.maximum_ttl.clamp(ttl, u32::MAX);
+        self.0.minimum_ttl = ttl;
+        self.0.maximum_ttl = self.0.maximum_ttl.max(ttl);
         self
     }
 
-    /// Set the [Settings::maximum_ttl] value.
+    /// Set the [Config::maximum_ttl] value.
     ///
     /// Limits how long it takes before a [SignedPacket] is considered expired.
     pub fn maximum_ttl(mut self, ttl: u32) -> Self {
-        self.maximum_ttl = ttl;
-        self.minimum_ttl = self.minimum_ttl.clamp(0, ttl);
+        self.0.maximum_ttl = ttl;
+        self.0.minimum_ttl = self.0.minimum_ttl.min(ttl);
         self
     }
 
     /// Set a custom implementation of [Cache].
     pub fn cache(mut self, cache: Box<dyn Cache>) -> Self {
-        self.cache = Some(cache);
+        self.0.cache = Some(cache);
         self
     }
 
-    /// Set [Settings::dht_settings]
-    pub fn dht_settings(mut self, settings: mainline::Settings) -> Self {
-        self.dht_settings = settings;
+    /// Set [Config::dht_config]
+    pub fn dht_config(mut self, settings: mainline::Config) -> Self {
+        self.0.dht_config = settings;
         self
     }
 
     /// Convienent methot to set the [mainline::Settings::bootstrap] from [mainline::Testnet::bootstrap]
     pub fn testnet(mut self, testnet: &Testnet) -> Self {
-        self.dht_settings = self.dht_settings.bootstrap(&testnet.bootstrap);
+        self.0.dht_config.bootstrap = testnet.bootstrap.clone();
 
         self
     }
 
     pub fn build(self) -> Result<Client, std::io::Error> {
-        Client::new(self)
+        Client::new(self.0)
     }
 }
 
@@ -141,27 +144,27 @@ pub struct Client {
 }
 
 impl Client {
-    pub fn new(settings: Settings) -> Result<Client, std::io::Error> {
+    pub fn new(config: Config) -> Result<Client, std::io::Error> {
         let (sender, receiver) = flume::bounded(32);
 
-        let cache = settings
+        let cache = config
             .cache
             .clone()
-            .unwrap_or(Box::new(InMemoryCache::new(settings.cache_size)));
+            .unwrap_or(Box::new(InMemoryCache::new(config.cache_size)));
         let cache_clone = cache.clone();
 
         let client = Client {
             sender,
             cache,
-            minimum_ttl: settings.minimum_ttl,
-            maximum_ttl: settings.maximum_ttl,
+            minimum_ttl: config.minimum_ttl.min(config.maximum_ttl),
+            maximum_ttl: config.maximum_ttl.max(config.minimum_ttl),
         };
 
-        debug!(?settings, "Starting Client main loop..");
+        debug!(?config, "Starting Client main loop..");
 
         thread::Builder::new()
             .name("Pkarr Dht actor thread".to_string())
-            .spawn(move || run(cache_clone, settings, receiver))?;
+            .spawn(move || run(cache_clone, config, receiver))?;
 
         let (tx, rx) = flume::bounded(1);
 
@@ -175,9 +178,9 @@ impl Client {
         Ok(client)
     }
 
-    /// Returns a builder to edit settings before creating Client.
-    pub fn builder() -> Settings {
-        Settings::default()
+    /// Returns a builder to edit config before creating Client.
+    pub fn builder() -> ClientBuilder {
+        ClientBuilder::default()
     }
 
     // === Getters ===
@@ -384,11 +387,23 @@ pub enum PublishError {
     MainlinePutError(#[from] PutError),
 }
 
-fn run(cache: Box<dyn Cache>, settings: Settings, receiver: Receiver<ActorMessage>) {
-    match settings.dht_settings.build_rpc() {
+fn run(cache: Box<dyn Cache>, config: Config, receiver: Receiver<ActorMessage>) {
+    match Rpc::new(
+        config
+            .dht_config
+            .bootstrap
+            .to_owned()
+            .iter()
+            .flat_map(|s| s.to_socket_addrs().map(|addrs| addrs.collect::<Vec<_>>()))
+            .flatten()
+            .collect::<Vec<_>>(),
+        config.dht_config.server.is_none(),
+        config.dht_config.request_timeout,
+        config.dht_config.port,
+    ) {
         Ok(mut rpc) => {
-            let mut server = settings.dht_settings.into_server();
-            actor_thread(&mut rpc, &mut server, cache, receiver, settings.resolvers)
+            let mut server = config.dht_config.server;
+            actor_thread(&mut rpc, &mut server, cache, receiver, config.resolvers)
         }
         Err(err) => {
             if let Ok(ActorMessage::Check(sender)) = receiver.try_recv() {
@@ -732,9 +747,10 @@ mod tests {
 
         let client = Client::builder()
             .testnet(&testnet)
-            .dht_settings(
-                mainline::Settings::default().request_timeout(Duration::from_millis(10).into()),
-            )
+            .dht_config(mainline::Config {
+                request_timeout: Duration::from_millis(10),
+                ..Default::default()
+            })
             // Everything is expired
             .maximum_ttl(0)
             .build()
diff --git a/pkarr/src/client/mod.rs b/pkarr/src/client/mod.rs
index 6eed8ff..4ed7fbf 100644
--- a/pkarr/src/client/mod.rs
+++ b/pkarr/src/client/mod.rs
@@ -1,14 +1,16 @@
 //! Client implementation.
 
+pub mod cache;
+
 #[cfg(all(not(target_arch = "wasm32"), feature = "dht"))]
 pub(crate) mod dht;
 #[cfg(all(not(target_arch = "wasm32"), feature = "dht"))]
-pub use dht::{Client, Settings};
+pub use dht::{Client, Config};
 
 #[cfg(target_arch = "wasm32")]
 pub(crate) mod relay;
 #[cfg(target_arch = "wasm32")]
-pub use relay::{Client, Settings};
+pub use relay::{Client, Config};
 
 #[cfg(all(not(target_arch = "wasm32"), feature = "relay"))]
 pub mod relay;
diff --git a/pkarr/src/client/relay.rs b/pkarr/src/client/relay.rs
index 9c3ac6e..5390def 100644
--- a/pkarr/src/client/relay.rs
+++ b/pkarr/src/client/relay.rs
@@ -19,8 +19,8 @@ use crate::{
 };
 
 #[derive(Debug, Clone)]
-/// [Client]'s settings
-pub struct Settings {
+/// [Client]'s Config
+pub struct Config {
     pub(crate) relays: Vec<String>,
     /// Defaults to [DEFAULT_CACHE_SIZE]
     pub(crate) cache_size: NonZeroUsize,
@@ -38,7 +38,7 @@ pub struct Settings {
     pub(crate) cache: Option<Box<dyn Cache>>,
 }
 
-impl Default for Settings {
+impl Default for Config {
     fn default() -> Self {
         Self {
             relays: DEFAULT_RELAYS.map(|s| s.into()).to_vec(),
@@ -52,14 +52,14 @@ impl Default for Settings {
     }
 }
 
-impl Settings {
+impl Config {
     /// Set the relays to publish and resolve [SignedPacket]s to and from.
     pub fn relays(mut self, relays: Vec<String>) -> Self {
         self.relays = relays;
         self
     }
 
-    /// Set the [Settings::cache_size].
+    /// Set the [Config::cache_size].
     ///
     /// Controls the capacity of [Cache].
     pub fn cache_size(mut self, cache_size: NonZeroUsize) -> Self {
@@ -67,7 +67,7 @@ impl Settings {
         self
     }
 
-    /// Set the [Settings::minimum_ttl] value.
+    /// Set the [Config::minimum_ttl] value.
     ///
     /// Limits how soon a [SignedPacket] is considered expired.
     pub fn minimum_ttl(mut self, ttl: u32) -> Self {
@@ -76,7 +76,7 @@ impl Settings {
         self
     }
 
-    /// Set the [Settings::maximum_ttl] value.
+    /// Set the [Config::maximum_ttl] value.
     ///
     /// Limits how long it takes before a [SignedPacket] is considered expired.
     pub fn maximum_ttl(mut self, ttl: u32) -> Self {
@@ -108,33 +108,33 @@ pub struct Client {
 
 impl Default for Client {
     fn default() -> Self {
-        Self::new(Settings::default()).expect("Pkarr Relay client default")
+        Self::new(Config::default()).expect("Pkarr Relay client default")
     }
 }
 
 impl Client {
-    pub fn new(settings: Settings) -> Result<Self, EmptyListOfRelays> {
-        if settings.relays.is_empty() {
+    pub fn new(config: Config) -> Result<Self, EmptyListOfRelays> {
+        if config.relays.is_empty() {
             return Err(EmptyListOfRelays);
         }
 
-        let cache = settings
+        let cache = config
             .cache
             .clone()
-            .unwrap_or(Box::new(InMemoryCache::new(settings.cache_size)));
+            .unwrap_or(Box::new(InMemoryCache::new(config.cache_size)));
 
         Ok(Self {
-            http_client: settings.http_client,
-            relays: settings.relays,
+            http_client: config.http_client,
+            relays: config.relays,
             cache,
-            minimum_ttl: settings.minimum_ttl,
-            maximum_ttl: settings.maximum_ttl,
+            minimum_ttl: config.minimum_ttl,
+            maximum_ttl: config.maximum_ttl,
         })
     }
 
-    /// Returns a builder to edit settings before creating Client.
-    pub fn builder() -> Settings {
-        Settings::default()
+    /// Returns a builder to edit config before creating Client.
+    pub fn builder() -> Config {
+        Config::default()
     }
 
     /// Returns a reference to the internal cache.
diff --git a/pkarr/src/extra/lmdb_cache.rs b/pkarr/src/extra/lmdb_cache.rs
index 2eddfb8..bebf879 100644
--- a/pkarr/src/extra/lmdb_cache.rs
+++ b/pkarr/src/extra/lmdb_cache.rs
@@ -1,4 +1,4 @@
-//! Persistent [crate::base::cache::Cache] implementation using LMDB's bindings [heed]
+//! Persistent [crate::Cache] implementation using LMDB's bindings [heed]
 
 use std::{
     borrow::Cow,
@@ -17,10 +17,7 @@ use tracing::debug;
 
 use pubky_timestamp::Timestamp;
 
-use crate::{
-    base::cache::{Cache, CacheKey},
-    SignedPacket,
-};
+use crate::{Cache, CacheKey, SignedPacket};
 
 const MAX_MAP_SIZE: usize = 10995116277760; // 10 TB
 const MIN_MAP_SIZE: usize = 10 * 1024 * 1024; // 10 mb
diff --git a/pkarr/src/base/keys.rs b/pkarr/src/keys.rs
similarity index 100%
rename from pkarr/src/base/keys.rs
rename to pkarr/src/keys.rs
diff --git a/pkarr/src/lib.rs b/pkarr/src/lib.rs
index 900d278..b2a2e46 100644
--- a/pkarr/src/lib.rs
+++ b/pkarr/src/lib.rs
@@ -4,14 +4,11 @@
 //!
 
 // Modules
-mod base;
+#[cfg(all(not(target_arch = "wasm32"), any(feature = "relay", feature = "dht")))]
 pub mod client;
 pub mod extra;
-
-// Exports
-pub use base::cache::{Cache, CacheKey, InMemoryCache};
-pub use base::keys::{Keypair, PublicKey};
-pub use base::signed_packet::SignedPacket;
+mod keys;
+mod signed_packet;
 
 /// Default minimum TTL: 5 minutes
 pub const DEFAULT_MINIMUM_TTL: u32 = 300;
@@ -24,10 +21,16 @@ pub const DEFAULT_RELAYS: [&str; 2] = ["https://relay.pkarr.org", "https://pkarr
 /// Default [resolver](https://pkarr.org/resolvers)s
 pub const DEFAULT_RESOLVERS: [&str; 2] = ["resolver.pkarr.org:6881", "pkarr.pubky.org:6881"];
 
+// Exports
+#[cfg(any(feature = "relay", feature = "dht"))]
+pub use client::cache::{Cache, CacheKey, InMemoryCache};
+pub use keys::{Keypair, PublicKey};
+pub use signed_packet::SignedPacket;
+
 #[cfg(all(not(target_arch = "wasm32"), feature = "dht"))]
 pub use client::dht::Info;
 #[cfg(any(target_arch = "wasm32", feature = "dht"))]
-pub use client::{Client, Settings};
+pub use client::{Client, Config};
 
 // Rexports
 pub use bytes;
@@ -45,7 +48,7 @@ pub mod errors {
     #[cfg(any(target_arch = "wasm32", feature = "relay"))]
     pub use super::client::relay::{EmptyListOfRelays, PublishToRelayError};
 
-    pub use super::base::keys::PublicKeyError;
-    pub use super::base::signed_packet::SignedPacketBuildError;
-    pub use super::base::signed_packet::SignedPacketVerifyError;
+    pub use super::keys::PublicKeyError;
+    pub use super::signed_packet::SignedPacketBuildError;
+    pub use super::signed_packet::SignedPacketVerifyError;
 }
diff --git a/pkarr/src/base/signed_packet.rs b/pkarr/src/signed_packet.rs
similarity index 100%
rename from pkarr/src/base/signed_packet.rs
rename to pkarr/src/signed_packet.rs
diff --git a/server/CHANGELOG.md b/server/CHANGELOG.md
new file mode 100644
index 0000000..2148b83
--- /dev/null
+++ b/server/CHANGELOG.md
@@ -0,0 +1,9 @@
+# Changelog
+
+All notable changes to pkarr relay will be documented in this file.
+
+## [Unreleased]
+
+### Added
+
+- Add `Relay` and `Config` to be able run an in-process relay with full control over configurations
diff --git a/server/Cargo.toml b/server/Cargo.toml
index 9da6dd4..2571c53 100644
--- a/server/Cargo.toml
+++ b/server/Cargo.toml
@@ -1,5 +1,5 @@
 [package]
-name = "pkarr-server"
+name = "pkarr-relay"
 version = "0.1.0"
 authors = ["Nuh <nuh@nuh.dev>"]
 edition = "2021"
diff --git a/server/src/config.example.toml b/server/src/config.example.toml
index 012828c..e7f5076 100644
--- a/server/src/config.example.toml
+++ b/server/src/config.example.toml
@@ -1,11 +1,36 @@
-relay_port = 6881
-dht_port = 6881
-# cache_path = "./storage/location"
-cache_size = 1_000_000
-resolvers =  []
+# HTTP server configurations.
+[http]
+# The port number to run the HTTP server on. 
+port = 6881
+
+# Internal Mainline node configurations
+[mainline]
+# Port to run the internal Mainline DHT node on.
+port = 6881
+
+# Cache settings
+[cache]
+# Set the path for the cache storage.
+path = "./storage/location"
+# Maximum number of SignedPackets to store, before evicting the oldest packets.
+size = 1_000_000
+
+# Minimum TTL before attempting to lookup a more recent version of a SignedPacket 
 minimum_ttl =  300
+# Maximum TTL before attempting to lookup a more recent version of a SignedPacket 
 maximum_ttl =  86400
+
 [rate_limiter]
+# Set to true if you are running this relay
+# behind a reverse proxy, to use smart IP address
+# extractors.
+# 
+# Make sure that your server is not also accessible
+# directly, otherwise an attacker can bypass the
+# Ip rate limiting by setting the trusted headers to 
+# a random IP address on every request.
 behind_proxy = false
-per_second = 2
+# Maximum number of requests per second.
 burst_size = 10
+# Number of seconds after which one request of the quota is replenished.
+per_second = 2
diff --git a/server/src/config.rs b/server/src/config.rs
index 410dbee..9de7c8e 100644
--- a/server/src/config.rs
+++ b/server/src/config.rs
@@ -1,45 +1,72 @@
 //! Configuration for the server
 
-use anyhow::{anyhow, Context, Result};
-use pkarr::{DEFAULT_CACHE_SIZE, DEFAULT_MAXIMUM_TTL, DEFAULT_MINIMUM_TTL};
+use anyhow::{Context, Result};
 use serde::{Deserialize, Serialize};
 use std::{
     fmt::Debug,
     path::{Path, PathBuf},
 };
 
+pub const DEFAULT_CACHE_SIZE: usize = 1_000_000;
+
 use crate::rate_limiting::RateLimiterConfig;
 
-/// Server configuration
+#[derive(Serialize, Deserialize, Default)]
+struct ConfigToml {
+    http: Option<HttpConfig>,
+    mainline: Option<MainlineConfig>,
+    cache_path: Option<String>,
+    cache_size: Option<usize>,
+    /// See [pkarr::Settings::minimum_ttl]
+    minimum_ttl: Option<u32>,
+    /// See [pkarr::Settings::maximum_ttl]
+    maximum_ttl: Option<u32>,
+    rate_limiter: RateLimiterConfig,
+}
+
+#[derive(Serialize, Deserialize, Default)]
+struct HttpConfig {
+    port: Option<u16>,
+}
+
+#[derive(Serialize, Deserialize, Default)]
+struct MainlineConfig {
+    port: Option<u16>,
+}
+
+/// Pkarr Relay configuration
 ///
 /// The config is usually loaded from a file with [`Self::load`].
-#[derive(Serialize, Deserialize, Default)]
+#[derive(Debug)]
 pub struct Config {
     /// TCP port to run the HTTP server on
     ///
     /// Defaults to `6881`
-    relay_port: Option<u16>,
-    /// UDP port to run the DHT on
-    ///
-    /// Defaults to `6881`
-    dht_port: Option<u16>,
+    pub http_port: u16,
+    /// Pkarr client configuration
+    pub pkarr_config: pkarr::Config,
     /// Path to cache database
     ///
     /// Defaults to a directory in the OS data directory
-    cache_path: Option<String>,
+    pub cache_path: Option<PathBuf>,
     /// See [pkarr::client::Settings::cache_size]
-    cache_size: Option<usize>,
-    /// Resolvers
     ///
-    /// Other servers to query in parallel with the Dht queries
-    ///
-    /// See [pkarr::client::Settings::resolvers]
-    resolvers: Option<Vec<String>>,
-    /// See [pkarr::Settings::minimum_ttl]
-    minimum_ttl: Option<u32>,
-    /// See [pkarr::Settings::maximum_ttl]
-    maximum_ttl: Option<u32>,
-    rate_limiter: RateLimiterConfig,
+    /// Defaults to 1000_000
+    pub cache_size: usize,
+    /// IP rete limiter configuration
+    pub rate_limiter: RateLimiterConfig,
+}
+
+impl Default for Config {
+    fn default() -> Self {
+        Self {
+            http_port: 6881,
+            pkarr_config: pkarr::Config::default(),
+            cache_path: None,
+            cache_size: DEFAULT_CACHE_SIZE,
+            rate_limiter: RateLimiterConfig::default(),
+        }
+    }
 }
 
 impl Config {
@@ -48,64 +75,32 @@ impl Config {
         let s = tokio::fs::read_to_string(path.as_ref())
             .await
             .with_context(|| format!("failed to read {}", path.as_ref().to_string_lossy()))?;
-        let config: Config = toml::from_str(&s)?;
-        Ok(config)
-    }
 
-    pub fn relay_port(&self) -> u16 {
-        self.relay_port.unwrap_or(6881)
-    }
+        let config_toml: ConfigToml = toml::from_str(&s)?;
 
-    pub fn dht_port(&self) -> u16 {
-        self.dht_port.unwrap_or(6881)
-    }
+        let mut config = Config::default();
 
-    pub fn resolvers(&self) -> Option<Vec<String>> {
-        self.resolvers.clone()
-    }
+        if let Some(ttl) = config_toml.minimum_ttl {
+            config.pkarr_config.minimum_ttl = ttl;
+        }
 
-    pub fn minimum_ttl(&self) -> u32 {
-        self.minimum_ttl.unwrap_or(DEFAULT_MINIMUM_TTL)
-    }
+        if let Some(ttl) = config_toml.maximum_ttl {
+            config.pkarr_config.maximum_ttl = ttl;
+        }
 
-    pub fn maximum_ttl(&self) -> u32 {
-        self.maximum_ttl.unwrap_or(DEFAULT_MAXIMUM_TTL)
-    }
+        config.pkarr_config.dht_config.port = config_toml.mainline.and_then(|m| m.port);
 
-    pub fn cache_size(&self) -> usize {
-        self.cache_size.unwrap_or(DEFAULT_CACHE_SIZE)
-    }
+        if let Some(HttpConfig { port: Some(port) }) = config_toml.http {
+            config.http_port = port;
+        }
 
-    pub fn rate_limiter(&self) -> &RateLimiterConfig {
-        &self.rate_limiter
-    }
+        if let Some(path) = config_toml.cache_path {
+            config.cache_path = Some(PathBuf::from(path).join("pkarr-cache"));
+        }
 
-    /// Get the path to the cache database file.
-    pub fn cache_path(&self) -> Result<PathBuf> {
-        let dir = if let Some(cache_path) = &self.cache_path {
-            PathBuf::from(cache_path)
-        } else {
-            let path = dirs_next::data_dir().ok_or_else(|| {
-                anyhow!("operating environment provides no directory for application data")
-            })?;
-            path.join("pkarr-server")
-        };
-
-        Ok(dir.join("pkarr-cache"))
-    }
-}
+        config.cache_size = config_toml.cache_size.unwrap_or(DEFAULT_CACHE_SIZE);
+        config.rate_limiter = config_toml.rate_limiter;
 
-impl Debug for Config {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        f.debug_map()
-            .entry(&"relay_port", &self.relay_port())
-            .entry(&"dht_port", &self.dht_port())
-            .entry(&"cache_path", &self.cache_path())
-            .entry(&"cache_size", &self.cache_size())
-            .entry(&"resolvers", &self.resolvers.clone().unwrap_or_default())
-            .entry(&"minimum_ttl", &self.minimum_ttl())
-            .entry(&"maximum_ttl", &self.maximum_ttl())
-            .entry(&"rate_limter", &self.rate_limiter())
-            .finish()
+        Ok(config)
     }
 }
diff --git a/server/src/dht_server.rs b/server/src/dht_server.rs
index 6547eb5..1535757 100644
--- a/server/src/dht_server.rs
+++ b/server/src/dht_server.rs
@@ -43,7 +43,7 @@ impl Debug for DhtServer {
 impl DhtServer {
     pub fn new(
         cache: Box<LmdbCache>,
-        resolvers: Option<Vec<String>>,
+        resolvers: Option<Vec<SocketAddr>>,
         minimum_ttl: u32,
         maximum_ttl: u32,
         rate_limiter: IpRateLimiter,
diff --git a/server/src/lib.rs b/server/src/lib.rs
index d8dd8d5..821e366 100644
--- a/server/src/lib.rs
+++ b/server/src/lib.rs
@@ -4,18 +4,76 @@ mod error;
 mod handlers;
 mod rate_limiting;
 
-use std::net::{SocketAddr, TcpListener};
+use std::{
+    net::{SocketAddr, TcpListener},
+    path::PathBuf,
+};
 
 use axum::{extract::DefaultBodyLimit, Router};
 use axum_server::Handle;
 
+use dht_server::DhtServer;
+use rate_limiting::RateLimiterConfig;
 use tower_http::{cors::CorsLayer, trace::TraceLayer};
 use tracing::info;
 
-use pkarr::{extra::lmdb_cache::LmdbCache, mainline, Client};
+use pkarr::{extra::lmdb_cache::LmdbCache, Client};
 
 pub use config::Config;
 
+#[derive(Debug, Default)]
+pub struct RelayBuilder(Config);
+
+impl RelayBuilder {
+    // Configure the port for the HTTP server to listen on
+    pub fn http_port(mut self, port: u16) -> Self {
+        self.0.http_port = port;
+
+        self
+    }
+
+    // Configure the port for the internal Mainline DHT node to listen on
+    pub fn dht_port(mut self, port: u16) -> Self {
+        self.0.pkarr_config.dht_config.port = Some(port);
+
+        self
+    }
+
+    // Configure the path to store the persistent cache at.
+    pub fn cache_path(mut self, path: PathBuf) -> Self {
+        self.0.cache_path = Some(path);
+
+        self
+    }
+
+    // Configure the maximum number of SignedPackets in the LRU cache
+    pub fn cache_size(mut self, size: usize) -> Self {
+        self.0.cache_size = size;
+
+        self
+    }
+
+    /// Disable rate limiting by setting the configuration as generous as possible
+    pub fn disable_rate_limiting(mut self) -> Self {
+        self.0.rate_limiter = RateLimiterConfig {
+            per_second: 1,
+            burst_size: u32::MAX,
+            behind_proxy: false,
+        };
+
+        self
+    }
+
+    /// Start a Pkarr [relay](https://pkarr.org/relays) http server as well as dht [resolver](https://pkarr.org/resolvers).
+    ///
+    /// # Safety
+    /// Relay uses LmdbCache, [opening][pkarr::extra::lmdb_cache::LmdbCache::open] which is marked unsafe,
+    /// because the possible Undefined Behavior (UB) if the lock file is broken.
+    pub async unsafe fn start(self) -> anyhow::Result<Relay> {
+        Ok(unsafe { Relay::start(self.0).await? })
+    }
+}
+
 pub struct Relay {
     handle: Handle,
     resolver_address: SocketAddr,
@@ -23,35 +81,48 @@ pub struct Relay {
 }
 
 impl Relay {
+    pub fn builder() -> RelayBuilder {
+        RelayBuilder::default()
+    }
+
     /// Start a Pkarr [relay](https://pkarr.org/relays) http server as well as dht [resolver](https://pkarr.org/resolvers).
     ///
     /// # Safety
     /// Relay uses LmdbCache, [opening][pkarr::extra::lmdb_cache::LmdbCache::open] which is marked unsafe,
     /// because the possible Undefined Behavior (UB) if the lock file is broken.
     pub async unsafe fn start(config: Config) -> anyhow::Result<Self> {
-        let cache = Box::new(LmdbCache::open(&config.cache_path()?, config.cache_size())?);
-
-        let rate_limiter = rate_limiting::IpRateLimiter::new(config.rate_limiter());
-
-        let client = Client::builder()
-            .dht_settings(
-                mainline::Settings::default()
-                    .port(config.dht_port())
-                    .custom_server(Box::new(dht_server::DhtServer::new(
-                        cache.clone(),
-                        config.resolvers(),
-                        config.minimum_ttl(),
-                        config.maximum_ttl(),
-                        rate_limiter.clone(),
-                    ))),
-            )
-            .resolvers(config.resolvers())
-            .minimum_ttl(config.minimum_ttl())
-            .maximum_ttl(config.maximum_ttl())
-            .cache(cache)
-            .build()?;
-
-        let listener = TcpListener::bind(SocketAddr::from(([0, 0, 0, 0], config.relay_port())))?;
+        let mut config = config;
+
+        let cache_path = match config.cache_path {
+            Some(path) => path,
+            None => {
+                let path = dirs_next::data_dir().ok_or_else(|| {
+                    anyhow::anyhow!(
+                        "operating environment provides no directory for application data"
+                    )
+                })?;
+                path.join("pkarr-server")
+            }
+        };
+
+        let cache = Box::new(LmdbCache::open(&cache_path, config.cache_size)?);
+
+        let rate_limiter = rate_limiting::IpRateLimiter::new(&config.rate_limiter);
+
+        let server = Box::new(DhtServer::new(
+            cache.clone(),
+            config.pkarr_config.resolvers.clone(),
+            config.pkarr_config.minimum_ttl,
+            config.pkarr_config.maximum_ttl,
+            rate_limiter.clone(),
+        ));
+
+        config.pkarr_config.dht_config.server = Some(server);
+        config.pkarr_config.cache = Some(cache);
+
+        let client = Client::new(config.pkarr_config)?;
+
+        let listener = TcpListener::bind(SocketAddr::from(([0, 0, 0, 0], config.http_port)))?;
 
         let resolver_address = *client.info()?.local_addr()?;
         let relay_address = listener.local_addr()?;
diff --git a/server/src/main.rs b/server/src/main.rs
index 786b6cc..4b97ff5 100644
--- a/server/src/main.rs
+++ b/server/src/main.rs
@@ -3,7 +3,7 @@ use clap::Parser;
 use std::path::PathBuf;
 use tracing::{debug, info};
 
-use pkarr_server::{Config, Relay};
+use pkarr_relay::{Config, Relay};
 
 #[derive(Parser, Debug)]
 struct Cli {

From 5f58f13f891807a0e2994939f10452a138af43ac Mon Sep 17 00:00:00 2001
From: nazeh <ar.nazeh@gmail.com>
Date: Thu, 12 Dec 2024 12:00:32 +0300
Subject: [PATCH 7/8] feat(relay): rename server to relay everywhere that is
 relevant

---
 Cargo.toml                                |  2 +-
 Dockerfile                                | 10 +++----
 README.md                                 | 34 +++++++++++------------
 flake.nix                                 | 10 +++----
 {server => relay}/CHANGELOG.md            |  0
 {server => relay}/Cargo.toml              |  0
 {server => relay}/README.md               | 27 +++++-------------
 {server => relay}/src/config.example.toml |  0
 {server => relay}/src/config.rs           |  2 +-
 {server => relay}/src/dht_server.rs       |  0
 {server => relay}/src/error.rs            |  0
 {server => relay}/src/handlers.rs         |  0
 {server => relay}/src/lib.rs              |  2 +-
 {server => relay}/src/main.rs             |  0
 {server => relay}/src/rate_limiting.rs    |  0
 15 files changed, 37 insertions(+), 50 deletions(-)
 rename {server => relay}/CHANGELOG.md (100%)
 rename {server => relay}/Cargo.toml (100%)
 rename {server => relay}/README.md (51%)
 rename {server => relay}/src/config.example.toml (100%)
 rename {server => relay}/src/config.rs (98%)
 rename {server => relay}/src/dht_server.rs (100%)
 rename {server => relay}/src/error.rs (100%)
 rename {server => relay}/src/handlers.rs (100%)
 rename {server => relay}/src/lib.rs (99%)
 rename {server => relay}/src/main.rs (100%)
 rename {server => relay}/src/rate_limiting.rs (100%)

diff --git a/Cargo.toml b/Cargo.toml
index c2b7906..0f10b1c 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
 [workspace]
 members = [
-  "pkarr", "server",
+  "pkarr", "relay",
 ]
 
 # See: https://github.com/rust-lang/rust/issues/90148#issuecomment-949194352
diff --git a/Dockerfile b/Dockerfile
index d66c275..bef4bac 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -33,7 +33,7 @@ COPY . .
 RUN cargo build --release --target x86_64-unknown-linux-musl
 
 # Strip the binary to reduce size
-RUN strip target/x86_64-unknown-linux-musl/release/pkarr-server
+RUN strip target/x86_64-unknown-linux-musl/release/pkarr-relay
 
 # ========================
 # Runtime Stage
@@ -44,13 +44,13 @@ FROM alpine:3.20
 RUN apk add --no-cache ca-certificates
 
 # Copy the compiled binary from the builder stage
-COPY --from=builder /usr/src/app/target/x86_64-unknown-linux-musl/release/pkarr-server /usr/local/bin/pkarr-server
+COPY --from=builder /usr/src/app/target/x86_64-unknown-linux-musl/release/pkarr-relay /usr/local/bin/pkarr-relay
 
 # Set the working directory
 WORKDIR /usr/local/bin
 
-# Expose the port the pkarr server listens on (should match that of config.toml)
+# Expose the port the pkarr relay listens on (should match that of config.toml)
 EXPOSE 6881
 
-# Set the default command to run the homeserver binary
-CMD ["pkarr-server", "--config=./config.toml"]
+# Set the default command to run the relay binary
+CMD ["pkarr-relay", "--config=./config.toml"]
diff --git a/README.md b/README.md
index 73bd88d..84dfd5a 100644
--- a/README.md
+++ b/README.md
@@ -9,8 +9,8 @@ Where we are going, this [https://o4dksfbqk85ogzdb5osziw6befigbuxmuxkuxq8434q89u
 ## TLDR
 - To publish resource records for your key, sign a small encoded DNS packet (<= 1000 bytes) and publish it on the DHT (through a relay if necessary).
 - To resolve some key's resources, applications query the DHT directly, or through a [relay](./design/relays.md), and verify the signature themselves. 
-- Clients and Pkarr servers cache records extensively and minimize DHT traffic as much as possible for improved scalability. 
-- The DHT drops records after a few hours, so users, their friends, or service providers should periodically republish their records to the DHT. Also Pkarr servers could republish records recently requested, to keep popular records alive too.
+- Clients and Relays cache records extensively and minimize DHT traffic as much as possible for improved scalability. 
+- The DHT drops records after a few hours, so users, their friends, or service providers should periodically republish their records to the DHT. Also Pkarr relays could republish records recently requested, to keep popular records alive too.
 - Optional: Existing applications unaware of Pkarr can still function if the user added a Pkarr-aware DNS servers to their operating system DNS servers. 
 
 ## DEMO 
@@ -30,14 +30,14 @@ Or if you prefer Rust [Examples](./pkarr/examples/README.md)
 ```mermaid
 sequenceDiagram
     participant Client
-    participant Server
+    participant Relay
     participant DHT
     participant Republisher
 
-    Client->>Server: Publish
-    note over Server: Optional Pkarr Server
-    Server->>DHT: Put
-    Note over Server,DHT: Store signed DNS packet
+    Client->>Relay: Publish
+    note over Relay: Optional Pkarr Relay
+    Relay->>DHT: Put
+    Note over Relay,DHT: Store signed DNS packet
 
     Client->>Republisher: Republish request
     note over Client, Republisher: Notify Hosting provider mentioned in RRs
@@ -46,31 +46,31 @@ sequenceDiagram
         Republisher->>DHT: Republish
     end
 
-    Client->>Server: Resolve
-    Server->>DHT: Get
-    DHT->>Server: Response
-    Server->>Client: Response
+    Client->>Relay: Resolve
+    Relay->>DHT: Get
+    DHT->>Relay: Response
+    Relay->>Client: Response
 ```
 
 ### Clients
 #### Pkarr enabled applications.
  
-Native applications, can directly query and verify signed records from the DHT if they are not behind NAT. Otherwise, they will need to use a Pkarr server as a relay.
+Native applications, can directly query and verify signed records from the DHT if they are not behind NAT. Otherwise, they will need to use a Pkarr Relay.
 
-Browser web apps should try calling local Pkarr server at the default port `6881`, if not accessible, they have to query a remote server instead. Eitherway, these apps should allow users to configure servers of their choice.
+Browser web apps should try calling local Pkarr relay at the default port `6881`, if not accessible, they have to query a remote relay in parallel to fallback on. Eitherway, these apps should allow users to configure relays of their choice.
  
-Clients with private keys are also capable of submitting signed records either to the DHT directly, or through Pkarr relay server, to update user's records when needed.
+Clients with private keys are also capable of submitting signed records either to the DHT directly, or through Pkarr relay, to update user's records when needed.
  
 #### Existing applications
 To support existing applications totally oblivious of Pkarr, users will have to (manually or programatically) edit their OS DNS servers to add one or more DNS servers that recognize Pkarr and query the DHT to resolve packets from there. However, the best outcome would be adoption from existing widely used resolvers like `1.1.1.1` and `8.8.8.8`.
 
-### Servers
+### Relays
 
 Pkarr relays are optional but they:
 1. Act as [relays](https://pkarr.org/relays) to enable web applications to query the DHT.
 2. Act as [resolvers](https://pkarr.org/resolvers) to provide lower latency, more reliability and scalability.
 
-Relays are very light and cheap to operate, that they can easily run altruistically, but private, and paid servers are possible too.
+Relays are very light and cheap to operate, that they can easily run altruistically, but private, and paid relays are possible too.
 
 ### Republishers
 
@@ -96,7 +96,7 @@ To ensure a good chance of scalability and resilience, a few expectations need t
     - Popular records may or may not be refreshed by the DNS servers as they get queries for them.
 2. This is **not a realtime communication** medium
     - Records are heavily cached like in any DNS system.
-    - You are expected to update your records rarely, so you should expect servers to enforce harsh rate-limiting and maybe demand proof of work.
+    - You are expected to update your records rarely, so you should expect relays to enforce harsh rate-limiting.
     - Records are going to be cached heavily to reduce traffic on the DHT, so updates might take some time to propagate, even if you set TTL to 1 second.
     - In case of a chache miss, traversing the DHT might take few seconds.
 
diff --git a/flake.nix b/flake.nix
index 58b539f..2af21ba 100644
--- a/flake.nix
+++ b/flake.nix
@@ -28,7 +28,7 @@
           "Cargo.toml"
           "Cargo.lock"
           "pkarr"
-          "server"
+          "relay"
         ];
 
         buildSrc = flakeboxLib.filterSubPaths {
@@ -56,16 +56,16 @@
             workspaceBuild = craneLib.buildWorkspace {
               cargoArtifacts = workspaceDeps;
             };
-            "pkarr-server" =  craneLib.buildPackageGroup {
-              packages = [ "pkarr-server" ];
-              mainProgram = "pkarr-server";
+            "pkarr-relay" =  craneLib.buildPackageGroup {
+              packages = [ "pkarr-relay" ];
+              mainProgram = "pkarr-relay";
             };
           }
         );
       in
       {
         packages = {
-          pkarr-server = multiBuild.pkarr-server;
+          pkarr-relay = multiBuild.pkarr-relay;
         };
 
         legacyPackages = multiBuild;
diff --git a/server/CHANGELOG.md b/relay/CHANGELOG.md
similarity index 100%
rename from server/CHANGELOG.md
rename to relay/CHANGELOG.md
diff --git a/server/Cargo.toml b/relay/Cargo.toml
similarity index 100%
rename from server/Cargo.toml
rename to relay/Cargo.toml
diff --git a/server/README.md b/relay/README.md
similarity index 51%
rename from server/README.md
rename to relay/README.md
index bc51165..5f56973 100644
--- a/server/README.md
+++ b/relay/README.md
@@ -1,4 +1,4 @@
-# Pkarr Server
+# Pkarr Relay
 
 A server that functions as a [pkarr](https://github.com/Nuhvi/pkarr/) [relay](https://pkarr.org/relays) and
 [resolver](https://pkarr.org/resolvers).
@@ -20,17 +20,17 @@ cp src/config.example.toml config.toml
 Run with an optional config file
 
 ```bash
-../target/release/pkarr-server --config=./config.toml
+../target/release/pkarr-relay --config=./config.toml
 ```
 
 You can customize logging levels
 
 ```bash
-../target/release/pkarr-server --config=./config.toml -t=pkarr=debug,tower_http=debug
+../target/release/pkarr-relay --config=./config.toml -t=pkarr=debug,tower_http=debug
 ```
 
 ## Using Docker
-To build and run the Pkarr server using Docker, this repository has a `Dockerfile` in the top level. You could use a small `docker-compose.yml` such as:
+To build and run the Pkarr relay using Docker, this repository has a `Dockerfile` in the top level. You could use a small `docker-compose.yml` such as:
 
 ```
 services:
@@ -40,23 +40,10 @@ services:
     volumes: 
       - ./config.toml:/config.toml
       - .pkarr_cache:/cache
-    command: pkarr-server --config=/config.toml
+    command: pkarr-relay --config=/config.toml
 ```
 Alternatively, lunch docker correctly attaching the `config.toml` as a volume in the right location. In the example above `.pkarr_cache` relative directory is used to permanently store pkarr cached keys.
 
-An example `./config.toml` here (we are mounting it on the container)
-```
-relay_port = 6881
-dht_port = 6881
-cache_path = "/cache"
-cache_size = 1_000_000
-resolvers = []
-minimum_ttl = 300
-maximum_ttl = 86400
-[rate_limiter]
-behind_proxy = false
-per_second = 2
-burst_size = 10
-```
+An example `./config.toml` can be copied from `./src/config.example.toml` and customized as needed.
 
-This will make the Pkarr server accessible at http://localhost:6881.
\ No newline at end of file
+This will make the Pkarr relay accessible at http://localhost:6881.
diff --git a/server/src/config.example.toml b/relay/src/config.example.toml
similarity index 100%
rename from server/src/config.example.toml
rename to relay/src/config.example.toml
diff --git a/server/src/config.rs b/relay/src/config.rs
similarity index 98%
rename from server/src/config.rs
rename to relay/src/config.rs
index 9de7c8e..ffa799c 100644
--- a/server/src/config.rs
+++ b/relay/src/config.rs
@@ -1,4 +1,4 @@
-//! Configuration for the server
+//! Configuration for Pkarr relay
 
 use anyhow::{Context, Result};
 use serde::{Deserialize, Serialize};
diff --git a/server/src/dht_server.rs b/relay/src/dht_server.rs
similarity index 100%
rename from server/src/dht_server.rs
rename to relay/src/dht_server.rs
diff --git a/server/src/error.rs b/relay/src/error.rs
similarity index 100%
rename from server/src/error.rs
rename to relay/src/error.rs
diff --git a/server/src/handlers.rs b/relay/src/handlers.rs
similarity index 100%
rename from server/src/handlers.rs
rename to relay/src/handlers.rs
diff --git a/server/src/lib.rs b/relay/src/lib.rs
similarity index 99%
rename from server/src/lib.rs
rename to relay/src/lib.rs
index 821e366..660108f 100644
--- a/server/src/lib.rs
+++ b/relay/src/lib.rs
@@ -101,7 +101,7 @@ impl Relay {
                         "operating environment provides no directory for application data"
                     )
                 })?;
-                path.join("pkarr-server")
+                path.join("pkarr-relay")
             }
         };
 
diff --git a/server/src/main.rs b/relay/src/main.rs
similarity index 100%
rename from server/src/main.rs
rename to relay/src/main.rs
diff --git a/server/src/rate_limiting.rs b/relay/src/rate_limiting.rs
similarity index 100%
rename from server/src/rate_limiting.rs
rename to relay/src/rate_limiting.rs

From 8069e1e70d0c6d3ffc047006e720f29134bad111 Mon Sep 17 00:00:00 2001
From: nazeh <ar.nazeh@gmail.com>
Date: Thu, 12 Dec 2024 12:02:49 +0300
Subject: [PATCH 8/8] docs: fix few places that uses Settings instead of Config

---
 pkarr/src/client/dht.rs       | 2 +-
 pkarr/src/extra/lmdb_cache.rs | 2 +-
 relay/src/config.rs           | 6 +++---
 3 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/pkarr/src/client/dht.rs b/pkarr/src/client/dht.rs
index 0038ee5..ffa28af 100644
--- a/pkarr/src/client/dht.rs
+++ b/pkarr/src/client/dht.rs
@@ -122,7 +122,7 @@ impl ClientBuilder {
         self
     }
 
-    /// Convienent methot to set the [mainline::Settings::bootstrap] from [mainline::Testnet::bootstrap]
+    /// Convienent methot to set the [mainline::Config::bootstrap] from [mainline::Testnet::bootstrap]
     pub fn testnet(mut self, testnet: &Testnet) -> Self {
         self.0.dht_config.bootstrap = testnet.bootstrap.clone();
 
diff --git a/pkarr/src/extra/lmdb_cache.rs b/pkarr/src/extra/lmdb_cache.rs
index bebf879..f7c3a3e 100644
--- a/pkarr/src/extra/lmdb_cache.rs
+++ b/pkarr/src/extra/lmdb_cache.rs
@@ -68,7 +68,7 @@ impl<'a> BytesDecode<'a> for SignedPacketCodec {
 }
 
 #[derive(Debug, Clone)]
-/// Persistent [crate::base::cache::Cache] implementation using LMDB's bindings [heed]
+/// Persistent [crate::Cache] implementation using LMDB's bindings [heed]
 pub struct LmdbCache {
     capacity: usize,
     env: Env,
diff --git a/relay/src/config.rs b/relay/src/config.rs
index ffa799c..68e7fc3 100644
--- a/relay/src/config.rs
+++ b/relay/src/config.rs
@@ -17,9 +17,9 @@ struct ConfigToml {
     mainline: Option<MainlineConfig>,
     cache_path: Option<String>,
     cache_size: Option<usize>,
-    /// See [pkarr::Settings::minimum_ttl]
+    /// See [pkarr::Config::minimum_ttl]
     minimum_ttl: Option<u32>,
-    /// See [pkarr::Settings::maximum_ttl]
+    /// See [pkarr::Config::maximum_ttl]
     maximum_ttl: Option<u32>,
     rate_limiter: RateLimiterConfig,
 }
@@ -49,7 +49,7 @@ pub struct Config {
     ///
     /// Defaults to a directory in the OS data directory
     pub cache_path: Option<PathBuf>,
-    /// See [pkarr::client::Settings::cache_size]
+    /// See [pkarr::client::Config::cache_size]
     ///
     /// Defaults to 1000_000
     pub cache_size: usize,