Skip to content

Commit

Permalink
Add Domain to Question, parse_question function
Browse files Browse the repository at this point in the history
  • Loading branch information
matei-radu committed Jul 27, 2024
1 parent 6977f9d commit 5e9b403
Show file tree
Hide file tree
Showing 5 changed files with 171 additions and 3 deletions.
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion lib/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "dns_lib"
version = "0.18.0"
version = "0.19.0"
description = "An implementation of the DNS protocol from scratch based on the many DNS RFCs."

rust-version.workspace = true
Expand Down
16 changes: 16 additions & 0 deletions lib/src/domain/name.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,22 @@ pub struct Domain {
labels: Vec<String>,
}

impl Domain {
pub fn new() -> Self {
Self { labels: Vec::new() }
}

pub fn add_label(&mut self, bytes: &[u8]) -> Result<(), TryFromError> {
match parse_label(bytes) {
Ok(label) => {
self.labels.push(label);
Ok(())
}
Err(e) => Err(e),
}
}
}

impl TryFrom<String> for Domain {
type Error = TryFromError;

Expand Down
2 changes: 1 addition & 1 deletion lib/src/message/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,4 @@ mod question;

pub use header::{Header, OpCode, RCode, Z};
pub use message::Message;
pub use question::{KnownQType, QType, Question};
pub use question::{parse_question, KnownQType, QType, Question};
152 changes: 152 additions & 0 deletions lib/src/message/question.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,101 @@
// See the License for the specific language governing permissions and
// limitations under the License.

use crate::domain::{error, Domain};

#[derive(Debug, PartialEq)]
pub struct Question {
pub q_name: Domain,
pub q_type: QType,
pub q_class: QClass,
}

#[derive(Debug, PartialEq)]
pub struct QuestionParseData {
pub question: Question,
pub bytes_read: usize,
}

pub fn parse_question(
message_bytes: &[u8],
offset: usize,
) -> Result<QuestionParseData, error::TryFromError> {
let mut question_pos = offset;
let mut using_compression = false;
let mut bytes_read: usize = 0;

let mut q_name = Domain::new();
let mut pos = question_pos;
loop {
// Are the current and next bytes a pointer?
if (message_bytes[pos] & 0b11000000) == 0b11000000 {
if !using_compression {
bytes_read += 2;
question_pos = pos + 2;
using_compression = true;
}
let pointer_first_6_bits = (u16::from(message_bytes[pos]) & 0b00111111) << 8;
let pointer_last_8_bits = u16::from(message_bytes[pos + 1]);
let pointer = pointer_first_6_bits | pointer_last_8_bits;
pos = pointer as usize;
continue;
}

let label_length = message_bytes[pos] as usize;

// zero length indicates end of QNAME portion.
if label_length == 0 {
pos += 1;
if !using_compression {
bytes_read += 1;
question_pos = pos;
}
break;
}

// Go into first byte of the label
pos += 1;
if !using_compression {
bytes_read += 1;
}
let label_slice = &message_bytes[pos..pos + label_length];

// Attempt to parse label, add to domain.
match q_name.add_label(label_slice) {
Ok(()) => {
pos += label_length;
if !using_compression {
bytes_read += label_length;
}
}
Err(e) => return Err(e),
}
}

let q_type_raw =
u16::from_be_bytes([message_bytes[question_pos], message_bytes[question_pos + 1]]);
let q_type = QType::new(q_type_raw);

question_pos += 2;
bytes_read += 2;
let q_class_raw =
u16::from_be_bytes([message_bytes[question_pos], message_bytes[question_pos + 1]]);
let q_class = QClass::new(q_class_raw);

bytes_read += 2;

let question = Question {
q_name,
q_type,
q_class,
};

Ok(QuestionParseData {
question,
bytes_read,
})
}

#[derive(Debug)]
pub struct QType {
pub value: u16,
Expand Down Expand Up @@ -294,4 +383,67 @@ mod tests {
assert_eq!(QClass::new(input), input);
assert_eq!(input, QClass::new(input));
}

#[rstest]
#[case(
// example.com, A, IN, 17 bytes
&[7, b'e', b'x', b'a', b'm', b'p', b'l', b'e', 3, b'c', b'o', b'm', 0, 0, 1, 0, 1],
0,
QuestionParseData{
question: Question{
q_name: Domain::try_from("example.com".to_string()).unwrap(),
q_type: QType::from(KnownQType::A),
q_class: QClass::from(KnownQClass::IN),
},
bytes_read: 17,
}
)]
#[case(
// example.com, A, IN, 17 bytes, but using offset
&[0, 0, 0, 7, b'e', b'x', b'a', b'm', b'p', b'l', b'e', 3, b'c', b'o', b'm', 0, 0, 1, 0, 1],
3,
QuestionParseData{
question: Question{
q_name: Domain::try_from("example.com".to_string()).unwrap(),
q_type: QType::from(KnownQType::A),
q_class: QClass::from(KnownQClass::IN),
},
bytes_read: 17,
}
)]
#[case(
// test.example.com, A, IN, 11 bytes, but using compression for example.com
&[0, 0, 0, 7, b'e', b'x', b'a', b'm', b'p', b'l', b'e', 3, b'c', b'o', b'm', 0, 4, b't', b'e', b's', b't', 0b11000000, 3, 0, 1, 0, 1],
16,
QuestionParseData{
question: Question{
q_name: Domain::try_from("test.example.com".to_string()).unwrap(),
q_type: QType::from(KnownQType::A),
q_class: QClass::from(KnownQClass::IN),
},
bytes_read: 11,
}
)]
#[case(
// test.example.com, A, IN, 11 bytes, but using nested compression for example.com
&[3, b'c', b'o', b'm', 0, 7, b'e', b'x', b'a', b'm', b'p', b'l', b'e', 0b11000000, 0, 4, b't', b'e', b's', b't', 0b11000000, 5, 0, 1, 0, 1],
15,
QuestionParseData{
question: Question{
q_name: Domain::try_from("test.example.com".to_string()).unwrap(),
q_type: QType::from(KnownQType::A),
q_class: QClass::from(KnownQClass::IN),
},
bytes_read: 11,
}
)]
fn parse_question_works(
#[case] input: &[u8],
#[case] offset: usize,
#[case] expected: QuestionParseData,
) {
let result = parse_question(input, offset);
assert!(result.is_ok());
assert_eq!(result.unwrap(), expected);
}
}

0 comments on commit 5e9b403

Please sign in to comment.