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:
Pat Whittingslow
2026-03-16 19:48:17 +01:00
committed by GitHub
parent 376e1a0b4f
commit bd645b5e1e
14 changed files with 1402 additions and 67 deletions
+1 -1
View File
@@ -34,7 +34,7 @@ func (sudp *Client) ConnectionID() *uint64 { return &sudp.connID }
func (c *Client) StartResolve(localPort, txid uint16, cfg ResolveConfig) error {
nd := len(cfg.Questions)
if nd > math.MaxUint16 {
return lneto.ErrBufferFull
return lneto.ErrInvalidConfig
}
c.reset(localPort, txid, dnsSendQuery, cfg.EnableRecursion)
c.msg.LimitResourceDecoding(uint16(nd), uint16(nd), 0, 0)
+115 -37
View File
@@ -57,6 +57,13 @@ type Name struct {
data []byte
}
// NamesEqual reports whether two DNS names are equal by comparing
// their wire-format representations directly. This is case-sensitive;
// for case-insensitive comparison use [NamesEqualFold].
func NamesEqual(a, b Name) bool {
return internal.BytesEqual(a.data, b.data)
}
type ZFlags uint16
func NewResource(name Name, typ Type, class Class, ttl uint32, data []byte) Resource {
@@ -86,83 +93,91 @@ func (r *Resource) SetEDNS0(UDPlength uint16, rcode RCode, zflags ZFlags, data [
r.data = append(r.data[:0], data...)
}
// Decode decodes the DNS message in b into m. It returns the number of bytes
// DecodeMessage decodes the DNS message into question, answer, authority and additional resources.
// It returns the number of bytes
// consumed from b (0 if no bytes were consumed) and any error encountered.
// If the message was not completely parsed due to LimitResourceDecoding,
// incompleteButOK is true and an error is returned, though the message is still usable.
func (m *Message) Decode(msg []byte) (_ uint16, incompleteButOK bool, err error) {
if len(msg) < SizeHeader {
return 0, false, errBaseLen
} else if len(msg) > math.MaxUint16 {
return 0, false, errResTooLong
}
m.Reset()
//
// The slice memory is overwritten and capacity used as the limit of encoding.
// If the argument slice is nil it is skipped for decoding but does not prevent further decoding
// of other answers, authorities or additionals from being decoded.
func DecodeMessage(q *[]Question, answers, authorities, additionals *[]Resource, msg []byte) (_ uint16, incompleteButOK bool, err error) {
hdr, err := NewFrame(msg)
if err != nil {
return 0, false, err
}
nq := int(hdr.QDCount())
qd := hdr.QDCount()
nq := int(qd)
off := uint16(SizeHeader)
// Return tooManyErr if found to flag to the caller that the message was
// decoded but contained too many resources to decode completely.
var tooManyErr error
switch {
case nq > cap(m.Questions):
case nq > caporzero(q):
tooManyErr = errTooManyQuestions
case hdr.ANCount() > uint16(cap(m.Answers)):
case int(hdr.ANCount()) > caporzero(answers):
tooManyErr = errTooManyAnswers
case hdr.NSCount() > uint16(cap(m.Authorities)):
case int(hdr.NSCount()) > caporzero(authorities):
tooManyErr = errTooManyAuthorities
case hdr.ARCount() > uint16(cap(m.Additionals)):
case int(hdr.ARCount()) > caporzero(additionals):
tooManyErr = errTooManyAdditionals
}
if nq > cap(m.Questions) {
nq = cap(m.Questions)
}
m.Questions = m.Questions[:nq]
for i := 0; i < nq; i++ {
off, err = m.Questions[i].Decode(msg, off)
if err != nil {
m.Questions = m.Questions[:i] // Trim non-decoded/failed questions.
return off, false, err
if q != nil {
if nq > cap(*q) {
nq = cap(*q)
}
*q = (*q)[:nq]
for i := 0; i < nq; i++ {
off, err = (*q)[i].Decode(msg, off)
if err != nil {
*q = (*q)[:i] // Trim non-decoded/failed questions.
return off, false, err
}
}
} else {
nq = 0 // No question slice provided, skip all questions below.
}
// Skip undecoded questions.
qd := hdr.QDCount()
for i := 0; i < int(qd)-nq; i++ {
off, err = skipQuestion(msg, off)
if err != nil {
return off, false, err
}
}
off, err = decodeToCapResources(&m.Answers, msg, hdr.ANCount(), off)
off, err = decodeToCapResources(answers, msg, hdr.ANCount(), off)
if err != nil {
return off, false, err
}
off, err = decodeToCapResources(&m.Authorities, msg, hdr.NSCount(), off)
off, err = decodeToCapResources(authorities, msg, hdr.NSCount(), off)
if err != nil {
return off, false, err
}
off, err = decodeToCapResources(&m.Additionals, msg, hdr.ARCount(), off)
off, err = decodeToCapResources(additionals, msg, hdr.ARCount(), off)
if err != nil {
return off, false, err
}
return off, tooManyErr != nil, tooManyErr
}
// Decode decodes the DNS message in b into m. It is a convenience wrapper for [DecodeMessage].
func (m *Message) Decode(msg []byte) (_ uint16, incompleteButOK bool, err error) {
return DecodeMessage(&m.Questions, &m.Answers, &m.Authorities, &m.Additionals, msg)
}
func decodeToCapResources(dst *[]Resource, msg []byte, nrec, off uint16) (_ uint16, err error) {
originalRec := nrec
if nrec > uint16(cap(*dst)) {
nrec = uint16(cap(*dst)) // Decode up to cap. Caller will return an error flag.
}
*dst = (*dst)[:nrec]
for i := uint16(0); i < nrec; i++ {
off, err = (*dst)[i].Decode(msg, off)
if err != nil {
*dst = (*dst)[:i] // Trim non-decoded/failed resources.
return off, err
if dst != nil {
if nrec > uint16(cap(*dst)) {
nrec = uint16(cap(*dst)) // Decode up to cap. Caller will return an error flag.
}
*dst = (*dst)[:nrec]
for i := uint16(0); i < nrec; i++ {
off, err = (*dst)[i].Decode(msg, off)
if err != nil {
*dst = (*dst)[:i] // Trim non-decoded/failed resources.
return off, err
}
}
}
// Parse undecoded resources, effectively skipping them.
@@ -468,6 +483,20 @@ func NewName(domain string) (Name, error) {
return name, nil
}
// TrimLabels returns a Name sharing the same backing data with the first n labels removed.
// For example, trimming 1 label from "My Web._http._tcp.local" yields "_http._tcp.local".
// Returns an empty Name if n exceeds the number of labels.
func (n Name) TrimLabels(skip int) Name {
off := 0
for i := 0; i < skip; i++ {
if off >= len(n.data) {
return Name{}
}
off += 1 + int(n.data[off])
}
return Name{data: n.data[off:]}
}
// Len returns the length over-the-wire of the encoded Name.
func (n *Name) Len() uint16 {
if len(n.data) > math.MaxUint16 {
@@ -666,6 +695,48 @@ func (dst *Resource) CopyFrom(r Resource) {
dst.data = append(dst.data[:0], r.data...)
}
// SetA sets an A (IPv4 address) resource record, reusing internal buffers.
func (r *Resource) SetA(name Name, class Class, ttl uint32, addr []byte) {
r.setHeader(name, TypeA, class, ttl)
r.data = append(r.data[:0], addr...)
r.header.Length = uint16(len(r.data))
}
// SetPTR sets a PTR (pointer) resource record, reusing internal buffers.
func (r *Resource) SetPTR(name Name, class Class, ttl uint32, target Name) {
r.setHeader(name, TypePTR, class, ttl)
r.data, _ = target.AppendTo(r.data[:0])
r.header.Length = uint16(len(r.data))
}
// SetSRV sets a SRV (service locator) resource record, reusing internal buffers.
func (r *Resource) SetSRV(name Name, class Class, ttl uint32, priority, weight, port uint16, target Name) {
r.setHeader(name, TypeSRV, class, ttl)
r.data = binary.BigEndian.AppendUint16(r.data[:0], priority)
r.data = binary.BigEndian.AppendUint16(r.data, weight)
r.data = binary.BigEndian.AppendUint16(r.data, port)
r.data, _ = target.AppendTo(r.data)
r.header.Length = uint16(len(r.data))
}
// SetTXT sets a TXT resource record, reusing internal buffers.
func (r *Resource) SetTXT(name Name, class Class, ttl uint32, txt []byte) {
r.setHeader(name, TypeTXT, class, ttl)
if len(txt) == 0 {
r.data = append(r.data[:0], 0)
} else {
r.data = append(r.data[:0], txt...)
}
r.header.Length = uint16(len(r.data))
}
func (r *Resource) setHeader(name Name, typ Type, class Class, ttl uint32) {
r.header.Name.CopyFrom(name)
r.header.Type = typ
r.header.Class = class
r.header.TTL = ttl
}
func (dst *ResourceHeader) CopyFrom(rh ResourceHeader) {
dst.Name.CopyFrom(rh.Name)
dst.Type = rh.Type
@@ -673,3 +744,10 @@ func (dst *ResourceHeader) CopyFrom(rh ResourceHeader) {
dst.TTL = rh.TTL
dst.Length = rh.Length
}
func caporzero[T any](v *[]T) int {
if v == nil {
return 0
}
return cap(*v)
}