Skip to content

Commit

Permalink
started DoQ
Browse files Browse the repository at this point in the history
  • Loading branch information
dandyvica committed Dec 11, 2024
1 parent b0e1d08 commit 2d0afe4
Show file tree
Hide file tree
Showing 14 changed files with 443 additions and 144 deletions.
3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ idna = "1.0.3"
lazy_static = "1.4.0"
log = "0.4.22"
mlua = { version = "0.9.4", features = [ "lua54", "serialize" ], optional = true }
quinn = "0.11.6"
rand = "0.8.5"
rcgen = "0.13.1"
regex = "1.11.1"
Expand All @@ -42,6 +43,8 @@ serde_json = { version = "1.0.111", features = ["preserve_order"] }
simplelog = "0.12.2"
tera = "1.20.0"
thiserror = "1.0.65"
tokio = { version = "1", features = ["full"] }
tokio-macros = { version = "0.2.0-alpha.6" }
type2network = { git = "https://github.com/dandyvica/type2network" }
type2network_derive = { git = "https://github.com/dandyvica/type2network/" }
webpki-roots = "0.26.0"
Expand Down
Binary file added doc/endpoint_cases.ods
Binary file not shown.
42 changes: 35 additions & 7 deletions src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,13 @@ impl CliOptions {
// if https:// is found in the server, it's DoH
if server.starts_with("https://") {
trp_options.transport_mode = Protocol::DoH;
trp_options.doh = true;
//trp_options.doh = true;
EndPoint::try_from(server)
}
// if quic:// is found in the server, it's DoQ
else if server.starts_with("quic://") {
trp_options.transport_mode = Protocol::DoQ;
// trp_options.doq = true;
EndPoint::try_from(server)
}
// this is a pattern like: @[2606:4700:4700::1111]:53
Expand Down Expand Up @@ -267,6 +273,14 @@ Caveat: all options starting with a dash (-) should be placed after optional [TY
.value_parser(clap::value_parser!(PathBuf))
.help_heading("Transport options")
)
.arg(
Arg::new("doq")
.long("doq")
.long_help("Sets transport to DNS over QUIC (DoQ).")
.action(ArgAction::SetTrue)
.value_name("doq")
.help_heading("Transport options")
)
.arg(
Arg::new("https")
.short('H')
Expand Down Expand Up @@ -642,13 +656,13 @@ Caveat: all options starting with a dash (-) should be placed after optional [TY
//───────────────────────────────────────────────────────────────────────────────────
// transport mode
//───────────────────────────────────────────────────────────────────────────────────
if matches.get_flag("tcp") || options.transport.tcp {
if matches.get_flag("tcp") {
options.transport.transport_mode = Protocol::Tcp;
}
if matches.get_flag("tls") || options.transport.tls || options.transport.dot {
}
if matches.get_flag("tls") {
options.transport.transport_mode = Protocol::DoT;
}
if matches.get_flag("https") || options.transport.https || options.transport.doh {
}
if matches.get_flag("https") || server.starts_with("https://") {
options.transport.transport_mode = Protocol::DoH;

// set HTTP version
Expand All @@ -660,6 +674,9 @@ Caveat: all options starting with a dash (-) should be placed after optional [TY
"v3" => options.transport.https_version = Some(version::Version::HTTP_3),
_ => unimplemented!("this version of HTTP is not implemented"),
}
}
if matches.get_flag("doq") || server.starts_with("quic://") {
options.transport.transport_mode = Protocol::DoQ;
}

//───────────────────────────────────────────────────────────────────────────────────
Expand All @@ -682,11 +699,22 @@ Caveat: all options starting with a dash (-) should be placed after optional [TY
options.transport.endpoint = EndPoint::try_from(options.transport.port)?;
}
// server was provided (e.g.: 1.1.1.1 or one.one.one.one)
//
// all possible cases:
//
// @1.1.1.1
// @1.1.1.1:53
// @2606:4700:4700::1111
// @[2606:4700:4700::1111]:53
// @one.one.one.one
// @one.one.one.one:53
// @https://cloudflare-dns.com/dns-query
// @quic://dns.adguard.com
else {
options.transport.endpoint = Self::analyze_resolver(server, &mut options.transport)?;
}

// println!("ep={}", options.transport.endpoint);
println!("ep={}", options.transport.endpoint);

//───────────────────────────────────────────────────────────────────────────────────
// QTypes, QClass
Expand Down
38 changes: 37 additions & 1 deletion src/dns/rfc/query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use type2network::ToNetworkOrder;
use type2network_derive::ToNetwork;

use crate::error::{Dns, Error, Result};
use crate::transport::network::Messenger;
use crate::transport::network::{AsyncMessenger, Messenger};

use super::{
domain::DomainName, flags::BitFlags, header::Header, qclass::QClass, qtype::QType, question::Question,
Expand Down Expand Up @@ -122,6 +122,42 @@ impl Query {
Ok(sent)
}

// Send the query through the wire, async version
pub async fn asend<T: AsyncMessenger>(&mut self, trp: &mut T, save_path: &Option<PathBuf>) -> Result<usize> {
// convert to network bytes
let mut buffer: Vec<u8> = Vec::new();
let message_size = self
.serialize_to(&mut buffer)
.map_err(|_| Error::Dns(Dns::CantSerialize))? as u16;
trace!(
"buffer to send before TCP length addition: {:0X?}, uses_leading_length={}",
buffer,
trp.uses_leading_length()
);

// if using TCP, we need to prepend the message sent with length of message
if trp.uses_leading_length() {
let bytes = (message_size - 2).to_be_bytes();
buffer[..2].copy_from_slice(&bytes);

// not really necessary but to be aligned with what is sent
self.length = Some(message_size);
};
trace!("buffer to send: {:0X?}", buffer);

// send packet through the wire
let sent = trp.asend(&buffer).await?;
debug!("sent {} bytes", sent);

// save query as raw bytes if requested
if let Some(path) = save_path {
let mut f = File::create(path).map_err(|e| Error::OpenFile(e, path.to_path_buf()))?;
f.write_all(&buffer).map_err(Error::Buffer)?;
}

Ok(sent)
}

pub fn display(&self) {
// header first
println!("HEADER: {}\n", self.header);
Expand Down
23 changes: 22 additions & 1 deletion src/dns/rfc/response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use super::{
use crate::dns::rfc::response_code::ResponseCode;
use crate::error::{Dns, Error};
use crate::show::{DisplayOptions, Show};
use crate::transport::network::Messenger;
use crate::transport::network::{AsyncMessenger, Messenger};

pub enum ResponseSection {
Answer,
Expand Down Expand Up @@ -103,6 +103,27 @@ impl Response {
Ok(received)
}

// Receive message for DNS resolver
pub async fn arecv<T: AsyncMessenger>(&mut self, trp: &mut T, buffer: &mut [u8]) -> crate::error::Result<usize> {
// receive packet from endpoint
let received = trp.arecv(buffer).await?;
debug!("received {} bytes", received);
trace!("received buffer {:X?}", &buffer[..received]);

// if using TCP, we get rid of 2 bytes which are the length of the message received
let mut cursor = Cursor::new(&buffer[..received]);

// get response
self.deserialize_from(&mut cursor)
.map_err(|_| Error::Dns(Dns::CantDeserialize))?;
trace!("response header: {}", self.header);
trace!("response query: {}", self.question);
trace!("response answer: {:?}", self.answer);
trace!("response authority: {:?}", self.authority);

Ok(received)
}

// return a random ip address in the glue records from the additional section
pub fn random_glue_record(&self, qt: &QType) -> Option<&ResourceRecord> {
if let Some(add) = &self.additional {
Expand Down
29 changes: 29 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use std::process::ExitCode;
use std::time::Duration;
use std::{fmt, io};

use quinn::{ConnectionError, ReadError, ReadExactError, WriteError};
use thiserror::Error;

/// A specific custom `Result` for all functions
Expand Down Expand Up @@ -86,6 +87,10 @@ pub enum Error {
#[error("TLS error ({0})")]
Tls(#[source] rustls::Error),

// QUIC errors
#[error("QUIC error ({0})")]
Quic(QuicError),

// Reqwest errors
#[error("https error ({0})")]
Reqwest(#[source] reqwest::Error),
Expand All @@ -110,6 +115,29 @@ pub enum Error {
Lua(#[source] mlua::Error),
}

#[derive(Debug)]
pub enum QuicError {
Connection(ConnectionError),
Read(ReadError),
ReadExact(ReadExactError),
Write(WriteError),
NoInitialCipherSuite,
}

impl fmt::Display for QuicError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
QuicError::Connection(e) => write!(f, "connection error: {}", e),
QuicError::Read(e) => write!(f, "read error: {}", e),
QuicError::ReadExact(e) => write!(f, "read error: {}", e),
QuicError::Write(e) => write!(f, "write error: {}", e),
QuicError::NoInitialCipherSuite => {
write!(f, "the initial cipher suite (AES-128-GCM-SHA256) is not available")
}
}
}
}

impl From<Error> for ExitCode {
// Required method
fn from(e: Error) -> Self {
Expand All @@ -124,6 +152,7 @@ impl From<Error> for ExitCode {
Error::IPParse(_, _) => ExitCode::from(8),
Error::Logger(_) => ExitCode::from(9),
Error::Resolver(_) => ExitCode::from(10),
Error::Quic(_) => ExitCode::from(11),
#[cfg(feature = "mlua")]
Error::Lua(_) => ExitCode::from(10),
}
Expand Down
25 changes: 23 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ mod transport;
use transport::{
https::HttpsProtocol,
network::{Messenger, Protocol},
quic::QuicProtocol,
root_servers::init_root_map,
tcp::TcpProtocol,
tls::TlsProtocol,
Expand All @@ -43,7 +44,7 @@ mod lua;
use lua::LuaDisplay;

// the initial length of the Vec buffer
const BUFFER_SIZE: usize = 4096;
const BUFFER_SIZE: usize = 8192;

//───────────────────────────────────────────────────────────────────────────────────
// get list of messages using transport
Expand Down Expand Up @@ -95,7 +96,27 @@ pub fn get_messages(info: Option<&mut QueryInfo>, options: &CliOptions) -> crate
get_messages_using_transport(info, &mut transport, options)
}
Protocol::DoQ => {
unimplemented!("DoQ is not yet implemented")
//

//use futures::executor::block_on;
use tokio::runtime::Runtime;

let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
let result = rt.block_on(async {
let mut transport = QuicProtocol::new(&options.transport).await.unwrap();
let messages = DnsProtocol::async_process_request(options, &mut transport, BUFFER_SIZE)
.await
.unwrap();
return messages;
});

Ok(result)

//
//unimplemented!("DoQ is not yet implemented")
}
}
}
Expand Down
70 changes: 69 additions & 1 deletion src/protocol.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use log::{debug, info};

use crate::error::{self, Error};
use crate::transport::network::{Messenger, Protocol};
use crate::transport::network::{AsyncMessenger, Messenger, Protocol};
use crate::transport::tcp::TcpProtocol;
use crate::{args::CliOptions, cli_options::FromOptions};
use crate::{
Expand Down Expand Up @@ -39,6 +39,24 @@ impl DnsProtocol {
Ok(query)
}

//───────────────────────────────────────────────────────────────────────────────────
// send the query to the resolver, async version
//───────────────────────────────────────────────────────────────────────────────────
async fn asend_query<T: AsyncMessenger>(options: &CliOptions, qt: &QType, trp: &mut T) -> error::Result<Query> {
// it's safe to unwrap here, see from_options() for Query
let mut query = Query::from_options(options, qt).unwrap();

// TCP needs to prepend with 2 bytes for message length
if trp.uses_leading_length() {
query = query.with_length();
}

// send query using the chosen transport
let bytes = query.asend(trp, &options.dump.write_query).await?;

Ok(query)
}

//───────────────────────────────────────────────────────────────────────────────────
// receive response from resolver
//───────────────────────────────────────────────────────────────────────────────────
Expand All @@ -50,6 +68,17 @@ impl DnsProtocol {
Ok(response)
}

//───────────────────────────────────────────────────────────────────────────────────
// receive response from resolver, async version
//───────────────────────────────────────────────────────────────────────────────────
#[inline(always)]
async fn areceive_response<T: AsyncMessenger>(trp: &mut T, buffer: &mut [u8]) -> crate::error::Result<Response> {
let mut response = Response::default();
let _ = response.arecv(trp, buffer).await?;

Ok(response)
}

//───────────────────────────────────────────────────────────────────────────────────
// this sends and receives queries using a transport
//───────────────────────────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -88,4 +117,43 @@ impl DnsProtocol {

Ok(MessageList::new(messages))
}

//───────────────────────────────────────────────────────────────────────────────────
// this sends and receives queries using a transport
//───────────────────────────────────────────────────────────────────────────────────
pub(crate) async fn async_process_request<T: AsyncMessenger>(
options: &CliOptions,
trp: &mut T,
buffer_size: usize,
) -> crate::error::Result<MessageList> {
// we'll have the same number of messages than the number of types to query
let mut messages = Vec::with_capacity(options.protocol.qtype.len());
let mut buffer = vec![0u8; buffer_size];

for qtype in options.protocol.qtype.iter() {
// send query, response is depending on TC flag if UDP
let mut query = Self::asend_query(options, qtype, trp).await?;
let mut response = Self::areceive_response(trp, &mut buffer).await?;

// check for the truncation (TC) header flag. If set and UDP, resend using TCP
if response.is_truncated() && trp.mode() == Protocol::Udp {
info!("query for {} caused truncation, resending using TCP", qtype);

// clear buffer using fill(), otherwise buffer will be empty if buffer.clear()
buffer.fill(0);

// resend using TCP
let mut tcp_transport = TcpProtocol::new(&options.transport)?;
query = Self::send_query(options, qtype, &mut tcp_transport)?;
response = Self::receive_response(&mut tcp_transport, &mut buffer)?;
}

// struct Message is a convenient way to gather both query and response
let msg = Message { query, response };
msg.check()?;
messages.push(msg);
}

Ok(MessageList::new(messages))
}
}
Loading

0 comments on commit 2d0afe4

Please sign in to comment.