Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 31 additions & 11 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
87 changes: 87 additions & 0 deletions client_test.go
Original file line number Diff line number Diff line change
@@ -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")
}
}