From 8f2364d15e066e182d302899a6e0c3a00016cca4 Mon Sep 17 00:00:00 2001 From: Solaris-star <820622658@qq.com> Date: Tue, 21 Jul 2026 18:11:42 +0800 Subject: [PATCH 1/2] fix: also send mDNS queries on multicast sockets sendQuery only wrote to the multicast destination from unicast UDP sockets. Some devices only answer queries that originate on a multicast socket (RFC 6762). Send via ipv4MulticastConn / ipv6MulticastConn as well, while keeping the existing unicast writes so local loopback discovery tests still pass. Fixes #144 --- client.go | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/client.go b/client.go index ebe321d..c7d0b86 100644 --- a/client.go +++ b/client.go @@ -397,25 +397,38 @@ 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. func (c *client) sendQuery(q *dns.Msg) error { buf, err := q.Pack() if err != nil { return err } + var last error + if c.ipv4MulticastConn != nil { + if _, err = c.ipv4MulticastConn.WriteToUDP(buf, ipv4Addr); err != nil { + last = err + } + } + if c.ipv6MulticastConn != nil { + if _, err = c.ipv6MulticastConn.WriteToUDP(buf, ipv6Addr); err != nil { + last = err + } + } if c.ipv4UnicastConn != nil { - _, err = c.ipv4UnicastConn.WriteToUDP(buf, ipv4Addr) - if err != nil { - return err + if _, err = c.ipv4UnicastConn.WriteToUDP(buf, ipv4Addr); err != nil { + last = err } } if c.ipv6UnicastConn != nil { - _, err = c.ipv6UnicastConn.WriteToUDP(buf, ipv6Addr) - if err != nil { - return err + if _, err = c.ipv6UnicastConn.WriteToUDP(buf, ipv6Addr); err != nil { + last = err } } - return nil + return last } // recv is used to receive until we get a shutdown From e58ea28fc668036218346d53cd699fc5fd58eb57 Mon Sep 17 00:00:00 2001 From: Solaris-star <820622658@qq.com> Date: Wed, 22 Jul 2026 08:54:48 +0800 Subject: [PATCH 2/2] fix: clarify sendQuery error handling and add unit tests Address review feedback: - Do not return a partial write error if any socket succeeds - Only surface an error when every attempted write fails - Add unit tests covering multicast+unicast writes and partial failure --- client.go | 41 ++++++++++++++---------- client_test.go | 87 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 111 insertions(+), 17 deletions(-) create mode 100644 client_test.go diff --git a/client.go b/client.go index c7d0b86..13305cc 100644 --- a/client.go +++ b/client.go @@ -398,35 +398,42 @@ func (c *client) query(params *QueryParam) error { } // 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 +// +// 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 } + var last error - if c.ipv4MulticastConn != nil { - if _, err = c.ipv4MulticastConn.WriteToUDP(buf, ipv4Addr); err != nil { - last = err + var wrote bool + tryWrite := func(conn *net.UDPConn, addr *net.UDPAddr) { + if conn == nil { + return } - } - if c.ipv6MulticastConn != nil { - if _, err = c.ipv6MulticastConn.WriteToUDP(buf, ipv6Addr); err != nil { - last = err - } - } - if c.ipv4UnicastConn != nil { - if _, err = c.ipv4UnicastConn.WriteToUDP(buf, ipv4Addr); err != nil { + if _, err := conn.WriteToUDP(buf, addr); err != nil { last = err + return } + wrote = true } - if c.ipv6UnicastConn != nil { - if _, err = c.ipv6UnicastConn.WriteToUDP(buf, ipv6Addr); err != nil { - last = err - } + + tryWrite(c.ipv4MulticastConn, ipv4Addr) + tryWrite(c.ipv6MulticastConn, ipv6Addr) + tryWrite(c.ipv4UnicastConn, ipv4Addr) + tryWrite(c.ipv6UnicastConn, ipv6Addr) + + if wrote { + return nil } return last } 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") + } +}