// MoonDNS - Pure MoonBit Authoritative DNS Server
// Copyright 2026 chgttyyr
// Licensed under the Apache-2.0 License.
///|
/// Result of an authoritative query lookup in a Zone database
pub(all) enum LookupResult {
ExactMatch(Array[@types.Record])
CNameMatch(@types.Record, String)
WildcardMatch(Array[@types.Record])
NoData(@types.Record)
NxDomain(@types.Record)
NotAuthoritative
} derive(Debug)
///|
/// Zone database holding records for an authoritative apex origin
pub(all) struct Zone {
mut origin : String
mut default_ttl : UInt
mut soa : @types.Record?
records : Array[@types.Record]
} derive(Debug)
///|
/// Create an empty Zone for a given origin domain
pub fn Zone::new(origin : String, default_ttl : UInt) -> Zone {
{
origin: @domain.normalize_fqdn(origin),
default_ttl,
soa: None,
records: [],
}
}
///|
/// Add a Resource Record into the zone
pub fn Zone::add_record(self : Zone, record : @types.Record) -> Unit {
let norm_rec = @types.Record::new(
@domain.normalize_fqdn(record.name),
record.rtype,
record.rclass,
record.ttl,
record.rdata,
)
if norm_rec.rtype == @types.RType::SOA {
self.soa = Some(norm_rec)
}
self.records.push(norm_rec)
}
///|
/// Return the number of records stored in this zone.
pub fn Zone::record_count(self : Zone) -> Int {
self.records.length()
}
///|
/// Get the authoritative SOA record or create a fallback default SOA
pub fn Zone::get_soa_or_fallback(self : Zone) -> @types.Record {
match self.soa {
Some(rec) => rec
None => {
let mname = "ns1." + self.origin
let rname = "hostmaster." + self.origin
let rdata = @types.RData::SOA(mname, rname, 1, 7200, 3600, 1209600, 3600)
@types.Record::new(
self.origin,
@types.RType::SOA,
@types.RClass::IN,
3600,
rdata,
)
}
}
}
///|
/// Lookup records for a given domain and QTYPE strictly implementing RFC 1034 / RFC 1035 rules
pub fn Zone::lookup(
self : Zone,
query_name : String,
qtype : @types.RType,
) -> LookupResult {
let qname = @domain.normalize_fqdn(query_name)
// Step 1: Verify authority boundary
if !@domain.is_subdomain_of(qname, self.origin) {
return NotAuthoritative
}
let soa_record = self.get_soa_or_fallback()
// Step 2: Check for CNAME records at this exact node (CNAME takes precedence unless QTYPE=CNAME)
if qtype != @types.RType::CNAME {
for i = 0; i < self.records.length(); i = i + 1 {
let rec = self.records[i]
if @domain.domain_equal(rec.name, qname) &&
rec.rtype == @types.RType::CNAME {
match rec.rdata {
@types.RData::CNAME(target) => return CNameMatch(rec, target)
_ => ()
}
}
}
}
// Step 3: Exact name matching
let matched_records : Array[@types.Record] = []
let mut name_exists = false
for i = 0; i < self.records.length(); i = i + 1 {
let rec = self.records[i]
if @domain.domain_equal(rec.name, qname) {
name_exists = true
if qtype == @types.RType::ANY || rec.rtype == qtype {
matched_records.push(rec)
}
}
}
if matched_records.length() > 0 {
return ExactMatch(matched_records)
}
// Step 4: Name exists but no matching QTYPE -> NODATA (RFC 1035 Section 4.3.4)
if name_exists {
return NoData(soa_record)
}
// Step 5: Wildcard synthesis (*.parent, RFC 1034 Section 4.3.3)
let q_labels = @domain.split_labels(qname)
let origin_labels = @domain.split_labels(self.origin)
let depth = q_labels.length() - origin_labels.length()
if depth > 0 {
for i = 1; i <= depth; i = i + 1 {
let wildcard_buf = StringBuilder()
wildcard_buf.write_string("*.")
for j = i; j < q_labels.length(); j = j + 1 {
wildcard_buf.write_string(q_labels[j])
wildcard_buf.write_char('.')
}
let wildcard_pattern = wildcard_buf.to_string()
let wildcard_matches : Array[@types.Record] = []
for k = 0; k < self.records.length(); k = k + 1 {
let rec = self.records[k]
if @domain.domain_equal(rec.name, wildcard_pattern) {
if qtype == @types.RType::ANY || rec.rtype == qtype {
// Synthesize record with query_name as owner
let synth_rec = @types.Record::new(
qname,
rec.rtype,
rec.rclass,
rec.ttl,
rec.rdata,
)
wildcard_matches.push(synth_rec)
}
}
}
if wildcard_matches.length() > 0 {
return WildcardMatch(wildcard_matches)
}
}
}
// Step 6: Neither exact name nor wildcard exists -> NXDOMAIN
NxDomain(soa_record)
}