Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
161 changes: 146 additions & 15 deletions src/net/dnsclient_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"io"
"os"
"runtime"
"strconv"
"sync"
"sync/atomic"
"time"
Expand All @@ -41,14 +42,8 @@ var (
errLameReferral = errors.New("lame referral")
errCannotUnmarshalDNSMessage = errors.New("cannot unmarshal DNS message")
errCannotMarshalDNSMessage = errors.New("cannot marshal DNS message")
errServerMisbehaving = errors.New("server misbehaving")
errInvalidDNSResponse = errors.New("invalid DNS response")
errNoAnswerFromDNSServer = errors.New("no answer from DNS server")

// errServerTemporarilyMisbehaving is like errServerMisbehaving, except
// that when it gets translated to a DNSError, the IsTemporary field
// gets set to true.
errServerTemporarilyMisbehaving = errors.New("server misbehaving")
)

func newRequest(q dnsmessage.Question, ad bool) (id uint16, udpReq, tcpReq []byte, err error) {
Expand Down Expand Up @@ -204,7 +199,8 @@ func (r *Resolver) exchange(ctx context.Context, server string, q dnsmessage.Que

// checkHeader performs basic sanity checks on the header.
func checkHeader(p *dnsmessage.Parser, h dnsmessage.Header) error {
rcode := extractExtendedRCode(*p, h)
pOPT := *p
rcode := extractExtendedRCode(&pOPT, h)

if rcode == dnsmessage.RCodeNameError {
return errNoSuchHost
Expand All @@ -221,16 +217,13 @@ func checkHeader(p *dnsmessage.Parser, h dnsmessage.Header) error {
return errLameReferral
}

if rcode != dnsmessage.RCodeSuccess && rcode != dnsmessage.RCodeNameError {
if rcode != dnsmessage.RCodeSuccess {
// None of the error codes make sense
// for the query we sent. If we didn't get
// a name error and we didn't get success,
// the server is behaving incorrectly or
// having temporary trouble.
if rcode == dnsmessage.RCodeServerFailure {
return errServerTemporarilyMisbehaving
}
return errServerMisbehaving
return extractExtendedDNSError(&pOPT, rcode)
}

return nil
Expand All @@ -256,7 +249,7 @@ func skipToAnswer(p *dnsmessage.Parser, qtype dnsmessage.Type) error {

// extractExtendedRCode extracts the extended RCode from the OPT resource (EDNS(0))
// If an OPT record is not found, the RCode from the hdr is returned.
func extractExtendedRCode(p dnsmessage.Parser, hdr dnsmessage.Header) dnsmessage.RCode {
func extractExtendedRCode(p *dnsmessage.Parser, hdr dnsmessage.Header) dnsmessage.RCode {
p.SkipAllAnswers()
p.SkipAllAuthorities()
for {
Expand All @@ -271,6 +264,144 @@ func extractExtendedRCode(p dnsmessage.Parser, hdr dnsmessage.Header) dnsmessage
}
}

// extractExtendedDNSError returns a *serverMisbehavingError error constructed from
// Extended DNS Error found in the OPT resource.
// The provided Parser should be as left by the extractExtendedRCode function.
func extractExtendedDNSError(p *dnsmessage.Parser, rcode dnsmessage.RCode) error {
ahdr, err := p.AdditionalHeader()
if err != nil {
return &serverMisbehavingError{
temporary: rcode == dnsmessage.RCodeServerFailure,
}
}
if ahdr.Type == dnsmessage.TypeOPT {
opt, err := p.OPTResource()
if err != nil {
return errCannotUnmarshalDNSMessage
}
for _, v := range opt.Options {
if v.Code == 15 {
if len(v.Data) < 2 {
return errCannotUnmarshalDNSMessage
}
err := &serverMisbehavingError{
temporary: rcode == dnsmessage.RCodeServerFailure,
extendedDNSErrorAvail: true,
extendedDNSErrorCode: uint16(v.Data[1]) | uint16(v.Data[0])<<8,
extendedDNSErrorText: v.Data[2:],
}
// The EXTRA-TEXT field is allowed to contain arbitrary UTF-8 string,
// this is nice, but it allows attacker to control the error message.
// We are a bit restrictive here and only allow printable ASCII characters.
if !safeExtendedDNSErrorText(err.extendedDNSErrorText) {
err.extendedDNSErrorText = nil
}
return err
}
}
}
return &serverMisbehavingError{
temporary: rcode == dnsmessage.RCodeServerFailure,
}
}

func safeExtendedDNSErrorText(text []byte) bool {
for _, v := range text {
if v < ' ' || v > '~' {
return false
}
}
return true
}

type serverMisbehavingError struct {
temporary bool

extendedDNSErrorAvail bool
extendedDNSErrorCode uint16
extendedDNSErrorText []byte
}

func (s *serverMisbehavingError) Error() string {
if s.extendedDNSErrorAvail {
extendedErr := s.extendedDNSError()
if extendedErr != "" {
return `server misbehaving: error from remote: ` + extendedErr
}
}
return "server misbehaving"
}

func prefixedErrorString(prefix string, text []byte) string {
if len(text) == 0 {
return prefix
}
return prefix + ": " + strconv.Quote(string(text))
}

func (e *serverMisbehavingError) extendedDNSError() string {
switch e.extendedDNSErrorCode {
case 0: // Other
if len(e.extendedDNSErrorText) == 0 {
return ""
}
return strconv.Quote(string(e.extendedDNSErrorText))
case 1:
return prefixedErrorString("Unsupported DNSKEY Algorithm", e.extendedDNSErrorText)
case 2:
return prefixedErrorString("Unsupported DS Digest Type", e.extendedDNSErrorText)
case 3:
return prefixedErrorString("Stale Answer", e.extendedDNSErrorText)
case 4:
return prefixedErrorString("Forged Answer", e.extendedDNSErrorText)
case 5:
return prefixedErrorString("DNSSEC Indeterminate", e.extendedDNSErrorText)
case 6:
return prefixedErrorString("DNSSEC Bogus", e.extendedDNSErrorText)
case 7:
return prefixedErrorString("Signature Expired", e.extendedDNSErrorText)
case 8:
return prefixedErrorString("Signature Not Yet Valid", e.extendedDNSErrorText)
case 9:
return prefixedErrorString("DNSKEY Missing", e.extendedDNSErrorText)
case 10:
return prefixedErrorString("RRSIGs Missing", e.extendedDNSErrorText)
case 11:
return prefixedErrorString("No Zone Key Bit Set", e.extendedDNSErrorText)
case 12:
return prefixedErrorString("NSEC Missing", e.extendedDNSErrorText)
case 13:
return prefixedErrorString("Cached Error", e.extendedDNSErrorText)
case 14:
return prefixedErrorString("Not Ready", e.extendedDNSErrorText)
case 15:
return prefixedErrorString("Blocked", e.extendedDNSErrorText)
case 16:
return prefixedErrorString("Censored", e.extendedDNSErrorText)
case 17:
return prefixedErrorString("Filtered", e.extendedDNSErrorText)
case 18:
return prefixedErrorString("Prohibited", e.extendedDNSErrorText)
case 19:
return prefixedErrorString("Stale NXDOMAIN Answer", e.extendedDNSErrorText)
case 20:
return prefixedErrorString("Not Authoritative", e.extendedDNSErrorText)
case 21:
return prefixedErrorString("Not Supported", e.extendedDNSErrorText)
case 22:
return prefixedErrorString("No Reachable Authority", e.extendedDNSErrorText)
case 23:
return prefixedErrorString("Network Error", e.extendedDNSErrorText)
case 24:
return prefixedErrorString("Invalid Data", e.extendedDNSErrorText)
default:
if len(e.extendedDNSErrorText) == 0 {
return ""
}
return strconv.Quote(string(e.extendedDNSErrorText))
}
}

// Do a lookup for a single name, which must be rooted
// (otherwise answer will not find the answers).
func (r *Resolver) tryOneName(ctx context.Context, cfg *dnsConfig, name string, qtype dnsmessage.Type) (dnsmessage.Parser, string, error) {
Expand Down Expand Up @@ -317,8 +448,8 @@ func (r *Resolver) tryOneName(ctx context.Context, cfg *dnsConfig, name string,
Name: name,
Server: server,
}
if err == errServerTemporarilyMisbehaving {
dnsErr.IsTemporary = true
if v, ok := err.(*serverMisbehavingError); ok {
dnsErr.IsTemporary = v.temporary
}
if err == errNoSuchHost {
// The name does not exist, so trying
Expand Down
43 changes: 42 additions & 1 deletion src/net/dnsclient_unix_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2655,7 +2655,48 @@ func TestExtendedRCode(t *testing.T) {
r := &Resolver{PreferGo: true, Dial: fake.DialContext}
_, _, err := r.tryOneName(context.Background(), getSystemDNSConfig(), "go.dev.", dnsmessage.TypeA)
var dnsErr *DNSError
if !(errors.As(err, &dnsErr) && dnsErr.Err == errServerMisbehaving.Error()) {
if !(errors.As(err, &dnsErr) && strings.Contains(err.Error(), "server misbehaving")) {
t.Fatalf("r.tryOneName(): unexpected error: %v", err)
}
}

func TestExtendedDNSError(t *testing.T) {
fake := fakeDNSServer{
rh: func(_, _ string, q dnsmessage.Message, _ time.Time) (dnsmessage.Message, error) {
var edns0Hdr dnsmessage.ResourceHeader
edns0Hdr.SetEDNS0(maxDNSPacketSize, dnsmessage.RCodeServerFailure, false)

return dnsmessage.Message{
Header: dnsmessage.Header{
ID: q.Header.ID,
Response: true,
RCode: dnsmessage.RCodeServerFailure,
},
Questions: []dnsmessage.Question{q.Questions[0]},
Additionals: []dnsmessage.Resource{{
Header: edns0Hdr,
Body: &dnsmessage.OPTResource{
Options: []dnsmessage.Option{
{Code: 59001},
{
Code: 15,
Data: slices.Concat(
[]byte{0, 15}, // Error Code (Blocked)
[]byte("your IP address has been blocked temporally"), // Extra-Text
),
},
},
},
}},
}, nil
},
}

r := &Resolver{PreferGo: true, Dial: fake.DialContext}
_, _, err := r.tryOneName(context.Background(), getSystemDNSConfig(), "dnssec-failed.org.", dnsmessage.TypeA)
if err == nil ||
!strings.Contains(err.Error(), "server misbehaving") ||
!strings.Contains(err.Error(), `Blocked: "your IP address has been blocked temporally"`) {
t.Fatalf("unexpected error: %v", err)
}
}