Download raw body.
rpki-client: backoff retry for persistently non-functional CAs
rpki-client: backoff retry for persistently non-functional CAs
rpki-client: backoff retry for persistently non-functional CAs
Dear all,
Here is an updated version of the diff I'd like to put forward for review.
The idea is to persist state about non-functional RPKI CAs to disk, and
then use that state in the next invocation of the utility to decide
whether to sync or defer syncing (to a future invocation) for a given
malfunctioning CA. The program's decisions are exposed through json &
metrics output and in verbose / ultra verbose mode some or more log
lines are emitted. Of course the /var/cache/rpki-client/.nca_history
state file itself is interesting to inspect.
What this backoff retry mechanism gives us is a differentiated treatment
for persistently non-functional CAs (the utility will spend less
resources on such CAs), and better (more detailed) reporting on the
start and duration of the non-functioning of these CAs. The latter
helps the operator community (me, but also RIRs/NIRs) quite a bit in
developing & executing revocation procedures for non-functional CAs. The
mechanism also batches CAs together by RRDP URL (if there is one!), in
order to further conserve syncing effort.
The guiding design principle here is that non-functional CAs should
not impact the utility's execution flow _every time_ the utility is
run. This results in second-order benefits like not bumping into
synchronisation timeouts or wasting bandwidth & inodes, _every time_.
I think this is the diff that would've helped reduce RPKI data
propagation delays when that quirky (now revoked) non-functional
rsync-only CA existed under NICBR [1]. Of course, chances are weird
CAs will appear again in the future, I think its good to have some
automation in place to mitigate the worst of it in such situations.
Kind regards,
Job
[1]: https://mailarchive.ietf.org/arch/msg/sidrops/TxyuAvPqX8OOpnWMvVAgRlbIY6Y/
Index: Makefile
===================================================================
RCS file: /cvs/src/usr.sbin/rpki-client/Makefile,v
diff -u -p -r1.40 Makefile
--- Makefile 1 May 2026 11:22:24 -0000 1.40
+++ Makefile 1 Jun 2026 21:02:41 -0000
@@ -19,6 +19,7 @@ SRCS+= json.c
SRCS+= main.c
SRCS+= mft.c
SRCS+= mkdir.c
+SRCS+= nca.c
SRCS+= ometric.c
SRCS+= output-bgpd.c
SRCS+= output-bird.c
Index: cert.c
===================================================================
RCS file: /cvs/src/usr.sbin/rpki-client/cert.c,v
diff -u -p -r1.237 cert.c
--- cert.c 16 May 2026 07:27:03 -0000 1.237
+++ cert.c 1 Jun 2026 21:02:42 -0000
@@ -2048,7 +2048,8 @@ RB_GENERATE(brk_tree, brk, entry, brkcmp
* Add each CA cert into the non-functional CA tree.
*/
void
-cert_insert_nca(struct nca_tree *tree, const struct cert *cert, struct repo *rp)
+cert_insert_nca(struct nca_tree *tree, const struct cert *cert, time_t since,
+ time_t last_attempt, int attempts, int do_sync)
{
struct nonfunc_ca *nca;
@@ -2062,12 +2063,22 @@ cert_insert_nca(struct nca_tree *tree, c
err(1, NULL);
if ((nca->ski = strdup(cert->ski)) == NULL)
err(1, NULL);
+
+ if (cert->notify == NULL)
+ nca->notify = NULL;
+ else if ((nca->notify = strdup(cert->notify)) == NULL)
+ err(1, NULL);
+
nca->certid = cert->certid;
+ nca->repoid = cert->repoid;
nca->talid = cert->talid;
+ nca->since = since;
+ nca->last_attempt = last_attempt;
+ nca->attempts = attempts;
+ nca->deferred = do_sync ? 0 : 1;
if (RB_INSERT(nca_tree, tree, nca) != NULL)
errx(1, "non-functional CA tree corrupted");
- repo_stat_inc(rp, nca->talid, RTYPE_CER, STYPE_NONFUNC);
}
void
@@ -2077,11 +2088,11 @@ cert_remove_nca(struct nca_tree *tree, i
if ((found = RB_FIND(nca_tree, tree, &needle)) != NULL) {
RB_REMOVE(nca_tree, tree, found);
- repo_stat_inc(rp, found->talid, RTYPE_CER, STYPE_FUNC);
free(found->location);
free(found->carepo);
free(found->mfturi);
free(found->ski);
+ free(found->notify);
free(found);
}
}
Index: extern.h
===================================================================
RCS file: /cvs/src/usr.sbin/rpki-client/extern.h,v
diff -u -p -r1.281 extern.h
--- extern.h 18 May 2026 16:26:41 -0000 1.281
+++ extern.h 1 Jun 2026 21:02:42 -0000
@@ -29,6 +29,12 @@
#define MAX_MSG_SIZE (50 * 1024 * 1024)
+struct fqdnlistentry {
+ LIST_ENTRY(fqdnlistentry) entry;
+ char *fqdn;
+};
+LIST_HEAD(fqdns, fqdnlistentry);
+
enum cert_as_type {
CERT_AS_ID, /* single identifier */
CERT_AS_INHERIT, /* inherit from issuer */
@@ -147,6 +153,18 @@ struct cert {
unsigned char mfthash[SHA256_DIGEST_LENGTH]; /* of the parent mft */
};
+struct nca_hist {
+ RB_ENTRY(nca_hist) entry;
+ char *ski;
+ char *location;
+ char *mfturi;
+ char *notify;
+ time_t since;
+ time_t last_attempt;
+ int attempts;
+ int do_sync;
+};
+
/*
* Non-functional CA tree element.
* Initially all CA and TA certs are added to this tree.
@@ -158,8 +176,14 @@ struct nonfunc_ca {
char *carepo;
char *mfturi;
char *ski;
+ char *notify;
int certid;
+ unsigned int repoid;
int talid;
+ time_t since;
+ time_t last_attempt;
+ int attempts;
+ int deferred;
};
/*
@@ -601,8 +625,6 @@ enum stype {
STYPE_PROVIDERS,
STYPE_OVERFLOW,
STYPE_SEQNUM_GAP,
- STYPE_FUNC,
- STYPE_NONFUNC,
};
struct repo;
@@ -617,6 +639,7 @@ struct repotalstats {
uint32_t certs; /* certificates */
uint32_t certs_fail; /* invalid certificate */
uint32_t certs_nonfunc; /* non-functional CA certificates */
+ uint32_t certs_nonfunc_deferred; /* sync deferred nonfunc CAs */
uint32_t mfts; /* total number of manifests */
uint32_t mfts_gap; /* manifests with sequence gaps */
uint32_t mfts_fail; /* failing syntactic parse */
@@ -698,6 +721,7 @@ extern int filemode;
extern int excludeaspa;
extern int experimental;
extern int excludeas0;
+extern int retry_all_ncas;
extern const char *tals[];
extern const char *taldescs[];
extern unsigned int talrepocnt[];
@@ -726,7 +750,7 @@ struct cert *ta_validate(const char *, s
struct cert *cert_read(struct ibuf *);
void cert_insert_brks(struct brk_tree *, struct cert *);
void cert_insert_nca(struct nca_tree *, const struct cert *,
- struct repo *);
+ time_t, time_t, int, int);
void cert_remove_nca(struct nca_tree *, int, struct repo *);
enum rtype rtype_from_file_extension(const char *);
@@ -867,6 +891,12 @@ void proc_rsync(char *, char *, int) _
void proc_http(char *, int) __attribute__((noreturn));
void proc_rrdp(int) __attribute__((noreturn));
+
+/* Non-functional CAs. */
+int nca_history_check(const struct cert *, struct nca_tree *);
+void nca_history_load(void);
+void nca_history_save(struct nca_tree *, time_t);
+
/* Repository handling */
int filepath_add(struct filepath_tree *, char *, int, time_t, int);
int filepath_valid(struct filepath_tree *, char *, int);
@@ -895,6 +925,7 @@ void repo_cleanup(struct filepath_tree
int repo_check_timeout(int);
void repostats_new_files_inc(struct repo *, const char *);
void repo_stat_inc(struct repo *, int, enum rtype, enum stype);
+void repo_stat_inc_nca(unsigned int, int, int);
void repo_tal_stats_collect(void (*)(const struct repo *,
const struct repotalstats *, void *), int, void *);
void repo_stats_collect(void (*)(const struct repo *,
Index: main.c
===================================================================
RCS file: /cvs/src/usr.sbin/rpki-client/main.c,v
diff -u -p -r1.306 main.c
--- main.c 9 May 2026 01:22:32 -0000 1.306
+++ main.c 1 Jun 2026 21:02:42 -0000
@@ -73,6 +73,7 @@ int shortlistmode;
int rrdpon = 1;
int repo_timeout;
int experimental;
+int retry_all_ncas = 0;
time_t deadline;
/* 9999-12-31 23:59:59 UTC */
@@ -84,12 +85,6 @@ int64_t evaluation_time = X509_TIME_MIN
struct stats stats;
-struct fqdnlistentry {
- LIST_ENTRY(fqdnlistentry) entry;
- char *fqdn;
-};
-LIST_HEAD(fqdns, fqdnlistentry);
-
struct fqdns shortlist = LIST_HEAD_INITIALIZER(fqdns);
struct fqdns skiplist = LIST_HEAD_INITIALIZER(fqdns);
@@ -546,6 +541,9 @@ queue_add_from_cert(const struct cert *c
return;
}
+ if (!nca_history_check(cert, ncas))
+ return;
+
repo = repo_lookup(cert->talid, cert->repo,
rrdpon ? cert->notify : NULL);
if (repo == NULL)
@@ -575,7 +573,6 @@ queue_add_from_cert(const struct cert *c
err(1, NULL);
}
- cert_insert_nca(ncas, cert, repo);
entityq_add(npath, nfile, RTYPE_MFT, DIR_UNKNOWN, repo, NULL, 0,
cert->talid, cert->certid, NULL);
}
@@ -809,6 +806,7 @@ sum_stats(const struct repo *rp, const s
out->certs += in->certs;
out->certs_fail += in->certs_fail;
out->certs_nonfunc += in->certs_nonfunc;
+ out->certs_nonfunc_deferred += in->certs_nonfunc_deferred;
out->roas += in->roas;
out->roas_fail += in->roas_fail;
out->roas_invalid += in->roas_invalid;
@@ -1064,7 +1062,7 @@ main(int argc, char *argv[])
err(1, "pledge");
while ((c =
- getopt(argc, argv, "0Ab:Bcd:e:fH:jmnop:P:Rs:S:t:vVx")) != -1)
+ getopt(argc, argv, "0Ab:Bcd:e:fH:jmNnop:P:Rs:S:t:vVx")) != -1)
switch (c) {
case '0':
excludeas0 = 0;
@@ -1101,6 +1099,9 @@ main(int argc, char *argv[])
case 'm':
outformats |= FORMAT_OMETRIC;
break;
+ case 'N':
+ retry_all_ncas = 1;
+ break;
case 'n':
noop = 1;
break;
@@ -1348,6 +1349,8 @@ main(int argc, char *argv[])
if (fchdir(cachefd) == -1)
err(1, "fchdir");
+ nca_history_load();
+
while (entity_queue > 0 && !killme) {
int polltim;
@@ -1561,6 +1564,8 @@ main(int argc, char *argv[])
vd.buildtime = get_current_time();
+ nca_history_save(&vd.ncas, vd.buildtime);
+
/* change working directory to the output directory */
if (fchdir(outdirfd) == -1)
err(1, "fchdir output dir");
@@ -1600,9 +1605,11 @@ main(int argc, char *argv[])
stats.repo_tal_stats.spls_invalid);
}
printf("BGPsec Router Certificates: %u\n", stats.repo_tal_stats.brks);
- printf("Certificates: %u (%u invalid, %u non-functional)\n",
- stats.repo_tal_stats.certs, stats.repo_tal_stats.certs_fail,
- stats.repo_tal_stats.certs_nonfunc);
+ printf("Certificates: %u (%u invalid, %u non-functional, %u sync "
+ "deferred)\n", stats.repo_tal_stats.certs,
+ stats.repo_tal_stats.certs_fail,
+ stats.repo_tal_stats.certs_nonfunc,
+ stats.repo_tal_stats.certs_nonfunc_deferred);
printf("Trust Anchor Locators: %u (%u invalid)\n",
stats.tals, talsz - stats.tals);
printf("Manifests: %u (%u failed parse, %u seqnum gaps)\n",
@@ -1632,7 +1639,7 @@ main(int argc, char *argv[])
usage:
fprintf(stderr,
- "usage: rpki-client [-0ABcjmnoRVvx] [-b sourceaddr] [-d cachedir]"
+ "usage: rpki-client [-0ABcjmNnoRVvx] [-b sourceaddr] [-d cachedir]"
" [-e rsync_prog]\n"
" [-H fqdn] [-P posix-seconds] [-p threads]"
" [-S skiplist]\n"
Index: nca.c
===================================================================
RCS file: nca.c
diff -N nca.c
--- /dev/null 1 Jan 1970 00:00:00 -0000
+++ nca.c 1 Jun 2026 21:02:42 -0000
@@ -0,0 +1,446 @@
+/* $OpenBSD$ */
+/*
+ * Copyright (c) 2026 Job Snijders <job@bsd.nl>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <sys/limits.h>
+#include <sys/stat.h>
+#include <sys/time.h>
+#include <err.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "extern.h"
+
+static LIST_HEAD(, fqdnlistentry) notifys = LIST_HEAD_INITIALIZER(notifys);
+
+static RB_HEAD(nca_hist_tree, nca_hist) ncas_hist = RB_INITIALIZER(&ncas_hist);
+
+static inline int
+nca_hist_cmp(struct nca_hist *a, struct nca_hist *b)
+{
+ int cmp;
+
+ cmp = strcmp(a->ski, b->ski);
+ if (cmp > 0)
+ return 1;
+ if (cmp < 0)
+ return -1;
+
+ cmp = strcmp(a->location, b->location);
+ if (cmp > 0)
+ return 1;
+ if (cmp < 0)
+ return -1;
+
+ cmp = strcmp(a->mfturi, b->mfturi);
+ if (cmp > 0)
+ return 1;
+ if (cmp < 0)
+ return -1;
+
+ return 0;
+}
+
+RB_PROTOTYPE_STATIC(nca_hist_tree, nca_hist, entry, nca_hist_cmp);
+RB_GENERATE_STATIC(nca_hist_tree, nca_hist, entry, nca_hist_cmp);
+
+static void
+nca_hist_free(struct nca_hist *nca_hist)
+{
+ if (nca_hist == NULL)
+ return;
+
+ free(nca_hist->ski);
+ free(nca_hist->location);
+ free(nca_hist->mfturi);
+ free(nca_hist->notify);
+ free(nca_hist);
+}
+
+static void
+ncas_hist_free(void)
+{
+ struct nca_hist *nca_hist, *nca_hist_tmp;
+
+ RB_FOREACH_SAFE(nca_hist, nca_hist_tree, &ncas_hist, nca_hist_tmp) {
+ RB_REMOVE(nca_hist_tree, &ncas_hist, nca_hist);
+ nca_hist_free(nca_hist);
+ }
+}
+
+/*
+ * Decide whether to schedule a synchronization retry in order to control the
+ * rate of attempts for non-functional CAs.
+ * First just retry a few times consecutively, then insert 90 minute pauses
+ * between the retries, and after 24 hours settle on retrying only once per day
+ * (modulo any RRDP rpkiNotify batching by the caller).
+ * Return 1 if it is time to queue a retry, 0 otherwise.
+ */
+static int
+nca_decide_retry(const struct nca_hist *nca_hist)
+{
+ time_t now, since, last_attempt;
+
+ now = get_current_time();
+
+ since = nca_hist->since;
+ last_attempt = nca_hist->last_attempt;
+
+ if (nca_hist->attempts < 3)
+ return 1;
+
+ if ((now - since < 24 * 60 * 60) && (now > last_attempt + 90 * 60))
+ return 1;
+
+ if ((now - since > 24 * 60 * 60) && (now - last_attempt > 24 * 60 * 60))
+ return 1;
+
+ return 0;
+}
+
+/*
+ * Determine which non-functioncal CAs are eligible for retry.
+ * If multiple NCAs point to the same RRDP repo and at least one is eligible
+ * for retry, batch all of those together.
+ */
+static void
+ncas_decide_retries(void)
+{
+ struct nca_hist *nca_hist;
+ struct fqdnlistentry *fle, *fle_tmp;
+ size_t notify_len;
+
+ RB_FOREACH(nca_hist, nca_hist_tree, &ncas_hist) {
+ if (!nca_decide_retry(nca_hist))
+ continue;
+
+ nca_hist->do_sync = 1;
+
+ if (nca_hist->notify == NULL)
+ continue;
+
+ if ((fle = malloc(sizeof(*fle))) == NULL)
+ err(1, NULL);
+
+ if ((fle->fqdn = strdup(nca_hist->notify)) == NULL)
+ err(1, NULL);
+
+ LIST_INSERT_HEAD(¬ifys, fle, entry);
+ }
+
+ RB_FOREACH(nca_hist, nca_hist_tree, &ncas_hist) {
+ if (nca_hist->notify == NULL)
+ continue;
+
+ notify_len = strlen(nca_hist->notify);
+
+ LIST_FOREACH(fle, ¬ifys, entry) {
+ if (strlen(fle->fqdn) == notify_len &&
+ strncasecmp(nca_hist->notify, fle->fqdn,
+ notify_len) == 0) {
+ nca_hist->do_sync = 1;
+ break;
+ }
+ }
+ }
+
+ LIST_FOREACH_SAFE(fle, ¬ifys, entry, fle_tmp) {
+ LIST_REMOVE(fle, entry);
+ free(fle->fqdn);
+ free(fle);
+ }
+}
+
+void
+nca_history_load(void)
+{
+ FILE *f;
+ char *line = NULL;
+ size_t linesize = 0;
+ ssize_t linelen;
+ const char *errstr;
+ struct nca_hist *nca_hist;
+
+ if ((f = fopen(".nca_history", "r")) == NULL) {
+ if (errno == ENOENT)
+ return;
+ err(1, "failed to open .nca_history");
+ }
+
+ while ((linelen = getline(&line, &linesize, f)) != -1) {
+ char *l, *ski, *loc, *mfturi, *notify,
+ *since, *last_attempt, *attempts;
+ size_t loc_len, mfturi_len, notify_len;
+
+ /*
+ * Hex-encoded SHA1, time, time, attempts, cert location, mft,
+ * and rpkiNotify (which is optional and therefor could be '-').
+ */
+ if (linelen < 40 + 1 + 10 + 1 + 10 + 1 + 1 + 1 + 13 + 1 + 21 +
+ 1 + 1)
+ goto err;
+
+ if (line[linelen - 1] == '\n')
+ line[linelen - 1] = '\0';
+
+ if ((nca_hist = calloc(1, sizeof(*nca_hist))) == NULL)
+ err(1, NULL);
+
+ l = line;
+
+ if ((ski = strsep(&l, " ")) == NULL)
+ goto err;
+ if ((nca_hist->ski = strdup(ski)) == NULL)
+ err(1, NULL);
+
+ if ((since = strsep(&l, " ")) == NULL)
+ goto err;
+ nca_hist->since = strtonum(since, 1, LLONG_MAX, &errstr);
+ if (errstr != NULL)
+ goto err;
+
+ if ((last_attempt = strsep(&l, " ")) == NULL)
+ goto err;
+ nca_hist->last_attempt = strtonum(last_attempt, 1, LLONG_MAX,
+ &errstr);
+ if (errstr != NULL)
+ goto err;
+
+ if ((attempts = strsep(&l, " ")) == NULL)
+ goto err;
+ nca_hist->attempts = strtonum(attempts, 1, LLONG_MAX, &errstr);
+ if (errstr != NULL)
+ goto err;
+
+ if ((loc = strsep(&l, " ")) == NULL)
+ goto err;
+
+ /* minimal example cert location: ab.cd/a/b.cer */
+ if ((loc_len = strlen(loc)) < 13)
+ goto err;
+ if (strcmp(loc + loc_len - 4, ".cer") != 0)
+ goto err;
+ if (!valid_uri(loc, strlen(loc), NULL))
+ goto err;
+ if ((nca_hist->location = strdup(loc)) == NULL)
+ err(1, NULL);
+
+ if ((mfturi = strsep(&l, " ")) == NULL)
+ goto err;
+
+ /* minimal example mft location: rsync://ab.cd/a/b.mft */
+ if ((mfturi_len = strlen(mfturi)) < 21)
+ goto err;
+ if (strcmp(mfturi + mfturi_len - 4, ".mft") != 0)
+ goto err;
+ if (!valid_uri(mfturi, strlen(mfturi), RSYNC_PROTO))
+ goto err;
+ if ((nca_hist->mfturi = strdup(mfturi)) == NULL)
+ err(1, NULL);
+
+ notify = l;
+ if (notify == NULL)
+ goto err;
+
+ if (strcmp("-", notify) == 0) {
+ nca_hist->notify = NULL;
+ } else {
+ /* minimal example rpkiNotify: https://ab.cd/a.xml */
+ if ((notify_len = strlen(notify)) < 19)
+ goto err;
+ if (strcmp(notify + notify_len - 4, ".xml") != 0)
+ goto err;
+ if (!valid_uri(notify, strlen(notify), HTTPS_PROTO))
+ goto err;
+ if ((nca_hist->notify = strdup(notify)) == NULL)
+ err(1, NULL);
+ }
+
+ if (RB_INSERT(nca_hist_tree, &ncas_hist, nca_hist) != NULL)
+ err(1, "ncas_hist_tree corrupted");
+ }
+
+ if (ferror(f))
+ goto err;
+
+ fclose(f);
+ free(line);
+
+ ncas_decide_retries();
+
+ return;
+
+ err:
+ warnx("error reading .nca_history");
+ fclose(f);
+ unlink(".nca_history");
+
+ free(line);
+
+ nca_hist_free(nca_hist);
+
+ ncas_hist_free();
+}
+
+/*
+ * Look up history and synchronization eligibility for a given CA, if any.
+ * Return 1 to trigger a synchronization attempt, otherwise 0.
+ */
+int
+nca_history_check(const struct cert *cert, struct nca_tree *ncas)
+{
+ struct nca_hist *nca_hist, needle;
+ time_t since, last_attempt;
+ int attempts = 1, do_sync = 1;
+
+ since = last_attempt = get_current_time();
+
+ needle.ski = cert->ski;
+ needle.location = cert->path;
+ needle.mfturi = cert->mft;
+ if ((nca_hist = RB_FIND(nca_hist_tree, &ncas_hist, &needle)) != NULL) {
+ since = nca_hist->since;
+ attempts = nca_hist->attempts;
+
+ if (retry_all_ncas || nca_hist->do_sync == 1) {
+ if (verbose)
+ warnx("%s: retrying, non-functional since %s",
+ cert->path, time2str(since));
+ attempts++;
+ } else {
+ if (verbose > 1)
+ warnx("%s: deferring sync, non-functional since"
+ " %s", cert->path, time2str(since));
+ last_attempt = nca_hist->last_attempt;
+ do_sync = 0;
+ }
+ }
+
+ cert_insert_nca(ncas, cert, since, last_attempt, attempts, do_sync);
+
+ return do_sync;
+}
+
+static int
+ncas_sorted_cmp(const void *a, const void *b)
+{
+ int cmp;
+ struct nonfunc_ca *na = *(struct nonfunc_ca **)a;
+ struct nonfunc_ca *nb = *(struct nonfunc_ca **)b;
+
+ cmp = strcmp(na->mfturi, nb->mfturi);
+ if (cmp > 0)
+ return 1;
+ if (cmp < 0)
+ return -1;
+
+ cmp = strcmp(na->location, nb->location);
+ if (cmp > 0)
+ return 1;
+ if (cmp < 0)
+ return -1;
+
+ cmp = strcmp(na->ski, nb->ski);
+ if (cmp > 0)
+ return 1;
+ if (cmp < 0)
+ return -1;
+
+ return 0;
+}
+
+void
+nca_history_save(struct nca_tree *ncas, time_t buildtime)
+{
+ char temp[] = ".nca_history.XXXXXXXX";
+ FILE *f = NULL;
+ int fd;
+ struct nonfunc_ca *nca, **ncas_sorted = NULL;
+ size_t ncas_num = 0, idx = 0;
+ struct timespec ts[2];
+
+ if (RB_EMPTY(ncas)) {
+ unlink(".nca_history");
+ return;
+ }
+
+ if ((fd = mkostemp(temp, O_CLOEXEC)) == -1)
+ goto err;
+ (void)fchmod(fd, 0644);
+
+ if ((f = fdopen(fd, "w")) == NULL)
+ err(1, "fopen");
+
+ RB_FOREACH(nca, nca_tree, ncas)
+ ncas_num++;
+
+ if ((ncas_sorted = calloc(ncas_num, sizeof(ncas_sorted[0]))) == NULL)
+ err(1, NULL);
+
+ RB_FOREACH(nca, nca_tree, ncas)
+ ncas_sorted[idx++] = nca;
+
+ qsort(ncas_sorted, ncas_num, sizeof(ncas_sorted[0]), ncas_sorted_cmp);
+
+ for (idx = 0; idx < ncas_num; idx++) {
+ nca = ncas_sorted[idx];
+
+ repo_stat_inc_nca(nca->repoid, nca->talid, nca->deferred);
+
+ if (fprintf(f, "%s %lld %lld %d %s %s", nca->ski,
+ (long long)nca->since, (long long)nca->last_attempt,
+ nca->attempts, nca->location, nca->mfturi) < 0)
+ goto err;
+
+ if (nca->notify == NULL) {
+ if (fprintf(f, " -\n") < 0)
+ goto err;
+ } else {
+ if (fprintf(f, " %s\n", nca->notify) < 0)
+ goto err;
+ }
+ }
+
+ if (fclose(f) != 0) {
+ f = NULL;
+ goto err;
+ }
+
+ ts[0].tv_nsec = UTIME_OMIT;
+ ts[1].tv_sec = buildtime;
+ ts[1].tv_nsec = 0;
+
+ if (utimensat(AT_FDCWD, temp, ts, 0) == -1)
+ goto err;
+
+ if (rename(temp, ".nca_history") == -1)
+ goto err;
+
+ free(ncas_sorted);
+ ncas_hist_free();
+
+ return;
+
+ err:
+ warn("error saving non-functional CA history to %s", temp);
+ if (f != NULL)
+ fclose(f);
+ unlink(temp);
+ free(ncas_sorted);
+ ncas_hist_free();
+}
Index: output-json.c
===================================================================
RCS file: /cvs/src/usr.sbin/rpki-client/output-json.c,v
diff -u -p -r1.59 output-json.c
--- output-json.c 13 Nov 2025 15:18:53 -0000 1.59
+++ output-json.c 1 Jun 2026 21:02:42 -0000
@@ -64,6 +64,7 @@ outputheader_json(struct validation_data
json_do_int("certificates", st->repo_tal_stats.certs);
json_do_int("invalidcertificates", st->repo_tal_stats.certs_fail);
json_do_int("nonfunctionalcas", st->repo_tal_stats.certs_nonfunc);
+ json_do_int("deferredcas", st->repo_tal_stats.certs_nonfunc_deferred);
json_do_int("taks", st->repo_tal_stats.taks);
json_do_int("tals", st->tals);
json_do_int("invalidtals", talsz - st->tals);
@@ -192,7 +193,13 @@ output_json(FILE *out, struct validation
json_do_string("ta", taldescs[nca->talid]);
json_do_string("caRepository", nca->carepo);
json_do_string("rpkiManifest", nca->mfturi);
+ if (nca->notify != NULL)
+ json_do_string("rpkiNotify", nca->notify);
json_do_string("ski", nca->ski);
+ json_do_int("since", (long long)nca->since);
+ json_do_int("last_attempt", (long long)nca->last_attempt);
+ json_do_int("total_attempts", nca->attempts);
+ json_do_bool("deferred", nca->deferred);
json_do_end();
}
json_do_end();
Index: output-ometric.c
===================================================================
RCS file: /cvs/src/usr.sbin/rpki-client/output-ometric.c,v
diff -u -p -r1.16 output-ometric.c
--- output-ometric.c 13 Nov 2025 15:18:53 -0000 1.16
+++ output-ometric.c 1 Jun 2026 21:02:42 -0000
@@ -44,6 +44,9 @@ set_common_stats(const struct repotalsta
OKV("type", "state"), OKV("cert", "failed parse"), ol);
ometric_set_int_with_labels(metric, in->certs_nonfunc,
OKV("type", "state"), OKV("cert", "non-functional"), ol);
+ ometric_set_int_with_labels(metric, in->certs_nonfunc_deferred,
+ OKV("type", "state"), OKV("cert", "sync-deferred"),
+ ol);
ometric_set_int_with_labels(metric, in->mfts,
OKV("type", "state"), OKV("manifest", "valid"), ol);
Index: output.c
===================================================================
RCS file: /cvs/src/usr.sbin/rpki-client/output.c,v
diff -u -p -r1.45 output.c
--- output.c 13 Nov 2025 15:18:53 -0000 1.45
+++ output.c 1 Jun 2026 21:02:42 -0000
@@ -269,14 +269,15 @@ outputheader(FILE *out, struct validatio
"# CCR validated ASPA payloads hash: %s\n"
"# Route Origin Authorizations: %u (%u failed parse, %u invalid)\n"
"# BGPsec Router Certificates: %u\n"
- "# Certificates: %u (%u invalid, %u non-functional)\n",
- hn, tbuf, (long long)st->elapsed_time.tv_sec,
+ "# Certificates: %u (%u invalid, %u non-functional, %u sync "
+ "deferred)\n", hn, tbuf, (long long)st->elapsed_time.tv_sec,
(long long)st->user_time.tv_sec, (long long)st->system_time.tv_sec,
vd->ccr.mfts_hash, vd->ccr.vrps_hash, vd->ccr.vaps_hash,
st->repo_tal_stats.roas, st->repo_tal_stats.roas_fail,
st->repo_tal_stats.roas_invalid, st->repo_tal_stats.brks,
st->repo_tal_stats.certs, st->repo_tal_stats.certs_fail,
- st->repo_tal_stats.certs_nonfunc) < 0)
+ st->repo_tal_stats.certs_nonfunc,
+ st->repo_tal_stats.certs_nonfunc_deferred) < 0)
return -1;
if (fprintf(out,
Index: repo.c
===================================================================
RCS file: /cvs/src/usr.sbin/rpki-client/repo.c,v
diff -u -p -r1.81 repo.c
--- repo.c 13 May 2026 04:38:42 -0000 1.81
+++ repo.c 1 Jun 2026 21:02:42 -0000
@@ -1537,6 +1537,23 @@ repostats_new_files_inc(struct repo *rp,
rp->repostats.new_files++;
}
+void
+repo_stat_inc_nca(unsigned int id, int talid, int deferred)
+{
+ struct repo *rp;
+
+ SLIST_FOREACH(rp, &repos, entry) {
+ if (rp->id == id) {
+ rp->stats[talid].certs_nonfunc++;
+
+ if (deferred)
+ rp->stats[talid].certs_nonfunc_deferred++;
+
+ break;
+ }
+ }
+}
+
/*
* Update stats object of repository depending on rtype and subtype.
*/
@@ -1552,10 +1569,6 @@ repo_stat_inc(struct repo *rp, int talid
rp->stats[talid].certs++;
if (subtype == STYPE_FAIL)
rp->stats[talid].certs_fail++;
- if (subtype == STYPE_NONFUNC)
- rp->stats[talid].certs_nonfunc++;
- if (subtype == STYPE_FUNC)
- rp->stats[talid].certs_nonfunc--;
if (subtype == STYPE_BGPSEC) {
rp->stats[talid].certs--;
rp->stats[talid].brks++;
@@ -1854,6 +1867,9 @@ repo_cleanup_entry(FTSENT *e, struct fil
path = skip_dotslash(e->fts_path);
switch (e->fts_info) {
case FTS_NSOK:
+ if (e->fts_level == 1 && fts_state.type == BASE_DIR &&
+ strcmp(e->fts_name, ".nca_history") == 0)
+ break;
if (filepath_exists(tree, path)) {
e->fts_parent->fts_number++;
break;
Index: rpki-client.8
===================================================================
RCS file: /cvs/src/usr.sbin/rpki-client/rpki-client.8,v
diff -u -p -r1.140 rpki-client.8
--- rpki-client.8 30 May 2026 02:09:04 -0000 1.140
+++ rpki-client.8 1 Jun 2026 21:02:42 -0000
@@ -22,7 +22,7 @@
.Nd RPKI validator to support BGP routing security
.Sh SYNOPSIS
.Nm
-.Op Fl 0ABcjmnoRVvx
+.Op Fl 0ABcjmNnoRVvx
.Op Fl b Ar sourceaddr
.Op Fl d Ar cachedir
.Op Fl e Ar rsync_prog
@@ -153,6 +153,12 @@ for a description of the fields.
Create output in the file
.Pa metrics
in the output directory in OpenMetrics format.
+.It Fl N
+Disable the backoff retry mechanism applied to non-functional CAs.
+By default
+.Nm
+progressively decreases the synchronization frequency for persistently
+non-functional CAs, eventually settling on retrying about once per day.
.It Fl n
Offline mode.
Validate the contents of
rpki-client: backoff retry for persistently non-functional CAs
rpki-client: backoff retry for persistently non-functional CAs
rpki-client: backoff retry for persistently non-functional CAs