From 662136ea745b4b467687c589df193196397ff35f Mon Sep 17 00:00:00 2001 From: Simon Ser Date: Thu, 12 Jan 2023 16:55:05 +0100 Subject: [PATCH] Add support for ACME DNS hooks Closes: https://todo.sr.ht/~emersion/tlstunnel/2 --- directives.go | 13 +++++++++ dns.go | 71 +++++++++++++++++++++++++++++++++++++++++++++++++ go.mod | 1 + tlstunnel.1.scd | 7 +++++ 4 files changed, 92 insertions(+) create mode 100644 dns.go diff --git a/directives.go b/directives.go index e963b4d..f188bba 100644 --- a/directives.go +++ b/directives.go @@ -175,6 +175,19 @@ func parseTLS(srv *Server, d *scfg.Directive) error { if err := parseTLSOnDemand(srv, child); err != nil { return err } + case "acme_dns_command": + var cmdName string + if err := child.ParseParams(&cmdName); err != nil { + return err + } + cmdParams := child.Params[1:] + + srv.ACMEIssuer.DNS01Solver = &certmagic.DNS01Solver{ + DNSProvider: &commandDNSProvider{ + Name: cmdName, + Params: cmdParams, + }, + } default: return fmt.Errorf("unknown %q directive", child.Name) } diff --git a/dns.go b/dns.go new file mode 100644 index 0000000..077f7c7 --- /dev/null +++ b/dns.go @@ -0,0 +1,71 @@ +package tlstunnel + +import ( + "context" + "fmt" + "os/exec" + "strings" + + "github.com/caddyserver/certmagic" + "github.com/libdns/libdns" +) + +type commandDNSProvider struct { + Name string + Params []string +} + +var _ certmagic.ACMEDNSProvider = (*commandDNSProvider)(nil) + +func (provider *commandDNSProvider) exec(ctx context.Context, subcmd string, subargs ...string) error { + var params []string + params = append(params, provider.Params...) + params = append(params, subcmd) + params = append(params, subargs...) + cmd := exec.CommandContext(ctx, provider.Name, params...) + + if out, err := cmd.CombinedOutput(); err != nil { + details := "" + if len(out) > 0 { + details = ": " + string(out) + } + return fmt.Errorf("failed to run DNS hook %v (%w)%v", subcmd, err, details) + } + + return nil +} + +func (provider *commandDNSProvider) processRecords(ctx context.Context, zone string, recs []libdns.Record, subcmd string) ([]libdns.Record, error) { + var ( + done []libdns.Record + err error + ) + for _, rec := range recs { + var domain string + if domain, err = domainFromACMEChallengeRecord(zone, &rec); err != nil { + break + } + if err = provider.exec(ctx, subcmd, domain, "-", rec.Value); err != nil { + break + } + done = append(done, rec) + } + return done, err +} + +func (provider *commandDNSProvider) AppendRecords(ctx context.Context, zone string, recs []libdns.Record) ([]libdns.Record, error) { + return provider.processRecords(ctx, zone, recs, "deploy_challenge") +} + +func (provider *commandDNSProvider) DeleteRecords(ctx context.Context, zone string, recs []libdns.Record) ([]libdns.Record, error) { + return provider.processRecords(ctx, zone, recs, "clean_challenge") +} + +func domainFromACMEChallengeRecord(zone string, rec *libdns.Record) (string, error) { + if rec.Type != "TXT" || !strings.HasPrefix(rec.Name, "_acme-challenge.") { + return "", fmt.Errorf("DNS record doesn't look like an ACME challenge: %v %v", rec.Type, rec.Name) + } + relName := strings.TrimPrefix(rec.Name, "_acme-challenge.") + relZone := strings.TrimSuffix(zone, ".") + return relName + "." + relZone, nil +} diff --git a/go.mod b/go.mod index 7cfe006..88c2c97 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( git.sr.ht/~emersion/go-scfg v0.0.0-20211215104734-c2c7a15d6c99 github.com/caddyserver/certmagic v0.17.2 github.com/klauspost/cpuid/v2 v2.2.1 // indirect + github.com/libdns/libdns v0.2.1 github.com/pires/go-proxyproto v0.6.2 github.com/pkg/errors v0.9.1 // indirect go.uber.org/atomic v1.10.0 // indirect diff --git a/tlstunnel.1.scd b/tlstunnel.1.scd index d11243c..67180e4 100644 --- a/tlstunnel.1.scd +++ b/tlstunnel.1.scd @@ -123,6 +123,13 @@ The following directives are supported: The environment will contain a *TLSTUNNEL_NAME* variable with the domain name to be validated. + *acme_dns_command* command [arguments...] + Configure the ACME DNS challenge using the specified command. + + The command must implement _deploy_challenge_ and _clean_challenge_ + as specified by dehydrated's hooks: + https://github.com/dehydrated-io/dehydrated + # FILES _/etc/tlstunnel/config_