diff --git a/app.go b/app.go
new file mode 100644
index 0000000..20a72d8
--- /dev/null
+++ b/app.go
@@ -0,0 +1,188 @@
+package stub
+
+import (
+ "fmt"
+
+ "github.com/caddyserver/caddy/v2"
+ "github.com/caddyserver/caddy/v2/caddyconfig"
+ "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
+ "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
+
+ "github.com/miekg/dns"
+ "go.uber.org/zap"
+)
+
+type App struct {
+ // the address & port on which to serve DNS for the challenge
+ Address string `json:"address,omitempty"`
+
+ // Statically configured set of records to serve
+ Records []string `json:"records,omitempty"`
+
+ ctx *caddy.Context // set in Provision()
+ logger *zap.Logger // set in Provision()
+
+ requests chan request // set in Provision()
+ shutdown chan struct{} // set in Provision()
+}
+
+func (App) CaddyModule() caddy.ModuleInfo {
+ return caddy.ModuleInfo{
+ ID: "dns",
+ New: func() caddy.Module { return &App{} },
+ }
+}
+
+// Provision sets up the module. Implements caddy.Provisioner.
+func (a *App) Provision(ctx caddy.Context) error {
+ a.logger = ctx.Logger()
+ if a.requests == nil {
+ a.requests = make(chan request)
+ }
+ if a.Records == nil {
+ a.Records = []string{}
+ }
+ if a.shutdown == nil {
+ a.shutdown = make(chan struct{})
+ }
+ if a.Address == "" {
+ a.Address = ":53"
+ }
+ return nil
+}
+
+func (a *App) Start() error {
+ parsed, err := caddy.ParseNetworkAddress(a.Address)
+ if err != nil {
+ return err
+ }
+ parsed.Network = "udp"
+ a.logger.Debug("starting app", zap.Stringer("address", parsed))
+ srv := Server{
+ Address: parsed,
+ logger: a.logger,
+ shutdown: a.shutdown,
+ ctx: a.ctx,
+ requests: a.requests,
+ Records: make(map[key][]dns.RR),
+ }
+ for _, record_string := range a.Records {
+ record, err := dns.NewRR(record_string)
+ if err != nil {
+ return err
+ }
+ srv.insert_record(record)
+ }
+ if len(a.Records) > 0 {
+ a.logger.Debug("loaded records", zap.Int("count", len(a.Records)))
+ } else {
+ a.logger.Debug("no records loaded")
+ }
+
+ err = srv.start_stop_server()
+ if err != nil {
+ return err
+ }
+ go srv.main()
+
+ return nil
+}
+
+func (a *App) Stop() error {
+ a.logger.Debug("stopping app")
+ close(a.shutdown)
+ return nil
+}
+
+// UnmarshalCaddyfile sets up the DNS provider from Caddyfile tokens. Syntax:
+//
+// dns [address] {
+// bind
+// [record ""]
+// }
+func (a *App) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
+ for d.Next() {
+ if d.NextArg() {
+ a.Address = d.Val()
+ _, err := caddy.ParseNetworkAddress(a.Address)
+ if err != nil {
+ return d.WrapErr(err)
+ }
+ }
+ if d.NextArg() {
+ return d.ArgErr()
+ }
+ for nesting := d.Nesting(); d.NextBlock(nesting); {
+ switch d.Val() {
+ case "bind":
+ if a.Address != "" {
+ return d.Err("Bind address already set")
+ }
+ if d.NextArg() {
+ a.Address = d.Val()
+ _, err := caddy.ParseNetworkAddress(a.Address)
+ if err != nil {
+ return d.WrapErr(err)
+ }
+ }
+ if d.NextArg() {
+ return d.ArgErr()
+ }
+ case "record":
+ if d.NextArg() {
+ rr, err := dns.NewRR(d.Val())
+ if err != nil {
+ return d.WrapErr(err)
+ }
+ if rr == nil {
+ return d.Err("invalid empty record")
+ }
+ a.Records = append(a.Records, rr.String())
+ } else {
+ return d.ArgErr()
+ }
+ if d.NextArg() {
+ return d.ArgErr()
+ }
+ default:
+ return d.Errf("unrecognized subdirective '%s'", d.Val())
+ }
+ }
+ }
+ if a.Address == "" {
+ a.Address = ":53"
+ }
+ return nil
+}
+
+// parseApp configures the "dns" global option from Caddyfile.
+// Syntax:
+//
+// dns [address] {
+// bind
+// [record ]
+// }
+func parseApp(d *caddyfile.Dispenser, prev interface{}) (interface{}, error) {
+ var a App
+ var warnings []caddyconfig.Warning
+ if prev != nil {
+ return nil, fmt.Errorf("multiple DNS servers are not supported!")
+ }
+
+ err := a.UnmarshalCaddyfile(d)
+ if err != nil {
+ return nil, err
+ }
+
+ // tell Caddyfile adapter that this is the JSON for an app
+ return httpcaddyfile.App{
+ Name: "dns",
+ Value: caddyconfig.JSON(a, &warnings),
+ }, nil
+}
+
+// Interface guards
+var (
+ _ caddy.App = (*App)(nil)
+ _ caddy.Provisioner = (*App)(nil)
+)
diff --git a/log.go b/log.go
index 3153020..750a60b 100644
--- a/log.go
+++ b/log.go
@@ -222,3 +222,29 @@ func log_libdns_record(record *libdns.Record) zapcore.ObjectMarshaler {
return zapcore.ObjectMarshalerFunc(f)
}
+
+// MarshalLogObject satisfies the zapcore.ObjectMarshaler interface.
+func (r request) MarshalLogObject(enc zapcore.ObjectEncoder) error {
+ enc.AddString("zone", r.zone)
+ if r.append {
+ enc.AddString("type", "append")
+ } else {
+ enc.AddString("type", "delete")
+ }
+
+ if len(r.records) > 0 {
+ array := func(arr zapcore.ArrayEncoder) error {
+ for _, r := range r.records {
+ object := func(obj zapcore.ObjectEncoder) error {
+ log_RR(obj, r)
+ return nil
+ }
+ arr.AppendObject(zapcore.ObjectMarshalerFunc(object))
+ }
+ return nil
+ }
+ enc.AddArray("records", zapcore.ArrayMarshalerFunc(array))
+ }
+
+ return nil
+}
diff --git a/provider.go b/provider.go
new file mode 100644
index 0000000..ec1871f
--- /dev/null
+++ b/provider.go
@@ -0,0 +1,144 @@
+package stub
+
+import (
+ "context"
+ "fmt"
+
+ "github.com/caddyserver/caddy/v2"
+ "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
+ "github.com/libdns/libdns"
+ "github.com/miekg/dns"
+ "go.uber.org/zap"
+)
+
+type Provider struct {
+ app_channel chan request
+ logger *zap.Logger // set in Provision()
+}
+
+// CaddyModule returns the Caddy module information.
+func (Provider) CaddyModule() caddy.ModuleInfo {
+ return caddy.ModuleInfo{
+ ID: "dns.providers.internal",
+ New: func() caddy.Module { return &Provider{} },
+ }
+}
+
+// Provision sets up the module. Implements caddy.Provisioner.
+func (p *Provider) Provision(ctx caddy.Context) error {
+ p.logger = ctx.Logger()
+ if !ctx.AppIsConfigured("dns") {
+ p.logger.Warn("DNS app not yet configured")
+ }
+ app, err := ctx.App("dns")
+ if err != nil {
+ return err
+ }
+ if app == nil {
+ return fmt.Errorf("failed to load DNS app")
+ }
+ dns_app, ok := app.(*App)
+ if !ok {
+ return fmt.Errorf("received invalid app")
+ }
+ p.app_channel = dns_app.requests
+ return nil
+}
+
+// UnmarshalCaddyfile sets up the DNS provider from Caddyfile tokens. Syntax:
+//
+// dns internal
+func (p *Provider) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
+ for d.Next() {
+ if d.NextArg() {
+ return d.ArgErr()
+ }
+ for nesting := d.Nesting(); d.NextBlock(nesting); {
+ switch d.Val() {
+ default:
+ return d.Errf("unrecognized subdirective '%s'", d.Val())
+ }
+ }
+ }
+ return nil
+}
+
+func (p *Provider) convert(zone string, set []libdns.Record) ([]dns.RR, error) {
+ converted := []dns.RR{}
+
+ for _, r := range set {
+ rr, err := record_to_rr(zone, r)
+ if err != nil {
+ p.logger.Error(
+ "failed to convert",
+ zap.Error(err),
+ zap.String("zone", zone),
+ zap.Object("record", log_libdns_record(&r)),
+ )
+ return nil, err
+ }
+ converted = append(converted, rr)
+ }
+ return converted, nil
+}
+
+func (p *Provider) make_request(
+ ctx context.Context,
+ zone string,
+ append bool,
+ recs []libdns.Record,
+) ([]libdns.Record, error) {
+ resp := make(chan error)
+
+ records, err := p.convert(zone, recs)
+ if err != nil {
+ return nil, err
+ }
+
+ req := request{
+ append: append,
+ zone: zone,
+ records: records,
+ responder: resp,
+ }
+
+ p.app_channel <- req
+ p.logger.Debug("sent request", zap.Object("request", req))
+
+ select {
+ case err = <-resp:
+ if err != nil {
+ p.logger.Debug("request failed", zap.Error(err))
+ return nil, err
+ } else {
+ p.logger.Debug("request succeeded")
+ return recs, nil
+ }
+ case <-ctx.Done():
+ return nil, ctx.Err()
+ }
+}
+
+func (p *Provider) AppendRecords(
+ ctx context.Context,
+ zone string,
+ recs []libdns.Record,
+) ([]libdns.Record, error) {
+ return p.make_request(ctx, zone, true, recs)
+}
+
+func (p *Provider) DeleteRecords(
+ ctx context.Context,
+ zone string,
+ recs []libdns.Record,
+) ([]libdns.Record, error) {
+ return p.make_request(ctx, zone, false, recs)
+}
+
+// Interface guards
+var (
+ _ caddy.Provisioner = (*Provider)(nil)
+ _ caddyfile.Unmarshaler = (*Provider)(nil)
+ _ libdns.RecordAppender = (*Provider)(nil)
+ _ libdns.RecordDeleter = (*Provider)(nil)
+)
diff --git a/server.go b/server.go
new file mode 100644
index 0000000..6800751
--- /dev/null
+++ b/server.go
@@ -0,0 +1,249 @@
+package stub
+
+import (
+ "errors"
+ "net"
+ "strings"
+
+ "github.com/caddyserver/caddy/v2"
+ "github.com/miekg/dns"
+ "go.uber.org/zap"
+)
+
+// A DNS Query coming in from outside
+type query struct {
+ w dns.ResponseWriter
+ r *dns.Msg
+}
+
+type key struct {
+ Type dns.Type
+ Name string
+}
+
+type Server struct {
+ // the address & port on which to serve DNS for the challenge
+ Address caddy.NetworkAddress `json:"address,omitempty"`
+
+ // Statically configured records to serve
+ Records map[key][]dns.RR `json:"records,omitempty"`
+
+ logger *zap.Logger // set by App.start()
+ ctx *caddy.Context // set by App.start()
+ shutdown chan struct{} // set by App.start()
+ requests chan request // set by App.start()
+
+ dns_server *dns.Server // set by start_stop_server()
+ queries chan query // set by start_stop_server()
+
+}
+
+func rr_key(record dns.RR) key {
+ return key{
+ Type: dns.Type(record.Header().Rrtype),
+ Name: strings.ToLower(record.Header().Name),
+ }
+}
+
+func (srv *Server) insert_record(record dns.RR) {
+ key := rr_key(record)
+ current, exists := srv.Records[key]
+ if exists {
+ // TODO: de-duplicate?
+ srv.Records[key] = append(current, record)
+ } else {
+ srv.Records[key] = []dns.RR{record}
+ }
+}
+
+func (srv *Server) delete_record(record dns.RR) {
+ key := rr_key(record)
+ current, exists := srv.Records[key]
+ if exists {
+ filtered := []dns.RR{}
+ for _, rec := range current {
+ if rec != record {
+ filtered = append(filtered, rec)
+ }
+ }
+ if len(filtered) == 0 {
+ delete(srv.Records, key)
+ } else {
+ srv.Records[key] = filtered
+ }
+ } else {
+ // doesn't exist, nothing to delete
+ }
+}
+
+// This is the "main loop" of the DNS server
+// To avoid having to synchronize access to the records map, it is owned
+// exclusively by this loop, and the methods it calls.
+// All DNS queries coming from outside, as well as all requests to create
+// or delete DNS records coming from within the process are serialized by
+// the select statement.
+func (srv *Server) main() {
+ srv.logger.Debug(
+ "main loop running",
+ zap.Int("record_count", len(srv.Records)),
+ )
+ for {
+ select {
+ case r := <-srv.requests:
+ srv.handle_request(r)
+ case q := <-srv.queries:
+ srv.handle_query(q)
+ case <-srv.shutdown:
+ srv.logger.Debug("stopping main loop")
+ if srv.dns_server != nil {
+ srv.dns_server.Shutdown()
+ }
+ return
+ }
+ }
+}
+
+func (srv *Server) handle_request(r request) {
+ srv.logger.Debug("received", zap.Object("request", r))
+
+ if r.append {
+ for _, record := range r.records {
+ srv.insert_record(record)
+ }
+ } else {
+ for _, record := range r.records {
+ srv.delete_record(record)
+ }
+ }
+
+ r.responder <- srv.start_stop_server()
+}
+
+func (srv *Server) start_stop_server() error {
+ if srv.queries == nil {
+ srv.queries = make(chan query)
+ }
+ if len(srv.Records) == 0 {
+ if srv.dns_server != nil {
+ srv.logger.Debug("no more records to serve, shutting down server")
+ err := srv.dns_server.Shutdown()
+ srv.dns_server = nil
+ return err
+ }
+ srv.logger.Debug("no records to serve")
+ return nil
+ } else {
+ if srv.dns_server == nil {
+ conn, err := srv.bind()
+ if err != nil {
+ srv.logger.Error(
+ "failed to bind",
+ zap.Stringer("address", srv.Address),
+ zap.Error(err),
+ )
+ return err
+ }
+
+ // spawn the server
+ handler := make_proxy(srv.queries)
+ server := &dns.Server{
+ PacketConn: conn,
+ Net: "udp",
+ Handler: handler,
+ TsigSecret: nil,
+ }
+ srv.logger.Debug(
+ "starting server",
+ zap.Int("record_count", len(srv.Records)),
+ )
+ go srv.serve(server)
+
+ // store the server for shutdown later
+ srv.dns_server = server
+ return nil
+ }
+ srv.logger.Debug(
+ "server already running",
+ zap.Int("record_count", len(srv.Records)),
+ )
+ return nil
+ }
+}
+
+func (srv *Server) bind() (net.PacketConn, error) {
+ conn, err := srv.Address.Listen(srv.ctx, 0, net.ListenConfig{})
+ if err != nil {
+ return nil, err
+ }
+ pkt_conn := conn.(net.PacketConn)
+ if pkt_conn == nil {
+ return nil, errors.New("invalid address")
+ }
+ srv.logger.Debug("bound to socket", zap.Stringer("address", srv.Address))
+ return pkt_conn, nil
+}
+
+func (srv *Server) handle_query(q query) {
+ // dns.DefaultMsgAcceptFunc already checks that the query is fairly
+ // reasonable.
+
+ m := new(dns.Msg)
+ m.SetReply(q.r)
+
+ reject_and_log := func(code int, reason string) {
+ m.Rcode = code
+ m.Answer = []dns.RR{}
+ srv.logger.Debug(
+ "rejecting query",
+ zap.Stringer("address", q.w.RemoteAddr()),
+ zap.String("reason", reason),
+ zap.Object("response", LoggableDNSMsg{m}),
+ )
+ q.w.WriteMsg(m)
+ }
+
+ qstn := q.r.Question[0]
+ if !(qstn.Qclass == dns.ClassINET || qstn.Qclass == dns.ClassANY) {
+ // TODO: consider just not worrying about this
+ reject_and_log(dns.RcodeNotImplemented, "invalid class")
+ return
+ }
+ // queries may be wAcKY casE
+ // https://datatracker.ietf.org/doc/html/draft-vixie-dnsext-dns0x20-00
+ key := key {
+ Type: dns.Type(qstn.Qtype),
+ Name: strings.ToLower(qstn.Name),
+ }
+ records, exists := srv.Records[key]
+ if !exists {
+ reject_and_log(dns.RcodeNameError, "no such record")
+ return
+ }
+
+ m.Authoritative = true
+ m.Answer = records
+
+ srv.logger.Debug(
+ "answering query",
+ zap.Stringer("address", q.w.RemoteAddr()),
+ zap.Object("response", LoggableDNSMsg{m}),
+ )
+ q.w.WriteMsg(m)
+}
+
+func (srv *Server) serve(server *dns.Server) {
+ err := server.ActivateAndServe()
+ if err != nil {
+ srv.logger.Error("dns.ActivateAndServe failed", zap.Error(err))
+ } else {
+ srv.logger.Debug("server terminated successfully")
+ }
+}
+
+// dns.HandlerFunc that forwards every query into a channel
+func make_proxy(sink chan query) dns.HandlerFunc {
+ return func(w dns.ResponseWriter, r *dns.Msg) {
+ q := query{w, r}
+ sink <- q
+ }
+}
diff --git a/stub.go b/stub.go
index 566763c..518d3aa 100644
--- a/stub.go
+++ b/stub.go
@@ -1,230 +1,47 @@
package stub
import (
- "context"
- "net"
- "strings"
+ "strconv"
"github.com/caddyserver/caddy/v2"
- "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
- "github.com/mholt/acmez"
- "github.com/mholt/acmez/acme"
+ "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
+ "github.com/libdns/libdns"
"github.com/miekg/dns"
)
-// 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()
- logger *zap.Logger // set in Provision()
+// An in-process request to create or delete a DNS record
+type request struct {
+ append bool
+ zone string
+ records []dns.RR
+ responder chan error
}
func init() {
- caddy.RegisterModule(StubDNS{})
+ caddy.RegisterModule(App{})
+ caddy.RegisterModule(Provider{})
+
+ httpcaddyfile.RegisterGlobalOption("dns", parseApp)
}
-// CaddyModule returns the Caddy module information.
-func (StubDNS) CaddyModule() caddy.ModuleInfo {
- return caddy.ModuleInfo{
- ID: "dns.providers.stub_dns",
- New: func() caddy.Module { return &StubDNS{} },
+func record_to_rr(zone string, record libdns.Record) (dns.RR, error) {
+ maybe_priority := ""
+ if record.Priority != 0 {
+ maybe_priority += strconv.FormatInt(int64(record.Priority), 10)
+ maybe_priority += " "
}
+ //TODO: consider fixing this with dns.StringToType & dns.TypeToRR
+ // Problem is putting the value in, since it will be a different field
+ // for every type.
+ // Also, will probably require parsing the value anyway (e.g. to net.IP)
+ //TODO: does the value need to be escaped?!
+ return dns.NewRR(
+ dns.Fqdn(record.Name+"."+zone) +
+ " " +
+ strconv.FormatInt(int64(record.TTL.Seconds()), 10) +
+ " IN " +
+ record.Type +
+ " " +
+ maybe_priority +
+ record.Value)
}
-
-// Provision sets up the module. Implements caddy.Provisioner.
-func (p *StubDNS) Provision(ctx caddy.Context) error {
- p.logger = ctx.Logger()
- repl := caddy.NewReplacer()
- before := p.Address
- p.Address = repl.ReplaceAll(p.Address, "")
- p.logger.Debug(
- "provisioned",
- zap.String("address", p.Address),
- zap.String("address_before_replace", before),
- )
- return nil
-}
-
-// UnmarshalCaddyfile sets up the DNS provider from Caddyfile tokens. Syntax:
-//
-// stub_dns [address] {
-// address
-// }
-func (s *StubDNS) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
- for d.Next() {
- if d.NextArg() {
- s.Address = d.Val()
- }
- if d.NextArg() {
- return d.ArgErr()
- }
- for nesting := d.Nesting(); d.NextBlock(nesting); {
- switch d.Val() {
- case "address":
- if s.Address != "" {
- return d.Err("Address already set")
- }
- if d.NextArg() {
- s.Address = d.Val()
- }
- if d.NextArg() {
- return d.ArgErr()
- }
- default:
- return d.Errf("unrecognized subdirective '%s'", d.Val())
- }
- }
- }
- if s.Address == "" {
- return d.Err("missing Address")
- }
- return nil
-}
-
-func (s *StubDNS) Present(ctx context.Context, challenge acme.Challenge) error {
- // get challenge parameters
- fqdn := dns.Fqdn(challenge.DNS01TXTRecordName())
- content := challenge.DNS01KeyAuthorization()
-
- s.logger.Debug(
- "presenting record",
- zap.String("name", fqdn),
- zap.String("content", content),
- zap.String("address", s.Address),
- )
-
- // dns.Server.ListenAndServe blocks when it binds successfully,
- // so it has to run in a separate task and can't return errors directly
-
- if err := try_bind(ctx, s.Address); err != nil {
- s.logger.Error(
- "failed to bind",
- zap.String("address", s.Address),
- zap.Error(err),
- )
- return err
- }
-
- // spawn the server
- handler := s.make_handler(fqdn, content)
- server := &dns.Server{
- Addr: s.Address,
- Net: "udp",
- Handler: handler,
- TsigSecret: nil,
- }
- go s.serve(server)
-
- // 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 {
- p.logger.Debug("server never started, nothing to clean up")
- return nil
- } else {
- p.logger.Debug(
- "shutting down DNS server",
- zap.String("address", p.Address),
- )
- return p.server.ShutdownContext(ctx)
- }
-}
-
-// quickly check whether it's possible to bind to the address
-func try_bind(ctx context.Context, address string) error {
- var lc net.ListenConfig
- conn, err := lc.ListenPacket(ctx, "udp", address)
- if conn != nil {
- return conn.Close()
- }
- return err
-}
-
-func (s *StubDNS) serve(server *dns.Server) {
- err := server.ListenAndServe()
- if err != nil {
- s.logger.Error(
- "DNS ListenAndServe returned an error!",
- zap.Error(err),
- )
- } else {
- s.logger.Debug("server terminated successfully")
- }
-}
-
-func (s *StubDNS) make_handler(fqdn string, txt string) dns.HandlerFunc {
- logger := s.logger
- handler := func(w dns.ResponseWriter, r *dns.Msg) {
- m := new(dns.Msg)
- m.SetReply(r)
-
- logger.Debug(
- "received DNS query",
- zap.Stringer("address", w.RemoteAddr()),
- zap.Object("request", LoggableDNSMsg{r}),
- )
-
- reject_and_log := func(code int, reason string) {
- m.Rcode = code
- m.Answer = []dns.RR{}
- logger.Debug(
- "rejecting query",
- zap.String("reason", reason),
- zap.Object("response", LoggableDNSMsg{m}),
- )
- w.WriteMsg(m)
- }
-
- if len(r.Question) != 1 {
- reject_and_log(dns.RcodeRefused, "not exactly 1 question")
- return
- }
- q := r.Question[0]
- domain := q.Name
-
- switch {
- case r.Response:
- reject_and_log(dns.RcodeRefused, "not a query")
- case !(q.Qclass == dns.ClassINET || q.Qclass == dns.ClassANY):
- reject_and_log(dns.RcodeNotImplemented, "invalid class")
- // queries may be wAcKY casE
- // https://datatracker.ietf.org/doc/html/draft-vixie-dnsext-dns0x20-00
- case !strings.EqualFold(domain, fqdn):
- reject_and_log(dns.RcodeNameError, "wrong domain")
- case q.Qtype != dns.TypeTXT:
- reject_and_log(dns.RcodeRefused, "invalid type")
- default:
- m.Authoritative = true
- rr := new(dns.TXT)
- rr.Hdr = dns.RR_Header{
- Name: fqdn, // only question section has to match wAcKY casE
- Rrtype: dns.TypeTXT,
- Class: dns.ClassINET,
- Ttl: uint32(challenge_ttl),
- }
- rr.Txt = []string{txt}
- m.Answer = []dns.RR{rr}
- logger.Debug(
- "replying",
- zap.Object("response", LoggableDNSMsg{m}),
- )
- w.WriteMsg(m)
- }
- }
-
- return handler
-}
-
-// Interface guards
-var (
- _ acmez.Solver = (*StubDNS)(nil)
- _ caddy.Provisioner = (*StubDNS)(nil)
- _ caddyfile.Unmarshaler = (*StubDNS)(nil)
-)