Skip to content

Commit

Permalink
feat: pkarr relay with DNS server (#2167)
Browse files Browse the repository at this point in the history
## Description

Imports https://github.com/n0-computer/iroh-dns-server into this repo.

See n0-computer/iroh-dns-server#5 for previous review/discussion.

Now includes an integration smoke test in `iroh-dns-server/src/lib.rs`.

## Notes & open questions

I *think* I addressed most review points that came up in the initial
review. Prominently still open is:

* The `redb` store is used from async context but only exposes a sync
interface (redb default). I think this is fine for medium load. However
for better performance we should reuse transactions, which likely means
we need an actor on a separate thread, as we do in iroh-bytes and
iroh-sync.

## Change checklist

- [ ] Self-review.
- [ ] Documentation updates if relevant.
- [ ] Tests if relevant.
  • Loading branch information
Frando authored Apr 11, 2024
1 parent b857a13 commit e6f56d3
Show file tree
Hide file tree
Showing 30 changed files with 3,107 additions and 33 deletions.
360 changes: 328 additions & 32 deletions Cargo.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ members = [
"iroh",
"iroh-bytes",
"iroh-base",
"iroh-dns-server",
"iroh-gossip",
"iroh-metrics",
"iroh-net",
Expand Down
54 changes: 54 additions & 0 deletions iroh-dns-server/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
[package]
name = "iroh-dns-server"
version = "0.1.0"
edition = "2021"
description = "A pkarr relay and DNS server"
license = "MIT OR Apache-2.0"
authors = ["Frando <franz@n0.computer>", "n0 team"]
repository = "https://github.com/n0-computer/iroh-dns-server"
keywords = ["networking", "pkarr", "dns", "dns-server", "iroh"]
readme = "README.md"

[dependencies]
anyhow = "1.0.80"
async-trait = "0.1.77"
axum = { version = "0.7.4", features = ["macros"] }
axum-server = { version = "0.6.0", features = ["tls-rustls"] }
base64-url = "2.0.2"
bytes = "1.5.0"
clap = { version = "4.5.1", features = ["derive"] }
derive_more = { version = "1.0.0-beta.1", features = ["debug", "display", "into", "from"] }
dirs-next = "2.0.0"
futures = "0.3.30"
governor = "0.6.3"
hickory-proto = "0.24.0"
hickory-server = { version = "0.24.0", features = ["dns-over-rustls"] }
http = "1.0.0"
iroh-metrics = { version = "0.13.0", path = "../iroh-metrics" }
lru = "0.12.3"
parking_lot = "0.12.1"
pkarr = { version = "1.1.2", features = [ "async", "relay"], default_features = false }
rcgen = "0.12.1"
redb = "2.0.0"
regex = "1.10.3"
rustls = "0.21"
rustls-pemfile = "1"
serde = { version = "1.0.197", features = ["derive"] }
struct_iterable = "0.1.1"
strum = { version = "0.26.1", features = ["derive"] }
tokio = { version = "1.36.0", features = ["full"] }
tokio-rustls = "0.24"
tokio-rustls-acme = { version = "0.3", features = ["axum"] }
tokio-stream = "0.1.14"
tokio-util = "0.7.10"
toml = "0.8.10"
tower-http = { version = "0.5.2", features = ["cors", "trace"] }
tower_governor = "0.3.2"
tracing = "0.1.40"
tracing-subscriber = "0.3.18"
url = "2.5.0"
z32 = "1.1.1"

[dev-dependencies]
hickory-resolver = "0.24.0"
iroh-net = { version = "0.13.0", path = "../iroh-net" }
38 changes: 38 additions & 0 deletions iroh-dns-server/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# iroh-dns-server

A server that functions as a [pkarr](https://github.com/Nuhvi/pkarr/) relay and
[DNS](https://de.wikipedia.org/wiki/Domain_Name_System) server.

This server compiles to a binary `iroh-dns-server`. It needs a config file, of
which there are two examples included:

- [`config.dev.toml`](./config.dev.toml) - suitable for local development
- [`config.prod.toml`](./config.dev.toml) - suitable for production, after
adjusting the domain names and IP addresses

The server will expose the following services:

- A DNS server listening on UDP and TCP for DNS queries
- A HTTP and/or HTTPS server which provides the following routes:
- `/pkarr`: `GET` and `PUT` for pkarr signed packets
- `/dns-query`: Answer DNS queries over
[DNS-over-HTTPS](https://datatracker.ietf.org/doc/html/rfc8484)

All received and valid pkarr signed packets will be served over DNS. The pkarr
packet origin will be appended with the origin as configured by this server.

# License

This project is licensed under either of

- Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or
http://www.apache.org/licenses/LICENSE-2.0)
- MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT)

at your option.

### Contribution

Unless you explicitly state otherwise, any contribution intentionally submitted
for inclusion in this project by you, as defined in the Apache-2.0 license,
shall be dual licensed as above, without any additional terms or conditions.
18 changes: 18 additions & 0 deletions iroh-dns-server/config.dev.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[http]
port = 8080
bind_addr = "127.0.0.1"

[https]
port = 8443
bind_addr = "127.0.0.1"
domains = ["localhost"]
cert_mode = "self_signed"

[dns]
port = 5300
bind_addr = "127.0.0.1"
default_soa = "dns1.irohdns.example hostmaster.irohdns.example 0 10800 3600 604800 3600"
default_ttl = 900
origins = ["irohdns.example.", "."]
rr_a = "127.0.0.1"
rr_ns = "ns1.irohdns.example."
13 changes: 13 additions & 0 deletions iroh-dns-server/config.prod.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[https]
port = 443
domains = ["irohdns.example.org"]
cert_mode = "lets_encrypt"
letsencrypt_prod = true

[dns]
port = 53
default_soa = "dns1.irohdns.example.org hostmaster.irohdns.example.org 0 10800 3600 604800 3600"
default_ttl = 30
origins = ["irohdns.example.org", "."]
rr_a = "203.0.10.10"
rr_ns = "ns1.irohdns.example.org."
33 changes: 33 additions & 0 deletions iroh-dns-server/examples/convert.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
use std::str::FromStr;

use clap::Parser;
use iroh_net::NodeId;

#[derive(Debug, Parser)]
struct Cli {
#[clap(subcommand)]
command: Command,
}

#[derive(Debug, Parser)]
enum Command {
NodeToPkarr { node_id: String },
PkarrToNode { z32_pubkey: String },
}

fn main() -> anyhow::Result<()> {
let args = Cli::parse();
match args.command {
Command::NodeToPkarr { node_id } => {
let node_id = NodeId::from_str(&node_id)?;
let public_key = pkarr::PublicKey::try_from(*node_id.as_bytes())?;
println!("{}", public_key.to_z32())
}
Command::PkarrToNode { z32_pubkey } => {
let public_key = pkarr::PublicKey::try_from(z32_pubkey.as_str())?;
let node_id = NodeId::from_bytes(public_key.as_bytes())?;
println!("{}", node_id)
}
}
Ok(())
}
106 changes: 106 additions & 0 deletions iroh-dns-server/examples/publish.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
use std::str::FromStr;

use anyhow::{bail, Result};
use clap::{Parser, ValueEnum};
use iroh_net::{
discovery::{
dns::N0_DNS_NODE_ORIGIN,
pkarr_publish::{PkarrRelayClient, N0_DNS_PKARR_RELAY},
},
dns::node_info::{to_z32, NodeInfo, IROH_TXT_NAME},
key::SecretKey,
NodeId,
};
use url::Url;

const LOCALHOST_PKARR: &str = "http://localhost:8080/pkarr";
const EXAMPLE_ORIGIN: &str = "irohdns.example";

#[derive(ValueEnum, Clone, Debug, Default, Copy, strum::Display)]
#[strum(serialize_all = "kebab-case")]
pub enum Env {
/// Use the pkarr relay run by number0.
#[default]
Default,
/// Use a relay listening at http://localhost:8080
Dev,
}

/// Publish a record to an irohdns server.
///
/// You have to set the IROH_SECRET environment variable to the node secret for which to publish.
#[derive(Parser, Debug)]
struct Cli {
/// Environment to publish to.
#[clap(value_enum, short, long, default_value_t = Env::Default)]
env: Env,
/// Pkarr Relay URL. If set, the --env option will be ignored.
#[clap(long, conflicts_with = "env")]
pkarr_relay: Option<Url>,
/// Home relay server to publish for this node
relay_url: Url,
/// Create a new node secret if IROH_SECRET is unset. Only for development / debugging.
#[clap(short, long)]
create: bool,
}

#[tokio::main]
async fn main() -> Result<()> {
tracing_subscriber::fmt::init();
let args = Cli::parse();

let secret_key = match std::env::var("IROH_SECRET") {
Ok(s) => SecretKey::from_str(&s)?,
Err(_) if args.create => {
let s = SecretKey::generate();
println!("Generated a new node secret. To reuse, set");
println!("IROH_SECRET={s}");
s
}
Err(_) => {
bail!("Environtment variable IROH_SECRET is not set. To create a new secret, use the --create option.")
}
};

let node_id = secret_key.public();
let pkarr_relay = match (args.pkarr_relay, args.env) {
(Some(pkarr_relay), _) => pkarr_relay,
(None, Env::Default) => N0_DNS_PKARR_RELAY.parse().expect("valid url"),
(None, Env::Dev) => LOCALHOST_PKARR.parse().expect("valid url"),
};

println!("announce {node_id}:");
println!(" relay={}", args.relay_url);
println!();
println!("publish to {pkarr_relay} ...");

let pkarr = PkarrRelayClient::new(pkarr_relay);
let node_info = NodeInfo::new(node_id, Some(args.relay_url));
let signed_packet = node_info.to_pkarr_signed_packet(&secret_key, 30)?;
pkarr.publish(&signed_packet).await?;

println!("signed packet published.");
println!("resolve with:");

match args.env {
Env::Default => {
println!(" cargo run --example resolve -- node {}", node_id);
println!(" dig {} TXT", fmt_domain(&node_id, N0_DNS_NODE_ORIGIN))
}
Env::Dev => {
println!(
" cargo run --example resolve -- --env dev node {}",
node_id
);
println!(
" dig @localhost -p 5300 {} TXT",
fmt_domain(&node_id, EXAMPLE_ORIGIN)
)
}
}
Ok(())
}

fn fmt_domain(node_id: &NodeId, origin: &str) -> String {
format!("{}.{}.{}", IROH_TXT_NAME, to_z32(node_id), origin)
}
77 changes: 77 additions & 0 deletions iroh-dns-server/examples/resolve.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
use std::net::SocketAddr;

use clap::{Parser, ValueEnum};
use hickory_resolver::{
config::{NameServerConfig, Protocol, ResolverConfig},
AsyncResolver,
};
use iroh_net::{
discovery::dns::N0_DNS_NODE_ORIGIN,
dns::{node_info::TxtAttrs, DnsResolver},
NodeId,
};

const LOCALHOST_DNS: &str = "127.0.0.1:5300";
const EXAMPLE_ORIGIN: &str = "irohdns.example";

#[derive(ValueEnum, Clone, Debug, Default)]
pub enum Env {
/// Use the system's nameservers with origin domain dns.iroh.link
#[default]
Default,
/// Use a localhost DNS server listening on port 5300
Dev,
}

#[derive(Debug, Parser)]
struct Cli {
#[clap(value_enum, short, long, default_value_t = Env::Default)]
env: Env,
#[clap(subcommand)]
command: Command,
}

#[derive(Debug, Parser)]
enum Command {
/// Resolve node info by node id.
Node { node_id: NodeId },
/// Resolve node info by domain.
Domain { domain: String },
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
let args = Cli::parse();
let (resolver, origin) = match args.env {
Env::Default => (
iroh_net::dns::default_resolver().clone(),
N0_DNS_NODE_ORIGIN,
),
Env::Dev => (
resolver_with_nameserver(LOCALHOST_DNS.parse()?),
EXAMPLE_ORIGIN,
),
};
let resolved = match args.command {
Command::Node { node_id } => {
TxtAttrs::<String>::lookup_by_id(&resolver, &node_id, origin).await?
}
Command::Domain { domain } => {
TxtAttrs::<String>::lookup_by_domain(&resolver, &domain).await?
}
};
println!("resolved node {}", resolved.node_id());
for (key, values) in resolved.attrs() {
for value in values {
println!(" {key}={value}");
}
}
Ok(())
}

fn resolver_with_nameserver(nameserver: SocketAddr) -> DnsResolver {
let mut config = ResolverConfig::new();
let nameserver_config = NameServerConfig::new(nameserver, Protocol::Udp);
config.add_name_server(nameserver_config);
AsyncResolver::tokio(config, Default::default())
}
Loading

0 comments on commit e6f56d3

Please sign in to comment.