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)
+ }
+ })
+ }
+}