]> git.ipfire.org Git - thirdparty/haproxy.git/commitdiff
MEDIUM: acme: add dns-01 DNS propagation pre-check
authorWilliam Lallemand <wlallemand@haproxy.com>
Tue, 24 Mar 2026 20:53:57 +0000 (21:53 +0100)
committerWilliam Lallemand <wlallemand@haproxy.com>
Mon, 30 Mar 2026 16:24:28 +0000 (18:24 +0200)
When using the dns-01 challenge type, TXT record propagation across
DNS servers can take time. If the ACME server verifies the challenge
before the record is visible, the challenge fails and it's not possible
to trigger it again.

This patch introduces an optional DNS pre-check mechanism controlled
by two new configuration directives in the "acme" section:

  - "dns-check on|off": enable DNS propagation verification before
    notifying the ACME server (default: off)
  - "dns-delay <time>": delay before querying DNS (default: 300s)

When enabled, three new states are inserted in the state machine
between AUTH and CHALLENGE:

  - ACME_RSLV_WAIT: waits dns-delay seconds before starting
  - ACME_RSLV_TRIGGER: starts an async TXT resolution for each
    pending authorization using HAProxy's resolver infrastructure
  - ACME_RSLV_READY: compares the resolved TXT record against the
    expected token; retries from ACME_RSLV_WAIT if any record is
    missing or does not match

The "acme_rslv" structure is implemented in acme_resolvers.c, it holds
the resolution for each domain. The "auth" structure which contains each
challenge to resolve contains an "acme_rslv" structure. Once
ACME_RSLV_TRIGGER leaves, the DNS tasks run on the same thread, and the
last DNS task which finishes will wake up acme_process().

Note that the resolution goes through the configured resolvers, not
through the authoritative name servers of the domain. The result may
therefore still be affected by DNS caching at the resolver level.

Makefile
include/haproxy/acme-t.h
include/haproxy/acme_resolvers-t.h [new file with mode: 0644]
include/haproxy/acme_resolvers.h [new file with mode: 0644]
include/haproxy/obj_type-t.h
include/haproxy/obj_type.h
src/acme.c
src/acme_resolvers.c [new file with mode: 0644]
src/resolvers.c

index b72189e9886531cc8e8ac360eaf2f5b0435a5285..064fed3af4d0187882840805c91729203a6fb5e3 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -643,7 +643,7 @@ ifneq ($(USE_OPENSSL:0=),)
   OPTIONS_OBJS += src/ssl_sock.o src/ssl_ckch.o src/ssl_ocsp.o src/ssl_crtlist.o       \
                   src/ssl_sample.o src/cfgparse-ssl.o src/ssl_gencert.o                \
                   src/ssl_utils.o src/jwt.o src/ssl_clienthello.o src/jws.o src/acme.o \
-                  src/ssl_trace.o src/jwe.o
+                  src/acme_resolvers.o src/ssl_trace.o src/jwe.o
 endif
 
 ifneq ($(USE_ENGINE:0=),)
index e93b617c7d80563e40641149ac143e84d53ebb70..8e828904e8f84c1681303a5bb435c535b7f8982e 100644 (file)
@@ -2,9 +2,12 @@
 #ifndef _ACME_T_H_
 #define _ACME_T_H_
 
+#include <haproxy/acme_resolvers-t.h>
 #include <haproxy/istbuf.h>
 #include <haproxy/openssl-compat.h>
 
+#if defined(HAVE_ACME)
+
 #define ACME_RETRY 5
 
 /* acme section configuration */
@@ -13,6 +16,8 @@ struct acme_cfg {
        int linenum;                /* config linenum */
        char *name;                 /* section name */
        int reuse_key;              /* do we need to renew the private key */
+       int dns_check;              /* enable DNS resolution to verify TXT record before challenge */
+       unsigned int dns_delay;     /* delay in seconds before re-triggering DNS resolution (default: 300) */
        char *directory;            /* directory URL */
        char *map;                  /* storage for tokens + thumbprint */
        struct {
@@ -40,6 +45,9 @@ enum acme_st {
        ACME_NEWACCOUNT,
        ACME_NEWORDER,
        ACME_AUTH,
+       ACME_RSLV_WAIT,
+       ACME_RSLV_TRIGGER,
+       ACME_RSLV_READY,
        ACME_CHALLENGE,
        ACME_CHKCHALLENGE,
        ACME_FINALIZE,
@@ -59,6 +67,7 @@ struct acme_auth {
        struct ist chall;  /* challenge URI */
        struct ist token;  /* token */
        int validated;     /* already validated */
+       struct acme_rslv *rslv; /* acme dns-01 resolver */
        int ready;         /* is the challenge ready ? */
        void *next;
 };
@@ -85,6 +94,7 @@ struct acme_ctx {
        X509_REQ *req;
        struct ist finalize;
        struct ist certificate;
+       unsigned int dnstasks;      /* number of DNS tasks running for this ctx */
        struct task *task;
        struct ebmb_node node;
        char name[VAR_ARRAY];
@@ -102,4 +112,6 @@ struct acme_ctx {
 #define ACME_VERB_ADVANCED 4
 #define ACME_VERB_COMPLETE 5
 
+#endif /* ! HAVE_ACME */
+
 #endif
diff --git a/include/haproxy/acme_resolvers-t.h b/include/haproxy/acme_resolvers-t.h
new file mode 100644 (file)
index 0000000..dde0612
--- /dev/null
@@ -0,0 +1,27 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#ifndef _HAPROXY_ACME_RESOLVERS_T_H
+#define _HAPROXY_ACME_RESOLVERS_T_H
+
+#include <haproxy/obj_type-t.h>
+#include <haproxy/resolvers-t.h>
+
+struct dns_counters;
+
+/* TXT records for dns-01 */
+
+struct acme_rslv {
+       enum obj_type obj_type;                   /* OBJ_TYPE_ACME_RSLV */
+       unsigned int *dnstasks;                   /* number of running DNS resolution for the same acme_task */
+       char *hostname_dn;
+       int hostname_dn_len;
+       struct resolvers *resolvers;
+       struct resolv_requester *requester;
+       int result;                               /* RSLV_STATUS_* — NONE until done */
+       int error_code;                           /* RSLV_RESP_* from the error callback */
+       struct task *acme_task;                    /* ACME task to wake on completion, or NULL */
+       struct ist txt;                           /* first TXT record found */
+       int (*success_cb)(struct resolv_requester *, struct dns_counters *);
+       int (*error_cb)(struct resolv_requester *, int);
+};
+
+#endif /* _HAPROXY_ACME_RESOLVERS_T_H */
diff --git a/include/haproxy/acme_resolvers.h b/include/haproxy/acme_resolvers.h
new file mode 100644 (file)
index 0000000..fb3d7df
--- /dev/null
@@ -0,0 +1,18 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#ifndef _HAPROXY_ACME_RESOLVERS_H
+#define _HAPROXY_ACME_RESOLVERS_H
+
+#include <haproxy/openssl-compat.h>
+
+#if defined(HAVE_ACME)
+
+#include <haproxy/acme_resolvers-t.h>
+#include <haproxy/acme-t.h>
+#include <haproxy/resolvers-t.h>
+
+struct acme_rslv *acme_rslv_start(struct acme_auth *auth, unsigned int *dnstasks, char **errmsg);
+void acme_rslv_free(struct acme_rslv *rslv);
+
+#endif
+
+#endif /* _HAPROXY_ACME_RESOLVERS_H */
index fd232b34781085deac35c811e24c6b23dd0df7bc..f57fbf01a1cce6fc73465fb532bfd60f3a76aa82 100644 (file)
@@ -47,6 +47,7 @@ enum obj_type {
        OBJ_TYPE_DGRAM,        /* object is a struct quic_dgram */
 #endif
        OBJ_TYPE_HATERM,       /* object is a struct hstream */
+       OBJ_TYPE_ACME_RSLV,   /* object is a struct acme_rslv */
        OBJ_TYPE_ENTRIES       /* last one : number of entries */
 } __attribute__((packed)) ;
 
index bd850b9697fcbd76572737d436e247221c4dcfe0..d10a3296900009c1efff7d4e52321a9f15ed5d37 100644 (file)
@@ -22,6 +22,7 @@
 #ifndef _HAPROXY_OBJ_TYPE_H
 #define _HAPROXY_OBJ_TYPE_H
 
+#include <haproxy/acme_resolvers-t.h>
 #include <haproxy/api.h>
 #include <haproxy/applet-t.h>
 #include <haproxy/check-t.h>
@@ -45,17 +46,18 @@ static inline enum obj_type obj_type(const enum obj_type *t)
 static inline const char *obj_type_name(const enum obj_type *t)
 {
        switch (obj_type(t)) {
-       case OBJ_TYPE_NONE:     return "NONE";
-       case OBJ_TYPE_LISTENER: return "LISTENER";
-       case OBJ_TYPE_PROXY:    return "PROXY";
-       case OBJ_TYPE_SERVER:   return "SERVER";
-       case OBJ_TYPE_APPLET:   return "APPLET";
-       case OBJ_TYPE_APPCTX:   return "APPCTX";
-       case OBJ_TYPE_CONN:     return "CONN";
-       case OBJ_TYPE_SRVRQ:    return "SRVRQ";
-       case OBJ_TYPE_SC:       return "SC";
-       case OBJ_TYPE_STREAM:   return "STREAM";
-       case OBJ_TYPE_CHECK:    return "CHECK";
+       case OBJ_TYPE_NONE:       return "NONE";
+       case OBJ_TYPE_LISTENER:   return "LISTENER";
+       case OBJ_TYPE_PROXY:      return "PROXY";
+       case OBJ_TYPE_SERVER:     return "SERVER";
+       case OBJ_TYPE_APPLET:     return "APPLET";
+       case OBJ_TYPE_APPCTX:     return "APPCTX";
+       case OBJ_TYPE_CONN:       return "CONN";
+       case OBJ_TYPE_SRVRQ:      return "SRVRQ";
+       case OBJ_TYPE_SC:         return "SC";
+       case OBJ_TYPE_STREAM:     return "STREAM";
+       case OBJ_TYPE_CHECK:      return "CHECK";
+       case OBJ_TYPE_ACME_RSLV:  return "ACME_RSLV";
 #ifdef USE_QUIC
        case OBJ_TYPE_DGRAM:    return "DGRAM";
 #endif
@@ -203,6 +205,18 @@ static inline struct hstream *objt_hstream(enum obj_type *t)
        return __objt_hstream(t);
 }
 
+static inline struct acme_rslv *__objt_acme_rslv(enum obj_type *t)
+{
+       return container_of(t, struct acme_rslv, obj_type);
+}
+
+static inline struct acme_rslv *objt_acme_rslv(enum obj_type *t)
+{
+       if (!t || *t != OBJ_TYPE_ACME_RSLV)
+               return NULL;
+       return __objt_acme_rslv(t);
+}
+
 #ifdef USE_QUIC
 static inline struct quic_dgram *__objt_dgram(enum obj_type *t)
 {
@@ -220,17 +234,18 @@ static inline struct quic_dgram *objt_dgram(enum obj_type *t)
 static inline void *obj_base_ptr(enum obj_type *t)
 {
        switch (obj_type(t)) {
-       case OBJ_TYPE_NONE:     return NULL;
-       case OBJ_TYPE_LISTENER: return __objt_listener(t);
-       case OBJ_TYPE_PROXY:    return __objt_proxy(t);
-       case OBJ_TYPE_SERVER:   return __objt_server(t);
-       case OBJ_TYPE_APPLET:   return __objt_applet(t);
-       case OBJ_TYPE_APPCTX:   return __objt_appctx(t);
-       case OBJ_TYPE_CONN:     return __objt_conn(t);
-       case OBJ_TYPE_SRVRQ:    return __objt_resolv_srvrq(t);
-       case OBJ_TYPE_SC:       return __objt_sc(t);
-       case OBJ_TYPE_STREAM:   return __objt_stream(t);
-       case OBJ_TYPE_CHECK:    return __objt_check(t);
+       case OBJ_TYPE_NONE:      return NULL;
+       case OBJ_TYPE_LISTENER:  return __objt_listener(t);
+       case OBJ_TYPE_PROXY:     return __objt_proxy(t);
+       case OBJ_TYPE_SERVER:    return __objt_server(t);
+       case OBJ_TYPE_APPLET:    return __objt_applet(t);
+       case OBJ_TYPE_APPCTX:    return __objt_appctx(t);
+       case OBJ_TYPE_CONN:      return __objt_conn(t);
+       case OBJ_TYPE_SRVRQ:     return __objt_resolv_srvrq(t);
+       case OBJ_TYPE_SC:        return __objt_sc(t);
+       case OBJ_TYPE_STREAM:    return __objt_stream(t);
+       case OBJ_TYPE_CHECK:     return __objt_check(t);
+       case OBJ_TYPE_ACME_RSLV: return __objt_acme_rslv(t);
 #ifdef USE_QUIC
        case OBJ_TYPE_DGRAM:    return __objt_dgram(t);
 #endif
index 3d762af48f2ac997c4f7025ecfe2a658ca9c319f..a21c326a0b3c35fdd0a72c79edb55ba08d743263 100644 (file)
@@ -4,6 +4,7 @@
  * Implements the ACMEv2 RFC 8555 protocol
  */
 
+#include "haproxy/ticks.h"
 #include <stddef.h>
 #include <stdlib.h>
 #include <string.h>
@@ -14,6 +15,7 @@
 
 #include <haproxy/acme-t.h>
 
+#include <haproxy/acme_resolvers.h>
 #include <haproxy/base64.h>
 #include <haproxy/intops.h>
 #include <haproxy/cfgparse.h>
@@ -24,6 +26,7 @@
 #include <haproxy/list.h>
 #include <haproxy/log.h>
 #include <haproxy/pattern.h>
+#include <haproxy/resolvers.h>
 #include <haproxy/sink.h>
 #include <haproxy/ssl_ckch.h>
 #include <haproxy/ssl_gencert.h>
@@ -118,6 +121,9 @@ static void acme_trace(enum trace_level level, uint64_t mask, const struct trace
                        case ACME_NEWACCOUNT:     chunk_appendf(&trace_buf, "ACME_NEWACCOUNT");   break;
                        case ACME_NEWORDER:       chunk_appendf(&trace_buf, "ACME_NEWORDER");     break;
                        case ACME_AUTH:           chunk_appendf(&trace_buf, "ACME_AUTH");         break;
+                       case ACME_RSLV_WAIT:      chunk_appendf(&trace_buf, "ACME_RSLV_WAIT");    break;
+                       case ACME_RSLV_TRIGGER:   chunk_appendf(&trace_buf, "ACME_RSLV_TRIGGER");   break;
+                       case ACME_RSLV_READY:     chunk_appendf(&trace_buf, "ACME_RSLV_READY");   break;
                        case ACME_CHALLENGE:      chunk_appendf(&trace_buf, "ACME_CHALLENGE");    break;
                        case ACME_CHKCHALLENGE:   chunk_appendf(&trace_buf, "ACME_CHKCHALLENGE"); break;
                        case ACME_FINALIZE:       chunk_appendf(&trace_buf, "ACME_FINALIZE");     break;
@@ -191,6 +197,7 @@ struct acme_cfg *new_acme_cfg(const char *name)
        ret->linenum = 0;
 
        ret->challenge = strdup("http-01"); /* default value */
+       ret->dns_delay = 300; /* default DNS re-trigger delay in seconds */
 
        /* The default generated keys are EC-384 */
        ret->key.type = EVP_PKEY_EC;
@@ -437,6 +444,49 @@ static int cfg_parse_acme_kws(char **args, int section_type, struct proxy *curpx
                        ha_alert("parsing [%s:%d]: out of memory.\n", file, linenum);
                        goto out;
                }
+       } else if (strcmp(args[0], "dns-check") == 0) {
+               if (!*args[1]) {
+                       ha_alert("parsing [%s:%d]: keyword '%s' in '%s' section requires an argument\n", file, linenum, args[0], cursection);
+                       err_code |= ERR_ALERT | ERR_FATAL;
+                       goto out;
+               }
+               if (alertif_too_many_args(1, file, linenum, args, &err_code))
+                       goto out;
+
+               if (strcmp(args[1], "on") == 0) {
+                       cur_acme->dns_check = 1;
+               } else if (strcmp(args[1], "off") == 0) {
+                       cur_acme->dns_check = 0;
+               } else {
+                       err_code |= ERR_ALERT | ERR_FATAL;
+                       ha_alert("parsing [%s:%d]: keyword '%s' in '%s' section requires either the 'on' or 'off' parameter\n", file, linenum, args[0], cursection);
+                       goto out;
+               }
+       } else if (strcmp(args[0], "dns-delay") == 0) {
+               const char *res;
+
+               if (!*args[1]) {
+                       ha_alert("parsing [%s:%d]: keyword '%s' in '%s' section requires an argument\n", file, linenum, args[0], cursection);
+                       err_code |= ERR_ALERT | ERR_FATAL;
+                       goto out;
+               }
+               if (alertif_too_many_args(1, file, linenum, args, &err_code))
+                       goto out;
+
+               res = parse_time_err(args[1], &cur_acme->dns_delay, TIME_UNIT_S);
+               if (res == PARSE_TIME_OVER) {
+                       ha_alert("parsing [%s:%d]: timer overflow in argument <%s> to '%s'\n", file, linenum, args[1], args[0]);
+                       err_code |= ERR_ALERT | ERR_FATAL;
+                       goto out;
+               } else if (res == PARSE_TIME_UNDER) {
+                       ha_alert("parsing [%s:%d]: timer underflow in argument <%s> to '%s'\n", file, linenum, args[1], args[0]);
+                       err_code |= ERR_ALERT | ERR_FATAL;
+                       goto out;
+               } else if (res) {
+                       ha_alert("parsing [%s:%d]: unexpected character '%c' in argument to '%s'\n", file, linenum, *res, args[0]);
+                       err_code |= ERR_ALERT | ERR_FATAL;
+                       goto out;
+               }
        } else if (strcmp(args[0], "reuse-key") == 0) {
                if (!*args[1]) {
                        ha_alert("parsing [%s:%d]: keyword '%s' in '%s' section requires an argument\n", file, linenum, args[0], cursection);
@@ -841,6 +891,8 @@ static struct cfg_kw_list cfg_kws_acme = {ILH, {
        { CFG_ACME, "curves",  cfg_parse_acme_cfg_key },
        { CFG_ACME, "map",  cfg_parse_acme_kws },
        { CFG_ACME, "reuse-key",  cfg_parse_acme_kws },
+       { CFG_ACME, "dns-check",  cfg_parse_acme_kws },
+       { CFG_ACME, "dns-delay",  cfg_parse_acme_kws },
        { CFG_ACME, "acme-vars",  cfg_parse_acme_vars_provider },
        { CFG_ACME, "provider-name",  cfg_parse_acme_vars_provider },
        { CFG_GLOBAL, "acme.scheduler", cfg_parse_global_acme_sched },
@@ -879,6 +931,7 @@ static void acme_ctx_destroy(struct acme_ctx *ctx)
                istfree(&auth->chall);
                istfree(&auth->token);
                istfree(&auth->dns);
+               acme_rslv_free(auth->rslv);
                next = auth->next;
                free(auth);
                auth = next;
@@ -2272,13 +2325,115 @@ re:
                                        goto retry;
                                }
                                if ((ctx->next_auth = ctx->next_auth->next) == NULL) {
-                                       st = ACME_CHALLENGE;
+                                       if (strcasecmp(ctx->cfg->challenge, "dns-01") == 0 && ctx->cfg->dns_check)
+                                               st = ACME_RSLV_WAIT;
+                                       else
+                                               st = ACME_CHALLENGE;
                                        ctx->next_auth = ctx->auths;
                                }
                                /* call with next auth or do the challenge step */
                                goto nextreq;
                        }
                break;
+               case ACME_RSLV_WAIT: {
+                       /* wait dns-delay */
+                       st = ACME_RSLV_TRIGGER;
+                       ctx->http_state = ACME_HTTP_REQ;
+                       ctx->state = st;
+                       send_log(NULL, LOG_NOTICE, "acme: %s: dns-01: triggering the resolution in %ds\n",
+                                                   ctx->store->path, ctx->cfg->dns_delay);
+
+                       task->expire = tick_add(now_ms, ctx->cfg->dns_delay * 1000);
+                       return task;
+               }
+               break;
+               case ACME_RSLV_TRIGGER: {
+                       struct acme_auth *auth;
+
+                       /* if it was trigger by the CLI, still wait dns_delay if
+                        * not everything is ready, or skip and to to
+                        * ACME_CHALLENGE */
+                       if (!(state & TASK_WOKEN_TIMER)) {
+                               int all_ready = 1;
+
+                               for (auth = ctx->auths; auth != NULL; auth = auth->next) {
+                                       if (auth->ready)
+                                               continue;
+                                       all_ready = 0;
+                               }
+                               if (all_ready) {
+                                       st = ACME_CHALLENGE;
+                                       ctx->http_state = ACME_HTTP_REQ;
+                                       ctx->state = st;
+                                       goto nextreq;
+                               } else {
+                                       return task;
+                               }
+                       }
+
+                       /* on timer expiry, re-trigger resolution for non-ready auths */
+                       for (auth = ctx->auths; auth != NULL; auth = auth->next) {
+                               if (auth->ready)
+                                       continue;
+
+                               HA_ATOMIC_INC(&ctx->dnstasks);
+
+                               auth->rslv = acme_rslv_start(auth, &ctx->dnstasks, &errmsg);
+                               if (!auth->rslv)
+                                       goto abort;
+                               auth->rslv->acme_task = task;
+                       }
+                       st = ACME_RSLV_READY;
+                       goto wait;
+               }
+               break;
+               case ACME_RSLV_READY: {
+                       struct acme_auth *auth;
+                       int all_ready = 1;
+
+                       /* if triggered by the CLI, wait for the DNS tasks to
+                        * finish
+                        */
+                        if (HA_ATOMIC_LOAD(&ctx->dnstasks) != 0)
+                               goto wait;
+
+                       /* triggered by the latest DNS task */
+                       for (auth = ctx->auths; auth != NULL; auth = auth->next) {
+                               if (auth->ready)
+                                       continue;
+                               if (auth->rslv->result != RSLV_STATUS_VALID) {
+                                       send_log(NULL, LOG_NOTICE, "acme: %s: dns-01: Couldn't get the TXT record for \"_acme-challenge.%.*s\", expected \"%.*s\" (status=%d)\n",
+                                                ctx->store->path, (int)auth->dns.len, auth->dns.ptr,
+                                                (int)auth->token.len, auth->token.ptr,
+                                                auth->rslv->result);
+                                       all_ready = 0;
+                               } else {
+                                       if (isteq(auth->rslv->txt, auth->token)) {
+                                               auth->ready = 1;
+                                       } else {
+                                               send_log(NULL, LOG_NOTICE, "acme: %s: dns-01: TXT record mismatch for \"_acme-challenge.%.*s\": expected \"%.*s\", got \"%.*s\"\n",
+                                                        ctx->store->path, (int)auth->dns.len, auth->dns.ptr,
+                                                        (int)auth->token.len, auth->token.ptr,
+                                                        (int)auth->rslv->txt.len, auth->rslv->txt.ptr);
+                                               all_ready = 0;
+                                       }
+                               }
+                               acme_rslv_free(auth->rslv);
+                               auth->rslv = NULL;
+                       }
+                       if (all_ready) {
+                               st = ACME_CHALLENGE;
+                               ctx->next_auth = ctx->auths;
+                               goto nextreq;
+                       }
+
+                       /* not all ready yet, retry after dns-delay */
+                       st = ACME_RSLV_WAIT;
+                       ctx->http_state = ACME_HTTP_REQ;
+                       ctx->state = st;
+                       goto nextreq;
+               }
+               break;
                case ACME_CHALLENGE:
                        if (http_st == ACME_HTTP_REQ) {
                                /* if challenge is already validated we skip this stage */
diff --git a/src/acme_resolvers.c b/src/acme_resolvers.c
new file mode 100644 (file)
index 0000000..65d03a3
--- /dev/null
@@ -0,0 +1,166 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+
+/*
+ * Implements the DNS resolution pre-check for dns-01
+ */
+
+#include <haproxy/openssl-compat.h>
+
+#if defined(HAVE_ACME)
+
+#include <stdlib.h>
+#include <string.h>
+
+
+#include <haproxy/acme_resolvers.h>
+#include <haproxy/applet.h>
+#include <haproxy/obj_type.h>
+#include <haproxy/resolvers.h>
+#include <haproxy/tools.h>
+
+/* success callback, copy the TXT string to rslv->txt */
+static int acme_rslv_success_cb(struct resolv_requester *req, struct dns_counters *counters)
+{
+       struct acme_rslv *rslv = objt_acme_rslv(req->owner);
+       struct resolv_resolution *res;
+       struct eb32_node *eb32;
+       struct resolv_answer_item *item;
+
+       if (!rslv)
+               return 1;
+
+       rslv->result = RSLV_STATUS_INVALID;
+
+       res = req->resolution;
+       if (!res)
+               goto done;
+
+       /* XXX: must fail on multiple TXT entries for the same dn */
+
+       /* copy the data from the response tree */
+       for (eb32 = eb32_first(&res->response.answer_tree); eb32 != NULL; eb32 = eb32_next(eb32)) {
+               item = eb32_entry(eb32, typeof(*item), link);
+               /* only handle 1 entry */
+               if (item->type == DNS_RTYPE_TXT) {
+                       int len = item->data_len;
+
+                       if (len > DNS_MAX_NAME_SIZE)
+                               len = DNS_MAX_NAME_SIZE;
+                       rslv->txt = istdup(ist2(item->data.target, len));
+                       break;
+               }
+       }
+
+       rslv->result = RSLV_STATUS_VALID;
+done:
+       /* if there's no other DNS task for this acme task, wake up acme_task */
+       if (HA_ATOMIC_SUB_FETCH(rslv->dnstasks, 1) == 0) {
+               if (rslv->acme_task)
+                       task_wakeup(rslv->acme_task, TASK_WOKEN_MSG);
+       }
+       return 1;
+}
+
+/* error callback, set the error code to rslv->result */
+static int acme_rslv_error_cb(struct resolv_requester *req, int error_code)
+{
+       struct acme_rslv *rslv = objt_acme_rslv(req->owner);
+
+       if (!rslv)
+               return 0;
+
+       rslv->result = error_code;
+       if (HA_ATOMIC_SUB_FETCH(rslv->dnstasks, 1) == 0) {
+               if (rslv->acme_task)
+                       task_wakeup(rslv->acme_task, TASK_WOKEN_MSG);
+       }
+
+       return 0;
+}
+
+/* unlink from the resolver and free the acme_rslv */
+void acme_rslv_free(struct acme_rslv *rslv)
+{
+       if (!rslv)
+               return;
+       if (rslv->requester)
+               resolv_unlink_resolution(rslv->requester);
+       free(rslv->hostname_dn);
+       istfree(&rslv->txt);
+       free(rslv);
+}
+
+struct acme_rslv *acme_rslv_start(struct acme_auth *auth, unsigned int *dnstasks, char **errmsg)
+{
+       struct acme_rslv *rslv = NULL;
+       struct resolvers *resolvers;
+       char hostname[DNS_MAX_NAME_SIZE + 1];
+       char dn[DNS_MAX_NAME_SIZE + 1];
+       int hostname_len;
+       int dn_len;
+
+       /* XXX: allow to change the resolvers section to use */
+       resolvers = find_resolvers_by_id("default");
+       if (!resolvers) {
+               memprintf(errmsg, "couldn't find the \"default\" resolvers section!\n");
+               goto error;
+       }
+
+       /* dns-01 TXT record lives at _acme-challenge.<domain> */
+       hostname_len = snprintf(hostname, sizeof(hostname), "_acme-challenge.%.*s",
+                               (int)auth->dns.len, auth->dns.ptr);
+       if (hostname_len < 0 || hostname_len >= (int)sizeof(hostname)) {
+               memprintf(errmsg, "hostname \"_acme-challenge.%.*s\" too long!\n", (int)auth->dns.len, auth->dns.ptr);
+               goto error;
+       }
+
+       dn_len = resolv_str_to_dn_label(hostname, hostname_len, dn, sizeof(dn));
+       if (dn_len <= 0) {
+               memprintf(errmsg, "couldn't convert hostname \"_acme-challenge.%.*s\" into dn label\n", (int)auth->dns.len, auth->dns.ptr);
+               goto error;
+       }
+
+       rslv = calloc(1, sizeof(*rslv));
+       if (!rslv) {
+               memprintf(errmsg, "Could not allocate memory\n");
+               goto error;
+       }
+
+       rslv->obj_type      = OBJ_TYPE_ACME_RSLV;
+       rslv->resolvers     = resolvers;
+       rslv->hostname_dn   = strdup(dn);
+       rslv->hostname_dn_len = dn_len;
+       rslv->result        = RSLV_STATUS_NONE;
+       rslv->success_cb    = acme_rslv_success_cb;
+       rslv->error_cb      = acme_rslv_error_cb;
+       rslv->dnstasks      = dnstasks;
+
+       if (!rslv->hostname_dn) {
+               memprintf(errmsg, "Could not allocate memory\n");
+               goto error;
+       }
+
+       if (resolv_link_resolution(rslv, OBJ_TYPE_ACME_RSLV, 0) < 0) {
+               memprintf(errmsg, "Could not create resolution task for \"%.*s\"\n", hostname_len, hostname);
+               goto error;
+       }
+
+       resolv_trigger_resolution(rslv->requester);
+
+       return rslv;
+
+error:
+       if (rslv)
+               free(rslv->hostname_dn);
+       free(rslv);
+       return NULL;
+}
+
+#endif /* HAVE_ACME */
+
+/*
+ * Local variables:
+ *  c-indent-level: 8
+ *  c-basic-offset: 8
+ * End:
+ */
index 4cc13179da50c1c5de8a9aa76fe95199f1548047..1b17b11e0a0df43fdc228027269bd2fb87d188bb 100644 (file)
@@ -21,6 +21,7 @@
 #include <import/cebis_tree.h>
 
 #include <haproxy/action.h>
+#include <haproxy/acme_resolvers.h>
 #include <haproxy/api.h>
 #include <haproxy/applet.h>
 #include <haproxy/cfgparse.h>
@@ -2170,6 +2171,25 @@ int resolv_link_resolution(void *requester, int requester_type, int requester_lo
                                           ? DNS_RTYPE_A
                                           : DNS_RTYPE_AAAA;
                        break;
+#if defined(HAVE_ACME)
+               case OBJ_TYPE_ACME_RSLV: {
+                       struct acme_rslv *acme_rslv = (struct acme_rslv *)requester;
+
+                       req = resolv_get_requester(&acme_rslv->requester,
+                                                  &acme_rslv->obj_type,
+                                                  acme_rslv->success_cb,
+                                                  acme_rslv->error_cb);
+                       if (!req)
+                               goto err;
+
+                       hostname_dn     = &acme_rslv->hostname_dn;
+                       hostname_dn_len = acme_rslv->hostname_dn_len;
+                       resolvers       = acme_rslv->resolvers;
+
+                       query_type      = DNS_RTYPE_TXT;
+                       break;
+               }
+#endif
                default:
                        goto err;
        }
@@ -2557,6 +2577,11 @@ struct task *process_resolvers(struct task *t, void *context, unsigned int state
                                                /* Always perform the resolution */
                                                must_run = 1;
                                                break;
+                                       case OBJ_TYPE_ACME_RSLV:
+                                               /* Always perform the resolution */
+                                               must_run = 1;
+                                               break;
+
                                        default:
                                                break;
                                }