mirror of https://github.com/tailscale/tailscale/
Merge ef2e3c64aa into 5db95ec376
commit
d735dcbc3f
@ -0,0 +1,294 @@
|
||||
package resolver
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"golang.org/x/net/dns/dnsmessage"
|
||||
)
|
||||
|
||||
// extractOPTResource parses a DNS message and returns the OPT resource if present.
|
||||
func extractOPTResource(msg []byte) *dnsmessage.Resource {
|
||||
var p dnsmessage.Parser
|
||||
if _, err := p.Start(msg); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var optRes *dnsmessage.Resource
|
||||
optRes = nil
|
||||
|
||||
// Fast-forward to find OPT
|
||||
if err := p.SkipAllQuestions(); err == nil {
|
||||
if err := p.SkipAllAnswers(); err == nil {
|
||||
if err := p.SkipAllAuthorities(); err == nil {
|
||||
for {
|
||||
r, err := p.Additional()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
if r.Header.Type == dnsmessage.TypeOPT {
|
||||
optRes = &r
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return optRes
|
||||
}
|
||||
|
||||
const minEDNS0Size = 512 // per RFC 6891 Section 6.2.5
|
||||
const maxEDNS0Size = 1232 // per DNS Flag Day 2020 recommendation
|
||||
|
||||
// extractEDNS0UDPSize extracts the advertised UDP buffer size from an EDNS0 OPT record
|
||||
// in a DNS query packet. If no EDNS0 record is present or the packet is malformed,
|
||||
// it returns 0, indicating the default 512-byte limit should be used.
|
||||
func extractEDNS0UDPSize(query []byte) uint16 {
|
||||
size := uint16(0)
|
||||
optRes := extractOPTResource(query)
|
||||
|
||||
if optRes != nil {
|
||||
// UDP payload size is encoded in the CLASS field of the OPT header.
|
||||
// Per RFC 6891 §6.2.5, treat any advertised UDP size smaller than 512
|
||||
// as 512. Per DNS Flag Day 2020 (https://www.dnsflagday.net/2020/),
|
||||
// the cap should be 1232 bytes, and newer versions of resolvers
|
||||
// have set 1232 as their default limit.
|
||||
size = uint16(optRes.Header.Class)
|
||||
if size < minEDNS0Size {
|
||||
size = minEDNS0Size
|
||||
}
|
||||
if size > maxEDNS0Size {
|
||||
size = maxEDNS0Size
|
||||
}
|
||||
}
|
||||
return size
|
||||
}
|
||||
|
||||
// truncateDNSResponse performs RFC-compliant truncation of a DNS
|
||||
// response message. It preserves the question section and as many
|
||||
// resource records as possible in the answer, authority, and
|
||||
// additional sections, setting the TC (truncated) bit if truncation
|
||||
// occurs. It enforces RFC 6891 Section 7 (preserving the OPT record
|
||||
// in truncated responses).
|
||||
func truncateDNSResponse(resp []byte, maxSize uint16) ([]byte, error) {
|
||||
// Sanity check on maxSize. It must be at least large enough
|
||||
// to hold a minimal DNS header (12 bytes) and at least one
|
||||
// question (5 bytes).
|
||||
if maxSize < 12+5 {
|
||||
return nil, errors.New("maxSize too small to hold minimal DNS message")
|
||||
}
|
||||
|
||||
var p dnsmessage.Parser
|
||||
|
||||
header, err := p.Start(resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 1. Extract all records into slices so we can manage them.
|
||||
questions, err := p.AllQuestions()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var answers, authorities, additionals []dnsmessage.Resource
|
||||
var optRes *dnsmessage.Resource
|
||||
|
||||
// Helper to extract resources from a section
|
||||
extractSection := func(sectionName string) ([]dnsmessage.Resource, error) {
|
||||
var extracted []dnsmessage.Resource
|
||||
for {
|
||||
var r dnsmessage.Resource
|
||||
var err error
|
||||
switch sectionName {
|
||||
case "Ans":
|
||||
r, err = p.Answer()
|
||||
case "Auth":
|
||||
r, err = p.Authority()
|
||||
case "Add":
|
||||
r, err = p.Additional()
|
||||
}
|
||||
if err == dnsmessage.ErrSectionDone {
|
||||
return extracted, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Identify and isolate the OPT record
|
||||
if r.Header.Type == dnsmessage.TypeOPT {
|
||||
// We found the OPT record. Save it separately.
|
||||
// (RFC 6891: Only one OPT record is allowed)
|
||||
optRes = &r
|
||||
} else {
|
||||
extracted = append(extracted, r)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We must parse sections in order: Skip Questions (already got them), then Ans, Auth, Add.
|
||||
// Note: p.AllQuestions() already advanced the parser past questions.
|
||||
|
||||
if answers, err = extractSection("Ans"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if authorities, err = extractSection("Auth"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if additionals, err = extractSection("Add"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 2. Try to build the FULL packet first (Happy Path).
|
||||
// If it fits, we avoid the expensive iterative logic.
|
||||
fullPacket, err := buildResponse(header, questions, answers, authorities, additionals, optRes)
|
||||
if err == nil && uint16(len(fullPacket)) <= maxSize {
|
||||
return fullPacket, nil
|
||||
}
|
||||
|
||||
// 3. Truncation Path.
|
||||
// The packet is too big. We must rebuild it record-by-record until full.
|
||||
// We MUST set the TC bit.
|
||||
header.Truncated = true
|
||||
|
||||
// We start with empty sections.
|
||||
var finalAns, finalAuth, finalAdd []dnsmessage.Resource
|
||||
|
||||
// Define the order of candidates we want to try adding.
|
||||
// (Answers first, then Authorities, then Additionals)
|
||||
// We use a list of *slices* to iterate section by section.
|
||||
sections := []struct {
|
||||
candidates []dnsmessage.Resource
|
||||
target *[]dnsmessage.Resource // Pointer to the slice we are building
|
||||
}{
|
||||
{answers, &finalAns},
|
||||
{authorities, &finalAuth},
|
||||
{additionals, &finalAdd},
|
||||
}
|
||||
|
||||
for _, section := range sections {
|
||||
for _, candidate := range section.candidates {
|
||||
// Speculatively add this candidate to the target list
|
||||
*section.target = append(*section.target, candidate)
|
||||
|
||||
// Build the packet with the current set of records + Mandatory OPT
|
||||
testPacket, err := buildResponse(header, questions, finalAns, finalAuth, finalAdd, optRes)
|
||||
if err != nil {
|
||||
return nil, err // Should not happen with valid resources
|
||||
}
|
||||
|
||||
// Check size
|
||||
if uint16(len(testPacket)) > maxSize {
|
||||
// Stop! This record broke the limit.
|
||||
// Remove the last added record (backtrack).
|
||||
*section.target = (*section.target)[:len(*section.target)-1]
|
||||
|
||||
// We are full. Return the last valid build.
|
||||
// Note: We need to rebuild one last time or save the previous successful 'testPacket'.
|
||||
// To be safe/clean, let's just rebuild the "safe" state.
|
||||
return buildResponse(header, questions, finalAns, finalAuth, finalAdd, optRes)
|
||||
}
|
||||
|
||||
// If it fits, continue loop to add next candidate.
|
||||
}
|
||||
}
|
||||
|
||||
// If we somehow finish the loop (unlikely given we failed the "Full" check), return what we have.
|
||||
return buildResponse(header, questions, finalAns, finalAuth, finalAdd, optRes)
|
||||
}
|
||||
|
||||
// buildResponse constructs a binary DNS message from the provided slices.
|
||||
// It handles the complex state machine of dnsmessage.Builder.
|
||||
func buildResponse(
|
||||
h dnsmessage.Header,
|
||||
qs []dnsmessage.Question,
|
||||
ans, auths, adds []dnsmessage.Resource,
|
||||
opt *dnsmessage.Resource,
|
||||
) ([]byte, error) {
|
||||
// Start with a nil buffer; Builder will allocate.
|
||||
b := dnsmessage.NewBuilder(nil, h)
|
||||
b.EnableCompression()
|
||||
|
||||
// 1. Questions
|
||||
if err := b.StartQuestions(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, q := range qs {
|
||||
if err := b.Question(q); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Answers
|
||||
if err := b.StartAnswers(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, r := range ans {
|
||||
if err := addResource(&b, r); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Authorities
|
||||
if err := b.StartAuthorities(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, r := range auths {
|
||||
if err := addResource(&b, r); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Additionals
|
||||
if err := b.StartAdditionals(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, r := range adds {
|
||||
if err := addResource(&b, r); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Always append the OPT record if it exists (RFC 6891)
|
||||
if opt != nil {
|
||||
if err := addResource(&b, *opt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Finish and return the bytes
|
||||
return b.Finish()
|
||||
}
|
||||
|
||||
// addResource is a helper to handle the various resource types
|
||||
// when adding individual resources to the Builder.
|
||||
func addResource(b *dnsmessage.Builder, r dnsmessage.Resource) error {
|
||||
switch body := r.Body.(type) {
|
||||
case *dnsmessage.AResource:
|
||||
return b.AResource(r.Header, *body)
|
||||
case *dnsmessage.AAAAResource:
|
||||
return b.AAAAResource(r.Header, *body)
|
||||
case *dnsmessage.CNAMEResource:
|
||||
return b.CNAMEResource(r.Header, *body)
|
||||
case *dnsmessage.HTTPSResource:
|
||||
return b.HTTPSResource(r.Header, *body)
|
||||
case *dnsmessage.NSResource:
|
||||
return b.NSResource(r.Header, *body)
|
||||
case *dnsmessage.PTRResource:
|
||||
return b.PTRResource(r.Header, *body)
|
||||
case *dnsmessage.SOAResource:
|
||||
return b.SOAResource(r.Header, *body)
|
||||
case *dnsmessage.MXResource:
|
||||
return b.MXResource(r.Header, *body)
|
||||
case *dnsmessage.TXTResource:
|
||||
return b.TXTResource(r.Header, *body)
|
||||
case *dnsmessage.SRVResource:
|
||||
return b.SRVResource(r.Header, *body)
|
||||
case *dnsmessage.OPTResource:
|
||||
return b.OPTResource(r.Header, *body)
|
||||
case *dnsmessage.UnknownResource:
|
||||
// Handles unsupported/generic types
|
||||
return b.UnknownResource(r.Header, *body)
|
||||
default:
|
||||
return errors.New("unsupported resource body type")
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,276 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package resolver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"testing"
|
||||
|
||||
dns "golang.org/x/net/dns/dnsmessage"
|
||||
"tailscale.com/types/dnstype"
|
||||
"tailscale.com/util/dnsname"
|
||||
)
|
||||
|
||||
// Note: This test file uses helper builders already present in other resolver
|
||||
// tests (e.g., makeTestRequest/makeTestResponse/dnspacket) since they are in
|
||||
// the same package test space.
|
||||
|
||||
func TestExtractValidEDNS0UDPSize(t *testing.T) {
|
||||
q := dnspacket("example.com.", dns.TypeA, 917)
|
||||
got := extractEDNS0UDPSize(q)
|
||||
if got != 917 {
|
||||
t.Fatalf("expected 917, got %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractSmallEDNS0UDPSize(t *testing.T) {
|
||||
q := dnspacket("example.com.", dns.TypeA, 100)
|
||||
got := extractEDNS0UDPSize(q)
|
||||
// extractEDNS0UDPSize enforces minimum of 512 per RFC 6891 §6.2.5
|
||||
if got != minEDNS0Size {
|
||||
t.Fatalf("expected %v, got %v", minEDNS0Size, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractLargeEDNS0UDPSize(t *testing.T) {
|
||||
q := dnspacket("example.com.", dns.TypeA, 5000)
|
||||
got := extractEDNS0UDPSize(q)
|
||||
// extractEDNS0UDPSize caps at maxEDNS0Size
|
||||
if got != maxEDNS0Size {
|
||||
t.Fatalf("expected %v, got %v", maxEDNS0Size, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTruncateNonEDNS(t *testing.T) {
|
||||
// Build a very large response (many A records) without EDNS
|
||||
// Create response with many answers
|
||||
name := dns.MustNewName("example.com.")
|
||||
b := dns.NewBuilder(nil, dns.Header{Response: true, Authoritative: true, RCode: dns.RCodeSuccess})
|
||||
if err := b.StartQuestions(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := b.Question(dns.Question{Name: name, Type: dns.TypeA, Class: dns.ClassINET}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := b.StartAnswers(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// add enough A records to exceed 512 bytes
|
||||
for i := 0; i < 200; i++ {
|
||||
b.AResource(dns.ResourceHeader{Name: name, Class: dns.ClassINET, TTL: 60}, dns.AResource{A: [4]byte{192, 0, 2, byte(i % 255)}})
|
||||
}
|
||||
resp, err := b.Finish()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(resp) <= 512 {
|
||||
t.Fatalf("response not large enough for test: %d", len(resp))
|
||||
}
|
||||
|
||||
tr, err := truncateDNSResponse(resp, 512)
|
||||
if err != nil {
|
||||
t.Fatalf("truncate failed: %v", err)
|
||||
}
|
||||
if len(tr) > 512 {
|
||||
t.Fatalf("truncated response too large: %d", len(tr))
|
||||
}
|
||||
// Check TC bit set
|
||||
var p dns.Parser
|
||||
h, err := p.Start(tr)
|
||||
if err != nil {
|
||||
t.Fatalf("parse truncated: %v", err)
|
||||
}
|
||||
if !h.Truncated {
|
||||
t.Fatalf("expected Truncated bit set")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEDNSAllowsLarger(t *testing.T) {
|
||||
// Build request that advertises EDNS size 1232
|
||||
ednsSize := uint16(1232)
|
||||
q := dnspacket("example.com.", dns.TypeA, ednsSize)
|
||||
if got := extractEDNS0UDPSize(q); got != ednsSize {
|
||||
t.Fatalf("expected 1232, got %v", got)
|
||||
}
|
||||
|
||||
// Build response of size >512 but <1232
|
||||
name := dns.MustNewName("example.com.")
|
||||
b := dns.NewBuilder(nil, dns.Header{Response: true, Authoritative: true, RCode: dns.RCodeSuccess})
|
||||
b.EnableCompression()
|
||||
b.StartQuestions()
|
||||
b.Question(dns.Question{Name: name, Type: dns.TypeA, Class: dns.ClassINET})
|
||||
b.StartAnswers()
|
||||
for i := 0; i < 50; i++ {
|
||||
b.AResource(dns.ResourceHeader{Name: name, Class: dns.ClassINET, TTL: 60}, dns.AResource{A: [4]byte{10, 0, 0, byte(i)}})
|
||||
}
|
||||
resp, err := b.Finish()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(resp) <= 512 || len(resp) >= int(ednsSize) {
|
||||
t.Fatalf("invalid response size %d", len(resp))
|
||||
}
|
||||
|
||||
tr, err := truncateDNSResponse(resp, ednsSize)
|
||||
if err != nil {
|
||||
t.Fatalf("truncate failed: %v", err)
|
||||
}
|
||||
if len(tr) != len(resp) {
|
||||
t.Fatalf("unexpected truncation when EDNS allows large: %d vs %d", len(tr), len(resp))
|
||||
}
|
||||
}
|
||||
|
||||
// TestTruncateDNSResponseImpossible verifies that truncateDNSResponse
|
||||
// returns an error when the provided maxSize is too small to even encode
|
||||
// the header+question portion of the message.
|
||||
func TestTruncateDNSResponseImpossible(t *testing.T) {
|
||||
// Build a normal query packet and attempt to truncate it to a very small
|
||||
// size that cannot contain the header+question.
|
||||
req := makeTestRequest(t, "example.com.")
|
||||
if len(req) < 20 {
|
||||
t.Fatalf("test request unexpectedly small: %d", len(req))
|
||||
}
|
||||
|
||||
// Choose a maxSize smaller than the request's header+question length.
|
||||
// Using 10 bytes is guaranteed to be too small.
|
||||
if _, err := truncateDNSResponse(req, 10); err == nil {
|
||||
t.Fatalf("expected error truncating to impossibly small size, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTruncateDNSResponseDirectCall tests truncateDNSResponse with a large
|
||||
// well-formed DNS response. This directly verifies that
|
||||
// truncateDNSResponse produces a syntactically valid truncated response
|
||||
// with the TC bit set.
|
||||
func TestTruncateDNSResponseDirectCall(t *testing.T) {
|
||||
const domain = "example.com."
|
||||
|
||||
// Build a very large DNS response (many A records)
|
||||
name := dns.MustNewName(domain)
|
||||
b := dns.NewBuilder(nil, dns.Header{Response: true, Authoritative: true, RCode: dns.RCodeSuccess})
|
||||
b.EnableCompression()
|
||||
if err := b.StartQuestions(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := b.Question(dns.Question{Name: name, Type: dns.TypeA, Class: dns.ClassINET}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := b.StartAnswers(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Add enough A records to exceed 512 bytes significantly.
|
||||
// Each A record is roughly 20 bytes, so 150 records will be ~3000 bytes.
|
||||
for i := 0; i < 150; i++ {
|
||||
err := b.AResource(
|
||||
dns.ResourceHeader{Name: name, Class: dns.ClassINET, TTL: 60},
|
||||
dns.AResource{A: [4]byte{10, 0, 0, byte(i % 256)}},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to add A record: %v", err)
|
||||
}
|
||||
}
|
||||
largeResp, err := b.Finish()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to build large response: %v", err)
|
||||
}
|
||||
|
||||
// Verify the response is large enough for truncation.
|
||||
if len(largeResp) <= 512 {
|
||||
t.Fatalf("test response not large enough for truncation: %d bytes", len(largeResp))
|
||||
}
|
||||
|
||||
tr, err := truncateDNSResponse(largeResp, 512)
|
||||
if err != nil {
|
||||
t.Fatalf("truncateDNSResponse failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify the truncated response:
|
||||
// 1. Fits within 512 bytes
|
||||
if len(tr) > 512 {
|
||||
t.Fatalf("truncated response exceeds 512 bytes: got %d", len(tr))
|
||||
}
|
||||
|
||||
// 2. Is syntactically valid
|
||||
var p dns.Parser
|
||||
h, err := p.Start(tr)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to parse truncated response: %v", err)
|
||||
}
|
||||
|
||||
// 3. Has TC (Truncated) bit set
|
||||
if !h.Truncated {
|
||||
t.Fatalf("expected TC (Truncated) bit to be set in truncated response")
|
||||
}
|
||||
}
|
||||
|
||||
// TestResolverSERVFAILOnImpossibleTruncation ensures that when a client
|
||||
// advertises a tiny EDNS buffer size such that the resolver cannot safely
|
||||
// encode even the header+question within that size, the resolver returns a
|
||||
// SERVFAIL response rather than an invalid/truncated packet.
|
||||
func TestResolverSERVFAILOnImpossibleTruncation(t *testing.T) {
|
||||
const domain = "srvfail.example.com."
|
||||
|
||||
// Build a request that advertises a very small EDNS size (50 bytes).
|
||||
// This is small enough to require truncation but large enough for header+question.
|
||||
request := dnspacket(domain, dns.TypeA, 50)
|
||||
|
||||
// Verify EDNS extraction enforces the RFC 6891 minimum of 512.
|
||||
ednsSize := extractEDNS0UDPSize(request)
|
||||
if ednsSize != 512 {
|
||||
t.Fatalf("EDNS extraction failed: expected 512, got %d", ednsSize)
|
||||
}
|
||||
|
||||
// Build a very large upstream response for the same domain so that the
|
||||
// resolver will attempt truncation and fail.
|
||||
_, largeResponse := makeLargeResponse(t, domain)
|
||||
|
||||
// Run a test DNS server returning the large response.
|
||||
port := runDNSServer(t, nil, largeResponse, func(isTCP bool, gotRequest []byte) {
|
||||
// DNS server received a request; just ensure the server is reachable
|
||||
})
|
||||
|
||||
// Configure resolver to forward queries to our server.
|
||||
r := newResolver(t)
|
||||
defer r.Close()
|
||||
cfg := Config{
|
||||
Routes: map[dnsname.FQDN][]*dnstype.Resolver{
|
||||
dnsname.FQDN("."): {{Addr: fmt.Sprintf("127.0.0.1:%d", port)}},
|
||||
},
|
||||
}
|
||||
if err := r.SetConfig(cfg); err != nil {
|
||||
t.Fatalf("SetConfig: %v", err)
|
||||
}
|
||||
|
||||
// Query the resolver over UDP with the tiny EDNS size.
|
||||
ctx := context.Background()
|
||||
out, err := r.Query(ctx, request, "udp", netip.MustParseAddrPort("127.0.0.1:12345"))
|
||||
if err != nil {
|
||||
t.Fatalf("Query failed: %v", err)
|
||||
}
|
||||
|
||||
// The response should be either:
|
||||
// 1. A SERVFAIL (if truncation was impossible), or
|
||||
// 2. A response that fits within the effective EDNS size (512 bytes) with TC bit set.
|
||||
var p dns.Parser
|
||||
h, err := p.Start(out)
|
||||
if err != nil {
|
||||
t.Fatalf("parse response: %v", err)
|
||||
}
|
||||
|
||||
if h.RCode == dns.RCodeServerFailure {
|
||||
// Good - impossible truncation was handled correctly
|
||||
return
|
||||
}
|
||||
|
||||
// Otherwise the response must fit within 512 bytes and have TC set.
|
||||
if len(out) > 512 {
|
||||
t.Fatalf("expected SERVFAIL or <=512 byte response, got %d bytes with RCode=%v",
|
||||
len(out), h.RCode)
|
||||
}
|
||||
if !h.Truncated {
|
||||
t.Fatalf("expected TC bit set for truncated response")
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue