diff --git a/acme/challenge.go b/acme/challenge.go index 131c4f251..82e326e4d 100644 --- a/acme/challenge.go +++ b/acme/challenge.go @@ -107,7 +107,7 @@ func (ch *Challenge) ToLog() (interface{}, error) { // 'validated' attributes are updated. func (ch *Challenge) Validate(ctx context.Context, db DB, jwk *jose.JSONWebKey, payload []byte) error { // If already valid or invalid then return without performing validation. - if ch.Status != StatusPending { + if ch.Status != StatusPending && ch.Status != StatusInvalid { return nil } switch ch.Type { @@ -402,6 +402,7 @@ func dns01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose.JSONWebK vc := MustClientFromContext(ctx) txtRecords, err := vc.LookupTxt(dns01ChallengeHost(domain)) if err != nil { + logrus.Warnf("error looking up TXT records for domain %s: %v", domain, err) return storeError(ctx, db, ch, false, WrapError(ErrorDNSType, err, "error looking up TXT records for domain %s", domain)) } @@ -1611,6 +1612,7 @@ func KeyAuthorization(token string, jwk *jose.JSONWebKey) (string, error) { func storeError(ctx context.Context, db DB, ch *Challenge, markInvalid bool, err *Error) error { ch.Error = err if markInvalid { + logrus.Warnf("marking challenge %s as invalid: %v", ch.ID, err) ch.Status = StatusInvalid } if err := db.UpdateChallenge(ctx, ch); err != nil { diff --git a/acme/client.go b/acme/client.go index 8f506ef95..57ec24778 100644 --- a/acme/client.go +++ b/acme/client.go @@ -3,9 +3,14 @@ package acme import ( "context" "crypto/tls" + "fmt" "net" "net/http" + "os" "time" + + "github.com/miekg/dns" + "github.com/sirupsen/logrus" ) // Client is the interface used to verify ACME challenges. @@ -72,8 +77,102 @@ func (c *client) Get(url string) (*http.Response, error) { return c.http.Get(url) } +var timeouts [5]time.Duration = [5]time.Duration{(time.Second * 1), (time.Second * 1), (time.Second * 2), (time.Second * 4), (time.Second * 2)} + +func ResolveWithTimeout(name, resolver string, qtype, qclass uint16) (*dns.Msg, error) { + client := new(dns.Client) + msg := &dns.Msg{ + MsgHdr: dns.MsgHdr{ + Id: dns.Id(), + RecursionDesired: true, + }, + Question: []dns.Question{{Name: dns.Fqdn(name), Qtype: qtype, Qclass: qclass}}, + } + msg.AuthenticatedData = true + msg.SetEdns0(4096, true) + + for i := 0; i < len(timeouts); i++ { + + client.Timeout = timeouts[i] + resp, _, err := client.Exchange(msg, fmt.Sprintf("%s:53", resolver)) + if err == nil && resp.Truncated { + tcpConn, _ := dns.Dial("tcp", fmt.Sprintf("%s:53", resolver)) + resp, _, err = client.ExchangeWithConn(msg, tcpConn) + } + if err != nil { + if err, ok := err.(net.Error); ok && err.Timeout() { + logrus.Warnf("Timeout querying %s records '%s' after %v", dns.TypeToString[qtype], name, timeouts[i]) + continue + } + return nil, err + } + + return resp, nil + + } + return nil, &net.DNSError{ + Name: name, + Err: "Final timeout.", + IsTimeout: true, + } +} + +func LookupTxt(name, resolver string) ([]string, error) { + logrus.Infof("Using custom resolver %s for lookup of %s", resolver, name) + resp, err := ResolveWithTimeout(name, resolver, dns.TypeTXT, dns.ClassINET) + if err != nil { + logrus.Warnf("Failed to lookup %s: %v", name, err) + return nil, err + } + nsData := []string{} + data := []string{} + + // Check if TXT records are present + for _, answer := range resp.Answer { + if txt, ok := answer.(*dns.TXT); ok { + logrus.Infof("Resolved TXT records for %s: %s", name, txt.Txt) + data = append(data, txt.Txt...) + } + } + if len(data) > 0 { + return data, nil + } + resp, err = ResolveWithTimeout(name, resolver, dns.TypeCNAME, dns.ClassINET) + if err != nil { + logrus.Warnf("Failed to lookup %s: %v", name, err) + return nil, err + } + // Check if CNAME records are present + for _, answer := range resp.Answer { + if cname, ok := answer.(*dns.CNAME); ok { + logrus.Infof("Resolved CNAME records for %s: %s", name, cname.Target) + // Get NS records for the CNAME + resp, err := ResolveWithTimeout(cname.Target, resolver, dns.TypeSOA, dns.ClassINET) + if err != nil { + logrus.Warnf("Failed to lookup %s: %v", cname.Target, err) + continue + } + for _, answer := range resp.Ns { + if ns, ok := answer.(*dns.SOA); ok { + logrus.Infof("Resolved NS records for %s: %s", cname.Target, ns.Ns) + nsData = append(nsData, ns.Ns) + } + } + for _, ns := range nsData { + logrus.Infof("Resolving %s using %s", cname.Target, ns) + return LookupTxt(cname.Target, ns) + } + } + } + + return data, nil +} func (c *client) LookupTxt(name string) ([]string, error) { - return net.LookupTXT(name) + resolver := os.Getenv("DNS_RESOLVER") + if resolver != "" { + return LookupTxt(name, resolver) + } + return c.LookupTxt(name) } func (c *client) TLSDial(network, addr string, config *tls.Config) (*tls.Conn, error) { diff --git a/go.mod b/go.mod index 9efb7ad4f..789a7d8fd 100644 --- a/go.mod +++ b/go.mod @@ -179,11 +179,13 @@ require ( go.opentelemetry.io/otel/metric v1.29.0 // indirect go.opentelemetry.io/otel/trace v1.29.0 // indirect go.opentelemetry.io/proto/otlp v1.2.0 // indirect + golang.org/x/mod v0.20.0 // indirect golang.org/x/oauth2 v0.23.0 // indirect golang.org/x/sync v0.8.0 // indirect golang.org/x/sys v0.25.0 // indirect golang.org/x/text v0.18.0 // indirect golang.org/x/time v0.6.0 // indirect + golang.org/x/tools v0.24.0 // indirect google.golang.org/genproto v0.0.0-20240903143218-8af14fe29dc1 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect @@ -195,4 +197,5 @@ replace github.com/smallstep/nosql => github.com/hm-edu/nosql v0.4.1-0.202405061 require ( github.com/eclipse/paho.mqtt.golang v1.4.3 github.com/gorilla/websocket v1.5.0 // indirect + github.com/miekg/dns v1.1.61 ) diff --git a/go.sum b/go.sum index d7ce6f9bc..943bdd211 100644 --- a/go.sum +++ b/go.sum @@ -393,6 +393,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= +github.com/miekg/dns v1.1.61 h1:nLxbwF3XxhwVSm8g9Dghm9MHPaUZuqhPiGL+675ZmEs= +github.com/miekg/dns v1.1.61/go.mod h1:mnAarhS3nWaW+NVP2wTkYVIZyHNJ098SJZUki3eykwQ= github.com/miekg/pkcs11 v1.0.3-0.20190429190417-a667d056470f/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= github.com/miekg/pkcs11 v1.1.1 h1:Ugu9pdy6vAYku5DEpVWVFPYnzV+bxB+iRdbuFSu7TvU= github.com/miekg/pkcs11 v1.1.1/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= @@ -585,6 +587,8 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0= +golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -686,6 +690,8 @@ golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapK golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24= +golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=