mdns+xnet: add test that replicates issue in cyw43439

This commit is contained in:
Patricio Whittingslow
2026-03-26 17:41:27 -03:00
parent c06a4fa99b
commit 33f492557a
4 changed files with 188 additions and 0 deletions
+22
View File
@@ -33,6 +33,28 @@ func BroadcastAddr() [6]byte {
return [6]byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff}
}
// IPv4MulticastToMAC maps an IPv4 multicast address to the corresponding
// Ethernet multicast MAC address.
//
// If ip is not in the IPv4 multicast range (224.0.0.0/4), ok is false and
// mac is zero.
func MulticastAddrFrom4(ip [4]byte) (mac [6]byte, ok bool) {
if ip[0]&0xf0 != 0xe0 {
return mac, false
}
mac[0] = 0x01
mac[1] = 0x00
mac[2] = 0x5e
// Lower 23 bits of IP
mac[3] = ip[1] & 0x7f // drop top bit
mac[4] = ip[2]
mac[5] = ip[3]
return mac, true
}
//go:generate stringer -type=Type -linecomment -output stringers.go .
type Type uint16
+7
View File
@@ -22,6 +22,7 @@ func NewFrame(buf []byte) (Frame, error) {
// Frame encapsulates the raw data of an IPv4 packet
// and provides methods for manipulating, validating and
// retreiving fields and payload data. See [RFC791].
// Below is an example of setting IPv4 fields for MDNS:
//
// [RFC791]: https://tools.ietf.org/html/rfc791
type Frame struct {
@@ -156,6 +157,12 @@ func (ifrm Frame) CRCWriteTCPPseudo(crc *lneto.CRC791) {
crc.AddUint16(uint16(ifrm.Protocol()))
}
// CRCWriteUDPPseudo writes the IPv4 UDP pseudo-header into crc for checksum
// calculation. Typical usage for computing or validating a UDP checksum:
//
// ifrm.CRCWriteUDPPseudo(&crc, ufrm.Length())
// // ufrm.SetCRC(0) // Zero before computing checksum for transmission.
// crcValue := crc.PayloadSum16(ifrm.Payload()) // For received frames, crcValue should be zero if valid.
func (ifrm Frame) CRCWriteUDPPseudo(crc *lneto.CRC791, udpLength uint16) {
crc.WriteEven(ifrm.sourceAndDestinationAddr())
crc.AddUint16(udpLength)
+30
View File
@@ -4,6 +4,19 @@ import (
"github.com/soypat/lneto/dns"
)
// IPv4MulticastAddr is the IPv4 multicast address used by mDNS (224.0.0.251).
// Defined by RFC 6762. Packets sent to this address use UDP port 5353 and are
// link-local (not routed beyond the local network segment).
func IPv4MulticastAddr() [4]byte {
return [4]byte{224, 0, 0, 251}
}
// IPv4MulticastMAC is the Ethernet multicast MAC address corresponding to
// 224.0.0.251 (01:00:5e:00:00:fb). Used for L2 delivery of mDNS over Ethernet.
func IPv4MulticastMAC() [6]byte {
return [6]byte{0x01, 0x00, 0x5e, 0x00, 0x00, 0xfb}
}
// Service describes a service to advertise via mDNS.
// A single Service produces PTR, SRV, TXT, and A resource records.
//
@@ -68,6 +81,23 @@ func matchQuestion(q *dns.Question, svc *Service) bool {
}
return false
}
func MulticastMAC(ip [4]byte) (mac [6]byte, ok bool) {
// Check IPv4 multicast range: 224.0.0.0/4
if ip[0]&0xf0 != 0xe0 {
return mac, false
}
mac[0] = 0x01
mac[1] = 0x00
mac[2] = 0x5e
// Lower 23 bits of IP
mac[3] = ip[1] & 0x7f // drop top bit
mac[4] = ip[2]
mac[5] = ip[3]
return mac, true
}
// addServiceAnswers adds the appropriate answer records for a matched question.
// It grows ans in-place, reusing existing Resource buffers when available.
+129
View File
@@ -5,9 +5,12 @@ import (
"net/netip"
"testing"
"github.com/soypat/lneto"
"github.com/soypat/lneto/dns"
"github.com/soypat/lneto/ethernet"
"github.com/soypat/lneto/ipv4"
"github.com/soypat/lneto/mdns"
"github.com/soypat/lneto/udp"
)
func TestMDNS_QueryResponse(t *testing.T) {
@@ -330,3 +333,129 @@ func mdnsQueryRespond(t *testing.T, querier, responder *StackAsync, buf []byte)
t.Fatal("querier demux:", err)
}
}
func TestMDNS_RealWorldQueries(t *testing.T) {
const MTU = 1500
responderMAC := [6]byte{0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0x01}
querierMAC := [6]byte{0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0x02}
responderIP := netip.AddrFrom4([4]byte{192, 168, 50, 81})
querierIP := netip.AddrFrom4([4]byte{192, 168, 50, 213})
mcastAddr := mdns.IPv4MulticastAddr()
// Service hosted by responder.
hostName := dns.MustNewName("server.local")
svc := mdns.Service{
Name: hostName,
Host: hostName,
Addr: []byte{192, 168, 50, 81},
Port: 80,
}
responderStack, _ := newMDNSStack(t, "responder", 1,
responderIP, responderMAC, querierMAC,
mdns.ClientConfig{
LocalPort: mdns.Port,
Services: []mdns.Service{svc},
MulticastAddr: mcastAddr[:],
},
)
pkts := []struct {
name string
qname string
qtype dns.Type
unicast bool // QU bit
}{
// Frame 1: QM multicast A server.local
{"QM_A_server", "server.local", dns.TypeA, false},
// Frame 2: QU unicast AAAA random.local
{"QU_AAAA_random", "rds-th-TH010-e6614864d3511735.local", dns.TypeAAAA, true},
// Frame 3: QU unicast A random.local
{"QU_A_random", "rds-th-TH010-e6614864d3511735.local", dns.TypeA, true},
}
var buf [MTU + ethernet.MaxOverheadSize]byte
checkNoData := func(msg string) {
t.Helper()
n, err := responderStack.Encapsulate(buf[:], -1, 0)
if err != nil {
t.Fatal(err)
} else if n != 0 {
t.Errorf(" %s: expected no data sent: %d", msg, n)
}
}
checkNoData("before transaction")
var msg dns.Message
for _, q := range pkts {
msg.Reset()
msg.AddQuestions([]dns.Question{
{
Name: dns.MustNewName(q.qname),
Type: q.qtype,
Class: withQU(q.unicast),
},
})
efrm, _ := ethernet.NewFrame(buf[:])
*efrm.DestinationHardwareAddr(), _ = ethernet.MulticastAddrFrom4(mdns.IPv4MulticastAddr())
*efrm.SourceHardwareAddr() = querierMAC
efrm.SetEtherType(ethernet.TypeIPv4)
ifrm, _ := ipv4.NewFrame(efrm.Payload())
ifrm.SetVersionAndIHL(4, 5) // No options. IHL=5, IPLEN=4*IHL=20
ifrm.SetToS(ipv4.NewToS(0, 0))
ifrm.SetTotalLength(20 + 8 + msg.Len())
ifrm.SetID(1337)
ifrm.SetFlags(ipv4.FlagDontFragment)
ifrm.SetTTL(64)
ifrm.SetProtocol(lneto.IPProtoUDP)
*ifrm.SourceAddr() = querierIP.As4()
*ifrm.DestinationAddr() = mdns.IPv4MulticastAddr()
ifrm.SetCRC(0) // Zero CRC before calculating CRC, as custom with lneto.
ifrm.SetCRC(ifrm.CalculateHeaderCRC())
ufrm, _ := udp.NewFrame(ifrm.Payload())
ufrm.SetSourcePort(mdns.Port)
ufrm.SetDestinationPort(mdns.Port)
ufrm.SetLength(8 + msg.Len())
mdnsPayload, _ := msg.AppendTo(ufrm.Payload()[:0], 0, 0)
if len(mdnsPayload) != int(msg.Len()) {
t.Fatal("unreachable")
}
var crc lneto.CRC791
ufrm.SetCRC(0)
ifrm.CRCWriteUDPPseudo(&crc, ufrm.Length())
got := crc.PayloadSum16(ifrm.Payload())
ufrm.SetCRC(got)
err := responderStack.Demux(buf[:14+20+8+msg.Len()], 0)
if err != nil {
t.Fatal(err)
}
}
n, err := responderStack.Encapsulate(buf[:], -1, 0)
if err != nil {
t.Fatal(err)
} else if n < 14+20+8+dns.SizeHeader {
t.Error("expected response", n)
}
n, err = responderStack.Encapsulate(buf[:], -1, 0)
if err != nil {
t.Fatal(err)
} else if n != 0 {
t.Error("expected single response")
}
}
func withQU(unicast bool) dns.Class {
if !unicast {
return dns.ClassINET
}
return dns.ClassINET | (1 << 15) // QU bit
}