# 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<String>,    // 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<String, Error> {
    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::<Vec<_>>()
                .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
