Skip to content

Commit e14c0f8

Browse files
committed
LLT-7050: Integrate local .nord DNS resolver
Handles resolving .nord domains locally, bypassing hickory server for those requests. But forwards requests to the upstream resolvers for any other request.
1 parent 37a80b6 commit e14c0f8

5 files changed

Lines changed: 603 additions & 38 deletions

File tree

.unreleased/LLT-7050_mesh_resolver_integration

Whitespace-only changes.

crates/telio-dns/src/nameserver.rs

Lines changed: 113 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
use crate::{
2+
packet_decoder::{find_nord_query, normalize_qname, parse_dns_query_packet, DnsParseError},
3+
packet_encoder::DnsResponseBuilder,
24
resolver::Resolver,
3-
zone::{AuthoritativeZone, ClonableZones, ForwardZone, Records},
5+
zone::{AuthoritativeZone, ClonableZones, ForwardZone, NordZone, Records, NORD_ZONE},
46
};
57
use async_trait::async_trait;
68
use hickory_server::{
@@ -10,6 +12,7 @@ use hickory_server::{
1012
};
1113
use neptun::noise::{Tunn, TunnResult};
1214
use pnet_packet::{
15+
dns::DnsQuery,
1316
ip::{IpNextHeaderProtocol, IpNextHeaderProtocols},
1417
ipv4::{checksum, Ipv4Packet, MutableIpv4Packet},
1518
ipv6::{Ipv6Packet, MutableIpv6Packet},
@@ -87,6 +90,8 @@ async fn update_wg_timers(
8790
/// Local name server.
8891
#[derive(Default)]
8992
pub struct LocalNameServer {
93+
/// .nord peers zone
94+
nord_zone: NordZone,
9095
zones: Arc<ClonableZones>,
9196
task_handle: Option<JoinHandle<()>>,
9297
}
@@ -96,6 +101,7 @@ impl LocalNameServer {
96101
/// configured for zone `.`.
97102
pub async fn new(forward_ips: &[IpAddr]) -> Result<Arc<RwLock<Self>>, String> {
98103
let ns = Arc::new(RwLock::new(LocalNameServer {
104+
nord_zone: NordZone::new(),
99105
zones: Arc::new(ClonableZones::new()),
100106
task_handle: None,
101107
}));
@@ -247,11 +253,6 @@ impl LocalNameServer {
247253
nameserver: Arc<RwLock<LocalNameServer>>,
248254
request_info: &mut RequestInfo,
249255
) -> Result<Vec<u8>, String> {
250-
telio_log_debug!("Resolving dns");
251-
let resolver = Resolver::new();
252-
telio_log_debug!("Getting DNS zones");
253-
let zones = nameserver.zones().await;
254-
255256
telio_log_debug!("Preparing DNS request");
256257
let dns_request = match &mut request_info.payload {
257258
PayloadRequestInfo::Udp {
@@ -262,17 +263,55 @@ impl LocalNameServer {
262263
.ok_or_else(|| String::from("Inexistent DNS request"))?,
263264
_ => return Ok(Vec::new()),
264265
};
265-
let dns_request = Request::new(dns_request, request_info.dns_source(), Protocol::Udp);
266-
telio_log_debug!("DNS request: {:?}", &dns_request);
267266

268-
zones
269-
.lookup(&dns_request, resolver.clone())
270-
.await
271-
.map_err(|e| format!("Lookup failed {e}"))?;
267+
let dns_response = match dns_request {
268+
PayloadDestination::Local {
269+
id,
270+
recursion_desired,
271+
query,
272+
} => {
273+
telio_log_debug!(
274+
"Local query for: {:?} {:?}",
275+
query.qtype,
276+
query.get_qname_parsed()
277+
);
278+
// lookup local zone for response
279+
let (response_kind, ttl) = {
280+
let ns = nameserver.read().await;
281+
(
282+
ns.nord_zone.resolve_local_response(&query),
283+
ns.nord_zone.ttl(),
284+
)
285+
};
286+
287+
telio_log_debug!("Local response: {:?}", response_kind);
272288

273-
let dns_response = resolver.0.lock().await;
274-
telio_log_debug!("Nameserver response: {:?}", &dns_response);
275-
Ok(dns_response.to_vec())
289+
// build response packet bytes
290+
DnsResponseBuilder::new(id, query, ttl.0, response_kind, recursion_desired)
291+
.build()
292+
.map_err(|e| format!("Failed building DNS response packet: {e:?}"))?
293+
}
294+
PayloadDestination::Forward(question) => {
295+
telio_log_debug!("Forwarding request dns");
296+
let resolver = Resolver::new();
297+
telio_log_debug!("Getting DNS zones");
298+
let zones = nameserver.zones().await;
299+
300+
let dns_request = Request::new(question, request_info.dns_source(), Protocol::Udp);
301+
telio_log_debug!("DNS request: {:?}", &dns_request);
302+
303+
zones
304+
.lookup(&dns_request, resolver.clone())
305+
.await
306+
.map_err(|e| format!("Lookup failed {e}"))?;
307+
308+
let dns_response = resolver.0.lock().await;
309+
telio_log_debug!("Nameserver response: {:?}", &dns_response);
310+
dns_response.to_vec()
311+
}
312+
};
313+
314+
Ok(dns_response)
276315
}
277316

278317
async fn process_packet(
@@ -347,13 +386,43 @@ impl LocalNameServer {
347386
return Err(String::from("Invalid DNS port"));
348387
}
349388

350-
let dns_request = MessageRequest::from_bytes(udp_request.payload())
351-
.map_err(|_| String::from("Failed to build MessageRequest from request packet"))?;
389+
let payload = udp_request.payload();
390+
391+
let dns_request = match parse_dns_query_packet(payload) {
392+
Ok(packet) => {
393+
if let Some(query) = find_nord_query(&packet) {
394+
// query for local .nord domain
395+
Some(PayloadDestination::Local {
396+
id: packet.get_id(),
397+
recursion_desired: packet.get_is_recursion_desirable() == 1,
398+
query,
399+
})
400+
} else {
401+
// not .nord, forward unchanged
402+
let forward = MessageRequest::from_bytes(payload).map_err(|_| {
403+
String::from("Failed to build MessageRequest from request packet")
404+
})?;
405+
Some(PayloadDestination::Forward(forward))
406+
}
407+
}
408+
Err(DnsParseError::UnsupportedOpcode(c)) => {
409+
telio_log_warn!("Unsupported DNS query Opcode: {c:?}");
410+
// still forward it as a fallback
411+
let forward = MessageRequest::from_bytes(payload).map_err(|_| {
412+
String::from("Failed to build MessageRequest from request packet")
413+
})?;
414+
Some(PayloadDestination::Forward(forward))
415+
}
416+
Err(e) => {
417+
// malformed DNS query
418+
return Err(format!("Failed to parse DNS payload: {e}"));
419+
}
420+
};
352421

353422
Ok(PayloadRequestInfo::Udp {
354423
source_port: udp_request.get_source(),
355424
destination_port: udp_request.get_destination(),
356-
dns_request: Some(dns_request),
425+
dns_request,
357426
})
358427
}
359428
}
@@ -397,12 +466,22 @@ impl IpRequestInfo {
397466
}
398467
}
399468

469+
#[allow(clippy::large_enum_variant)]
470+
enum PayloadDestination {
471+
Local {
472+
id: u16,
473+
recursion_desired: bool,
474+
query: DnsQuery,
475+
},
476+
Forward(MessageRequest),
477+
}
478+
400479
#[allow(clippy::large_enum_variant)]
401480
enum PayloadRequestInfo {
402481
Udp {
403482
source_port: u16,
404483
destination_port: u16,
405-
dns_request: Option<MessageRequest>,
484+
dns_request: Option<PayloadDestination>,
406485
},
407486
Tcp {
408487
source_port: u16,
@@ -692,11 +771,19 @@ impl NameServer for Arc<RwLock<LocalNameServer>> {
692771
records: &Records,
693772
ttl_value: TtlValue,
694773
) -> Result<(), String> {
695-
let azone = Arc::new(AuthoritativeZone::new(zone, records, ttl_value).await?);
774+
if normalize_qname(zone) == NORD_ZONE {
775+
let mut ns = self.write().await;
776+
ns.nord_zone
777+
.upsert(records, ttl_value)
778+
.map_err(|e| format!("Upsert failed: {e}"))?;
779+
}
696780

781+
// TODO: LLT-7054: Remove AuthoritativeZone once migration is fully validated
782+
let azone = Arc::new(AuthoritativeZone::new(zone, records, ttl_value).await?);
697783
self.zones_mut()
698784
.await
699785
.upsert(LowerName::from_str(zone)?, Box::new(azone));
786+
700787
Ok(())
701788
}
702789

@@ -727,7 +814,8 @@ impl NameServer for Arc<RwLock<LocalNameServer>> {
727814

728815
#[cfg(test)]
729816
mod tests {
730-
use crate::zone::Records;
817+
use super::*;
818+
use crate::zone::{Records, NORD_ZONE};
731819
use hickory_server::{
732820
authority::MessageRequest,
733821
proto::{
@@ -739,8 +827,6 @@ mod tests {
739827
};
740828
use std::{net::Ipv4Addr, str::FromStr};
741829

742-
use super::*;
743-
744830
fn dns_request(host: String) -> Request {
745831
let mut question = Message::new();
746832
let mut query = Query::new();
@@ -767,7 +853,7 @@ mod tests {
767853
.await
768854
.unwrap();
769855
nameserver
770-
.upsert("nord", &records, TtlValue(60))
856+
.upsert(NORD_ZONE, &records, TtlValue(60))
771857
.await
772858
.unwrap();
773859
let request = dns_request(entry_name.clone());
@@ -803,7 +889,7 @@ mod tests {
803889
.unwrap();
804890
let raw_read_ptr1 = Arc::as_ptr(&nameserver.zones().await);
805891
nameserver
806-
.upsert("nord", &records, TtlValue(60))
892+
.upsert(NORD_ZONE, &records, TtlValue(60))
807893
.await
808894
.unwrap();
809895
let raw_read_ptr2 = Arc::as_ptr(&nameserver.zones().await);
@@ -818,7 +904,7 @@ mod tests {
818904

819905
let read_ptr3 = nameserver.zones().await;
820906
nameserver
821-
.upsert("nord2", &records, TtlValue(60))
907+
.upsert("nord2.", &records, TtlValue(60))
822908
.await
823909
.unwrap();
824910
let raw_read_ptr4 = Arc::as_ptr(&nameserver.zones().await);
@@ -841,7 +927,7 @@ mod tests {
841927
.await
842928
.unwrap();
843929
nameserver
844-
.upsert("nord.", &records, TtlValue(60))
930+
.upsert(NORD_ZONE, &records, TtlValue(60))
845931
.await
846932
.unwrap();
847933

crates/telio-dns/src/packet_decoder.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,7 @@
22
//!
33
//! This module provides a layer for parsing a single DNS question from raw UDP bytes
44
5-
// TODO: LLT-7050 Remove after merging integration
6-
#![allow(dead_code)]
7-
5+
use crate::zone::{NORD_ZONE, NORD_ZONE_SUFFIX};
86
use pnet_packet::{
97
dns::{DnsClasses, DnsPacket, DnsQuery, Opcode},
108
FromPacket,
@@ -35,7 +33,7 @@ fn is_nord_name(name: &str) -> bool {
3533
if normalized.is_empty() {
3634
return false;
3735
}
38-
normalized == "nord." || normalized.ends_with(".nord.")
36+
normalized == NORD_ZONE || normalized.ends_with(NORD_ZONE_SUFFIX)
3937
}
4038

4139
/// Normalize the name, converting to lowercase and appending trailing dot

crates/telio-dns/src/packet_encoder.rs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,6 @@
22
//!
33
//! This module provides a layer for constructing minimal DNS responses for meshnet nord domains
44
5-
// TODO: LLT-7050 Remove after merging integration
6-
#![allow(dead_code)]
7-
85
use crate::packet_decoder::normalize_qname;
96
use pnet_packet::dns::{
107
DnsClasses, DnsQuery, DnsType, DnsTypes, MutableDnsPacket, MutableDnsResponsePacket, Opcode,
@@ -114,7 +111,7 @@ pub(crate) enum DnsBuildError {
114111
/// Response option for the nord resolver
115112
///
116113
/// Use [`DnsResponseBuilder`] to construct a complete DNS response packet
117-
#[derive(Debug, Clone)]
114+
#[derive(Debug, Clone, PartialEq)]
118115
pub(crate) enum ResponseKind {
119116
/// Successful answer with IPv4 addresses
120117
AnswerA { addresses: Vec<Ipv4Addr> },
@@ -181,6 +178,8 @@ impl DnsResponseBuilder {
181178
}
182179
}
183180

181+
// TODO: LLT-7054: remove if not needed after complete regression testing
182+
#[cfg(test)]
184183
/// Set if the resolver is the authority for the domain
185184
///
186185
/// Sets AA flag
@@ -190,6 +189,8 @@ impl DnsResponseBuilder {
190189
self
191190
}
192191

192+
// TODO: LLT-7054: remove if not needed after complete regression testing
193+
#[cfg(test)]
193194
/// Set if the resolver can handle recursive queries
194195
///
195196
/// Sets RA flag if recursion is available
@@ -199,6 +200,8 @@ impl DnsResponseBuilder {
199200
self
200201
}
201202

203+
// TODO: LLT-7054: remove if not needed after complete regression testing
204+
#[cfg(test)]
202205
/// Set cap for UDP response size
203206
///
204207
/// Sets TC flag if truncation occurs

0 commit comments

Comments
 (0)