/* $OpenBSD: rtr.c,v 1.21 2024/04/09 12:05:07 claudio Exp $ */ /* * Copyright (c) 2020 Claudio Jeker * * 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 #include #include #include #include #include "bgpd.h" #include "session.h" #include "log.h" static void rtr_dispatch_imsg_parent(struct imsgbuf *); static void rtr_dispatch_imsg_rde(struct imsgbuf *); volatile sig_atomic_t rtr_quit; static struct imsgbuf *ibuf_main; static struct imsgbuf *ibuf_rde; static struct bgpd_config *conf, *nconf; static struct timer_head expire_timer; static int rtr_recalc_semaphore; static void rtr_sighdlr(int sig) { switch (sig) { case SIGINT: case SIGTERM: rtr_quit = 1; break; } } #define PFD_PIPE_MAIN 0 #define PFD_PIPE_RDE 1 #define PFD_PIPE_COUNT 2 #define EXPIRE_TIMEOUT 300 void rtr_sem_acquire(int cnt) { rtr_recalc_semaphore += cnt; } void rtr_sem_release(int cnt) { rtr_recalc_semaphore -= cnt; if (rtr_recalc_semaphore < 0) fatalx("rtr recalc semaphore underflow"); } /* * Every EXPIRE_TIMEOUT seconds traverse the static roa-set table and expire * all elements where the expires timestamp is smaller or equal to now. * If any change is done recalculate the RTR table. */ static unsigned int rtr_expire_roas(time_t now) { struct roa *roa, *nr; unsigned int recalc = 0; RB_FOREACH_SAFE(roa, roa_tree, &conf->roa, nr) { if (roa->expires != 0 && roa->expires <= now) { recalc++; RB_REMOVE(roa_tree, &conf->roa, roa); free(roa); } } if (recalc != 0) log_info("%u roa-set entries expired", recalc); return recalc; } static unsigned int rtr_expire_aspa(time_t now) { struct aspa_set *aspa, *na; unsigned int recalc = 0; RB_FOREACH_SAFE(aspa, aspa_tree, &conf->aspa, na) { if (aspa->expires != 0 && aspa->expires <= now) { recalc++; RB_REMOVE(aspa_tree, &conf->aspa, aspa); free_aspa(aspa); } } if (recalc != 0) log_info("%u aspa-set entries expired", recalc); return recalc; } void rtr_roa_insert(struct roa_tree *rt, struct roa *in) { struct roa *roa; if ((roa = malloc(sizeof(*roa))) == NULL) fatal("roa alloc"); memcpy(roa, in, sizeof(*roa)); if (RB_INSERT(roa_tree, rt, roa) != NULL) /* just ignore duplicates */ free(roa); } /* * Add an asnum to the aspa_set. The aspa_set is sorted by asnum. */ static void aspa_set_entry(struct aspa_set *aspa, uint32_t asnum) { uint32_t i, num, *newtas; for (i = 0; i < aspa->num; i++) { if (asnum < aspa->tas[i]) break; if (asnum == aspa->tas[i]) return; } num = aspa->num + 1; newtas = recallocarray(aspa->tas, aspa->num, num, sizeof(uint32_t)); if (newtas == NULL) fatal("aspa_set merge"); if (i < aspa->num) { memmove(newtas + i + 1, newtas + i, (aspa->num - i) * sizeof(uint32_t)); } newtas[i] = asnum; aspa->num = num; aspa->tas = newtas; } /* * Insert and merge an aspa_set into the aspa_tree at. */ void rtr_aspa_insert(struct aspa_tree *at, struct aspa_set *mergeset) { struct aspa_set *aspa, needle = { .as = mergeset->as }; uint32_t i; aspa = RB_FIND(aspa_tree, at, &needle); if (aspa == NULL) { if ((aspa = calloc(1, sizeof(*aspa))) == NULL) fatal("aspa insert"); aspa->as = mergeset->as; RB_INSERT(aspa_tree, at, aspa); } for (i = 0; i < mergeset->num; i++) aspa_set_entry(aspa, mergeset->tas[i]); } void rtr_main(int debug, int verbose) { struct passwd *pw; struct pollfd *pfd = NULL; void *newp; size_t pfd_elms = 0, i; time_t timeout; log_init(debug, LOG_DAEMON); log_setverbose(verbose); log_procinit(log_procnames[PROC_RTR]); if ((pw = getpwnam(BGPD_USER)) == NULL) fatal("getpwnam"); if (chroot(pw->pw_dir) == -1) fatal("chroot"); if (chdir("/") == -1) fatal("chdir(\"/\")"); setproctitle("rtr engine"); if (setgroups(1, &pw->pw_gid) || setresgid(pw->pw_gid, pw->pw_gid, pw->pw_gid) || setresuid(pw->pw_uid, pw->pw_uid, pw->pw_uid)) fatal("can't drop privileges"); if (pledge("stdio recvfd", NULL) == -1) fatal("pledge"); signal(SIGTERM, rtr_sighdlr); signal(SIGINT, rtr_sighdlr); signal(SIGPIPE, SIG_IGN); signal(SIGHUP, SIG_IGN); signal(SIGALRM, SIG_IGN); signal(SIGUSR1, SIG_IGN); if ((ibuf_main = malloc(sizeof(struct imsgbuf))) == NULL) fatal(NULL); imsg_init(ibuf_main, 3); conf = new_config(); log_info("rtr engine ready"); TAILQ_INIT(&expire_timer); timer_set(&expire_timer, Timer_Rtr_Expire, EXPIRE_TIMEOUT); while (rtr_quit == 0) { i = rtr_count(); if (pfd_elms < PFD_PIPE_COUNT + i) { if ((newp = reallocarray(pfd, PFD_PIPE_COUNT + i, sizeof(struct pollfd))) == NULL) fatal("realloc pollfd"); pfd = newp; pfd_elms = PFD_PIPE_COUNT + i; } /* run the expire timeout every EXPIRE_TIMEOUT seconds */ timeout = timer_nextduein(&expire_timer, getmonotime()); if (timeout == -1) fatalx("roa-set expire timer no longer running"); memset(pfd, 0, sizeof(struct pollfd) * pfd_elms); set_pollfd(&pfd[PFD_PIPE_MAIN], ibuf_main); set_pollfd(&pfd[PFD_PIPE_RDE], ibuf_rde); i = PFD_PIPE_COUNT; i += rtr_poll_events(pfd + i, pfd_elms - i, &timeout); if (poll(pfd, i, timeout * 1000) == -1) { if (errno == EINTR) continue; fatal("poll error"); } if (handle_pollfd(&pfd[PFD_PIPE_MAIN], ibuf_main) == -1) fatalx("Lost connection to parent"); else rtr_dispatch_imsg_parent(ibuf_main); if (handle_pollfd(&pfd[PFD_PIPE_RDE], ibuf_rde) == -1) { log_warnx("RTR: Lost connection to RDE"); msgbuf_clear(&ibuf_rde->w); free(ibuf_rde); ibuf_rde = NULL; } else rtr_dispatch_imsg_rde(ibuf_rde); i = PFD_PIPE_COUNT; rtr_check_events(pfd + i, pfd_elms - i); if (timer_nextisdue(&expire_timer, getmonotime()) != NULL) { timer_set(&expire_timer, Timer_Rtr_Expire, EXPIRE_TIMEOUT); if (rtr_expire_roas(time(NULL)) != 0) rtr_recalc(); if (rtr_expire_aspa(time(NULL)) != 0) rtr_recalc(); } } rtr_shutdown(); free_config(conf); free(pfd); /* close pipes */ if (ibuf_rde) { msgbuf_clear(&ibuf_rde->w); close(ibuf_rde->fd); free(ibuf_rde); } msgbuf_clear(&ibuf_main->w); close(ibuf_main->fd); free(ibuf_main); log_info("rtr engine exiting"); exit(0); } static void rtr_dispatch_imsg_parent(struct imsgbuf *imsgbuf) { static struct aspa_set *aspa; struct imsg imsg; struct bgpd_config tconf; struct roa roa; char descr[PEER_DESCR_LEN]; struct rtr_session *rs; uint32_t rtrid; int n, fd; while (imsgbuf) { if ((n = imsg_get(imsgbuf, &imsg)) == -1) fatal("%s: imsg_get error", __func__); if (n == 0) break; rtrid = imsg_get_id(&imsg); switch (imsg_get_type(&imsg)) { case IMSG_SOCKET_CONN_RTR: if ((fd = imsg_get_fd(&imsg)) == -1) { log_warnx("expected to receive imsg fd " "but didn't receive any"); break; } if (ibuf_rde) { log_warnx("Unexpected imsg ctl " "connection to RDE received"); msgbuf_clear(&ibuf_rde->w); free(ibuf_rde); } if ((ibuf_rde = malloc(sizeof(struct imsgbuf))) == NULL) fatal(NULL); imsg_init(ibuf_rde, fd); break; case IMSG_SOCKET_CONN: if ((fd = imsg_get_fd(&imsg)) == -1) { log_warnx("expected to receive imsg fd " "but didn't receive any"); break; } if ((rs = rtr_get(rtrid)) == NULL) { log_warnx("IMSG_SOCKET_CONN: unknown rtr id %d", rtrid); close(fd); break; } rtr_open(rs, fd); break; case IMSG_RECONF_CONF: if (imsg_get_data(&imsg, &tconf, sizeof(tconf)) == -1) fatal("imsg_get_data"); nconf = new_config(); copy_config(nconf, &tconf); rtr_config_prep(); break; case IMSG_RECONF_ROA_ITEM: if (imsg_get_data(&imsg, &roa, sizeof(roa)) == -1) fatal("imsg_get_data"); rtr_roa_insert(&nconf->roa, &roa); break; case IMSG_RECONF_ASPA: if (aspa != NULL) fatalx("unexpected IMSG_RECONF_ASPA"); if ((aspa = calloc(1, sizeof(*aspa))) == NULL) fatal("aspa alloc"); if (imsg_get_data(&imsg, aspa, offsetof(struct aspa_set, tas)) == -1) fatal("imsg_get_data"); break; case IMSG_RECONF_ASPA_TAS: if (aspa == NULL) fatalx("unexpected IMSG_RECONF_ASPA_TAS"); aspa->tas = reallocarray(NULL, aspa->num, sizeof(*aspa->tas)); if (aspa->tas == NULL) fatal("aspa tas alloc"); if (imsg_get_data(&imsg, aspa->tas, aspa->num * sizeof(*aspa->tas)) == -1) fatal("imsg_get_data"); break; case IMSG_RECONF_ASPA_DONE: if (aspa == NULL) fatalx("unexpected IMSG_RECONF_ASPA_DONE"); if (RB_INSERT(aspa_tree, &nconf->aspa, aspa) != NULL) { log_warnx("duplicate ASPA set received"); free_aspa(aspa); } aspa = NULL; break; case IMSG_RECONF_RTR_CONFIG: if (imsg_get_data(&imsg, descr, sizeof(descr)) == -1) fatal("imsg_get_data"); rs = rtr_get(rtrid); if (rs == NULL) rtr_new(rtrid, descr); else rtr_config_keep(rs); break; case IMSG_RECONF_DRAIN: imsg_compose(ibuf_main, IMSG_RECONF_DRAIN, 0, 0, -1, NULL, 0); break; case IMSG_RECONF_DONE: if (nconf == NULL) fatalx("got IMSG_RECONF_DONE but no config"); copy_config(conf, nconf); /* switch the roa, first remove the old one */ free_roatree(&conf->roa); /* then move the RB tree root */ RB_ROOT(&conf->roa) = RB_ROOT(&nconf->roa); RB_ROOT(&nconf->roa) = NULL; /* switch the aspa tree, first remove the old one */ free_aspatree(&conf->aspa); /* then move the RB tree root */ RB_ROOT(&conf->aspa) = RB_ROOT(&nconf->aspa); RB_ROOT(&nconf->aspa) = NULL; /* finally merge the rtr session */ rtr_config_merge(); rtr_expire_roas(time(NULL)); rtr_expire_aspa(time(NULL)); rtr_recalc(); log_info("RTR engine reconfigured"); imsg_compose(ibuf_main, IMSG_RECONF_DONE, 0, 0, -1, NULL, 0); free_config(nconf); nconf = NULL; break; case IMSG_CTL_SHOW_RTR: if ((rs = rtr_get(rtrid)) == NULL) { log_warnx("IMSG_CTL_SHOW_RTR: " "unknown rtr id %d", rtrid); break; } rtr_show(rs, imsg_get_pid(&imsg)); break; case IMSG_CTL_END: imsg_compose(ibuf_main, IMSG_CTL_END, 0, imsg_get_pid(&imsg), -1, NULL, 0); break; } imsg_free(&imsg); } } static void rtr_dispatch_imsg_rde(struct imsgbuf *imsgbuf) { struct imsg imsg; int n; while (imsgbuf) { if ((n = imsg_get(imsgbuf, &imsg)) == -1) fatal("%s: imsg_get error", __func__); if (n == 0) break; /* NOTHING */ imsg_free(&imsg); } } void rtr_imsg_compose(int type, uint32_t id, pid_t pid, void *data, size_t datalen) { imsg_compose(ibuf_main, type, id, pid, -1, data, datalen); } /* * Compress aspa_set tas_aid into the bitfield used by the RDE. * Returns the size of tas and tas_aid bitfield required for this aspa_set. * At the same time tas_aid is overwritten with the bitmasks or cleared * if no extra aid masks are needed. */ static size_t rtr_aspa_set_size(struct aspa_set *aspa) { return aspa->num * sizeof(uint32_t); } /* * Merge all RPKI ROA trees into one as one big union. * Simply try to add all roa entries into a new RB tree. * This could be made a fair bit faster but for now this is good enough. */ void rtr_recalc(void) { struct roa_tree rt; struct aspa_tree at; struct roa *roa, *nr; struct aspa_set *aspa; struct aspa_prep ap = { 0 }; if (rtr_recalc_semaphore > 0) return; RB_INIT(&rt); RB_INIT(&at); RB_FOREACH(roa, roa_tree, &conf->roa) rtr_roa_insert(&rt, roa); rtr_roa_merge(&rt); imsg_compose(ibuf_rde, IMSG_RECONF_ROA_SET, 0, 0, -1, NULL, 0); RB_FOREACH_SAFE(roa, roa_tree, &rt, nr) { imsg_compose(ibuf_rde, IMSG_RECONF_ROA_ITEM, 0, 0, -1, roa, sizeof(*roa)); } free_roatree(&rt); RB_FOREACH(aspa, aspa_tree, &conf->aspa) rtr_aspa_insert(&at, aspa); rtr_aspa_merge(&at); RB_FOREACH(aspa, aspa_tree, &at) { ap.datasize += rtr_aspa_set_size(aspa); ap.entries++; } imsg_compose(ibuf_rde, IMSG_RECONF_ASPA_PREP, 0, 0, -1, &ap, sizeof(ap)); /* walk tree in reverse because aspa_add_set requires that */ RB_FOREACH_REVERSE(aspa, aspa_tree, &at) { struct aspa_set as = { .as = aspa->as, .num = aspa->num }; /* XXX prevent oversized IMSG for now */ if (aspa->num * sizeof(*aspa->tas) > MAX_IMSGSIZE - IMSG_HEADER_SIZE) { log_warnx("oversized ASPA set for customer-as %s, %s", log_as(aspa->as), "dropped"); continue; } imsg_compose(ibuf_rde, IMSG_RECONF_ASPA, 0, 0, -1, &as, offsetof(struct aspa_set, tas)); imsg_compose(ibuf_rde, IMSG_RECONF_ASPA_TAS, 0, 0, -1, aspa->tas, aspa->num * sizeof(*aspa->tas)); imsg_compose(ibuf_rde, IMSG_RECONF_ASPA_DONE, 0, 0, -1, NULL, 0); } free_aspatree(&at); imsg_compose(ibuf_rde, IMSG_RECONF_DONE, 0, 0, -1, NULL, 0); }