diff --git a/dns/client.go b/dns/client.go index d5f3800..e0eee23 100644 --- a/dns/client.go +++ b/dns/client.go @@ -24,8 +24,11 @@ type ResolveConfig struct { Questions []Question Additional []Resource EnableRecursion bool - MaxIPs uint16 - MaxCNAMEs uint16 + // MaxResponseAnswers limits how many answer records are decoded from the + // DNS response. If zero it defaults to the number of Questions. Answers + // are decoded in wire order regardless of type, so a response resolved + // through CNAMEs needs room for the CNAME records as well as the addresses. + MaxResponseAnswers uint16 } func (sudp *Client) Protocol() uint64 { return uint64(lneto.IPProtoUDP) } @@ -39,11 +42,10 @@ func (c *Client) StartResolve(localPort, txid uint16, cfg ResolveConfig) error { if nd > math.MaxUint16 || nd == 0 { return lneto.ErrInvalidConfig } - maxIPs := cfg.MaxIPs - if maxIPs == 0 { - maxIPs = uint16(nd) + maxAns := cfg.MaxResponseAnswers + if maxAns == 0 { + maxAns = uint16(nd) } - maxAns := maxIPs + cfg.MaxCNAMEs c.reset(localPort, txid, CQueryPending, cfg.EnableRecursion) c.msg.LimitResourceDecoding(uint16(nd), maxAns, 0, 0) c.msg.AddQuestions(cfg.Questions) diff --git a/dns/definitions.go b/dns/definitions.go index 087ee13..08b2723 100644 --- a/dns/definitions.go +++ b/dns/definitions.go @@ -197,6 +197,8 @@ const ( TypeALL Type = 255 // ALL ) +func (tp Type) IsIPAddr() bool { return tp == TypeA || tp == TypeAAAA } + // A Class is a type of network. type Class uint16 diff --git a/dns/dns.go b/dns/dns.go index e314782..1ebd6ba 100644 --- a/dns/dns.go +++ b/dns/dns.go @@ -99,6 +99,12 @@ func NamesEqual(a, b Name) bool { return internal.BytesEqual(a.data, b.data) } +// NamesEqualFold reports whether two DNS names are equal under ASCII case +// folding, which is how DNS labels compare per RFC 1035 section 2.3.3. +func NamesEqualFold(a, b Name) bool { + return internal.BytesEqualFoldASCII(a.data, b.data) +} + type ZFlags uint16 func NewResource(name Name, typ Type, class Class, ttl uint32, data []byte) Resource { @@ -299,64 +305,55 @@ func (m *Message) AppendTo(buf []byte, txid uint16, flags HeaderFlags) (_ []byte return buf, nil } -// WriteAnswers follows CNAMEs from host among the answers and writes the -// resulting addresses into dst. +// WriteAnswers writes the addresses answering host into dst, following the +// CNAME chain rooted at host. It returns the number of addresses written. func (m *Message) WriteAnswers(dst []netip.Addr, host string) (n uint16, err error) { - var alias Name + // Each round resolves one CNAME, which consumes an answer. Bounding the + // walk by the answer count is thus enough to reach the addresses, and + // terminates on cyclic chains. + var alias Name // Canonical name reached so far; zero means host itself. for range m.Answers { - var ( - next Name - hasAddrs bool - ) + var next Name for i := range m.Answers { ans := &m.Answers[i] - hdr := ans.Header() - if !hdr.pertainsTo(alias, host) { + if !ans.header.ownedBy(alias, host) { continue } - switch hdr.Type { - case TypeA, TypeAAAA: - hasAddrs = true - case TypeCNAME: - if cname, ok := ans.CNAMEView(); ok && cname.Len() != 0 { + switch { + case ans.header.Type.IsIPAddr(): + if int(n) >= len(dst) { + return n, lneto.ErrExhausted + } + addr, ok := netip.AddrFromSlice(ans.RawData()) + if !ok { + err = lneto.ErrInvalidAddr + continue + } + dst[n] = addr + n++ + case ans.header.Type == TypeCNAME: + if cname := ans.CNAMEView(); cname.Len() != 0 { next = cname } } } - if hasAddrs || next.Len() == 0 { + if n > 0 || next.Len() == 0 { break } alias = next } - - for i := range m.Answers { - if int(n) >= len(dst) { - return n, lneto.ErrExhausted - } - ans := &m.Answers[i] - hdr := ans.Header() - isAddr := hdr.Type == TypeA || hdr.Type == TypeAAAA - if !isAddr || !hdr.pertainsTo(alias, host) { - continue - } - var ok bool - dst[n], ok = netip.AddrFromSlice(ans.RawData()) - if !ok { - err = lneto.ErrInvalidAddr - } else { - n++ - } - } return n, err } -// pertainsTo reports whether the record's owner name is the given name: host -// or one of the aliases resolved so far. -func (h *ResourceHeader) pertainsTo(alias Name, host string) bool { +// ownedBy reports whether the record's owner name is the name being resolved: +// the alias reached by following CNAMEs, or host at the root of the chain. +func (h *ResourceHeader) ownedBy(alias Name, host string) bool { if alias.Len() == 0 { return h.Name.EqualString(host) } - return NamesEqual(h.Name, alias) + // Fold: the server chooses the case of both the CNAME target and the owner + // name of the records it aliases, and may randomize it (DNS 0x20). + return NamesEqualFold(h.Name, alias) } func (m *Message) Len() uint16 { @@ -450,11 +447,13 @@ func (r *Resource) RawData() []byte { return r.data[:length] } -func (r *Resource) CNAMEView() (Name, bool) { - if r.header.Type == TypeCNAME { - return Name{data: r.RawData()}, true +// CNAMEView returns the canonical name held by a CNAME record, aliasing the +// Resource's buffer. It returns a zero Name for any other record type. +func (r *Resource) CNAMEView() Name { + if r.header.Type != TypeCNAME { + return Name{} } - return Name{}, false + return Name{data: r.RawData()} } func (q *Question) Reset() { @@ -507,33 +506,20 @@ func (r *Resource) Decode(b []byte, off uint16) (uint16, error) { return off, errResourceLen } end := off + r.header.Length - r.data = append(r.data[:0], b[off:end]...) if r.header.Type == TypeCNAME { - raw := b[off:end] - data, derr := expandName(r.data[:0], b, off) - if derr == nil { - r.data = data + // CNAME data is a name which may use message compression. Expand it now + // since r.data is detached from b, leaving pointers unresolvable later. + cname := Name{data: r.data[:0]} + if _, derr := cname.Decode(b, off); derr == nil { + r.data = cname.data r.header.Length = uint16(len(r.data)) - } else { - r.data = append(r.data[:0], raw...) + return end, nil } } + r.data = append(r.data[:0], b[off:end]...) return end, nil } -func expandName(buf []byte, msg []byte, off uint16) ([]byte, error) { - appended := 0 - _, err := visitAllLabels(msg, off, func(label []byte) { - buf = append(buf, byte(len(label))) - buf = append(buf, label...) - appended += 1 + len(label) - }, allowCompression) - if err != nil { - return buf[:len(buf)-appended], err - } - return append(buf, 0), nil -} - func (r *Resource) appendTo(buf []byte) (_ []byte, err error) { buf, err = r.header.appendTo(buf) if err != nil { diff --git a/dns/dns_test.go b/dns/dns_test.go index 3407fcb..3eaebc3 100644 --- a/dns/dns_test.go +++ b/dns/dns_test.go @@ -268,9 +268,8 @@ func TestClient_CNAMEResponse(t *testing.T) { Type: TypeA, Class: ClassINET, }}, - EnableRecursion: true, - MaxIPs: 4, - MaxCNAMEs: 2, + EnableRecursion: true, + MaxResponseAnswers: 6, }) if err != nil { t.Fatal("failed to start DNS resolve:", err) @@ -342,6 +341,24 @@ func TestMessage_WriteAnswers(t *testing.T) { }, want: nil, }, + { + name: "CNAME target case differs from owner name", + host: "a.com", + // A server picks the case of both the CNAME target and the owner + // name of the record it aliases, and may randomize it (DNS 0x20), + // so the two must compare under ASCII case folding. + response: []byte{ + // Header: txid 0xabcd, QR|RD|RA, QD=1 AN=2 NS=0 AR=0. + 0xab, 0xcd, 0x81, 0x80, 0x00, 0x01, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, + // Question: a.com A IN. + 0x01, 'a', 0x03, 'c', 'o', 'm', 0x00, 0x00, 0x01, 0x00, 0x01, + // Answer 1: a.com CNAME B.CoM. + 0x01, 'a', 0x03, 'c', 'o', 'm', 0x00, 0x00, 0x05, 0x00, 0x01, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x07, 0x01, 'B', 0x03, 'C', 'o', 'M', 0x00, + // Answer 2: b.com A IN ttl=10 rdlen=4 1.2.3.4. + 0x01, 'b', 0x03, 'c', 'o', 'm', 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x04, 0x01, 0x02, 0x03, 0x04, + }, + want: []netip.Addr{netip.AddrFrom4([4]byte{1, 2, 3, 4})}, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -372,7 +389,7 @@ func TestClient_ReceivesDNSResponse(t *testing.T) { const hostname = "example.com" const txid = uint16(12345) const clientPort = uint16(54321) - const maxIPs = 4 + const maxAnswers = 4 allIPs := [5][4]byte{ {192, 0, 2, 1}, {192, 0, 2, 2}, @@ -387,7 +404,7 @@ func TestClient_ReceivesDNSResponse(t *testing.T) { }{ {name: "single_answer", responseIPs: allIPs[:1], wantAnswers: 1}, {name: "multiple_answers", responseIPs: allIPs[:4], wantAnswers: 4}, - {name: "answer_limit", responseIPs: allIPs[:5], wantAnswers: maxIPs}, + {name: "answer_limit", responseIPs: allIPs[:5], wantAnswers: maxAnswers}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -419,9 +436,8 @@ func TestClient_ReceivesDNSResponse(t *testing.T) { Type: TypeA, Class: ClassINET, }}, - EnableRecursion: true, - MaxIPs: maxIPs, - MaxCNAMEs: 0, // No CNAME records in this response. + EnableRecursion: true, + MaxResponseAnswers: maxAnswers, }) if err != nil { t.Fatal("failed to start DNS resolve:", err) @@ -437,7 +453,7 @@ func TestClient_ReceivesDNSResponse(t *testing.T) { t.Fatal("failed to demux DNS response:", err) } - var addrs [maxIPs]netip.Addr + var addrs [maxAnswers]netip.Addr answers, err := client.ResponseAnswerLookup(addrs[:], hostname) if err != nil { t.Fatal("failed to look up DNS response answers:", err) diff --git a/http/httpraw/kvbuffer.go b/http/httpraw/kvbuffer.go index 7fd3005..279d64c 100644 --- a/http/httpraw/kvbuffer.go +++ b/http/httpraw/kvbuffer.go @@ -300,25 +300,7 @@ func (kvb *kvBuffer) getFoldIdx(key string) int { // EqualFoldASCII reports whether a and b are equal under ASCII case folding. // Unlike strings.EqualFold it does not fold non-ASCII runes, so no multi-byte // rune such as U+212A KELVIN SIGN can alias a header key. -func EqualFoldASCII(a, b string) bool { - if len(a) != len(b) { - return false - } - const asciiCapDiff = 'a' - 'A' - for i := 0; i < len(a); i++ { - ca, cb := a[i], b[i] - if ca >= 'A' && ca <= 'Z' { - ca += asciiCapDiff - } - if cb >= 'A' && cb <= 'Z' { - cb += asciiCapDiff - } - if ca != cb { - return false - } - } - return true -} +func EqualFoldASCII(a, b string) bool { return internal.EqualFoldASCII(a, b) } // reserve ensures need free bytes are available in the buffer, growing it when // permitted. It accounts for the byte-0 reservation on an empty buffer (see diff --git a/http/httpraw/multipart.go b/http/httpraw/multipart.go index e24672f..4661850 100644 --- a/http/httpraw/multipart.go +++ b/http/httpraw/multipart.go @@ -253,20 +253,7 @@ func trimOWS(b []byte) []byte { return b } -// equalFold compares b to the ASCII lowercase key, case insensitively. +// equalFold compares b to key, case insensitively. func equalFold(b []byte, key string) bool { - if len(b) != len(key) { - return false - } - const asciiCapDiff = 'a' - 'A' - for i := range b { - c := b[i] - if c >= 'A' && c <= 'Z' { - c += asciiCapDiff - } - if c != key[i] { - return false - } - } - return true + return EqualFoldASCII(b2s(b), key) } diff --git a/internal/slices.go b/internal/slices.go index 4126220..ef73bc0 100644 --- a/internal/slices.go +++ b/internal/slices.go @@ -14,6 +14,40 @@ func BytesEqual(a, b []byte) bool { return unsafe.String(&a[0], len(a)) == unsafe.String(&b[0], len(b)) } +// EqualFoldASCII reports whether a and b are equal under ASCII case folding. +// Unlike [strings.EqualFold] it does not fold non-ASCII runes, so no multi-byte +// rune such as U+212A KELVIN SIGN can alias an ASCII key. +func EqualFoldASCII(a, b string) bool { + if len(a) != len(b) { + return false + } + const asciiCapDiff = 'a' - 'A' + for i := 0; i < len(a); i++ { + ca, cb := a[i], b[i] + if ca >= 'A' && ca <= 'Z' { + ca += asciiCapDiff + } + if cb >= 'A' && cb <= 'Z' { + cb += asciiCapDiff + } + if ca != cb { + return false + } + } + return true +} + +// BytesEqualFoldASCII is the []byte form of [EqualFoldASCII]. Like [BytesEqual] +// it is heapless in tinygo, unlike [bytes.EqualFold] which also folds non-ASCII. +func BytesEqualFoldASCII(a, b []byte) bool { + if len(a) != len(b) { + return false + } else if len(a) == 0 { + return true + } + return EqualFoldASCII(unsafe.String(&a[0], len(a)), unsafe.String(&b[0], len(b))) +} + // IsZeroed returns true if all arguments are set to their zero value. func IsZeroed[T comparable](a ...T) bool { var z T diff --git a/x/xnet/stack-async.go b/x/xnet/stack-async.go index 4483d21..f2d9bf4 100644 --- a/x/xnet/stack-async.go +++ b/x/xnet/stack-async.go @@ -630,8 +630,9 @@ func (s *StackAsync) StartLookupIPType(host string, qtype dns.Type) error { s.ednsopt, }, EnableRecursion: true, - MaxIPs: uint16(len(s.addrbufnip)), - MaxCNAMEs: 8, + // Leave headroom above the address buffer for CNAME records, which + // occupy answer slots before the addresses they alias. + MaxResponseAnswers: uint16(len(s.addrbufnip)) + 8, }) if err != nil { return err