diff --git a/src/net/dnsclient_unix.go b/src/net/dnsclient_unix.go index c291d5eb4f20e0..51887bd839b091 100644 --- a/src/net/dnsclient_unix.go +++ b/src/net/dnsclient_unix.go @@ -20,6 +20,7 @@ import ( "io" "os" "runtime" + "strconv" "sync" "sync/atomic" "time" @@ -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) { @@ -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 @@ -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 @@ -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 { @@ -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) { @@ -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 diff --git a/src/net/dnsclient_unix_test.go b/src/net/dnsclient_unix_test.go index 0da36303cc8887..f0bcb0329b709d 100644 --- a/src/net/dnsclient_unix_test.go +++ b/src/net/dnsclient_unix_test.go @@ -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) + } +}