mirror of
https://github.com/soypat/lneto.git
synced 2026-09-10 16:49:37 +00:00
Add mdns package and Client implementation (#55)
* 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
This commit is contained in:
+32
-5
@@ -46,6 +46,8 @@ type StackAsync struct {
|
||||
ntpUDP internet.StackUDPPort
|
||||
ntp ntp.Client
|
||||
|
||||
userUDPs []internet.StackUDPPort
|
||||
|
||||
sysprec int8 // NTP system precision.
|
||||
|
||||
prng uint32
|
||||
@@ -60,12 +62,16 @@ type StackConfig struct {
|
||||
StaticAddress netip.Addr
|
||||
DNSServer netip.Addr
|
||||
NTPServer netip.Addr
|
||||
RandSeed int64
|
||||
Hostname string
|
||||
MaxTCPConns int
|
||||
RandSeed int64
|
||||
HardwareAddress [6]byte
|
||||
MTU uint16
|
||||
MaxUDPConns int
|
||||
EthernetTxCRC32Update func(crc uint32, b []byte) uint32
|
||||
|
||||
HardwareAddress [6]byte
|
||||
MTU uint16
|
||||
// Accept multicast ethernet and IP packets. Needed for MDNS.
|
||||
AcceptMulticast bool
|
||||
}
|
||||
|
||||
func (s *StackAsync) Hostname() string {
|
||||
@@ -120,18 +126,24 @@ func (s *StackAsync) Reset(cfg StackConfig) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.link.SetAcceptMulticast(cfg.AcceptMulticast)
|
||||
const ipNodes = 2 // UDP, TCP ports.
|
||||
err = s.ip.Reset(addr, ipNodes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.ip.SetAcceptMulticast(cfg.AcceptMulticast)
|
||||
//
|
||||
err = s.resetARP()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
const udpMaintenanceConns = 3 // DHCP, DNS, NTP.
|
||||
err = s.udps.ResetUDP(udpMaintenanceConns)
|
||||
udpConns := 3 + cfg.MaxUDPConns // DHCP, DNS, NTP + user-registered.
|
||||
err = s.udps.ResetUDP(udpConns)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
internal.SliceReuse(&s.userUDPs, cfg.MaxUDPConns)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -322,6 +334,21 @@ func (s *StackAsync) RegisterListener(listener *tcp.Listener) (err error) {
|
||||
return s.tcps.Register(listener, nil)
|
||||
}
|
||||
|
||||
// RegisterUDP registers a StackNode on a UDP port with the given remote address and port.
|
||||
// The StackUDPPort wrapping is handled internally. The number of user-registered UDP ports
|
||||
// is limited by [StackConfig.MaxUDPConns].
|
||||
func (s *StackAsync) RegisterUDP(node internet.StackNode, remoteAddr []byte, remotePort uint16) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
idx := len(s.userUDPs)
|
||||
if idx >= cap(s.userUDPs) {
|
||||
return lneto.ErrBufferFull
|
||||
}
|
||||
s.userUDPs = s.userUDPs[:idx+1]
|
||||
s.userUDPs[idx].SetStackNode(node, remoteAddr, remotePort)
|
||||
return s.udps.Register(&s.userUDPs[idx])
|
||||
}
|
||||
|
||||
var errNoDNSServer = errors.New("no DNS server- did DHCP complete? You can set a predetermined DNS server in Stack configuration")
|
||||
|
||||
func (s *StackAsync) StartLookupIP(host string) error {
|
||||
|
||||
@@ -0,0 +1,332 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user