-
Notifications
You must be signed in to change notification settings - Fork 1
Batch NS glue record lookups #132
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
68311c3
6f360ef
155f13e
7a88470
b95c5bb
2a6dd29
ef41ff5
0476c93
3d9317f
5e470e2
3685e03
fd07ad8
6c4b7d6
5a12038
308164f
504cc6f
c864dce
f7323a9
7a84230
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,146 @@ | ||
| # cloudDNS - Claude Code Context | ||
|
|
||
| ## Project Overview | ||
|
|
||
| **cloudDNS** is a high-performance, authoritative and recursive DNS server written in Go (1.26.1). It implements strict RFC standards with DNSSEC signing/validation, BGP anycast integration, multi-layer caching (L1 in-memory + L2 Redis), DNS over HTTPS (DoH), IXFR zone transfers, and a REST API for management. | ||
|
|
||
| ## Architecture | ||
|
|
||
| ### Hexagonal (Ports & Adapters) Architecture | ||
|
|
||
| ```text | ||
| ┌─────────────────────────────────────────────────────────────┐ | ||
| │ cmd/ (Entry Points) │ | ||
| ├─────────────────────────────────────────────────────────────┤ | ||
| │ internal/adapters/api/ │ | ||
| │ (REST API HTTP handlers) │ | ||
| ├─────────────────────────────────────────────────────────────┤ | ||
| │ internal/core/ │ | ||
| │ ┌─────────┬──────────┬─────────┬──────────┬───────────┐ │ | ||
| │ │ domain/ │ services/ │ ports/ │ config/ │ utils/ │ │ | ||
| │ │ (ents) │ (biz log)│ (ifaces)│ (cfg) │ (util) │ │ | ||
| │ └─────────┴──────────┴─────────┴──────────┴───────────┘ │ | ||
| ├─────────────────────────────────────────────────────────────┤ | ||
| │ internal/dns/ │ | ||
| │ ┌─────────┬──────────┬─────────┬──────────┐ │ | ||
| │ │ packet/ │ server/ │ master/ │ cache/ │ │ | ||
| │ │ (wire) │ (impl) │ (xfr) │ (l1/l2) │ │ | ||
| │ └─────────┴──────────┴─────────┴──────────┘ │ | ||
| ├─────────────────────────────────────────────────────────────┤ | ||
| │ internal/adapters/repository/ │ | ||
| │ (PostgreSQL implementations) │ | ||
| ├─────────────────────────────────────────────────────────────┤ | ||
| │ internal/adapters/routing/ │ | ||
| │ (GoBGP integration) │ | ||
| └─────────────────────────────────────────────────────────────┘ | ||
| ``` | ||
|
|
||
| ## Key Packages | ||
|
|
||
| ### `cmd/clouddns/` - Main DNS server | ||
| - Entry point: `cmd/clouddns/main.go` | ||
| - Server configured via environment variables (no config files) | ||
|
|
||
| ### `internal/dns/server/` - DNS protocol implementation | ||
| - **server.go** (~2100 lines): Core `Server` struct handling UDP/TCP/DoT/DoH | ||
| - **cache.go**: L1 (sharded in-memory) and L2 (Redis) caching | ||
| - **recursive.go**: Iterative recursive resolution with root hints | ||
| - **ratelimit.go**: Token bucket rate limiting (500k req/s, burst 200k) | ||
|
|
||
| ### `internal/dns/packet/` - DNS wire format | ||
| - `DNSPacket` struct: Header, Questions, Answers, Authorities, Resources | ||
| - Supports all record types: A, AAAA, MX, TXT, CNAME, NS, SOA, PTR, SRV, CAA, DS, DNSKEY, RRSIG, NSEC, NSEC3, IXFR, AXFR, OPT, TSIG | ||
| - EDNS0 support: NSID, Cookie, Padding, EDE (RFC 8914) | ||
|
|
||
| ### `internal/core/domain/` - Domain entities | ||
| - `Zone`: id, name, role (master/slave), vpcid | ||
| - `Record`: id, zoneid, name, type, content, ttl, priority/weight/port for MX/SRV | ||
| - `UpdateOperation`: ADD, DELETE_RRSET, DELETE_ALL, DELETE_SPECIFIC | ||
| - `ZoneChange`: audit trail for zone changes | ||
|
|
||
| ### `internal/core/services/` - Business logic (10 subdirectories) | ||
| - DNSSEC signing and validation | ||
| - Recursive resolution | ||
| - Zone transfers (AXFR/IXFR) | ||
| - Dynamic updates (RFC 2136) | ||
|
|
||
| ### `internal/adapters/repository/` - PostgreSQL implementations | ||
| - Implements `ports.DNSRepository` interface | ||
|
|
||
| ## Configuration | ||
|
|
||
| All configuration via environment variables: | ||
| - `DATABASE_URL` - PostgreSQL (default: `postgres://postgres:postgres@localhost:5432/clouddns?sslmode=disable`) | ||
| - `REDIS_URL` - Redis cache | ||
| - `DNS_ADDR` - DNS bind address (default: `127.0.0.1:1053`; uses 1053 instead of privileged port 53) | ||
| - `API_ADDR` - Management API bind (default: `:8080`) | ||
| - `LOG_LEVEL`, `LOG_FORMAT` | ||
| - `DNSSEC_MODE` - `disabled`, `ad-bit-only`, `strict` | ||
| - `ANYCAST_*` / `BGP_*` - Anycast/BGP configuration | ||
| - `TRUST_ANCHOR_<zone>` - Base64-encoded DNSSEC trust anchors | ||
|
|
||
| ## Build & Deploy | ||
|
|
||
| ### Build | ||
| - `go build -o clouddns-bin cmd/clouddns/main.go` | ||
| - Docker multi-stage: `golang:1.26-alpine` builder → `alpine:3.20` runtime | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Stale Go image tag in Build & Deploy section. Line 86 references 📝 Proposed fix- - Docker multi-stage: `golang:1.26-alpine` builder → `alpine:3.20` runtime
+ - Docker multi-stage: `golang:1.26.1-alpine` builder → `alpine:3.20` runtime🤖 Prompt for AI Agents |
||
| - Statically linked with `CGO_ENABLED=0` | ||
|
|
||
| ### Test | ||
| ```bash | ||
| go test -short -timeout 5m ./... | ||
| go test -v -timeout 10m -coverprofile=coverage.txt $(go list ./... | grep -v "top1m-import") | ||
| ``` | ||
| - Coverage threshold: 80% minimum | ||
|
|
||
| ### Deploy | ||
| - ~~GitHub Actions: lint → test → build → push to GCP Artifact Registry → GKE deployment~~ | ||
| - **Note:** GKE deployment is disabled — we outgrew the gcloud subscription and no longer use deploy workflows | ||
| - Ports: 1053/udp, 1053/tcp, 8080/tcp, 853/tcp | ||
|
|
||
| ## Query Flow | ||
|
|
||
| 1. Rate limit check | ||
| 2. Parse packet (`request.FromBuffer()`) | ||
| 3. Cache check (L1 → L2) | ||
| 4. EDNS0 processing | ||
| 5. Zone lookup (traverse domain labels) | ||
| 6. Record resolution (direct or wildcard) | ||
| 7. NXDOMAIN → SOA + NSEC/NSEC3 proofs if DNSSEC | ||
| 8. Recursive fallback (if `RecursionEnabled` and RD bit set) | ||
| 9. DNSSEC signing (if DO bit set) | ||
| 10. DNSSEC validation (if validator configured) | ||
| 11. Padding (RFC 7830/8467) | ||
| 12. Truncation (if response > maxSize) | ||
| 13. Cache result | ||
| 14. Send response | ||
|
|
||
| ## Important Files | ||
|
|
||
| - `internal/dns/server/server.go` - Main server implementation | ||
| - `internal/dns/packet/packet.go` - DNS packet parsing | ||
| - `internal/dns/server/cache.go` - Multi-layer cache | ||
| - `internal/dns/server/recursive.go` - Recursive resolver | ||
| - `internal/core/ports/ports.go` - Repository interface definition | ||
| - `internal/core/domain/dns.go` - Domain entities | ||
| - `infra/k8s/deployment.yaml` - Kubernetes deployment | ||
| - `.github/workflows/go.yml` - CI pipeline | ||
|
|
||
| ## Documentation | ||
|
|
||
| - `README.md` - Project overview | ||
| - `features.md` - Feature list | ||
| - `docs/dnssec.md` - DNSSEC documentation | ||
| - `docs/decisions/` - Architecture Decision Records (ADRs) | ||
|
|
||
| ## Design Decisions (ADRs) | ||
|
|
||
| 1. **0001** - Hexagonal architecture | ||
| 2. **0002** - Anycast/BGP integration | ||
| 3. **0003** - Distributed cache invalidation | ||
| 4. **0004** - API authentication and RBAC | ||
| 5. **0005** - Smart engine GSLB health checks | ||
| 6. **0006** - Incremental zone transfer (IXFR) | ||
| 7. **0007** - CAA record support | ||
| 8. **0008** - DNSSEC validation | ||
| 9. **0009** - Multi-algorithm DNSSEC | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -103,6 +103,84 @@ func (r *PostgresRepository) GetRecords(ctx context.Context, name string, qType | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return records, nil | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // GetRecordsByNames returns records for multiple names with a single query. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Used for batch-fetching glue records to avoid N+1 queries. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func (r *PostgresRepository) GetRecordsByNames(ctx context.Context, names []string, qType domain.RecordType, clientIP string) (map[string][]domain.Record, error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if len(names) == 0 { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return nil, nil | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Build query: WHERE LOWER(r.name) IN (LOWER($1), LOWER($2), ...) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| placeholders := make([]string, len(names)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| args := make([]interface{}, len(names)+2) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| args[0] = clientIP | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for i, name := range names { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| placeholders[i] = fmt.Sprintf("LOWER($%d)", i+2) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| args[i+1] = name | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| query := fmt.Sprintf(`SELECT r.id, r.zone_id, r.name, r.type, r.content, r.ttl, r.priority, r.weight, r.port, r.network, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| r.health_check_type, r.health_check_target, COALESCE(h.status, 'UNKNOWN') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| FROM dns_records r | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| LEFT JOIN record_health h ON r.id = h.record_id | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| WHERE LOWER(r.name) IN (%s) AND (r.network IS NULL OR $1::inet <<= r.network)`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| strings.Join(placeholders, ",")) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if qType != "" { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| query += fmt.Sprintf(` AND r.type = $%d`, len(names)+2) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| args = append(args, string(qType)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+115
to
+131
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fix the placeholder/argument indexing before calling Line 115 preallocates one extra element in Proposed fix- placeholders := make([]string, len(names))
- args := make([]interface{}, len(names)+2)
- args[0] = clientIP
+ placeholders := make([]string, len(names))
+ args := make([]interface{}, 0, len(names)+2)
+ args = append(args, clientIP)
for i, name := range names {
placeholders[i] = fmt.Sprintf("LOWER($%d)", i+2)
- args[i+1] = name
+ args = append(args, name)
}
@@
if qType != "" {
- query += fmt.Sprintf(` AND r.type = $%d`, len(names)+2)
+ query += fmt.Sprintf(` AND r.type = $%d`, len(args)+1)
args = append(args, string(qType))
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| rows, errQuery := r.db.QueryContext(ctx, query, args...) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if errQuery != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return nil, errQuery | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| defer func() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if errClose := rows.Close(); errClose != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| log.Printf("failed to close rows: %v", errClose) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| result := make(map[string][]domain.Record) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for rows.Next() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var rec domain.Record | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var priority, weight, port sql.NullInt32 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var hcType, hcTarget, hStatus sql.NullString | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if errScan := rows.Scan(&rec.ID, &rec.ZoneID, &rec.Name, &rec.Type, &rec.Content, &rec.TTL, &priority, &weight, &port, &rec.Network, &hcType, &hcTarget, &hStatus); errScan != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return nil, errScan | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if priority.Valid { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| p := int(priority.Int32) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| rec.Priority = &p | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if weight.Valid { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| w := int(weight.Int32) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| rec.Weight = &w | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if port.Valid { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| p := int(port.Int32) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| rec.Port = &p | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if hcType.Valid { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| rec.HealthCheckType = domain.HealthCheckType(hcType.String) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if hcTarget.Valid { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| rec.HealthCheckTarget = hcTarget.String | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if hStatus.Valid { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| rec.HealthStatus = domain.HealthStatus(hStatus.String) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Normalize key with trailing dot to match ConvertDomainToPacketRecord behavior | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| key := rec.Name | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if !strings.HasSuffix(key, ".") { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| key += "." | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| result[key] = append(result[key], rec) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+173
to
+178
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Key the result map by the requested/canonical name, not The SQL match is case-insensitive, but Lines 174-178 use the stored record name as the lookup key. If the caller asks for Proposed fix+ requestedKeys := make(map[string]string, len(names))
+ for _, name := range names {
+ key := strings.TrimSuffix(strings.ToLower(name), ".") + "."
+ if _, exists := requestedKeys[key]; !exists {
+ requestedKeys[key] = key
+ }
+ }
+
result := make(map[string][]domain.Record)
for rows.Next() {
@@
- key := rec.Name
- if !strings.HasSuffix(key, ".") {
- key += "."
- }
+ key := strings.TrimSuffix(strings.ToLower(rec.Name), ".") + "."
+ if requestedKey, ok := requestedKeys[key]; ok {
+ key = requestedKey
+ }
result[key] = append(result[key], rec)
}🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return result, rows.Err() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // GetIPsForName implements ports.DNSRepository. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func (r *PostgresRepository) GetIPsForName(ctx context.Context, name string, clientIP string) ([]string, error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Optimized query returning only content for Type A | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.