From: Alan T. DeKok Date: Thu, 2 Mar 2023 21:27:11 +0000 (-0500) Subject: move more of the state machine into proto_bfd X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=9215754db55b17e4430e62464dd9ed1e3d74ee54;p=thirdparty%2Ffreeradius-server.git move more of the state machine into proto_bfd the state machine doesn't send or receive packets, but that will gradually get fixed. --- diff --git a/src/listen/bfd/proto_bfd.c b/src/listen/bfd/proto_bfd.c index 06932e844ec..4965431a7d8 100644 --- a/src/listen/bfd/proto_bfd.c +++ b/src/listen/bfd/proto_bfd.c @@ -42,10 +42,10 @@ static CONF_PARSER const proto_bfd_config[] = { }; static const CONF_PARSER peer_config[] = { - { FR_CONF_OFFSET("min_transmit_interval", FR_TYPE_TIME_DELTA, proto_bfd_peer_t, min_transmit_interval ) }, - { FR_CONF_OFFSET("min_receive_interval", FR_TYPE_TIME_DELTA, proto_bfd_peer_t, min_receive_interval ) }, - { FR_CONF_OFFSET("max_timeouts", FR_TYPE_UINT32, proto_bfd_peer_t, max_timeouts ) }, - + { FR_CONF_OFFSET("min_transmit_interval", FR_TYPE_TIME_DELTA, proto_bfd_peer_t, desired_min_tx_interval ) }, + { FR_CONF_OFFSET("min_receive_interval", FR_TYPE_TIME_DELTA, proto_bfd_peer_t, required_min_rx_interval ) }, + { FR_CONF_OFFSET("max_timeouts", FR_TYPE_UINT32, proto_bfd_peer_t, detect_multi ) }, + { FR_CONF_OFFSET("demand", FR_TYPE_BOOL, proto_bfd_peer_t, demand_mode ) }, CONF_PARSER_TERMINATOR }; @@ -433,14 +433,14 @@ static int mod_bootstrap(module_inst_ctx_t const *mctx) peer = (proto_bfd_peer_t *) c; - FR_TIME_DELTA_BOUND_CHECK("peer.min_transmit_interval", peer->min_transmit_interval, >=, fr_time_delta_from_usec(30)); - FR_TIME_DELTA_BOUND_CHECK("peer.min_transmit_interval", peer->min_transmit_interval, <=, fr_time_delta_from_sec(2)); + FR_TIME_DELTA_BOUND_CHECK("peer.min_transmit_interval", peer->desired_min_tx_interval, >=, fr_time_delta_from_usec(30)); + FR_TIME_DELTA_BOUND_CHECK("peer.min_transmit_interval", peer->desired_min_tx_interval, <=, fr_time_delta_from_sec(2)); - FR_TIME_DELTA_BOUND_CHECK("peer.min_recieve_interval", peer->min_transmit_interval, >=, fr_time_delta_from_usec(30)); - FR_TIME_DELTA_BOUND_CHECK("peer.min_received_interval", peer->min_transmit_interval, <=, fr_time_delta_from_sec(2)); + FR_TIME_DELTA_BOUND_CHECK("peer.min_recieve_interval", peer->required_min_rx_interval, >=, fr_time_delta_from_usec(30)); + FR_TIME_DELTA_BOUND_CHECK("peer.min_received_interval", peer->required_min_rx_interval, <=, fr_time_delta_from_sec(2)); - FR_INTEGER_BOUND_CHECK("peer.max_timeouts", peer->max_timeouts, >=, 1); - FR_INTEGER_BOUND_CHECK("peer.max_timeouts", peer->max_timeouts, <=, 10); + FR_INTEGER_BOUND_CHECK("peer.max_timeouts", peer->detect_multi, >=, 1); + FR_INTEGER_BOUND_CHECK("peer.max_timeouts", peer->detect_multi, <=, 10); if (((c->ipaddr.af == AF_INET) && (c->ipaddr.prefix != 32)) || ((c->ipaddr.af == AF_INET6) && (c->ipaddr.prefix != 128))) { diff --git a/src/listen/bfd/proto_bfd.h b/src/listen/bfd/proto_bfd.h index 0de1304270f..6baa7909e00 100644 --- a/src/listen/bfd/proto_bfd.h +++ b/src/listen/bfd/proto_bfd.h @@ -18,10 +18,10 @@ /* * $Id$ * - * @file proto_radius.h + * @file proto_bfd.h * @brief Structures for the RADIUS protocol * - * @copyright 2018 Alan DeKok (aland@freeradius.org) + * @copyright 2023 Network RADIUS SAS (legal@networkradius.com) */ #include #include @@ -41,7 +41,76 @@ typedef struct { typedef struct { fr_client_t client; //!< might as well re-use this, others need it - fr_time_delta_t min_transmit_interval; - fr_time_delta_t min_receive_interval; - uint32_t max_timeouts; + /* + * Peers are defined globally to a virtual server. Each + * peer can only have one session associated with it. + */ + void *inst; //!< proto_bfd_udp instance using this session + fr_listen_t *listen; //!< associated listener + + int sockfd; //!< cached for laziness + fr_event_list_t *el; + + struct sockaddr_storage remote_sockaddr; //!< cached for laziness + socklen_t salen; + + /* + * Internal state management + */ + fr_event_timer_t const *ev_timeout; //!< when we time out for not receiving a packet + fr_event_timer_t const *ev_packet; //!< for when we next send a packet + fr_time_t last_recv; //!< last received packet + fr_time_t next_recv; //!< when we next expect to receive a packet + fr_time_t last_sent; //!< the last time we sent a packet + + bfd_session_state_t session_state; //!< our view of the session state + bfd_session_state_t remote_session_state; //!< their view of the session state + + /* + * BFD state machine, and fields we use to manage it. + * + * The public names in the configuration files are what makes sense. + * + * The names here are the names from the protocol, so that we can be sure the state machine is + * implemented correctly. + */ + uint32_t local_disc; //!< our session ID, which is unique to this session + uint32_t remote_disc; //!< their session ID + + bfd_diag_t local_diag; //!< diagnostics for errors + + uint32_t detect_multi; + + fr_time_delta_t desired_min_tx_interval; //!< intervals between transmits + fr_time_delta_t required_min_rx_interval; //!< intervals between receives + + fr_time_delta_t remote_min_rx_interval; //!< their min_rx_interval + fr_time_delta_t remote_min_echo_rx_interval; //!< their min_echo_rx_interval + + fr_time_delta_t my_min_echo_rx_interval; //!< what we send for echo_rx_interval + + fr_time_delta_t next_min_tx_interval; //!< how to update this when we're polling + + + bool demand_mode; //!< demand is "once session is up, stop sending packets" + bool remote_demand_mode; //!< their demand mode + + bool doing_poll; + + /* + * Authentication configuration and states. + */ + bool auth_seq_known; //!< do we know the authentication sequence number? + + bfd_auth_type_t auth_type; //!< what kind of authentication is used + + uint32_t recv_auth_seq; //!< their auth_seq number + uint32_t xmit_auth_seq; //!< our auth_seq number + + size_t secret_len; //!< doesn't change while we're running + + fr_time_delta_t detection_time; //!< used to set ev_timeout + int detection_timeouts; //!< too many timeouts means !auth_seq_known + + bool passive; //!< active or passive role from RFC 5880 - unused } proto_bfd_peer_t; diff --git a/src/listen/bfd/proto_bfd_udp.c b/src/listen/bfd/proto_bfd_udp.c index dc815fe1bed..95d66d59e44 100644 --- a/src/listen/bfd/proto_bfd_udp.c +++ b/src/listen/bfd/proto_bfd_udp.c @@ -30,7 +30,7 @@ #include #include -#include "proto_bfd.h" +#include "session.h" extern fr_app_io_t proto_bfd_udp; @@ -321,11 +321,13 @@ static char const *mod_name(fr_listen_t *li) static int mod_bootstrap(module_inst_ctx_t const *mctx) { - proto_bfd_udp_t *inst = talloc_get_type_abort(mctx->inst->data, proto_bfd_udp_t); + proto_bfd_udp_t *inst = talloc_get_type_abort(mctx->inst->data, proto_bfd_udp_t); CONF_SECTION *conf = mctx->inst->conf; size_t num; CONF_ITEM *ci; CONF_SECTION *server_cs; + fr_rb_iter_inorder_t iter; + proto_bfd_peer_t *peer; inst->cs = conf; @@ -400,6 +402,22 @@ static int mod_bootstrap(module_inst_ctx_t const *mctx) return -1; } + /* + * Walk over the list of peers, associating them with this listener. + */ + for (peer = fr_rb_iter_init_inorder(&iter, inst->peers); + peer != NULL; + peer = fr_rb_iter_next_inorder(&iter)) { + if (peer->client.ipaddr.af != inst->ipaddr.af) continue; + + if (peer->inst) continue; + + peer->inst = inst; + if (bfd_session_init(peer) < 0) { + return -1; + } + } + return 0; } diff --git a/src/listen/bfd/proto_bfd_udp.mk b/src/listen/bfd/proto_bfd_udp.mk index a2363202c81..c6de8e314c4 100644 --- a/src/listen/bfd/proto_bfd_udp.mk +++ b/src/listen/bfd/proto_bfd_udp.mk @@ -4,6 +4,6 @@ ifneq "$(TARGETNAME)" "" TARGET := $(TARGETNAME)$(L) endif -SOURCES := $(TARGETNAME).c +SOURCES := $(TARGETNAME).c session.c TGT_PREREQS := libfreeradius-bfd$(L) diff --git a/src/listen/bfd/session.c b/src/listen/bfd/session.c new file mode 100644 index 00000000000..27ed565b256 --- /dev/null +++ b/src/listen/bfd/session.c @@ -0,0 +1,1280 @@ +/* + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA + * + */ + +/** + * $Id$ + * @file src/listen/bfd/session.c + * @brief BFD Session handling + * + * @copyright 2023 Network RADIUS SAS (legal@networkradius.com) + */ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "session.h" + + +#define BFD_MAX_SECRET_LENGTH 20 + +#if 0 +typedef struct { + uint8_t auth_type; + uint8_t auth_len; + uint8_t key_id; +} __attribute__ ((packed)) bfd_auth_basic_t; + + +typedef struct { + uint8_t auth_type; + uint8_t auth_len; + uint8_t key_id; + uint8_t password[16]; +} __attribute__ ((packed)) bfd_auth_simple_t; + +typedef struct { + uint8_t auth_type; + uint8_t auth_len; + uint8_t key_id; + uint8_t reserved; + uint32_t sequence_no; + uint8_t digest[MD5_DIGEST_LENGTH]; +} __attribute__ ((packed)) bfd_auth_md5_t; + +typedef struct { + uint8_t auth_type; + uint8_t auth_len; + uint8_t key_id; + uint8_t reserved; + uint32_t sequence_no; + uint8_t digest[SHA1_DIGEST_LENGTH]; +} __attribute__ ((packed)) bfd_auth_sha1_t; + +typedef union bfd_auth_t { + bfd_auth_basic_t basic; + bfd_auth_simple_t password; + bfd_auth_md5_t md5; + bfd_auth_sha1_t sha1; +} __attribute__ ((packed)) bfd_auth_t; + + +static int bfd_verify_sequence(proto_bfd_peer_t *session, uint32_t sequence_no, + int keyed) +{ + uint32_t start, stop; + + start = session->recv_auth_seq; + if (keyed) { + start++; + } + stop = start + 3 * session->detect_multi; + + if (start < stop) { + if ((sequence_no < start) || + (sequence_no > stop)) { + return 0; + } + + } else { /* start is ~2^32, stop is ~10 */ + if ((sequence_no > start) && + (sequence_no < stop)) { + return 0; + } + } + + return 1; +} + +static void bfd_calc_md5(proto_bfd_peer_t *session, bfd_packet_t *bfd) +{ + bfd_auth_md5_t *md5 = &bfd->auth.md5; + + fr_assert(session->secret_len <= sizeof(md5->digest)); + fr_assert(md5->auth_len == sizeof(*md5)); + + memset(md5->digest, 0, sizeof(md5->digest)); + memcpy(md5->digest, session->secret, session->secret_len); + + fr_md5_calc(md5->digest,(const uint8_t *) bfd, bfd->length); +} + +static void bfd_auth_md5(proto_bfd_peer_t *session, bfd_packet_t *bfd) +{ + bfd_auth_md5_t *md5 = &bfd->auth.md5; + + md5->auth_type = session->auth_type; + md5->auth_len = sizeof(*md5); + bfd->length += md5->auth_len; + + md5->key_id = 0; + md5->sequence_no = session->xmit_auth_seq++; + + bfd_calc_md5(session, bfd); +} + +static int bfd_verify_md5(proto_bfd_peer_t *session, bfd_packet_t *bfd) +{ + int rcode; + bfd_auth_md5_t *md5 = &bfd->auth.md5; + uint8_t digest[sizeof(md5->digest)]; + + if (md5->auth_len != sizeof(*md5)) return 0; + + if (md5->key_id != 0) return 0; + + memcpy(digest, md5->digest, sizeof(digest)); + + bfd_calc_md5(session, bfd); + rcode = fr_digest_cmp(digest, md5->digest, sizeof(digest)); + + memcpy(md5->digest, digest, sizeof(md5->digest)); /* pedantic */ + + if (rcode != 0) { + DEBUG("BFD %s MD5 Digest failed: **** RE-ENTER THE SECRET ON BOTH ENDS ****", session->client.shortname); + return 0; + } + + /* + * Do this AFTER the authentication instead of before! + */ + if (!session->auth_seq_known) { + session->auth_seq_known = 1; + + } else if (!bfd_verify_sequence(session, md5->sequence_no, + (md5->auth_type == BFD_AUTH_MET_KEYED_MD5))) { + DEBUG("MD5 sequence out of window"); + return 0; + } + + session->recv_auth_seq = md5->sequence_no; + + return 1; +} + +static void bfd_calc_sha1(proto_bfd_peer_t *session, bfd_packet_t *bfd) +{ + fr_sha1_ctx ctx; + bfd_auth_sha1_t *sha1 = &bfd->auth.sha1; + + fr_assert(session->secret_len <= sizeof(sha1->digest)); + fr_assert(sha1->auth_len == sizeof(*sha1)); + + memset(sha1->digest, 0, sizeof(sha1->digest)); + memcpy(sha1->digest, session->secret, session->secret_len); + + fr_sha1_init(&ctx); + fr_sha1_update(&ctx, (const uint8_t *) bfd, bfd->length); + fr_sha1_final(sha1->digest, &ctx); +} + +static void bfd_auth_sha1(proto_bfd_peer_t *session, bfd_packet_t *bfd) +{ + bfd_auth_sha1_t *sha1 = &bfd->auth.sha1; + + sha1->auth_type = session->auth_type; + sha1->auth_len = sizeof(*sha1); + bfd->length += sha1->auth_len; + + sha1->key_id = 0; + sha1->sequence_no = session->xmit_auth_seq++; + + bfd_calc_sha1(session, bfd); +} + +static int bfd_verify_sha1(proto_bfd_peer_t *session, bfd_packet_t *bfd) +{ + int rcode; + bfd_auth_sha1_t *sha1 = &bfd->auth.sha1; + uint8_t digest[sizeof(sha1->digest)]; + + if (sha1->auth_len != sizeof(*sha1)) return 0; + + if (sha1->key_id != 0) return 0; + + memcpy(digest, sha1->digest, sizeof(digest)); + + bfd_calc_sha1(session, bfd); + rcode = fr_digest_cmp(digest, sha1->digest, sizeof(digest)); + + memcpy(sha1->digest, digest, sizeof(sha1->digest)); /* pedantic */ + + if (rcode != 0) { + DEBUG("BFD %d SHA1 Digest failed: **** RE-ENTER THE SECRET ON BOTH ENDS ****", session->number); + return 0; + } + + /* + * Do this AFTER the authentication instead of before! + */ + if (!session->auth_seq_known) { + session->auth_seq_known = 1; + + } else if (!bfd_verify_sequence(session, sha1->sequence_no, + (sha1->auth_type == BFD_AUTH_MET_KEYED_SHA1))) { + DEBUG("SHA1 sequence out of window"); + return 0; + } + + session->recv_auth_seq = sha1->sequence_no; + + return 1; +} + + +static int bfd_authenticate(proto_bfd_peer_t *session, bfd_packet_t *bfd) +{ + switch (bfd->auth.basic.auth_type) { + case BFD_AUTH_RESERVED: + return 0; + + case BFD_AUTH_SIMPLE: + break; + + case BFD_AUTH_KEYED_MD5: + case BFD_AUTH_MET_KEYED_MD5: + return bfd_verify_md5(session, bfd); + + case BFD_AUTH_KEYED_SHA1: + case BFD_AUTH_MET_KEYED_SHA1: + return bfd_verify_sha1(session, bfd); + } + + return 0; +} + + + +static void bfd_sign(proto_bfd_peer_t *session, bfd_packet_t *bfd) +{ + if (bfd->auth_present) { + switch (session->auth_type) { + case BFD_AUTH_RESERVED: + break; + + case BFD_AUTH_SIMPLE: + break; + + case BFD_AUTH_KEYED_MD5: + case BFD_AUTH_MET_KEYED_MD5: + bfd_auth_md5(session, bfd); + break; + + case BFD_AUTH_KEYED_SHA1: + case BFD_AUTH_MET_KEYED_SHA1: + bfd_auth_sha1(session, bfd); + break; + } + } +} + + +/* + * Send an immediate response to a poll request. + * + * Note that this doesn't affect our "last_sent" timer. + * That's set only when we intend to send a packet. + */ +static void bfd_poll_response(proto_bfd_peer_t *session) +{ + bfd_packet_t bfd; + + bfd_control_packet_init(session, &bfd); + bfd.poll = 0; /* Section 6.5 */ + bfd.final = 1; + + /* + * TO DO: rate limit poll responses. + */ + + bfd_sign(session, &bfd); + + if (sendto(session->socket.fd, &bfd, bfd.length, 0, + (struct sockaddr *) &session->remote_sockaddr, + session->salen) < 0) { + ERROR("Failed sending poll response: %s", fr_syserror(errno)); + } +} + + +static int bfd_process(proto_bfd_peer_t *session, bfd_packet_t *bfd) +{ + if (bfd->auth_present && + (session->auth_type == BFD_AUTH_RESERVED)) { + DEBUG("BFD %d packet asked to authenticate an unauthenticated session.", session->number); + return 0; + } + + if (!bfd->auth_present && + (session->auth_type != BFD_AUTH_RESERVED)) { + DEBUG("BFD %d packet failed to authenticate an authenticated session.", session->number); + return 0; + } + + if (bfd->auth_present && !bfd_authenticate(session, bfd)) { + return 0; + } + + DEBUG("BFD %d processing packet", session->number); + session->remote_disc = bfd->my_disc; + session->remote_session_state = bfd->state; + session->remote_demand_mode = bfd->demand; + + /* + * The other end is reducing the RX interval. Do that + * now. + */ + if ((bfd->required_min_rx_interval < session->remote_min_rx_interval) && + !session->demand_mode) { + bfd_stop_control(session); + bfd_start_control(session); + } + session->remote_min_rx_interval = bfd->required_min_rx_interval; + + session->remote_min_echo_rx_interval = bfd->min_echo_rx_interval; + if (bfd->min_echo_rx_interval == 0) { +#if 0 + /* + * Echo packets are BFD packets with + * application-layer data echoed back to the + * sender. We don't do that. + */ + bfd_stop_echo(session); +#endif + } + + if (session->doing_poll && bfd->final) { + bfd_stop_poll(session); + } + + /* + * Update transmit intervals as in 6.8.7 + */ + + /* + * Update detection times as in 6.8.4 + */ + if (!session->demand_mode) { + if (session->required_min_rx_interval >= bfd->desired_min_tx_interval) { + session->detection_time = session->required_min_rx_interval; + } else { + session->detection_time = bfd->desired_min_tx_interval; + } + } else { + if (session->desired_min_tx_interval >= session->remote_min_rx_interval) { + session->detection_time = session->desired_min_tx_interval; + } else { + session->detection_time = session->remote_min_rx_interval; + } + } + session->detection_time *= session->detect_multi; + + if (session->session_state == BFD_STATE_ADMIN_DOWN) { + DEBUG("Discarding BFD packet (admin down)"); + return 0; + } + + if (bfd->state == BFD_STATE_ADMIN_DOWN) { + if (bfd->state != BFD_STATE_DOWN) { + session->local_diag = BFD_NEIGHBOR_DOWN; + } + + DEBUG("BFD %d State %s -> DOWN (admin down)", + session->number, bfd_state[session->session_state]); + session->session_state = BFD_STATE_DOWN; + bfd_trigger(session); + + bfd_set_desired_min_tx_interval(session, USEC); + + } else { + switch (session->session_state) { + case BFD_STATE_DOWN: + switch (bfd->state) { + case BFD_STATE_DOWN: + DEBUG("BFD %d State DOWN -> INIT (neighbor down)", + session->number); + session->session_state = BFD_STATE_INIT; + bfd_trigger(session); + + bfd_set_desired_min_tx_interval(session, USEC); + break; + + case BFD_STATE_INIT: + DEBUG("BFD %d State DOWN -> UP (neighbor INIT)", + session->number); + session->session_state = BFD_STATE_UP; + bfd_trigger(session); + break; + + default: /* don't change anything */ + break; + } + break; + + case BFD_STATE_INIT: + switch (bfd->state) { + case BFD_STATE_INIT: + case BFD_STATE_UP: + DEBUG("BFD %d State INIT -> UP", + session->number); + session->session_state = BFD_STATE_UP; + bfd_trigger(session); + break; + + default: /* don't change anything */ + break; + } + break; + + case BFD_STATE_UP: + switch (bfd->state) { + case BFD_STATE_DOWN: + session->local_diag = BFD_NEIGHBOR_DOWN; + + DEBUG("BFD %d State UP -> DOWN (neighbor down)", session->number); + session->session_state = BFD_STATE_DOWN; + bfd_trigger(session); + + bfd_set_desired_min_tx_interval(session, USEC); + break; + + default: + break; + } + break; + + default: + DEBUG("Internal sanity check failed"); + return 0; + } + } + + /* + * Check if demand mode should be active (Section 6.6) + */ + if (session->remote_demand_mode && + (session->session_state == BFD_STATE_UP) && + (session->remote_session_state == BFD_STATE_UP)) { + DEBUG("BFD %d demand mode UP / UP, stopping packets", session->number); + bfd_stop_control(session); + } + + if (bfd->poll) { + bfd_poll_response(session); + } + + /* + * We've received the packet for the purpose of Section + * 6.8.4. + */ + session->last_recv = fr_time(); + + /* + * We've received a packet, but missed the previous one. + * Warn about it. + */ + if ((session->detect_multi >= 2) && (fr_time_gt(session->last_recv, session->next_recv))) { + fr_radius_packet_t packet; + request_t request; + + bfd_request(session, &request, &packet); + + trigger_exec(unlang_interpret_get_thread_default(), NULL, "server.bfd.warn", false, NULL); + } + + + if ((!session->remote_demand_mode) || + (session->session_state != BFD_STATE_UP) || + (session->remote_session_state != BFD_STATE_UP)) { + bfd_start_control(session); + } + + if (session->server_cs) { + request_t *request; + fr_radius_packet_t *packet, *reply; + + request = request_alloc_internal(session, NULL); + packet = fr_radius_packet_alloc(request, 0); + reply = fr_radius_packet_alloc(request, 0); + + bfd_request(session, request, packet); + + memset(reply, 0, sizeof(*reply)); + + request->reply = reply; + fr_socket_addr_swap(&request->reply->socket, &session->socket); + + /* + * FIXME: add my state, remote state as VPs? + */ + if (fr_debug_lvl) { + request->log.dst = talloc_zero(request, log_dst_t); + request->log.dst->func = vlog_request; + request->log.dst->uctx = &default_log; + + request->log.lvl = fr_debug_lvl; + } + request->component = NULL; + request->module = NULL; + + DEBUG2("server %s {", cf_section_name2(unlang_call_current(request))); + if (unlang_interpret_push_section(request, session->unlang, RLM_MODULE_NOTFOUND, UNLANG_TOP_FRAME) < 0) { + talloc_free(request); + return 0; + } + unlang_interpret_synchronous(unlang_interpret_event_list(request), request); + DEBUG("}"); + + /* + * FIXME: grab attributes from the reply + * and cache them for use in the next request. + */ + talloc_free(request); + } + + return 1; +} + + +/* + * Requirements of 6.8.3 + * + * Changes to: + * + * session->desired_min_tx_interval + * session->required_min_rx_interval + * + * mean we start polling. + */ + +/* + * Check if an incoming request is "ok" + * + * It takes packets, not requests. It sees if the packet looks + * OK. If so, it does a number of sanity checks on it. + */ +static int bfd_socket_recv(rad_listen_t *listener) +{ + ssize_t rcode; + bfd_socket_t *sock = listener->data; + proto_bfd_peer_t *session; + proto_bfd_peer_t my_session; + struct sockaddr_storage src; + socklen_t sizeof_src = sizeof(src); + bfd_packet_t bfd; + + rcode = recvfrom(listener->fd, &bfd, sizeof(bfd), 0, + (struct sockaddr *)&src, &sizeof_src); + if (rcode < 0) { + ERROR("Failed receiving packet: %s", fr_syserror(errno)); + return 0; + } + + if (rcode < 24) { + DEBUG("BFD packet is too short (%d < 24)", (int) rcode); + return 0; + } + + if (bfd.version != 1) { + DEBUG("BFD packet has wrong version (%d != 1)", bfd.version); + return 0; + } + + if (bfd.length < 24) { + DEBUG("BFD packet has wrong length (%d < 24)", bfd.length); + return 0; + } + + if (bfd.length > sizeof(bfd)) { + DEBUG("BFD packet has wrong length (%d > %zd)", bfd.length, sizeof(bfd)); + return 0; + } + + if (bfd.auth_present) { + if (bfd.length < 26) { + DEBUG("BFD packet has wrong length (%d < 26)", + bfd.length); + return 0; + } + + if (bfd.length < 24 + bfd.auth.basic.auth_len) { + DEBUG("BFD packet is too short (%d < %d)", + bfd.length, 24 + bfd.auth.basic.auth_len); + return 0; + + } + + if (bfd.length != 24 + bfd.auth.basic.auth_len) { + DEBUG("WARNING: What is the extra data?"); + } + + } + + if (bfd.detect_multi == 0) { + DEBUG("BFD packet has detect_multi == 0"); + return 0; + } + + if (bfd.multipoint != 0) { + DEBUG("BFD packet has multi != 0"); + return 0; + } + + if (bfd.my_disc == 0) { + DEBUG("BFD packet has my_disc == 0"); + return 0; + } + + if ((bfd.your_disc == 0) && + !((bfd.state == BFD_STATE_DOWN) || + (bfd.state == BFD_STATE_ADMIN_DOWN))) { + DEBUG("BFD packet has invalid your-disc / state"); + return 0; + } + + /* + * We SHOULD use "your_disc", but what the heck. + */ + fr_ipaddr_from_sockaddr(&my_session.socket.inet.dst_ipaddr, + &my_session.socket.inet.dst_port, &src, sizeof_src); + + session = fr_rb_find(sock->session_tree, &my_session); + if (!session) { + DEBUG("BFD unknown peer"); + return 0; + } + + if (!event_list) { + uint8_t *p = (uint8_t *) &bfd; + size_t total = bfd.length; + + /* + * A child has had a problem. Do some cleanups. + */ + if (session->blocked) bfd_pthread_free(session); + + /* + * No event list, try to create a new one. + */ + if (!session->el && !bfd_pthread_create(session)) { + DEBUG("BFD %d - error trying to create child thread", + session->number); + return 0; + } + + do { + rcode = write(session->pipefd[1], p, total); + if ((rcode < 0) && (errno == EINTR)) continue; + + if (rcode < 0) { + session->blocked = true; + return 0; + } + + total -= rcode; + p += rcode; + } while (total > 0); + return 0; + } + + return bfd_process(session, &bfd); +} + +static int bfd_parse_ip_port(CONF_SECTION *cs, fr_ipaddr_t *ipaddr, uint16_t *port) +{ + int rcode; + + /* + * Try IPv4 first + */ + memset(ipaddr, 0, sizeof(*ipaddr)); + ipaddr->addr.v4.s_addr = htonl(INADDR_NONE); + rcode = cf_pair_parse(NULL, cs, "ipaddr", FR_ITEM_POINTER(FR_TYPE_IPV4_ADDR, ipaddr), NULL, T_INVALID); + if (rcode < 0) return -1; + + if (rcode == 0) { /* successfully parsed IPv4 */ + ipaddr->af = AF_INET; + + } else { /* maybe IPv6? */ + rcode = cf_pair_parse(NULL, cs, "ipv6addr", FR_ITEM_POINTER(FR_TYPE_IPV6_ADDR, ipaddr), NULL, T_INVALID); + if (rcode < 0) return -1; + + if (rcode == 1) { + cf_log_err(cf_section_to_item(cs), + "No address specified in section"); + return -1; + } + ipaddr->af = AF_INET6; + } + + rcode = cf_pair_parse(NULL, cs, "port", FR_ITEM_POINTER(FR_TYPE_UINT16, port), "0", T_INVALID); + if (rcode < 0) return -1; + + return 0; +} + +/* + * @fixme: move some of this to parse + */ +static int bfd_init_sessions(CONF_SECTION *cs, bfd_socket_t *sock, int sockfd) +{ + CONF_ITEM *ci; + CONF_SECTION *peer; + uint16_t port; + fr_ipaddr_t ipaddr; + + for (ci=cf_item_next(cs, NULL); + ci != NULL; + ci=cf_item_next(cs, ci)) { + proto_bfd_peer_t *session, my_session; + + if (!cf_item_is_section(ci)) continue; + + peer = cf_item_to_section(ci); + + if (strcmp(cf_section_name1(peer), "peer") != 0) continue; + + if (bfd_parse_ip_port(peer, &ipaddr, &port) < 0) { + return -1; + } + + my_session.socket.inet.dst_ipaddr = ipaddr; + my_session.socket.inet.dst_port = port; + if (fr_rb_find(sock->session_tree, &my_session) != NULL) { + cf_log_err(ci, "Peers must have unique IP addresses"); + return -1; + } + + session = bfd_new_session(sock, sockfd, peer, &ipaddr, port); + if (!session) return -1; + } + + return 0; +} + + +/* + * None of these functions are used. + */ +static int bfd_socket_send(UNUSED rad_listen_t *listener, UNUSED request_t *request) +{ + fr_assert(0 == 1); + return 0; +} + + +static int bfd_socket_encode(UNUSED rad_listen_t *listener, UNUSED request_t *request) +{ + fr_assert(0 == 1); + return 0; +} + + +static int bfd_socket_decode(UNUSED rad_listen_t *listener, UNUSED request_t *request) +{ + fr_assert(0 == 1); + return 0; +} + +static fr_table_num_sorted_t const auth_types[] = { + { L("keyed-md5"), BFD_AUTH_KEYED_MD5 }, + { L("keyed-sha1"), BFD_AUTH_KEYED_SHA1 }, + { L("met-keyed-md5"), BFD_AUTH_MET_KEYED_MD5 }, + { L("met-keyed-sha1"), BFD_AUTH_MET_KEYED_SHA1 }, + { L("none"), BFD_AUTH_RESERVED }, + { L("simple"), BFD_AUTH_SIMPLE } +}; +static size_t auth_types_len = NUM_ELEMENTS(auth_types); + +static int bfd_socket_parse(CONF_SECTION *cs, rad_listen_t *this) +{ + bfd_socket_t *sock = this->data; + char const *auth_type_str = NULL; + uint16_t listen_port; + fr_ipaddr_t ipaddr; + + fr_assert(sock != NULL); + + if (bfd_parse_ip_port(cs, &ipaddr, &listen_port) < 0) { + return -1; + } + + sock->my_ipaddr = ipaddr; + sock->my_port = listen_port; + + if (cf_pair_parse(sock, cs, "interface", FR_ITEM_POINTER(FR_TYPE_STRING, &sock->interface), NULL, T_INVALID) < 0) return -1; + + if (cf_pair_parse(sock, cs, "min_receive_interval", FR_ITEM_POINTER(FR_TYPE_UINT32, + &sock->min_rx_interval), "1000", T_BARE_WORD) < 0) return -1; + if (cf_pair_parse(sock, cs, "max_timeouts", FR_ITEM_POINTER(FR_TYPE_UINT32, + &sock->max_timeouts), "3", T_BARE_WORD) < 0) return -1; + if (cf_pair_parse(sock, cs, "demand", FR_ITEM_POINTER(FR_TYPE_BOOL, &sock->demand), + "no", T_DOUBLE_QUOTED_STRING) < 0) return -1; + if (cf_pair_parse(NULL, cs, "auth_type", FR_ITEM_POINTER(FR_TYPE_STRING, &auth_type_str), + NULL, T_INVALID) < 0) return -1; + + if (!this->server) { + char const *server; + if (cf_pair_parse(sock, cs, "server", FR_ITEM_POINTER(FR_TYPE_STRING, &server), + NULL, T_INVALID) < 0) return -1; + sock->server_cs = virtual_server_find(server); + } else { + sock->server_cs = this->server_cs; + } + + if (sock->min_tx_interval < 100) sock->min_tx_interval = 100; + if (sock->min_tx_interval > 10000) sock->min_tx_interval = 10000; + + if (sock->min_rx_interval < 100) sock->min_rx_interval = 100; + if (sock->min_rx_interval > 10000) sock->min_rx_interval = 10000; + + if (sock->max_timeouts == 0) sock->max_timeouts = 1; + if (sock->max_timeouts > 10) sock->max_timeouts = 10; + + sock->auth_type = fr_table_value_by_str(auth_types, auth_type_str, BFD_AUTH_INVALID); + if (sock->auth_type == BFD_AUTH_INVALID) { + ERROR("Unknown auth_type '%s'", auth_type_str); + return -1; + } + + if (sock->auth_type == BFD_AUTH_SIMPLE) { + ERROR("'simple' authentication is insecure and is not supported"); + return -1; + } + + if (sock->auth_type != BFD_AUTH_RESERVED) { + sock->secret_len = bfd_parse_secret(cs, sock->secret); + + if (sock->secret_len == 0) { + ERROR("Cannot have empty secret"); + return -1; + } + + if (((sock->auth_type == BFD_AUTH_KEYED_MD5) || + (sock->auth_type == BFD_AUTH_MET_KEYED_MD5)) && + (sock->secret_len > 16)) { + ERROR("Secret must be no more than 16 bytes when using MD5"); + return -1; + } + } + + sock->session_tree = fr_rb_inline_talloc_alloc(sock, proto_bfd_peer_t, node, bfd_session_cmp, bfd_session_free); + if (!sock->session_tree) { + ERROR("Failed creating session tree!"); + return -1; + } + + /* + * Find the sibling "bfd" section of the "listen" section. + */ + sock->unlang = cf_section_find(cf_item_to_section(cf_parent(cs)), "bfd", NULL); + + return 0; +} + + /* + * Bootstrap the initial set of connections. + */ + if (bfd_init_sessions(cs, sock, this->fd) < 0) { + return -1; + } + + return 0; +} +#endif + +//static int bfd_start_packets(proto_bfd_peer_t *session); +static int bfd_start_control(proto_bfd_peer_t *session); +static int bfd_stop_control(proto_bfd_peer_t *session); +//static int bfd_process(proto_bfd_peer_t *session, bfd_packet_t *bfd); +static void bfd_set_timeout(proto_bfd_peer_t *session, fr_time_t when); + +static int bfd_start_packets(proto_bfd_peer_t *session); + +static const char *bfd_state[] = { + "admin-down", + "down", + "init", + "up" +}; + + +static void bfd_control_packet_init(proto_bfd_peer_t *session, + bfd_packet_t *bfd) +{ + memset(bfd, 0, sizeof(*bfd)); + + bfd->version = 1; + bfd->diag = session->local_diag; + bfd->state = session->session_state; + bfd->poll = 0; /* fixed by poll response */ + bfd->final = 0; /* fixed by poll response */ + bfd->control_plane_independent = 0; + + bfd->auth_present = (session->auth_type != BFD_AUTH_RESERVED); + + /* + * If we're UP / UP, signal that we've entered demand + * mode, and stop sending packets. + */ + if (session->demand_mode && + (session->session_state == BFD_STATE_UP) && + (session->remote_session_state == BFD_STATE_UP)) { + bfd->demand = true; + + DEBUG("BFD %s demand mode UP / UP, sending ACK and done.", + session->client.shortname); + bfd_stop_control(session); + } else { + bfd->demand = false; + } + + bfd->multipoint = 0; + bfd->detect_multi = session->detect_multi; + bfd->length = 24; /* auth types add to this later */ + + bfd->my_disc = session->local_disc; + bfd->your_disc = session->remote_disc; + + bfd->desired_min_tx_interval = fr_time_delta_to_usec(session->desired_min_tx_interval); + bfd->required_min_rx_interval = fr_time_delta_to_usec(session->required_min_rx_interval); + + bfd->min_echo_rx_interval = fr_time_delta_to_usec(session->my_min_echo_rx_interval); +} + + +/* + * Send one BFD packet. + */ +static void bfd_send_packet(UNUSED fr_event_list_t *el, UNUSED fr_time_t now, void *ctx) +{ + proto_bfd_peer_t *session = ctx; + bfd_packet_t bfd; + + bfd_control_packet_init(session, &bfd); + + bfd.poll = session->doing_poll; + + if (!bfd.demand) { + bfd_start_packets(session); + } + +// bfd_sign(session, &bfd); + + DEBUG("BFD %s sending packet state %s", + session->client.shortname, bfd_state[session->session_state]); + +#if 0 + if (sendto(session->socket.fd, &bfd, bfd.length, 0, + (struct sockaddr *) &session->remote_sockaddr, + session->salen) < 0) { + ERROR("Failed sending packet: %s", fr_syserror(errno)); + } +#endif +} + +/* + * Start sending packets. + */ +static int bfd_start_packets(proto_bfd_peer_t *session) +{ + uint64_t interval, base; + uint64_t jitter; + + /* + * Reset the timers. + */ + fr_event_timer_delete(&session->ev_packet); + + session->last_sent = fr_time(); + + if (fr_time_delta_cmp(session->desired_min_tx_interval, session->remote_min_rx_interval) >= 0) { + interval = fr_time_delta_unwrap(session->desired_min_tx_interval); + } else { + interval = fr_time_delta_unwrap(session->remote_min_rx_interval); + } + base = (interval * 3) / 4; + jitter = fr_rand(); /* 32-bit number */ + + if (session->detect_multi == 1) { + jitter *= 644245094; /* 15% of 2^32 */ + + } else { + jitter *= (1 << 30); /* 25% of 2^32 */ + } + + jitter >>= 32; + jitter *= interval; + jitter >>= 32; + interval = base; + interval += jitter; + + if (fr_event_timer_in(session, session->el, &session->ev_packet, + fr_time_delta_wrap(interval), + bfd_send_packet, session) < 0) { + fr_assert("Failed to insert event" == NULL); + } + + return 0; +} + + +/* + * Start polling for the peer. + */ +static int bfd_start_poll(proto_bfd_peer_t *session) +{ + if (session->doing_poll) return 0; + + /* + * Already sending packets. Reset the timers and set the + * poll bit. + */ + if (!session->remote_demand_mode) { + bfd_stop_control(session); + } + + session->doing_poll = true; + + /* + * Send POLL packets, even if we're not sending CONTROL + * packets. + */ + return bfd_start_packets(session); +} + +/* + * Stop polling for packets. + */ +static int bfd_stop_poll(proto_bfd_peer_t *session) +{ + if (!session->doing_poll) return 0; + + /* + * We tried to increase the min_tx during a polling + * sequence. That isn't kosher, so we instead waited + * until now. + */ + if (fr_time_delta_unwrap(session->next_min_tx_interval) > 0) { + session->desired_min_tx_interval = session->next_min_tx_interval; + session->next_min_tx_interval = fr_time_delta_wrap(0); + } + + /* + * Already sending packets. Clear the poll bit and + * re-set the timers. + */ + if (!session->remote_demand_mode) { + fr_assert(session->ev_timeout != NULL); + fr_assert(session->ev_packet != NULL); + session->doing_poll = false; + + bfd_stop_control(session); + bfd_start_control(session); + return 1; + } + + session->doing_poll = false; + + return bfd_stop_control(session); +} + +/* + * Timer functions + */ +static void bfd_set_desired_min_tx_interval(proto_bfd_peer_t *session, fr_time_delta_t value) +{ + /* + * Increasing the value: don't change it if we're already + * polling. + */ + if (session->doing_poll && + (session->session_state == BFD_STATE_UP) && + (fr_time_delta_cmp(value, session->desired_min_tx_interval) > 0)) { + session->next_min_tx_interval = value; + return; + } + + /* + * Don't poll more than once per second. + */ + if (session->session_state != BFD_STATE_UP) { + if (fr_time_delta_cmp(value, fr_time_delta_from_sec(1)) < 0) value = fr_time_delta_from_sec(1); + } + + session->desired_min_tx_interval = value; + bfd_stop_control(session); + session->doing_poll = 0; + + bfd_start_poll(session); +} + + +/* + * We failed to see a packet. + */ +static void bfd_detection_timeout(UNUSED fr_event_list_t *el, fr_time_t now, void *ctx) +{ + proto_bfd_peer_t *session = ctx; + + DEBUG("BFD %s Timeout state %s ****** ", session->client.shortname, + bfd_state[session->session_state]); + + if (!session->demand_mode) { + switch (session->session_state) { + case BFD_STATE_INIT: + case BFD_STATE_UP: + goto start_poll; + + default: + break; + } + + } else if (!session->doing_poll) { + start_poll: + DEBUG("BFD %s State -> DOWN (control expired)", session->client.shortname); + session->session_state = BFD_STATE_DOWN; + session->local_diag = BFD_CTRL_EXPIRED; +// bfd_trigger(session); + + bfd_set_desired_min_tx_interval(session, fr_time_delta_from_sec(1)); + } + + session->remote_disc = 0; + + if (session->detection_timeouts >= 2) { + session->auth_seq_known = 0; + } + + session->detection_timeouts++; + + bfd_set_timeout(session, now); +} + +/* + * Set the timeout for when we've lost enough packets to be + * worried. + */ +static void bfd_set_timeout(proto_bfd_peer_t *session, fr_time_t when) +{ + fr_time_t timeout; + uint64_t delay; + + fr_event_timer_delete(&session->ev_timeout); + + timeout = fr_time_add(when, session->detection_time); + + /* + * When we SHOULD have received the next packet. + */ + if (session->detect_multi >= 2) { + delay = fr_time_delta_unwrap(session->detection_time) / session->detect_multi; + } else { + delay = fr_time_delta_unwrap(session->detection_time); + } + delay += delay / 2; + + session->next_recv = fr_time_add(when, fr_time_delta_from_usec(delay)); + + if (fr_event_timer_at(session, session->el, &session->ev_timeout, + timeout, bfd_detection_timeout, session) < 0) { + fr_assert("Failed to insert event" == NULL); + } +} + + +/* + * Start / stop control packets. + */ +static int bfd_stop_control(proto_bfd_peer_t *session) +{ + fr_event_timer_delete(&session->ev_timeout); + fr_event_timer_delete(&session->ev_packet); + return 1; +} + +static int bfd_start_control(proto_bfd_peer_t *session) +{ + /* + * @todo - change our discriminator? + */ + + /* + * We don't expect to see remote packets, so don't do anything. + */ + if (fr_time_delta_unwrap(session->remote_min_rx_interval) == 0) return 0; + + if ((session->remote_disc == 0) && session->passive) return 0; + + /* + * We were asked to go "up" when we were alread "up" + */ + if (session->remote_demand_mode && + (session->session_state == BFD_STATE_UP) && + (session->remote_session_state == BFD_STATE_UP) && + !session->doing_poll) { + DEBUG("BFD %s warning: asked to start UP / UP ?", + session->client.shortname); + fr_assert(0 == 1); + bfd_stop_control(session); + return 0; + } + + bfd_set_timeout(session, session->last_recv); + + if (session->ev_packet) return 0; + + /* + * Start sending packets. + */ + return bfd_start_packets(session); +} + + +int bfd_session_init(proto_bfd_peer_t *session) +{ + session->session_state = BFD_STATE_DOWN; + session->local_disc = fr_rand(); + session->remote_disc = 0; + session->local_diag = BFD_DIAG_NONE; + session->remote_min_rx_interval = fr_time_delta_wrap(0); + session->remote_demand_mode = false; + session->recv_auth_seq = 0; + session->xmit_auth_seq = fr_rand(); + session->auth_seq_known = 0; + + /* + * Initialize the detection time. + */ + if (!session->demand_mode) { + session->detection_time = session->required_min_rx_interval; + } else { + session->detection_time = session->desired_min_tx_interval; + } + session->detection_time = fr_time_delta_wrap(session->detect_multi * fr_time_delta_unwrap(session->detection_time)); + + bfd_stop_poll(session); /* compilation hack for now */ + + return 0; +} diff --git a/src/listen/bfd/session.h b/src/listen/bfd/session.h new file mode 100644 index 00000000000..e783254c3ef --- /dev/null +++ b/src/listen/bfd/session.h @@ -0,0 +1,28 @@ +#pragma once +/* + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA + */ + +/* + * $Id$ + * + * @file src/listen/bfd/session.h + * @brief BFD Session handling + * + * @copyright 2023 Network RADIUS SAS (legal@networkradius.com) + */ +#include "proto_bfd.h" + +int bfd_session_init(proto_bfd_peer_t *session);