photoncloud-monorepo/specifications/flashdns/README.md
centra 5c6eb04a46 T036: Add VM cluster deployment configs for nixos-anywhere
- netboot-base.nix with SSH key auth
- Launch scripts for node01/02/03
- Node configuration.nix and disko.nix
- Nix modules for first-boot automation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-11 09:59:19 +09:00

1166 lines
37 KiB
Markdown

# FlashDNS Specification
> Version: 1.0 | Status: Draft | Last Updated: 2025-12-08
## 1. Overview
### 1.1 Purpose
FlashDNS is an authoritative DNS service providing zone management and DNS resolution for the cloud platform. It enables organizations and projects to host DNS zones with full CRUD operations on DNS records, supporting standard record types and multi-tenant isolation.
The name "FlashDNS" reflects fast, reliable DNS resolution with the "Flash" prefix denoting speed and the cloud platform family branding.
### 1.2 Scope
- **In scope**: Authoritative DNS serving (UDP/TCP port 53), zone management (SOA, NS configuration), DNS record CRUD (A, AAAA, CNAME, MX, TXT, SRV, NS, PTR), multi-tenant zones (org/project scoped), gRPC management API, aegis integration for access control, ChainFire storage backend
- **Out of scope**: Recursive DNS resolution, DNSSEC signing (planned), DNS-over-HTTPS/TLS (planned), dynamic DNS updates (RFC 2136), zone transfers (AXFR/IXFR - planned), GeoDNS/latency-based routing
### 1.3 Design Goals
- **Authoritative-only**: Serve authoritative responses for managed zones
- **Multi-tenant from day one**: Full org/project zone isolation with aegis integration
- **High-performance resolution**: Sub-millisecond query responses with in-memory zone cache
- **Standard DNS compliance**: RFC 1035 wire format, common record types
- **Cloud-native management**: gRPC API for zone/record management, Prometheus metrics
- **Consistent storage**: ChainFire for zone/record persistence with strong consistency
## 2. Architecture
### 2.1 Crate Structure
```
flashdns/
├── crates/
│ ├── flashdns-api/ # gRPC service implementations
│ ├── flashdns-client/ # Rust client library
│ ├── flashdns-server/ # Server binary (DNS + gRPC)
│ ├── flashdns-proto/ # DNS wire protocol handling
│ └── flashdns-types/ # Shared types (Zone, Record, etc.)
└── proto/
└── flashdns.proto # gRPC API definitions
```
### 2.2 Component Topology
```
┌─────────────────────────────────────────────────────────────────┐
│ FlashDNS Server │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ flashdns-proto │ │ flashdns-api │ │ flashdns-types │ │
│ │ (UDP/TCP :53) │ │ (gRPC) │ │ (core types) │ │
│ └────────┬────────┘ └────────┬────────┘ └────────┬────────┘ │
│ │ │ │ │
│ └────────────────────┼────────────────────┘ │
│ │ │
│ ┌──────▼──────┐ │
│ │ Core │ │
│ │ (zones, │ │
│ │ records, │ │
│ │ cache) │ │
│ └──────┬──────┘ │
└────────────────────────────────┼────────────────────────────────┘
┌────────────┼────────────┐
▼ ▼ ▼
┌───────────┐ ┌───────────┐ ┌───────────┐
│ ChainFire │ │ In-Mem │ │ Aegis │
│ (storage) │ │ Cache │ │ (IAM) │
└───────────┘ └───────────┘ └───────────┘
```
### 2.3 Data Flow
**DNS Query Flow**:
```
[DNS Client] → [UDP/TCP :53] → [Query Parser] → [Zone Lookup]
[Cache Check]
┌─────────────────┼─────────────────┐
▼ ▼
[Cache Hit] [Cache Miss]
│ │
│ [ChainFire Fetch]
│ │
└─────────────────┬─────────────────┘
[Build Response]
[DNS Client]
```
**Management API Flow**:
```
[gRPC Client] → [flashdns-api] → [Aegis AuthZ] → [Core Service]
[ChainFire Store]
[Cache Invalidate]
```
### 2.4 Dependencies
| Crate | Version | Purpose |
|-------|---------|---------|
| tokio | 1.x | Async runtime |
| tonic | 0.12 | gRPC framework |
| prost | 0.13 | Protocol buffers |
| trust-dns-proto | 0.24 | DNS wire protocol |
| dashmap | 6.x | Concurrent zone cache |
| uuid | 1.x | Zone/record identifiers |
## 3. Core Concepts
### 3.1 Zone
A DNS zone representing a contiguous portion of the DNS namespace, scoped to an organization and optionally a project.
```rust
pub struct Zone {
pub id: String, // UUID
pub name: String, // FQDN (e.g., "example.com.")
pub org_id: String, // Owner organization
pub project_id: Option<String>, // Optional project scope
pub soa: SoaRecord, // Start of Authority
pub ns_records: Vec<String>, // Nameserver FQDNs
pub created_at: u64, // Creation timestamp (Unix ms)
pub updated_at: u64, // Last modification
pub created_by: String, // Principal ID
pub serial: u32, // SOA serial (auto-incremented)
pub status: ZoneStatus,
pub metadata: HashMap<String, String>,
pub tags: HashMap<String, String>,
}
pub struct SoaRecord {
pub mname: String, // Primary nameserver
pub rname: String, // Admin email (dns format)
pub serial: u32, // Zone serial number
pub refresh: u32, // Refresh interval (seconds)
pub retry: u32, // Retry interval (seconds)
pub expire: u32, // Expire time (seconds)
pub minimum: u32, // Minimum TTL (negative cache)
}
pub enum ZoneStatus {
Active, // Zone is serving
Pending, // Zone created, not yet propagated
Disabled, // Zone disabled by admin
}
impl Default for SoaRecord {
fn default() -> Self {
Self {
mname: "ns1.flashdns.local.".into(),
rname: "hostmaster.flashdns.local.".into(),
serial: 1,
refresh: 7200, // 2 hours
retry: 3600, // 1 hour
expire: 1209600, // 2 weeks
minimum: 3600, // 1 hour
}
}
}
```
**Zone Naming Rules**:
- Valid DNS domain name (RFC 1035)
- Must end with trailing dot (FQDN)
- Max 253 characters
- Labels: 1-63 characters each
- Unique within org (or project if project-scoped)
- Fully qualified ID: `{org_id}/{project_id}/{name}` or `{org_id}/{name}`
### 3.2 Record
A DNS resource record within a zone.
```rust
pub struct Record {
pub id: String, // UUID
pub zone_id: String, // Parent zone ID
pub name: String, // Record name (relative or FQDN)
pub record_type: RecordType,
pub ttl: u32, // Time-to-live (seconds)
pub data: RecordData, // Type-specific data
pub created_at: u64,
pub updated_at: u64,
pub created_by: String,
pub enabled: bool, // Soft disable
}
pub enum RecordType {
A, // IPv4 address
AAAA, // IPv6 address
CNAME, // Canonical name
MX, // Mail exchange
TXT, // Text record
SRV, // Service locator
NS, // Nameserver
PTR, // Pointer (reverse DNS)
CAA, // Certification Authority Authorization
SOA, // Start of Authority (zone-level only)
}
pub enum RecordData {
A { address: Ipv4Addr },
AAAA { address: Ipv6Addr },
CNAME { target: String },
MX { preference: u16, exchange: String },
TXT { value: String },
SRV {
priority: u16,
weight: u16,
port: u16,
target: String,
},
NS { nsdname: String },
PTR { ptrdname: String },
CAA { flags: u8, tag: String, value: String },
}
```
**Record Name Rules**:
- Relative names are appended to zone name
- `@` represents zone apex
- Wildcards: `*.example.com.` supported
- Labels validated per RFC 1035
### 3.3 RecordSet
Logical grouping of records with same name and type.
```rust
pub struct RecordSet {
pub zone_id: String,
pub name: String, // Record name
pub record_type: RecordType,
pub ttl: u32,
pub records: Vec<RecordData>, // Multiple values (e.g., round-robin A)
}
impl RecordSet {
/// Convert to individual Record entries
pub fn to_records(&self) -> Vec<Record>;
/// Merge records into a set for response building
pub fn from_records(records: Vec<Record>) -> Self;
}
```
### 3.4 Query
Represents an incoming DNS query.
```rust
pub struct DnsQuery {
pub id: u16, // Transaction ID
pub name: String, // Query name (FQDN)
pub qtype: RecordType, // Query type
pub qclass: DnsClass, // Usually IN (Internet)
pub source_ip: IpAddr, // Client IP
pub transport: Transport, // UDP or TCP
pub received_at: u64,
}
pub enum DnsClass {
IN, // Internet
CH, // Chaos (for version queries)
ANY, // Any class
}
pub enum Transport {
Udp,
Tcp,
}
```
## 4. API
### 4.1 gRPC Services
#### Zone Service (`flashdns.v1.ZoneService`)
```protobuf
service ZoneService {
// Zone CRUD
rpc CreateZone(CreateZoneRequest) returns (Zone);
rpc GetZone(GetZoneRequest) returns (Zone);
rpc UpdateZone(UpdateZoneRequest) returns (Zone);
rpc DeleteZone(DeleteZoneRequest) returns (DeleteZoneResponse);
rpc ListZones(ListZonesRequest) returns (ListZonesResponse);
// Zone operations
rpc GetZoneByName(GetZoneByNameRequest) returns (Zone);
rpc IncrementSerial(IncrementSerialRequest) returns (Zone);
}
message CreateZoneRequest {
string name = 1; // Zone FQDN (e.g., "example.com.")
string org_id = 2;
optional string project_id = 3;
optional SoaRecord soa = 4; // Custom SOA (or use defaults)
repeated string ns_records = 5; // Nameserver FQDNs
map<string, string> tags = 6;
}
message GetZoneRequest {
string zone_id = 1;
}
message GetZoneByNameRequest {
string name = 1; // Zone FQDN
string org_id = 2;
optional string project_id = 3;
}
message UpdateZoneRequest {
string zone_id = 1;
optional SoaRecord soa = 2;
repeated string ns_records = 3;
map<string, string> tags = 4;
optional ZoneStatus status = 5;
}
message DeleteZoneRequest {
string zone_id = 1;
bool force = 2; // Delete even if records exist
}
message ListZonesRequest {
string org_id = 1;
optional string project_id = 2;
optional string name_filter = 3; // Prefix match
uint32 limit = 4; // Max results (default: 100)
string page_token = 5;
}
message ListZonesResponse {
repeated Zone zones = 1;
string next_page_token = 2;
uint32 total_count = 3;
}
```
#### Record Service (`flashdns.v1.RecordService`)
```protobuf
service RecordService {
// Record CRUD
rpc CreateRecord(CreateRecordRequest) returns (Record);
rpc GetRecord(GetRecordRequest) returns (Record);
rpc UpdateRecord(UpdateRecordRequest) returns (Record);
rpc DeleteRecord(DeleteRecordRequest) returns (DeleteRecordResponse);
rpc ListRecords(ListRecordsRequest) returns (ListRecordsResponse);
// Batch operations
rpc BatchCreateRecords(BatchCreateRecordsRequest) returns (BatchCreateRecordsResponse);
rpc BatchDeleteRecords(BatchDeleteRecordsRequest) returns (BatchDeleteRecordsResponse);
// Query helpers
rpc ListRecordsByName(ListRecordsByNameRequest) returns (ListRecordsResponse);
rpc ListRecordsByType(ListRecordsByTypeRequest) returns (ListRecordsResponse);
}
message CreateRecordRequest {
string zone_id = 1;
string name = 2; // Record name (relative or FQDN)
RecordType record_type = 3;
uint32 ttl = 4; // Default: 300
RecordData data = 5;
}
message GetRecordRequest {
string record_id = 1;
}
message UpdateRecordRequest {
string record_id = 1;
optional uint32 ttl = 2;
optional RecordData data = 3;
optional bool enabled = 4;
}
message DeleteRecordRequest {
string record_id = 1;
}
message ListRecordsRequest {
string zone_id = 1;
optional string name_filter = 2; // Prefix/glob match
optional RecordType type_filter = 3;
uint32 limit = 4;
string page_token = 5;
}
message ListRecordsByNameRequest {
string zone_id = 1;
string name = 2; // Exact match
}
message ListRecordsByTypeRequest {
string zone_id = 1;
RecordType record_type = 2;
}
message BatchCreateRecordsRequest {
string zone_id = 1;
repeated CreateRecordRequest records = 2;
}
message BatchCreateRecordsResponse {
repeated Record records = 1;
repeated BatchError errors = 2; // Partial success support
}
```
### 4.2 DNS Protocol (UDP/TCP Port 53)
**Supported Query Types**:
| Type | Code | Description |
|------|------|-------------|
| A | 1 | IPv4 address |
| NS | 2 | Nameserver |
| CNAME | 5 | Canonical name |
| SOA | 6 | Start of authority |
| PTR | 12 | Pointer record |
| MX | 15 | Mail exchange |
| TXT | 16 | Text record |
| AAAA | 28 | IPv6 address |
| SRV | 33 | Service locator |
| CAA | 257 | CA Authorization |
| ANY | 255 | All records (limited) |
**Response Codes**:
| Code | Name | Description |
|------|------|-------------|
| 0 | NOERROR | Success |
| 1 | FORMERR | Format error |
| 2 | SERVFAIL | Server failure |
| 3 | NXDOMAIN | Name does not exist |
| 4 | NOTIMP | Not implemented |
| 5 | REFUSED | Query refused |
**Wire Protocol**:
- RFC 1035 compliant message format
- UDP: Max 512 bytes (without EDNS), 4096 with EDNS0
- TCP: Full message length prefix (2 bytes)
- EDNS0 support for larger responses
### 4.3 Authentication
**gRPC API**:
- aegis bearer tokens in `authorization` metadata
- mTLS for service-to-service communication
- API key header (`x-api-key`) for simple auth
**DNS Protocol**:
- No authentication (standard DNS)
- Rate limiting per source IP
- Query logging for audit
### 4.4 Client Library
```rust
use flashdns_client::FlashDnsClient;
let client = FlashDnsClient::connect("http://127.0.0.1:5300").await?;
// Create zone
let zone = client.create_zone(CreateZoneRequest {
name: "example.com.".into(),
org_id: "org-1".into(),
project_id: Some("proj-1".into()),
ns_records: vec![
"ns1.flashdns.example.com.".into(),
"ns2.flashdns.example.com.".into(),
],
..Default::default()
}).await?;
// Create A record
let record = client.create_record(CreateRecordRequest {
zone_id: zone.id.clone(),
name: "www".into(), // Becomes www.example.com.
record_type: RecordType::A,
ttl: 300,
data: RecordData::A {
address: "192.0.2.1".parse()?,
},
}).await?;
// Create MX records
client.batch_create_records(BatchCreateRecordsRequest {
zone_id: zone.id.clone(),
records: vec![
CreateRecordRequest {
name: "@".into(), // Zone apex
record_type: RecordType::MX,
ttl: 3600,
data: RecordData::MX {
preference: 10,
exchange: "mail1.example.com.".into(),
},
..Default::default()
},
CreateRecordRequest {
name: "@".into(),
record_type: RecordType::MX,
ttl: 3600,
data: RecordData::MX {
preference: 20,
exchange: "mail2.example.com.".into(),
},
..Default::default()
},
],
}).await?;
// List records
let records = client.list_records(ListRecordsRequest {
zone_id: zone.id.clone(),
type_filter: Some(RecordType::A),
limit: 100,
..Default::default()
}).await?;
```
## 5. Multi-Tenancy
### 5.1 Scope Hierarchy
```
System (platform operators)
└─ Organization (tenant boundary)
├─ Org-level zones (shared across projects)
└─ Project (workload isolation)
└─ Project-level zones
```
### 5.2 Zone Scoping
```rust
pub enum ZoneScope {
/// Zone accessible to all projects in org
Organization { org_id: String },
/// Zone scoped to specific project
Project { org_id: String, project_id: String },
}
impl Zone {
pub fn scope(&self) -> ZoneScope {
match &self.project_id {
Some(pid) => ZoneScope::Project {
org_id: self.org_id.clone(),
project_id: pid.clone()
},
None => ZoneScope::Organization {
org_id: self.org_id.clone()
},
}
}
}
```
### 5.3 Access Control Integration
```rust
// aegis action patterns for flashdns
const ACTIONS: &[&str] = &[
"flashdns:zones:create",
"flashdns:zones:get",
"flashdns:zones:list",
"flashdns:zones:update",
"flashdns:zones:delete",
"flashdns:records:create",
"flashdns:records:get",
"flashdns:records:list",
"flashdns:records:update",
"flashdns:records:delete",
];
// Resource path format
// org/{org_id}/project/{project_id}/zone/{zone_name}
// org/{org_id}/project/{project_id}/zone/{zone_name}/record/{record_id}
async fn authorize_zone_access(
iam: &IamClient,
principal: &PrincipalRef,
action: &str,
zone: &Zone,
) -> Result<()> {
let resource = ResourceRef {
kind: "zone".into(),
id: zone.name.clone(),
org_id: zone.org_id.clone(),
project_id: zone.project_id.clone().unwrap_or_default(),
..Default::default()
};
let allowed = iam.authorize(principal, action, &resource).await?;
if !allowed {
return Err(Error::AccessDenied);
}
Ok(())
}
async fn authorize_record_access(
iam: &IamClient,
principal: &PrincipalRef,
action: &str,
zone: &Zone,
record: &Record,
) -> Result<()> {
let resource = ResourceRef {
kind: "record".into(),
id: record.id.clone(),
org_id: zone.org_id.clone(),
project_id: zone.project_id.clone().unwrap_or_default(),
..Default::default()
};
let allowed = iam.authorize(principal, action, &resource).await?;
if !allowed {
return Err(Error::AccessDenied);
}
Ok(())
}
```
### 5.4 Zone Isolation
- Zones with same name can exist in different orgs/projects
- DNS queries route to correct zone based on configured delegation
- Internal resolution can scope queries by org/project context
## 6. Storage
### 6.1 ChainFire Key Schema
**Zones**:
```
flashdns/zones/{zone_id} # Zone record (by ID)
flashdns/zones/by-name/{org_id}/{zone_name} # Name lookup (org-level)
flashdns/zones/by-name/{org_id}/{project_id}/{name} # Name lookup (project-level)
flashdns/zones/by-org/{org_id}/{zone_id} # Org index
flashdns/zones/by-project/{project_id}/{zone_id} # Project index
```
**Records**:
```
flashdns/records/{record_id} # Record by ID
flashdns/records/by-zone/{zone_id}/{record_id} # Zone index
flashdns/records/by-name/{zone_id}/{name}/{type}/{id} # Name+type lookup
flashdns/records/by-type/{zone_id}/{type}/{id} # Type index
```
**Cache Metadata**:
```
flashdns/cache/zones/{zone_id}/version # Cache invalidation version
flashdns/cache/serial/{zone_id} # Current serial number
```
### 6.2 Storage Operations
```rust
#[async_trait]
pub trait ZoneStore: Send + Sync {
async fn create_zone(&self, zone: &Zone) -> Result<()>;
async fn get_zone(&self, zone_id: &str) -> Result<Option<Zone>>;
async fn get_zone_by_name(
&self,
org_id: &str,
project_id: Option<&str>,
name: &str,
) -> Result<Option<Zone>>;
async fn update_zone(&self, zone: &Zone) -> Result<()>;
async fn delete_zone(&self, zone_id: &str) -> Result<bool>;
async fn list_zones(
&self,
org_id: &str,
project_id: Option<&str>,
limit: usize,
page_token: Option<&str>,
) -> Result<(Vec<Zone>, Option<String>)>;
}
#[async_trait]
pub trait RecordStore: Send + Sync {
async fn create_record(&self, record: &Record) -> Result<()>;
async fn get_record(&self, record_id: &str) -> Result<Option<Record>>;
async fn update_record(&self, record: &Record) -> Result<()>;
async fn delete_record(&self, record_id: &str) -> Result<bool>;
async fn list_records_by_zone(
&self,
zone_id: &str,
limit: usize,
page_token: Option<&str>,
) -> Result<(Vec<Record>, Option<String>)>;
async fn find_records(
&self,
zone_id: &str,
name: &str,
record_type: Option<RecordType>,
) -> Result<Vec<Record>>;
}
```
### 6.3 Zone Cache
```rust
pub struct ZoneCache {
zones: DashMap<String, CachedZone>, // zone_id -> zone data
name_index: DashMap<String, String>, // fqdn -> zone_id
records: DashMap<String, Vec<Record>>, // zone_id:name:type -> records
config: CacheConfig,
}
pub struct CachedZone {
pub zone: Zone,
pub records: HashMap<(String, RecordType), Vec<Record>>,
pub loaded_at: u64,
pub version: u64,
}
impl ZoneCache {
/// Lookup zone by FQDN (for DNS queries)
pub fn find_zone(&self, qname: &str) -> Option<&CachedZone>;
/// Lookup records for DNS query
pub fn find_records(
&self,
zone_id: &str,
name: &str,
qtype: RecordType,
) -> Option<Vec<Record>>;
/// Invalidate zone cache on update
pub fn invalidate_zone(&self, zone_id: &str);
/// Load zone from storage into cache
pub async fn load_zone(&self, store: &dyn ZoneStore, zone_id: &str) -> Result<()>;
}
```
## 7. Configuration
### 7.1 Config File Format (TOML)
```toml
[server]
grpc_addr = "0.0.0.0:5300" # gRPC management API
dns_udp_addr = "0.0.0.0:53" # DNS UDP listener
dns_tcp_addr = "0.0.0.0:53" # DNS TCP listener
[server.tls]
cert_file = "/etc/flashdns/tls/server.crt"
key_file = "/etc/flashdns/tls/server.key"
ca_file = "/etc/flashdns/tls/ca.crt"
[storage]
backend = "chainfire" # "chainfire" | "memory"
chainfire_endpoints = ["http://chainfire-1:2379", "http://chainfire-2:2379"]
[dns]
default_ttl = 300 # Default record TTL
min_ttl = 60 # Minimum allowed TTL
max_ttl = 86400 # Maximum allowed TTL (24h)
negative_ttl = 300 # NXDOMAIN cache TTL
edns_udp_size = 4096 # EDNS0 buffer size
[dns.soa_defaults]
refresh = 7200 # 2 hours
retry = 3600 # 1 hour
expire = 1209600 # 2 weeks
minimum = 3600 # 1 hour
[dns.rate_limit]
enabled = true
queries_per_second = 1000 # Per source IP
burst = 100
[cache]
enabled = true
max_zones = 10000
max_records_per_zone = 100000
ttl_seconds = 300 # Cache refresh interval
preload_zones = true # Load all zones on startup
[iam]
endpoint = "http://aegis:9090"
service_account = "flashdns"
token_path = "/var/run/secrets/iam/token"
[logging]
level = "info"
format = "json"
```
### 7.2 Environment Variables
| Variable | Default | Description |
|----------|---------|-------------|
| `FLASHDNS_CONFIG` | - | Config file path |
| `FLASHDNS_GRPC_ADDR` | `0.0.0.0:5300` | gRPC listen address |
| `FLASHDNS_DNS_ADDR` | `0.0.0.0:53` | DNS listen address |
| `FLASHDNS_LOG_LEVEL` | `info` | Log level |
| `FLASHDNS_STORE_BACKEND` | `memory` | Storage backend |
### 7.3 CLI Arguments
```
flashdns-server [OPTIONS]
-c, --config <PATH> Config file path
--grpc-addr <ADDR> gRPC listen address
--dns-addr <ADDR> DNS listen address (UDP & TCP)
-l, --log-level <LEVEL> Log level
-h, --help Print help
-V, --version Print version
```
## 8. Security
### 8.1 Authentication
**gRPC API**:
- aegis bearer tokens for user/service authentication
- mTLS for service-to-service communication
- API key header for programmatic access
**DNS Protocol**:
- No authentication (standard DNS behavior)
- Access controlled via network policies
- Query logging for audit trail
### 8.2 Authorization
- All management operations authorized via aegis
- Zone-level and record-level permissions
- Scope enforcement (org/project boundaries)
- Owner-based access patterns supported
### 8.3 Data Security
- TLS 1.3 for gRPC transport
- DNS queries unencrypted (standard UDP/TCP)
- DNS-over-TLS planned for future
- Zone data encrypted at rest in ChainFire
### 8.4 Rate Limiting
```rust
pub struct RateLimiter {
limits: DashMap<IpAddr, TokenBucket>,
config: RateLimitConfig,
}
pub struct RateLimitConfig {
pub queries_per_second: u32,
pub burst_size: u32,
pub cleanup_interval_secs: u64,
}
impl RateLimiter {
pub fn check(&self, source_ip: IpAddr) -> bool;
pub fn record_query(&self, source_ip: IpAddr);
}
```
### 8.5 Audit
- All management API calls logged with principal, action, resource
- DNS queries logged with source IP, query name, type, response
- Integration with platform audit system
## 9. Operations
### 9.1 Deployment
**Single Node (Development)**:
```bash
flashdns-server --config config.toml
```
**Production Cluster**:
```bash
# Multiple stateless API servers behind load balancer
# DNS servers with anycast IPs
flashdns-server --config config.toml
# Shared metadata (ChainFire cluster)
# Cache preloading on startup
```
### 9.2 Monitoring
**Metrics (Prometheus)**:
| Metric | Type | Description |
|--------|------|-------------|
| `flashdns_queries_total` | Counter | Total DNS queries |
| `flashdns_queries_by_type` | Counter | Queries by record type |
| `flashdns_query_latency_seconds` | Histogram | Query response latency |
| `flashdns_responses_by_rcode` | Counter | Responses by RCODE |
| `flashdns_zones_total` | Gauge | Total zones |
| `flashdns_records_total` | Gauge | Total records |
| `flashdns_cache_hits_total` | Counter | Cache hits |
| `flashdns_cache_misses_total` | Counter | Cache misses |
| `flashdns_rate_limited_total` | Counter | Rate limited queries |
| `flashdns_grpc_requests_total` | Counter | gRPC API requests |
**Health Endpoints**:
- `GET /health` - Liveness check
- `GET /ready` - Readiness check (storage connected, cache loaded)
### 9.3 Backup & Recovery
- **Zone data**: ChainFire snapshots
- **Export**: Zone export via gRPC API (planned)
- **Import**: Bulk zone import from BIND format (planned)
## 10. Compatibility
### 10.1 API Versioning
- gRPC package: `flashdns.v1`
- Semantic versioning for breaking changes
- Backward compatible additions within major version
### 10.2 DNS Standards
| RFC | Description | Status |
|-----|-------------|--------|
| RFC 1035 | Domain Names - Implementation | Supported |
| RFC 2782 | SRV Records | Supported |
| RFC 3596 | AAAA Records | Supported |
| RFC 6891 | EDNS0 | Supported |
| RFC 8659 | CAA Records | Supported |
| RFC 4034 | DNSSEC | Planned |
| RFC 7858 | DNS over TLS | Planned |
| RFC 8484 | DNS over HTTPS | Planned |
### 10.3 Client Compatibility
Tested with:
- dig (BIND utilities)
- nslookup
- host
- drill
- kdig (Knot DNS)
## Appendix
### A. Error Codes
**gRPC Errors**:
| Error | Description |
|-------|-------------|
| ZONE_NOT_FOUND | Zone does not exist |
| RECORD_NOT_FOUND | Record does not exist |
| ZONE_ALREADY_EXISTS | Zone name already in use |
| INVALID_ZONE_NAME | Zone name format invalid |
| INVALID_RECORD_NAME | Record name format invalid |
| INVALID_RECORD_DATA | Record data invalid for type |
| ACCESS_DENIED | Permission denied |
| ZONE_NOT_EMPTY | Cannot delete zone with records |
| QUOTA_EXCEEDED | Zone/record quota exceeded |
**DNS RCODEs**:
| RCODE | Name | Description |
|-------|------|-------------|
| 0 | NOERROR | Success |
| 1 | FORMERR | Query format error |
| 2 | SERVFAIL | Server failure |
| 3 | NXDOMAIN | Name does not exist |
| 4 | NOTIMP | Query type not implemented |
| 5 | REFUSED | Query refused |
### B. Port Assignments
| Port | Protocol | Purpose |
|------|----------|---------|
| 53 | UDP | DNS queries |
| 53 | TCP | DNS queries (large responses) |
| 5300 | gRPC | Management API |
### C. Glossary
- **Zone**: A contiguous portion of the DNS namespace under single administrative control
- **Record**: A DNS resource record containing name, type, TTL, and data
- **SOA**: Start of Authority - defines zone parameters and primary nameserver
- **NS**: Nameserver record - identifies authoritative servers for a zone
- **TTL**: Time-to-live - how long resolvers should cache a record
- **FQDN**: Fully Qualified Domain Name - complete domain name ending with dot
- **RCODE**: Response code - status of DNS query response
- **EDNS**: Extension mechanisms for DNS - supports larger responses
### D. Example Zone Configuration
**Web application zone**:
```rust
// Create zone
let zone = client.create_zone(CreateZoneRequest {
name: "myapp.example.com.".into(),
org_id: "acme".into(),
project_id: Some("web-prod".into()),
..Default::default()
}).await?;
// Add records
client.batch_create_records(BatchCreateRecordsRequest {
zone_id: zone.id.clone(),
records: vec![
// A records for load balancer
CreateRecordRequest {
name: "@".into(),
record_type: RecordType::A,
ttl: 60,
data: RecordData::A { address: "203.0.113.1".parse()? },
..Default::default()
},
CreateRecordRequest {
name: "@".into(),
record_type: RecordType::A,
ttl: 60,
data: RecordData::A { address: "203.0.113.2".parse()? },
..Default::default()
},
// CNAME for www
CreateRecordRequest {
name: "www".into(),
record_type: RecordType::CNAME,
ttl: 300,
data: RecordData::CNAME { target: "myapp.example.com.".into() },
..Default::default()
},
// MX records
CreateRecordRequest {
name: "@".into(),
record_type: RecordType::MX,
ttl: 3600,
data: RecordData::MX { preference: 10, exchange: "mail.example.com.".into() },
..Default::default()
},
// TXT for SPF
CreateRecordRequest {
name: "@".into(),
record_type: RecordType::TXT,
ttl: 3600,
data: RecordData::TXT { value: "v=spf1 include:_spf.example.com ~all".into() },
..Default::default()
},
// SRV for internal service discovery
CreateRecordRequest {
name: "_http._tcp".into(),
record_type: RecordType::SRV,
ttl: 300,
data: RecordData::SRV {
priority: 0,
weight: 100,
port: 80,
target: "myapp.example.com.".into(),
},
..Default::default()
},
],
}).await?;
```
### E. Performance Considerations
- **Zone caching**: All active zones cached in memory for sub-ms queries
- **Preloading**: Zones loaded on startup to avoid cold-start latency
- **UDP preference**: Most queries served over UDP (lower overhead)
- **Connection pooling**: gRPC/ChainFire connections pooled
- **Rate limiting**: Per-IP rate limits prevent abuse
- **Batch operations**: Use batch APIs for bulk record management
## Reverse DNS Support
FlashDNS supports pattern-based reverse DNS zones, eliminating the need to create individual PTR records for each IP address in a subnet.
### Overview
Traditional reverse DNS requires creating one PTR record per IP address:
- A /24 subnet requires 256 PTR records
- A /16 subnet requires 65,536 PTR records
- A /8 subnet requires 16M+ PTR records
FlashDNS's pattern-based approach allows you to define **one ReverseZone** that dynamically generates PTR responses for all IPs within a CIDR block.
### ReverseZone API
**CreateReverseZone**
```protobuf
message CreateReverseZoneRequest {
string org_id = 1;
optional string project_id = 2;
string cidr = 3; // e.g., "192.168.0.0/16"
string ptr_pattern = 4; // e.g., "{4}-{3}.net.example.com."
uint32 ttl = 5; // Default: 3600
}
```
**GetReverseZone**
```protobuf
message GetReverseZoneRequest {
string zone_id = 1;
}
```
**DeleteReverseZone**
```protobuf
message DeleteReverseZoneRequest {
string zone_id = 1;
}
```
**ListReverseZones**
```protobuf
message ListReverseZonesRequest {
string org_id = 1;
optional string project_id = 2;
}
```
### Pattern Substitution
#### IPv4 Variables
| Variable | Description | Example (for 192.168.1.5) |
|----------|-------------|---------------------------|
| `{1}` | First octet | `192` |
| `{2}` | Second octet | `168` |
| `{3}` | Third octet | `1` |
| `{4}` | Fourth octet | `5` |
| `{ip}` | Full IP with dashes | `192-168-1-5` |
#### IPv6 Variables
| Variable | Description | Example (for 2001:db8::1) |
|----------|-------------|---------------------------|
| `{short}` | Compressed form with dashes | `2001-db8--1` |
| `{full}` | Full expanded form with dashes | `2001-0db8-0000-0000-0000-0000-0000-0001` |
### Examples
**Example 1: Subnet-based naming**
```
CIDR: 192.168.0.0/16
Pattern: {4}-{3}.net.example.com.
Query: 5.1.168.192.in-addr.arpa (PTR for 192.168.1.5)
Response: 5-1.net.example.com.
```
**Example 2: Flat namespace**
```
CIDR: 10.0.0.0/8
Pattern: host-{ip}.cloud.local.
Query: 3.2.1.10.in-addr.arpa (PTR for 10.1.2.3)
Response: host-10-1-2-3.cloud.local.
```
**Example 3: IPv6**
```
CIDR: 2001:db8::/32
Pattern: v6-{short}.example.com.
Query: (nibble-reversed ip6.arpa)
Response: v6-2001-db8-<rest>.example.com.
```
### Use Cases
1. **Data Center IP Management**: Pattern-based PTR for all VMs in a subnet
2. **ISP Customer Blocks**: Dynamic PTR without explicit record creation
3. **Development/Test Environments**: Quick reverse DNS setup without maintenance
4. **Large-Scale Deployments**: Eliminate configuration file explosion
### Longest Prefix Matching
When multiple ReverseZones overlap (e.g., /8, /16, /24), the most specific (longest prefix) match wins:
```
Zones:
- 192.0.0.0/8 → pattern A
- 192.168.0.0/16 → pattern B
- 192.168.1.0/24 → pattern C
Query: 192.168.1.5
Match: 192.168.1.0/24 (pattern C) ✓ (most specific)
```
This enables hierarchical delegation where specific subnets can override broader patterns.
### Storage Schema
- Primary key: `/flashdns/reverse_zones/{org_id}/{project_id}/{zone_id}`
- CIDR index: `/flashdns/reverse_zones/by-cidr/{normalized_cidr}` → zone_id
All three storage backends (ChainFire, FlareDB, InMemory) support reverse zones.