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
30 changes: 26 additions & 4 deletions src/dns/doh.rs
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ async fn lookup_doh_wire_records_with_client(
return Err(WireDohError::Fallback);
}

match doh_records_from_wire_response(response.body(), id) {
match doh_records_from_wire_response(response.body(), id, host, dns_type) {
Ok(records) => Ok(records),
Err(err) if is_dns_message_response(&response) || is_dns_wire_error(&err) => {
Err(WireDohError::Fatal(err))
Expand Down Expand Up @@ -345,24 +345,27 @@ fn ip_records(records: Vec<DohRecord>, answer_type: u16) -> Result<Vec<DnsRecord
fn doh_records_from_wire_response(
raw: &[u8],
expected_id: u16,
expected_name: &str,
expected_type: u16,
) -> Result<Vec<DohRecord>, DnsError> {
let records =
wire::parse_response(raw, expected_id).map_err(|err| DnsError(err.to_string()))?;
wire::parse_response(raw, expected_id, expected_name, expected_type, DNS_CLASS_IN)
.map_err(|err| DnsError(err.to_string()))?;
let mut out = Vec::new();
for record in records {
if record.class != DNS_CLASS_IN {
continue;
}
out.push(DohRecord {
answer_type: record.typ,
data: wire_record_data(raw, record)?,
data: wire_record_data(raw, &record)?,
ttl: Some(record.ttl),
});
}
Ok(out)
}

fn wire_record_data(packet: &[u8], record: wire::ResourceRecord<'_>) -> Result<String, DnsError> {
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;
Expand Down Expand Up @@ -915,6 +918,25 @@ mod tests {
response
}

#[test]
fn wire_response_rejects_unrelated_answer_owner() {
let query = wire::build_query(0x1234, "example.com", wire::TYPE_A).unwrap();
let (_, _, question_end) = test_dns_question(&query).unwrap();
let mut response = wire_response(&query, vec![(wire::TYPE_A, 30, vec![192, 0, 2, 1])]);
let mut owner = Vec::new();
for label in "unrelated.example".split('.') {
owner.push(label.len() as u8);
owner.extend_from_slice(label.as_bytes());
}
owner.push(0);
response.splice(question_end..question_end + 2, owner);

let records =
doh_records_from_wire_response(&response, 0x1234, "example.com", wire::TYPE_A).unwrap();

assert!(records.is_empty());
}

#[tokio::test]
async fn lookup_doh_uses_rfc8484_wire_format() {
let seen = Arc::new(Mutex::new(Vec::new()));
Expand Down
44 changes: 30 additions & 14 deletions src/dns/inspect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -480,16 +480,18 @@ async fn lookup_udp_records(
let mut response = crate::dns::transport::query_udp(server_addr, &raw, udp_timeout)
.await
.map_err(QueryError::other)?;
let raw_records = match wire::parse_response(&response, id) {
Ok(records) => records,
Err(err) if err.is_truncated() => {
response = crate::dns::transport::query_tcp(server_addr, &raw, timeout)
.await
.map_err(QueryError::truncated)?;
wire::parse_response(&response, id).map_err(QueryError::truncated)?
}
Err(err) => return Err(QueryError::other(err)),
};
let raw_records =
match wire::parse_response(&response, id, host, query_type.dns_type, DNS_CLASS_IN) {
Ok(records) => records,
Err(err) if err.is_truncated() => {
response = crate::dns::transport::query_tcp(server_addr, &raw, timeout)
.await
.map_err(QueryError::truncated)?;
wire::parse_response(&response, id, host, query_type.dns_type, DNS_CLASS_IN)
.map_err(QueryError::truncated)?
}
Err(err) => return Err(QueryError::other(err)),
};
records_from_wire_response(&response, raw_records)
}

Expand Down Expand Up @@ -522,7 +524,7 @@ async fn lookup_tcp_records(
})
.await
.map_err(|_| QueryError::other("DNS lookup timed out"))??;
inspect_records_from_response(&response, id)
inspect_records_from_response(&response, id, host, query_type.dns_type)
}

/// Query a single record type over a fresh TCP+TLS connection.
Expand Down Expand Up @@ -556,7 +558,7 @@ async fn lookup_tls_records(
})
.await
.map_err(|_| QueryError::other("DNS lookup timed out"))??;
inspect_records_from_response(&response, id)
inspect_records_from_response(&response, id, host, query_type.dns_type)
}

/// Query a single record type over a fresh QUIC connection.
Expand Down Expand Up @@ -592,14 +594,28 @@ async fn lookup_quic_records(
.await
.map_err(|_| QueryError::other("DNS lookup timed out"))?
.map_err(QueryError::other)?;
inspect_records_from_response(&response, crate::dns::resolver::DOQ_MESSAGE_ID)
inspect_records_from_response(
&response,
crate::dns::resolver::DOQ_MESSAGE_ID,
host,
query_type.dns_type,
)
}

fn inspect_records_from_response(
response: &[u8],
expected_id: u16,
expected_name: &str,
expected_type: u16,
) -> Result<Vec<Record>, QueryError> {
let raw_records = wire::parse_response(response, expected_id).map_err(QueryError::other)?;
let raw_records = wire::parse_response(
response,
expected_id,
expected_name,
expected_type,
DNS_CLASS_IN,
)
.map_err(QueryError::other)?;
records_from_wire_response(response, raw_records)
}

Expand Down
87 changes: 76 additions & 11 deletions src/dns/resolver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,13 +80,13 @@ async fn lookup_udp_type_with_budget(
let response = crate::dns::transport::query_udp(*server_addr, &raw, timeout)
.await
.map_err(resolver_error)?;
match dns_records_from_response(&response, id) {
match dns_records_from_response(&response, id, host, dns_type) {
Ok(records) => Ok(records),
Err(err) if err.is_truncated() => {
let response = crate::dns::transport::query_tcp(*server_addr, &raw, budget)
.await
.map_err(resolver_error)?;
dns_records_from_response(&response, id).map_err(resolver_error)
dns_records_from_response(&response, id, host, dns_type).map_err(resolver_error)
}
Err(err) => Err(resolver_error(err)),
}
Expand Down Expand Up @@ -216,7 +216,7 @@ pub(crate) async fn lookup_quic_type(
.map_err(resolver_error)
})
.await?;
dns_records_from_response(&response, DOQ_MESSAGE_ID).map_err(resolver_error)
dns_records_from_response(&response, DOQ_MESSAGE_ID, host, dns_type).map_err(resolver_error)
}

async fn lookup_stream_type<S: AsyncRead + AsyncWrite + Unpin>(
Expand All @@ -240,7 +240,7 @@ async fn lookup_stream_type<S: AsyncRead + AsyncWrite + Unpin>(
if response.len() < 2 {
return Err(ResolverError("short DNS response".to_string()));
}
dns_records_from_response(&response, id).map_err(resolver_error)
dns_records_from_response(&response, id, host, dns_type).map_err(resolver_error)
}

async fn run_stream_lookup<S: AsyncRead + AsyncWrite + Unpin>(
Expand Down Expand Up @@ -286,8 +286,10 @@ async fn run_stream_lookup<S: AsyncRead + AsyncWrite + Unpin>(
let a_response = a_response.ok_or_else(|| ResolverError("missing DNS response".to_string()))?;
let aaaa_response =
aaaa_response.ok_or_else(|| ResolverError("missing DNS response".to_string()))?;
let a_records = dns_records_from_response(&a_response, id_a).map_err(resolver_error);
let aaaa_records = dns_records_from_response(&aaaa_response, id_aaaa).map_err(resolver_error);
let a_records =
dns_records_from_response(&a_response, id_a, host, DNS_TYPE_A).map_err(resolver_error);
let aaaa_records = dns_records_from_response(&aaaa_response, id_aaaa, host, DNS_TYPE_AAAA)
.map_err(resolver_error);
combine_results(a_records, aaaa_records)
}

Expand All @@ -304,8 +306,8 @@ async fn run_quic_lookup(
let timeout = budget.remaining().map_err(resolver_error)?;
let (a_records, aaaa_records) = with_optional_timeout(timeout, async {
let (a_result, aaaa_result) = tokio::join!(
quic_single_query(connection, query_a, DOQ_MESSAGE_ID),
quic_single_query(connection, query_aaaa, DOQ_MESSAGE_ID),
quic_single_query(connection, query_a, DOQ_MESSAGE_ID, host, DNS_TYPE_A),
quic_single_query(connection, query_aaaa, DOQ_MESSAGE_ID, host, DNS_TYPE_AAAA),
);
Ok::<_, ResolverError>((a_result, aaaa_result))
})
Expand All @@ -330,11 +332,13 @@ async fn quic_single_query(
connection: &quinn::Connection,
query: Vec<u8>,
expected_id: u16,
host: &str,
dns_type: u16,
) -> Result<Vec<DnsRecord>, ResolverError> {
let response = crate::dns::transport::quic_query(connection, &query)
.await
.map_err(resolver_error)?;
dns_records_from_response(&response, expected_id).map_err(resolver_error)
dns_records_from_response(&response, expected_id, host, dns_type).map_err(resolver_error)
}

fn combine_results(
Expand Down Expand Up @@ -388,9 +392,17 @@ fn dns_server_value_error(server: &str) -> ResolverError {
fn dns_records_from_response(
raw: &[u8],
expected_id: u16,
expected_name: &str,
expected_type: u16,
) -> Result<Vec<DnsRecord>, wire::WireError> {
let records = wire::parse_response(raw, expected_id)?;
Ok(records.into_iter().filter_map(ip_record).collect())
let records =
wire::parse_response(raw, expected_id, expected_name, expected_type, DNS_CLASS_IN)?;

Ok(records
.into_iter()
.filter(|record| record.typ == expected_type)
.filter_map(ip_record)
.collect())
}

fn ip_record(record: wire::ResourceRecord<'_>) -> Option<DnsRecord> {
Expand Down Expand Up @@ -556,6 +568,30 @@ mod tests {
drop(socket);
}

#[test]
fn response_rejects_unrelated_address_owner() {
let response =
test_response_with_answers(&[("unrelated.example", DNS_TYPE_A, vec![192, 0, 2, 1])]);

let records =
dns_records_from_response(&response, 0x1234, "example.com", DNS_TYPE_A).unwrap();
assert!(records.is_empty());
}

#[test]
fn response_accepts_address_through_cname_chain() {
let mut cname = Vec::new();
write_test_name(&mut cname, "alias.example");
let response = test_response_with_answers(&[
("example.com", wire::TYPE_CNAME, cname),
("alias.example", DNS_TYPE_A, vec![192, 0, 2, 1]),
]);

let records =
dns_records_from_response(&response, 0x1234, "example.com", DNS_TYPE_A).unwrap();
assert_eq!(records[0].ip, "192.0.2.1".parse::<IpAddr>().unwrap());
}

#[test]
fn normalize_udp_dns_server_matches_go_parser() {
assert_eq!(
Expand Down Expand Up @@ -1167,6 +1203,35 @@ mod tests {
Some(query)
}

fn test_response_with_answers(answers: &[(&str, u16, Vec<u8>)]) -> Vec<u8> {
let query = wire::build_query(0x1234, "example.com", DNS_TYPE_A).unwrap();
let question_end = question_end(&query).unwrap() + 4;
let mut response = Vec::new();
response.extend_from_slice(&0x1234u16.to_be_bytes());
response.extend_from_slice(&0x8180u16.to_be_bytes());
response.extend_from_slice(&1u16.to_be_bytes());
response.extend_from_slice(&(answers.len() as u16).to_be_bytes());
response.extend_from_slice(&0u32.to_be_bytes());
response.extend_from_slice(&query[12..question_end]);
for (name, typ, data) in answers {
write_test_name(&mut response, name);
response.extend_from_slice(&typ.to_be_bytes());
response.extend_from_slice(&DNS_CLASS_IN.to_be_bytes());
response.extend_from_slice(&60u32.to_be_bytes());
response.extend_from_slice(&(data.len() as u16).to_be_bytes());
response.extend_from_slice(data);
}
response
}

fn write_test_name(raw: &mut Vec<u8>, name: &str) {
for label in name.trim_end_matches('.').split('.') {
raw.push(label.len() as u8);
raw.extend_from_slice(label.as_bytes());
}
raw.push(0);
}

fn write_answer(response: &mut Vec<u8>, dns_type: u16, ttl: u32, data: &[u8]) {
response.extend_from_slice(&[0xc0, 0x0c]);
response.extend_from_slice(&dns_type.to_be_bytes());
Expand Down
5 changes: 3 additions & 2 deletions src/dns/svcb.rs
Original file line number Diff line number Diff line change
Expand Up @@ -304,13 +304,14 @@ async fn lookup_udp_https_records(
let mut response = crate::dns::transport::query_udp(server_addr, &raw, udp_timeout)
.await
.map_err(|err| FetchError::Runtime(err.to_string()))?;
let raw_records = match wire::parse_response(&response, id) {
let raw_records = match wire::parse_response(&response, id, host, DNS_TYPE_HTTPS, DNS_CLASS_IN)
{
Ok(records) => records,
Err(err) if err.is_truncated() => {
response = crate::dns::transport::query_tcp(server_addr, &raw, timeout)
.await
.map_err(|err| FetchError::Runtime(err.to_string()))?;
wire::parse_response(&response, id)
wire::parse_response(&response, id, host, DNS_TYPE_HTTPS, DNS_CLASS_IN)
.map_err(|err| FetchError::Runtime(err.to_string()))?
}
Err(err) => return Err(FetchError::Runtime(err.to_string())),
Expand Down
21 changes: 17 additions & 4 deletions src/dns/svcb/system.rs
Original file line number Diff line number Diff line change
Expand Up @@ -411,7 +411,8 @@ fn lookup_https_records_blocking(
#[cfg(test)]
fn records_from_wire_response(raw: &[u8]) -> Result<Vec<SvcbRecord>, FetchError> {
let records =
wire::parse_response_without_id(raw).map_err(|err| FetchError::Runtime(err.to_string()))?;
wire::parse_response_without_id(raw, "example.com", wire::TYPE_HTTPS, wire::CLASS_IN)
.map_err(|err| FetchError::Runtime(err.to_string()))?;
Ok(records
.into_iter()
.filter(|record| record.class == wire::CLASS_IN && record.typ == wire::TYPE_HTTPS)
Expand Down Expand Up @@ -446,7 +447,10 @@ fn write_dns_name(out: &mut Vec<u8>, name: &str) -> Option<()> {
mod tests {
use super::*;

fn dns_response_without_matching_query_id(answer_rdata: Vec<u8>) -> Vec<u8> {
fn dns_response_without_matching_query_id(
answer_owner: &str,
answer_rdata: Vec<u8>,
) -> Vec<u8> {
let mut out = Vec::new();
out.extend_from_slice(&0x1234_u16.to_be_bytes());
out.extend_from_slice(&0x8180_u16.to_be_bytes());
Expand All @@ -457,7 +461,7 @@ mod tests {
write_dns_name(&mut out, "example.com.").unwrap();
out.extend_from_slice(&wire::TYPE_HTTPS.to_be_bytes());
out.extend_from_slice(&wire::CLASS_IN.to_be_bytes());
out.extend_from_slice(&[0xc0, 0x0c]);
write_dns_name(&mut out, answer_owner).unwrap();
out.extend_from_slice(&wire::TYPE_HTTPS.to_be_bytes());
out.extend_from_slice(&wire::CLASS_IN.to_be_bytes());
out.extend_from_slice(&30_u32.to_be_bytes());
Expand All @@ -478,7 +482,7 @@ mod tests {

#[test]
fn parses_system_wire_response_without_generated_query_id() {
let raw = dns_response_without_matching_query_id(https_rdata());
let raw = dns_response_without_matching_query_id("example.com.", https_rdata());

let records = records_from_wire_response(&raw).unwrap();

Expand All @@ -487,4 +491,13 @@ mod tests {
assert_eq!(records[0].target, ".");
assert_eq!(records[0].alpn, ["h3"]);
}

#[test]
fn rejects_unrelated_https_answer_owner() {
let raw = dns_response_without_matching_query_id("unrelated.example.", https_rdata());

let records = records_from_wire_response(&raw).unwrap();

assert!(records.is_empty());
}
}
Loading
Loading