Skip to content

Commit e6f56d3

Browse files
authored
feat: pkarr relay with DNS server (#2167)
## 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.
1 parent b857a13 commit e6f56d3

30 files changed

+3107
-33
lines changed

Cargo.lock

Lines changed: 328 additions & 32 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ members = [
33
"iroh",
44
"iroh-bytes",
55
"iroh-base",
6+
"iroh-dns-server",
67
"iroh-gossip",
78
"iroh-metrics",
89
"iroh-net",

iroh-dns-server/Cargo.toml

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
[package]
2+
name = "iroh-dns-server"
3+
version = "0.1.0"
4+
edition = "2021"
5+
description = "A pkarr relay and DNS server"
6+
license = "MIT OR Apache-2.0"
7+
authors = ["Frando <franz@n0.computer>", "n0 team"]
8+
repository = "https://github.com/n0-computer/iroh-dns-server"
9+
keywords = ["networking", "pkarr", "dns", "dns-server", "iroh"]
10+
readme = "README.md"
11+
12+
[dependencies]
13+
anyhow = "1.0.80"
14+
async-trait = "0.1.77"
15+
axum = { version = "0.7.4", features = ["macros"] }
16+
axum-server = { version = "0.6.0", features = ["tls-rustls"] }
17+
base64-url = "2.0.2"
18+
bytes = "1.5.0"
19+
clap = { version = "4.5.1", features = ["derive"] }
20+
derive_more = { version = "1.0.0-beta.1", features = ["debug", "display", "into", "from"] }
21+
dirs-next = "2.0.0"
22+
futures = "0.3.30"
23+
governor = "0.6.3"
24+
hickory-proto = "0.24.0"
25+
hickory-server = { version = "0.24.0", features = ["dns-over-rustls"] }
26+
http = "1.0.0"
27+
iroh-metrics = { version = "0.13.0", path = "../iroh-metrics" }
28+
lru = "0.12.3"
29+
parking_lot = "0.12.1"
30+
pkarr = { version = "1.1.2", features = [ "async", "relay"], default_features = false }
31+
rcgen = "0.12.1"
32+
redb = "2.0.0"
33+
regex = "1.10.3"
34+
rustls = "0.21"
35+
rustls-pemfile = "1"
36+
serde = { version = "1.0.197", features = ["derive"] }
37+
struct_iterable = "0.1.1"
38+
strum = { version = "0.26.1", features = ["derive"] }
39+
tokio = { version = "1.36.0", features = ["full"] }
40+
tokio-rustls = "0.24"
41+
tokio-rustls-acme = { version = "0.3", features = ["axum"] }
42+
tokio-stream = "0.1.14"
43+
tokio-util = "0.7.10"
44+
toml = "0.8.10"
45+
tower-http = { version = "0.5.2", features = ["cors", "trace"] }
46+
tower_governor = "0.3.2"
47+
tracing = "0.1.40"
48+
tracing-subscriber = "0.3.18"
49+
url = "2.5.0"
50+
z32 = "1.1.1"
51+
52+
[dev-dependencies]
53+
hickory-resolver = "0.24.0"
54+
iroh-net = { version = "0.13.0", path = "../iroh-net" }

iroh-dns-server/README.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# iroh-dns-server
2+
3+
A server that functions as a [pkarr](https://github.com/Nuhvi/pkarr/) relay and
4+
[DNS](https://de.wikipedia.org/wiki/Domain_Name_System) server.
5+
6+
This server compiles to a binary `iroh-dns-server`. It needs a config file, of
7+
which there are two examples included:
8+
9+
- [`config.dev.toml`](./config.dev.toml) - suitable for local development
10+
- [`config.prod.toml`](./config.dev.toml) - suitable for production, after
11+
adjusting the domain names and IP addresses
12+
13+
The server will expose the following services:
14+
15+
- A DNS server listening on UDP and TCP for DNS queries
16+
- A HTTP and/or HTTPS server which provides the following routes:
17+
- `/pkarr`: `GET` and `PUT` for pkarr signed packets
18+
- `/dns-query`: Answer DNS queries over
19+
[DNS-over-HTTPS](https://datatracker.ietf.org/doc/html/rfc8484)
20+
21+
All received and valid pkarr signed packets will be served over DNS. The pkarr
22+
packet origin will be appended with the origin as configured by this server.
23+
24+
# License
25+
26+
This project is licensed under either of
27+
28+
- Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or
29+
http://www.apache.org/licenses/LICENSE-2.0)
30+
- MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT)
31+
32+
at your option.
33+
34+
### Contribution
35+
36+
Unless you explicitly state otherwise, any contribution intentionally submitted
37+
for inclusion in this project by you, as defined in the Apache-2.0 license,
38+
shall be dual licensed as above, without any additional terms or conditions.

iroh-dns-server/config.dev.toml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
[http]
2+
port = 8080
3+
bind_addr = "127.0.0.1"
4+
5+
[https]
6+
port = 8443
7+
bind_addr = "127.0.0.1"
8+
domains = ["localhost"]
9+
cert_mode = "self_signed"
10+
11+
[dns]
12+
port = 5300
13+
bind_addr = "127.0.0.1"
14+
default_soa = "dns1.irohdns.example hostmaster.irohdns.example 0 10800 3600 604800 3600"
15+
default_ttl = 900
16+
origins = ["irohdns.example.", "."]
17+
rr_a = "127.0.0.1"
18+
rr_ns = "ns1.irohdns.example."

iroh-dns-server/config.prod.toml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
[https]
2+
port = 443
3+
domains = ["irohdns.example.org"]
4+
cert_mode = "lets_encrypt"
5+
letsencrypt_prod = true
6+
7+
[dns]
8+
port = 53
9+
default_soa = "dns1.irohdns.example.org hostmaster.irohdns.example.org 0 10800 3600 604800 3600"
10+
default_ttl = 30
11+
origins = ["irohdns.example.org", "."]
12+
rr_a = "203.0.10.10"
13+
rr_ns = "ns1.irohdns.example.org."

iroh-dns-server/examples/convert.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
use std::str::FromStr;
2+
3+
use clap::Parser;
4+
use iroh_net::NodeId;
5+
6+
#[derive(Debug, Parser)]
7+
struct Cli {
8+
#[clap(subcommand)]
9+
command: Command,
10+
}
11+
12+
#[derive(Debug, Parser)]
13+
enum Command {
14+
NodeToPkarr { node_id: String },
15+
PkarrToNode { z32_pubkey: String },
16+
}
17+
18+
fn main() -> anyhow::Result<()> {
19+
let args = Cli::parse();
20+
match args.command {
21+
Command::NodeToPkarr { node_id } => {
22+
let node_id = NodeId::from_str(&node_id)?;
23+
let public_key = pkarr::PublicKey::try_from(*node_id.as_bytes())?;
24+
println!("{}", public_key.to_z32())
25+
}
26+
Command::PkarrToNode { z32_pubkey } => {
27+
let public_key = pkarr::PublicKey::try_from(z32_pubkey.as_str())?;
28+
let node_id = NodeId::from_bytes(public_key.as_bytes())?;
29+
println!("{}", node_id)
30+
}
31+
}
32+
Ok(())
33+
}

iroh-dns-server/examples/publish.rs

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
use std::str::FromStr;
2+
3+
use anyhow::{bail, Result};
4+
use clap::{Parser, ValueEnum};
5+
use iroh_net::{
6+
discovery::{
7+
dns::N0_DNS_NODE_ORIGIN,
8+
pkarr_publish::{PkarrRelayClient, N0_DNS_PKARR_RELAY},
9+
},
10+
dns::node_info::{to_z32, NodeInfo, IROH_TXT_NAME},
11+
key::SecretKey,
12+
NodeId,
13+
};
14+
use url::Url;
15+
16+
const LOCALHOST_PKARR: &str = "http://localhost:8080/pkarr";
17+
const EXAMPLE_ORIGIN: &str = "irohdns.example";
18+
19+
#[derive(ValueEnum, Clone, Debug, Default, Copy, strum::Display)]
20+
#[strum(serialize_all = "kebab-case")]
21+
pub enum Env {
22+
/// Use the pkarr relay run by number0.
23+
#[default]
24+
Default,
25+
/// Use a relay listening at http://localhost:8080
26+
Dev,
27+
}
28+
29+
/// Publish a record to an irohdns server.
30+
///
31+
/// You have to set the IROH_SECRET environment variable to the node secret for which to publish.
32+
#[derive(Parser, Debug)]
33+
struct Cli {
34+
/// Environment to publish to.
35+
#[clap(value_enum, short, long, default_value_t = Env::Default)]
36+
env: Env,
37+
/// Pkarr Relay URL. If set, the --env option will be ignored.
38+
#[clap(long, conflicts_with = "env")]
39+
pkarr_relay: Option<Url>,
40+
/// Home relay server to publish for this node
41+
relay_url: Url,
42+
/// Create a new node secret if IROH_SECRET is unset. Only for development / debugging.
43+
#[clap(short, long)]
44+
create: bool,
45+
}
46+
47+
#[tokio::main]
48+
async fn main() -> Result<()> {
49+
tracing_subscriber::fmt::init();
50+
let args = Cli::parse();
51+
52+
let secret_key = match std::env::var("IROH_SECRET") {
53+
Ok(s) => SecretKey::from_str(&s)?,
54+
Err(_) if args.create => {
55+
let s = SecretKey::generate();
56+
println!("Generated a new node secret. To reuse, set");
57+
println!("IROH_SECRET={s}");
58+
s
59+
}
60+
Err(_) => {
61+
bail!("Environtment variable IROH_SECRET is not set. To create a new secret, use the --create option.")
62+
}
63+
};
64+
65+
let node_id = secret_key.public();
66+
let pkarr_relay = match (args.pkarr_relay, args.env) {
67+
(Some(pkarr_relay), _) => pkarr_relay,
68+
(None, Env::Default) => N0_DNS_PKARR_RELAY.parse().expect("valid url"),
69+
(None, Env::Dev) => LOCALHOST_PKARR.parse().expect("valid url"),
70+
};
71+
72+
println!("announce {node_id}:");
73+
println!(" relay={}", args.relay_url);
74+
println!();
75+
println!("publish to {pkarr_relay} ...");
76+
77+
let pkarr = PkarrRelayClient::new(pkarr_relay);
78+
let node_info = NodeInfo::new(node_id, Some(args.relay_url));
79+
let signed_packet = node_info.to_pkarr_signed_packet(&secret_key, 30)?;
80+
pkarr.publish(&signed_packet).await?;
81+
82+
println!("signed packet published.");
83+
println!("resolve with:");
84+
85+
match args.env {
86+
Env::Default => {
87+
println!(" cargo run --example resolve -- node {}", node_id);
88+
println!(" dig {} TXT", fmt_domain(&node_id, N0_DNS_NODE_ORIGIN))
89+
}
90+
Env::Dev => {
91+
println!(
92+
" cargo run --example resolve -- --env dev node {}",
93+
node_id
94+
);
95+
println!(
96+
" dig @localhost -p 5300 {} TXT",
97+
fmt_domain(&node_id, EXAMPLE_ORIGIN)
98+
)
99+
}
100+
}
101+
Ok(())
102+
}
103+
104+
fn fmt_domain(node_id: &NodeId, origin: &str) -> String {
105+
format!("{}.{}.{}", IROH_TXT_NAME, to_z32(node_id), origin)
106+
}

iroh-dns-server/examples/resolve.rs

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
use std::net::SocketAddr;
2+
3+
use clap::{Parser, ValueEnum};
4+
use hickory_resolver::{
5+
config::{NameServerConfig, Protocol, ResolverConfig},
6+
AsyncResolver,
7+
};
8+
use iroh_net::{
9+
discovery::dns::N0_DNS_NODE_ORIGIN,
10+
dns::{node_info::TxtAttrs, DnsResolver},
11+
NodeId,
12+
};
13+
14+
const LOCALHOST_DNS: &str = "127.0.0.1:5300";
15+
const EXAMPLE_ORIGIN: &str = "irohdns.example";
16+
17+
#[derive(ValueEnum, Clone, Debug, Default)]
18+
pub enum Env {
19+
/// Use the system's nameservers with origin domain dns.iroh.link
20+
#[default]
21+
Default,
22+
/// Use a localhost DNS server listening on port 5300
23+
Dev,
24+
}
25+
26+
#[derive(Debug, Parser)]
27+
struct Cli {
28+
#[clap(value_enum, short, long, default_value_t = Env::Default)]
29+
env: Env,
30+
#[clap(subcommand)]
31+
command: Command,
32+
}
33+
34+
#[derive(Debug, Parser)]
35+
enum Command {
36+
/// Resolve node info by node id.
37+
Node { node_id: NodeId },
38+
/// Resolve node info by domain.
39+
Domain { domain: String },
40+
}
41+
42+
#[tokio::main]
43+
async fn main() -> anyhow::Result<()> {
44+
let args = Cli::parse();
45+
let (resolver, origin) = match args.env {
46+
Env::Default => (
47+
iroh_net::dns::default_resolver().clone(),
48+
N0_DNS_NODE_ORIGIN,
49+
),
50+
Env::Dev => (
51+
resolver_with_nameserver(LOCALHOST_DNS.parse()?),
52+
EXAMPLE_ORIGIN,
53+
),
54+
};
55+
let resolved = match args.command {
56+
Command::Node { node_id } => {
57+
TxtAttrs::<String>::lookup_by_id(&resolver, &node_id, origin).await?
58+
}
59+
Command::Domain { domain } => {
60+
TxtAttrs::<String>::lookup_by_domain(&resolver, &domain).await?
61+
}
62+
};
63+
println!("resolved node {}", resolved.node_id());
64+
for (key, values) in resolved.attrs() {
65+
for value in values {
66+
println!(" {key}={value}");
67+
}
68+
}
69+
Ok(())
70+
}
71+
72+
fn resolver_with_nameserver(nameserver: SocketAddr) -> DnsResolver {
73+
let mut config = ResolverConfig::new();
74+
let nameserver_config = NameServerConfig::new(nameserver, Protocol::Udp);
75+
config.add_name_server(nameserver_config);
76+
AsyncResolver::tokio(config, Default::default())
77+
}

0 commit comments

Comments
 (0)