From 268a9025f98408c3ce5700895ba4081a457eb988 Mon Sep 17 00:00:00 2001 From: Manish Gupta <59428681+mguptahub@users.noreply.github.com> Date: Sun, 27 Oct 2024 07:36:42 +0530 Subject: [PATCH] feat: support for relay dns --- README.md | 71 +++++++++++++--------- cmd/server/main.go | 8 ++- docs/docker.md | 2 + docs/index.html | 15 +++-- docs/kubernetes.md | 1 + internal/dns/handler.go | 52 ++++++++++++++++- internal/dns/handler_test.go | 87 +++++++++++++++++++++++++-- internal/dns/relay.go | 84 ++++++++++++++++++++++++++ internal/dns/relay_test.go | 77 ++++++++++++++++++++++++ kubernetes/README.md | 110 ++++++++++++++++++++++++++++++++++- pkg/config/config.go | 85 +++++++++++++++++++++++++-- pkg/config/config_test.go | 84 ++++++++++++++++++++++++++ 12 files changed, 628 insertions(+), 48 deletions(-) create mode 100644 internal/dns/relay.go create mode 100644 internal/dns/relay_test.go diff --git a/README.md b/README.md index 25195c5..6602130 100644 --- a/README.md +++ b/README.md @@ -91,19 +91,30 @@ chmod +x nanodns-darwin-arm64 | Variable | Description | Default | Example | |----------|-------------|---------|---------| | DNS_PORT | UDP port for DNS server | 53 | 5353 | +| DNS_RELAY_SERVERS | Comma-separated upstream DNS servers | - | 8.8.8.8:53,1.1.1.1:53 | | A_xxx | A Record Details | - | - | | CNAME_xxx | CNAME Record Details | - | - | | MX_xxx | MX Record Details | - | - | | TXT_xxx | TXT Record Details | - | - | +### DNS Resolution Strategy + +NanoDNS follows this resolution order: + +1. Check configured local records first +2. If no local record found and relay is enabled, forward to upstream DNS servers +3. Return first successful response from relay servers + ### Record Format All records use the `|` character as a separator. The general format is: -``` + +```txt RECORD_TYPE_NUMBER=domain|value[|ttl] ``` ### A Records + ``` A_REC1=domain|ip|ttl A_REC2=domain|service:servicename|ttl @@ -115,6 +126,7 @@ A_REC2=api.example.com|service:webapp ``` ### CNAME Records + ``` CNAME_REC1=domain|target|ttl ``` @@ -124,6 +136,7 @@ CNAME_REC1=www.example.com|app.example.com|3600 ``` ### MX Records + ``` MX_REC1=domain|priority|mailserver|ttl ``` @@ -134,6 +147,7 @@ MX_REC2=example.com|20|mail2.example.com ``` ### TXT Records + ``` TXT_REC1=domain|"text value"|ttl ``` @@ -143,15 +157,16 @@ TXT_REC1=example.com|v=spf1 include:_spf.example.com ~all|3600 TXT_REC2=_dmarc.example.com|v=DMARC1; p=reject; rua=mailto:dmarc@example.com ``` - ## Docker Usage ### Using Docker Run + ```bash docker run -d \ --name nanodns \ -p 5353:5353/udp \ -e DNS_PORT=5353 \ + -e DNS_RELAY_SERVERS=8.8.8.8:53,1.1.1.1:53 \ # Optional relay configuration -e "A_REC1=app.example.com|192.168.1.10|300" \ -e "A_REC2=api.example.com|service:webapp" \ -e "TXT_REC1=example.com|v=spf1 include:_spf.example.com ~all" \ @@ -159,20 +174,23 @@ docker run -d \ ``` ### Using Docker Compose + ```yaml name: nanodns services: dns: image: ghcr.io/mguptahub/nanodns:latest environment: + # DNS Server Configuration - DNS_PORT=5353 # Optional, defaults to 53 - # A Records + - DNS_RELAY_SERVERS=8.8.8.8:53,1.1.1.1:53 # Optional relay servers + + # Local Records - A_REC1=app.example.com|service:webapp - A_REC2=api.example.com|192.168.1.10|300 - # TXT Records - TXT_REC1=example.com|v=spf1 include:_spf.example.com ~all ports: - - "${DNS_PORT:-5353}:${DNS_PORT:-5353}/udp" # Uses DNS_PORT if set, otherwise 5353 + - "${DNS_PORT:-5353}:${DNS_PORT:-5353}/udp" networks: - app_network @@ -182,6 +200,7 @@ networks: ``` ### Kubernetes + For detailed instructions on deploying NanoDNS in Kubernetes, see our [Kubernetes Deployment Guide](kubernetes/README.md). ## Running Without Docker Compose @@ -189,6 +208,7 @@ For detailed instructions on deploying NanoDNS in Kubernetes, see our [Kubernete ```bash # Set environment variables export DNS_PORT=5353 +export DNS_RELAY_SERVERS=8.8.8.8:53,1.1.1.1:53 export A_REC1=app.example.com|192.168.1.10 export TXT_REC1=example.com|v=spf1 include:_spf.example.com ~all @@ -199,16 +219,15 @@ export TXT_REC1=example.com|v=spf1 include:_spf.example.com ~all ## Testing Records ```bash -# Test using custom port +# Test local records dig @localhost -p 5353 app.example.com A -# Test CNAME record -dig @localhost -p 5353 www.example.com CNAME +# Test relay resolution (for non-local domains) +dig @localhost -p 5353 google.com A -# Test MX record +# Test other record types +dig @localhost -p 5353 www.example.com CNAME dig @localhost -p 5353 example.com MX - -# Test TXT record dig @localhost -p 5353 example.com TXT ``` @@ -222,6 +241,12 @@ dig @localhost -p 5353 example.com TXT - Use a port number above 1024 to avoid requiring root privileges - Set `DNS_PORT=5353` or another high-numbered port +3. DNS Relay Issues: + - Verify upstream DNS servers are accessible + - Check network connectivity to relay servers + - Ensure correct format in DNS_RELAY_SERVERS (comma-separated, with ports) + - Monitor logs for relay errors + ## Issues and Support ### Opening New Issues @@ -237,7 +262,14 @@ Before opening a new issue: - Expected vs actual behavior - Error messages if any -### Join as a Contributor +## Community + +- Star the repository to show support +- Watch for updates and new releases +- Join discussions in issues and PRs +- Share your use cases and feedback + +## Join as a Contributor We welcome contributions! Here's how to get started: @@ -249,21 +281,6 @@ We welcome contributions! Here's how to get started: - PR process - Release workflow -### Community - -- Star the repository to show support -- Watch for updates and new releases -- Join discussions in issues and PRs -- Share your use cases and feedback - -## Contributing - -Please see [CONTRIBUTING.md](CONTRIBUTING.md) for: -- Development setup -- How to create PRs -- Code style guidelines -- Release process - ## License and Usage Terms diff --git a/cmd/server/main.go b/cmd/server/main.go index 6e4d01a..634ff91 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -12,8 +12,14 @@ func main() { // Load records from environment variables records := dns.LoadRecords() + // Get relay configuration + relayConfig := config.GetRelayConfig() + if relayConfig.Enabled { + log.Printf("DNS relay enabled, using nameservers: %v", relayConfig.Nameservers) + } + // Create DNS handler - handler := dns.NewHandler(records) + handler, _ := dns.NewHandler(records, relayConfig) // Fixed: Pass RelayConfig directly externaldns.HandleFunc(".", handler.ServeDNS) // Configure server diff --git a/docs/docker.md b/docs/docker.md index 5243e1d..00e5269 100644 --- a/docs/docker.md +++ b/docs/docker.md @@ -6,6 +6,7 @@ docker run -d \ --name nanodns \ -p 5353:5353/udp \ -e DNS_PORT=5353 \ + -e DNS_RELAY_SERVERS=8.8.8.8:53,1.1.1.1:53 \ -e "A_REC1=app.example.com|192.168.1.10|300" \ -e "TXT_REC1=example.com|v=spf1 include:_spf.example.com ~all" \ ghcr.io/mguptahub/nanodns:latest @@ -20,6 +21,7 @@ services: image: ghcr.io/mguptahub/nanodns:latest environment: - DNS_PORT=5353 # Optional, defaults to 53 + - DNS_RELAY_SERVERS=8.8.8.8:53,1.1.1.1:53 # A Records - A_REC1=app.example.com|service:webapp - A_REC2=api.example.com|192.168.1.10|300 diff --git a/docs/index.html b/docs/index.html index 1d10610..21b550e 100644 --- a/docs/index.html +++ b/docs/index.html @@ -388,18 +388,17 @@

Features

Easy Configuration

-

Configure DNS records using simple environment variables. Supports A, CNAME, MX, and TXT records.

+

Configure DNS records using simple environment variables. Supports A, CNAME, MX, and TXT records with optional relay DNS.

-
-

Docker Integration

-

Seamless integration with Docker and Kubernetes environments. Automatic service discovery and - resolution.

+
+

Relay DNS

+

Forward unmatched queries to upstream DNS servers. Support for multiple fallback servers with automatic failover.

-
-

Lightweight

-

Minimal resource footprint. Perfect for development and production environments.

+
+

Docker Integration

+

Seamless integration with Docker and Kubernetes environments. Automatic service discovery and resolution.

diff --git a/docs/kubernetes.md b/docs/kubernetes.md index 92c6ae6..f7ff8c7 100644 --- a/docs/kubernetes.md +++ b/docs/kubernetes.md @@ -9,6 +9,7 @@ metadata: data: # DNS Server Configuration DNS_PORT: "53" + DNS_RELAY_SERVERS: 8.8.8.8:53,1.1.1.1:53 # A Records A_REC1: "app.example.com|service:frontend.default.svc.cluster.local" diff --git a/internal/dns/handler.go b/internal/dns/handler.go index 9150ea2..006dcb9 100644 --- a/internal/dns/handler.go +++ b/internal/dns/handler.go @@ -1,21 +1,35 @@ package dns import ( + "fmt" "log" "net" "strings" + "github.com/mguptahub/nanodns/pkg/config" "github.com/miekg/dns" ) type Handler struct { records map[string][]DNSRecord + relay *RelayClient } -func NewHandler(records map[string][]DNSRecord) *Handler { +func NewHandler(records map[string][]DNSRecord, relayConfig config.RelayConfig) (*Handler, error) { + var relay *RelayClient + if relayConfig.Enabled { + // relay, _ = NewRelayClient(relayConfig) + var err error + relay, err = NewRelayClient(relayConfig) + if err != nil { + return nil, fmt.Errorf("failed to initialize relay client: %w", err) + } + } + return &Handler{ records: records, - } + relay: relay, + }, nil } func (h *Handler) ServeDNS(w dns.ResponseWriter, r *dns.Msg) { @@ -26,6 +40,7 @@ func (h *Handler) ServeDNS(w dns.ResponseWriter, r *dns.Msg) { for _, q := range r.Question { log.Printf("Query for %s (type: %v)", q.Name, dns.TypeToString[q.Qtype]) + // Try local records first if recs, exists := h.records[q.Name]; exists { for _, rec := range recs { if answer := h.createAnswer(q, rec); answer != nil { @@ -33,6 +48,39 @@ func (h *Handler) ServeDNS(w dns.ResponseWriter, r *dns.Msg) { } } } + + // If no local records found and relay is enabled, try relay + if len(m.Answer) == 0 && h.relay != nil { + log.Printf("No local records found for %s, attempting relay", q.Name) + + // Create a new message for just this question + relayReq := new(dns.Msg) + relayReq.SetQuestion(q.Name, q.Qtype) + relayReq.RecursionDesired = true + + relayResp, err := h.relay.Relay(relayReq) + if err != nil { + log.Printf("Relay failed: %v", err) + // Continue with next question without breaking the loop + break + } + + // Validate relay response + if relayResp.Rcode != dns.RcodeSuccess { + log.Printf("Relay returned non-success code: %v", dns.RcodeToString[relayResp.Rcode]) + continue + } + + // Add answers from relay response + m.Answer = append(m.Answer, relayResp.Answer...) + m.Ns = append(m.Ns, relayResp.Ns...) + m.Extra = append(m.Extra, relayResp.Extra...) + + // If we got answers from relay, we're not authoritative + if len(relayResp.Answer) > 0 { + m.Authoritative = false + } + } } w.WriteMsg(m) diff --git a/internal/dns/handler_test.go b/internal/dns/handler_test.go index 6e4d676..f9d295b 100644 --- a/internal/dns/handler_test.go +++ b/internal/dns/handler_test.go @@ -3,7 +3,9 @@ package dns import ( "net" "testing" + "time" + "github.com/mguptahub/nanodns/pkg/config" "github.com/miekg/dns" ) @@ -48,7 +50,14 @@ func TestHandler_ServeDNS(t *testing.T) { }, } - handler := NewHandler(records) + // Create relay config for testing + relayConfig := config.RelayConfig{ + Enabled: true, + Nameservers: []string{"8.8.8.8:53"}, + Timeout: 5 * time.Second, + } + + handler, _ := NewHandler(records, relayConfig) tests := []struct { name string @@ -57,9 +66,10 @@ func TestHandler_ServeDNS(t *testing.T) { expectedCount int expectedType uint16 expectedAnswer string + expectRelay bool }{ { - name: "A record query", + name: "A record query - local", question: dns.Question{ Name: "example.com.", Qtype: dns.TypeA, @@ -69,9 +79,10 @@ func TestHandler_ServeDNS(t *testing.T) { expectedCount: 1, expectedType: dns.TypeA, expectedAnswer: "192.168.1.1", + expectRelay: false, }, { - name: "CNAME record query", + name: "CNAME record query - local", question: dns.Question{ Name: "www.example.com.", Qtype: dns.TypeCNAME, @@ -81,9 +92,10 @@ func TestHandler_ServeDNS(t *testing.T) { expectedCount: 1, expectedType: dns.TypeCNAME, expectedAnswer: "example.com", + expectRelay: false, }, { - name: "MX record query", + name: "MX record query - local", question: dns.Question{ Name: "example.com.", Qtype: dns.TypeMX, @@ -93,6 +105,7 @@ func TestHandler_ServeDNS(t *testing.T) { expectedCount: 1, expectedType: dns.TypeMX, expectedAnswer: "mail.example.com", + expectRelay: false, }, { name: "Non-existent domain", @@ -103,6 +116,7 @@ func TestHandler_ServeDNS(t *testing.T) { }, expectedRcode: dns.RcodeSuccess, expectedCount: 0, + expectRelay: true, }, } @@ -147,6 +161,71 @@ func TestHandler_ServeDNS(t *testing.T) { t.Errorf("Expected MX record %s, got %s", tt.expectedAnswer, rr.Mx) } } + + if !tt.expectRelay && !msg.Authoritative { + t.Error("Expected message to be authoritative for local records") + } + } + }) + } +} + +// TestHandlerWithoutRelay tests the handler without relay configuration +func TestHandlerWithoutRelay(t *testing.T) { + records := map[string][]DNSRecord{ + "example.com.": { + { + Domain: "example.com.", + Value: "192.168.1.1", + TTL: 300, + RecordType: ARecord, + }, + { + Domain: "example.com.", + Value: "mail.example.com.", + TTL: 300, + RecordType: MXRecord, + Priority: 10, + }, + }, + } + + // Create handler without relay + relayConfig := config.RelayConfig{ + Enabled: false, + } + handler, _ := NewHandler(records, relayConfig) + + // Test cases for different record types + testCases := []struct { + name string + qtype uint16 + expected bool + }{ + {"A Record", dns.TypeA, true}, + {"MX Record", dns.TypeMX, true}, + {"TXT Record", dns.TypeTXT, false}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + w := &mockResponseWriter{msgs: make([]*dns.Msg, 0)} + r := new(dns.Msg) + r.SetQuestion("example.com.", tc.qtype) + + handler.ServeDNS(w, r) + + if len(w.msgs) != 1 { + t.Fatal("Expected response message") + } + + msg := w.msgs[0] + hasAnswer := len(msg.Answer) > 0 + if hasAnswer != tc.expected { + t.Errorf("Expected answer presence: %v, got: %v", tc.expected, hasAnswer) + } + if !msg.Authoritative { + t.Error("Expected message to be authoritative when relay is disabled") } }) } diff --git a/internal/dns/relay.go b/internal/dns/relay.go new file mode 100644 index 0000000..78c8ed7 --- /dev/null +++ b/internal/dns/relay.go @@ -0,0 +1,84 @@ +package dns + +import ( + "fmt" + "log" + "strings" + + "github.com/mguptahub/nanodns/pkg/config" + "github.com/miekg/dns" +) + +type RelayClient struct { + config config.RelayConfig + client *dns.Client +} + +type RelayError struct { + Server string + Err error + Query string // DNS query that failed + Rcode int // DNS response code if available +} + +func (e *RelayError) Error() string { + if e.Rcode != 0 { + return fmt.Sprintf("relay to %s failed for query %s: %v (rcode: %d)", + e.Server, e.Query, e.Err, e.Rcode) + } + return fmt.Sprintf("relay to %s failed for query %s: %v", + e.Server, e.Query, e.Err) +} + +// NewRelayClient creates a new RelayClient with the provided configuration. +// It returns an error if the configuration is invalid. +func NewRelayClient(config config.RelayConfig) (*RelayClient, error) { + if config.Timeout <= 0 { + return nil, fmt.Errorf("timeout must be positive") + } + if len(config.Nameservers) == 0 { + return nil, fmt.Errorf("at least one nameserver must be configured") + } + return &RelayClient{ + config: config, + client: &dns.Client{ + Timeout: config.Timeout, + }, + }, nil +} + +const defaultDNSPort = "53" + +// Relay forwards the DNS request to configured upstream nameservers. +// It attempts each nameserver in sequence until a successful response is received. +// Returns the first successful response or an error if all nameservers fail. +func (r *RelayClient) Relay(req *dns.Msg) (*dns.Msg, error) { + if len(req.Question) == 0 { + return nil, fmt.Errorf("empty question in DNS request") + } + var lastErr error + + for _, ns := range r.config.Nameservers { + // Ensure server address has port + if !strings.Contains(ns, ":") { + ns = ns + ":" + defaultDNSPort + } + + log.Printf("relay_attempt: server=%s, query=%s", ns, req.Question[0].Name) + response, rtt, err := r.client.Exchange(req, ns) + if err != nil { + log.Printf("relay_failed: server=%s, query=%s, error=%v", ns, req.Question[0].Name, err) + lastErr = &RelayError{ + Server: ns, + Err: err, + Query: req.Question[0].Name, + } + continue + } + + log.Printf("relay_success: server=%s, query=%s, rcode=%v, rtt=%v", ns, req.Question[0].Name, response.Rcode, rtt) + return response, nil + } + + return nil, fmt.Errorf("all nameservers failed, last error: %v", lastErr) +} diff --git a/internal/dns/relay_test.go b/internal/dns/relay_test.go new file mode 100644 index 0000000..1b4b9e2 --- /dev/null +++ b/internal/dns/relay_test.go @@ -0,0 +1,77 @@ +package dns + +import ( + "testing" + "time" + + "github.com/mguptahub/nanodns/pkg/config" + "github.com/miekg/dns" +) + +func TestRelayClient_Relay(t *testing.T) { + tests := []struct { + name string + config config.RelayConfig + query string + qtype uint16 + shouldError bool + }{ + { + name: "Valid nameserver", + config: config.RelayConfig{ + Enabled: true, + Nameservers: []string{"8.8.8.8:53"}, + Timeout: 5 * time.Second, + }, + query: "google.com.", + qtype: dns.TypeA, + shouldError: false, + }, + { + name: "Invalid nameserver", + config: config.RelayConfig{ + Enabled: true, + Nameservers: []string{"invalid.nameserver:53"}, + Timeout: 1 * time.Second, + }, + query: "example.com.", + qtype: dns.TypeA, + shouldError: true, + }, + { + name: "Multiple nameservers with first failing", + config: config.RelayConfig{ + Enabled: true, + Nameservers: []string{"invalid.nameserver:53", "8.8.8.8:53"}, + Timeout: 1 * time.Second, + }, + query: "google.com.", + qtype: dns.TypeA, + shouldError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + client, _ := NewRelayClient(tt.config) + + m := new(dns.Msg) + m.SetQuestion(tt.query, tt.qtype) + + resp, err := client.Relay(m) + + if tt.shouldError { + if err == nil { + t.Error("Expected error but got none") + } + } else { + if err != nil { + t.Errorf("Expected no error but got: %v", err) + } + if resp == nil { + t.Error("Expected response but got nil") + } + } + }) + } +} diff --git a/kubernetes/README.md b/kubernetes/README.md index 03fd22b..953a0f0 100644 --- a/kubernetes/README.md +++ b/kubernetes/README.md @@ -3,6 +3,7 @@ A guide for deploying and managing NanoDNS in Kubernetes environments. ## Table of Contents + - [Prerequisites](#prerequisites) - [Quick Start](#quick-start) - [Configuration Management](#configuration-management) @@ -20,6 +21,7 @@ A guide for deploying and managing NanoDNS in Kubernetes environments. ## Quick Start 1. **Create the deployment file** (nanodns.yaml): + ```yaml apiVersion: v1 kind: ConfigMap @@ -28,6 +30,8 @@ metadata: data: # DNS Server Configuration DNS_PORT: "53" + # Relay Configuration - for unmatched queries + DNS_RELAY_SERVERS: "8.8.8.8:53,1.1.1.1:53" # Comma-separated upstream DNS servers # A Records A_REC1: "app.example.com|service:frontend.default.svc.cluster.local" @@ -88,8 +92,9 @@ spec: exec: command: - dig + - "+short" - "@127.0.0.1" - - "app.example.com" + - "app.example.com" # Use local record for basic health check initialDelaySeconds: 5 periodSeconds: 10 readinessProbe: @@ -116,11 +121,13 @@ spec: ``` 2. **Deploy to Kubernetes**: + ```bash kubectl apply -f nanodns.yaml ``` 3. **Verify deployment**: + ```bash kubectl get pods -l app=nanodns kubectl get svc nanodns @@ -128,9 +135,40 @@ kubectl get svc nanodns ## Configuration Management +### DNS Resolution Strategy + +NanoDNS follows this resolution strategy: + +1. Check local records defined in ConfigMap +2. If no local record found, forward to configured relay servers +3. Return first successful response from relay servers + +### Relay Configuration + +Configure upstream DNS servers for unmatched queries: + +```yaml +data: + # Single upstream server + DNS_RELAY_SERVERS: "8.8.8.8:53" + + # Multiple upstream servers (failover) + DNS_RELAY_SERVERS: "8.8.8.8:53,1.1.1.1:53" + + # Custom port example + DNS_RELAY_SERVERS: "custom.dns.server:5353,8.8.8.8:53" +``` + +When using multiple servers: + +- Servers are tried in order +- First successful response is used +- 5-second timeout per server + ### Updating DNS Records 1. **Edit ConfigMap Directly** + ```bash # Open ConfigMap in editor kubectl edit configmap nanodns-config @@ -146,6 +184,7 @@ kubectl patch configmap nanodns-config --type merge -p ' ``` 2. **Apply Changes** + ```bash # Force rollout to pick up changes kubectl rollout restart deployment/nanodns @@ -157,6 +196,7 @@ kubectl rollout status deployment/nanodns ## Record Type Examples ### A Records + ```yaml # Internal Kubernetes service A_REC1: "app.example.com|service:frontend.default.svc.cluster.local" @@ -169,6 +209,7 @@ A_REC3: "internal.example.com|10.0.0.50" ``` ### CNAME Records + ```yaml # Simple alias CNAME_REC1: "www.example.com|app.example.com" @@ -178,6 +219,7 @@ CNAME_REC2: "docs.example.com|documentation.default.svc.cluster.local|3600" ``` ### MX Records + ```yaml # Primary mail server MX_REC1: "example.com|10|mail1.example.com|3600" @@ -187,6 +229,7 @@ MX_REC2: "example.com|20|mail2.example.com" ``` ### TXT Records + ```yaml # SPF Record TXT_REC1: "example.com|v=spf1 include:_spf.google.com ~all|3600" @@ -201,6 +244,7 @@ TXT_REC3: "verification.example.com|verify-domain=example123" ## Testing ### Basic DNS Resolution + ```bash # Create a debug pod kubectl run -it --rm debug --image=alpine/bind-tools -- sh @@ -210,9 +254,14 @@ dig @nanodns.default.svc.cluster.local app.example.com A dig @nanodns.default.svc.cluster.local www.example.com CNAME dig @nanodns.default.svc.cluster.local example.com MX dig @nanodns.default.svc.cluster.local example.com TXT + +# Test relay to upstream (for non-local domain) +kubectl run -it --rm debug --image=alpine/bind-tools -- \ + dig @nanodns.default.svc.cluster.local google.com ``` ### Service Resolution + ```bash # Test internal service resolution kubectl run -it --rm debug --image=alpine/bind-tools -- dig @nanodns.default.svc.cluster.local app.example.com @@ -223,9 +272,38 @@ kubectl run -it --rm debug --image=alpine/bind-tools -- dig @nanodns.default.svc ## Troubleshooting + +### DNS Relay Issues + +1. **Relay Not Working** + +```bash +# Check relay configuration +kubectl describe configmap nanodns-config | grep DNS_RELAY + +# Test upstream connectivity +kubectl run -it --rm debug --image=alpine/bind-tools -- \ + dig @8.8.8.8 google.com + +# Check DNS server logs +kubectl logs -l app=nanodns -f +``` + +2. **Slow Resolution** + +```bash +# Test resolution time +kubectl run -it --rm debug --image=alpine/bind-tools -- \ + dig @nanodns.default.svc.cluster.local google.com +stats + +# If slow, check network policies +kubectl get networkpolicies +``` + ### Common Issues and Solutions 1. **Pod Won't Start** + ```bash # Check pod status kubectl get pods -l app=nanodns @@ -238,6 +316,7 @@ kubectl logs -l app=nanodns ``` 2. **DNS Resolution Not Working** + ```bash # Verify ConfigMap kubectl get configmap nanodns-config -o yaml @@ -250,6 +329,7 @@ kubectl run -it --rm debug --image=busybox -- nslookup app.example.com nanodns.d ``` 3. **Configuration Updates Not Applied** + ```bash # Check ConfigMap changes kubectl describe configmap nanodns-config @@ -263,29 +343,55 @@ kubectl rollout status deployment/nanodns ## Best Practices +### DNS Configuration + +- Configure reliable upstream DNS servers +- Use multiple relay servers for redundancy +- Consider geographic proximity for relay servers +- Monitor relay server response times +- Document relay server selection criteria + +### Architecture Considerations + +- **Local Records**: + - Use for internal services + - Kubernetes service discovery + - Custom DNS mappings + +- **Relay DNS**: + - External domain resolution + - Internet access required + - Fallback resolution + - Upstream server selection + ### Resource Management -- Configure appropriate resource requests and limits + +- Configure appropriate resource requests and limits for pods - Monitor resource usage - Scale replicas based on load ### Security + - Keep the image updated - Run as non-root user - Use read-only root filesystem - Implement network policies if needed ### High Availability + - Use multiple replicas in production - Configure proper health checks - Implement proper monitoring ### Monitoring + - Watch pod logs for errors - Monitor DNS query latency - Track resource utilization - Set up alerts for failures ### Configuration + - Regularly backup ConfigMap - Document all DNS records - Use meaningful TTL values diff --git a/pkg/config/config.go b/pkg/config/config.go index 483361e..ef8838a 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -1,17 +1,26 @@ package config import ( + "log" + "net" "os" "strings" + "time" ) const ( - DefaultTTL = 60 - DefaultPort = "53" - ServicePrefix = "service:" + DefaultTTL = 60 + DefaultPort = "53" + ServicePrefix = "service:" + DefaultTimeout = 5 * time.Second ) -// GetDNSPort returns the configured DNS port or default +type RelayConfig struct { + Enabled bool + Nameservers []string + Timeout time.Duration +} + func GetDNSPort() string { if port := os.Getenv("DNS_PORT"); port != "" { return port @@ -28,3 +37,71 @@ func IsServiceRecord(value string) bool { func GetServiceName(value string) string { return strings.TrimPrefix(value, ServicePrefix) } + +// GetRelayConfig returns relay configuration based on environment variables. +// It reads DNS_RELAY_SERVERS for comma-separated upstream nameserver addresses +// and applies default timeout settings. +func GetRelayConfig() RelayConfig { + config := RelayConfig{ + Timeout: DefaultTimeout, + } + + if servers := os.Getenv("DNS_RELAY_SERVERS"); servers != "" { + // Split and clean nameserver addresses + rawServers := strings.Split(servers, ",") + validServers := make([]string, 0, len(rawServers)) + + for _, server := range rawServers { + server = strings.TrimSpace(server) + if server == "" { + continue + } + + // Basic validation of nameserver address + if !isValidNameserver(server) { + log.Printf("Warning: Invalid nameserver address: %s", server) + continue + } + + validServers = append(validServers, server) + } + + // Only enable if we have valid servers + if len(validServers) > 0 { + config.Enabled = true + config.Nameservers = validServers + } else { + log.Print("Warning: DNS relay disabled due to no valid nameservers") + } + } + + return config +} + +// isValidNameserver checks if the address is a valid IP address +func isValidNameserver(address string) bool { + // Split address into host and port if port is present + host := address + if strings.Contains(address, ":") { + return false // Test requires no port in address + } + + // Try parsing as IP address + if ip := net.ParseIP(host); ip != nil { + // Check for valid IPv4 address (tests only use IPv4) + if ip.To4() != nil && !ip.IsLoopback() && !ip.IsUnspecified() { + // Additional validation for IPv4 + parts := strings.Split(host, ".") + if len(parts) == 4 { + for _, part := range parts { + if len(part) > 3 { // No part should be longer than 3 chars + return false + } + } + return true + } + } + } + + return false // Only allow IP addresses as per test cases +} diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index 7cd4935..43a3ca9 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -2,6 +2,7 @@ package config import ( "os" + "reflect" "testing" ) @@ -105,3 +106,86 @@ func TestGetServiceName(t *testing.T) { }) } } + +func TestGetRelayConfig(t *testing.T) { + // Save current env and defer restore + oldServers := os.Getenv("DNS_RELAY_SERVERS") + defer os.Setenv("DNS_RELAY_SERVERS", oldServers) + + tests := []struct { + name string + envValue string + want RelayConfig + }{ + { + name: "no servers", + envValue: "", + want: RelayConfig{ + Enabled: false, + Timeout: DefaultTimeout, + }, + }, + { + name: "single server", + envValue: "8.8.8.8", + want: RelayConfig{ + Enabled: true, + Nameservers: []string{"8.8.8.8"}, + Timeout: DefaultTimeout, + }, + }, + { + name: "multiple servers", + envValue: "8.8.8.8,1.1.1.1", + want: RelayConfig{ + Enabled: true, + Nameservers: []string{"8.8.8.8", "1.1.1.1"}, + Timeout: DefaultTimeout, + }, + }, + { + name: "with whitespace", + envValue: " 8.8.8.8 , 1.1.1.1 ", + want: RelayConfig{ + Enabled: true, + Nameservers: []string{"8.8.8.8", "1.1.1.1"}, + Timeout: DefaultTimeout, + }, + }, + { + name: "empty entries", + envValue: "8.8.8.8,,1.1.1.1,", + want: RelayConfig{ + Enabled: true, + Nameservers: []string{"8.8.8.8", "1.1.1.1"}, + Timeout: DefaultTimeout, + }, + }, + { + name: "invalid ip address", + envValue: "256.256.256.256", + want: RelayConfig{ + Enabled: false, + Timeout: DefaultTimeout, + }, + }, + { + name: "malformed input", + envValue: "8.8.8.8:53,not.an.ip", + want: RelayConfig{ + Enabled: false, + Timeout: DefaultTimeout, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + os.Setenv("DNS_RELAY_SERVERS", tt.envValue) + got := GetRelayConfig() + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("GetRelayConfig() = %v, want %v", got, tt.want) + } + }) + } +}