Skip to content

Commit

Permalink
dns: initial opt record support
Browse files Browse the repository at this point in the history
This change introduces the ability to parse OPT records but
does not yet add them to requests or uses them when they are
part of a response. A future PR will start making use of OPT
records.

This change also does some unrelated cleanup, removing `--dns-local`
flags from each of the binaries in favor of picking this value
automatically. Additionally, the `--timeout-secs` option is removed
from the `dns` binary since it has been unused since resolv.conf
support was added.

Part of #107
  • Loading branch information
56quarters committed Jun 15, 2024
1 parent 7842f99 commit ee4943b
Show file tree
Hide file tree
Showing 10 changed files with 319 additions and 84 deletions.
14 changes: 6 additions & 8 deletions mtop-client/src/dns/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,15 @@ const DEFAULT_MESSAGE_BUFFER: usize = 512;

#[derive(Debug)]
pub struct DnsClient {
local: SocketAddr,
config: ResolvConf,
server: AtomicUsize,
}

impl DnsClient {
/// Create a new DnsClient that will use a local address to open UDP or TCP
/// connections and behavior based on a resolv.conf configuration file.
pub fn new(local: SocketAddr, config: ResolvConf) -> Self {
/// Create a new DnsClient that will resolve names using UDP or TCP connections
/// and behavior based on a resolv.conf configuration file.
pub fn new(config: ResolvConf) -> Self {
Self {
local,
config,
server: AtomicUsize::new(0),
}
Expand Down Expand Up @@ -84,7 +82,8 @@ impl DnsClient {
}

async fn udp_client(&self, server: SocketAddr) -> Result<UdpClient<UdpSocket>, MtopError> {
let sock = UdpSocket::bind(&self.local).await?;
let local = if server.is_ipv4() { "0.0.0.0:0" } else { "[::]:0" };
let sock = UdpSocket::bind(local).await?;
sock.connect(server).await?;
Ok(UdpClient::new(sock, DEFAULT_MESSAGE_BUFFER))
}
Expand All @@ -109,7 +108,6 @@ impl DnsClient {
impl Clone for DnsClient {
fn clone(&self) -> Self {
Self {
local: self.local,
config: self.config.clone(),
server: AtomicUsize::new(0),
}
Expand Down Expand Up @@ -160,7 +158,7 @@ where
let res = Message::read_network_bytes(&mut cur)?;
if res.id() != msg.id() {
Err(MtopError::runtime(format!(
"unexpected DNS MessageId. expected {}, got {}",
"unexpected DNS MessageId; expected {}, got {}",
msg.id(),
res.id()
)))
Expand Down
11 changes: 9 additions & 2 deletions mtop-client/src/dns/message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,7 @@ pub enum ResponseCode {
NxRrSet = 8,
NotAuth = 9,
NotZone = 10,
BadVersion = 16,
}

impl Default for ResponseCode {
Expand All @@ -385,6 +386,7 @@ impl TryFrom<u16> for ResponseCode {
8 => Ok(ResponseCode::NxRrSet),
9 => Ok(ResponseCode::NotAuth),
10 => Ok(ResponseCode::NotZone),
16 => Ok(ResponseCode::BadVersion),
_ => Err(MtopError::runtime(format!(
"invalid or unsupported response code {}",
value
Expand Down Expand Up @@ -524,9 +526,14 @@ impl Record {
where
T: WriteBytesExt,
{
// It shouldn't be possible to overflow u16 so if we do, that's a bug.
// It shouldn't be possible for rdata to overflow u16 so if we do, that's a bug.
let size = self.rdata.size();
assert!(size <= usize::from(u16::MAX), "rdata length exceeds {} bytes", u16::MAX);
assert!(
size <= usize::from(u16::MAX),
"rdata length of {} bytes exceeds max of {} bytes",
size,
u16::MAX
);

self.name.write_network_bytes(&mut buf)?;
buf.write_u16::<NetworkEndian>(self.rtype.into())?;
Expand Down
4 changes: 2 additions & 2 deletions mtop-client/src/dns/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ pub use crate::dns::core::{RecordClass, RecordType};
pub use crate::dns::message::{Flags, Message, MessageId, Operation, Question, Record, ResponseCode};
pub use crate::dns::name::Name;
pub use crate::dns::rdata::{
RecordData, RecordDataA, RecordDataAAAA, RecordDataCNAME, RecordDataNS, RecordDataSOA, RecordDataSRV,
RecordDataTXT, RecordDataUnknown,
RecordData, RecordDataA, RecordDataAAAA, RecordDataCNAME, RecordDataNS, RecordDataOpt, RecordDataSOA,
RecordDataSRV, RecordDataTXT, RecordDataUnknown,
};
pub use crate::dns::resolv::{config, ResolvConf, ResolvConfOptions};
94 changes: 70 additions & 24 deletions mtop-client/src/dns/name.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,18 +82,26 @@ impl Name {
// offsets and read the labels for the name into `parts`. After resolving
// all pointers and reading labels, reset the stream back to immediately
// after the pointer.
if Self::is_offset(len) {
if Self::is_compressed_label(len) {
let offset = Self::get_offset(len, buf.read_u8()?);
let current = buf.stream_position()?;
Self::read_offset_into(&mut buf, offset, &mut parts)?;
buf.seek(SeekFrom::Start(current))?;
break;
}

// If the length is a length, read the next label (segment) of the name breaking
// the loop once we read the "root" label (`.`) signified by a length of 0.
if Self::read_label_into(&mut buf, len, &mut parts)? {
break;
} else if Self::is_standard_label(len) {
// If the length is a length, read the next label (segment) of the name
// breaking the loop once we read the "root" label (`.`) signified by a
// length of 0.
if Self::read_label_into(&mut buf, len, &mut parts)? {
break;
}
} else {
// Binary labels are deprecated (RFC 6891) and there are (currently) no other
// types of labels that we should expect. Return an error to make this obvious.
return Err(MtopError::runtime(format!(
"unsupported Name label type found: {}",
len
)));
}
}

Expand Down Expand Up @@ -121,22 +129,25 @@ impl Name {
}

let len = buf.read_u8()?;
if Self::is_offset(len) {
if Self::is_compressed_label(len) {
// If this length is actually a pointer to another name or label within
// the message, seek there to read it on the next iteration. We don't
// bother keeping track of where to seek back to after resolving it because
// this is unnecessary since we're already resolving a pointer if this
// method is being called from `read_ne_bytes`.
// method is being called from `read_network_bytes`.
let offset = Self::get_offset(len, buf.read_u8()?);
buf.seek(SeekFrom::Start(offset))?;
pointers += 1;
continue;
}

// If the length is a length, read the next label (segment) of the name
// returning once we read the "root" label (`.`) signified by a length of 0.
if Self::read_label_into(&mut buf, len, out)? {
return Ok(());
} else if Self::is_standard_label(len) {
// If the length is a length, read the next label (segment) of the name
// returning once we read the "root" label (`.`) signified by a length of 0.
if Self::read_label_into(&mut buf, len, out)? {
return Ok(());
}
} else {
// Binary labels are deprecated (RFC 6891) and there are (currently) no other
// types of labels that we should expect. Return an error to make this obvious.
return Err(MtopError::runtime(format!("unsupported Name label type: {}", len)));
}
}
}
Expand Down Expand Up @@ -182,7 +193,11 @@ impl Name {
Ok(false)
}

fn is_offset(len: u8) -> bool {
fn is_standard_label(len: u8) -> bool {
len & 0b1100_0000 == 0
}

fn is_compressed_label(len: u8) -> bool {
// The top two bits of the length byte of a name label (section) are used
// to indicate the name is actually an offset in the DNS message to a previous
// name to avoid duplicating the same names over and over.
Expand Down Expand Up @@ -212,7 +227,7 @@ impl FromStr for Name {

if s.len() > Self::MAX_LENGTH {
return Err(MtopError::runtime(format!(
"Names are limited to {} bytes max: {}",
"Name too long; max {} bytes, got {}",
Self::MAX_LENGTH,
s
)));
Expand All @@ -224,7 +239,7 @@ impl FromStr for Name {
let len = label.len();
if len > Self::MAX_LABEL_LENGTH {
return Err(MtopError::runtime(format!(
"Name labels are limited to {} bytes max: {}",
"Name label too long; max {} bytes, got {}",
Self::MAX_LABEL_LENGTH,
label
)));
Expand All @@ -233,17 +248,17 @@ impl FromStr for Name {
for (i, c) in label.char_indices() {
if i == 0 && !c.is_ascii_alphanumeric() && c != '_' {
return Err(MtopError::runtime(format!(
"label must begin with ASCII letter, number, or underscore: {}",
"Name label must begin with ASCII letter, number, or underscore; got {}",
label
)));
} else if i == len - 1 && !c.is_ascii_alphanumeric() {
return Err(MtopError::runtime(format!(
"label must end with ASCII letter or number: {}",
"Name label must end with ASCII letter or number; got {}",
label
)));
} else if !c.is_ascii_alphanumeric() && c != '-' && c != '_' {
return Err(MtopError::runtime(format!(
"label must be ASCII letter, number, hyphen, or underscore: {}",
"Name label must be ASCII letter, number, hyphen, or underscore; got {}",
label
)));
}
Expand Down Expand Up @@ -350,7 +365,7 @@ mod test {
}

#[test]
fn test_to_fqdn_not_fqdn() {
fn test_name_to_fqdn_not_fqdn() {
let name = Name::from_str("example.com").unwrap();
assert!(!name.is_fqdn());

Expand All @@ -359,7 +374,7 @@ mod test {
}

#[test]
fn test_to_fqdn_already_fqdn() {
fn test_name_to_fqdn_already_fqdn() {
let name = Name::from_str("example.com.").unwrap();
assert!(name.is_fqdn());

Expand Down Expand Up @@ -483,6 +498,37 @@ mod test {
assert!(name.is_fqdn());
}

#[rustfmt::skip]
#[test]
fn test_name_read_network_bytes_bad_label_type() {
let cur = Cursor::new(vec![
64, // length, deprecated binary labels from RFC 2673
0, // count of binary labels
]);

let res = Name::read_network_bytes(cur);
assert!(res.is_err());
}

#[rustfmt::skip]
#[test]
fn test_name_read_network_bytes_bad_label_type_after_single_pointer() {
let mut cur = Cursor::new(vec![
7, // length
101, 120, 97, 109, 112, 108, 101, // "example"
64, // length, deprecated binary labels from RFC 2673
0, // count of binary labels
3, // length
119, 119, 119, // "www"
192, 0, // pointer to offset 0
]);

cur.set_position(10);

let res = Name::read_network_bytes(cur);
assert!(res.is_err());
}

#[rustfmt::skip]
#[test]
fn test_name_read_network_bytes_single_pointer() {
Expand Down
Loading

0 comments on commit ee4943b

Please sign in to comment.