diff --git a/client.go b/client.go index ebe321d..13305cc 100644 --- a/client.go +++ b/client.go @@ -397,25 +397,45 @@ func (c *client) query(params *QueryParam) error { } } -// sendQuery is used to multicast a query out +// sendQuery is used to multicast a query out. +// +// Queries are written to the multicast destination (RFC 6762). Some devices +// only answer queries that originate on a multicast socket, so we send via the +// multicast connections when available. We also still write from the unicast +// sockets for environments where loopback/self-discovery relies on them. +// +// Error handling: each available socket is attempted. If any write succeeds the +// query is considered sent and nil is returned. Only when every attempted write +// fails do we surface the last write error (matching the original "fail if we +// could not send" contract without aborting mid-attempt). func (c *client) sendQuery(q *dns.Msg) error { buf, err := q.Pack() if err != nil { return err } - if c.ipv4UnicastConn != nil { - _, err = c.ipv4UnicastConn.WriteToUDP(buf, ipv4Addr) - if err != nil { - return err + + var last error + var wrote bool + tryWrite := func(conn *net.UDPConn, addr *net.UDPAddr) { + if conn == nil { + return } - } - if c.ipv6UnicastConn != nil { - _, err = c.ipv6UnicastConn.WriteToUDP(buf, ipv6Addr) - if err != nil { - return err + if _, err := conn.WriteToUDP(buf, addr); err != nil { + last = err + return } + wrote = true } - return nil + + tryWrite(c.ipv4MulticastConn, ipv4Addr) + tryWrite(c.ipv6MulticastConn, ipv6Addr) + tryWrite(c.ipv4UnicastConn, ipv4Addr) + tryWrite(c.ipv6UnicastConn, ipv6Addr) + + if wrote { + return nil + } + return last } // recv is used to receive until we get a shutdown diff --git a/client_test.go b/client_test.go new file mode 100644 index 0000000..ee87143 --- /dev/null +++ b/client_test.go @@ -0,0 +1,87 @@ +// Copyright IBM Corp. 2014, 2026 +// SPDX-License-Identifier: MIT + +package mdns + +import ( + "log" + "net" + "testing" + + "github.com/miekg/dns" +) + +func testQuestion() *dns.Msg { + m := new(dns.Msg) + m.SetQuestion("_foobar._tcp.local.", dns.TypePTR) + m.RecursionDesired = false + return m +} + +func TestSendQuery_WritesOnMulticastAndUnicast(t *testing.T) { + // Bind ephemeral UDP sockets and wire them into a client so sendQuery can + // exercise both multicast and unicast paths without requiring the real + // 5353 mDNS port. + mconn, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IPv4zero, Port: 0}) + if err != nil { + t.Fatalf("listen multicast stand-in: %v", err) + } + defer mconn.Close() + + uconn, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IPv4zero, Port: 0}) + if err != nil { + t.Fatalf("listen unicast stand-in: %v", err) + } + defer uconn.Close() + + c := &client{ + ipv4MulticastConn: mconn, + ipv4UnicastConn: uconn, + log: log.Default(), + } + + if err := c.sendQuery(testQuestion()); err != nil { + t.Fatalf("sendQuery: %v", err) + } +} + +func TestSendQuery_SucceedsIfAnySocketWrites(t *testing.T) { + // Closed connection always fails WriteToUDP; open one should still make + // sendQuery return nil (do not surface partial failures). + bad, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IPv4zero, Port: 0}) + if err != nil { + t.Fatalf("listen: %v", err) + } + _ = bad.Close() + + good, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IPv4zero, Port: 0}) + if err != nil { + t.Fatalf("listen: %v", err) + } + defer good.Close() + + c := &client{ + ipv4MulticastConn: bad, + ipv4UnicastConn: good, + log: log.Default(), + } + if err := c.sendQuery(testQuestion()); err != nil { + t.Fatalf("expected nil when at least one write succeeds, got %v", err) + } +} + +func TestSendQuery_AllWritesFail(t *testing.T) { + bad, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IPv4zero, Port: 0}) + if err != nil { + t.Fatalf("listen: %v", err) + } + _ = bad.Close() + + c := &client{ + ipv4UnicastConn: bad, + log: log.Default(), + } + if err := c.sendQuery(testQuestion()); err == nil { + t.Fatal("expected error when every write fails") + } +}