Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 45 additions & 69 deletions src/dns/doh.rs
Original file line number Diff line number Diff line change
Expand Up @@ -366,79 +366,38 @@ fn doh_records_from_wire_response(
}

fn wire_record_data(packet: &[u8], record: &wire::ResourceRecord<'_>) -> Result<String, DnsError> {
let offset = record.data_offset;
let len = record.data.len();
let rdata = record.data;
let value = match (record.typ, len) {
(DNS_TYPE_A, 4) => IpAddr::from([rdata[0], rdata[1], rdata[2], rdata[3]]).to_string(),
(DNS_TYPE_AAAA, 16) => {
let mut octets = [0u8; 16];
octets.copy_from_slice(rdata);
IpAddr::from(octets).to_string()
}
(wire::TYPE_CNAME | wire::TYPE_NS, _) => {
wire::read_name(packet, offset)
.map_err(|err| DnsError(err.to_string()))?
.0
}
(wire::TYPE_TXT, _) => parse_txt_rdata(rdata),
(wire::TYPE_MX, 3..) => {
let pref = wire::read_u16(packet, offset).map_err(|err| DnsError(err.to_string()))?;
let name = wire::read_name(packet, offset + 2)
.map_err(|err| DnsError(err.to_string()))?
.0;
format!("{pref} {name}")
}
(wire::TYPE_SOA, _) => parse_soa_rdata(packet, offset)?,
(wire::TYPE_SRV, 7..) => {
let priority =
wire::read_u16(packet, offset).map_err(|err| DnsError(err.to_string()))?;
let weight =
wire::read_u16(packet, offset + 2).map_err(|err| DnsError(err.to_string()))?;
let port =
wire::read_u16(packet, offset + 4).map_err(|err| DnsError(err.to_string()))?;
let target = wire::read_name(packet, offset + 6)
.map_err(|err| DnsError(err.to_string()))?
.0;
format!("{priority} {weight} {port} {target}")
}
_ => generic_rdata(rdata),
let decoded = wire::decode_rdata(packet, record.typ, record.data_offset, record.data.len())
.map_err(|err| DnsError(err.to_string()))?;
let value = match decoded {
wire::DecodedRdata::Address(address) => address.to_string(),
wire::DecodedRdata::Name(name) => name,
wire::DecodedRdata::Text(text) => text,
wire::DecodedRdata::Mx {
preference,
exchange,
} => format!("{preference} {exchange}"),
wire::DecodedRdata::Soa {
ns,
mailbox,
serial,
refresh,
retry,
expire,
minimum,
} => format!(
"{ns} {mailbox} serial={serial} refresh={refresh} retry={retry} expire={expire} minttl={minimum}"
),
wire::DecodedRdata::Srv {
priority,
weight,
port,
target,
} => format!("{priority} {weight} {port} {target}"),
wire::DecodedRdata::Raw(raw) => generic_rdata(raw),
};
Ok(value)
}

fn parse_txt_rdata(raw: &[u8]) -> String {
let mut parts = Vec::new();
let mut offset = 0;
while offset < raw.len() {
let len = usize::from(raw[offset]);
offset += 1;
if offset + len > raw.len() {
parts.push(String::from_utf8_lossy(&raw[offset - 1..]).into_owned());
break;
}
parts.push(String::from_utf8_lossy(&raw[offset..offset + len]).into_owned());
offset += len;
}
parts.join(" ")
}

fn parse_soa_rdata(packet: &[u8], offset: usize) -> Result<String, DnsError> {
let (ns, mut next) =
wire::read_name(packet, offset).map_err(|err| DnsError(err.to_string()))?;
let (mbox, next_after_mbox) =
wire::read_name(packet, next).map_err(|err| DnsError(err.to_string()))?;
next = next_after_mbox;
let serial = wire::read_u32(packet, next).map_err(|err| DnsError(err.to_string()))?;
let refresh = wire::read_u32(packet, next + 4).map_err(|err| DnsError(err.to_string()))?;
let retry = wire::read_u32(packet, next + 8).map_err(|err| DnsError(err.to_string()))?;
let expire = wire::read_u32(packet, next + 12).map_err(|err| DnsError(err.to_string()))?;
let min_ttl = wire::read_u32(packet, next + 16).map_err(|err| DnsError(err.to_string()))?;
Ok(format!(
"{ns} {mbox} serial={serial} refresh={refresh} retry={retry} expire={expire} minttl={min_ttl}"
))
}

fn generic_rdata(raw: &[u8]) -> String {
format!(r"\# {} {}", raw.len(), hex_encode(raw))
}
Expand Down Expand Up @@ -918,6 +877,23 @@ mod tests {
response
}

#[test]
fn wire_response_rejects_malformed_rdata_before_adjacent_record() {
let query = wire::build_query(0x1234, "example.com", wire::TYPE_MX).unwrap();
let response = wire_response(
&query,
vec![
(wire::TYPE_MX, 30, vec![0, 10, 3, b'm']),
(wire::TYPE_MX, 30, vec![0, 10, 0]),
],
);

let err = doh_records_from_wire_response(&response, 0x1234, "example.com", wire::TYPE_MX)
.unwrap_err();

assert!(err.to_string().contains("short DNS name label"));
}

#[test]
fn wire_response_rejects_unrelated_answer_owner() {
let query = wire::build_query(0x1234, "example.com", wire::TYPE_A).unwrap();
Expand Down
111 changes: 33 additions & 78 deletions src/dns/inspect/rdata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,88 +38,43 @@ pub(super) fn resource_value(
offset: usize,
len: usize,
) -> Result<Option<String>, FetchError> {
let rdata = &packet[offset..offset + len];
let value = match typ {
DNS_TYPE_A if len == 4 => {
IpAddr::from([rdata[0], rdata[1], rdata[2], rdata[3]]).to_string()
}
DNS_TYPE_AAAA if len == 16 => {
let mut octets = [0u8; 16];
octets.copy_from_slice(rdata);
IpAddr::from(octets).to_string()
}
DNS_TYPE_CNAME | DNS_TYPE_NS => {
wire::read_name(packet, offset)
.map_err(|err| FetchError::Message(err.to_string()))?
.0
}
DNS_TYPE_TXT => parse_txt_rdata(rdata),
DNS_TYPE_MX if len >= 3 => {
let pref = wire::read_u16(packet, offset)
.map_err(|err| FetchError::Message(err.to_string()))?;
let name = wire::read_name(packet, offset + 2)
.map_err(|err| FetchError::Message(err.to_string()))?
.0;
format!("{pref} {name}")
}
DNS_TYPE_SOA => parse_soa_rdata(packet, offset)?,
DNS_TYPE_SRV if len >= 7 => {
let priority = wire::read_u16(packet, offset)
.map_err(|err| FetchError::Message(err.to_string()))?;
let weight = wire::read_u16(packet, offset + 2)
.map_err(|err| FetchError::Message(err.to_string()))?;
let port = wire::read_u16(packet, offset + 4)
.map_err(|err| FetchError::Message(err.to_string()))?;
let target = wire::read_name(packet, offset + 6)
.map_err(|err| FetchError::Message(err.to_string()))?
.0;
format!("{priority} {weight} {port} {target}")
}
DNS_TYPE_SVCB | DNS_TYPE_HTTPS => crate::dns::svcb::format_rdata(rdata)
.unwrap_or_else(|| format!("0x{}", hex_encode(rdata))),
DNS_TYPE_CAA => format_caa(rdata),
_ => return Ok(None),
let decoded = wire::decode_rdata(packet, typ, offset, len)
.map_err(|err| FetchError::Message(err.to_string()))?;
let value = match decoded {
wire::DecodedRdata::Address(address) => address.to_string(),
wire::DecodedRdata::Name(name) => name,
wire::DecodedRdata::Text(text) => text,
wire::DecodedRdata::Mx {
preference,
exchange,
} => format!("{preference} {exchange}"),
wire::DecodedRdata::Soa {
ns,
mailbox,
serial,
refresh,
retry,
expire,
minimum,
} => format!(
"{ns} {mailbox} serial={serial} refresh={refresh} retry={retry} expire={expire} minttl={minimum}"
),
wire::DecodedRdata::Srv {
priority,
weight,
port,
target,
} => format!("{priority} {weight} {port} {target}"),
wire::DecodedRdata::Raw(raw) => match typ {
DNS_TYPE_SVCB | DNS_TYPE_HTTPS => crate::dns::svcb::format_rdata(raw)
.ok_or_else(|| FetchError::Message("malformed DNS RDATA".to_string()))?,
DNS_TYPE_CAA => format_caa(raw),
_ => return Ok(None),
},
};
Ok(Some(value))
}

fn parse_txt_rdata(raw: &[u8]) -> String {
let mut parts = Vec::new();
let mut offset = 0;
while offset < raw.len() {
let len = usize::from(raw[offset]);
offset += 1;
if offset + len > raw.len() {
parts.push(String::from_utf8_lossy(&raw[offset - 1..]).into_owned());
break;
}
parts.push(String::from_utf8_lossy(&raw[offset..offset + len]).into_owned());
offset += len;
}
parts.join(" ")
}

fn parse_soa_rdata(packet: &[u8], offset: usize) -> Result<String, FetchError> {
let (ns, mut next) =
wire::read_name(packet, offset).map_err(|err| FetchError::Message(err.to_string()))?;
let (mbox, next_after_mbox) =
wire::read_name(packet, next).map_err(|err| FetchError::Message(err.to_string()))?;
next = next_after_mbox;
let serial =
wire::read_u32(packet, next).map_err(|err| FetchError::Message(err.to_string()))?;
let refresh =
wire::read_u32(packet, next + 4).map_err(|err| FetchError::Message(err.to_string()))?;
let retry =
wire::read_u32(packet, next + 8).map_err(|err| FetchError::Message(err.to_string()))?;
let expire =
wire::read_u32(packet, next + 12).map_err(|err| FetchError::Message(err.to_string()))?;
let min_ttl =
wire::read_u32(packet, next + 16).map_err(|err| FetchError::Message(err.to_string()))?;
Ok(format!(
"{ns} {mbox} serial={serial} refresh={refresh} retry={retry} expire={expire} minttl={min_ttl}"
))
}

pub(super) fn type_label(typ: u16) -> String {
match typ {
DNS_TYPE_A => "A".to_string(),
Expand Down
Loading
Loading