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>
424 lines
14 KiB
Markdown
424 lines
14 KiB
Markdown
# 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
|
|
|
|
```sql
|
|
-- 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
|
|
|
|
```protobuf
|
|
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:**
|
|
```json
|
|
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:**
|
|
```json
|
|
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:**
|
|
```json
|
|
{
|
|
"error": "CNAME record conflicts with existing A record for 'metacrypt'"
|
|
}
|
|
```
|
|
|
|
## Configuration
|
|
|
|
```toml
|
|
[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
|
|
|
|
```dockerfile
|
|
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)
|
|
|
|
```yaml
|
|
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.
|