// Copyright 2024 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. /// This module parses DNS response records and extracts fully /// qualified domain names (FQDNs) along with their corresponding /// IP Addresses (IpAddr). /// /// **Note:** This is not a general-purpose DNS response parser. It is /// designed to handle specific record types and response formats. use std::convert::TryFrom; use std::fmt; use std::io::{Cursor, Read, Seek, SeekFrom}; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; use std::str; #[allow(unused_imports)] use std::str::FromStr; // REGION CURSOR /// Extension trait providing convenient methods for reading primitive /// data types used by DNS messages from a `Cursor<&[u8]>`. trait CursorExt: Read + Seek + Clone { fn read_u8(&mut self) -> std::io::Result; fn read_u16(&mut self) -> std::io::Result; fn read_u32(&mut self) -> std::io::Result; fn read_ipv4addr(&mut self) -> std::io::Result; fn read_ipv6addr(&mut self) -> std::io::Result; fn get_ref(&self) -> &[u8]; fn position(&self) -> u64; fn set_position(&mut self, pos: u64); } impl CursorExt for Cursor<&[u8]> { fn read_u8(&mut self) -> std::io::Result { let mut buf = [0; 1]; self.read_exact(&mut buf)?; Ok(buf[0]) } fn read_u16(&mut self) -> std::io::Result { let mut buf = [0; 2]; self.read_exact(&mut buf)?; Ok(u16::from_be_bytes(buf)) } fn read_u32(&mut self) -> std::io::Result { let mut buf = [0; 4]; self.read_exact(&mut buf)?; Ok(u32::from_be_bytes(buf)) } fn read_ipv4addr(&mut self) -> std::io::Result { let mut buf = [0; 4]; self.read_exact(&mut buf)?; Ok(Ipv4Addr::from(buf)) } fn read_ipv6addr(&mut self) -> std::io::Result { let mut buf = [0; 16]; self.read_exact(&mut buf)?; Ok(Ipv6Addr::from(buf)) } fn get_ref(&self) -> &[u8] { self.get_ref() // Call the original get_ref method } fn position(&self) -> u64 { self.position() } fn set_position(&mut self, pos: u64) { self.set_position(pos) } } // END REGION CURSOR // REGION MESSAGE /// ''' /// +---------------------+ /// | Header | /// +---------------------+ /// | Question | the question for the name server /// +---------------------+ /// | Answer | RRs answering the question /// +---------------------+ /// | Authority | RRs pointing toward an authority /// +---------------------+ /// | Additional | RRs holding additional information /// +---------------------+ /// ''' #[derive(Debug)] struct Message { #[allow(dead_code)] header: Header, #[allow(dead_code)] questions: Vec, answers: Vec, // Other types not needed // Authority // Additional } impl Message { fn parse(cursor: &mut impl CursorExt) -> Result { let header = Header::parse(cursor)?; // Reject DNS messages that are not responses if !header.response { return Err(DnsError::ResponseExpected); } if header.opcode != Opcode::StandardQuery { return Err(DnsError::StandardQueryExpected); } if header.response_code != ResponseCode::NoError { return Err(DnsError::ResponseCodeExpected); } if header.answer_count == 0 { return Err(DnsError::AnswerExpected); } let mut questions = Vec::with_capacity(header.question_count); for _i in 0..header.question_count { let question = Question::split_once(cursor)?; questions.push(question); } let mut answers = Vec::with_capacity(header.answer_count); for _i in 0..header.answer_count { let answer = ResourceRecord::split_once(cursor)?; answers.push(answer); } Ok(Message { header, questions, answers }) } } pub fn parse_answers(bytes: &[u8]) -> Result> { let mut cursor = Cursor::new(bytes); let msg = Message::parse(&mut cursor)?; let mut responses = Vec::with_capacity(msg.answers.len()); for answer in msg.answers { responses.push((answer.resource_data.into(), answer.name)); } Ok(responses) } // END REGION MESSAGE // REGION HEADER /// Represents parsed header of the packet. /// The header contains the following fields: /// ''' /// 1 1 1 1 1 1 /// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 /// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ /// | ID | /// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ /// |QR| Opcode |AA|TC|RD|RA| Z | RCODE | /// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ /// | QDCOUNT | /// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ /// | ANCOUNT | /// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ /// | NSCOUNT | /// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ /// | ARCOUNT | /// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ /// ''' #[derive(Debug, PartialEq, Eq, Clone, Copy)] struct Header { /// A 16 bit identifier assigned by the program that /// generates any kind of query. This identifier is copied /// the corresponding reply and can be used by the requester /// to match up replies to outstanding queries. id: u16, /// A one bit field that specifies whether this message is a /// query (0), or a response (1). response: bool, /// A four bit field that specifies kind of query in this /// message. This value is set by the originator of a query /// and copied into the response. opcode: Opcode, response_code: ResponseCode, question_count: usize, answer_count: usize, nameserver_count: usize, additional_count: usize, } #[derive(Debug, PartialEq, Eq, Clone, Copy)] enum Opcode { /// Normal query StandardQuery = 0, /// Inverse query (query a name by IP) InverseQuery = 1, /// Server status request ServerStatusRequest = 2, } /// The RCODE value according to RFC 1035 #[derive(Debug, PartialEq, Eq, Clone, Copy)] enum ResponseCode { NoError, FormatError, ServerFailure, NameError, NotImplemented, Refused, } impl TryFrom for ResponseCode { type Error = DnsError; fn try_from(value: u16) -> Result { match value { 0 => Ok(ResponseCode::NoError), 1 => Ok(ResponseCode::FormatError), 2 => Ok(ResponseCode::ServerFailure), 3 => Ok(ResponseCode::NameError), 4 => Ok(ResponseCode::NotImplemented), 5 => Ok(ResponseCode::Refused), _ => Err(DnsError::InvalidResponseCode(value)), } } } impl TryFrom for Opcode { type Error = DnsError; fn try_from(value: u16) -> Result { match value { 0 => Ok(Opcode::StandardQuery), 1 => Ok(Opcode::InverseQuery), 2 => Ok(Opcode::ServerStatusRequest), _ => Err(DnsError::InvalidOpcode(value)), } } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] struct Flag(u16); impl Flag { const RESPONSE: u16 = 0x8000; const OPCODE_MASK: u16 = 0x7800; const RESERVED_MASK: u16 = 0x0004; const RESPONSE_CODE_MASK: u16 = 0x000F; fn new(value: u16) -> Self { Self(value) } fn is_set(&self, mask: u16) -> bool { (self.0 & mask) == mask } fn get(&self, mask: u16) -> u16 { (self.0 & mask) >> mask.trailing_zeros() } } impl Header { /// Parse the header into a header structure fn parse(cursor: &mut impl CursorExt) -> Result
{ let id = cursor.read_u16()?; let f = cursor.read_u16()?; let question_count = cursor.read_u16()? as usize; let answer_count = cursor.read_u16()? as usize; let nameserver_count = cursor.read_u16()? as usize; let additional_count = cursor.read_u16()? as usize; let flags = Flag::new(f); if flags.get(Flag::RESERVED_MASK) != 0 { return Err(DnsError::ReservedBitsAreNonZero); } let header = Header { id, response: flags.is_set(Flag::RESPONSE), opcode: Opcode::try_from(flags.get(Flag::OPCODE_MASK))?, response_code: ResponseCode::try_from(flags.get(Flag::RESPONSE_CODE_MASK))?, question_count, answer_count, nameserver_count, additional_count, }; Ok(header) } } // END REGION HEADER // REGION QUESTION /// 4.1.2. Question section format /// /// The question section is used to carry the "question" in most queries, /// i.e., the parameters that define what is being asked. The section /// contains QDCOUNT (usually 1) entries, each of the following format: /// ''' /// 1 1 1 1 1 1 /// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 /// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ /// | | /// / QNAME / /// / / /// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ /// | QTYPE | /// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ /// | QCLASS | /// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ /// ''' #[derive(Debug)] struct Question { #[allow(dead_code)] name: String, #[allow(dead_code)] qtype: u16, #[allow(dead_code)] qclass: u16, } impl Question { fn split_once(cursor: &mut impl CursorExt) -> Result { let name = Name::to_string(cursor)?; let qtype = cursor.read_u16()?; let qclass = cursor.read_u16()?; Ok(Question { name, qtype, qclass }) } } // END REGION QUESTION // REGION RESOURCE RECORD /// All RRs have the same top level format shown below: /// /// ''' /// 1 1 1 1 1 1 /// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 /// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ /// | | /// / / /// / NAME / /// | | /// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ /// | TYPE | /// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ /// | CLASS | /// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ /// | TTL | /// | | /// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ /// | RDLENGTH | /// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--| /// / RDATA / /// / / /// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ /// ''' // DNS resource record classes. // // The only one we care about is Internet #[derive(Debug)] enum ResourceClass { Internet = 1, } // Type fields in resource records. // // The only ones we care about are A and AAAA #[derive(Debug)] enum ResourceType { // IPv4 address. A = 1, // IPv6 address, see RFC 3596. Aaaa = 28, } #[derive(Debug)] struct ResourceRecord { name: String, #[allow(dead_code)] resource_type: ResourceType, #[allow(dead_code)] resource_class: ResourceClass, #[allow(dead_code)] ttl: u32, resource_data: ResourceData, } impl ResourceRecord { fn split_once(cursor: &mut impl CursorExt) -> Result { let name = Name::to_string(cursor)?; let rtype = cursor.read_u16()?; let resource_type = match rtype { x if x == ResourceType::A as u16 => ResourceType::A, x if x == ResourceType::Aaaa as u16 => ResourceType::Aaaa, _ => return Err(DnsError::InvalidResourceType), }; let rclass = cursor.read_u16()?; let resource_class = match rclass { x if x == ResourceClass::Internet as u16 => ResourceClass::Internet, _ => return Err(DnsError::InvalidResourceClass), }; let ttl = cursor.read_u32()?; let _ = cursor.read_u16()?; let resource_data = ResourceData::split_once(cursor, &resource_type)?; Ok(ResourceRecord { name, resource_type, resource_class, ttl, resource_data }) } } // Only interested in IpAddr resource data #[derive(Debug, PartialEq)] struct ResourceData(IpAddr); impl From for IpAddr { fn from(resource_data: ResourceData) -> Self { resource_data.0 } } impl ResourceData { fn split_once( cursor: &mut impl CursorExt, resource_type: &ResourceType, ) -> Result { match resource_type { ResourceType::A => Ok(ResourceData(cursor.read_ipv4addr()?.into())), ResourceType::Aaaa => Ok(ResourceData(cursor.read_ipv6addr()?.into())), } } } // END REGION RESOURCE RECORD // REGION LABEL type Result = core::result::Result; #[derive(Debug)] pub enum DnsError { ResponseExpected, StandardQueryExpected, ResponseCodeExpected, AnswerExpected, PointerLoop, InvalidLength, Utf8Error(str::Utf8Error), InvalidResourceType, InvalidResourceClass, AddrParseError(std::net::AddrParseError), InvalidOpcode(u16), InvalidResponseCode(u16), ReservedBitsAreNonZero, IoError(std::io::Error), } impl std::error::Error for DnsError {} impl fmt::Display for DnsError { fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { write!(fmt, "{self:?}") } } impl From for DnsError { fn from(err: std::io::Error) -> Self { DnsError::IoError(err) } } impl From for DnsError { fn from(err: str::Utf8Error) -> Self { DnsError::Utf8Error(err) } } impl From for DnsError { fn from(err: std::net::AddrParseError) -> Self { DnsError::AddrParseError(err) } } // REGION NAME /// RFC 1035 4.1.4. Message compression /// /// In order to reduce the size of messages, the domain system /// utilizes a compression scheme which eliminates the repetition of /// domain names in a message. In this scheme, an entire domain name /// or a list of labels at the end of a domain name is replaced with a /// pointer to a prior occurrence of the same name. /// /// The pointer takes the form of a two octet sequence: /// /// ''' /// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ /// | 1 1| OFFSET | /// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ /// ''' enum NamePart { Label(String), Pointer(u64), Root, } const PTR_MASK: u8 = 0b11000000; impl NamePart { /// Domain name labels have a maximum length of 63 octets. const MAX: u8 = 63; #[allow(dead_code)] fn split_once(cursor: &mut impl CursorExt) -> Result { let size = cursor.read_u8()?; if size & PTR_MASK == PTR_MASK { let two = cursor.read_u8()?; let offset: u64 = u16::from_be_bytes([size & !PTR_MASK, two]).into(); return Ok(NamePart::Pointer(offset)); } if size == 0 { return Ok(NamePart::Root); } if size > Self::MAX { return Err(DnsError::InvalidLength); } let end = size as usize; let buffer_ref: &[u8] = cursor.get_ref(); let start = cursor.position() as usize; let label = str::from_utf8(&buffer_ref[start..start + end])?.to_string(); cursor.seek(SeekFrom::Current(end as i64))?; Ok(NamePart::Label(label)) } } /// The Fully Qualitifed Domain Name from ANSWER and RR records struct Name(); impl Name { // Convert a variable length QNAME or NAME to a String. // // The cursor is updated to the end of the first sequence of // labels, and not the position after a Pointer. This allows the // cursor to be used for reading the remainder of the Question or // ResourceRecord. // // Limit the number of Pointers in malificient messages to avoid // looping. // fn to_string(cursor: &mut impl CursorExt) -> Result { Self::to_string_guard(cursor, 0) } fn to_string_guard(cursor: &mut impl CursorExt, jumps: usize) -> Result { if jumps > 2 { return Err(DnsError::PointerLoop); } let mut name = String::with_capacity(255); loop { match NamePart::split_once(cursor)? { NamePart::Root => return Ok(name), NamePart::Pointer(offset) => { let mut pointer_cursor = cursor.clone(); pointer_cursor.set_position(offset); let pointer_name = Name::to_string_guard(&mut pointer_cursor, jumps + 1)?; name.push_str(&pointer_name); return Ok(name); } NamePart::Label(label) => { if !name.is_empty() { name.push('.'); } name.push_str(&label); } }; } } } // END REGION NAME #[cfg(test)] mod test_message { use super::*; #[test] fn test_dns_responses() -> Result<()> { let bytes: [u8; 81] = [ 0xc2, 0x87, 0x81, 0x80, 0x0, 0x1, 0x0, 0x2, 0x0, 0x0, 0x0, 0x0, 0x3, 0x69, 0x62, 0x6d, 0x3, 0x63, 0x6f, 0x6d, 0x0, 0x0, 0x1c, 0x0, 0x1, 0xc0, 0xc, 0x0, 0x1c, 0x0, 0x1, 0x0, 0x0, 0x0, 0x8, 0x0, 0x10, 0x26, 0x0, 0x14, 0x6, 0x5e, 0x0, 0x2, 0x93, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x38, 0x31, 0xc0, 0xc, 0x0, 0x1c, 0x0, 0x1, 0x0, 0x0, 0x0, 0x8, 0x0, 0x10, 0x26, 0x0, 0x14, 0x6, 0x5e, 0x0, 0x2, 0xaa, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x38, 0x31, ]; let bytes: &[u8] = &bytes; let answers = parse_answers(bytes)?; assert_eq!( *answers.get(0).unwrap(), (Ipv6Addr::from_str("2600:1406:5e00:293::3831")?.into(), "ibm.com".to_string()) ); assert_eq!( *answers.get(1).unwrap(), (Ipv6Addr::from_str("2600:1406:5e00:2aa::3831")?.into(), "ibm.com".to_string()) ); Ok(()) } }