feat(rust): add Rust-based DNS server backend with IPC management and TypeScript bridge

This commit is contained in:
2026-02-11 11:24:10 +00:00
parent abbb971d6a
commit 60371e1ad5
37 changed files with 4509 additions and 1272 deletions

View File

@@ -0,0 +1,6 @@
[package]
name = "rustdns-protocol"
version = "0.1.0"
edition = "2021"
[dependencies]

View File

@@ -0,0 +1,3 @@
pub mod types;
pub mod name;
pub mod packet;

View File

@@ -0,0 +1,108 @@
/// Encode a domain name into DNS wire format.
/// e.g. "example.com" -> [7, 'e','x','a','m','p','l','e', 3, 'c','o','m', 0]
pub fn encode_name(name: &str) -> Vec<u8> {
let mut buf = Vec::new();
let trimmed = name.strip_suffix('.').unwrap_or(name);
if trimmed.is_empty() {
buf.push(0);
return buf;
}
for label in trimmed.split('.') {
let len = label.len();
if len > 63 {
// Truncate to 63 per DNS spec
buf.push(63);
buf.extend_from_slice(&label.as_bytes()[..63]);
} else {
buf.push(len as u8);
buf.extend_from_slice(label.as_bytes());
}
}
buf.push(0); // root label
buf
}
/// Decode a domain name from DNS wire format at the given offset.
/// Returns (name, bytes_consumed).
/// Handles compression pointers (0xC0 prefix).
pub fn decode_name(data: &[u8], offset: usize) -> Result<(String, usize), &'static str> {
let mut labels: Vec<String> = Vec::new();
let mut pos = offset;
let mut bytes_consumed = 0;
let mut jumped = false;
loop {
if pos >= data.len() {
return Err("unexpected end of data in name");
}
let len = data[pos] as usize;
if len == 0 {
// Root label
if !jumped {
bytes_consumed = pos - offset + 1;
}
break;
}
// Check for compression pointer
if len & 0xC0 == 0xC0 {
if pos + 1 >= data.len() {
return Err("unexpected end of data in compression pointer");
}
let pointer = ((len & 0x3F) << 8) | (data[pos + 1] as usize);
if !jumped {
bytes_consumed = pos - offset + 2;
jumped = true;
}
pos = pointer;
continue;
}
// Regular label
pos += 1;
if pos + len > data.len() {
return Err("label extends beyond data");
}
let label = std::str::from_utf8(&data[pos..pos + len]).map_err(|_| "invalid UTF-8 in label")?;
labels.push(label.to_string());
pos += len;
}
if bytes_consumed == 0 && !jumped {
bytes_consumed = 1; // just the root label
}
Ok((labels.join("."), bytes_consumed))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_encode_decode_roundtrip() {
let names = vec!["example.com", "sub.domain.example.com", "localhost", "a.b.c.d.e"];
for name in names {
let encoded = encode_name(name);
let (decoded, consumed) = decode_name(&encoded, 0).unwrap();
assert_eq!(decoded, name);
assert_eq!(consumed, encoded.len());
}
}
#[test]
fn test_encode_trailing_dot() {
let a = encode_name("example.com.");
let b = encode_name("example.com");
assert_eq!(a, b);
}
#[test]
fn test_root_name() {
let encoded = encode_name("");
assert_eq!(encoded, vec![0]);
let (decoded, _) = decode_name(&encoded, 0).unwrap();
assert_eq!(decoded, "");
}
}

View File

@@ -0,0 +1,442 @@
use crate::name::{decode_name, encode_name};
use crate::types::{QClass, QType, FLAG_QR, FLAG_AA, FLAG_RD, FLAG_RA, EDNS_DO_BIT};
/// A parsed DNS question.
#[derive(Debug, Clone)]
pub struct DnsQuestion {
pub name: String,
pub qtype: QType,
pub qclass: QClass,
}
/// A parsed DNS resource record.
#[derive(Debug, Clone)]
pub struct DnsRecord {
pub name: String,
pub rtype: QType,
pub rclass: QClass,
pub ttl: u32,
pub rdata: Vec<u8>,
// For OPT records, the flags are stored in the TTL field position
pub opt_flags: Option<u16>,
}
/// A complete DNS packet (parsed).
#[derive(Debug, Clone)]
pub struct DnsPacket {
pub id: u16,
pub flags: u16,
pub questions: Vec<DnsQuestion>,
pub answers: Vec<DnsRecord>,
pub authorities: Vec<DnsRecord>,
pub additionals: Vec<DnsRecord>,
}
impl DnsPacket {
/// Create a new empty query packet.
pub fn new_query(id: u16) -> Self {
DnsPacket {
id,
flags: 0,
questions: Vec::new(),
answers: Vec::new(),
authorities: Vec::new(),
additionals: Vec::new(),
}
}
/// Create a response packet for a given request.
pub fn new_response(request: &DnsPacket) -> Self {
let mut flags = FLAG_QR | FLAG_AA | FLAG_RA;
if request.flags & FLAG_RD != 0 {
flags |= FLAG_RD;
}
DnsPacket {
id: request.id,
flags,
questions: request.questions.clone(),
answers: Vec::new(),
authorities: Vec::new(),
additionals: Vec::new(),
}
}
/// Check if DNSSEC (DO bit) is requested in the OPT record.
pub fn is_dnssec_requested(&self) -> bool {
for additional in &self.additionals {
if additional.rtype == QType::OPT {
if let Some(flags) = additional.opt_flags {
if flags & EDNS_DO_BIT != 0 {
return true;
}
}
}
}
false
}
/// Parse a DNS packet from wire format bytes.
pub fn parse(data: &[u8]) -> Result<Self, String> {
if data.len() < 12 {
return Err("packet too short for DNS header".into());
}
let id = u16::from_be_bytes([data[0], data[1]]);
let flags = u16::from_be_bytes([data[2], data[3]]);
let qdcount = u16::from_be_bytes([data[4], data[5]]) as usize;
let ancount = u16::from_be_bytes([data[6], data[7]]) as usize;
let nscount = u16::from_be_bytes([data[8], data[9]]) as usize;
let arcount = u16::from_be_bytes([data[10], data[11]]) as usize;
let mut offset = 12;
// Parse questions
let mut questions = Vec::with_capacity(qdcount);
for _ in 0..qdcount {
let (name, consumed) = decode_name(data, offset).map_err(|e| e.to_string())?;
offset += consumed;
if offset + 4 > data.len() {
return Err("packet too short for question fields".into());
}
let qtype = QType::from_u16(u16::from_be_bytes([data[offset], data[offset + 1]]));
let qclass = QClass::from_u16(u16::from_be_bytes([data[offset + 2], data[offset + 3]]));
offset += 4;
questions.push(DnsQuestion { name, qtype, qclass });
}
// Parse resource records
fn parse_records(data: &[u8], offset: &mut usize, count: usize) -> Result<Vec<DnsRecord>, String> {
let mut records = Vec::with_capacity(count);
for _ in 0..count {
let (name, consumed) = decode_name(data, *offset).map_err(|e| e.to_string())?;
*offset += consumed;
if *offset + 10 > data.len() {
return Err("packet too short for RR fields".into());
}
let rtype = QType::from_u16(u16::from_be_bytes([data[*offset], data[*offset + 1]]));
let rclass_or_payload = u16::from_be_bytes([data[*offset + 2], data[*offset + 3]]);
let ttl_bytes = u32::from_be_bytes([data[*offset + 4], data[*offset + 5], data[*offset + 6], data[*offset + 7]]);
let rdlength = u16::from_be_bytes([data[*offset + 8], data[*offset + 9]]) as usize;
*offset += 10;
if *offset + rdlength > data.len() {
return Err("packet too short for RDATA".into());
}
let rdata = data[*offset..*offset + rdlength].to_vec();
*offset += rdlength;
// For OPT records, extract flags from the TTL position
let (rclass, ttl, opt_flags) = if rtype == QType::OPT {
// OPT: class = UDP payload size, TTL upper 16 = extended RCODE + version, lower 16 = flags
let flags = (ttl_bytes & 0xFFFF) as u16;
(QClass::from_u16(rclass_or_payload), 0, Some(flags))
} else {
(QClass::from_u16(rclass_or_payload), ttl_bytes, None)
};
records.push(DnsRecord {
name,
rtype,
rclass,
ttl,
rdata,
opt_flags,
});
}
Ok(records)
}
let answers = parse_records(data, &mut offset, ancount)?;
let authorities = parse_records(data, &mut offset, nscount)?;
let additionals = parse_records(data, &mut offset, arcount)?;
Ok(DnsPacket {
id,
flags,
questions,
answers,
authorities,
additionals,
})
}
/// Encode this DNS packet to wire format bytes.
pub fn encode(&self) -> Vec<u8> {
let mut buf = Vec::with_capacity(512);
// Header
buf.extend_from_slice(&self.id.to_be_bytes());
buf.extend_from_slice(&self.flags.to_be_bytes());
buf.extend_from_slice(&(self.questions.len() as u16).to_be_bytes());
buf.extend_from_slice(&(self.answers.len() as u16).to_be_bytes());
buf.extend_from_slice(&(self.authorities.len() as u16).to_be_bytes());
buf.extend_from_slice(&(self.additionals.len() as u16).to_be_bytes());
// Questions
for q in &self.questions {
buf.extend_from_slice(&encode_name(&q.name));
buf.extend_from_slice(&q.qtype.to_u16().to_be_bytes());
buf.extend_from_slice(&q.qclass.to_u16().to_be_bytes());
}
// Resource records
fn encode_records(buf: &mut Vec<u8>, records: &[DnsRecord]) {
for rr in records {
buf.extend_from_slice(&encode_name(&rr.name));
buf.extend_from_slice(&rr.rtype.to_u16().to_be_bytes());
if rr.rtype == QType::OPT {
// OPT: class = UDP payload size (4096), TTL = ext rcode + flags
buf.extend_from_slice(&rr.rclass.to_u16().to_be_bytes());
let flags = rr.opt_flags.unwrap_or(0) as u32;
buf.extend_from_slice(&flags.to_be_bytes());
} else {
buf.extend_from_slice(&rr.rclass.to_u16().to_be_bytes());
buf.extend_from_slice(&rr.ttl.to_be_bytes());
}
buf.extend_from_slice(&(rr.rdata.len() as u16).to_be_bytes());
buf.extend_from_slice(&rr.rdata);
}
}
encode_records(&mut buf, &self.answers);
encode_records(&mut buf, &self.authorities);
encode_records(&mut buf, &self.additionals);
buf
}
}
// ── RDATA encoding helpers ─────────────────────────────────────────
/// Encode an A record (IPv4 address string -> 4 bytes).
pub fn encode_a(ip: &str) -> Vec<u8> {
ip.split('.')
.filter_map(|s| s.parse::<u8>().ok())
.collect()
}
/// Encode an AAAA record (IPv6 address string -> 16 bytes).
pub fn encode_aaaa(ip: &str) -> Vec<u8> {
// Handle :: expansion
let expanded = expand_ipv6(ip);
expanded
.split(':')
.flat_map(|seg| {
let val = u16::from_str_radix(seg, 16).unwrap_or(0);
val.to_be_bytes().to_vec()
})
.collect()
}
fn expand_ipv6(ip: &str) -> String {
if !ip.contains("::") {
return ip.to_string();
}
let parts: Vec<&str> = ip.split("::").collect();
let left: Vec<&str> = if parts[0].is_empty() {
vec![]
} else {
parts[0].split(':').collect()
};
let right: Vec<&str> = if parts.len() > 1 && !parts[1].is_empty() {
parts[1].split(':').collect()
} else {
vec![]
};
let fill_count = 8 - left.len() - right.len();
let mut result: Vec<String> = left.iter().map(|s| s.to_string()).collect();
for _ in 0..fill_count {
result.push("0".to_string());
}
result.extend(right.iter().map(|s| s.to_string()));
result.join(":")
}
/// Encode a TXT record (array of strings -> length-prefixed chunks).
pub fn encode_txt(strings: &[String]) -> Vec<u8> {
let mut buf = Vec::new();
for s in strings {
let bytes = s.as_bytes();
// TXT strings must be <= 255 bytes each
let len = bytes.len().min(255);
buf.push(len as u8);
buf.extend_from_slice(&bytes[..len]);
}
buf
}
/// Encode a domain name for use in RDATA (NS, CNAME, PTR, etc.).
pub fn encode_name_rdata(name: &str) -> Vec<u8> {
encode_name(name)
}
/// Encode a SOA record RDATA.
pub fn encode_soa(mname: &str, rname: &str, serial: u32, refresh: u32, retry: u32, expire: u32, minimum: u32) -> Vec<u8> {
let mut buf = Vec::new();
buf.extend_from_slice(&encode_name(mname));
buf.extend_from_slice(&encode_name(rname));
buf.extend_from_slice(&serial.to_be_bytes());
buf.extend_from_slice(&refresh.to_be_bytes());
buf.extend_from_slice(&retry.to_be_bytes());
buf.extend_from_slice(&expire.to_be_bytes());
buf.extend_from_slice(&minimum.to_be_bytes());
buf
}
/// Encode an MX record RDATA.
pub fn encode_mx(preference: u16, exchange: &str) -> Vec<u8> {
let mut buf = Vec::new();
buf.extend_from_slice(&preference.to_be_bytes());
buf.extend_from_slice(&encode_name(exchange));
buf
}
/// Encode a SRV record RDATA.
pub fn encode_srv(priority: u16, weight: u16, port: u16, target: &str) -> Vec<u8> {
let mut buf = Vec::new();
buf.extend_from_slice(&priority.to_be_bytes());
buf.extend_from_slice(&weight.to_be_bytes());
buf.extend_from_slice(&port.to_be_bytes());
buf.extend_from_slice(&encode_name(target));
buf
}
/// Encode a DNSKEY record RDATA.
pub fn encode_dnskey(flags: u16, protocol: u8, algorithm: u8, public_key: &[u8]) -> Vec<u8> {
let mut buf = Vec::new();
buf.extend_from_slice(&flags.to_be_bytes());
buf.push(protocol);
buf.push(algorithm);
buf.extend_from_slice(public_key);
buf
}
/// Encode an RRSIG record RDATA.
pub fn encode_rrsig(
type_covered: u16,
algorithm: u8,
labels: u8,
original_ttl: u32,
expiration: u32,
inception: u32,
key_tag: u16,
signers_name: &str,
signature: &[u8],
) -> Vec<u8> {
let mut buf = Vec::new();
buf.extend_from_slice(&type_covered.to_be_bytes());
buf.push(algorithm);
buf.push(labels);
buf.extend_from_slice(&original_ttl.to_be_bytes());
buf.extend_from_slice(&expiration.to_be_bytes());
buf.extend_from_slice(&inception.to_be_bytes());
buf.extend_from_slice(&key_tag.to_be_bytes());
buf.extend_from_slice(&encode_name(signers_name));
buf.extend_from_slice(signature);
buf
}
/// Build a DnsRecord from high-level data.
pub fn build_record(name: &str, rtype: QType, ttl: u32, rdata: Vec<u8>) -> DnsRecord {
DnsRecord {
name: name.to_string(),
rtype,
rclass: QClass::IN,
ttl,
rdata,
opt_flags: None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_encode_roundtrip() {
// Build a simple query
let mut query = DnsPacket::new_query(0x1234);
query.flags = FLAG_RD;
query.questions.push(DnsQuestion {
name: "example.com".to_string(),
qtype: QType::A,
qclass: QClass::IN,
});
let encoded = query.encode();
let parsed = DnsPacket::parse(&encoded).unwrap();
assert_eq!(parsed.id, 0x1234);
assert_eq!(parsed.questions.len(), 1);
assert_eq!(parsed.questions[0].name, "example.com");
assert_eq!(parsed.questions[0].qtype, QType::A);
}
#[test]
fn test_response_with_answer() {
let mut query = DnsPacket::new_query(0x5678);
query.flags = FLAG_RD;
query.questions.push(DnsQuestion {
name: "test.example.com".to_string(),
qtype: QType::A,
qclass: QClass::IN,
});
let mut response = DnsPacket::new_response(&query);
response.answers.push(build_record(
"test.example.com",
QType::A,
300,
encode_a("127.0.0.1"),
));
let encoded = response.encode();
let parsed = DnsPacket::parse(&encoded).unwrap();
assert_eq!(parsed.id, 0x5678);
assert!(parsed.flags & FLAG_QR != 0); // Is a response
assert!(parsed.flags & FLAG_AA != 0); // Authoritative
assert_eq!(parsed.answers.len(), 1);
assert_eq!(parsed.answers[0].rdata, vec![127, 0, 0, 1]);
}
#[test]
fn test_encode_aaaa() {
let data = encode_aaaa("::1");
assert_eq!(data.len(), 16);
assert_eq!(data[15], 1);
assert!(data[..15].iter().all(|&b| b == 0));
}
#[test]
fn test_encode_txt() {
let data = encode_txt(&["hello".to_string(), "world".to_string()]);
assert_eq!(data[0], 5); // length of "hello"
assert_eq!(&data[1..6], b"hello");
assert_eq!(data[6], 5); // length of "world"
assert_eq!(&data[7..12], b"world");
}
#[test]
fn test_dnssec_do_bit() {
let mut query = DnsPacket::new_query(1);
query.questions.push(DnsQuestion {
name: "example.com".to_string(),
qtype: QType::A,
qclass: QClass::IN,
});
// No OPT record = no DNSSEC
assert!(!query.is_dnssec_requested());
// Add OPT with DO bit
query.additionals.push(DnsRecord {
name: ".".to_string(),
rtype: QType::OPT,
rclass: QClass::from_u16(4096), // UDP payload size
ttl: 0,
rdata: vec![],
opt_flags: Some(EDNS_DO_BIT),
});
assert!(query.is_dnssec_requested());
}
}

View File

@@ -0,0 +1,131 @@
/// DNS record types
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(u16)]
pub enum QType {
A = 1,
NS = 2,
CNAME = 5,
SOA = 6,
PTR = 12,
MX = 15,
TXT = 16,
AAAA = 28,
SRV = 33,
OPT = 41,
RRSIG = 46,
DNSKEY = 48,
Unknown(u16),
}
impl QType {
pub fn from_u16(val: u16) -> Self {
match val {
1 => QType::A,
2 => QType::NS,
5 => QType::CNAME,
6 => QType::SOA,
12 => QType::PTR,
15 => QType::MX,
16 => QType::TXT,
28 => QType::AAAA,
33 => QType::SRV,
41 => QType::OPT,
46 => QType::RRSIG,
48 => QType::DNSKEY,
v => QType::Unknown(v),
}
}
pub fn to_u16(self) -> u16 {
match self {
QType::A => 1,
QType::NS => 2,
QType::CNAME => 5,
QType::SOA => 6,
QType::PTR => 12,
QType::MX => 15,
QType::TXT => 16,
QType::AAAA => 28,
QType::SRV => 33,
QType::OPT => 41,
QType::RRSIG => 46,
QType::DNSKEY => 48,
QType::Unknown(v) => v,
}
}
pub fn from_str(s: &str) -> Self {
match s.to_uppercase().as_str() {
"A" => QType::A,
"NS" => QType::NS,
"CNAME" => QType::CNAME,
"SOA" => QType::SOA,
"PTR" => QType::PTR,
"MX" => QType::MX,
"TXT" => QType::TXT,
"AAAA" => QType::AAAA,
"SRV" => QType::SRV,
"OPT" => QType::OPT,
"RRSIG" => QType::RRSIG,
"DNSKEY" => QType::DNSKEY,
_ => QType::Unknown(0),
}
}
pub fn as_str(&self) -> &'static str {
match self {
QType::A => "A",
QType::NS => "NS",
QType::CNAME => "CNAME",
QType::SOA => "SOA",
QType::PTR => "PTR",
QType::MX => "MX",
QType::TXT => "TXT",
QType::AAAA => "AAAA",
QType::SRV => "SRV",
QType::OPT => "OPT",
QType::RRSIG => "RRSIG",
QType::DNSKEY => "DNSKEY",
QType::Unknown(_) => "UNKNOWN",
}
}
}
/// DNS record classes
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u16)]
pub enum QClass {
IN = 1,
CH = 3,
HS = 4,
Unknown(u16),
}
impl QClass {
pub fn from_u16(val: u16) -> Self {
match val {
1 => QClass::IN,
3 => QClass::CH,
4 => QClass::HS,
v => QClass::Unknown(v),
}
}
pub fn to_u16(self) -> u16 {
match self {
QClass::IN => 1,
QClass::CH => 3,
QClass::HS => 4,
QClass::Unknown(v) => v,
}
}
}
/// DNS header flags
pub const FLAG_QR: u16 = 0x8000;
pub const FLAG_AA: u16 = 0x0400;
pub const FLAG_RD: u16 = 0x0100;
pub const FLAG_RA: u16 = 0x0080;
/// OPT record DO bit (DNSSEC OK)
pub const EDNS_DO_BIT: u16 = 0x8000;