2023-02-25 10:21:30 +01:00
|
|
|
package stub
|
2023-02-25 10:30:08 +01:00
|
|
|
|
|
|
|
|
import (
|
2023-02-25 10:49:47 +01:00
|
|
|
"context"
|
2023-02-25 11:09:56 +01:00
|
|
|
"net"
|
2023-02-27 04:06:37 +01:00
|
|
|
"strings"
|
2023-02-25 10:49:47 +01:00
|
|
|
|
2023-02-25 11:21:28 +01:00
|
|
|
"github.com/caddyserver/caddy/v2"
|
|
|
|
|
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
|
|
|
|
"github.com/mholt/acmez"
|
|
|
|
|
"github.com/mholt/acmez/acme"
|
2023-02-25 10:49:47 +01:00
|
|
|
"github.com/miekg/dns"
|
2023-02-25 11:14:18 +01:00
|
|
|
"go.uber.org/zap"
|
2023-02-25 11:18:42 +01:00
|
|
|
"go.uber.org/zap/zapcore"
|
2023-02-25 10:30:08 +01:00
|
|
|
)
|
|
|
|
|
|
2023-02-25 10:49:47 +01:00
|
|
|
// TTL of the challenge TXT record to serve
|
|
|
|
|
const challenge_ttl = 600 // (anything is probably fine here)
|
|
|
|
|
|
2023-02-25 10:30:08 +01:00
|
|
|
type StubDNS struct {
|
|
|
|
|
// the address & port on which to serve DNS for the challenge
|
|
|
|
|
Address string `json:"address,omitempty"`
|
|
|
|
|
|
2023-02-25 10:49:47 +01:00
|
|
|
server *dns.Server // set in Present()
|
2023-02-25 11:14:18 +01:00
|
|
|
logger *zap.Logger // set in Provision()
|
2023-02-25 10:30:08 +01:00
|
|
|
}
|
|
|
|
|
|
2023-02-25 11:18:42 +01:00
|
|
|
// Wrapper for logging (relevant parts of) dns.Msg
|
2023-02-25 11:21:28 +01:00
|
|
|
type LoggableDNSMsg struct{ *dns.Msg }
|
2023-02-25 10:30:08 +01:00
|
|
|
|
|
|
|
|
func init() {
|
|
|
|
|
caddy.RegisterModule(StubDNS{})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// CaddyModule returns the Caddy module information.
|
|
|
|
|
func (StubDNS) CaddyModule() caddy.ModuleInfo {
|
|
|
|
|
return caddy.ModuleInfo{
|
|
|
|
|
ID: "dns.providers.stub_dns",
|
2023-02-25 11:21:28 +01:00
|
|
|
New: func() caddy.Module { return &StubDNS{} },
|
2023-02-25 10:30:08 +01:00
|
|
|
}
|
|
|
|
|
}
|
2023-02-25 10:33:21 +01:00
|
|
|
|
|
|
|
|
// Provision sets up the module. Implements caddy.Provisioner.
|
|
|
|
|
func (p *StubDNS) Provision(ctx caddy.Context) error {
|
2023-02-25 11:14:18 +01:00
|
|
|
p.logger = ctx.Logger()
|
2023-02-25 10:33:21 +01:00
|
|
|
repl := caddy.NewReplacer()
|
2023-02-25 11:14:18 +01:00
|
|
|
before := p.Address
|
2023-02-25 10:33:21 +01:00
|
|
|
p.Address = repl.ReplaceAll(p.Address, "")
|
2023-02-25 11:14:18 +01:00
|
|
|
p.logger.Debug(
|
|
|
|
|
"provisioned",
|
|
|
|
|
zap.String("address", p.Address),
|
|
|
|
|
zap.String("address_before_replace", before),
|
|
|
|
|
)
|
2023-02-25 10:33:21 +01:00
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// UnmarshalCaddyfile sets up the DNS provider from Caddyfile tokens. Syntax:
|
|
|
|
|
//
|
2023-02-25 11:21:28 +01:00
|
|
|
// stub_dns [address] {
|
|
|
|
|
// address <address>
|
|
|
|
|
// }
|
2023-02-25 10:33:21 +01:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-25 10:49:47 +01:00
|
|
|
func (s *StubDNS) Present(ctx context.Context, challenge acme.Challenge) error {
|
|
|
|
|
// get challenge parameters
|
|
|
|
|
fqdn := dns.Fqdn(challenge.DNS01TXTRecordName())
|
|
|
|
|
content := challenge.DNS01KeyAuthorization()
|
2023-02-25 11:09:56 +01:00
|
|
|
|
2023-02-25 11:14:18 +01:00
|
|
|
s.logger.Debug(
|
|
|
|
|
"presenting record",
|
|
|
|
|
zap.String("name", fqdn),
|
|
|
|
|
zap.String("content", content),
|
|
|
|
|
zap.String("address", s.Address),
|
|
|
|
|
)
|
|
|
|
|
|
2023-02-25 11:09:56 +01:00
|
|
|
// 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 {
|
2023-02-25 11:14:18 +01:00
|
|
|
s.logger.Error(
|
|
|
|
|
"failed to bind",
|
|
|
|
|
zap.String("address", s.Address),
|
|
|
|
|
zap.Error(err),
|
|
|
|
|
)
|
2023-02-25 11:09:56 +01:00
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-25 10:49:47 +01:00
|
|
|
// spawn the server
|
|
|
|
|
handler := s.make_handler(fqdn, content)
|
2023-02-25 11:14:18 +01:00
|
|
|
// could also use fqdn as pattern, but "." allows logging invalid requests
|
2023-02-25 10:49:47 +01:00
|
|
|
dns.HandleFunc(".", handler)
|
2023-02-25 11:21:28 +01:00
|
|
|
server := &dns.Server{Addr: s.Address, Net: "udp", TsigSecret: nil}
|
2023-02-25 11:14:18 +01:00
|
|
|
go s.serve(server)
|
2023-02-25 10:49:47 +01:00
|
|
|
|
|
|
|
|
// 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 {
|
2023-02-25 11:14:18 +01:00
|
|
|
p.logger.Debug("server never started, nothing to clean up")
|
2023-02-25 10:49:47 +01:00
|
|
|
return nil
|
|
|
|
|
} else {
|
2023-02-25 11:14:18 +01:00
|
|
|
p.logger.Debug(
|
|
|
|
|
"shutting down DNS server",
|
|
|
|
|
zap.String("address", p.Address),
|
|
|
|
|
)
|
2023-02-25 10:49:47 +01:00
|
|
|
return p.server.ShutdownContext(ctx)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-25 11:09:56 +01:00
|
|
|
// 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
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-25 11:14:18 +01:00
|
|
|
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")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-25 10:49:47 +01:00
|
|
|
func (s *StubDNS) make_handler(fqdn string, txt string) dns.HandlerFunc {
|
2023-02-25 11:14:18 +01:00
|
|
|
logger := s.logger
|
2023-02-25 10:49:47 +01:00
|
|
|
handler := func(w dns.ResponseWriter, r *dns.Msg) {
|
|
|
|
|
m := new(dns.Msg)
|
|
|
|
|
m.SetReply(r)
|
2023-02-25 11:14:18 +01:00
|
|
|
|
|
|
|
|
logger.Debug(
|
|
|
|
|
"received DNS query",
|
|
|
|
|
zap.Stringer("address", w.RemoteAddr()),
|
2023-02-25 11:18:42 +01:00
|
|
|
zap.Object("request", LoggableDNSMsg{r}),
|
2023-02-25 11:14:18 +01:00
|
|
|
)
|
2023-02-25 11:18:42 +01:00
|
|
|
|
2023-02-27 03:10:47 +01:00
|
|
|
reject_and_log := func(code int, reason string) {
|
|
|
|
|
m.Rcode = code
|
2023-02-25 10:49:47 +01:00
|
|
|
m.Answer = []dns.RR{}
|
2023-02-27 03:10:47 +01:00
|
|
|
logger.Debug(
|
|
|
|
|
"rejecting query",
|
|
|
|
|
zap.String("reason", reason),
|
|
|
|
|
zap.Object("response", LoggableDNSMsg{m}),
|
|
|
|
|
)
|
|
|
|
|
w.WriteMsg(m)
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-27 04:22:04 +01:00
|
|
|
if len(r.Question) != 1 {
|
|
|
|
|
reject_and_log(dns.RcodeRefused, "not exactly 1 question")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
q := r.Question[0]
|
|
|
|
|
domain := q.Name
|
|
|
|
|
|
2023-02-27 03:10:47 +01:00
|
|
|
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")
|
2023-02-27 04:06:37 +01:00
|
|
|
// 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")
|
2023-02-27 03:10:47 +01:00
|
|
|
case q.Qtype != dns.TypeTXT:
|
2023-02-27 03:13:10 +01:00
|
|
|
reject_and_log(dns.RcodeRefused, "invalid type")
|
2023-02-27 03:10:47 +01:00
|
|
|
default:
|
2023-02-25 10:49:47 +01:00
|
|
|
m.Authoritative = true
|
|
|
|
|
rr := new(dns.TXT)
|
|
|
|
|
rr.Hdr = dns.RR_Header{
|
2023-02-27 04:06:37 +01:00
|
|
|
Name: fqdn, // only question section has to match wAcKY casE
|
2023-02-25 10:49:47 +01:00
|
|
|
Rrtype: dns.TypeTXT,
|
2023-02-25 11:21:28 +01:00
|
|
|
Class: dns.ClassINET,
|
|
|
|
|
Ttl: uint32(challenge_ttl),
|
2023-02-25 10:49:47 +01:00
|
|
|
}
|
|
|
|
|
rr.Txt = []string{txt}
|
|
|
|
|
m.Answer = []dns.RR{rr}
|
2023-02-27 03:10:47 +01:00
|
|
|
logger.Debug(
|
|
|
|
|
"replying",
|
|
|
|
|
zap.Object("response", LoggableDNSMsg{m}),
|
|
|
|
|
)
|
|
|
|
|
w.WriteMsg(m)
|
2023-02-25 10:49:47 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return handler
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-25 11:18:42 +01:00
|
|
|
// MarshalLogObject satisfies the zapcore.ObjectMarshaler interface.
|
|
|
|
|
func (m LoggableDNSMsg) MarshalLogObject(enc zapcore.ObjectEncoder) error {
|
|
|
|
|
// adapted version of MsgHdr.String() from github.com/miekg/dns
|
|
|
|
|
enc.AddUint16("id", m.Id)
|
|
|
|
|
enc.AddString("opcode", dns.OpcodeToString[m.Opcode])
|
|
|
|
|
enc.AddString("status", dns.RcodeToString[m.Rcode])
|
|
|
|
|
|
|
|
|
|
flag_array := func(arr zapcore.ArrayEncoder) error {
|
2023-02-25 11:21:28 +01:00
|
|
|
if m.Response {
|
|
|
|
|
arr.AppendString("qr")
|
|
|
|
|
}
|
|
|
|
|
if m.Authoritative {
|
|
|
|
|
arr.AppendString("aa")
|
|
|
|
|
}
|
|
|
|
|
if m.Truncated {
|
|
|
|
|
arr.AppendString("tc")
|
|
|
|
|
}
|
|
|
|
|
if m.RecursionDesired {
|
|
|
|
|
arr.AppendString("rd")
|
|
|
|
|
}
|
|
|
|
|
if m.RecursionAvailable {
|
|
|
|
|
arr.AppendString("ra")
|
|
|
|
|
}
|
|
|
|
|
if m.Zero {
|
|
|
|
|
arr.AppendString("z")
|
|
|
|
|
}
|
|
|
|
|
if m.AuthenticatedData {
|
|
|
|
|
arr.AppendString("ad")
|
|
|
|
|
}
|
|
|
|
|
if m.CheckingDisabled {
|
|
|
|
|
arr.AppendString("cd")
|
|
|
|
|
}
|
2023-02-25 11:18:42 +01:00
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
enc.AddArray("flags", zapcore.ArrayMarshalerFunc(flag_array))
|
|
|
|
|
|
|
|
|
|
log_questions(enc, &m.Question)
|
|
|
|
|
log_answers(enc, &m.Answer)
|
|
|
|
|
// not logged:
|
|
|
|
|
// - EDNS0 "OPT pseudosection" from m.IsEdns0()
|
|
|
|
|
// - "authority section" in m.Ns
|
|
|
|
|
// - "additional section" in m.Extra
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func log_answers(enc zapcore.ObjectEncoder, answers *[]dns.RR) {
|
|
|
|
|
if len(*answers) > 0 {
|
|
|
|
|
array := func(arr zapcore.ArrayEncoder) error {
|
|
|
|
|
for _, r := range *answers {
|
|
|
|
|
// since we only serve TXT records
|
|
|
|
|
txt, ok := r.(*dns.TXT)
|
|
|
|
|
if ok {
|
|
|
|
|
object := func(obj zapcore.ObjectEncoder) error {
|
|
|
|
|
obj.AddString("name", txt.Hdr.Name)
|
|
|
|
|
obj.AddString("class", dns.ClassToString[txt.Hdr.Class])
|
|
|
|
|
obj.AddString("type", dns.TypeToString[txt.Hdr.Rrtype])
|
|
|
|
|
obj.AddUint32("TTL", txt.Hdr.Ttl)
|
|
|
|
|
rec := func(arr2 zapcore.ArrayEncoder) error {
|
|
|
|
|
for _, t := range txt.Txt {
|
|
|
|
|
arr2.AppendString(t)
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
obj.AddArray("content", zapcore.ArrayMarshalerFunc(rec))
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
arr.AppendObject(zapcore.ObjectMarshalerFunc(object))
|
|
|
|
|
} else {
|
|
|
|
|
// fallback for other record types, serialized dig-style
|
|
|
|
|
arr.AppendString(r.String())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
enc.AddArray("answer", zapcore.ArrayMarshalerFunc(array))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func log_questions(enc zapcore.ObjectEncoder, questions *[]dns.Question) {
|
|
|
|
|
if len(*questions) > 0 {
|
|
|
|
|
array := func(arr zapcore.ArrayEncoder) error {
|
|
|
|
|
for _, q := range *questions {
|
|
|
|
|
object := func(obj zapcore.ObjectEncoder) error {
|
|
|
|
|
obj.AddString("name", q.Name)
|
|
|
|
|
obj.AddString("class", dns.ClassToString[q.Qclass])
|
|
|
|
|
obj.AddString("type", dns.TypeToString[q.Qtype])
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
arr.AppendObject(zapcore.ObjectMarshalerFunc(object))
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
enc.AddArray("question", zapcore.ArrayMarshalerFunc(array))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-25 10:33:21 +01:00
|
|
|
// Interface guards
|
|
|
|
|
var (
|
2023-02-25 11:21:28 +01:00
|
|
|
_ acmez.Solver = (*StubDNS)(nil)
|
|
|
|
|
_ caddy.Provisioner = (*StubDNS)(nil)
|
2023-02-25 10:33:21 +01:00
|
|
|
_ caddyfile.Unmarshaler = (*StubDNS)(nil)
|
|
|
|
|
)
|