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>
This commit is contained in:
2026-03-26 18:24:07 -07:00
parent 0c1234663d
commit a545fec658
2 changed files with 482 additions and 35 deletions

423
ARCHITECTURE.md Normal file
View File

@@ -0,0 +1,423 @@
# 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.

View File

@@ -2,46 +2,70 @@
## Overview ## Overview
MCNS precursor — a CoreDNS instance serving internal DNS zones for the MCNS (Metacircular Networking Service) is an authoritative DNS server for
Metacircular platform until the full MCNS service is built. the Metacircular platform. It serves DNS zones from SQLite, forwards
non-authoritative queries to upstream resolvers, and exposes a gRPC/REST
management API authenticated through MCIAS.
## Zones ## Build Commands
| Zone | Purpose |
|------|---------|
| `svc.mcp.metacircular.net` | Internal service addresses (e.g. `metacrypt.svc.mcp.metacircular.net`) |
| `mcp.metacircular.net` | Node addresses (e.g. `rift.mcp.metacircular.net`) |
Everything else forwards to 1.1.1.1 and 8.8.8.8.
## Files
- `Corefile` — CoreDNS configuration
- `zones/` — Zone files (manually maintained until MCP manages them)
- `deploy/docker/docker-compose-rift.yml` — Docker compose for rift deployment
## Operations
```bash ```bash
# Start make all # vet → lint → test → build
docker compose -f deploy/docker/docker-compose-rift.yml up -d make build # go build ./...
make test # go test ./...
# Test resolution make vet # go vet ./...
dig @192.168.88.181 metacrypt.svc.mcp.metacircular.net make lint # golangci-lint run ./...
dig @192.168.88.181 rift.mcp.metacircular.net make proto # regenerate gRPC code from .proto files
make proto-lint # buf lint + buf breaking
# After editing zone files, bump the serial and restart make mcns # build mcns binary with version injection
docker compose -f deploy/docker/docker-compose-rift.yml restart make docker # build container image
make clean # remove binaries
make devserver # build and run against srv/ config
``` ```
## Adding a service Run a single test: `go test ./internal/dns/ -run TestHandlerA`
1. Add an A record to `zones/svc.mcp.metacircular.net.zone` ## Architecture
2. Bump the serial number (YYYYMMDDNN format)
3. Restart CoreDNS
## Adding a node Three listeners on one binary:
1. Add an A record to `zones/mcp.metacircular.net.zone` - **DNS server** (`:53`, UDP+TCP) — serves authoritative zones from SQLite,
2. Bump the serial number forwards everything else to upstream resolvers. No authentication.
3. Restart CoreDNS - **REST API** (`:8443`) — management API for zones and records. MCIAS auth.
- **gRPC API** (`:9443`) — same management operations. MCIAS auth.
Records stored in SQLite. Changes visible to DNS immediately on commit.
## Project Structure
```
cmd/mcns/ CLI entry point (server, snapshot, status)
internal/
config/ TOML config with DNS-specific sections
db/ SQLite schema, migrations, zone/record queries
dns/ DNS server, handler, forwarder, cache
server/ REST API routes and handlers
grpcserver/ gRPC server, interceptors, service handlers
proto/mcns/v1/ Protobuf definitions
gen/mcns/v1/ Generated Go code (do not edit)
deploy/ Docker, systemd, install scripts, examples
```
## Ignored Directories
- `srv/` — local dev runtime data
- `gen/` — regenerated from proto, not hand-edited
## Critical Rules
1. **REST/gRPC sync**: Every REST endpoint must have a corresponding gRPC
RPC, updated in the same change.
2. **gRPC interceptor maps**: New RPCs must be added to the correct
interceptor map (Public, AuthRequired, AdminRequired). Forgetting this
is a security defect.
3. **CNAME exclusivity**: A name cannot have both CNAME and A/AAAA records.
Enforced in the database layer within a transaction.
4. **SOA serial**: Auto-incremented on every record mutation. Never manually
set or decremented.
5. **DNS has no auth**: Port 53 serves records to any client. All mutations
go through the authenticated management API.