mirror of
https://github.com/soypat/lneto.git
synced 2026-08-19 06:04:01 +00:00
bd645b5e1e
* add mdns * define mdns.Client * fix some mdns stuff * refine dns package for use with mdns * remove Querier and Responder and replace with Client * add record setting methods on dns.record to reuse record buffer * protect against unbounded client answer growth * round off sharp mdns edges; reduce allocs * add mutlicast to stacks; add xnet tests for mdns * fix documentation on acceptmulticast field * mdns working
333 lines
8.3 KiB
Go
333 lines
8.3 KiB
Go
package xnet
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"net/netip"
|
|
"testing"
|
|
|
|
"github.com/soypat/lneto/dns"
|
|
"github.com/soypat/lneto/ethernet"
|
|
"github.com/soypat/lneto/mdns"
|
|
)
|
|
|
|
func TestMDNS_QueryResponse(t *testing.T) {
|
|
const MTU = 1500
|
|
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 := netip.AddrFrom4([4]byte{192, 168, 1, 50})
|
|
responderMAC := [6]byte{0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0x01}
|
|
querierAddr := netip.AddrFrom4([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,
|
|
StaticAddress: responderAddr,
|
|
HardwareAddress: responderMAC,
|
|
MTU: MTU,
|
|
MaxUDPConns: 1,
|
|
AcceptMulticast: true,
|
|
})
|
|
if err != nil {
|
|
t.Fatal("responder reset:", err)
|
|
}
|
|
responderStack.SetGateway6(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.RegisterUDP(&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,
|
|
StaticAddress: querierAddr,
|
|
HardwareAddress: querierMAC,
|
|
MTU: MTU,
|
|
MaxUDPConns: 1,
|
|
AcceptMulticast: true,
|
|
})
|
|
if err != nil {
|
|
t.Fatal("querier reset:", err)
|
|
}
|
|
querierStack.SetGateway6(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.RegisterUDP(&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.Encapsulate(buf[:], -1, 0)
|
|
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.Demux(buf[:n], 0)
|
|
if err != nil {
|
|
t.Fatal("responder demux:", err)
|
|
}
|
|
|
|
// Responder encapsulates response.
|
|
n, err = responderStack.Encapsulate(buf[:], -1, 0)
|
|
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.Demux(buf[:n], 0)
|
|
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 = 1500
|
|
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 = 1500
|
|
stack := new(StackAsync)
|
|
err := stack.Reset(StackConfig{
|
|
Hostname: hostname,
|
|
RandSeed: seed,
|
|
StaticAddress: addr,
|
|
HardwareAddress: mac,
|
|
MTU: MTU,
|
|
MaxUDPConns: 1,
|
|
AcceptMulticast: true,
|
|
})
|
|
if err != nil {
|
|
t.Fatal(hostname, "reset:", err)
|
|
}
|
|
stack.SetGateway6(gatewayMAC)
|
|
|
|
var client mdns.Client
|
|
err = client.Configure(mdnsCfg)
|
|
if err != nil {
|
|
t.Fatal(hostname, "mdns configure:", err)
|
|
}
|
|
|
|
err = stack.RegisterUDP(&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.Encapsulate(buf, -1, 0)
|
|
if err != nil || n == 0 {
|
|
t.Fatal("querier encapsulate:", err, n)
|
|
}
|
|
|
|
// Responder demuxes multicast query directly.
|
|
err = responder.Demux(buf[:n], 0)
|
|
if err != nil {
|
|
t.Fatal("responder demux:", err)
|
|
}
|
|
|
|
// Responder encapsulates response.
|
|
n, err = responder.Encapsulate(buf, -1, 0)
|
|
if err != nil || n == 0 {
|
|
t.Fatal("responder encapsulate:", err, n)
|
|
}
|
|
|
|
// Querier demuxes multicast response.
|
|
err = querier.Demux(buf[:n], 0)
|
|
if err != nil {
|
|
t.Fatal("querier demux:", err)
|
|
}
|
|
}
|