Adopt mature Git config and DNS codecs

This commit is contained in:
ms2k 2026-07-19 00:40:26 +09:00
commit bda342890f

View file

@ -5,6 +5,10 @@ use std::os::fd::{AsFd, AsRawFd, OwnedFd};
use std::path::Path;
use std::time::{Duration, Instant};
use hickory_proto::{
op::{Message, MessageType, OpCode, Query, ResponseCode},
rr::{DNSClass, Name, RData, RecordType},
};
use lazaret_core::codec::decode;
use lazaret_core::identity::{
ConfigurationFingerprint, DerivedPolicyDigest, DomainId, GenerationId, NamespaceIdentity,
@ -328,7 +332,7 @@ fn dns_probe(target: &DnsProbeTarget, transaction_id: u16) -> io::Result<DnsObse
"DNS probe response source mismatched resolver",
));
}
let resolved_addresses = dns_answers(&response[..length], transaction_id)?;
let resolved_addresses = dns_answers(&response[..length], transaction_id, &target.query_name)?;
Ok(DnsObservation {
resolver: target.resolver,
query_name: target.query_name.clone(),
@ -337,13 +341,25 @@ fn dns_probe(target: &DnsProbeTarget, transaction_id: u16) -> io::Result<DnsObse
}
fn dns_query(transaction_id: u16, name: &str) -> io::Result<Vec<u8>> {
let name = dns_name(name)?;
let mut query = Message::new(transaction_id, MessageType::Query, OpCode::Query);
query.metadata.recursion_desired = true;
query.add_query(Query::query(name, RecordType::A));
let bytes = query
.to_vec()
.map_err(|error| io::Error::other(format!("DNS query encoding failed: {error}")))?;
if bytes.len() > MAX_DNS_PACKET {
return Err(io::Error::other("DNS query exceeds packet bound"));
}
Ok(bytes)
}
fn dns_name(name: &str) -> io::Result<Name> {
if name.is_empty() || name.len() > 253 || !name.is_ascii() {
return Err(io::Error::other("DNS probe name is invalid"));
}
let mut query = Vec::with_capacity(name.len() + 18);
query.extend_from_slice(&transaction_id.to_be_bytes());
query.extend_from_slice(&[0x01, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0]);
for label in name.trim_end_matches('.').split('.') {
let labels = name.trim_end_matches('.');
for label in labels.split('.') {
if label.is_empty()
|| label.len() > 63
|| !label
@ -352,87 +368,62 @@ fn dns_query(transaction_id: u16, name: &str) -> io::Result<Vec<u8>> {
{
return Err(io::Error::other("DNS probe label is invalid"));
}
query.push(
u8::try_from(label.len()).map_err(|_| io::Error::other("DNS label is oversized"))?,
);
query.extend_from_slice(label.as_bytes());
}
query.extend_from_slice(&[0, 0, 1, 0, 1]);
Ok(query)
let mut name = Name::from_ascii(labels)
.map_err(|error| io::Error::other(format!("DNS probe name is invalid: {error}")))?;
name.set_fqdn(true);
Ok(name)
}
fn dns_answers(response: &[u8], transaction_id: u16) -> io::Result<Vec<IpAddress>> {
if response.len() < 12
|| response[..2] != transaction_id.to_be_bytes()
|| response[2] & 0x80 == 0
|| response[3] & 0x0f != 0
|| u16::from_be_bytes([response[4], response[5]]) != 1
fn dns_answers(
response: &[u8],
transaction_id: u16,
expected_name: &str,
) -> io::Result<Vec<IpAddress>> {
if response.len() > MAX_DNS_PACKET {
return Err(io::Error::other("DNS response exceeds packet bound"));
}
let expected_name = dns_name(expected_name)?;
let message = Message::from_vec(response)
.map_err(|error| io::Error::other(format!("DNS response decoding failed: {error}")))?;
if message.metadata.id != transaction_id
|| message.metadata.message_type != MessageType::Response
|| message.metadata.op_code != OpCode::Query
|| message.metadata.response_code != ResponseCode::NoError
|| message.metadata.truncation
{
return Err(io::Error::other("DNS probe response header is invalid"));
return Err(io::Error::other(
"DNS response metadata does not match query",
));
}
let answers = u16::from_be_bytes([response[6], response[7]]);
let mut offset = skip_dns_name(response, 12)?;
offset = offset
.checked_add(4)
.filter(|offset| *offset <= response.len())
.ok_or_else(|| io::Error::other("DNS probe question is truncated"))?;
let mut addresses = Vec::new();
for _ in 0..answers {
offset = skip_dns_name(response, offset)?;
let header_end = offset
.checked_add(10)
.filter(|end| *end <= response.len())
.ok_or_else(|| io::Error::other("DNS probe answer is truncated"))?;
let record_type = u16::from_be_bytes([response[offset], response[offset + 1]]);
let class = u16::from_be_bytes([response[offset + 2], response[offset + 3]]);
let data_length = usize::from(u16::from_be_bytes([
response[offset + 8],
response[offset + 9],
]));
let data_end = header_end
.checked_add(data_length)
.filter(|end| *end <= response.len())
.ok_or_else(|| io::Error::other("DNS probe record data is truncated"))?;
if record_type == 1 && class == 1 && data_length == 4 {
addresses.push(IpAddress::V4(
response[header_end..data_end]
.try_into()
.map_err(|_| io::Error::other("DNS A record is malformed"))?,
));
}
offset = data_end;
let [question] = message.queries.as_slice() else {
return Err(io::Error::other(
"DNS response does not contain exactly one question",
));
};
if question.name() != &expected_name
|| question.query_type() != RecordType::A
|| question.query_class() != DNSClass::IN
{
return Err(io::Error::other(
"DNS response question does not match query",
));
}
let addresses = message
.answers
.iter()
.filter(|answer| answer.record_type() == RecordType::A && answer.dns_class == DNSClass::IN)
.filter_map(|answer| match &answer.data {
RData::A(address) => Some(IpAddress::V4(Ipv4Addr::from(*address).octets())),
_ => None,
})
.collect::<Vec<_>>();
if addresses.is_empty() {
return Err(io::Error::other("DNS probe returned no IPv4 addresses"));
}
Ok(addresses)
}
fn skip_dns_name(packet: &[u8], mut offset: usize) -> io::Result<usize> {
loop {
let length = *packet
.get(offset)
.ok_or_else(|| io::Error::other("DNS name is truncated"))?;
offset += 1;
if length == 0 {
return Ok(offset);
}
if length & 0xc0 == 0xc0 {
return offset
.checked_add(1)
.filter(|end| *end <= packet.len())
.ok_or_else(|| io::Error::other("DNS compression pointer is truncated"));
}
if length & 0xc0 != 0 || length > 63 {
return Err(io::Error::other("DNS name label is malformed"));
}
offset = offset
.checked_add(usize::from(length))
.filter(|end| *end <= packet.len())
.ok_or_else(|| io::Error::other("DNS name label is truncated"))?;
}
}
fn read_sealed_policy(
policy_file: &File,
expected_digest: DerivedPolicyDigest,
@ -660,6 +651,12 @@ fn ipv4_cidr(address: [u8; 4], prefix_length: u8) -> String {
#[cfg(test)]
mod tests {
use std::net::Ipv4Addr;
use hickory_proto::rr::{
Record,
rdata::{A, CNAME},
};
use lazaret_core::network::derive_policy;
use lazaret_core::records::{
AddressFamily, AddressFamilyDisposition, AddressFamilyDispositionValue, DefaultAction,
@ -687,13 +684,110 @@ mod tests {
response[6..8].copy_from_slice(&1_u16.to_be_bytes());
response.extend_from_slice(&[0xc0, 0x0c, 0, 1, 0, 1, 0, 0, 0, 60, 0, 4, 93, 184, 216, 34]);
assert_eq!(
dns_answers(&response, transaction).unwrap(),
dns_answers(&response, transaction, "example.com").unwrap(),
vec![IpAddress::V4([93, 184, 216, 34])]
);
assert!(dns_answers(&response, transaction + 1).is_err());
assert!(dns_answers(&response, transaction + 1, "example.com").is_err());
assert!(dns_query(transaction, "bad..name").is_err());
}
#[test]
fn dns_codec_refuses_changed_metadata_and_question() {
assert!(
dns_answers(
&dns_response(|message| message.metadata.message_type = MessageType::Query),
17,
"example.com"
)
.is_err()
);
assert!(
dns_answers(
&dns_response(|message| message.metadata.op_code = OpCode::Update),
17,
"example.com"
)
.is_err()
);
assert!(
dns_answers(
&dns_response(|message| {
message.metadata.response_code = ResponseCode::ServFail;
}),
17,
"example.com"
)
.is_err()
);
assert!(
dns_answers(
&dns_response(|message| message.metadata.truncation = true),
17,
"example.com"
)
.is_err()
);
assert!(
dns_answers(
&dns_response(|message| {
message.queries[0].set_name(Name::from_ascii("other.example").unwrap());
}),
17,
"example.com"
)
.is_err()
);
assert!(
dns_answers(
&dns_response(|message| {
message.queries[0].set_query_type(RecordType::AAAA);
}),
17,
"example.com"
)
.is_err()
);
assert!(
dns_answers(
&dns_response(|message| {
message.queries[0].set_query_class(DNSClass::CH);
}),
17,
"example.com"
)
.is_err()
);
}
#[test]
fn dns_codec_accepts_terminal_a_after_cname_and_refuses_malformed_packets() {
let canonical = Name::from_ascii("canonical.example").unwrap();
let response = dns_response(|message| {
message.answers.clear();
message.add_answer(Record::from_rdata(
Name::from_ascii("example.com").unwrap(),
60,
RData::CNAME(CNAME(canonical.clone())),
));
message.add_answer(Record::from_rdata(
canonical,
60,
RData::A(A(Ipv4Addr::new(192, 0, 2, 2))),
));
});
assert_eq!(
dns_answers(&response, 17, "example.com").unwrap(),
vec![IpAddress::V4([192, 0, 2, 2])]
);
let mut invalid_pointer = dns_response(|_| {});
let answer_offset = dns_query(17, "example.com").unwrap().len();
invalid_pointer[answer_offset] = 0xff;
invalid_pointer[answer_offset + 1] = 0xff;
assert!(dns_answers(&invalid_pointer, 17, "example.com").is_err());
assert!(dns_answers(&vec![0; MAX_DNS_PACKET + 1], 17, "example.com").is_err());
}
#[test]
fn probe_refuses_a_changed_enforcement_namespace_before_network_io() {
let mut namespace = namespace_identity(Path::new("/proc/self/ns/net")).unwrap();
@ -732,6 +826,21 @@ mod tests {
assert_eq!(u32::from_le_bytes(mask.to_be_bytes()), 0x0000_00ff);
}
fn dns_response(change: impl FnOnce(&mut Message)) -> Vec<u8> {
let name = Name::from_ascii("example.com").unwrap();
let mut response = Message::new(17, MessageType::Response, OpCode::Query);
response.metadata.recursion_desired = true;
response.metadata.recursion_available = true;
response.add_query(Query::query(name.clone(), RecordType::A));
response.add_answer(Record::from_rdata(
name,
60,
RData::A(A(Ipv4Addr::new(93, 184, 216, 34))),
));
change(&mut response);
response.to_vec().unwrap()
}
fn policy() -> CanonicalDerivedNetworkPolicy {
derive_policy(DerivedNetworkPolicyFields {
address_families: vec![