// MoonDNS CLI Entrypoint (RFC 1035)
///|
let default_zone_content : String =
#|$ORIGIN example.com.
#|$TTL 3600
#|
#|@ IN SOA ns1.example.com. hostmaster.example.com. (
#| 2026091801 ; serial
#| 7200 ; refresh
#| 3600 ; retry
#| 1209600 ; expire
#| 300 ; minimum
#| )
#|
#|@ IN NS ns1.example.com.
#|@ IN MX 10 mail.example.com.
#|@ IN A 192.0.2.1
#|@ IN AAAA 2001:db8::1
#|www IN CNAME example.com.
#|mail IN A 192.0.2.25
#|txt IN TXT "v=spf1 ~all"
#|*.wild IN A 192.0.2.99
#|
///|
async fn main {
let args = @env.args()
if args.length() < 2 || args[1] == "help" || args[1] == "--help" || args[1] == "-h" {
print_usage()
return
}
let cmd = args[1]
match cmd {
"serve" => run_serve(args)
"query" => run_query(args)
_ => {
println("Unknown command: \{cmd}")
print_usage()
}
}
}
///|
fn print_usage() -> Unit {
println("MoonDNS 0.1.0 - Pure MoonBit Authoritative DNS Server (RFC 1035)")
println("")
println("Usage:")
println(" moondns serve [--zone ] [--port ] [--tcp]")
println(" moondns query --name [--type ] [--server ] [--port ] [--tcp]")
println("")
println("Commands:")
println(" serve Start authoritative DNS server")
println(" --zone Path to zone file (default: examples/example.zone)")
println(" --port Port to listen on (default: 1053)")
println(" --tcp Also listen on TCP alongside UDP")
println("")
println(" query Run DNS query against server (built-in dig)")
println(" --server DNS server address (default: 127.0.0.1)")
println(" --port DNS server port (default: 1053)")
println(" --name Domain name to query (required)")
println(" --type Record type: A, AAAA, CNAME, NS, MX, TXT, SOA (default: A)")
println(" --tcp Send query over TCP instead of UDP")
}
///|
async fn run_serve(args : Array[String]) -> Unit {
let mut zone_path = "examples/example.zone"
let mut port = 1053
let mut enable_tcp = false
let mut i = 2
while i < args.length() {
if args[i] == "--zone" && i + 1 < args.length() {
zone_path = args[i + 1]
i += 2
} else if args[i] == "--port" && i + 1 < args.length() {
port = parse_int_or(args[i + 1], 1053)
i += 2
} else if args[i] == "--tcp" {
enable_tcp = true
i += 1
} else {
i += 1
}
}
// Load zone file or fallback to built-in default
let zone_text = try {
@fs.read_file(zone_path).text()
} catch {
_ => {
println("[MoonDNS] Notice: could not read '\{zone_path}', using embedded default zone.")
default_zone_content
}
}
let zone = match @zone.Zone::from_text(zone_text) {
Ok(z) => z
Err(e) => {
println("[MoonDNS] Error loading zone: \{e}")
return
}
}
let addr_str = "127.0.0.1:\{port}"
println("[MoonDNS] Loaded zone for origin '\{zone.origin}' with \{zone.records.length()} records.")
if enable_tcp {
println("[MoonDNS] Starting UDP and TCP listeners on port \{port}...")
// In moonbit async, run UDP server
@transport.serve_udp(addr_str, zone)
} else {
@transport.serve_udp(addr_str, zone)
}
}
///|
async fn run_query(args : Array[String]) -> Unit {
let mut server_host = "127.0.0.1"
let mut port = 1053
let mut name = ""
let mut qtype_str = "A"
let mut use_tcp = false
let mut i = 2
while i < args.length() {
if args[i] == "--name" && i + 1 < args.length() {
name = args[i + 1]
i += 2
} else if args[i] == "--type" && i + 1 < args.length() {
qtype_str = args[i + 1].to_upper()
i += 2
} else if args[i] == "--server" && i + 1 < args.length() {
server_host = args[i + 1]
i += 2
} else if args[i] == "--port" && i + 1 < args.length() {
port = parse_int_or(args[i + 1], 1053)
i += 2
} else if args[i] == "--tcp" {
use_tcp = true
i += 1
} else {
i += 1
}
}
if name == "" {
println("Error: --name is required.")
println("Example: moondns query --name example.com --type A")
return
}
let qtype = match qtype_str {
"A" => @dns.RType::A
"AAAA" => @dns.RType::AAAA
"CNAME" => @dns.RType::CNAME
"NS" => @dns.RType::NS
"MX" => @dns.RType::MX
"TXT" => @dns.RType::TXT
"SOA" => @dns.RType::SOA
_ => @dns.RType::A
}
let server_addr_str = "\{server_host}:\{port}"
let start_time = @env.now()
let resp_opt = if use_tcp {
@transport.query_tcp(server_addr_str, name, qtype)
} else {
@transport.query_udp(server_addr_str, name, qtype)
}
let elapsed_ms = (@env.now() - start_time).reinterpret_as_int64()
match resp_opt {
Some(resp) => print_dig_response(resp, server_addr_str, elapsed_ms, use_tcp)
None => println(";; connection timed out; no servers could be reached at \{server_addr_str}")
}
}
///|
fn parse_int_or(s : String, fallback : Int) -> Int {
let mut val = 0
for c in s {
if c < '0' || c > '9' {
return fallback
}
val = val * 10 + (c.to_int() - '0'.to_int())
}
val
}
///|
/// Print response in standard dig output format
fn print_dig_response(
resp : @dns.Message,
server_addr : String,
elapsed_ms : Int64,
is_tcp : Bool,
) -> Unit {
let status_str = match resp.header.rcode {
0 => "NOERROR"
1 => "FORMERR"
2 => "SERVFAIL"
3 => "NXDOMAIN"
4 => "NOTIMP"
5 => "REFUSED"
_ => "RCODE(\{resp.header.rcode})"
}
println(";; ->>HEADER<<- opcode: QUERY, status: \{status_str}, id: \{resp.header.id.to_int()}")
let mut flags = "flags: qr"
if resp.header.aa { flags = flags + " aa" }
if resp.header.tc { flags = flags + " tc" }
if resp.header.rd { flags = flags + " rd" }
if resp.header.ra { flags = flags + " ra" }
println(";; \{flags}; QUERY: \{resp.questions.length()}, ANSWER: \{resp.answers.length()}, AUTHORITY: \{resp.authorities.length()}, ADDITIONAL: \{resp.additionals.length()}")
println("")
println(";; QUESTION SECTION:")
for q in resp.questions {
println(";\{q.name}.\t\t\tIN\t\{q.qtype}")
}
println("")
if resp.answers.length() > 0 {
println(";; ANSWER SECTION:")
for a in resp.answers {
println(format_record(a))
}
println("")
}
if resp.authorities.length() > 0 {
println(";; AUTHORITY SECTION:")
for auth in resp.authorities {
println(format_record(auth))
}
println("")
}
if resp.additionals.length() > 0 {
println(";; ADDITIONAL SECTION:")
for add in resp.additionals {
println(format_record(add))
}
println("")
}
let proto = if is_tcp { "TCP" } else { "UDP" }
println(";; Query time: \{elapsed_ms} msec")
println(";; SERVER: \{server_addr} (\{proto})")
}
///|
fn format_record(r : @dns.Record) -> String {
let rdata_str = match r.rdata {
A(ip) => @records.ipv4_to_string(ip)
AAAA(ip6) => @records.ipv6_to_string(ip6)
CNAME(t) => t + "."
NS(ns) => ns + "."
MX(pref, ex) => "\{pref.to_int()} \{ex}."
TXT(strings) => {
let sb = StringBuilder()
for i in 0.. 0 { sb.write_char(' ') }
sb.write_char('"')
sb.write_string(strings[i])
sb.write_char('"')
}
sb.to_string()
}
SOA(mname~, rname~, serial~, refresh~, retry~, expire~, minimum~) =>
"\{mname}. \{rname}. \{serial} \{refresh} \{retry} \{expire} \{minimum}"
OPT(_) => "EDNS OPT"
Raw(_) => ""
}
"\{r.name}.\t\{r.ttl}\tIN\t\{r.rtype}\t\{rdata_str}"
}