mirror of
https://github.com/GyulyVGC/sniffnet.git
synced 2026-07-10 18:33:05 +00:00
feat(dns): add manual DNS protocol parser (RFC 1035)
Add a new `networking::dns` module that parses DNS messages directly from raw UDP/TCP payload bytes, with no external DNS crate: - `types.rs`: DnsMessage, DnsEvent, DnsQuestion, DnsRecord and the DnsRecordType / DnsRCode / DnsRData / DnsFlags enums, with Display impls. - `parser.rs`: `parse_dns(&[u8])` decoding the 12-byte header (bitwise flags), the Question section and Answer resource records, with compression-pointer support (RFC 1035 §4.1.4), RDATA interpreted per type (A, AAAA, CNAME/NS/PTR, MX, TXT, SOA) and defensive handling of malformed/truncated input (no panics). Includes 9 unit tests over fixed byte vectors (A query, compressed A response, AAAA, MX, NXDOMAIN, too-short, truncated, pointer loop). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
ab4340abe8
commit
2d5431a970
4 changed files with 667 additions and 0 deletions
8
src/networking/dns/mod.rs
Normal file
8
src/networking/dns/mod.rs
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
//! Application-layer DNS protocol analysis (RFC 1035).
|
||||
//!
|
||||
//! [`parser`] turns the raw bytes of a UDP/TCP payload into the strongly-typed
|
||||
//! [`types::DnsMessage`]; [`types`] defines those data types together with the
|
||||
//! [`types::DnsEvent`] capture wrapper.
|
||||
|
||||
pub mod parser;
|
||||
pub mod types;
|
||||
413
src/networking/dns/parser.rs
Normal file
413
src/networking/dns/parser.rs
Normal file
|
|
@ -0,0 +1,413 @@
|
|||
//! Manual parser for the DNS message format (RFC 1035), operating directly on
|
||||
//! the raw bytes of a UDP/TCP payload.
|
||||
//!
|
||||
//! No external DNS parsing crate is used: every field is read by hand from the
|
||||
//! byte slice. The parser is defensive — any malformed or truncated input
|
||||
//! yields `None` (or a partial result) instead of panicking.
|
||||
|
||||
use std::net::{Ipv4Addr, Ipv6Addr};
|
||||
|
||||
use crate::networking::dns::types::{
|
||||
DnsFlags, DnsMessage, DnsQuestion, DnsRCode, DnsRData, DnsRecord, DnsRecordType,
|
||||
};
|
||||
|
||||
/// Maximum number of compression-pointer jumps allowed while decoding a single
|
||||
/// name, to guard against maliciously crafted pointer loops.
|
||||
const MAX_NAME_JUMPS: usize = 64;
|
||||
|
||||
/// Parses a DNS message from the bytes of a transport payload.
|
||||
///
|
||||
/// `buf` must start at the DNS header (for TCP, strip the 2-byte length prefix
|
||||
/// beforehand). Returns `None` if the input is too short or the header/question
|
||||
/// section is malformed.
|
||||
pub fn parse_dns(buf: &[u8]) -> Option<DnsMessage> {
|
||||
// The header is a fixed 12 bytes.
|
||||
if buf.len() < 12 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let id = u16::from_be_bytes([buf[0], buf[1]]);
|
||||
let flags = u16::from_be_bytes([buf[2], buf[3]]);
|
||||
let qr = (flags >> 15) & 1;
|
||||
let opcode = ((flags >> 11) & 0x0F) as u8;
|
||||
let aa = (flags >> 10) & 1 == 1;
|
||||
let tc = (flags >> 9) & 1 == 1;
|
||||
let rd = (flags >> 8) & 1 == 1;
|
||||
let ra = (flags >> 7) & 1 == 1;
|
||||
let rcode = (flags & 0x0F) as u8;
|
||||
let qdcount = u16::from_be_bytes([buf[4], buf[5]]);
|
||||
let ancount = u16::from_be_bytes([buf[6], buf[7]]);
|
||||
// NSCOUNT (buf[8..10]) and ARCOUNT (buf[10..12]) are not parsed.
|
||||
|
||||
let mut pos = 12;
|
||||
|
||||
// Question section.
|
||||
let mut questions = Vec::with_capacity(qdcount as usize);
|
||||
for _ in 0..qdcount {
|
||||
let (name, next) = read_name(buf, pos)?;
|
||||
pos = next;
|
||||
let qtype = read_u16(buf, pos)?;
|
||||
pos += 2;
|
||||
let qclass = read_u16(buf, pos)?;
|
||||
pos += 2;
|
||||
questions.push(DnsQuestion {
|
||||
name,
|
||||
qtype: DnsRecordType::from_u16(qtype),
|
||||
qclass,
|
||||
});
|
||||
}
|
||||
|
||||
// Answer section. Tolerate truncation: stop at the first record we cannot
|
||||
// fully read, keeping whatever we already decoded.
|
||||
let mut answers = Vec::with_capacity(ancount as usize);
|
||||
for _ in 0..ancount {
|
||||
match read_record(buf, pos) {
|
||||
Some((record, next)) => {
|
||||
pos = next;
|
||||
answers.push(record);
|
||||
}
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
|
||||
Some(DnsMessage {
|
||||
id,
|
||||
is_response: qr == 1,
|
||||
opcode,
|
||||
flags: DnsFlags { aa, tc, rd, ra },
|
||||
rcode: DnsRCode::from_u8(rcode),
|
||||
questions,
|
||||
answers,
|
||||
})
|
||||
}
|
||||
|
||||
/// Decodes a DNS name starting at `start`, following compression pointers
|
||||
/// (RFC 1035 §4.1.4). Returns the decoded name and the position immediately
|
||||
/// after the name in the byte stream (i.e. after the first pointer, if any).
|
||||
fn read_name(buf: &[u8], start: usize) -> Option<(String, usize)> {
|
||||
let mut labels: Vec<String> = Vec::new();
|
||||
let mut pos = start;
|
||||
// Position to resume reading the outer stream once the name is decoded.
|
||||
// Set when the first pointer is encountered; otherwise it is the byte after
|
||||
// the terminating zero length.
|
||||
let mut next_pos: Option<usize> = None;
|
||||
let mut jumps = 0;
|
||||
|
||||
loop {
|
||||
let len = *buf.get(pos)?;
|
||||
match len & 0xC0 {
|
||||
// Regular label: top two bits are 00.
|
||||
0x00 => {
|
||||
if len == 0 {
|
||||
pos += 1;
|
||||
next_pos.get_or_insert(pos);
|
||||
break;
|
||||
}
|
||||
let label_len = len as usize;
|
||||
let label = buf.get(pos + 1..pos + 1 + label_len)?;
|
||||
labels.push(String::from_utf8_lossy(label).into_owned());
|
||||
pos += 1 + label_len;
|
||||
}
|
||||
// Compression pointer: top two bits are 11.
|
||||
0xC0 => {
|
||||
let second = *buf.get(pos + 1)?;
|
||||
let offset = (((len & 0x3F) as usize) << 8) | second as usize;
|
||||
next_pos.get_or_insert(pos + 2);
|
||||
jumps += 1;
|
||||
if jumps > MAX_NAME_JUMPS || offset >= buf.len() {
|
||||
return None;
|
||||
}
|
||||
pos = offset;
|
||||
}
|
||||
// 0x40 and 0x80 are reserved and must not appear.
|
||||
_ => return None,
|
||||
}
|
||||
}
|
||||
|
||||
let name = if labels.is_empty() {
|
||||
".".to_string()
|
||||
} else {
|
||||
labels.join(".")
|
||||
};
|
||||
Some((name, next_pos?))
|
||||
}
|
||||
|
||||
/// Reads a single Resource Record starting at `start`. Returns the record and
|
||||
/// the position immediately after it.
|
||||
fn read_record(buf: &[u8], start: usize) -> Option<(DnsRecord, usize)> {
|
||||
let (name, mut pos) = read_name(buf, start)?;
|
||||
let rtype_raw = read_u16(buf, pos)?;
|
||||
pos += 2;
|
||||
let class = read_u16(buf, pos)?;
|
||||
pos += 2;
|
||||
let ttl = read_u32(buf, pos)?;
|
||||
pos += 4;
|
||||
let rdlength = read_u16(buf, pos)? as usize;
|
||||
pos += 2;
|
||||
|
||||
let rdata_start = pos;
|
||||
let rdata_end = rdata_start.checked_add(rdlength)?;
|
||||
if rdata_end > buf.len() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let rtype = DnsRecordType::from_u16(rtype_raw);
|
||||
let rdata = parse_rdata(buf, rtype, rdata_start, rdlength)?;
|
||||
|
||||
Some((
|
||||
DnsRecord {
|
||||
name,
|
||||
rtype,
|
||||
class,
|
||||
ttl,
|
||||
rdata,
|
||||
},
|
||||
rdata_end,
|
||||
))
|
||||
}
|
||||
|
||||
/// Interprets the RDATA of a record according to its type. `start`/`len`
|
||||
/// delimit the RDATA inside `buf`; names inside RDATA may use compression and
|
||||
/// therefore are resolved against the whole message.
|
||||
fn parse_rdata(buf: &[u8], rtype: DnsRecordType, start: usize, len: usize) -> Option<DnsRData> {
|
||||
let end = start.checked_add(len)?;
|
||||
let data = buf.get(start..end)?;
|
||||
|
||||
let rdata = match rtype {
|
||||
DnsRecordType::A => {
|
||||
let octets: [u8; 4] = data.try_into().ok()?;
|
||||
DnsRData::A(Ipv4Addr::from(octets))
|
||||
}
|
||||
DnsRecordType::Aaaa => {
|
||||
let octets: [u8; 16] = data.try_into().ok()?;
|
||||
DnsRData::Aaaa(Ipv6Addr::from(octets))
|
||||
}
|
||||
DnsRecordType::Cname | DnsRecordType::Ns | DnsRecordType::Ptr => {
|
||||
let (name, _) = read_name(buf, start)?;
|
||||
DnsRData::Name(name)
|
||||
}
|
||||
DnsRecordType::Mx => {
|
||||
let preference = read_u16(buf, start)?;
|
||||
let (exchange, _) = read_name(buf, start + 2)?;
|
||||
DnsRData::Mx {
|
||||
preference,
|
||||
exchange,
|
||||
}
|
||||
}
|
||||
DnsRecordType::Txt => {
|
||||
// RDATA is one or more length-prefixed <character-string>s.
|
||||
let mut strings = Vec::new();
|
||||
let mut p = start;
|
||||
while p < end {
|
||||
let str_len = *buf.get(p)? as usize;
|
||||
p += 1;
|
||||
let bytes = buf.get(p..p + str_len)?;
|
||||
strings.push(String::from_utf8_lossy(bytes).into_owned());
|
||||
p += str_len;
|
||||
}
|
||||
DnsRData::Txt(strings)
|
||||
}
|
||||
DnsRecordType::Soa => {
|
||||
let (mname, p) = read_name(buf, start)?;
|
||||
let (rname, p) = read_name(buf, p)?;
|
||||
let serial = read_u32(buf, p)?;
|
||||
let refresh = read_u32(buf, p + 4)?;
|
||||
let retry = read_u32(buf, p + 8)?;
|
||||
let expire = read_u32(buf, p + 12)?;
|
||||
let minimum = read_u32(buf, p + 16)?;
|
||||
DnsRData::Soa {
|
||||
mname,
|
||||
rname,
|
||||
serial,
|
||||
refresh,
|
||||
retry,
|
||||
expire,
|
||||
minimum,
|
||||
}
|
||||
}
|
||||
DnsRecordType::Srv | DnsRecordType::Other(_) => DnsRData::Other(data.to_vec()),
|
||||
};
|
||||
|
||||
Some(rdata)
|
||||
}
|
||||
|
||||
fn read_u16(buf: &[u8], pos: usize) -> Option<u16> {
|
||||
Some(u16::from_be_bytes([*buf.get(pos)?, *buf.get(pos + 1)?]))
|
||||
}
|
||||
|
||||
fn read_u32(buf: &[u8], pos: usize) -> Option<u32> {
|
||||
Some(u32::from_be_bytes([
|
||||
*buf.get(pos)?,
|
||||
*buf.get(pos + 1)?,
|
||||
*buf.get(pos + 2)?,
|
||||
*buf.get(pos + 3)?,
|
||||
]))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Query for `google.com`, type A, class IN. Header: id=0x1234, RD set,
|
||||
/// QDCOUNT=1.
|
||||
const QUERY_A: &[u8] = &[
|
||||
0x12, 0x34, // ID
|
||||
0x01, 0x00, // flags: RD=1
|
||||
0x00, 0x01, // QDCOUNT
|
||||
0x00, 0x00, // ANCOUNT
|
||||
0x00, 0x00, // NSCOUNT
|
||||
0x00, 0x00, // ARCOUNT
|
||||
0x06, b'g', b'o', b'o', b'g', b'l', b'e', // "google"
|
||||
0x03, b'c', b'o', b'm', // "com"
|
||||
0x00, // end of name
|
||||
0x00, 0x01, // QTYPE = A
|
||||
0x00, 0x01, // QCLASS = IN
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn parses_query_a() {
|
||||
let msg = parse_dns(QUERY_A).expect("should parse");
|
||||
assert_eq!(msg.id, 0x1234);
|
||||
assert!(!msg.is_response);
|
||||
assert_eq!(msg.opcode, 0);
|
||||
assert!(msg.flags.rd);
|
||||
assert_eq!(msg.rcode, DnsRCode::NoError);
|
||||
assert_eq!(msg.questions.len(), 1);
|
||||
assert_eq!(msg.query_name(), Some("google.com"));
|
||||
assert_eq!(msg.query_type(), Some(DnsRecordType::A));
|
||||
assert_eq!(msg.questions[0].qclass, 1);
|
||||
assert!(msg.answers.is_empty());
|
||||
}
|
||||
|
||||
/// Response for `google.com` A, using a compression pointer (0xC00C) in the
|
||||
/// answer's NAME field to reference the question's name at offset 12.
|
||||
const RESPONSE_A_COMPRESSED: &[u8] = &[
|
||||
0x12, 0x34, // ID
|
||||
0x81, 0x80, // flags: QR=1, RD=1, RA=1, RCODE=0
|
||||
0x00, 0x01, // QDCOUNT
|
||||
0x00, 0x01, // ANCOUNT
|
||||
0x00, 0x00, // NSCOUNT
|
||||
0x00, 0x00, // ARCOUNT
|
||||
// Question (name starts at offset 12)
|
||||
0x06, b'g', b'o', b'o', b'g', b'l', b'e', 0x03, b'c', b'o', b'm', 0x00, 0x00, 0x01,
|
||||
0x00, 0x01, // QTYPE=A, QCLASS=IN
|
||||
// Answer
|
||||
0xC0, 0x0C, // NAME -> pointer to offset 12
|
||||
0x00, 0x01, // TYPE=A
|
||||
0x00, 0x01, // CLASS=IN
|
||||
0x00, 0x00, 0x01, 0x2C, // TTL=300
|
||||
0x00, 0x04, // RDLENGTH=4
|
||||
0x08, 0x08, 0x08, 0x08, // RDATA = 8.8.8.8
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn parses_response_a_with_compression() {
|
||||
let msg = parse_dns(RESPONSE_A_COMPRESSED).expect("should parse");
|
||||
assert!(msg.is_response);
|
||||
assert!(msg.flags.ra);
|
||||
assert_eq!(msg.rcode, DnsRCode::NoError);
|
||||
assert_eq!(msg.query_name(), Some("google.com"));
|
||||
assert_eq!(msg.answers.len(), 1);
|
||||
let answer = &msg.answers[0];
|
||||
assert_eq!(answer.name, "google.com");
|
||||
assert_eq!(answer.rtype, DnsRecordType::A);
|
||||
assert_eq!(answer.ttl, 300);
|
||||
assert_eq!(answer.rdata, DnsRData::A(Ipv4Addr::new(8, 8, 8, 8)));
|
||||
}
|
||||
|
||||
/// AAAA response (::1) with a compression pointer.
|
||||
const RESPONSE_AAAA: &[u8] = &[
|
||||
0x00, 0x01, 0x81, 0x80, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, //
|
||||
0x04, b'i', b'p', b'v', b'6', 0x00, // name "ipv6"
|
||||
0x00, 0x1C, 0x00, 0x01, // QTYPE=AAAA, QCLASS=IN
|
||||
0xC0, 0x0C, // pointer to "ipv6"
|
||||
0x00, 0x1C, // TYPE=AAAA
|
||||
0x00, 0x01, // CLASS=IN
|
||||
0x00, 0x00, 0x00, 0x3C, // TTL=60
|
||||
0x00, 0x10, // RDLENGTH=16
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x01, // ::1
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn parses_aaaa() {
|
||||
let msg = parse_dns(RESPONSE_AAAA).expect("should parse");
|
||||
assert_eq!(msg.answers.len(), 1);
|
||||
assert_eq!(
|
||||
msg.answers[0].rdata,
|
||||
DnsRData::Aaaa(Ipv6Addr::LOCALHOST)
|
||||
);
|
||||
}
|
||||
|
||||
/// MX response: preference 10, exchange "mail" (compressed back to "ex").
|
||||
const RESPONSE_MX: &[u8] = &[
|
||||
0x00, 0x02, 0x81, 0x80, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, //
|
||||
0x02, b'e', b'x', 0x00, // name "ex"
|
||||
0x00, 0x0F, 0x00, 0x01, // QTYPE=MX, QCLASS=IN
|
||||
0xC0, 0x0C, // pointer to "ex"
|
||||
0x00, 0x0F, // TYPE=MX
|
||||
0x00, 0x01, // CLASS=IN
|
||||
0x00, 0x00, 0x00, 0x3C, // TTL=60
|
||||
0x00, 0x09, // RDLENGTH=9
|
||||
0x00, 0x0A, // preference=10
|
||||
0x04, b'm', b'a', b'i', b'l', 0xC0, 0x0C, // "mail" + pointer to "ex"
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn parses_mx() {
|
||||
let msg = parse_dns(RESPONSE_MX).expect("should parse");
|
||||
assert_eq!(msg.answers.len(), 1);
|
||||
assert_eq!(
|
||||
msg.answers[0].rdata,
|
||||
DnsRData::Mx {
|
||||
preference: 10,
|
||||
exchange: "mail.ex".to_string(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/// NXDOMAIN response (RCODE=3) with no answers.
|
||||
const RESPONSE_NXDOMAIN: &[u8] = &[
|
||||
0x00, 0x03, 0x81, 0x83, // QR=1, RD=1, RA=1, RCODE=3
|
||||
0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
|
||||
0x07, b'm', b'i', b's', b's', b'i', b'n', b'g', 0x03, b'c', b'o', b'm', 0x00, //
|
||||
0x00, 0x01, 0x00, 0x01, // QTYPE=A, QCLASS=IN
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn parses_nxdomain() {
|
||||
let msg = parse_dns(RESPONSE_NXDOMAIN).expect("should parse");
|
||||
assert!(msg.is_response);
|
||||
assert_eq!(msg.rcode, DnsRCode::NxDomain);
|
||||
assert_eq!(msg.query_name(), Some("missing.com"));
|
||||
assert!(msg.answers.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_too_short() {
|
||||
assert!(parse_dns(&[0x00, 0x01, 0x02]).is_none());
|
||||
assert!(parse_dns(&[]).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn truncated_question_returns_none() {
|
||||
// Header claims one question but the name never terminates.
|
||||
let buf = [
|
||||
0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
|
||||
0x06, b'g', b'o', b'o', // truncated label, no terminator
|
||||
];
|
||||
assert!(parse_dns(&buf).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pointer_loop_does_not_hang() {
|
||||
// A name at offset 12 that points to itself.
|
||||
let buf = [
|
||||
0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
|
||||
0xC0, 0x0C, // pointer -> offset 12 (itself)
|
||||
0x00, 0x01, 0x00, 0x01,
|
||||
];
|
||||
// Must terminate (return None) rather than loop forever.
|
||||
assert!(parse_dns(&buf).is_none());
|
||||
}
|
||||
}
|
||||
245
src/networking/dns/types.rs
Normal file
245
src/networking/dns/types.rs
Normal file
|
|
@ -0,0 +1,245 @@
|
|||
//! Data types representing a parsed DNS message (RFC 1035) and the capture
|
||||
//! metadata associated with it.
|
||||
//!
|
||||
//! These types are intentionally independent of any external DNS parsing
|
||||
//! crate: the parsing is performed manually over the raw bytes (see
|
||||
//! [`super::parser`]), so every field here maps directly to a field of the
|
||||
//! on-the-wire DNS format.
|
||||
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
|
||||
|
||||
use crate::networking::types::protocol::Protocol;
|
||||
use crate::utils::types::timestamp::Timestamp;
|
||||
|
||||
/// A DNS message captured from the network, enriched with the capture context
|
||||
/// (when it was seen and between which endpoints).
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct DnsEvent {
|
||||
/// Timestamp of the packet carrying this DNS message.
|
||||
pub timestamp: Timestamp,
|
||||
/// Source IP address of the packet.
|
||||
pub src: IpAddr,
|
||||
/// Destination IP address of the packet (the DNS server, for a query).
|
||||
pub dst: IpAddr,
|
||||
/// Transport protocol carrying the DNS message (UDP or TCP).
|
||||
pub transport: Protocol,
|
||||
/// The parsed DNS message itself.
|
||||
pub message: DnsMessage,
|
||||
}
|
||||
|
||||
/// A fully parsed DNS message: the 12-byte header plus the Question and Answer
|
||||
/// sections. The Authority and Additional sections are not retained.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct DnsMessage {
|
||||
/// Transaction ID, used to correlate a query with its response.
|
||||
pub id: u16,
|
||||
/// `QR` bit: `false` = query, `true` = response.
|
||||
pub is_response: bool,
|
||||
/// `OPCODE` field (0 = standard QUERY, 1 = IQUERY, 2 = STATUS, ...).
|
||||
pub opcode: u8,
|
||||
/// Header flags (AA, TC, RD, RA).
|
||||
pub flags: DnsFlags,
|
||||
/// `RCODE` field (response code).
|
||||
pub rcode: DnsRCode,
|
||||
/// Entries of the Question section.
|
||||
pub questions: Vec<DnsQuestion>,
|
||||
/// Resource Records of the Answer section.
|
||||
pub answers: Vec<DnsRecord>,
|
||||
}
|
||||
|
||||
impl DnsMessage {
|
||||
/// Name of the first question, if any (the domain being looked up).
|
||||
pub fn query_name(&self) -> Option<&str> {
|
||||
self.questions.first().map(|q| q.name.as_str())
|
||||
}
|
||||
|
||||
/// Type of the first question, if any.
|
||||
pub fn query_type(&self) -> Option<DnsRecordType> {
|
||||
self.questions.first().map(|q| q.qtype)
|
||||
}
|
||||
|
||||
/// Human-readable, comma-separated summary of the answer records' data,
|
||||
/// suitable for a single table cell.
|
||||
pub fn answers_summary(&self) -> String {
|
||||
self.answers
|
||||
.iter()
|
||||
.map(|r| r.rdata.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
}
|
||||
}
|
||||
|
||||
/// Header flags retained from the DNS header.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
|
||||
pub struct DnsFlags {
|
||||
/// Authoritative Answer.
|
||||
pub aa: bool,
|
||||
/// TrunCation: the message was truncated.
|
||||
pub tc: bool,
|
||||
/// Recursion Desired.
|
||||
pub rd: bool,
|
||||
/// Recursion Available.
|
||||
pub ra: bool,
|
||||
}
|
||||
|
||||
/// An entry of the Question section.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct DnsQuestion {
|
||||
/// Queried domain name (already decoded, dots between labels).
|
||||
pub name: String,
|
||||
/// Query type (QTYPE).
|
||||
pub qtype: DnsRecordType,
|
||||
/// Query class (QCLASS); 1 = IN (Internet).
|
||||
pub qclass: u16,
|
||||
}
|
||||
|
||||
/// A Resource Record (used here for the Answer section).
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct DnsRecord {
|
||||
/// Owner name of the record.
|
||||
pub name: String,
|
||||
/// Record type.
|
||||
pub rtype: DnsRecordType,
|
||||
/// Record class; 1 = IN (Internet).
|
||||
pub class: u16,
|
||||
/// Time to live, in seconds.
|
||||
pub ttl: u32,
|
||||
/// Interpreted record data.
|
||||
pub rdata: DnsRData,
|
||||
}
|
||||
|
||||
/// DNS record/query type (TYPE / QTYPE field).
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub enum DnsRecordType {
|
||||
A,
|
||||
Ns,
|
||||
Cname,
|
||||
Soa,
|
||||
Ptr,
|
||||
Mx,
|
||||
Txt,
|
||||
Aaaa,
|
||||
Srv,
|
||||
/// Any other type, keeping its numeric value.
|
||||
Other(u16),
|
||||
}
|
||||
|
||||
impl DnsRecordType {
|
||||
pub fn from_u16(value: u16) -> Self {
|
||||
match value {
|
||||
1 => Self::A,
|
||||
2 => Self::Ns,
|
||||
5 => Self::Cname,
|
||||
6 => Self::Soa,
|
||||
12 => Self::Ptr,
|
||||
15 => Self::Mx,
|
||||
16 => Self::Txt,
|
||||
28 => Self::Aaaa,
|
||||
33 => Self::Srv,
|
||||
other => Self::Other(other),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for DnsRecordType {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::A => write!(f, "A"),
|
||||
Self::Ns => write!(f, "NS"),
|
||||
Self::Cname => write!(f, "CNAME"),
|
||||
Self::Soa => write!(f, "SOA"),
|
||||
Self::Ptr => write!(f, "PTR"),
|
||||
Self::Mx => write!(f, "MX"),
|
||||
Self::Txt => write!(f, "TXT"),
|
||||
Self::Aaaa => write!(f, "AAAA"),
|
||||
Self::Srv => write!(f, "SRV"),
|
||||
Self::Other(n) => write!(f, "TYPE{n}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// DNS response code (RCODE field).
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub enum DnsRCode {
|
||||
NoError,
|
||||
FormErr,
|
||||
ServFail,
|
||||
NxDomain,
|
||||
NotImpl,
|
||||
Refused,
|
||||
/// Any other code, keeping its numeric value.
|
||||
Other(u8),
|
||||
}
|
||||
|
||||
impl DnsRCode {
|
||||
pub fn from_u8(value: u8) -> Self {
|
||||
match value {
|
||||
0 => Self::NoError,
|
||||
1 => Self::FormErr,
|
||||
2 => Self::ServFail,
|
||||
3 => Self::NxDomain,
|
||||
4 => Self::NotImpl,
|
||||
5 => Self::Refused,
|
||||
other => Self::Other(other),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for DnsRCode {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::NoError => write!(f, "NOERROR"),
|
||||
Self::FormErr => write!(f, "FORMERR"),
|
||||
Self::ServFail => write!(f, "SERVFAIL"),
|
||||
Self::NxDomain => write!(f, "NXDOMAIN"),
|
||||
Self::NotImpl => write!(f, "NOTIMP"),
|
||||
Self::Refused => write!(f, "REFUSED"),
|
||||
Self::Other(n) => write!(f, "RCODE{n}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Interpreted record data (RDATA), by record type.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub enum DnsRData {
|
||||
/// IPv4 address (type A).
|
||||
A(Ipv4Addr),
|
||||
/// IPv6 address (type AAAA).
|
||||
Aaaa(Ipv6Addr),
|
||||
/// A domain name (types CNAME, NS, PTR).
|
||||
Name(String),
|
||||
/// Mail exchange (type MX): preference and mail server name.
|
||||
Mx { preference: u16, exchange: String },
|
||||
/// Text strings (type TXT).
|
||||
Txt(Vec<String>),
|
||||
/// Start of authority (type SOA).
|
||||
Soa {
|
||||
mname: String,
|
||||
rname: String,
|
||||
serial: u32,
|
||||
refresh: u32,
|
||||
retry: u32,
|
||||
expire: u32,
|
||||
minimum: u32,
|
||||
},
|
||||
/// Raw, uninterpreted data for unsupported types.
|
||||
Other(Vec<u8>),
|
||||
}
|
||||
|
||||
impl Display for DnsRData {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::A(ip) => write!(f, "{ip}"),
|
||||
Self::Aaaa(ip) => write!(f, "{ip}"),
|
||||
Self::Name(name) => write!(f, "{name}"),
|
||||
Self::Mx {
|
||||
preference,
|
||||
exchange,
|
||||
} => write!(f, "{preference} {exchange}"),
|
||||
Self::Txt(strings) => write!(f, "{}", strings.join(" ")),
|
||||
Self::Soa { mname, rname, .. } => write!(f, "{mname} {rname}"),
|
||||
Self::Other(bytes) => write!(f, "{} bytes", bytes.len()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
pub mod dns;
|
||||
pub mod manage_packets;
|
||||
pub mod parse_packets;
|
||||
pub mod traffic_preview;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue