From: Job Snijders Subject: Re: rpki-client: backoff retry for persistently non-functional CAs To: tech@openbsd.org Date: Sat, 30 May 2026 17:08:55 +0000 Dear all, I just wanted to share some work-in-progress - On Fri, May 15, 2026 at 11:56:17AM +0000, Job Snijders wrote: > While some large CAs (e.g., RIPE & APNIC) have adopted a policy of > eventually revoking persistently non-functional CAs, such a practice is > not universally adopted among RIR and NIRs (nor is expected to ever be). > With the below changeset, the existing detection mechanism for > non-functional CAs is extended into a stateful backoff retry mechanism. > While there, expose the newly gathered state in the JSON output and fix > failure attribution in the openmetrics output (by counting detected > non-func CAs towards the parent repo instead of the broken CA's own > repo). This new version of the changeset (partially) resolves a metastable behaviour I didn't quite like in the previous diff. This version groups together non-functional CAs based on the FQDN, so that retries eventually end up being done in batches. My goal is that _if_ an effort is made to synchronize against a given FQDN (via one or another transport), we'd better check if that one synchronisation task unjiggled any of the non-functioncal CAs dependent on that synchronisation task to not waste syncing effort. With the previous diff, the RP could end up syncing frequently to a publication point (more than once a day) without actually trying to find manifests for all the non-functioncal CAs pointing into that remote repo. This metastable behaviour was most noticable under NICBR where CAs frequently switch from functional to non-functioncal throughout the day. My next step is to figure out a batching scheme that also takes the transpot into account: when it's time to retry an rsync-only non-functioncal CA, this in itself shouldn't trigger RRDP work related to adjacent non-functioncal CAs. I think I need to slice the NCA batches not just by FQDN but maybe also some other parameters. Kind regards, Job 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 30 May 2026 07:43:57 -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 30 May 2026 07:43:57 -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; @@ -2063,11 +2064,15 @@ cert_insert_nca(struct nca_tree *tree, c if ((nca->ski = strdup(cert->ski)) == 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,7 +2082,6 @@ 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); 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 30 May 2026 07:43:57 -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,17 @@ 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; + time_t since; + time_t last_attempt; + int attempts; + int defer; +}; + /* * Non-functional CA tree element. * Initially all CA and TA certs are added to this tree. @@ -159,7 +176,12 @@ struct nonfunc_ca { char *mfturi; char *ski; int certid; + unsigned int repoid; int talid; + time_t since; + time_t last_attempt; + int attempts; + int deferred; }; /* @@ -601,8 +623,6 @@ enum stype { STYPE_PROVIDERS, STYPE_OVERFLOW, STYPE_SEQNUM_GAP, - STYPE_FUNC, - STYPE_NONFUNC, }; struct repo; @@ -617,6 +637,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 +719,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 +748,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 +889,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 *); + /* Repository handling */ int filepath_add(struct filepath_tree *, char *, int, time_t, int); int filepath_valid(struct filepath_tree *, char *, int); @@ -895,6 +923,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 30 May 2026 07:43:57 -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); + /* 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 30 May 2026 07:43:57 -0000 @@ -0,0 +1,410 @@ +/* $OpenBSD$ */ +/* + * Copyright (c) 2026 Job Snijders + * + * 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 +#include +#include +#include +#include +#include +#include + +#include "extern.h" + +static LIST_HEAD(, fqdnlistentry) retrylist = LIST_HEAD_INITIALIZER(retrylist); + +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); +} + +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 retry in order to control the rate of + * synchronization attempts for non-functional CAs. + * First just retry a few times consecutively, then insert 90 minute + * pauses between retries, and after a full day settle on retrying only + * once per day. + * 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; +} + +/* + * Batch retries per FQDN. + */ +static void +ncas_batch_retries(void) +{ + struct nca_hist *nca_hist; + char *fqdn, *host; + size_t host_len; + struct fqdnlistentry *fle, *fle_tmp; + + RB_FOREACH(nca_hist, nca_hist_tree, &ncas_hist) { + nca_hist->defer = 1; + + if (!nca_decide_retry(nca_hist)) + continue; + + fqdn = strdup(nca_hist->mfturi + RSYNC_PROTO_LEN); + if (fqdn == NULL) + err(1, NULL); + + fqdn[strcspn(fqdn, "/")] = '\0'; + + if ((fle = malloc(sizeof(*fle))) == NULL) + err(1, NULL); + if ((fle->fqdn = strdup(fqdn)) == NULL) + err(1, NULL); + + LIST_INSERT_HEAD(&retrylist, fle, entry); + + free(fqdn); + fqdn = NULL; + fle = NULL; + } + + RB_FOREACH(nca_hist, nca_hist_tree, &ncas_hist) { + LIST_FOREACH(fle, &retrylist, entry) { + host = nca_hist->mfturi + RSYNC_PROTO_LEN; + host_len = strcspn(host, "/"); + + if (strlen(fle->fqdn) == host_len && + strncasecmp(host, fle->fqdn, host_len) == 0) { + nca_hist->defer = 0; + break; + } + } + } + + LIST_FOREACH_SAFE(fle, &retrylist, 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, *since, *last_attempt, *attempts; + size_t loc_len, mfturi_len; + + /* + * Hex-encoded SHA1, time, time, attempts, cert location & mft. + */ + if (linelen < 40 + 1 + 10 + 1 + 10 + 1 + 1 + 1 + 13 + 1 + 21) + 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 (l == NULL) + goto err; + + /* minimal example mft location: rsync://ab.cd/a/b.mft */ + mfturi = l; + 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); + + if (RB_INSERT(nca_hist_tree, &ncas_hist, nca_hist) != NULL) + err(1, "ncas_hist_tree corrupted"); + + nca_hist = NULL; + } + + if (ferror(f)) + goto err; + + fclose(f); + free(line); + + ncas_batch_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 for a given CA, if any. + * Return 1 to schedule a sync, 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->defer == 0) + attempts++; + else { + last_attempt = nca_hist->last_attempt; + do_sync = 0; + } + } + + if (!do_sync && verbose > 1) + warnx("%s: deferring sync, non-functional since %s", + cert->path, time2str(since)); + + 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) +{ + char temp[] = ".nca_history.XXXXXXXX"; + FILE *f = NULL; + int fd; + struct nonfunc_ca *nca, **ncas_sorted = NULL; + size_t ncas_num = 0, idx = 0; + + 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\n", nca->ski, + (long long)nca->since, (long long)nca->last_attempt, + nca->attempts, nca->location, nca->mfturi) < 0) + goto err; + } + + if (fclose(f) != 0) { + f = NULL; + 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 30 May 2026 07:43:57 -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); @@ -193,6 +194,10 @@ output_json(FILE *out, struct validation json_do_string("caRepository", nca->carepo); json_do_string("rpkiManifest", nca->mfturi); 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 30 May 2026 07:43:57 -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 30 May 2026 07:43:57 -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 30 May 2026 07:43:58 -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.139 rpki-client.8 --- rpki-client.8 17 Feb 2026 13:54:42 -0000 1.139 +++ rpki-client.8 30 May 2026 07:43:58 -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 once per day. .It Fl n Offline mode. Validate the contents of