| Crates.io | cdns-rs |
| lib.rs | cdns-rs |
| version | 1.2.2 |
| created_at | 2021-11-07 02:11:14.327395+00 |
| updated_at | 2025-10-10 18:07:32.029662+00 |
| description | A native Sync/Async Rust implementation of client DNS resolver. |
| homepage | |
| repository | https://codeberg.org/4neko/cdns-rs |
| max_upload_size | |
| id | 477922 |
| size | 504,055 |
An implementation of client side DNS query library which also is capable to look for host name in /etc/hosts.
Also it is able to /etc/resolv.conf and uses options from this file to configure itself. So it acts like libc's gethostbyname(3) or gethostbyaddr(3). The configuration can be overriden.
This library supports both sync and async code. The async is provided for tokio crate or for custom realization using common interface traits.
Also the experimental IDN (international domain names) were added.
The pull requests are now supported because the repository was moved to Codeberg. The alternative way is to send patches over the email to patch[at]4neko.org.
In case if you would like to contribute the code, please use pull request. Your pull request should include:
Description of changes and why it is needed.
Test the pull request.
In case of you prefer email and patch files please consider the following:
For each feature or fix, please send patches separatly.
Please write what your patch is implementing or fixing.
I can read the code and I am able to understand it, so don't write a poem or essay in the description to the patches.
Please test your patch.
Yes, MPL- and Apache-licensed code can be used with an MIT codebase (so in that sense, they are "compatible"). However, the MPL- / Apache-licensed code remains under its original license. (So although compatible, you cannot relicense someone else's MPL or Apache code into the MIT license.) This means that your final codebase will contain a mix of MPL, Apache, and MIT licensed code. As an example, MPL has weak copyleft, so if you modified an MPL file, that file (including your changes) must remain under the MPL license.
You must inform the recipients where they can get the source for the MPLed code in the executable program or library you are distributing (i.e., you must comply with Section 3.2). You may distribute any executables you create under a license of your choosing, as long as that license does not interfere with the recipients' rights to the source under the terms of the MPL.
You should use this license if you are located in the EU which gives you more advantages over GPL because in case of any disputes, the license allows you to defend your rights in a European Union country, in this case it will be Spain. It has also been translated into all languages of the EU member states.
Matrix of EUPL compatible open source licences
EUPL-1.2 is incompatiable with GPL according to GNU ORG
This is a free software license. By itself, it has a copyleft comparable to the GPL's, and incompatible with it.
v 1.2.0
Sources are available under: MPL-2.0 OR EUPL-1.2
The project has moved to Codeberg.
Windows is not supported.
To use the DNS-over-TLS, the record to system's resolv.conf can be added:
nameserver 1.1.1.1#@853#cloudflare-dns.com
All after the # is considered as extension if there is no space between IP address and '#'.
enable_IDN_support - (enabled by default) allows to resolve IDN
use_sync - enabled a sync code base
use_sync_tls - enables a TLS support (HTTPS is not yet functional)
no_error_output - does not output any errors to stderr
One of the following:
use_async - enables an async code baseuse_async_tokio - enablesan async tokio code baseOne of the following:
use_async_tls - enables a general TLS support (own implementation out of crate)
use_async_tokio_tls - enables a tokio based TLS
use cdns_rs::sync::{QDns, QuerySetup, QType, request};
fn main()
{
// a, aaaa
let res_a = request::resolve_fqdn("protonmail.com", None).unwrap();
println!("A/AAAA:");
for a in res_a
{
println!("\t{}", a);
}
}
use cdns_rs::sync::{QDns, QuerySetup, QType, request, caches::CACHE};
fn main()
{
// soa
let mut dns_req =
QDns::make_empty(None, QuerySetup::default()).unwrap();
dns_req.add_request(QType::SOA, "protonmail.com");
// sending request and receiving results
let res = dns_req.query();
println!("SOA:");
let soa_res = res.get_result();
if soa_res.is_err() == true
{
println!("{}", soa_res.err().unwrap());
}
else
{
let soa = soa_res.unwrap();
if soa.is_empty() == true
{
println!("\tNo SOA found!")
}
else
{
for s in soa
{
for i in s.resp
{
println!("\t{}", i)
}
}
}
}
}
use std::{net::{IpAddr, SocketAddr}, sync::Arc, time::Instant};
use cdns_rs::{cfg_resolv_parser::{OptionFlags, ResolveConfEntry}, common::bind_all, sync::{request, ResolveConfig}};
fn main()
{
let now = Instant::now();
let cfg =
"nameserver 8.8.8.8 \n\
options use-vc";
// -- or --
let resolver_ip: IpAddr = "8.8.8.8".parse().unwrap();
let mut resolve = ResolveConfig::default();
resolve.nameservers.push(Arc::new(ResolveConfEntry::new(SocketAddr::new(resolver_ip, 53), None, bind_all(resolver_ip)).unwrap()));
resolve.option_flags = OptionFlags::OPT_USE_VC;
let cust_parsed = Arc::new(ResolveConfig::custom_config(cfg).unwrap());
let cust_manual = Arc::new(resolve);
assert_eq!(cust_parsed, cust_manual);
// a, aaaa
let res_a = request::resolve_fqdn("protonmail.com", Some(cust_parsed)).unwrap();
let elapsed = now.elapsed();
println!("Elapsed: {:.2?}", elapsed);
println!("A/AAAA:");
for a in res_a
{
println!("\t{}", a);
}
return;
}
use std::{sync::Arc, time::Instant};
use cdns_rs::sync::{request, ResolveConfig};
fn main()
{
let now = Instant::now();
let cfg = "nameserver 1.1.1.1#@853#cloudflare-dns.com \n\
options use-vc single-request";
let cust = Arc::new(ResolveConfig::custom_config(cfg).unwrap());
// a, aaaa
let res_a = request::resolve_fqdn("google.com", Some(cust.clone())).unwrap();
let elapsed = now.elapsed();
println!("Elapsed: {:.2?}", elapsed);
println!("A/AAAA:");
for a in res_a
{
println!("\t{}", a);
}
return;
}
use std::net::UdpSocket;
use cdns_rs::sync::query::QDnsSockerAddr;
// run in shell `nc -u -l 44444` a "test" should be received
fn main()
{
let udp = UdpSocket::bind("127.0.0.1:33333").unwrap();
udp.connect(QDnsSockerAddr::resolve("localhost:44444").unwrap()).unwrap();
udp.send("test".as_bytes()).unwrap();
return;
}
use std::sync::Arc;
use cdns_rs::a_sync::{request, CachesController, IoInterf, QDns, QType, QuerySetup, ResolveConfig, SocketBase};
use tokio::time::Instant;
#[tokio::main]
async fn main()
{
let cache = Arc::new(CachesController::new().await.unwrap());
let cfg = "nameserver 1.1.1.1";
let cust = Arc::new(ResolveConfig::async_custom_config(cfg).await.unwrap());
let now = Instant::now();
// a, aaaa
let res_a = request::resolve_fqdn::<_, SocketBase, SocketBase, IoInterf>("protonmail.com", Some(cust.clone()), cache.clone()).await.unwrap();
// mx
let res_mx = request::resolve_mx::<_, SocketBase, SocketBase, IoInterf>("protonmail.com", Some(cust.clone()), cache.clone()).await.unwrap();
let mut dns_req =
QDns::<SocketBase, SocketBase, IoInterf>::make_empty(Some(cust.clone()), QuerySetup::default(), cache.clone())
.await
.unwrap();
dns_req.add_request(QType::SOA, "protonmail.com");
// sending request and receiving results
let res = dns_req.query().await;
let elapsed = now.elapsed();
println!("Elapsed: {:.2?}", elapsed);
println!("A/AAAA:");
for a in res_a
{
println!("\t{}", a);
}
println!("MX:");
for mx in res_mx
{
println!("\t{}", mx);
}
println!("SOA:");
let soa_res = res.get_result();
if soa_res.is_err() == true
{
println!("error: {}", soa_res.err().unwrap());
}
else
{
let soa = soa_res.unwrap();
if soa.is_empty() == true
{
println!("\tNo SOA found!")
}
else
{
for s in soa
{
for i in s.resp
{
println!("\t{}", i)
}
}
}
}
}
// This example demonstrates how to use the async code with custom executor.
use std::{io::ErrorKind, os::fd::AsFd, sync::Arc, time::Duration};
use async_trait::async_trait;
use cdns_rs::
{
a_sync::
{
interface::{AsyncMutex, AsyncMutexGuard, MutexedCaches, UnifiedFs},
network::NetworkTapType,
request,
CacheInstance,
CachesController,
NetworkTap,
QDns,
SocketTap,
SocketTaps
},
cfg_resolv_parser::ResolveConfEntry,
internal_error,
internal_error_map,
CDnsErrorType,
CDnsResult,
HostConfig,
QType,
QuerySetup,
ResolveConfig,
SocketTapCommon
};
use tokio::
{
fs::File,
io::{AsyncReadExt, AsyncWriteExt},
net::{TcpSocket, TcpStream, UdpSocket},
sync::{Mutex, MutexGuard},
time::{timeout, Instant}
};
/// Implementaton of the custom sockets interface where the appropriate socket types are
/// defined. This is needed for the crate to use async sockets.
#[derive(Clone, Debug)]
pub struct SocketCustomBase;
/// Implementing a [SocketTaps] for out [SocketCustomBase]. The generic in the trait is needed
/// to overcome the Rust restrictions on implementing foreign traits on foreign structs.
impl SocketTaps<SocketCustomBase> for SocketCustomBase
{
type TcpSock = LocaltcpStrem;
type UdpSock = LocalUdpSocket;
#[inline]
fn new_tcp_socket(resolver: Arc<ResolveConfEntry>, timeout: Duration) -> CDnsResult<Box<NetworkTapType<SocketCustomBase>>>
{
return NetworkTap::<Self::TcpSock, SocketCustomBase>::new(resolver, timeout)
}
#[inline]
fn new_udp_socket(resolver: Arc<ResolveConfEntry>, timeout: Duration) -> CDnsResult<Box<NetworkTapType<SocketCustomBase>>>
{
return NetworkTap::<Self::UdpSock, SocketCustomBase>::new(resolver, timeout)
}
}
/// Defining the struct to avoiding the same problem with foreign struct and traits.
/// Implementation for the UdpSocket.
#[repr(transparent)]
#[derive(Debug)]
pub struct LocalUdpSocket(UdpSocket);
impl AsFd for LocalUdpSocket
{
fn as_fd(&self) -> std::os::unix::prelude::BorrowedFd<'_>
{
return self.0.as_fd();
}
}
#[derive(Debug)]
pub struct LocaltcpStrem(TcpStream);
impl AsFd for LocaltcpStrem
{
fn as_fd(&self) -> std::os::unix::prelude::BorrowedFd<'_>
{
return self.0.as_fd();
}
}
async
fn new_tcp_stream(cfg: &ResolveConfEntry, conn_timeout: Option<Duration>) -> CDnsResult<TcpStream>
{
// create socket
let socket =
if cfg.get_resolver_ip().is_ipv4() == true
{
TcpSocket::new_v4()
}
else
{
TcpSocket::new_v6()
}
.map_err(|e| internal_error_map!(CDnsErrorType::IoError, "{}", e))?;
// bind address
socket.bind(*cfg.get_adapter_ip()).map_err(|e| internal_error_map!(CDnsErrorType::IoError, "{}", e))?;
socket.set_keepalive(false).map_err(|e| internal_error_map!(CDnsErrorType::IoError, "{}", e))?;
socket.set_nodelay(true).map_err(|e| internal_error_map!(CDnsErrorType::IoError, "{}", e))?;
// connect
let tcpstream =
if let Some(c_timeout) = conn_timeout
{
timeout(c_timeout, socket.connect(*cfg.get_resolver_sa()))
.await
.map_err(|e| internal_error_map!(CDnsErrorType::IoError, "{}", e))?
.map_err(|e| internal_error_map!(CDnsErrorType::IoError, "{}", e))?
}
else
{
socket
.connect(*cfg.get_resolver_sa())
.await
.map_err(|e| internal_error_map!(CDnsErrorType::IoError, "{}", e))?
};
return Ok(tcpstream);
}
#[async_trait]
impl SocketTap<SocketCustomBase> for NetworkTap<LocaltcpStrem, SocketCustomBase>
{
async
fn connect(&mut self, conn_timeout: Option<Duration>) -> CDnsResult<()>
{
if self.sock.is_some() == true
{
// ignore
return Ok(());
}
// create socket
let tcpstream = new_tcp_stream(&self.cfg, conn_timeout).await?;
self.sock = Some(LocaltcpStrem(tcpstream));
return Ok(());
}
fn is_encrypted(&self) -> bool
{
return false;
}
fn is_tcp(&self) -> bool
{
return true;
}
fn should_append_len(&self) -> bool
{
return true;
}
async
fn poll_read(&self) -> CDnsResult<()>
{
timeout(self.timeout, self.sock.as_ref().unwrap().0.readable())
.await
.map_err(|e|
internal_error_map!(CDnsErrorType::IoError, "Timeout {}", e)
)?
.map_err(|e|
internal_error_map!(CDnsErrorType::IoError, "socket poll error {}", e)
)
}
async
fn send(&mut self, sndbuf: &[u8]) -> CDnsResult<usize>
{
return
self
.sock
.as_mut()
.unwrap()
.0
.write(sndbuf)
.await
.map_err(|e| internal_error_map!(CDnsErrorType::IoError, "{}", e));
}
async
fn recv(&mut self, rcvbuf: &mut [u8]) -> CDnsResult<usize>
{
async
fn sub_recv(this: &mut NetworkTap<LocaltcpStrem, SocketCustomBase>, rcvbuf: &mut [u8]) -> CDnsResult<usize>
{
loop
{
match this.sock.as_mut().unwrap().0.read(rcvbuf).await
{
Ok(n) =>
{
return Ok(n);
},
Err(ref e) if e.kind() == ErrorKind::WouldBlock =>
{
continue;
},
Err(ref e) if e.kind() == ErrorKind::Interrupted =>
{
continue;
},
Err(e) =>
{
internal_error!(CDnsErrorType::IoError, "{}", e);
}
} // match
} // loop
}
// wait for timeout
match timeout(self.timeout, sub_recv(self, rcvbuf)).await
{
Ok(r) => return r,
Err(e) => internal_error!(CDnsErrorType::RequestTimeout, "{}", e)
}
}
}
#[async_trait]
impl SocketTap<SocketCustomBase> for NetworkTap<LocalUdpSocket, SocketCustomBase>
{
async
fn connect(&mut self, _conn_timeout: Option<Duration>) -> CDnsResult<()>
{
if self.sock.is_some() == true
{
// ignore
return Ok(());
}
let socket =
UdpSocket::bind(self.cfg.get_adapter_ip())
.await
.map_err(|e| internal_error_map!(CDnsErrorType::InternalError, "{}", e))?;
socket.connect(self.cfg.get_resolver_sa())
.await
.map_err(|e| internal_error_map!(CDnsErrorType::IoError, "{}", e))?;
self.sock = Some(LocalUdpSocket(socket));
return Ok(());
}
fn is_encrypted(&self) -> bool
{
return false;
}
fn is_tcp(&self) -> bool
{
return false;
}
fn should_append_len(&self) -> bool
{
return false;
}
async
fn poll_read(&self) -> CDnsResult<()>
{
timeout(self.timeout, self.sock.as_ref().unwrap().0.readable())
.await
.map_err(|e|
internal_error_map!(CDnsErrorType::IoError, "Timeout {}", e)
)?
.map_err(|e|
internal_error_map!(CDnsErrorType::IoError, "socket poll error {}", e)
)
}
async
fn send(&mut self, sndbuf: &[u8]) -> CDnsResult<usize>
{
return
self.sock.as_mut()
.unwrap()
.0
.send(sndbuf)
.await
.map_err(|e| internal_error_map!(CDnsErrorType::IoError, "{}", e));
}
async
fn recv(&mut self, rcvbuf: &mut [u8]) -> CDnsResult<usize>
{
async
fn sub_recv(this: &mut NetworkTap<LocalUdpSocket, SocketCustomBase>, rcvbuf: &mut [u8]) -> CDnsResult<usize>
{
loop
{
match this.sock.as_mut().unwrap().0.recv_from(rcvbuf).await
{
Ok((rcv_len, rcv_src)) =>
{
// this should not fail because socket is "connected"
if &rcv_src != this.get_remote_addr()
{
internal_error!(
CDnsErrorType::DnsResponse,
"received answer from unknown host: '{}' exp: '{}'",
this.get_remote_addr(),
rcv_src
);
}
return Ok(rcv_len);
},
Err(ref e) if e.kind() == ErrorKind::WouldBlock =>
{
continue;
},
Err(ref e) if e.kind() == ErrorKind::Interrupted =>
{
continue;
},
Err(e) =>
{
internal_error!(CDnsErrorType::IoError, "{}", e);
}
} // match
} // loop
}
// wait for timeout
match timeout(self.timeout, sub_recv(self, rcvbuf)).await
{
Ok(r) =>
return r,
Err(e) =>
internal_error!(CDnsErrorType::RequestTimeout, "{}", e)
}
}
}
/// A struct for our IO interface.
#[derive(Debug)]
pub struct LocalTokioInterf;
/// implementation of the IO interface.
impl MutexedCaches for LocalTokioInterf
{
type MetadataFs = LocalFile;
type ResolveCache = LocalMutex<CacheInstance<ResolveConfig, Self::MetadataFs>>;
type HostCahae = LocalMutex<CacheInstance<HostConfig, Self::MetadataFs>>;
}
#[derive(Debug)]
pub struct LocalFile;
impl UnifiedFs for LocalFile
{
type ErrRes = std::io::Error;
type FileOp = File;
async
fn metadata(path: &std::path::Path) -> Result<std::fs::Metadata, Self::ErrRes>
{
return tokio::fs::metadata(path).await;
}
async
fn open<P: AsRef<std::path::Path>>(path: P) -> std::io::Result<Self::FileOp>
{
return tokio::fs::File::open(path).await;
}
async
fn read_to_string(file: &mut Self::FileOp, buf: &mut String) -> std::io::Result<usize>
{
return file.read_to_string(buf).await;
}
}
#[repr(transparent)]
#[derive(Debug)]
pub struct LocalMutex<DS: Sized>(Mutex<DS>);
impl<DS: Sized> AsyncMutex<DS> for LocalMutex<DS>
{
type MutxGuard<'mux> = LocalMutexGuard<'mux, DS> where DS: 'mux;
fn a_new(v: DS) -> Self
{
return LocalMutex(Mutex::new(v));
}
async
fn a_lock<'mux>(&'mux self) -> Self::MutxGuard<'mux>
{
return LocalMutexGuard(self.0.lock().await);
}
}
#[repr(transparent)]
#[derive(Debug)]
pub struct LocalMutexGuard<'mux, DS: Sized>(MutexGuard<'mux, DS>);
impl<'mux, DS: Sized> AsyncMutexGuard<'mux, DS>for LocalMutexGuard<'mux, DS>
{
fn guard(&self) -> &DS
{
return &self.0;
}
fn guard_mut(&mut self) -> &mut DS
{
return &mut self.0;
}
}
#[tokio::main]
async fn main()
{
let cache = Arc::new(CachesController::<LocalTokioInterf>::new_custom().await.unwrap());
let cfg = "nameserver 1.1.1.1";
let cust = Arc::new(ResolveConfig::async_custom_config(cfg).await.unwrap());
let now = Instant::now();
// a, aaaa
let res_a = request::resolve_fqdn::<_, SocketCustomBase, SocketCustomBase, LocalTokioInterf>("protonmail.com", Some(cust.clone()), cache.clone()).await.unwrap();
// mx
let res_mx = request::resolve_mx::<_, SocketCustomBase, SocketCustomBase, LocalTokioInterf>("protonmail.com", Some(cust.clone()), cache.clone()).await.unwrap();
// ptr
let res_ptr_local = request::resolve_reverse::<_, SocketCustomBase, SocketCustomBase, LocalTokioInterf>("::1", Some(cust.clone()), cache.clone()).await.unwrap();
// ptr
let res_ptr = request::resolve_reverse::<_, SocketCustomBase, SocketCustomBase, LocalTokioInterf>("8.8.8.8", Some(cust.clone()), cache.clone()).await.unwrap();
// soa
//let resolvers = CACHE.clone_resolve_list().await.unwrap();
let mut dns_req =
QDns::<SocketCustomBase, SocketCustomBase, LocalTokioInterf>::make_empty(Some(cust.clone()), QuerySetup::default(), cache.clone())
.await
.unwrap();
dns_req.add_request(QType::SOA, "protonmail.com");
// sending request and receiving results
let res = dns_req.query().await;
let elapsed = now.elapsed();
println!("Elapsed: {:.2?}", elapsed);
println!("A/AAAA:");
for a in res_a
{
println!("\t{}", a);
}
println!("MX:");
for mx in res_mx
{
println!("\t{}", mx);
}
println!("PTR local:");
for ptr in res_ptr_local
{
println!("\t{}", ptr);
}
println!("PTR:");
for ptr in res_ptr
{
println!("\t{}", ptr);
}
println!("SOA:");
let soa_res = res.get_result();
if soa_res.is_err() == true
{
println!("error: {}", soa_res.err().unwrap());
}
else
{
let soa = soa_res.unwrap();
if soa.is_empty() == true
{
println!("\tNo SOA found!")
}
else
{
for s in soa
{
for i in s.resp
{
println!("\t{}", i)
}
}
}
}
return;
}