Files
mcns/ARCHITECTURE.md
Kyle Isom 9ac944fb39 Document ListRecords filtering, gRPC examples, and expand CLAUDE.md
ARCHITECTURE.md:
- Note optional ?name=/&type= query filters on GET /v1/zones/{zone}/records
- Document ListRecordsRequest name/type fields as optional filters in gRPC service
- Add gRPC usage examples section with grpcurl commands

CLAUDE.md:
- Add mcdsl shared library section
- Add testing patterns (stdlib only, real SQLite, no mocks)
- Add key invariants: SOA serial YYYYMMDDNN format, CNAME exclusivity at DB layer

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

451 lines
15 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 (optional `?name=`/`?type=` filters) |
| 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 {
// ListRecords returns records in a zone. The name and type fields in
// ListRecordsRequest are optional filters; omit them to return all records.
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'"
}
```
### gRPC Usage Examples
**List zones with grpcurl:**
```bash
grpcurl -cacert ca.pem \
-H "Authorization: Bearer $TOKEN" \
mcns.svc.mcp.metacircular.net:9443 mcns.v1.ZoneService/ListZones
```
**Create a record with grpcurl:**
```bash
grpcurl -cacert ca.pem \
-H "Authorization: Bearer $TOKEN" \
-d '{"zone":"svc.mcp.metacircular.net","name":"metacrypt","type":"A","value":"192.168.88.181","ttl":300}' \
mcns.svc.mcp.metacircular.net:9443 mcns.v1.RecordService/CreateRecord
```
**List records with name filter:**
```bash
grpcurl -cacert ca.pem \
-H "Authorization: Bearer $TOKEN" \
-d '{"zone":"svc.mcp.metacircular.net","name":"metacrypt"}' \
mcns.svc.mcp.metacircular.net:9443 mcns.v1.RecordService/ListRecords
```
## 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.