initial commit

This commit is contained in:
Severin Buhler
2024-01-30 17:58:06 +01:00
commit 299b071cbe
11 changed files with 2580 additions and 0 deletions

13
.gitignore vendored Normal file
View File

@@ -0,0 +1,13 @@
# Generated by Cargo
# will have compiled files and executables
debug/
target/
# These are backup files generated by rustfmt
**/*.rs.bk
# MSVC Windows builds of rustc generate these, which store debugging information
*.pdb
# Mac pollution
.DS_STORE

1928
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

16
Cargo.toml Normal file
View File

@@ -0,0 +1,16 @@
[package]
name = "pkdns"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
ctrlc = "3.4.2"
simple-dns = "0.6.0"
pknames_core = "0.1.0"
pkarr = "1.0.1"
zbase32 = "0.1.2"
ttl_cache = "0.5.1"
clap = "4.4.18"
any-dns = "0.1.1"

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (Severin Alexander Bühler) 2023
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

30
README.md Normal file
View File

@@ -0,0 +1,30 @@
# pkdns
DNS server resolving [pkarr](https://github.com/nuhvi/pkarr) self-sovereign domains.
## Getting Started
### Build Yourself
1. Run `cargo run`.
2. Configure you system to send DNS requests to `127.0.0.1:53`.
3. Test by going to [http://7fmjpcuuzf54hw18bsgi3zihzyh4awseeuq5tmojefaezjbd64cy/](http://7fmjpcuuzf54hw18bsgi3zihzyh4awseeuq5tmojefaezjbd64cy/).
## Options
```
Usage: pkdns [OPTIONS]
Options:
-f, --forward <forward> ICANN fallback DNS server. IP:Port [default: 192.168.1.1:53]
-s, --socket <socket> Socket the server should listen on. IP:Port [default: 0.0.0.0:53]
-v, --verbose Show verbose output.
--no-cache Disable DHT packet caching.
--threads <threads> Number of threads to process dns queries. [default: 4]
-d, --directory <directory> pknames source directory. [default: ~/.pknames]
-h, --help Print help
-V, --version Print version
```

36
build.sh Executable file
View File

@@ -0,0 +1,36 @@
#!/bin/bash
echo Build OSX amd64
cargo build --release --package=pkdns
echo Build Linux amd64
cargo build --release --package=pkdns --target=x86_64-unknown-linux-gnu
echo Build Windows amd64
cargo build --release --package=pkdns --target=x86_64-pc-windows-gnu
echo
echo Build packets
rm -rf target/github-release
cd target
mkdir github-release
echo Tar osx
mkdir github-release/pknames-osx-amd64
cp release/pkdns github-release/pknames-osx-amd64
cd github-release && tar -czf pknames-osx-amd64.tar.gz pknames-osx-amd64 && cd ..
rm -rf github-release/pknames-osx-amd64
echo Tar linux
mkdir github-release/pknames-linux-amd64
cp x86_64-unknown-linux-gnu/release/pkdns github-release/pknames-linux-amd64
cd github-release && tar -czf pknames-linux-amd64.tar.gz pknames-linux-amd64 && cd ..
rm -rf github-release/pknames-linux-amd64
echo Tar Windows
mkdir github-release/pknames-windows-amd64
cp x86_64-pc-windows-gnu/release/pkdns.exe github-release/pknames-windows-amd64
cd github-release && tar -czf pknames-windows-amd64.tar.gz pknames-windows-amd64 && cd ..
rm -rf github-release/pknames-windows-amd64
echo
cd ..
tree target/github-release

1
rustfmt.toml Normal file
View File

@@ -0,0 +1 @@
max_width = 120

152
src/main.rs Normal file
View File

@@ -0,0 +1,152 @@
use any_dns::{Builder, CustomHandler};
use ctrlc;
use pkarr::dns::Packet;
use pknames_resolver::PknamesResolver;
use std::{error::Error, net::SocketAddr, sync::mpsc::channel, time::Instant};
mod pkarr_cache;
mod pkarr_resolver;
mod pknames_resolver;
#[derive(Clone)]
struct MyHandler {
pub pkarr: PknamesResolver,
}
impl MyHandler {
pub fn new(max_cache_ttl: u64, config_dir_path: &str) -> Self {
Self {
pkarr: PknamesResolver::new(max_cache_ttl, config_dir_path),
}
}
}
impl CustomHandler for MyHandler {
fn lookup(&mut self, query: &Vec<u8>) -> std::prelude::v1::Result<Vec<u8>, Box<dyn Error>> {
let start = Instant::now();
let result = self.pkarr.resolve(query);
if result.is_ok() {
let query = Packet::parse(&query).unwrap();
println!(
"Resolved {:?} within {}ms",
query.questions.first().unwrap(),
start.elapsed().as_millis()
);
};
result
}
}
fn wait_on_ctrl_c() {
let (tx, rx) = channel();
ctrlc::set_handler(move || tx.send(()).expect("Could not send signal on channel."))
.expect("Error setting Ctrl-C handler");
rx.recv().expect("Could not receive from channel.");
}
fn main() -> Result<(), Box<dyn Error>> {
const VERSION: &str = env!("CARGO_PKG_VERSION");
let cmd = clap::Command::new("pkdns")
.about("A DNS server for pkarr self-sovereign domains.")
.version(VERSION)
.arg(
clap::Arg::new("forward")
.short('f')
.long("forward")
.required(false)
.default_value("192.168.1.1:53")
.help("ICANN fallback DNS server. IP:Port"),
)
.arg(
clap::Arg::new("socket")
.short('s')
.long("socket")
.required(false)
.default_value("0.0.0.0:53")
.help("Socket the server should listen on. IP:Port"),
)
.arg(
clap::Arg::new("verbose")
.short('v')
.long("verbose")
.required(false)
.num_args(0)
.help("Show verbose output."),
)
.arg(
clap::Arg::new("no-cache")
.long("no-cache")
.required(false)
.num_args(0)
.help("Disable DHT packet caching."),
)
.arg(
clap::Arg::new("threads")
.long("threads")
.required(false)
.default_value("4")
.help("Number of threads to process dns queries."),
)
.arg(
clap::Arg::new("directory")
.short('d')
.long("directory")
.required(false)
.help("pknames source directory.")
.default_value("~/.pknames"),
);
let matches = cmd.get_matches();
let verbose: bool = *matches.get_one("verbose").unwrap();
let no_cache: bool = *matches.get_one("no-cache").unwrap();
let directory: &String = matches.get_one("directory").unwrap();
let threads: &String = matches.get_one("threads").unwrap();
let threads: u8 = threads.parse().expect("threads should be valid positive integer.");
let forward: &String = matches.get_one("forward").unwrap();
let forward: SocketAddr = forward.parse().expect("forward should be valid IP:Port combination.");
let socket: &String = matches.get_one("socket").unwrap();
let socket: SocketAddr = socket.parse().expect("socket should be valid IP:Port combination.");
if verbose {
println!("Verbose mode");
}
if no_cache {
println!("Disabled DHT cache")
}
if threads != 4 {
println!("Use {} threads", threads);
}
if directory != "~/.pknames" {
println!("Use pknames directory {}", directory);
}
if forward.to_string() != "192.168.1.1:53" {
println!("Forward ICANN queries to {}", forward);
}
let max_ttl = if no_cache {
1
} else {
60*60
};
let anydns = Builder::new()
.handler(MyHandler::new(max_ttl, directory))
.threads(threads)
.verbose(verbose)
.icann_resolver(forward)
.listen(socket)
.build();
println!("Listening on {}. Waiting for Ctrl-C...", socket);
wait_on_ctrl_c();
println!("Got it! Exiting...");
anydns.join();
Ok(())
}

45
src/pkarr_cache.rs Normal file
View File

@@ -0,0 +1,45 @@
use std::time::Duration;
use pkarr::{dns::Packet, PublicKey};
use ttl_cache::TtlCache;
/**
* Pkarr record ttl cache
*/
pub struct PkarrPacketTtlCache{
cache: TtlCache<String, Vec<u8>>,
max_cache_ttl: u64,
}
impl PkarrPacketTtlCache {
pub fn new(max_cache_ttl: u64) -> Self {
PkarrPacketTtlCache{
cache: TtlCache::new(100),
max_cache_ttl
}
}
/**
* Adds packet and caches it for the ttl the least long lived answer is valid for.
*/
pub fn add(&mut self, pubkey: PublicKey, reply: Vec<u8>) {
let default_ttl = 1200;
let packet = Packet::parse(&reply).unwrap();
let min_ttl = packet.answers.iter().map(|answer| answer.ttl).min().unwrap_or(default_ttl) as u64;
let ttl = 60.max(min_ttl); // At least 1min
let ttl = ttl.min(self.max_cache_ttl);
let ttl = Duration::from_secs(ttl as u64);
self.cache.insert(pubkey.to_z32(), reply, ttl);
}
pub fn get(&self, pubkey: &PublicKey) -> Option<Vec<u8>> {
let z32 = pubkey.to_z32();
self.cache.get(&z32).map(|value| value.clone())
}
}

236
src/pkarr_resolver.rs Normal file
View File

@@ -0,0 +1,236 @@
use std::{error::Error, sync::{Arc, Mutex}};
use pkarr::{
dns::{Packet, ResourceRecord},
PkarrClient, PublicKey,
};
use crate::pkarr_cache::PkarrPacketTtlCache;
/**
* Pkarr resolver with cache.
*/
#[derive(Clone)]
pub struct PkarrResolver {
client: PkarrClient,
cache: Arc<Mutex<PkarrPacketTtlCache>>,
}
impl PkarrResolver {
pub fn new(max_cache_ttl: u64) -> Self {
Self {
client: PkarrClient::new(),
cache: Arc::new(Mutex::new(PkarrPacketTtlCache::new(max_cache_ttl))),
}
}
pub fn parse_pkarr_uri(uri: &str) -> Option<PublicKey> {
let decoded = zbase32::decode_full_bytes_str(uri);
if decoded.is_err() {
return None;
};
let decoded = decoded.unwrap();
if decoded.len() != 32 {
return None;
};
let trying: Result<PublicKey, _> = uri.try_into();
trying.ok()
}
fn resolve_pubkey_respect_cache(&mut self, pubkey: &PublicKey) -> Option<Vec<u8>> {
let mut cache = self.cache.lock().unwrap();
let cached_opt = cache.get(pubkey);
if cached_opt.is_some() {
let reply_bytes = cached_opt.unwrap();
return Some(reply_bytes)
};
let packet_option = self.client.resolve(pubkey.clone());
if packet_option.is_none() {
return None;
};
let reply_bytes = packet_option.unwrap().packet().build_bytes_vec().unwrap();
cache.add(pubkey.clone(), reply_bytes);
cache.get(pubkey)
}
/**
* Resolves a domain with pkarr.
*/
pub fn resolve(
&mut self,
query: &Vec<u8>
) -> std::prelude::v1::Result<Vec<u8>, Box<dyn Error>> {
let request = Packet::parse(query)?;
let question_opt = request.questions.first();
if question_opt.is_none() {
return Err("Missing question".into());
}
let question = question_opt.unwrap();
let labels = question.qname.get_labels();
let raw_pubkey = labels.last().unwrap().to_string();
let parsed_option = Self::parse_pkarr_uri(&raw_pubkey);
if parsed_option.is_none() {
return Err("Invalid pkarr pubkey".into());
}
let pubkey = parsed_option.unwrap();
let packet_option = self.resolve_pubkey_respect_cache(&pubkey);
if packet_option.is_none() {
return Err("No pkarr packet found for pubkey".into());
}
let packet = packet_option.unwrap();
let packet = Packet::parse(&packet).unwrap();
let matching_records: Vec<ResourceRecord<'_>> = packet.answers.iter()
.filter(|record| record.match_qclass(question.qclass) && record.match_qtype(question.qtype) && record.name == question.qname)
.map(|record| record.clone())
.collect();
let mut reply = request.into_reply();
reply.answers = matching_records;
// println!("Pkarr reply {:?} with {} ms", reply, start.elapsed().as_millis());
let reply_bytes: Vec<u8> = reply.build_bytes_vec()?;
Ok(reply_bytes)
}
}
#[cfg(test)]
mod tests {
use pkarr::{
dns::{Name, Packet, Question, ResourceRecord},
Keypair, SignedPacket,
};
// use simple_dns::{Name, Question, Packet};
use super::*;
use std::net::Ipv4Addr;
use zbase32;
fn get_test_keypair() -> Keypair {
// pk:cb7xxx6wtqr5d6yqudkt47drqswxk57dzy3h7qj3udym5puy9cso
let secret = "6kfe1u5jyqxg644eqfgk1cp4w9yjzwq51rn11ftysuo6xkpc64by";
let seed = zbase32::decode_full_bytes_str(secret).unwrap();
let slice: &[u8; 32] = &seed[0..32].try_into().unwrap();
let keypair = Keypair::from_secret_key(slice);
keypair
}
fn publish_record() {
let keypair = get_test_keypair();
// let uri = keypair.to_uri_string();
// println!("Publish packet with pubkey {}", uri);
let mut packet = Packet::new_reply(0);
let ip: Ipv4Addr = "93.184.216.34".parse().unwrap();
let record = ResourceRecord::new(
Name::new("pknames.p2p").unwrap(),
pkarr::dns::CLASS::IN,
100,
pkarr::dns::rdata::RData::A(ip.try_into().unwrap()),
);
packet.answers.push(record);
let record = ResourceRecord::new(
Name::new(".").unwrap(),
pkarr::dns::CLASS::IN,
100,
pkarr::dns::rdata::RData::A(ip.try_into().unwrap()),
);
packet.answers.push(record);
let signed_packet = SignedPacket::from_packet(&keypair, &packet).unwrap();
let client = PkarrClient::new();
let result = client.publish(&signed_packet);
result.expect("Should have published.");
}
#[test]
fn query_domain() {
publish_record();
let keypair = get_test_keypair();
let domain = format!("pknames.p2p.{}", keypair.to_z32());
let name = Name::new(&domain).unwrap();
let mut query = Packet::new_query(0);
let question = Question::new(
name.clone(),
pkarr::dns::QTYPE::TYPE(pkarr::dns::TYPE::A),
pkarr::dns::QCLASS::CLASS(pkarr::dns::CLASS::IN),
true,
);
query.questions.push(question);
let mut resolver = PkarrResolver::new(0);
let result = resolver.resolve(&query.build_bytes_vec().unwrap());
assert!(result.is_ok());
let reply_bytes = result.unwrap();
let reply = Packet::parse(&reply_bytes).unwrap();
assert_eq!(reply.id(), query.id());
assert_eq!(reply.answers.len(), 1);
let answer = reply.answers.first().unwrap();
assert_eq!(answer.name.to_string(), name.to_string());
assert_eq!(answer.rdata.type_code(), pkarr::dns::TYPE::A);
}
#[test]
fn query_pubkey() {
publish_record();
let keypair = get_test_keypair();
let domain = keypair.to_z32();
let name = Name::new(&domain).unwrap();
let mut query = Packet::new_query(0);
let question = Question::new(
name.clone(),
pkarr::dns::QTYPE::TYPE(pkarr::dns::TYPE::A),
pkarr::dns::QCLASS::CLASS(pkarr::dns::CLASS::IN),
true,
);
query.questions.push(question);
let mut resolver = PkarrResolver::new(0);
let result = resolver.resolve(&query.build_bytes_vec().unwrap());
assert!(result.is_ok());
let reply_bytes = result.unwrap();
let reply = Packet::parse(&reply_bytes).unwrap();
assert_eq!(reply.id(), query.id());
assert_eq!(reply.answers.len(), 1);
let answer = reply.answers.first().unwrap();
assert_eq!(answer.name.to_string(), name.to_string());
assert_eq!(answer.rdata.type_code(), pkarr::dns::TYPE::A);
}
#[test]
fn query_invalid_pubkey() {
let domain = "invalid_pubkey";
let name = Name::new(&domain).unwrap();
let mut query = Packet::new_query(0);
let question = Question::new(
name.clone(),
pkarr::dns::QTYPE::TYPE(pkarr::dns::TYPE::A),
pkarr::dns::QCLASS::CLASS(pkarr::dns::CLASS::IN),
true,
);
query.questions.push(question);
let mut resolver = PkarrResolver::new(0);
let result = resolver.resolve(&query.build_bytes_vec().unwrap());
assert!(result.is_err());
// println!("{}", result.unwrap_err());
}
#[test]
fn pkarr_parse() {
let domain = "cb7xxx6wtqr5d6yqudkt47drqswxk57dzy3h7qj3udym5puy9cso";
let decoded = zbase32::decode_full_bytes_str(domain);
// assert!(decoded.is_err());
let decoded = decoded.unwrap();
println!("{:?}", decoded);
if decoded.len() != 32 {
println!("wrong length");
return;
}
let trying: Result<PublicKey, _> = domain.try_into();
assert!(trying.is_err());
}
}

102
src/pknames_resolver.rs Normal file
View File

@@ -0,0 +1,102 @@
use pkarr::dns::{Name, Packet};
use pknames_core::resolve::resolve_standalone;
use crate::pkarr_resolver::PkarrResolver;
#[derive(Clone)]
pub struct PknamesResolver {
pkarr: PkarrResolver,
config_dir_path: String,
}
impl PknamesResolver {
pub fn new(max_cache_ttl: u64, config_dir_path: &str) -> Self {
PknamesResolver {
pkarr: PkarrResolver::new(max_cache_ttl),
config_dir_path: config_dir_path.to_string()
}
}
/**
* Resolve a regular pknames domain into a pkarr domain.
* Example: `pknames.p2p` -> `pknames.p2p.7fmjpcuuzf54hw18bsgi3zihzyh4awseeuq5tmojefaezjbd64cy`.
*/
fn predict_pknames_domain(&self, domain: &str) -> Result<String, Box<dyn std::error::Error>> {
let result = resolve_standalone(&domain, &self.config_dir_path);
if result.is_err() {
return Err("Neither pkarr nor pknames domain.".into());
};
let predictions = result.unwrap();
let best_class = predictions.get_best_class().expect("Class should be available.");
let best_pubkey = best_class.pubkey.clone();
let best_pubkey = best_pubkey.replace("pk:", ""); // Just to be sure
let full_domain = format!("{}.{}", domain, best_pubkey);
Ok(full_domain)
}
pub fn resolve(&mut self, query: &Vec<u8>) -> std::prelude::v1::Result<Vec<u8>, Box<dyn std::error::Error>> {
let original_query = Packet::parse(query)?;
let pkarr_result = self.pkarr.resolve(&query.clone());
if pkarr_result.is_ok() {
return pkarr_result; // It was a pkarr hostname
}
let question = original_query.questions.first().unwrap();
let domain = question.qname.to_string();
let pkarr_domain = self.predict_pknames_domain(&domain)?;
let qname = Name::new(&pkarr_domain).unwrap();
let mut pkarr_query = original_query.clone();
pkarr_query.questions[0].qname = qname;
let pkarr_query = pkarr_query.build_bytes_vec().unwrap();
let pkarr_reply = self.pkarr.resolve(&pkarr_query)?;
let pkarr_reply = Packet::parse(&pkarr_reply).unwrap();
let mut reply = original_query.clone().into_reply();
for answer in pkarr_reply.answers.iter() {
let mut answer = answer.clone();
answer.name = question.qname.clone();
reply.answers.push(answer);
};
Ok(reply.build_bytes_vec().unwrap())
}
}
#[cfg(test)]
mod tests {
use pkarr::dns::{Name, Packet, Question};
use super::PknamesResolver;
#[test]
fn query_pubkey() {
let mut pknames = PknamesResolver::new(1, "~/.pknames");
let mut query = Packet::new_query(0);
let name = Name::new("pknames.p2p").unwrap();
let question = Question::new(name, pkarr::dns::QTYPE::TYPE(pkarr::dns::TYPE::A), pkarr::dns::QCLASS::CLASS(pkarr::dns::CLASS::IN), false);
query.questions.push(question);
let query_bytes = query.build_bytes_vec().unwrap();
let result = pknames.resolve(&query_bytes);
if result.is_err() {
eprintln!("{:?}", result.unwrap_err());
assert!(false);
return;
}
assert!(result.is_ok());
let reply = result.unwrap();
let reply = Packet::parse(&reply).unwrap();
println!("{:?}", reply);
}
}