From 3ec357090a67ec59de5030e1d261edec8cd9cf71 Mon Sep 17 00:00:00 2001 From: Thales Maciel Date: Sun, 26 Apr 2026 18:57:27 -0300 Subject: [PATCH] daemon: doctor passes vm dns when banger itself owns the port MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous check tried to bind 127.0.0.1:42069 and warned on 'address already in use' — which is exactly the state when the banger daemon is running, the case the user ran 'doctor' to confirm. The warning was actively misleading. Now, on 'address already in use', probe the listener with a *.vm DNS query that only banger's vmdns server answers authoritatively (NXDOMAIN with Authoritative=true). If the shape matches we pass; if the port is held by something else we still warn. Tests cover both branches: a real vmdns server is accepted, and a silent UDP listener on the same port is rejected. Co-Authored-By: Claude Opus 4.7 (1M context) --- internal/daemon/capabilities.go | 29 +++++++++++++++++++++++++++- internal/daemon/capabilities_test.go | 28 +++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/internal/daemon/capabilities.go b/internal/daemon/capabilities.go index 730be93..b730591 100644 --- a/internal/daemon/capabilities.go +++ b/internal/daemon/capabilities.go @@ -7,6 +7,9 @@ import ( "net" "os" "strings" + "time" + + "github.com/miekg/dns" "banger/internal/firecracker" "banger/internal/guestconfig" @@ -282,7 +285,15 @@ func (dnsCapability) AddDoctorChecks(_ context.Context, report *system.Report) { conn, err := net.ListenPacket("udp", vmdns.DefaultListenAddr) if err != nil { if strings.Contains(strings.ToLower(err.Error()), "address already in use") { - report.AddWarn("feature vm dns", "listener address "+vmdns.DefaultListenAddr+" is already in use") + // "Already in use" is the expected state when banger's own + // daemon is running. Probe the listener with a *.vm query + // the banger DNS server is the only thing on the host + // authoritative for, and pass if the response shape matches. + if probeBangerDNS(vmdns.DefaultListenAddr) { + report.AddPass("feature vm dns", "banger DNS server is already serving "+vmdns.DefaultListenAddr) + return + } + report.AddWarn("feature vm dns", "listener address "+vmdns.DefaultListenAddr+" is held by another process") return } report.AddFail("feature vm dns", "cannot bind "+vmdns.DefaultListenAddr+": "+err.Error()) @@ -292,6 +303,22 @@ func (dnsCapability) AddDoctorChecks(_ context.Context, report *system.Report) { report.AddPass("feature vm dns", "listener can bind "+vmdns.DefaultListenAddr) } +// probeBangerDNS returns true iff a UDP DNS query to addr is answered +// by something that behaves like banger's vmdns server: a *.vm name +// produces an authoritative NXDOMAIN. Any other listener (a stub +// resolver, a different DNS server) either refuses, recurses, or +// returns non-authoritative — all distinguishable from this probe. +func probeBangerDNS(addr string) bool { + client := &dns.Client{Net: "udp", Timeout: 500 * time.Millisecond} + req := new(dns.Msg) + req.SetQuestion("doctor-probe-not-a-real-vm.vm.", dns.TypeA) + resp, _, err := client.Exchange(req, addr) + if err != nil || resp == nil { + return false + } + return resp.Authoritative && resp.Rcode == dns.RcodeNameError +} + // natCapability sets up host-side NAT so guest traffic can reach the // outside world. Needs VMService (tap lookup + aliveness) and // HostNetwork (NAT rules), plus the daemon logger for the cleanup diff --git a/internal/daemon/capabilities_test.go b/internal/daemon/capabilities_test.go index 35cc888..e1376a1 100644 --- a/internal/daemon/capabilities_test.go +++ b/internal/daemon/capabilities_test.go @@ -3,6 +3,7 @@ package daemon import ( "context" "errors" + "net" "reflect" "testing" @@ -10,6 +11,7 @@ import ( "banger/internal/guestconfig" "banger/internal/model" "banger/internal/system" + "banger/internal/vmdns" ) type testCapability struct { @@ -146,6 +148,32 @@ func TestContributeHooksPopulateGuestAndMachineConfig(t *testing.T) { } } +func TestProbeBangerDNSAcceptsRealServer(t *testing.T) { + server, err := vmdns.New("127.0.0.1:0", nil) + if err != nil { + t.Fatalf("vmdns.New: %v", err) + } + t.Cleanup(func() { _ = server.Close() }) + + if !probeBangerDNS(server.Addr()) { + t.Fatal("probeBangerDNS rejected the real banger DNS server") + } +} + +func TestProbeBangerDNSRejectsSilentListener(t *testing.T) { + // A UDP listener that drops every datagram. The probe should + // time out and return false — i.e. "this is not banger". + conn, err := net.ListenPacket("udp", "127.0.0.1:0") + if err != nil { + t.Fatalf("ListenPacket: %v", err) + } + t.Cleanup(func() { _ = conn.Close() }) + + if probeBangerDNS(conn.LocalAddr().String()) { + t.Fatal("probeBangerDNS accepted a silent non-DNS listener") + } +} + func TestDefaultCapabilitiesInOrder(t *testing.T) { d := &Daemon{} wireServices(d)