moondns

    Pure MoonBit authoritative DNS server (RFC 1035) with robust compression pointer defense.

    dns
    dns-server
    rfc1035
    authoritative
    pure-moonbit
    Download zip
    Author
    Version
    0.1.0
    License
    Apache-2.0
    Last updated
    4 hours ago
    Downloads
    1

    Dependencies

    #MoonDNS 🌙

    纯 MoonBit 实现的权威 DNS 服务器 (RFC 1035 Authoritative DNS Server) 零外部 FFI 依赖 | 恶意压缩指针防御 (无 Panic/死循环) | 严格权威语义 (AA=1/NXDOMAIN/NODATA) | UDP & TCP 双传输 | 内置迷你 dig

    License Language Tests Mooncakes


    #1. 生态定位与项目价值

    在 MoonBit 语言快速发展的生态中,网络与基础设施层已有 DNS 客户端 stub(如 moon-dns-stub)以及 zone 配置文件解析工具(如 dns-zone-toolkit),但缺少原生权威 DNS 服务端(Authoritative DNS Server)

    MoonDNS 补齐了 MoonBit 基础网络协议栈中“权威域名服务”这一关键基础设施拼图
    • 完整权威解析:让 MoonBit 首次能够独立、完整地托管与解析一个域名的权威请求。
    • 100% 纯 MoonBit 实现:所有 DNS 协议核心逻辑(报文编解码、指针压缩与恶意指针防御、Zone 数据匹配、RFC 权威应答构建)完全由 MoonBit 编写,零外部 C/JS FFI 依赖
    • 架构完全解耦:协议核心与网络传输层(UDP/TCP)彻底分离,核心协议解析与应答构建均为确定性纯函数,便于单元测试与确定性回归。
    • 坚固的安全防御:工业级压缩指针防护,天然防御针对 DNS 解析器的自环、互相循环、前向越界与跳转深度放大拒绝服务攻击(DoS)。


    #2. 核心特性

    • 完整 RFC 1035 协议支持
      • 标准 12 字节 Header、Question 与 Resource Record(RR)二进制编解码。
      • 域名 Label 序列化与自适应压缩指针生成。
    • 高安全级别压缩指针防御体系
      • 针对 DNS 安全攻击中常见的指针自指(Self-loop)、双向环路(A↔B loop)、前向越界以及超长跳转链,内建硬防线(≤128 跳深度上限、环路目标哈希防重、越界严格边界检查),保证面对任意恶意报文均返回安全 Result::Err,绝不 panic,绝不死循环。
    • 丰富的记录类型支持(7+1 种)
      • A (IPv4)
      • AAAA (IPv6, RFC 3596)
      • CNAME (规范别名,支持同区链式解析)
      • NS (权威名称服务器)
      • MX (邮件交换)
      • TXT (遵循 RFC 1035 标准,支持单个及多 character-string 连续存储)
      • SOA (区授权起始,含完整 5 项定时器与主从邮箱)
      • OPT / EDNS0 (RFC 6891 扩展,支持大报文缓冲协商)
    • 严格 RFC 权威语义
      • 成功或区内解析应答置位 AA=1(Authoritative Answer)。
      • 不可递归声明 RA=0
      • 域名 ASCII 大小写不敏感精准匹配(wWw.ExAmPlE.cOm == www.example.com)。
      • NXDOMAIN(名字不存在):返回 RCODE=3ANCOUNT=0,Authority 节填充本区 SOA 用于负缓存。
      • NODATA(名字存在但无请求类型):返回 RCODE=0ANCOUNT=0,Authority 节填充本区 SOA。
      • TC 截断机制:UDP 应答超限(512 字节或 EDNS0 协商大小)时自动置位 TC=1 并安全裁剪报文。
    • 双协议传输与 CLI 工具
      • 支持 UDPTCP(带 2 字节长度前缀,支持客户端在 UDP 截断后发起 TCP 完整重试)。
      • 内置双功能 CLI:serve 启动服务端、query 内置轻量级 dig 发送查询并格式化输出。


    #3. 技术范围与非目标 (Scope & Non-Goals)

    详见完整文档 docs/scope.md

    • 在范围内 (In Scope)
      • RFC 1035 二进制报文编解码与权威应答语义。
      • 域名压缩指针解析、循环/越界安全防护与压缩指针生成。
      • 7 种标准记录类型 (A, AAAA, CNAME, NS, MX, TXT, SOA) 及 OPT (EDNS0)。
      • Zone 文本解析(支持多行 SOA、注释、通配符 *.example.com)与大小写不敏感匹配。
      • UDP / TCP 双传输支持。
      • 命令行 servequery (内置 mini-dig)。
    • 非目标 (Non-Goals)
      • 递归查询与缓存解析器(仅做权威服务)。
      • DNSSEC 动态签名与链式校验。
      • 动态更新(RFC 2136 Dynamic Update)。
      • 真实 AXFR 全量区传送(收到请求安全拒绝)。
      • DoH (DNS-over-HTTPS) / DoT (DNS-over-TLS) 加密传输。


    #4. 架构设计

    +-------------------+ | moondns CLI | | (serve / query) | +---------+---------+ | +----------------------+----------------------+ | | v v +-------------------+ +-------------------+ | UDP Transport | | TCP Transport | | (async / socket) | | (async / socket) | +---------+---------+ +---------+---------+ | | +----------------------+----------------------+ | (Raw Request Bytes) v +-------------------------+ | transport/pipeline | (Memory Pipeline) | handle_raw_query | +------------+------------+ | v +-------------------------+ | moondns/codec | | parse / encode Message | +------------+------------+ | (Parsed Query Message) v +-------------------------+ | moondns/resolver | <---+ (Loads Zone File) | build_response (Pure) | | +------------+------------+ +--+--------------+ | | moondns/zone | v | Zone & Lookup | (Authoritative Response) +-----------------+

    #关键模块说明

    1. wjcoomk/moondns (Root):
      • 核心基础类型定义(Header, Question, Record, RType, RClass, Message, DnsError)。
      • 二进制安全缓冲区 DnsBuffer
      • 域名解析、编码与压缩器 parse_name, encode_name, NameCompressor
    2. wjcoomk/moondns/records:
      • 各类型 RDATA 专用序列化与反序列化(A, AAAA, CNAME, NS, MX, TXT, SOA, OPT)。
      • IPv4 / IPv6 纯字符串解析器与序列化器。
    3. wjcoomk/moondns/codec:
      • 完整 DNS 报文的二进制 parseencode
    4. wjcoomk/moondns/zone:
      • RFC 1035 Zone 文本文件解析器,支持 $ORIGIN, $TTL, 括号换行 SOA, 注释及通配符匹配(Wildcard)。
    5. wjcoomk/moondns/resolver:
      • 纯函数权威响应构建器 build_response。实现 AA=1、NXDOMAIN、NODATA、CNAME 链路追踪及 TC=1 截断。
    6. wjcoomk/moondns/transport:
      • 纯内存管线 handle_raw_query 与异步网络服务(UDP / TCP)。
    7. cmd/main:
      • CLI 客户端与服务端。


    #5. 快速开始

    #5.1 环境要求

    • MoonBit 工具链(>= 0.1.20260915)

    #5.2 构建与测试

    # 构建项目 (无警告、无错误) moon build --target native # 运行完整 8 组规范测试集 (60 个测试用例全部通过) moon test

    #5.3 一键演示脚本

    项目提供跨平台自动化演示脚本(自动构建、启动服务、执行多项查询验证、退出清理):
    • Windows (PowerShell):
      powershell -ExecutionPolicy Bypass -File examples/demo.ps1
    • Linux / macOS (Bash):
      chmod +x examples/demo.sh ./examples/demo.sh

    #5.4 手动启动服务 (Serve)

    # 以 1053 端口启动权威 DNS 服务器,加载示例 Zone 文件 moon run cmd/main -- serve --zone examples/example.zone --port 1053 # 支持同时监听 TCP 端口 moon run cmd/main -- serve --zone examples/example.zone --port 1053 --tcp

    #5.5 发起查询 (Query / 内置迷你 dig)

    打开另一个终端窗口:
    # 1. 查询 Apex A 记录 moon run cmd/main -- query --name example.com --type A --port 1053 # 2. 查询 CNAME 别名记录 (自动展示 CNAME 与跟随的 A 记录) moon run cmd/main -- query --name www.example.com --type A --port 1053 # 3. 查询不存在的域名 (展示 NXDOMAIN 状态与 Authority 节 SOA 负缓存) moon run cmd/main -- query --name ghost.example.com --type A --port 1053 # 4. 查询多字符串 TXT 记录 moon run cmd/main -- query --name text.example.com --type TXT --port 1053 # 5. 查询 AAAA IPv6 记录 moon run cmd/main -- query --name ns1.example.com --type AAAA --port 1053 # 6. 使用 TCP 查询 moon run cmd/main -- query --name example.com --type A --port 1053 --tcp

    #5.6 系统标准 dig 验证

    服务端启动后,亦可直接使用系统标准的 dig 工具发起查询:
    dig @127.0.0.1 -p 1053 example.com A dig @127.0.0.1 -p 1053 www.example.com A dig @127.0.0.1 -p 1053 nonexistent.example.com A


    #6. 测试结果与规范对齐报告

    MoonDNS 严格遵循比赛规范 Section 6 要求,构建了 8 组完整的自动化测试套件,存放于 tests/ 目录中。

    当前测试运行结果:
    Total tests: 60, passed: 60, failed: 0. (通过率: 100%)

    #测试套件对应源文件测试范围与 RFC 规范依据测试用例数状态
    1编解码黄金测试tests/suite1_golden_test.mbtRFC 1035 §4.1: 多组手工构造的真实报文字节向量,执行 parse -> encode 逐字节完全等价断言3✅ 通过
    2压缩指针解析tests/suite2_pointer_test.mbtRFC 1035 §4.1.4: 共享后缀域名解析、多跳跨指针链(Multi-hop chain)、根域名指针解析3✅ 通过
    3恶意指针防护tests/suite3_malicious_test.mbt指针自环(A->A)、互相循环(AB)、偏移越界、超长跳转深度(>128 跳)、截断报文、零长度输入、非法标签类型全部返回 Result::Err,绝不 panic/死循环8✅ 通过
    4域名大小写不敏感tests/suite4_case_test.mbtRFC 1035 §2.3.3: 表驱动 ASCII 大小写等价测试,wWw.ExAmPlE.tEsTwww.example.test 查询返回相同记录2✅ 通过
    5各记录类型往返tests/suite5_records_test.mbtRFC 1035 §3.2/§3.3, RFC 3596, RFC 6891: A, AAAA, CNAME, NS, MX, TXT (含多 character-string), SOA, OPT 逐类构造、编码、解析、字段全等验证8✅ 通过
    6TC 截断机制tests/suite6_truncation_test.mbtRFC 1035 §4.1.1, §4.2.1: UDP 报文超过 512 字节触发 TC=1 截断且长度 ≤512;TCP 及 EDNS0 (4096) 模式下保留完整报文2✅ 通过
    7权威语义正确性tests/suite7_semantics_test.mbtRFC 1035 §4.1.1, RFC 2308: 正常命中 (AA=1, NOERROR)、NXDOMAIN (RCODE=3 + Authority SOA)、NODATA (RCODE=0 + Authority SOA)、未知类型、未知 Class、CNAME 链式追踪6✅ 通过
    8端到端测试tests/suite8_e2e_test.mbt纯内存端到端管道 handle_raw_query: Zone 文件加载 -> 真实请求线缆字节 -> 管道处理 -> 真实响应线缆字节 -> 客户端解包完整验证3✅ 通过
    +模块单元测试codec/, records/, zone/, resolver/模块内部辅助功能(IP 解析器、Zone 文本标记化、通配符匹配、序列化)25✅ 通过
    总计全项目测试集覆盖 RFC 1035 所有核心规范与防护要求60✅ 100%


    #7. CLI 输出示例

    #7.1 启动服务

    $ moon run cmd/main -- serve --zone examples/example.zone --port 1053 [MoonDNS] Loaded zone for origin 'example.com' with 16 records.

    #7.2 查询 Apex A 记录

    $ moon run cmd/main -- query --name example.com --type A --port 1053 ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 4951 ;; flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0 ;; QUESTION SECTION: ;example.com. IN A ;; ANSWER SECTION: example.com. 3600 IN A 93.184.216.34 ;; Query time: 2 msec ;; SERVER: 127.0.0.1:1053 (UDP)

    #7.3 查询 CNAME 别名追踪

    $ moon run cmd/main -- query --name www.example.com --type A --port 1053 ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 4951 ;; flags: qr aa rd; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 0 ;; QUESTION SECTION: ;www.example.com. IN A ;; ANSWER SECTION: www.example.com. 3600 IN CNAME example.com. example.com. 3600 IN A 93.184.216.34 ;; Query time: 4 msec ;; SERVER: 127.0.0.1:1053 (UDP)

    #7.4 NXDOMAIN 负缓存查询

    $ moon run cmd/main -- query --name ghost.example.com --type A --port 1053 ;; ->>HEADER<<- opcode: QUERY, status: NXDOMAIN, id: 4951 ;; flags: qr aa rd; QUERY: 1, ANSWER: 0, AUTHORITY: 1, ADDITIONAL: 0 ;; QUESTION SECTION: ;ghost.example.com. IN A ;; AUTHORITY SECTION: example.com. 3600 IN SOA ns1.example.com. hostmaster.example.com. 2026092001 7200 3600 1209600 3600 ;; Query time: 2 msec ;; SERVER: 127.0.0.1:1053 (UDP)


    #8. 参考来源与致谢

    • RFC 1035: DOMAIN NAMES - IMPLEMENTATION AND SPECIFICATION
    • RFC 2308: DNS NCACHE - Negative Caching of DNS Queries (DNS NCACHE)
    • RFC 3596: DNS Extensions to Support IP Version 6
    • RFC 6891: Extension Mechanisms for DNS (EDNS(0))
    • MoonBit 官方团队提供的标准库与工具链支持。


    #9. 开源许可证

    本项目遵循 Apache-2.0 许可证发布。

    DnsBuffer

    pub(all) struct DnsBuffer {
    bytes : Array[Byte]
    }

    Dynamic byte buffer with big-endian integer support and in-place patching

    DnsBuffer::length

    fn DnsBuffer::length(self : DnsBuffer) -> Int

    DnsBuffer::new

    fn DnsBuffer::new() -> DnsBuffer

    DnsBuffer::set_uint16_be

    fn DnsBuffer::set_uint16_be(self : DnsBuffer, pos : Int, val : Int) -> Unit

    DnsBuffer::to_bytes

    fn DnsBuffer::to_bytes(self : DnsBuffer) -> Bytes

    DnsBuffer::write_byte

    fn DnsBuffer::write_byte(self : DnsBuffer, b : Byte) -> Unit

    DnsBuffer::write_bytes

    fn DnsBuffer::write_bytes(self : DnsBuffer, bs : Bytes) -> Unit

    DnsBuffer::write_fixedarray

    fn DnsBuffer::write_fixedarray(self : DnsBuffer, arr : FixedArray[Byte]) -> Unit

    DnsBuffer::write_uint16_be

    fn DnsBuffer::write_uint16_be(self : DnsBuffer, val : UInt16) -> Unit

    DnsBuffer::write_uint_be

    fn DnsBuffer::write_uint_be(self : DnsBuffer, val : UInt) -> Unit

    DnsError

    pub(all) enum DnsError {
    BufferUnderflow
    MalformedPacket(String)
    PointerLoop(String)
    PointerOutOfBounds(String)
    PointerJumpLimitExceeded
    InvalidLabelLength(Int)
    InvalidDomainName(String)
    UnexpectedEof
    Other(String)
    } derive(Eq,
    Debug
    )

    DNS Parsing and Protocol Errors
    pub(all) struct Header {
    id : UInt16
    qr : Bool
    opcode : Int
    aa : Bool
    tc : Bool
    rd : Bool
    ra : Bool
    z : Int
    rcode : Int
    qdcount : UInt16
    ancount : UInt16
    nscount : UInt16
    arcount : UInt16
    } derive(Eq,
    Debug
    )

    DNS Header (12 bytes, RFC 1035 Section 4.1.1)

    Header::new

    fn Header::new(id : UInt16, qr : Bool, opcode : Int, aa : Bool, tc : Bool, rd : Bool, ra : Bool, z : Int, rcode : Int, qdcount : UInt16, ancount : UInt16, nscount : UInt16, arcount : UInt16) -> Header

    Message

    pub(all) struct Message {
    header : Header
    questions : Array[Question]
    answers : Array[Record]
    authorities : Array[Record]
    additionals : Array[Record]
    } derive(Eq,
    Debug
    )

    Complete DNS Message

    Message::new

    fn Message::new(header : Header, questions : Array[Question], answers : Array[Record], authorities : Array[Record], additionals : Array[Record]) -> Message

    NameCompressor

    pub(all) struct NameCompressor {
    entries : Array[(String, Int)]
    }

    Compression dictionary for domain name serialization

    NameCompressor::add

    fn NameCompressor::add(self : NameCompressor, suffix : String, offset : Int) -> Unit

    NameCompressor::find

    fn NameCompressor::find(self : NameCompressor, suffix : String) -> Int?

    NameCompressor::new

    Question

    pub(all) struct Question {
    name : String
    qtype : RType
    qclass : RClass
    } derive(Eq,
    Debug
    )

    DNS Question (RFC 1035 Section 4.1.2)

    Question::new

    fn Question::new(name : String, qtype : RType, qclass : RClass) -> Question

    RClass

    pub(all) enum RClass {
    IN
    CS
    CH
    HS
    ANY
    Unknown(UInt16)
    } derive(Compare, Eq,
    Debug
    )

    DNS Resource Record Classes
    impl Show for RClass

    RClass::from_uint16

    fn RClass::from_uint16(val : UInt16) -> RClass

    RClass::to_uint16

    fn RClass::to_uint16(self : RClass) -> UInt16

    RData

    pub(all) enum RData {
    A(UInt)
    AAAA(FixedArray[Byte])
    CNAME(String)
    NS(String)
    MX(UInt16, String)
    TXT(Array[String])
    SOA(mname~ : String, rname~ : String, serial~ : UInt, refresh~ : UInt, retry~ : UInt, expire~ : UInt, minimum~ : UInt)
    OPT(FixedArray[Byte])
    Raw(FixedArray[Byte])
    } derive(Eq,
    Debug
    )

    DNS Resource Record Data

    RType

    pub(all) enum RType {
    A
    NS
    CNAME
    SOA
    MX
    TXT
    AAAA
    OPT
    Unknown(UInt16)
    } derive(Compare, Eq,
    Debug
    )

    DNS Resource Record Types
    impl Show for RType

    RType::from_uint16

    fn RType::from_uint16(val : UInt16) -> RType

    RType::to_uint16

    fn RType::to_uint16(self : RType) -> UInt16

    Record

    pub(all) struct Record {
    name : String
    rtype : RType
    rclass : RClass
    ttl : UInt
    rdata : RData
    } derive(Eq,
    Debug
    )

    DNS Resource Record (RFC 1035 Section 4.1.3)

    Record::new

    fn Record::new(name : String, rtype : RType, rclass : RClass, ttl : UInt, rdata : RData) -> Record

    domain_equal

    fn domain_equal(a : String, b : String) -> Bool

    RFC 1035 Case-insensitive domain equality

    encode_name

    fn encode_name(name : String, buf : DnsBuffer, comp : NameCompressor?, compress : Bool) -> Unit

    Encode domain name into DnsBuffer, optionally using compression

    normalize_domain

    fn normalize_domain(name : String) -> String

    Normalize domain name: lowercase ASCII and strip trailing dot

    parse_name

    fn parse_name(bytes : Bytes, start_offset : Int) -> Result[(String, Int), DnsError]

    Parse domain name from bytes with full security guards:
    • Guard 1: Loop detection (A -> A, A -> B -> A)
    • Guard 2: Forward pointer / self pointer defense (target >= ptr_pos)
    • Guard 3: Out of bounds defense (target >= bytes.length)
    • Guard 4: Jump limit defense (<= 128 jumps)
    • Guard 5: Label length <= 63 bytes
    • Guard 6: Total name length <= 255 bytes

    split_labels

    fn split_labels(domain : String) -> Array[String]

    Split a normalized domain into labels

    to_ascii_lower

    fn to_ascii_lower(s : String) -> String

    Convert ASCII uppercase letters to lowercase