blob: e464afdd58759750029a25a5298692b20a1ca911 [file]
// 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<u8>;
fn read_u16(&mut self) -> std::io::Result<u16>;
fn read_u32(&mut self) -> std::io::Result<u32>;
fn read_ipv4addr(&mut self) -> std::io::Result<Ipv4Addr>;
fn read_ipv6addr(&mut self) -> std::io::Result<Ipv6Addr>;
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<u8> {
let mut buf = [0; 1];
self.read_exact(&mut buf)?;
Ok(buf[0])
}
fn read_u16(&mut self) -> std::io::Result<u16> {
let mut buf = [0; 2];
self.read_exact(&mut buf)?;
Ok(u16::from_be_bytes(buf))
}
fn read_u32(&mut self) -> std::io::Result<u32> {
let mut buf = [0; 4];
self.read_exact(&mut buf)?;
Ok(u32::from_be_bytes(buf))
}
fn read_ipv4addr(&mut self) -> std::io::Result<Ipv4Addr> {
let mut buf = [0; 4];
self.read_exact(&mut buf)?;
Ok(Ipv4Addr::from(buf))
}
fn read_ipv6addr(&mut self) -> std::io::Result<Ipv6Addr> {
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<Question>,
answers: Vec<ResourceRecord>,
// Other types not needed
// Authority
// Additional
}
impl Message {
fn parse(cursor: &mut impl CursorExt) -> Result<Message> {
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<Vec<(IpAddr, String)>> {
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<u16> for ResponseCode {
type Error = DnsError;
fn try_from(value: u16) -> Result<Self> {
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<u16> for Opcode {
type Error = DnsError;
fn try_from(value: u16) -> Result<Self> {
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<Header> {
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<Question> {
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<ResourceRecord> {
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<ResourceData> for IpAddr {
fn from(resource_data: ResourceData) -> Self {
resource_data.0
}
}
impl ResourceData {
fn split_once(
cursor: &mut impl CursorExt,
resource_type: &ResourceType,
) -> Result<ResourceData> {
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<T> = core::result::Result<T, DnsError>;
#[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<std::io::Error> for DnsError {
fn from(err: std::io::Error) -> Self {
DnsError::IoError(err)
}
}
impl From<str::Utf8Error> for DnsError {
fn from(err: str::Utf8Error) -> Self {
DnsError::Utf8Error(err)
}
}
impl From<std::net::AddrParseError> 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<NamePart> {
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<String> {
Self::to_string_guard(cursor, 0)
}
fn to_string_guard(cursor: &mut impl CursorExt, jumps: usize) -> Result<String> {
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(())
}
}