Skip to content

LLT-7052: DNS packet codec#1688

Open
tomasz-grz wants to merge 1 commit intomainfrom
packet_codec
Open

LLT-7052: DNS packet codec#1688
tomasz-grz wants to merge 1 commit intomainfrom
packet_codec

Conversation

@tomasz-grz
Copy link
Contributor

@tomasz-grz tomasz-grz commented Feb 20, 2026

Problem

We want to get rid of all hickory_server dependencies, because it set's up a full blown DNS server, we maintain our own (outdated) fork of, and it was source of a lot of bugs.

As a first step we need to decide if DNS requests are related to .nord zone.
If they are, they should handled by our local resolver, and we need to serve the response packet back.
If not, the request is forwarded and resolved by the upstream nameservers.

Solution

This module incorporates:

  • a lightweight decoder, that converts DNS request bytes into a DnsPacket<'_> struct, and exposes a helper functions like parse_dns_packet(), find_nord_query() and normalize_qname() to simplify with the correct request handling.
  • A packet builder specific for the local resolver use case, that converts a ResponseKind enum into appropriate DNS response bytes.

☑️ Definition of Done checklist

  • Commit history is clean (requirements)
  • README.md is updated
  • Functionality is covered by unit or integration tests

Copy link
Contributor

@stalowyjez stalowyjez left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have mixed feeling for this PR, reducing dependencies is always nice, but it feels a bit like reinventing the wheel. We also have some code in libfirewall which does some DNS parsing, maybe we should create some public DNS helper crate for pnet which would address our needs?

Comment on lines +21 to +28
pub struct Soa {
mname: &'static str,
rname: &'static str,
serial: u32,
refresh: i32,
retry: i32,
expire: i32,
minimum: u32,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This struct feels more like ttl_to_nord_soa_bytes function with everything written 3 times instead of just once ^^'

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you elaborate a little bit more?

In essence I wanted all of it to be static, but we do have configurable TTL..

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess having a const array (created in compile time with come const stuff) and just changing the ttl byte would be fastest, but I guess sth like:

fn create_nord_soa_with_ttl(ttl: u32) -> Result<Vec<u8>, DnsBuildError> {
        let mut bytes = Vec::new();
        encode_name(&mut bytes, "mesh.nordsec.com.");
        encode_name(&mut bytes, "support.nordsec.com.");
        bytes.extend_from_slice(&2015082403.to_be_bytes());
        bytes.extend_from_slice(&7200.to_be_bytes());
        bytes.extend_from_slice([&self.retry.to_be_bytes()](i32::try_from(ttl).map_err(|_| DnsBuildError::SoaTtlOutOfRange)?));
        bytes.extend_from_slice(&1209600.to_be_bytes());
        bytes.extend_from_slice(&ttl);
        bytes
}

makes sense here. But probably reusing https://docs.rs/domain/0.11.1/domain/rdata/rfc1035/struct.Soa.html#impl-Soa%3CN%3E would also be nice? This would add a dependency on domain, but I think it is also a nice option to use instead of pnet dns stuff?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ofc, we could add some comments, or wrap stuff in come constants to make it clearer

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wanted to use domain in the beginning but was asked not to add any additional dependencies since we have pnet..

I'll try to see how can I make this part "nicer"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed the struct and converted to create_nord_soa_bytes

Copy link
Contributor

@stalowyjez stalowyjez left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good enough, +1

Comment on lines +43 to +46
/// Normalize the name, converting to lowercase and trimming trailing dot
pub fn normalize(name: &str) -> String {
name.trim().trim_end_matches('.').to_ascii_lowercase()
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, do we need it as a separate function? And should it be public?

Copy link
Contributor Author

@tomasz-grz tomasz-grz Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's necessary for populating or searching the local "nord" records zone.

Comment on lines +21 to +28
pub struct Soa {
mname: &'static str,
rname: &'static str,
serial: u32,
refresh: i32,
retry: i32,
expire: i32,
minimum: u32,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess having a const array (created in compile time with come const stuff) and just changing the ttl byte would be fastest, but I guess sth like:

fn create_nord_soa_with_ttl(ttl: u32) -> Result<Vec<u8>, DnsBuildError> {
        let mut bytes = Vec::new();
        encode_name(&mut bytes, "mesh.nordsec.com.");
        encode_name(&mut bytes, "support.nordsec.com.");
        bytes.extend_from_slice(&2015082403.to_be_bytes());
        bytes.extend_from_slice(&7200.to_be_bytes());
        bytes.extend_from_slice([&self.retry.to_be_bytes()](i32::try_from(ttl).map_err(|_| DnsBuildError::SoaTtlOutOfRange)?));
        bytes.extend_from_slice(&1209600.to_be_bytes());
        bytes.extend_from_slice(&ttl);
        bytes
}

makes sense here. But probably reusing https://docs.rs/domain/0.11.1/domain/rdata/rfc1035/struct.Soa.html#impl-Soa%3CN%3E would also be nice? This would add a dependency on domain, but I think it is also a nice option to use instead of pnet dns stuff?

Comment on lines +21 to +28
pub struct Soa {
mname: &'static str,
rname: &'static str,
serial: u32,
refresh: i32,
retry: i32,
expire: i32,
minimum: u32,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ofc, we could add some comments, or wrap stuff in come constants to make it clearer

@tomaszklak
Copy link
Contributor

I have mixed feeling for this PR, reducing dependencies is always nice, but it feels a bit like reinventing the wheel.

I have the same feeling, and I'm also missing explanation why we want to "get rid of all hickory_server dependencies."

@tomaszklak
Copy link
Contributor

If they are, they should handled by our local resolver, and we need to serve the response packet back.

And what if they are not?

@tomaszklak
Copy link
Contributor

I feel like there should be our rfc that describes what behavior do we want from magic dns.

@tomaszklak
Copy link
Contributor

Please, remember to cleanup (squash?) commits.

@tomaszklak
Copy link
Contributor

+1

@Jauler
Copy link
Contributor

Jauler commented Mar 18, 2026

Next time: It would be really nice to include ticket number in the branch name 🙏 🙏 🙏


/// Normalize the name, converting to lowercase and trimming trailing dot
pub(crate) fn normalize_qname(name: &str) -> String {
name.trim().trim_end_matches('.').to_ascii_lowercase()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why the trailing dot trimming? 🤔 maybe it would be nice to operate with FQDNs?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So that we can operate on domains with trailing dot and or without eg :test.nord and test.nord.

We either need to trim the trailing dot, or append the trailing dot if it's not present.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated to operate on FQDNs with trailing dot

/// Constant values for .nord SOA record
const SOA_MNAME: &str = "mesh.nordsec.com";
const SOA_RNAME: &str = "support.nordsec.com";
const SOA_SERIAL: u32 = 2015082403;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

while maybe it is not the most important part, but we probably should bump the serial number.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bumped

///
/// Trailing dot is optional, it is ignored
fn encode_name(out: &mut Vec<u8>, name: &str) -> Result<(), DnsBuildError> {
let trimmed = name.trim_end_matches('.');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would recommend to operate on FQDNs internally always, this would not require us to trim the trailing '.' and would reduce the amount of domain representations to support.

truncated: &mut bool,
record_count: &mut u16,
) -> Result<(), DnsBuildError> {
let rr_len = DNS_HEADER_OFFSET + data.len();
Copy link
Contributor

@Jauler Jauler Mar 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this is incorrect constant being used. I mean, the value of it is correct, but it is coincidental, but my understanding was the this constnat is meant to represent for DNS packet header length, and here we are calculated fixed-data size for resource-record.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm yeah semantically you're right, though are both the same size. I will introduce a separate constant.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a separate const

Copy link
Contributor

@Jauler Jauler left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1.0

Copy link
Contributor

@mathiaspeters mathiaspeters left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

This module incorporates:
- a lightweight decoder, that converts DNS request bytes into a `DnsPacket<'_>`, and exposes a helper functions like
`parse_dns_query_packet()`, `find_nord_query()` and `normalize_qname()` to simplify with the correct request handling.
- A packet builder specific for the local resolver use case, that converts a `ResponseKind` enum into appropriate DNS response bytes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants