diff --git a/go.mod b/go.mod index 8ad8442..9c25989 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,8 @@ go 1.20 require ( github.com/caddyserver/caddy/v2 v2.6.4 + github.com/mholt/acmez v1.1.0 + github.com/miekg/dns v1.1.50 ) require ( diff --git a/stub.go b/stub.go index d91ad85..361b2e0 100644 --- a/stub.go +++ b/stub.go @@ -1,14 +1,23 @@ package stub import ( + "context" + + "github.com/miekg/dns" + "github.com/mholt/acmez" + "github.com/mholt/acmez/acme" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" ) +// TTL of the challenge TXT record to serve +const challenge_ttl = 600 // (anything is probably fine here) + type StubDNS struct { // the address & port on which to serve DNS for the challenge Address string `json:"address,omitempty"` + server *dns.Server // set in Present() } @@ -69,8 +78,73 @@ func (s *StubDNS) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { } +func (s *StubDNS) Present(ctx context.Context, challenge acme.Challenge) error { + // get challenge parameters + fqdn := dns.Fqdn(challenge.DNS01TXTRecordName()) + content := challenge.DNS01KeyAuthorization() + // spawn the server + handler := s.make_handler(fqdn, content) + dns.HandleFunc(".", handler) + server := &dns.Server{Addr: s.Address, Net: "udp", TsigSecret: nil,} + go server.ListenAndServe() + + // store the server for shutdown later + s.server = server + return nil +} + +func (p *StubDNS) CleanUp(ctx context.Context, _ acme.Challenge) error { + if p.server == nil { + return nil + } else { + return p.server.ShutdownContext(ctx) + } +} + +func (s *StubDNS) make_handler(fqdn string, txt string) dns.HandlerFunc { + handler := func(w dns.ResponseWriter, r *dns.Msg) { + m := new(dns.Msg) + m.SetReply(r) + if len(r.Question) != 1 { + m.Rcode = dns.RcodeRefused + m.Answer = []dns.RR{} + w.WriteMsg(m) + return + } + + q := r.Question[0] + domain := q.Name + + valid := r.Response == false && + (q.Qclass == dns.ClassINET || q.Qclass == dns.ClassANY) && + q.Qtype == dns.TypeTXT + if !valid { + m.Rcode = dns.RcodeNotImplemented + m.Answer = []dns.RR{} + } else if domain != fqdn { + m.Rcode = dns.RcodeNameError + m.Answer = []dns.RR{} + } else { + m.Authoritative = true + rr := new(dns.TXT) + rr.Hdr = dns.RR_Header{ + Name: domain, + Rrtype: dns.TypeTXT, + Class: dns.ClassINET, + Ttl: uint32(challenge_ttl), + } + rr.Txt = []string{txt} + m.Answer = []dns.RR{rr} + } + w.WriteMsg(m) + } + + return handler +} + // Interface guards var ( + _ acmez.Solver = (*StubDNS)(nil) _ caddy.Provisioner = (*StubDNS)(nil) _ caddyfile.Unmarshaler = (*StubDNS)(nil) )