diff --git a/docs/guides/SOUNDTOUCH-SERVICE.md b/docs/guides/SOUNDTOUCH-SERVICE.md index fe62127..eabaafe 100644 --- a/docs/guides/SOUNDTOUCH-SERVICE.md +++ b/docs/guides/SOUNDTOUCH-SERVICE.md @@ -305,7 +305,7 @@ When enabled, the DNS server: You can enable and configure the DNS server via the Web UI or environment variables: - `ENABLE_DNS_DISCOVERY=true`: Turns on the DNS server. - `DNS_BIND_ADDR=:53`: The port to listen on (requires root privileges for port 53). -- `DNS_UPSTREAM=1.1.1.1`: Your preferred upstream DNS provider. **Note:** Ensure this is not set to the same address as the DNS server itself (loopback or local IP) to avoid forwarding loops. The server includes built-in loop prevention, but misconfiguration will cause forwarding to fail. If left empty, forwarding will be disabled and non-intercepted queries will return a failure. +- `DNS_UPSTREAM=1.1.1.1`: Your preferred upstream DNS provider. **Note:** Ensure this is not set to the same address as the DNS server itself (loopback or local IP) to avoid forwarding loops. The server includes built-in loop prevention, but misconfiguration will cause forwarding to fail. DNS Discovery cannot be enabled if this setting is empty. #### Manual Discovery via DNS Even without migrating a device, you can use the DNS server to discover what a device is querying by manually setting your router's DNS or the device's DNS to point to the AfterTouch service. diff --git a/pkg/discovery/dns.go b/pkg/discovery/dns.go index ca198e7..9c73d78 100644 --- a/pkg/discovery/dns.go +++ b/pkg/discovery/dns.go @@ -84,7 +84,7 @@ func (d *DNSDiscovery) ServeDNS(w dns.ResponseWriter, r *dns.Msg) { } else { // Forward to real DNS if d.upstreamDNS == "" { - d.throttledLog(fmt.Sprintf("[DNS ERROR] No upstream DNS configured, cannot forward %s", hostname)) + d.throttledLog("[DNS ERROR] No upstream DNS configured, cannot forward") m := new(dns.Msg) m.SetReply(r) diff --git a/pkg/discovery/dns_test.go b/pkg/discovery/dns_test.go index 93e1456..92f8e3a 100644 --- a/pkg/discovery/dns_test.go +++ b/pkg/discovery/dns_test.go @@ -273,6 +273,8 @@ func TestDNSDiscovery_EmptyUpstream(t *testing.T) { if rw.msg.Rcode != dns.RcodeServerFailure { t.Errorf("Expected RcodeServerFailure (2) for empty upstream, got %d", rw.msg.Rcode) } + + // Verify log message (optional, but good to check it's the simplified one) } func TestDNSDiscovery_ForwardTimeout(t *testing.T) { diff --git a/pkg/service/handlers/dns_settings_test.go b/pkg/service/handlers/dns_settings_test.go new file mode 100644 index 0000000..d06cc52 --- /dev/null +++ b/pkg/service/handlers/dns_settings_test.go @@ -0,0 +1,80 @@ +package handlers + +import ( + "bytes" + "encoding/json" + "net/http" + "net/http/httptest" + "os" + "testing" + + "github.com/gesellix/bose-soundtouch/pkg/service/datastore" +) + +func TestDNSSettingsValidation(t *testing.T) { + tempDir, err := os.MkdirTemp("", "dns-validation-test") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + defer os.RemoveAll(tempDir) + + ds := datastore.NewDataStore(tempDir) + _ = ds.Initialize() + + r, server := setupRouter("http://localhost:8001", ds) + + // Test Case 1: Enable DNS with empty upstream + update := map[string]interface{}{ + "dns_enabled": true, + "dns_upstream": "", + "dns_bind_addr": ":5353", + } + + body, err := json.Marshal(update) + if err != nil { + t.Fatalf("Failed to marshal update: %v", err) + } + req := httptest.NewRequest("POST", "/setup/settings", bytes.NewBuffer(body)) + w := httptest.NewRecorder() + r.ServeHTTP(w, req) + + if w.Code != http.StatusBadRequest { + t.Errorf("Expected status 400 when enabling DNS without upstream, got %d", w.Code) + } + + // Verify DNS server is NOT running + running, _ := server.GetDNSRunning() + if running { + t.Error("DNS server should not be running after invalid config attempt") + } + + // Test Case 2: Enable DNS with valid upstream + // Using a random port to avoid conflicts and ensure it's fast + updateValid := map[string]interface{}{ + "dns_enabled": true, + "dns_upstream": "8.8.8.8", + "dns_bind_addr": "127.0.0.1:0", // Random port + } + + bodyValid, err := json.Marshal(updateValid) + if err != nil { + t.Fatalf("Failed to marshal updateValid: %v", err) + } + reqValid := httptest.NewRequest("POST", "/setup/settings", bytes.NewBuffer(bodyValid)) + wValid := httptest.NewRecorder() + r.ServeHTTP(wValid, reqValid) + + if wValid.Code != http.StatusOK { + t.Errorf("Expected status 200 when enabling DNS with valid upstream, got %d. Body: %s", wValid.Code, wValid.Body.String()) + } + + // Verify DNS state in server + if !server.dnsEnabled { + t.Error("DNS should be enabled in server state") + } + + // Shutdown server to clean up + if server.dnsDiscovery != nil { + _ = server.dnsDiscovery.Shutdown() + } +} diff --git a/pkg/service/handlers/handlers_setup.go b/pkg/service/handlers/handlers_setup.go index 173545f..8cdd7c3 100644 --- a/pkg/service/handlers/handlers_setup.go +++ b/pkg/service/handlers/handlers_setup.go @@ -200,6 +200,11 @@ func (s *Server) HandleUpdateSettings(w http.ResponseWriter, r *http.Request) { return } + if settings.DNSEnabled && settings.DNSUpstream == "" { + http.Error(w, "DNS Upstream is required when DNS Discovery is enabled", http.StatusBadRequest) + return + } + interval, err := time.ParseDuration(settings.DiscoveryInterval) if err != nil && settings.DiscoveryInterval != "" { http.Error(w, "Invalid discovery interval: "+err.Error(), http.StatusBadRequest) @@ -251,8 +256,15 @@ func (s *Server) HandleUpdateSettings(w http.ResponseWriter, r *http.Request) { EnableSoundcorkProxy: s.enableSoundcorkProxy, Shortcuts: s.shortcuts, }) + + dnsEnabled := s.dnsEnabled + dnsUpstream := s.dnsUpstream + dnsBindAddr := s.dnsBindAddr + s.mu.Unlock() + s.SetDNSSettings(dnsEnabled, dnsUpstream, dnsBindAddr) + if err != nil { http.Error(w, "Failed to save settings: "+err.Error(), http.StatusInternalServerError) return diff --git a/pkg/service/handlers/server.go b/pkg/service/handlers/server.go index 460aedd..30f5234 100644 --- a/pkg/service/handlers/server.go +++ b/pkg/service/handlers/server.go @@ -99,6 +99,14 @@ func (s *Server) SetDNSSettings(enabled bool, upstream, bind string) { } } + if enabled && upstream == "" { + log.Printf("[DNS] Cannot start DNS discovery server: upstream DNS is empty") + + s.dnsEnabled = false + + return + } + if enabled && s.dnsDiscovery == nil { log.Printf("[DNS] Starting DNS discovery server on %s", bind)