package discovery import ( "log" "net" "strings" "testing" "time" "github.com/miekg/dns" ) func TestDNSDiscovery_Interception(t *testing.T) { serviceIP := "192.168.1.100" upstreamDNS := "8.8.8.8" d := NewDNSDiscovery(upstreamDNS, serviceIP) // Test intercepting Bose service m := new(dns.Msg) m.SetQuestion("api.bose.com.", dns.TypeA) rw := &mockResponseWriter{} d.ServeDNS(rw, m) if rw.msg == nil { t.Fatal("Expected a response message, got nil") } if len(rw.msg.Answer) == 0 { t.Fatal("Expected an answer in the response") } if a, ok := rw.msg.Answer[0].(*dns.A); ok { if a.A.String() != serviceIP { t.Errorf("Expected intercepted IP %s, got %s", serviceIP, a.A.String()) } } else { t.Errorf("Expected A record, got %T", rw.msg.Answer[0]) } // Test aftertouch.test m2 := new(dns.Msg) m2.SetQuestion("aftertouch.test.", dns.TypeA) rw2 := &mockResponseWriter{} d.ServeDNS(rw2, m2) if rw2.msg == nil || len(rw2.msg.Answer) == 0 { t.Fatal("Expected response for aftertouch.test") } if a, ok := rw2.msg.Answer[0].(*dns.A); ok { if a.A.String() != serviceIP { t.Errorf("Expected intercepted IP %s for aftertouch.test, got %s", serviceIP, a.A.String()) } } else { t.Errorf("Expected A record for aftertouch.test, got %T", rw2.msg.Answer[0]) } } func TestDNSDiscovery_Forwarding(t *testing.T) { // This test is harder because it needs a real upstream or a mock. // For now, let's just test that it calls forward and record. serviceIP := "192.168.1.100" upstreamDNS := "127.0.0.1:5353" // Use a port that is likely closed or we can mock d := NewDNSDiscovery(upstreamDNS, serviceIP) m := new(dns.Msg) m.SetQuestion("google.com.", dns.TypeA) rw := &mockResponseWriter{} // Start a mock upstream DNS server mux := dns.NewServeMux() mux.HandleFunc("google.com.", func(w dns.ResponseWriter, r *dns.Msg) { m := new(dns.Msg) m.SetReply(r) _ = w.WriteMsg(m) }) ts := &dns.Server{Addr: "127.0.0.1:5353", Net: "udp", Handler: mux, ReadTimeout: 100 * time.Millisecond, WriteTimeout: 100 * time.Millisecond} go func() { _ = ts.ListenAndServe() }() defer func() { _ = ts.Shutdown() }() // Give it a moment to start time.Sleep(100 * time.Millisecond) // We expect forward to succeed d.ServeDNS(rw, m) d.mu.RLock() host, exists := d.discovered["google.com"] d.mu.RUnlock() if !exists { t.Error("Expected google.com to be recorded in discovery") } if host.IsBoseService { t.Error("google.com should not be identified as a Bose service") } } func TestDNSDiscovery_StartTCP(t *testing.T) { serviceIP := "192.168.1.100" upstreamDNS := "8.8.8.8" d := NewDNSDiscovery(upstreamDNS, serviceIP) addr := "127.0.0.1:5354" go func() { _ = d.Start(addr) }() // Give it a moment to start time.Sleep(200 * time.Millisecond) // Test TCP resolution m := new(dns.Msg) m.SetQuestion("api.bose.com.", dns.TypeA) c := new(dns.Client) c.Net = "tcp" in, _, err := c.Exchange(m, addr) if err != nil { t.Fatalf("Failed to exchange via TCP: %v", err) } if len(in.Answer) == 0 { t.Fatal("Expected answer in TCP response") } if a, ok := in.Answer[0].(*dns.A); ok { if a.A.String() != serviceIP { t.Errorf("Expected intercepted IP %s via TCP, got %s", serviceIP, a.A.String()) } } else { t.Errorf("Expected A record via TCP, got %T", in.Answer[0]) } // Test Shutdown err = d.Shutdown() if err != nil { t.Errorf("Shutdown failed: %v", err) } // Verify it's really shut down by trying to connect _, _, err = c.Exchange(m, addr) if err == nil { t.Error("Expected error after shutdown, but could still exchange") } } func TestDNSDiscovery_IsRunning(t *testing.T) { serviceIP := "192.168.1.100" upstreamDNS := "8.8.8.8" d := NewDNSDiscovery(upstreamDNS, serviceIP) addr := "127.0.0.1:5355" if d.IsRunning(addr) { t.Error("Expected IsRunning to be false before Start") } go func() { _ = d.Start(addr) }() // Give it a moment to start time.Sleep(200 * time.Millisecond) if !d.IsRunning(addr) { t.Error("Expected IsRunning to be true after Start") } if d.IsRunning("127.0.0.1:9999") { t.Error("Expected IsRunning to be false for wrong address") } _ = d.Shutdown() if d.IsRunning(addr) { t.Error("Expected IsRunning to be false after Shutdown") } } type mockResponseWriter struct { msg *dns.Msg } func (m *mockResponseWriter) LocalAddr() net.Addr { return nil } func (m *mockResponseWriter) RemoteAddr() net.Addr { return nil } func (m *mockResponseWriter) WriteMsg(msg *dns.Msg) error { m.msg = msg; return nil } func (m *mockResponseWriter) Write([]byte) (int, error) { return 0, nil } func (m *mockResponseWriter) Close() error { return nil } func (m *mockResponseWriter) TsigStatus() error { return nil } func (m *mockResponseWriter) TsigTimersOnly(bool) {} func (m *mockResponseWriter) Hijack() {} func TestDNSDiscovery_LogThrottling(t *testing.T) { d := NewDNSDiscovery("8.8.8.8", "192.168.1.100") // Capture log output var logBuf strings.Builder oldOutput := log.Writer() log.SetOutput(&logBuf) defer log.SetOutput(oldOutput) msg := "Test log message" d.throttledLog(msg) d.throttledLog(msg) d.throttledLog(msg) count := strings.Count(logBuf.String(), msg) if count != 1 { t.Errorf("Expected log message to appear once due to throttling, but appeared %d times", count) } // Advance time by 11 seconds to bypass throttling d.lastLogMu.Lock() d.lastLog[msg] = time.Now().Add(-11 * time.Second) d.lastLogMu.Unlock() d.throttledLog(msg) count = strings.Count(logBuf.String(), msg) if count != 2 { t.Errorf("Expected log message to appear twice after advancing time, but appeared %d times", count) } } func TestDNSDiscovery_LoopPrevention(t *testing.T) { serviceIP := "192.168.1.100" bindAddr := "127.0.0.1:53" upstreamDNS := "127.0.0.1:53" d := NewDNSDiscovery(upstreamDNS, serviceIP) d.bindAddr = bindAddr // Capture log output to avoid panic if it's being throttled/logged var logBuf strings.Builder oldOutput := log.Writer() log.SetOutput(&logBuf) defer log.SetOutput(oldOutput) m := new(dns.Msg) m.SetQuestion("google.com.", dns.TypeA) rw := &mockResponseWriter{} d.forward(rw, m) if rw.msg == nil { t.Fatal("Expected a response message") } if rw.msg.Rcode != dns.RcodeServerFailure { t.Errorf("Expected RcodeServerFailure (2), got %d", rw.msg.Rcode) } } func TestDNSDiscovery_EmptyUpstream(t *testing.T) { serviceIP := "192.168.1.100" upstreamDNS := "" // Empty upstream d := NewDNSDiscovery(upstreamDNS, serviceIP) d.bindAddr = ":53" m := new(dns.Msg) m.SetQuestion("google.com.", dns.TypeA) rw := &mockResponseWriter{} d.ServeDNS(rw, m) if rw.msg == nil { t.Fatal("Expected a response message, got nil") } 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) { serviceIP := "192.168.1.100" // Use an IP that is unroutable or doesn't exist on the network to ensure timeout upstreamDNS := "192.0.2.1:53" // TEST-NET-1, usually non-routable d := NewDNSDiscovery(upstreamDNS, serviceIP) m := new(dns.Msg) m.SetQuestion("google.com.", dns.TypeA) rw := &mockResponseWriter{} start := time.Now() d.forward(rw, m) duration := time.Since(start) if duration < 2*time.Second { t.Errorf("Expected forward to take at least 2 seconds (timeout), but took %v", duration) } if rw.msg == nil || rw.msg.Rcode != dns.RcodeServerFailure { t.Errorf("Expected RcodeServerFailure after timeout") } }