Files
lneto/x/xnet/xnet_mdns_test.go
T
Pat Whittingslow a2970b923d add ipv6 to xnet.StackAsync (#107)
* add ipv6 to xnet.StackAsync

* dns improvements

* improve DNS workings of StackAsync

* add tentative ICMPv6

* work on prefixes and fix some small bugs, plan UDP/TCP6

* fix bugs in StackAsync and ipv4.Prefix.Contains

* update arpsubtable

* completely remove legacy internet.StackIP for StackIPv4/v6

* ipv4/ipv6 tcp/udp

* add TCP6/UDP6 dialing APIs

* add xnet.Stack6 interface

* more ipv6 integration into StackAsync; various tweaks to lneto and documentation+TODOs

* add stack6 tests

* replace netip.Prefix with ipv4.Prefix where it makes sense
2026-05-13 15:31:18 -03:00

462 lines
12 KiB
Go

package xnet
import (
"encoding/binary"
"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) {
const MTU = ethernet.MaxMTU
svcName, err := dns.NewName("My Web._http._tcp.local")
if err != nil {
t.Fatal(err)
}
hostName, err := dns.NewName("mydevice.local")
if err != nil {
t.Fatal(err)
}
svcType, err := dns.NewName("_http._tcp.local")
if err != nil {
t.Fatal(err)
}
svc := mdns.Service{
Name: svcName,
Host: hostName,
Addr: []byte{192, 168, 1, 50},
Port: 80,
}
responderAddr := [4]byte{192, 168, 1, 50}
responderMAC := [6]byte{0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0x01}
querierAddr := [4]byte{192, 168, 1, 100}
querierMAC := [6]byte{0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0x02}
mcastAddr := []byte{224, 0, 0, 251}
// Setup responder stack with mDNS service.
responderStack := new(StackAsync)
err = responderStack.Reset(StackConfig{
Hostname: "responder",
RandSeed: 1234,
StaticAddress4: responderAddr,
HardwareAddress: responderMAC,
MTU: MTU,
MaxActiveUDPPorts: 1,
AcceptMulticast: true,
})
if err != nil {
t.Fatal("responder reset:", err)
}
responderStack.SetGatewayHardwareAddr(querierMAC)
var responderClient mdns.Client
err = responderClient.Configure(mdns.ClientConfig{
LocalPort: mdns.Port,
Services: []mdns.Service{svc},
MulticastAddr: mcastAddr,
})
if err != nil {
t.Fatal("responder configure:", err)
}
err = responderStack.RegisterUDP4(&responderClient, mcastAddr, mdns.Port)
if err != nil {
t.Fatal("responder register:", err)
}
// Setup querier stack.
querierStack := new(StackAsync)
err = querierStack.Reset(StackConfig{
Hostname: "querier",
RandSeed: 5678,
StaticAddress4: querierAddr,
HardwareAddress: querierMAC,
MTU: MTU,
MaxActiveUDPPorts: 1,
AcceptMulticast: true,
})
if err != nil {
t.Fatal("querier reset:", err)
}
querierStack.SetGatewayHardwareAddr(responderMAC)
var querierClient mdns.Client
err = querierClient.Configure(mdns.ClientConfig{
LocalPort: mdns.Port,
MulticastAddr: mcastAddr,
})
if err != nil {
t.Fatal("querier configure:", err)
}
err = querierClient.StartResolve(mdns.ResolveConfig{
Questions: []dns.Question{{
Name: svcType,
Type: dns.TypePTR,
Class: dns.ClassINET,
}},
MaxResponseAnswers: 4,
})
if err != nil {
t.Fatal("start resolve:", err)
}
err = querierStack.RegisterUDP4(&querierClient, mcastAddr, mdns.Port)
if err != nil {
t.Fatal("querier register:", err)
}
const carrierDataSize = MTU + ethernet.MaxOverheadSize
var buf [carrierDataSize]byte
// Querier encapsulates query through full stack (Ethernet+IP+UDP+mDNS).
n, err := querierStack.EgressEthernet(buf[:])
if err != nil || n == 0 {
t.Fatal("querier encapsulate:", err, n)
}
// Verify mDNS query wire format at DNS layer.
const ethHdrLen = 14
ipIHL := int(buf[ethHdrLen]&0x0f) * 4
dnsStart := ethHdrLen + ipIHL + 8
dnsFrame, err := dns.NewFrame(buf[dnsStart:n])
if err != nil {
t.Fatal("parse query dns frame:", err)
}
if dnsFrame.TxID() != 0 {
t.Errorf("mDNS query txid=%d, want 0", dnsFrame.TxID())
}
if dnsFrame.Flags() != 0 {
t.Errorf("mDNS query flags=%d, want 0", dnsFrame.Flags())
}
// Responder demuxes the query (multicast MAC+IP accepted via AcceptMulticast).
err = responderStack.IngressEthernet(buf[:n])
if err != nil {
t.Fatal("responder demux:", err)
}
// Responder encapsulates response.
n, err = responderStack.EgressEthernet(buf[:])
if err != nil || n == 0 {
t.Fatal("responder encapsulate:", err, n)
}
// Verify response DNS flags.
ipIHL = int(buf[ethHdrLen]&0x0f) * 4
dnsStart = ethHdrLen + ipIHL + 8
dnsFrame, err = dns.NewFrame(buf[dnsStart:n])
if err != nil {
t.Fatal("parse response dns frame:", err)
}
flags := dnsFrame.Flags()
if !flags.IsResponse() {
t.Error("mDNS response missing QR bit")
}
if !flags.IsAuthorativeAnswer() {
t.Error("mDNS response missing AA bit")
}
if dnsFrame.ANCount() == 0 {
t.Fatal("mDNS response has 0 answers")
}
// Querier demuxes response.
err = querierStack.IngressEthernet(buf[:n])
if err != nil {
t.Fatal("querier demux:", err)
}
// Read answers.
var answers [4]dns.Resource
nans, done, err := querierClient.AnswersCopyTo(answers[:])
if err != nil {
t.Fatal("answers:", err)
}
if !done {
t.Fatal("expected done")
}
if nans == 0 {
t.Fatal("got 0 answers")
}
// Verify PTR answer points to our service instance name.
ptrData := answers[0].RawData()
var ptrTarget dns.Name
_, err = ptrTarget.Decode(ptrData, 0)
if err != nil {
t.Fatal("decode PTR target:", err)
}
if !dns.NamesEqual(ptrTarget, svcName) {
t.Errorf("PTR target=%q, want %q", ptrTarget.String(), svcName.String())
}
}
func TestMDNS_SRVThroughStack(t *testing.T) {
const MTU = ethernet.MaxMTU
svcName, err := dns.NewName("My Web._http._tcp.local")
if err != nil {
t.Fatal(err)
}
hostName, err := dns.NewName("mydevice.local")
if err != nil {
t.Fatal(err)
}
svc := mdns.Service{
Name: svcName,
Host: hostName,
Addr: []byte{192, 168, 1, 50},
Port: 80,
}
mcastAddr := []byte{224, 0, 0, 251}
responderMAC := [6]byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x01}
querierMAC := [6]byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x02}
// Create responder.
responderStack, _ := newMDNSStack(t, "responder", 1111,
netip.AddrFrom4([4]byte{192, 168, 1, 50}), responderMAC, querierMAC,
mdns.ClientConfig{LocalPort: mdns.Port, Services: []mdns.Service{svc}, MulticastAddr: mcastAddr},
)
// Create querier.
querierStack, querierClient := newMDNSStack(t, "querier", 2222,
netip.AddrFrom4([4]byte{192, 168, 1, 100}), querierMAC, responderMAC,
mdns.ClientConfig{LocalPort: mdns.Port, MulticastAddr: mcastAddr},
)
err = querierClient.StartResolve(mdns.ResolveConfig{
Questions: []dns.Question{{
Name: svcName,
Type: dns.TypeSRV,
Class: dns.ClassINET,
}},
MaxResponseAnswers: 4,
})
if err != nil {
t.Fatal("start resolve:", err)
}
// Full round-trip through both stacks.
var buf [MTU + ethernet.MaxOverheadSize]byte
mdnsQueryRespond(t, querierStack, responderStack, buf[:])
var answers [4]dns.Resource
nans, done, err := querierClient.AnswersCopyTo(answers[:])
if err != nil || !done {
t.Fatal("expected done:", err)
}
if nans < 2 {
t.Fatalf("expected at least 2 answers (SRV+A), got %d", nans)
}
// Verify SRV port.
srvData := answers[0].RawData()
if len(srvData) < 6 {
t.Fatalf("SRV data too short: %d", len(srvData))
}
gotPort := binary.BigEndian.Uint16(srvData[4:6])
if gotPort != svc.Port {
t.Errorf("SRV port=%d, want %d", gotPort, svc.Port)
}
// Verify A record.
aData := answers[1].RawData()
if [4]byte(aData) != [4]byte(svc.Addr) {
t.Errorf("A record addr=%v, want %v", aData, svc.Addr)
}
}
// newMDNSStack creates a StackAsync with an mDNS client registered on its UDP ports.
func newMDNSStack(t *testing.T, hostname string, seed int64,
addr netip.Addr, mac, gatewayMAC [6]byte,
mdnsCfg mdns.ClientConfig,
) (*StackAsync, *mdns.Client) {
t.Helper()
const MTU = ethernet.MaxMTU
stack := new(StackAsync)
err := stack.Reset(StackConfig{
Hostname: hostname,
RandSeed: seed,
StaticAddress4: addr.As4(),
HardwareAddress: mac,
MTU: MTU,
MaxActiveUDPPorts: 1,
AcceptMulticast: true,
})
if err != nil {
t.Fatal(hostname, "reset:", err)
}
stack.SetGatewayHardwareAddr(gatewayMAC)
var client mdns.Client
err = client.Configure(mdnsCfg)
if err != nil {
t.Fatal(hostname, "mdns configure:", err)
}
err = stack.RegisterUDP4(&client, mdnsCfg.MulticastAddr, mdns.Port)
if err != nil {
t.Fatal(hostname, "register udp:", err)
}
return stack, &client
}
// mdnsQueryRespond performs a full Ethernet+IP+UDP+mDNS query→response cycle
// between two stacks with AcceptMulticast enabled.
func mdnsQueryRespond(t *testing.T, querier, responder *StackAsync, buf []byte) {
t.Helper()
// Querier encapsulates query.
n, err := querier.EgressEthernet(buf)
if err != nil || n == 0 {
t.Fatal("querier encapsulate:", err, n)
}
// Responder demuxes multicast query directly.
err = responder.IngressEthernet(buf[:n])
if err != nil {
t.Fatal("responder demux:", err)
}
// Responder encapsulates response.
n, err = responder.EgressEthernet(buf)
if err != nil || n == 0 {
t.Fatal("responder encapsulate:", err, n)
}
// Querier demuxes multicast response.
err = querier.IngressEthernet(buf[:n])
if err != nil {
t.Fatal("querier demux:", err)
}
}
func TestMDNS_RealWorldQueries(t *testing.T) {
const MTU = ethernet.MaxMTU
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.EgressEthernet(buf[:])
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.IngressEthernet(buf[:14+20+8+msg.Len()])
if err != nil {
t.Fatal(err)
}
}
n, err := responderStack.EgressEthernet(buf[:])
if err != nil {
t.Fatal(err)
} else if n < 14+20+8+dns.SizeHeader {
t.Error("expected response", n)
}
n, err = responderStack.EgressEthernet(buf[:])
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
}