use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
#[must_use]
pub fn pad_query(query: &[u8], min_len: usize) -> Vec<u8> {
let mut padded = query.to_vec();
padded.push(0x80);
let current = padded.len().max(min_len);
let base_target = current.next_multiple_of(64);
let rb: [u8; 4] = crate::random_bytes();
let extra_blocks = usize::try_from(u32::from_ne_bytes(rb) % 3).unwrap_or(0);
let target = base_target.saturating_add(extra_blocks.saturating_mul(64));
padded.resize(target, 0x00);
padded
}
pub fn unpad_response(decrypted: &[u8]) -> Result<Vec<u8>, String> {
let Some(marker_pos) = decrypted.iter().rposition(|&b| b != 0x00) else {
return Err("Invalid ISO/IEC 7816-4 padding".to_string());
};
if decrypted.get(marker_pos) != Some(&0x80) {
return Err("Invalid ISO/IEC 7816-4 padding".to_string());
}
Ok(decrypted.get(..marker_pos).unwrap_or_default().to_vec())
}
pub fn validate_domain(domain: &str) -> Result<(), String> {
if domain.is_empty() {
return Err("domain name is empty".to_string());
}
let mut encoded_len = 1usize; for label in domain.split('.') {
if label.is_empty() {
continue;
}
if label.len() > 63 {
return Err(format!("domain label '{label}' exceeds 63 bytes"));
}
encoded_len = encoded_len.saturating_add(1).saturating_add(label.len());
}
if encoded_len > 255 {
return Err("domain name exceeds 255 bytes when encoded".to_string());
}
Ok(())
}
#[must_use]
pub fn build_txt_record_query(domain: &str, txid: [u8; 2]) -> Vec<u8> {
build_query(domain, 0x0010, txid) }
#[must_use]
pub fn build_a_record_query(domain: &str, txid: [u8; 2]) -> Vec<u8> {
build_query(domain, 0x0001, txid) }
#[must_use]
pub fn build_aaaa_record_query(domain: &str, txid: [u8; 2]) -> Vec<u8> {
build_query(domain, 0x001c, txid) }
fn build_query(domain: &str, qtype: u16, txid: [u8; 2]) -> Vec<u8> {
let mut query: Vec<u8> = vec![
txid[0], txid[1], 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ];
for label in domain.split('.') {
if label.is_empty() {
continue;
}
let len = label.len().min(63);
query.push(u8::try_from(len).unwrap_or(63));
query.extend_from_slice(label.as_bytes().get(..len).unwrap_or(&[]));
}
query.push(0x00); query.extend_from_slice(&qtype.to_be_bytes());
query.extend_from_slice(&[0x00, 0x01]); query
}
fn read_u16(data: &[u8], offset: usize) -> Option<u16> {
let end = offset.checked_add(2)?;
let bytes: [u8; 2] = data.get(offset..end)?.try_into().ok()?;
Some(u16::from_be_bytes(bytes))
}
#[must_use]
pub fn parse_txt_response(data: &[u8]) -> Vec<Vec<u8>> {
let mut results = Vec::new();
if data.len() < 12 {
return results;
}
let Some(ancount) = read_u16(data, 6) else {
return results;
};
let mut offset = 12;
offset = skip_name(data, offset);
offset = offset.saturating_add(4);
for _ in 0..ancount {
offset = skip_name(data, offset);
let Some(rtype) = read_u16(data, offset) else {
break;
};
let Some(rdlength) = read_u16(data, offset.saturating_add(8)) else {
break;
};
let rdlength = usize::from(rdlength);
offset = offset.saturating_add(10);
let Some(rdata) = data.get(offset..offset.saturating_add(rdlength)) else {
break;
};
if rtype == 16 {
let mut txt_data = Vec::new();
let mut i = 0;
while let Some(&chunk_len_byte) = rdata.get(i) {
let chunk_len = usize::from(chunk_len_byte);
i = i.saturating_add(1);
let Some(chunk) = rdata.get(i..i.saturating_add(chunk_len)) else {
break;
};
txt_data.extend_from_slice(chunk);
i = i.saturating_add(chunk_len);
}
if !txt_data.is_empty() {
results.push(txt_data);
}
}
offset = offset.saturating_add(rdlength);
}
results
}
#[must_use]
pub fn parse_dns_response(data: &[u8]) -> Vec<IpAddr> {
let mut ips = Vec::new();
if data.len() < 12 {
return ips;
}
let Some(ancount) = read_u16(data, 6) else {
return ips;
};
let mut offset = 12;
offset = skip_name(data, offset);
offset = offset.saturating_add(4);
for _ in 0..ancount {
offset = skip_name(data, offset);
let Some(atype) = read_u16(data, offset) else {
break;
};
let Some(rdlength) = read_u16(data, offset.saturating_add(8)) else {
break;
};
let rdlength = usize::from(rdlength);
offset = offset.saturating_add(10);
if atype == 1
&& rdlength == 4
&& let Some(Ok(bytes)) = data
.get(offset..offset.saturating_add(4))
.map(<[u8; 4]>::try_from)
{
ips.push(IpAddr::V4(Ipv4Addr::from(bytes)));
} else if atype == 28
&& rdlength == 16
&& let Some(Ok(bytes)) = data
.get(offset..offset.saturating_add(16))
.map(<[u8; 16]>::try_from)
{
ips.push(IpAddr::V6(Ipv6Addr::from(bytes)));
}
let Some(next_offset) = offset
.checked_add(rdlength)
.filter(|&end| end <= data.len())
else {
break;
};
offset = next_offset;
}
ips
}
fn skip_name(data: &[u8], mut offset: usize) -> usize {
loop {
let Some(&byte) = data.get(offset) else {
return offset;
};
if (byte & 0xC0) == 0xC0 {
return offset.saturating_add(2);
}
let label_len = usize::from(byte);
if label_len == 0 {
return offset.saturating_add(1);
}
offset = offset.saturating_add(1).saturating_add(label_len);
}
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::cast_possible_truncation,
clippy::indexing_slicing,
clippy::arithmetic_side_effects,
clippy::as_conversions
)]
mod tests {
use super::*;
#[test]
fn test_validate_domain_accepts_normal_names() {
assert!(validate_domain("github.com").is_ok());
assert!(validate_domain("a.b.c.example.").is_ok());
}
#[test]
fn test_validate_domain_rejects_empty() {
assert!(validate_domain("").is_err());
}
#[test]
fn test_validate_domain_rejects_oversized_label() {
let label = "a".repeat(64);
assert!(validate_domain(&format!("{label}.com")).is_err());
let ok_label = "a".repeat(63);
assert!(validate_domain(&format!("{ok_label}.com")).is_ok());
}
#[test]
fn test_validate_domain_rejects_oversized_name() {
let label = "a".repeat(63);
let domain = [label.as_str(); 5].join(".");
assert!(validate_domain(&domain).is_err());
}
#[test]
fn test_parse_dns_response_aaaa_record() {
let mut response = Vec::new();
response.extend_from_slice(&[
0xab, 0xcd, 0x81, 0x80, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00,
]);
response.extend_from_slice(&[6]);
response.extend_from_slice(b"github");
response.extend_from_slice(&[3]);
response.extend_from_slice(b"com");
response.extend_from_slice(&[0]);
response.extend_from_slice(&[0x00, 0x1c, 0x00, 0x01]); response.extend_from_slice(&[0xc0, 0x0c]);
response.extend_from_slice(&[0x00, 0x1c, 0x00, 0x01]);
response.extend_from_slice(&[0x00, 0x00, 0x00, 0x3c]);
response.extend_from_slice(&[0x00, 0x10]); let addr = Ipv6Addr::new(0x2606, 0x4700, 0x4700, 0, 0, 0, 0, 0x1111);
response.extend_from_slice(&addr.octets());
let parsed = parse_dns_response(&response);
assert_eq!(parsed, vec![IpAddr::V6(addr)]);
}
#[test]
fn test_build_txt_record_query_has_txt_qtype() {
let query = build_txt_record_query("2.dnscrypt-cert.quad9.net", [0x11, 0x22]);
assert_eq!(&query[query.len() - 4..query.len() - 2], &[0x00, 0x10]);
}
#[test]
fn test_parse_txt_response_extracts_chunks() {
let mut response = Vec::new();
response.extend_from_slice(&[
0xab, 0xcd, 0x81, 0x80, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00,
]);
response.extend_from_slice(&[4]);
response.extend_from_slice(b"test");
response.extend_from_slice(&[0]);
response.extend_from_slice(&[0x00, 0x10, 0x00, 0x01]); response.extend_from_slice(&[0xc0, 0x0c]);
response.extend_from_slice(&[0x00, 0x10, 0x00, 0x01]); response.extend_from_slice(&[0x00, 0x00, 0x00, 0x3c]);
let txt_chunk = b"hello";
response.extend_from_slice(&[0x00, (txt_chunk.len() + 1) as u8]); response.push(txt_chunk.len() as u8);
response.extend_from_slice(txt_chunk);
let parsed = parse_txt_response(&response);
assert_eq!(parsed, vec![txt_chunk.to_vec()]);
}
#[test]
fn test_build_query_skips_empty_labels() {
let query = build_a_record_query(".github..com.", [0, 0]);
assert_eq!(query[12], 6);
assert_eq!(&query[13..19], b"github");
assert_eq!(query[19], 3);
assert_eq!(&query[20..23], b"com");
assert_eq!(query[23], 0); }
#[test]
fn test_parse_txt_response_rejects_short_input() {
assert!(parse_txt_response(&[]).is_empty());
assert!(parse_txt_response(&[0; 10]).is_empty());
}
#[test]
fn test_parse_txt_response_ignores_non_txt_records() {
let mut response = Vec::new();
response.extend_from_slice(&[
0xab, 0xcd, 0x81, 0x80, 0x00, 0x01, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00,
]);
response.extend_from_slice(&[4]);
response.extend_from_slice(b"test");
response.extend_from_slice(&[0]);
response.extend_from_slice(&[0x00, 0x10, 0x00, 0x01]);
response.extend_from_slice(&[0xc0, 0x0c]);
response.extend_from_slice(&[0x00, 0x01, 0x00, 0x01]);
response.extend_from_slice(&[0x00, 0x00, 0x00, 0x3c]);
response.extend_from_slice(&[0x00, 0x04]);
response.extend_from_slice(&[1, 2, 3, 4]);
response.extend_from_slice(&[0xc0, 0x0c]);
response.extend_from_slice(&[0x00, 0x10, 0x00, 0x01]);
response.extend_from_slice(&[0x00, 0x00, 0x00, 0x3c]);
let txt_chunk = b"hi";
response.extend_from_slice(&[0x00, (txt_chunk.len() + 1) as u8]);
response.push(txt_chunk.len() as u8);
response.extend_from_slice(txt_chunk);
let parsed = parse_txt_response(&response);
assert_eq!(parsed, vec![txt_chunk.to_vec()]);
}
#[test]
fn test_parse_txt_response_truncated_answer_header() {
let mut response = Vec::new();
response.extend_from_slice(&[
0xab, 0xcd, 0x81, 0x80, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00,
]);
response.extend_from_slice(&[4]);
response.extend_from_slice(b"test");
response.extend_from_slice(&[0]);
response.extend_from_slice(&[0x00, 0x10, 0x00, 0x01]);
response.extend_from_slice(&[0xc0, 0x0c]);
response.extend_from_slice(&[0x00, 0x10]); assert!(parse_txt_response(&response).is_empty());
}
#[test]
fn test_parse_txt_response_rdlength_exceeds_buffer() {
let mut response = Vec::new();
response.extend_from_slice(&[
0xab, 0xcd, 0x81, 0x80, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00,
]);
response.extend_from_slice(&[4]);
response.extend_from_slice(b"test");
response.extend_from_slice(&[0]);
response.extend_from_slice(&[0x00, 0x10, 0x00, 0x01]);
response.extend_from_slice(&[0xc0, 0x0c]);
response.extend_from_slice(&[0x00, 0x10, 0x00, 0x01]);
response.extend_from_slice(&[0x00, 0x00, 0x00, 0x3c]);
response.extend_from_slice(&[0xff, 0xff]); assert!(parse_txt_response(&response).is_empty());
}
#[test]
fn test_parse_txt_response_chunk_len_overflow() {
let mut response = Vec::new();
response.extend_from_slice(&[
0xab, 0xcd, 0x81, 0x80, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00,
]);
response.extend_from_slice(&[4]);
response.extend_from_slice(b"test");
response.extend_from_slice(&[0]);
response.extend_from_slice(&[0x00, 0x10, 0x00, 0x01]);
response.extend_from_slice(&[0xc0, 0x0c]);
response.extend_from_slice(&[0x00, 0x10, 0x00, 0x01]);
response.extend_from_slice(&[0x00, 0x00, 0x00, 0x3c]);
response.extend_from_slice(&[0x00, 0x02]); response.extend_from_slice(&[0xff, 0x00]); assert!(parse_txt_response(&response).is_empty());
}
#[test]
fn test_parse_dns_response_truncated_answer_and_rdlength_overflow() {
assert!(parse_dns_response(&[]).is_empty());
assert!(parse_dns_response(&[0; 10]).is_empty());
let mut response = Vec::new();
response.extend_from_slice(&[
0xab, 0xcd, 0x81, 0x80, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00,
]);
response.extend_from_slice(&[6]);
response.extend_from_slice(b"github");
response.extend_from_slice(&[3]);
response.extend_from_slice(b"com");
response.extend_from_slice(&[0]);
response.extend_from_slice(&[0x00, 0x01, 0x00, 0x01]);
response.extend_from_slice(&[0xc0, 0x0c]);
response.extend_from_slice(&[0x00, 0x01, 0x00, 0x01]);
response.extend_from_slice(&[0x00, 0x00, 0x00, 0x3c]);
response.extend_from_slice(&[0xff, 0xff]); assert!(parse_dns_response(&response).is_empty());
}
#[test]
fn test_skip_name_handles_truncated_label() {
let data = [0u8; 5];
let mut truncated = data.to_vec();
truncated[0] = 100; assert_eq!(skip_name(&truncated, 0), 101);
assert_eq!(skip_name(&truncated, 10), 10);
}
}