Files
mcns/ARCHITECTURE.md
Kyle Isom a545fec658 Add ARCHITECTURE.md for custom Go DNS server replacing CoreDNS
Design MCNS as a purpose-built authoritative DNS server with SQLite-backed
zone/record storage and a gRPC+REST management API. Supports A, AAAA, and
CNAME records with upstream forwarding for non-authoritative queries.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 18:24:07 -07:00

14 KiB

MCNS Architecture

Metacircular Networking Service — an authoritative DNS server for the Metacircular platform with a management API for dynamic record updates.

System Overview

MCNS replaces the CoreDNS precursor with a purpose-built Go DNS server. It serves authoritative DNS for platform zones (e.g., svc.mcp.metacircular.net, mcp.metacircular.net) and forwards all other queries to upstream resolvers. Records are stored in SQLite and managed via gRPC and REST APIs authenticated through MCIAS.

Architecture Layers

┌─────────────────────────────────────────────────────────┐
│                     Clients                              │
│  DNS resolvers (port 53)  │  API clients (8443/9443)    │
└──────────┬────────────────┴──────────────┬──────────────┘
           │                               │
┌──────────▼──────────┐   ┌───────────────▼───────────────┐
│     DNS Server      │   │       Management API           │
│  UDP+TCP :53        │   │  REST :8443  │  gRPC :9443    │
│  miekg/dns          │   │  chi router  │  grpc-go       │
└──────────┬──────────┘   └───────────────┬───────────────┘
           │                               │
           │         ┌─────────────────┐   │
           │         │   Auth Layer    │   │
           │         │   MCIAS via     │   │
           │         │   mcdsl/auth    │   │
           │         └────────┬────────┘   │
           │                  │            │
┌──────────▼──────────────────▼────────────▼──────────────┐
│                    Storage Layer                          │
│              SQLite (zones + records)                     │
└──────────────────────────┬──────────────────────────────┘
                           │
              ┌────────────▼────────────┐
              │     Upstream Forwarder  │
              │  1.1.1.1 / 8.8.8.8     │
              └─────────────────────────┘

Design Principles

  1. DNS is the fast path. The DNS handler reads from SQLite and responds. No locks beyond SQLite's own WAL reader concurrency. DNS queries never touch the auth layer.
  2. Management is the authenticated path. All record mutations go through the REST/gRPC API, authenticated via MCIAS.
  3. Immediate consistency. Record changes are visible to DNS queries as soon as the SQLite transaction commits. No restart required.
  4. Forward everything else. Non-authoritative queries are forwarded to configured upstream resolvers. Forwarded responses are cached in-memory with TTL-based expiry.

Record Types

v1 supports three record types:

Type Value Format Example
A IPv4 address 192.168.88.181
AAAA IPv6 address 2001:db8::1
CNAME Fully-qualified domain name rift.mcp.metacircular.net.

CNAME records follow standard DNS rules: a name with a CNAME record must not have any other record types (enforced at the database level).

DNS Server

Listening

The DNS server listens on a configurable address (default :53) for both UDP and TCP. UDP is the primary transport; TCP is required for responses exceeding 512 bytes and for zone transfer compatibility.

Query Handling

Query received
  │
  ├─ Is the qname within an authoritative zone?
  │    │
  │    ├─ Yes → Look up records from SQLite
  │    │         ├─ Records found → Build authoritative response (AA=1)
  │    │         └─ No records → Return NXDOMAIN (AA=1, SOA in authority)
  │    │
  │    └─ No → Forward to upstream
  │              ├─ Check cache → Hit → Return cached response
  │              └─ Miss → Query upstream, cache response, return
  │
  └─ Special cases:
       ├─ SOA query for zone apex → Return SOA from zone config
       └─ NS query for zone apex → Return NS from zone config

SOA Serial Management

Zone SOA serial numbers use the YYYYMMDDNN format. When a record is created, updated, or deleted, the zone's serial is auto-incremented:

  • If the current serial's date prefix matches today, increment NN.
  • If the date prefix is older, reset to today with NN=01.
  • Maximum 99 changes per day per zone before the counter overflows to the next day.

Forwarding and Caching

Non-authoritative queries are forwarded to upstream resolvers configured in [dns]. The forwarder:

  • Tries each upstream in order, with a 2-second timeout per attempt.
  • Caches successful responses keyed by (qname, qtype, qclass).
  • Cache TTL is the minimum TTL from the response, capped at 300 seconds.
  • Cache is in-memory with lazy expiry (checked on read).
  • SERVFAIL and refused responses are not cached.

Storage

Database

SQLite with WAL mode, opened via mcdsl/db.Open. Single database file at the configured path (default /srv/mcns/mcns.db).

Schema

-- Migration 1: zones and records

CREATE TABLE zones (
    id          INTEGER PRIMARY KEY,
    name        TEXT NOT NULL UNIQUE,
    primary_ns  TEXT NOT NULL,
    admin_email TEXT NOT NULL,
    refresh     INTEGER NOT NULL DEFAULT 3600,
    retry       INTEGER NOT NULL DEFAULT 600,
    expire      INTEGER NOT NULL DEFAULT 86400,
    minimum_ttl INTEGER NOT NULL DEFAULT 300,
    serial      INTEGER NOT NULL DEFAULT 0,
    created_at  TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
    updated_at  TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
);

CREATE TABLE records (
    id         INTEGER PRIMARY KEY,
    zone_id    INTEGER NOT NULL REFERENCES zones(id) ON DELETE CASCADE,
    name       TEXT NOT NULL,
    type       TEXT NOT NULL CHECK (type IN ('A', 'AAAA', 'CNAME')),
    value      TEXT NOT NULL,
    ttl        INTEGER NOT NULL DEFAULT 300,
    created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
    updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
    UNIQUE(zone_id, name, type, value)
);

CREATE INDEX idx_records_zone_name ON records(zone_id, name);

Constraints

  • Zone names are stored without trailing dot, lowercased.
  • Record names are relative to the zone (e.g., metacrypt not metacrypt.svc.mcp.metacircular.net). The special name @ refers to the zone apex.
  • CNAME exclusivity: inserting a CNAME for a name that already has A/AAAA records (or vice versa) is rejected. Enforced in application logic within the transaction.
  • A record's value must be a valid IPv4 address. AAAA must be valid IPv6. CNAME must be a valid domain name ending with a dot.

Authentication and Authorization

Authentication is delegated to MCIAS via mcdsl/auth. The DNS server (port 53) has no authentication — it serves DNS to any client, as is standard for DNS.

The management API (REST + gRPC) uses MCIAS bearer tokens:

Role Permissions
admin Full access: create/update/delete zones and records
user Read-only: list and get zones and records
guest Read-only: list and get zones and records

gRPC Interceptor Maps

Map Methods
Public Health
AuthRequired ListZones, GetZone, ListRecords, GetRecord
AdminRequired CreateZone, UpdateZone, DeleteZone, CreateRecord, UpdateRecord, DeleteRecord

API Surface

REST Endpoints

Method Path Auth Description
POST /v1/auth/login None Login via MCIAS
POST /v1/auth/logout Bearer Logout
GET /v1/health None Health check
GET /v1/zones Bearer List all zones
POST /v1/zones Admin Create a zone
GET /v1/zones/{zone} Bearer Get zone details
PUT /v1/zones/{zone} Admin Update zone SOA parameters
DELETE /v1/zones/{zone} Admin Delete zone and all its records
GET /v1/zones/{zone}/records Bearer List records in a zone
POST /v1/zones/{zone}/records Admin Create a record
GET /v1/zones/{zone}/records/{id} Bearer Get a record
PUT /v1/zones/{zone}/records/{id} Admin Update a record
DELETE /v1/zones/{zone}/records/{id} Admin Delete a record

gRPC Services

service AuthService {
  rpc Login(LoginRequest) returns (LoginResponse);
  rpc Logout(LogoutRequest) returns (LogoutResponse);
}

service ZoneService {
  rpc ListZones(ListZonesRequest) returns (ListZonesResponse);
  rpc CreateZone(CreateZoneRequest) returns (Zone);
  rpc GetZone(GetZoneRequest) returns (Zone);
  rpc UpdateZone(UpdateZoneRequest) returns (Zone);
  rpc DeleteZone(DeleteZoneRequest) returns (DeleteZoneResponse);
}

service RecordService {
  rpc ListRecords(ListRecordsRequest) returns (ListRecordsResponse);
  rpc CreateRecord(CreateRecordRequest) returns (Record);
  rpc GetRecord(GetRecordRequest) returns (Record);
  rpc UpdateRecord(UpdateRecordRequest) returns (Record);
  rpc DeleteRecord(DeleteRecordRequest) returns (DeleteRecordResponse);
}

service AdminService {
  rpc Health(HealthRequest) returns (HealthResponse);
}

Request/Response Examples

Create Zone:

POST /v1/zones
{
  "name": "svc.mcp.metacircular.net",
  "primary_ns": "ns.mcp.metacircular.net.",
  "admin_email": "admin.metacircular.net."
}

Response 201:
{
  "id": 1,
  "name": "svc.mcp.metacircular.net",
  "primary_ns": "ns.mcp.metacircular.net.",
  "admin_email": "admin.metacircular.net.",
  "refresh": 3600,
  "retry": 600,
  "expire": 86400,
  "minimum_ttl": 300,
  "serial": 2026032601,
  "created_at": "2026-03-26T20:00:00Z",
  "updated_at": "2026-03-26T20:00:00Z"
}

Create Record:

POST /v1/zones/svc.mcp.metacircular.net/records
{
  "name": "metacrypt",
  "type": "A",
  "value": "192.168.88.181",
  "ttl": 300
}

Response 201:
{
  "id": 1,
  "zone": "svc.mcp.metacircular.net",
  "name": "metacrypt",
  "type": "A",
  "value": "192.168.88.181",
  "ttl": 300,
  "created_at": "2026-03-26T20:00:00Z",
  "updated_at": "2026-03-26T20:00:00Z"
}

Error Response:

{
  "error": "CNAME record conflicts with existing A record for 'metacrypt'"
}

Configuration

[server]
listen_addr = ":8443"
grpc_addr   = ":9443"
tls_cert    = "/srv/mcns/certs/cert.pem"
tls_key     = "/srv/mcns/certs/key.pem"

[database]
path = "/srv/mcns/mcns.db"

[dns]
listen_addr = ":53"
upstreams   = ["1.1.1.1:53", "8.8.8.8:53"]

[mcias]
server_url   = "https://svc.metacircular.net:8443"
ca_cert      = ""
service_name = "mcns"
tags         = []

[log]
level = "info"

DNS-Specific Configuration

Field Required Default Description
dns.listen_addr No :53 UDP+TCP listen address
dns.upstreams No ["1.1.1.1:53", "8.8.8.8:53"] Upstream forwarders

CLI Commands

Command Purpose
mcns server Start the DNS + API servers
mcns snapshot Create a database backup
mcns status Query a running instance's health

Deployment

Container

Single Dockerfile, single binary. The container exposes:

  • Port 53 (UDP+TCP) — DNS
  • Port 8443 (TCP) — REST API
  • Port 9443 (TCP) — gRPC API
FROM golang:1.25-alpine AS builder
# ... build with CGO_ENABLED=0 ...

FROM alpine:3.21
RUN addgroup -S mcns && adduser -S mcns -G mcns
COPY --from=builder /build/mcns /usr/local/bin/mcns
USER mcns
EXPOSE 53/udp 53/tcp 8443 9443
ENTRYPOINT ["mcns", "server"]

Docker Compose (rift)

services:
  mcns:
    image: mcr.svc.mcp.metacircular.net:8443/mcns:latest
    container_name: mcns
    restart: unless-stopped
    command: ["server", "--config", "/srv/mcns/mcns.toml"]
    ports:
      - "192.168.88.181:53:53/udp"
      - "192.168.88.181:53:53/tcp"
      - "100.95.252.120:53:53/udp"
      - "100.95.252.120:53:53/tcp"
    volumes:
      - /srv/mcns:/srv/mcns

The API ports (8443/9443) are fronted by MC-Proxy, not exposed directly.

Data Directory

/srv/mcns/
├── mcns.toml         Configuration
├── mcns.db           SQLite database
├── certs/            TLS certificates
└── backups/          Database snapshots

Security Model

Threat Mitigations

Threat Mitigation
DNS cache poisoning Forwarded responses validated by upstream; TXID randomized by miekg/dns
DNS amplification Response rate limiting (future); TCP fallback for large responses
Unauthorized record modification Admin-only writes via MCIAS-authenticated API
SQL injection Parameterized queries throughout
Zone enumeration via API Requires authentication (user or admin role)

Security Invariants

  1. DNS port 53 serves read-only data. No mutations possible via DNS protocol.
  2. All record mutations require admin authentication through MCIAS.
  3. CNAME exclusivity is enforced transactionally — no name can have both CNAME and address records.
  4. Zone serial numbers are monotonically increasing — rollback is not possible.
  5. TLS 1.3 minimum on management API (enforced by mcdsl/httpserver).

Future Work

  • MX, TXT, SRV records — additional record types as needed.
  • DNSSEC — zone signing for authoritative responses.
  • Zone transfer (AXFR/IXFR) — for secondary DNS servers.
  • MCP integration — MCP agent automatically registers service records.
  • Web UI — htmx interface for zone and record management.
  • Rate limiting — DNS query rate limiting to mitigate amplification.
  • Metrics — Prometheus endpoint for query rates, cache hit ratio, latency.