Merge pull request #1121 from letsencrypt/no-500-dns
Don't serve 500's on DNS timeout.
This commit is contained in:
commit
af83b734eb
|
|
@ -0,0 +1,29 @@
|
|||
package dns
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/letsencrypt/boulder/core"
|
||||
)
|
||||
|
||||
const detailDNSTimeout = "DNS query timed out"
|
||||
const detailDNSNetFailure = "DNS networking error"
|
||||
const detailServerFailure = "Server failure at resolver"
|
||||
|
||||
// ProblemDetailsFromDNSError checks the error returned from Lookup...
|
||||
// methods and tests if the error was an underlying net.OpError or an error
|
||||
// caused by resolver returning SERVFAIL or other invalid Rcodes and returns
|
||||
// the relevant core.ProblemDetails.
|
||||
func ProblemDetailsFromDNSError(err error) *core.ProblemDetails {
|
||||
problem := &core.ProblemDetails{Type: core.ConnectionProblem}
|
||||
if netErr, ok := err.(*net.OpError); ok {
|
||||
if netErr.Timeout() {
|
||||
problem.Detail = detailDNSTimeout
|
||||
} else {
|
||||
problem.Detail = detailDNSNetFailure
|
||||
}
|
||||
} else {
|
||||
problem.Detail = detailServerFailure
|
||||
}
|
||||
return problem
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
package dns
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"github.com/letsencrypt/boulder/core"
|
||||
"github.com/letsencrypt/boulder/mocks"
|
||||
)
|
||||
|
||||
func TestProblemDetailsFromDNSError(t *testing.T) {
|
||||
testCases := []struct {
|
||||
err error
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
mocks.TimeoutError(),
|
||||
detailDNSTimeout,
|
||||
}, {
|
||||
errors.New("other failure"),
|
||||
detailServerFailure,
|
||||
}, {
|
||||
&net.OpError{Err: errors.New("some net error")},
|
||||
detailDNSNetFailure,
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
err := ProblemDetailsFromDNSError(tc.err)
|
||||
if err.Type != core.ConnectionProblem {
|
||||
t.Errorf("ProblemDetailsFromDNSError(%q).Type = %q, expected %q", tc.err, err.Type, core.ConnectionProblem)
|
||||
}
|
||||
if err.Detail != tc.expected {
|
||||
t.Errorf("ProblemDetailsFromDNSError(%q).Detail = %q, expected %q", tc.err, err.Detail, tc.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -12,6 +12,7 @@ import (
|
|||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
|
@ -44,11 +45,35 @@ func (mock *DNSResolver) LookupTXT(hostname string) ([]string, time.Duration, er
|
|||
return []string{"hostname"}, 0, nil
|
||||
}
|
||||
|
||||
// TimeoutError returns a a net.OpError for which Timeout() returns true.
|
||||
func TimeoutError() *net.OpError {
|
||||
return &net.OpError{
|
||||
Err: os.NewSyscallError("ugh timeout", timeoutError{}),
|
||||
}
|
||||
}
|
||||
|
||||
type timeoutError struct{}
|
||||
|
||||
func (t timeoutError) Error() string {
|
||||
return "so sloooow"
|
||||
}
|
||||
func (t timeoutError) Timeout() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// LookupHost is a mock
|
||||
func (mock *DNSResolver) LookupHost(hostname string) ([]net.IP, time.Duration, error) {
|
||||
if hostname == "always.invalid" || hostname == "invalid.invalid" {
|
||||
return []net.IP{}, 0, nil
|
||||
}
|
||||
if hostname == "always.timeout" {
|
||||
return []net.IP{}, 0, TimeoutError()
|
||||
}
|
||||
if hostname == "always.error" {
|
||||
return []net.IP{}, 0, &net.OpError{
|
||||
Err: errors.New("some net error"),
|
||||
}
|
||||
}
|
||||
ip := net.ParseIP("127.0.0.1")
|
||||
return []net.IP{ip}, 0, nil
|
||||
}
|
||||
|
|
@ -58,6 +83,8 @@ func (mock *DNSResolver) LookupCAA(domain string) ([]*dns.CAA, time.Duration, er
|
|||
var results []*dns.CAA
|
||||
var record dns.CAA
|
||||
switch strings.TrimRight(domain, ".") {
|
||||
case "caa-timeout.com":
|
||||
return nil, 0, TimeoutError()
|
||||
case "reserved.com":
|
||||
record.Tag = "issue"
|
||||
record.Value = "symantec.com"
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import (
|
|||
|
||||
"github.com/letsencrypt/boulder/cmd"
|
||||
"github.com/letsencrypt/boulder/core"
|
||||
"github.com/letsencrypt/boulder/dns"
|
||||
blog "github.com/letsencrypt/boulder/log"
|
||||
)
|
||||
|
||||
|
|
@ -77,19 +78,23 @@ func NewRegistrationAuthorityImpl(clk clock.Clock, logger *blog.AuditLogger, sta
|
|||
return ra
|
||||
}
|
||||
|
||||
var errUnparseableEmail = errors.New("not a valid e-mail address")
|
||||
var errEmptyDNSResponse = errors.New("empty DNS response")
|
||||
|
||||
func validateEmail(address string, resolver core.DNSResolver) (rtt time.Duration, err error) {
|
||||
_, err = mail.ParseAddress(address)
|
||||
if err != nil {
|
||||
err = core.MalformedRequestError(fmt.Sprintf("%s is not a valid e-mail address", address))
|
||||
return
|
||||
return time.Duration(0), errUnparseableEmail
|
||||
}
|
||||
splitEmail := strings.SplitN(address, "@", -1)
|
||||
domain := strings.ToLower(splitEmail[len(splitEmail)-1])
|
||||
var mx []string
|
||||
mx, rtt, err = resolver.LookupMX(domain)
|
||||
if err != nil || len(mx) == 0 {
|
||||
err = core.MalformedRequestError(fmt.Sprintf("No MX record for domain %s", domain))
|
||||
return
|
||||
result, rtt, err := resolver.LookupHost(domain)
|
||||
if err == nil && len(result) == 0 {
|
||||
err = errEmptyDNSResponse
|
||||
}
|
||||
if err != nil {
|
||||
problem := dns.ProblemDetailsFromDNSError(err)
|
||||
err = core.MalformedRequestError(problem.Detail)
|
||||
}
|
||||
|
||||
return
|
||||
|
|
@ -216,10 +221,11 @@ func (ra *RegistrationAuthorityImpl) validateContacts(contacts []*core.AcmeURL)
|
|||
continue
|
||||
case "mailto":
|
||||
rtt, err := validateEmail(contact.Opaque, ra.DNSResolver)
|
||||
ra.stats.TimingDuration("RA.DNS.RTT.MX", rtt, 1.0)
|
||||
ra.stats.TimingDuration("RA.DNS.RTT.A", rtt, 1.0)
|
||||
ra.stats.Inc("RA.DNS.Rate", 1, 1.0)
|
||||
if err != nil {
|
||||
return err
|
||||
return core.MalformedRequestError(fmt.Sprintf(
|
||||
"Validation of contact %s failed: %s", contact, err))
|
||||
}
|
||||
default:
|
||||
err = core.MalformedRequestError(fmt.Sprintf("Contact method %s is not supported", contact.Scheme))
|
||||
|
|
@ -281,18 +287,6 @@ func (ra *RegistrationAuthorityImpl) NewAuthorization(request core.Authorization
|
|||
}
|
||||
}
|
||||
|
||||
// Check CAA records for the requested identifier
|
||||
present, valid, err := ra.VA.CheckCAARecords(identifier)
|
||||
if err != nil {
|
||||
return authz, err
|
||||
}
|
||||
// AUDIT[ Certificate Requests ] 11917fa4-10ef-4e0d-9105-bacbe7836a3c
|
||||
ra.log.Audit(fmt.Sprintf("Checked CAA records for %s, registration ID %d [Present: %t, Valid for issuance: %t]", identifier.Value, regID, present, valid))
|
||||
if !valid {
|
||||
err = errors.New("CAA check for identifier failed")
|
||||
return authz, err
|
||||
}
|
||||
|
||||
// Create validations. The WFE will update them with URIs before sending them out.
|
||||
challenges, combinations, err := ra.PA.ChallengesFor(identifier, ®.Key)
|
||||
|
||||
|
|
|
|||
|
|
@ -279,7 +279,6 @@ func TestValidateContacts(t *testing.T) {
|
|||
tel, _ := core.ParseAcmeURL("tel:")
|
||||
ansible, _ := core.ParseAcmeURL("ansible:earth.sol.milkyway.laniakea/letsencrypt")
|
||||
validEmail, _ := core.ParseAcmeURL("mailto:admin@email.com")
|
||||
invalidEmail, _ := core.ParseAcmeURL("mailto:admin@example.com")
|
||||
malformedEmail, _ := core.ParseAcmeURL("mailto:admin.com")
|
||||
|
||||
err := ra.validateContacts([]*core.AcmeURL{})
|
||||
|
|
@ -294,30 +293,33 @@ func TestValidateContacts(t *testing.T) {
|
|||
err = ra.validateContacts([]*core.AcmeURL{validEmail})
|
||||
test.AssertNotError(t, err, "Valid Email")
|
||||
|
||||
err = ra.validateContacts([]*core.AcmeURL{invalidEmail})
|
||||
test.AssertError(t, err, "Invalid Email")
|
||||
|
||||
err = ra.validateContacts([]*core.AcmeURL{malformedEmail})
|
||||
test.AssertError(t, err, "Malformed Email")
|
||||
|
||||
err = ra.validateContacts([]*core.AcmeURL{ansible})
|
||||
test.AssertError(t, err, "Unknown scehme")
|
||||
test.AssertError(t, err, "Unknown scheme")
|
||||
}
|
||||
|
||||
func TestValidateEmail(t *testing.T) {
|
||||
_, err := validateEmail("an email`", &mocks.DNSResolver{})
|
||||
test.AssertError(t, err, "Malformed")
|
||||
|
||||
_, err = validateEmail("a@not.a.domain", &mocks.DNSResolver{})
|
||||
test.AssertError(t, err, "Cannot resolve")
|
||||
t.Logf("No Resolve: %s", err)
|
||||
|
||||
_, err = validateEmail("a@example.com", &mocks.DNSResolver{})
|
||||
test.AssertError(t, err, "No MX Record")
|
||||
t.Logf("No MX: %s", err)
|
||||
|
||||
_, err = validateEmail("a@email.com", &mocks.DNSResolver{})
|
||||
test.AssertNotError(t, err, "Valid")
|
||||
testCases := []struct {
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{"an email`", errUnparseableEmail.Error()},
|
||||
{"a@always.invalid", "Server failure at resolver"},
|
||||
{"a@always.timeout", "DNS query timed out"},
|
||||
{"a@always.error", "DNS networking error"},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
_, err := validateEmail(tc.input, &mocks.DNSResolver{})
|
||||
if err.Error() != tc.expected {
|
||||
t.Errorf("validateEmail(%q): got %#v, expected %#v",
|
||||
tc.input, err, tc.expected)
|
||||
}
|
||||
}
|
||||
if _, err := validateEmail("a@email.com", &mocks.DNSResolver{}); err != nil {
|
||||
t.Errorf("Expected a@email.com to validate, but it failed: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewRegistration(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ import (
|
|||
"crypto/tls"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
|
|
@ -28,18 +27,14 @@ import (
|
|||
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/miekg/dns"
|
||||
|
||||
"github.com/letsencrypt/boulder/core"
|
||||
bdns "github.com/letsencrypt/boulder/dns"
|
||||
blog "github.com/letsencrypt/boulder/log"
|
||||
)
|
||||
|
||||
const maxCNAME = 16 // Prevents infinite loops. Same limit as BIND.
|
||||
const maxRedirect = 10
|
||||
|
||||
var validationTimeout = time.Second * 5
|
||||
|
||||
// ErrTooManyCNAME is returned by CheckCAARecords if it has to follow too many
|
||||
// consecutive CNAME lookups.
|
||||
var ErrTooManyCNAME = errors.New("too many CNAME/DNAME lookups")
|
||||
|
||||
// ValidationAuthorityImpl represents a VA
|
||||
type ValidationAuthorityImpl struct {
|
||||
RA core.RegistrationAuthority
|
||||
|
|
@ -124,24 +119,6 @@ func verifyValidationJWS(validation *jose.JsonWebSignature, accountKey *jose.Jso
|
|||
return nil
|
||||
}
|
||||
|
||||
// problemDetailsFromDNSError checks the error returned from Lookup...
|
||||
// methods and tests if the error was an underlying net.OpError or an error
|
||||
// caused by resolver returning SERVFAIL or other invalid Rcodes and returns
|
||||
// the relevant core.ProblemDetails.
|
||||
func problemDetailsFromDNSError(err error) *core.ProblemDetails {
|
||||
problem := &core.ProblemDetails{Type: core.ConnectionProblem}
|
||||
if netErr, ok := err.(*net.OpError); ok {
|
||||
if netErr.Timeout() {
|
||||
problem.Detail = "DNS query timed out"
|
||||
} else if netErr.Temporary() {
|
||||
problem.Detail = "Temporary network connectivity error"
|
||||
}
|
||||
} else {
|
||||
problem.Detail = "Server failure at resolver"
|
||||
}
|
||||
return problem
|
||||
}
|
||||
|
||||
// getAddr will query for all A records associated with hostname and return the
|
||||
// prefered address, the first net.IP in the addrs slice, and all addresses resolved.
|
||||
// This is the same choice made by the Go internal resolution library used by
|
||||
|
|
@ -150,7 +127,7 @@ func problemDetailsFromDNSError(err error) *core.ProblemDetails {
|
|||
func (va ValidationAuthorityImpl) getAddr(hostname string) (addr net.IP, addrs []net.IP, problem *core.ProblemDetails) {
|
||||
addrs, rtt, err := va.DNSResolver.LookupHost(hostname)
|
||||
if err != nil {
|
||||
problem = problemDetailsFromDNSError(err)
|
||||
problem = bdns.ProblemDetailsFromDNSError(err)
|
||||
va.log.Debug(fmt.Sprintf("%s DNS failure: %s", hostname, err))
|
||||
return
|
||||
}
|
||||
|
|
@ -634,7 +611,7 @@ func (va *ValidationAuthorityImpl) validateDNS01(identifier core.AcmeIdentifier,
|
|||
|
||||
if err != nil {
|
||||
challenge.Status = core.StatusInvalid
|
||||
challenge.Error = problemDetailsFromDNSError(err)
|
||||
challenge.Error = bdns.ProblemDetailsFromDNSError(err)
|
||||
va.log.Debug(fmt.Sprintf("%s [%s] DNS failure: %s", challenge.Type, identifier, err))
|
||||
return challenge, challenge.Error
|
||||
}
|
||||
|
|
@ -654,6 +631,24 @@ func (va *ValidationAuthorityImpl) validateDNS01(identifier core.AcmeIdentifier,
|
|||
return challenge, challenge.Error
|
||||
}
|
||||
|
||||
func (va *ValidationAuthorityImpl) checkCAA(identifier core.AcmeIdentifier, regID int64) *core.ProblemDetails {
|
||||
// Check CAA records for the requested identifier
|
||||
present, valid, err := va.CheckCAARecords(identifier)
|
||||
if err != nil {
|
||||
va.log.Warning(fmt.Sprintf("Problem checking CAA: %s", err))
|
||||
return bdns.ProblemDetailsFromDNSError(err)
|
||||
}
|
||||
// AUDIT[ Certificate Requests ] 11917fa4-10ef-4e0d-9105-bacbe7836a3c
|
||||
va.log.Audit(fmt.Sprintf("Checked CAA records for %s, registration ID %d [Present: %t, Valid for issuance: %t]", identifier.Value, regID, present, valid))
|
||||
if !valid {
|
||||
return &core.ProblemDetails{
|
||||
Type: core.ConnectionProblem,
|
||||
Detail: "CAA check for identifier failed",
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Overall validation process
|
||||
|
||||
func (va *ValidationAuthorityImpl) validate(authz core.Authorization, challengeIndex int) {
|
||||
|
|
@ -673,20 +668,7 @@ func (va *ValidationAuthorityImpl) validate(authz core.Authorization, challengeI
|
|||
var err error
|
||||
|
||||
vStart := va.clk.Now()
|
||||
switch authz.Challenges[challengeIndex].Type {
|
||||
case core.ChallengeTypeSimpleHTTP:
|
||||
// TODO(https://github.com/letsencrypt/boulder/issues/894): Delete this case
|
||||
authz.Challenges[challengeIndex], err = va.validateSimpleHTTP(authz.Identifier, authz.Challenges[challengeIndex])
|
||||
case core.ChallengeTypeDVSNI:
|
||||
// TODO(https://github.com/letsencrypt/boulder/issues/894): Delete this case
|
||||
authz.Challenges[challengeIndex], err = va.validateDvsni(authz.Identifier, authz.Challenges[challengeIndex])
|
||||
case core.ChallengeTypeHTTP01:
|
||||
authz.Challenges[challengeIndex], err = va.validateHTTP01(authz.Identifier, authz.Challenges[challengeIndex])
|
||||
case core.ChallengeTypeTLSSNI01:
|
||||
authz.Challenges[challengeIndex], err = va.validateTLSSNI01(authz.Identifier, authz.Challenges[challengeIndex])
|
||||
case core.ChallengeTypeDNS01:
|
||||
authz.Challenges[challengeIndex], err = va.validateDNS01(authz.Identifier, authz.Challenges[challengeIndex])
|
||||
}
|
||||
authz.Challenges[challengeIndex], err = va.validateChallengeAndCAA(authz.Identifier, authz.Challenges[challengeIndex], authz.RegistrationID)
|
||||
va.stats.TimingDuration(fmt.Sprintf("VA.Validations.%s.%s", authz.Challenges[challengeIndex].Type, authz.Challenges[challengeIndex].Status), time.Since(vStart), 1.0)
|
||||
|
||||
if err != nil {
|
||||
|
|
@ -709,6 +691,42 @@ func (va *ValidationAuthorityImpl) validate(authz core.Authorization, challengeI
|
|||
va.RA.OnValidationUpdate(authz)
|
||||
}
|
||||
|
||||
func (va *ValidationAuthorityImpl) validateChallengeAndCAA(identifier core.AcmeIdentifier, challenge core.Challenge, regID int64) (core.Challenge, error) {
|
||||
result, err := va.validateChallenge(identifier, challenge)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
|
||||
// Checking CAA happens after challenge validation because DNS errors affect
|
||||
// both, and giving a DNS error on validation makes more sense than a DNS
|
||||
// error on CAA.
|
||||
problemDetails := va.checkCAA(identifier, regID)
|
||||
if problemDetails != nil {
|
||||
challenge.Error = problemDetails
|
||||
challenge.Status = core.StatusInvalid
|
||||
return result, problemDetails
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (va *ValidationAuthorityImpl) validateChallenge(identifier core.AcmeIdentifier, challenge core.Challenge) (core.Challenge, error) {
|
||||
switch challenge.Type {
|
||||
case core.ChallengeTypeSimpleHTTP:
|
||||
// TODO(https://github.com/letsencrypt/boulder/issues/894): Delete this case
|
||||
return va.validateSimpleHTTP(identifier, challenge)
|
||||
case core.ChallengeTypeDVSNI:
|
||||
// TODO(https://github.com/letsencrypt/boulder/issues/894): Delete this case
|
||||
return va.validateDvsni(identifier, challenge)
|
||||
case core.ChallengeTypeHTTP01:
|
||||
return va.validateHTTP01(identifier, challenge)
|
||||
case core.ChallengeTypeTLSSNI01:
|
||||
return va.validateTLSSNI01(identifier, challenge)
|
||||
case core.ChallengeTypeDNS01:
|
||||
return va.validateDNS01(identifier, challenge)
|
||||
}
|
||||
return core.Challenge{}, fmt.Errorf("invalid challenge type %s", challenge.Type)
|
||||
}
|
||||
|
||||
// UpdateValidations runs the validate() method asynchronously using goroutines.
|
||||
func (va *ValidationAuthorityImpl) UpdateValidations(authz core.Authorization, challengeIndex int) error {
|
||||
go va.validate(authz, challengeIndex)
|
||||
|
|
|
|||
|
|
@ -1061,6 +1061,21 @@ func TestUpdateValidations(t *testing.T) {
|
|||
test.Assert(t, (took < (time.Second * 3)), "UpdateValidations blocked")
|
||||
}
|
||||
|
||||
func TestCAATimeout(t *testing.T) {
|
||||
stats, _ := statsd.NewNoopClient()
|
||||
va := NewValidationAuthorityImpl(&PortConfig{}, nil, stats, clock.Default())
|
||||
va.DNSResolver = &mocks.DNSResolver{}
|
||||
va.IssuerDomain = "letsencrypt.org"
|
||||
err := va.checkCAA(core.AcmeIdentifier{Type: core.IdentifierDNS, Value: "caa-timeout.com"}, 101)
|
||||
if err.Type != core.ConnectionProblem {
|
||||
t.Errorf("Expected timeout error type %s, got %s", core.ConnectionProblem, err.Type)
|
||||
}
|
||||
expected := "DNS query timed out"
|
||||
if err.Detail != expected {
|
||||
t.Errorf("checkCAA: got %s, expected %s", err.Detail, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCAAChecking(t *testing.T) {
|
||||
type CAATest struct {
|
||||
Domain string
|
||||
|
|
|
|||
Loading…
Reference in New Issue