# T021: Reverse DNS Zone Model Design ## Problem Statement From PROJECT.md: > 逆引きDNSをやるためにとんでもない行数のBINDのファイルを書くというのがあり、バカバカしすぎるのでサブネットマスクみたいなものに対応すると良い Traditional reverse DNS requires creating individual PTR records for each IP address: - A /24 subnet = 256 PTR records - A /16 subnet = 65,536 PTR records - A /8 subnet = 16M+ PTR records This is operationally unsustainable. ## Solution: Pattern-Based Reverse Zones Instead of storing individual PTR records, FlashDNS will support **ReverseZone** with pattern-based PTR generation. ### Core Types ```rust /// A reverse DNS zone with pattern-based PTR generation pub struct ReverseZone { pub id: String, // UUID pub org_id: String, // Tenant org pub project_id: Option, // Optional project scope pub cidr: IpNet, // e.g., "192.168.1.0/24" or "2001:db8::/32" pub arpa_zone: String, // Auto-generated: "1.168.192.in-addr.arpa." pub ptr_pattern: String, // e.g., "{4}-{3}-{2}-{1}.hosts.example.com." pub ttl: u32, // Default TTL for generated PTRs pub created_at: u64, pub updated_at: u64, pub status: ZoneStatus, } /// Supported CIDR sizes for automatic arpa zone generation pub enum SupportedCidr { // IPv4 V4Classful8, // /8 -> x.in-addr.arpa V4Classful16, // /16 -> y.x.in-addr.arpa V4Classful24, // /24 -> z.y.x.in-addr.arpa // IPv6 V6Nibble64, // /64 -> ...ip6.arpa (16 nibbles) V6Nibble48, // /48 -> ...ip6.arpa (12 nibbles) V6Nibble32, // /32 -> ...ip6.arpa (8 nibbles) } ``` ### Pattern Substitution PTR patterns support placeholders that get substituted at query time: **IPv4 Placeholders:** - `{1}` - First octet (e.g., 192) - `{2}` - Second octet (e.g., 168) - `{3}` - Third octet (e.g., 1) - `{4}` - Fourth octet (e.g., 5) - `{ip}` - Full IP with dashes (e.g., 192-168-1-5) **IPv6 Placeholders:** - `{full}` - Full expanded address with dashes - `{short}` - Compressed representation **Examples:** | CIDR | Pattern | Query | Result | |------|---------|-------|--------| | 192.168.0.0/16 | `{4}-{3}.net.example.com.` | 5.1.168.192.in-addr.arpa | `5-1.net.example.com.` | | 10.0.0.0/8 | `host-{ip}.cloud.local.` | 5.2.1.10.in-addr.arpa | `host-10-0-1-5.cloud.local.` | | 2001:db8::/32 | `v6-{short}.example.com.` | (nibble query) | `v6-2001-db8-....example.com.` | ### CIDR to ARPA Zone Conversion ```rust /// Convert CIDR to in-addr.arpa zone name pub fn cidr_to_arpa(cidr: &IpNet) -> Result { match cidr { IpNet::V4(net) => { let octets = net.addr().octets(); match net.prefix_len() { 8 => Ok(format!("{}.in-addr.arpa.", octets[0])), 16 => Ok(format!("{}.{}.in-addr.arpa.", octets[1], octets[0])), 24 => Ok(format!("{}.{}.{}.in-addr.arpa.", octets[2], octets[1], octets[0])), _ => Err(Error::UnsupportedCidr(net.prefix_len())), } } IpNet::V6(net) => { // Convert to nibble format for ip6.arpa let nibbles = ipv6_to_nibbles(net.addr()); let prefix_nibbles = (net.prefix_len() / 4) as usize; let arpa_part = nibbles[..prefix_nibbles] .iter() .rev() .map(|n| format!("{:x}", n)) .collect::>() .join("."); Ok(format!("{}.ip6.arpa.", arpa_part)) } } } ``` ### Storage Schema ``` flashdns/reverse_zones/{zone_id} # Full zone data flashdns/reverse_zones/by-cidr/{cidr_normalized} # CIDR lookup index flashdns/reverse_zones/by-org/{org_id}/{zone_id} # Org index ``` Key format for CIDR index: Replace `/` with `_` and `.` with `-`: - `192.168.1.0/24` → `192-168-1-0_24` - `2001:db8::/32` → `2001-db8--_32` ### Query Resolution Flow ``` DNS Query: 5.1.168.192.in-addr.arpa PTR │ ▼ ┌─────────────────────────────────────┐ │ 1. Parse query → IP: 192.168.1.5 │ └─────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────┐ │ 2. Find matching ReverseZone │ │ - Check 192.168.1.0/24 │ │ - Check 192.168.0.0/16 │ │ - Check 192.0.0.0/8 │ │ (most specific match wins) │ └─────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────┐ │ 3. Apply pattern substitution │ │ Pattern: "{4}-{3}.hosts.ex.com." │ │ Result: "5-1.hosts.ex.com." │ └─────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────┐ │ 4. Return PTR response │ │ TTL from ReverseZone.ttl │ └─────────────────────────────────────┘ ``` ### API Extensions ```protobuf service ReverseZoneService { rpc CreateReverseZone(CreateReverseZoneRequest) returns (ReverseZone); rpc GetReverseZone(GetReverseZoneRequest) returns (ReverseZone); rpc DeleteReverseZone(DeleteReverseZoneRequest) returns (DeleteReverseZoneResponse); rpc ListReverseZones(ListReverseZonesRequest) returns (ListReverseZonesResponse); rpc ResolvePtrForIp(ResolvePtrForIpRequest) returns (ResolvePtrForIpResponse); } message CreateReverseZoneRequest { string org_id = 1; string project_id = 2; string cidr = 3; // "192.168.0.0/16" string ptr_pattern = 4; // "{4}-{3}.hosts.example.com." uint32 ttl = 5; // Default: 3600 } ``` ### Override Support (Optional) For cases where specific IPs need custom PTR values: ```rust pub struct PtrOverride { pub reverse_zone_id: String, pub ip: IpAddr, // Specific IP to override pub ptr_value: String, // Custom PTR (overrides pattern) } ``` Storage: `flashdns/ptr_overrides/{reverse_zone_id}/{ip_normalized}` Query resolution checks overrides first, falls back to pattern. ## Implementation Steps (T021) 1. **S1**: ReverseZone type + CIDR→arpa conversion utility (this design) 2. **S2**: ReverseZoneService gRPC + storage 3. **S3**: DNS handler integration (PTR pattern resolution) 4. **S4**: Zone transfer (AXFR) support 5. **S5**: NOTIFY on zone changes 6. **S6**: Integration tests ## Benefits | Approach | /24 Records | /16 Records | /8 Records | |----------|-------------|-------------|------------| | Traditional | 256 | 65,536 | 16M+ | | Pattern-based | 1 | 1 | 1 | Massive reduction in configuration complexity and storage requirements. ## Dependencies - `ipnet` crate for CIDR parsing - Existing FlashDNS types (Zone, Record, etc.) - hickory-proto for DNS wire format