Commit 07458c3dbb for frr
commit 07458c3dbbc797e769b48caac56fcf9571ef6092
Author: Abdul Wasey <w453y.me@gmail.com>
Date: Tue Sep 8 16:30:54 2026 +0000
bfdd: carry authentication keys to the data plane with their lifetimes
The data plane protocol had nowhere to put an authentication key, so a
session that authenticates could be offloaded and would then be
transmitted in the clear while `show bfd peer` reported it as
authenticated.
Sending the key that is active right now is not enough. A key chain rolls
over on a clock: `bfd_keychain_key_find_active()` is consulted from
ptm_bfd_snd() and bfd_check_auth() on every packet, so the software path
follows a rollover without being told. Nothing re-reads a lifetime for an
offloaded session, and there is no expiry hook in the key chain, so a
data plane handed the active key would keep signing with it long after it
expired. Pushing a new key at every boundary would put the daemon back in
a path that offloading exists to keep it out of.
So send the keys with the periods in which each may be used, and let the
data plane decide. It has the packets, so it is the only side that can
tell which key applies to one. The two periods overlap on purpose during
a rollover, a key ceasing to be used for transmit before it ceases to be
accepted, and a data plane holding a single key cannot honour that.
DP_SESSION_AUTH carries the keys and is variable length, since a key chain
may hold several. The array is declared at its full size inside the
message so a receiver may copy a message in and read it in place, which is
how every other message here is handled; only the keys that are present go
on the wire.
SESSION_AUTH says whether a session authenticates at all, so a data plane
that cannot use keys can refuse it rather than run it unprotected, and so
that removing a key chain is signalled by the flag clearing rather than by
an empty message. There is no capability exchange in this protocol, so
nothing can be enforced from this end.
A key the chain was never given a period for is stored zeroed, and the
chain reads a zero start as always valid whatever the end says. The
protocol spells that as a zero start and an end of -1, which is what goes
on the wire, so a data plane need not know the chain's convention.
Signed-off-by: Abdul Wasey <w453y.me@gmail.com>
diff --git a/bfdd/bfddp_packet.h b/bfdd/bfddp_packet.h
index f833e2118c..54e9658058 100644
--- a/bfdd/bfddp_packet.h
+++ b/bfdd/bfddp_packet.h
@@ -54,6 +54,27 @@
/** BFD data plane protocol version. */
#define BFD_DP_VERSION 1
+/**
+ * Longest authentication key `DP_SESSION_AUTH` will carry.
+ *
+ * One HMAC block, so any key a digest defined by RFC 5880 Section 6.7 can
+ * use without first being hashed down fits. The RFC bounds the keys
+ * themselves far lower: 16 bytes for a simple password and 20 for keyed
+ * SHA1.
+ */
+#define BFDDP_AUTH_KEY_MAX 64
+
+/**
+ * Most keys a single `DP_SESSION_AUTH` message will carry.
+ *
+ * A bound on the message rather than on what a key chain may hold. A
+ * rollover needs the outgoing key and whatever is still valid for accept,
+ * which is a handful, so this is already generous. It is kept small
+ * enough that `struct bfddp_message` stays cheap to put on the stack,
+ * since every message type shares that size.
+ */
+#define BFDDP_AUTH_KEY_COUNT_MAX 16
+
/** BFD data plane message types. */
enum bfddp_message_type {
/** Ask for BFD daemon or data plane for echo packet. */
@@ -71,6 +92,9 @@ enum bfddp_message_type {
DP_REQUEST_SESSION_COUNTERS = 5,
/** Tell BFD daemon about counters values. */
BFD_SESSION_COUNTERS = 6,
+
+ /** Send a session's authentication keys. */
+ DP_SESSION_AUTH = 7,
};
/**
@@ -109,6 +133,22 @@ enum bfddp_session_flag {
SESSION_PASSIVE = (1 << 5),
/** Set when session is administrative down. */
SESSION_SHUTDOWN = (1 << 6),
+ /**
+ * Set when the session is configured to authenticate.
+ *
+ * The keys arrive separately. \see DP_SESSION_AUTH. A data plane
+ * that cannot authenticate must refuse a session carrying this flag
+ * rather than run it in the clear, because the peer will be
+ * authenticating and `show bfd peer` reports the session as
+ * authenticated either way.
+ *
+ * There is no capability exchange in this protocol, so the daemon
+ * cannot tell whether the data plane honours any of this. A data
+ * plane written against an earlier version ignores both the flag and
+ * the keys, and such a session runs unauthenticated, which is what
+ * happens today for want of anywhere to carry a key at all.
+ */
+ SESSION_AUTH = (1 << 7),
};
/**
@@ -174,7 +214,11 @@ struct bfddp_session {
/** Interface name (empty when unavailable). */
char ifname[64];
- /* TODO: missing authentication. */
+ /*
+ * Authentication keys are not carried here. They change on their own
+ * schedule rather than with the session, and there may be several.
+ * \see DP_SESSION_AUTH.
+ */
};
/** BFD packet state values as defined in RFC 5880, Section 4.1. */
@@ -297,6 +341,89 @@ struct bfddp_control_packet {
uint32_t required_echo_rx;
};
+/**
+ * Period during which a key may be used.
+ *
+ * Seconds since the Unix epoch. A `start` of zero means the key has always
+ * been valid, and an `end` of `-1` means it never expires. Both sentinels
+ * come from the BFD daemon's key chain and a data plane must honour them,
+ * since a key configured without lifetimes carries them.
+ */
+struct bfddp_key_lifetime {
+ /** First second the key may be used, or zero for no start. */
+ int64_t start;
+ /** Last second the key may be used, or `-1` for no expiry. */
+ int64_t end;
+};
+
+/**
+ * One authentication key belonging to a session.
+ *
+ * The data plane decides which key to use and when, so the lifetimes
+ * travel with the key. Transmit with the key whose `send` period contains
+ * the current time, and verify a received packet with the key whose
+ * `key_id` matches the Auth Key ID field, provided its `accept` period
+ * contains the current time.
+ *
+ * The two periods overlap on purpose during a rollover: a key stops being
+ * used to transmit before it stops being accepted, so packets already in
+ * flight still verify.
+ */
+struct bfddp_auth_key {
+ /** Authentication type, as in RFC 5880 Section 4.1. */
+ uint8_t type;
+ /** Authentication Key ID, as in RFC 5880 Section 4.2. */
+ uint8_t key_id;
+ /** Length of `key`, in bytes. */
+ uint8_t key_len;
+ /**
+ * Reserved / zeroed.
+ *
+ * Sized so the lifetimes below start on an eight byte boundary
+ * without the compiler inserting padding of its own.
+ */
+ uint8_t zero[5];
+
+ /** When this key may be used to transmit. */
+ struct bfddp_key_lifetime send;
+ /** When this key may be used to verify a received packet. */
+ struct bfddp_key_lifetime accept;
+
+ /** Key material, zero padded. */
+ char key[BFDDP_AUTH_KEY_MAX];
+};
+
+/**
+ * `DP_SESSION_AUTH` data payload.
+ *
+ * `key_count` `struct bfddp_auth_key` follow this header, so the message
+ * is variable length and `bfddp_message_header.length` is what bounds it.
+ *
+ * Sent whenever the session's keys change, which is a configuration event
+ * rather than a rollover: a rollover is the data plane noticing that a
+ * lifetime has passed. A message carrying no keys means the session has
+ * none left and must not be authenticated.
+ */
+struct bfddp_session_auth {
+ /** Session local discriminator. */
+ uint32_t lid;
+ /** Number of entries of `keys` that are present. */
+ uint16_t key_count;
+ /** Reserved / zeroed. */
+ uint16_t zero;
+ /**
+ * The keys themselves.
+ *
+ * Only the first `key_count` entries are sent, so the message on the
+ * wire is shorter than this structure and
+ * `bfddp_message_header.length` is what says how much of it is
+ * there. The array is declared at its full size so that a receiver
+ * may copy a message into `struct bfddp_message` and read it in
+ * place, which is how the rest of this protocol is handled.
+ */
+ struct bfddp_auth_key keys[BFDDP_AUTH_KEY_COUNT_MAX];
+};
+
/**
* The protocol wire message header structure.
*/
@@ -371,6 +498,7 @@ struct bfddp_message {
struct bfddp_control_packet control;
struct bfddp_request_counters counters_req;
struct bfddp_session_counters session_counters;
+ struct bfddp_session_auth session_auth;
} data;
};
diff --git a/bfdd/dplane.c b/bfdd/dplane.c
index 890c97a579..3663c787cf 100644
--- a/bfdd/dplane.c
+++ b/bfdd/dplane.c
@@ -23,6 +23,7 @@
#include <time.h>
#include "lib/hook.h"
+#include "lib/keychain.h"
#include "lib/network.h"
#include "lib/printfrr.h"
#include "lib/stream.h"
@@ -122,6 +123,8 @@ static const char *bfd_dplane_messagetype2str(enum bfddp_message_type bmt)
return "DP_REQUEST_SESSION_COUNTERS";
case BFD_SESSION_COUNTERS:
return "BFD_SESSION_COUNTERS";
+ case DP_SESSION_AUTH:
+ return "DP_SESSION_AUTH";
default:
return "UNKNOWN";
}
@@ -192,6 +195,15 @@ static void bfd_dplane_debug_message(const struct bfddp_message *msg)
msg->data.session.ifname);
break;
+ case DP_SESSION_AUTH:
+ /*
+ * Key material is deliberately absent: this is written to
+ * the log at debug level.
+ */
+ zlog_debug(" [lid=%u keys=%u]", ntohl(msg->data.session_auth.lid),
+ ntohs(msg->data.session_auth.key_count));
+ break;
+
case BFD_STATE_CHANGE:
buf[0] = 0;
flags = ntohl(msg->data.state.remote_flags);
@@ -548,6 +560,7 @@ static void bfd_dplane_handle_message(struct bfddp_message *msg, void *arg)
case DP_ADD_SESSION:
case DP_DELETE_SESSION:
case DP_REQUEST_SESSION_COUNTERS:
+ case DP_SESSION_AUTH:
/* NOTHING: we are not supposed to receive this. */
break;
case BFD_SESSION_COUNTERS:
@@ -902,6 +915,8 @@ static void _bfd_dplane_session_fill(const struct bfd_session *bs,
msg->data.session.flags |= SESSION_DEMAND;
if (bs->flags & BFD_SESS_FLAG_PASSIVE)
msg->data.session.flags |= SESSION_PASSIVE;
+ if (bs->kc)
+ msg->data.session.flags |= SESSION_AUTH;
if (bs->flags & BFD_SESS_FLAG_SHUTDOWN)
msg->data.session.flags |= SESSION_SHUTDOWN;
@@ -1267,9 +1282,102 @@ int bfd_dplane_add_session(struct bfd_session *bs)
return -1;
}
+/*
+ * The end of a period as the protocol spells it. The key chain stores a key
+ * never given a period zeroed and reads a zero start as always valid,
+ * whatever the end says; bfddp_packet.h promises a data plane a zero start
+ * with an end of -1 instead, so it need not know the key chain's convention.
+ */
+static int64_t bfd_dplane_key_end(const struct key_range *range)
+{
+ return range->start == 0 ? -1 : (int64_t)range->end;
+}
+
+/*
+ * Send every key the session's key chain holds, with the lifetimes that
+ * say when each may be used.
+ *
+ * The data plane picks the key, not us. It has the packets, so it is the
+ * only side that can tell which key applies to one, and pushing a new key
+ * at every rollover would put the BFD daemon back in a path that
+ * offloading exists to keep it out of.
+ *
+ * Only sent for a session that has a key chain. `SESSION_AUTH` in the
+ * session message is what says whether the session authenticates at all,
+ * so a data plane drops the keys it holds when that flag goes away.
+ */
+static int bfd_dplane_send_session_auth(const struct bfd_session *bs)
+{
+ struct bfddp_message msg = {};
+ struct bfddp_auth_key *keys = msg.data.session_auth.keys;
+ struct listnode *node;
+ struct key *key;
+ uint16_t count = 0;
+ uint16_t msglen;
+
+ for (ALL_LIST_ELEMENTS_RO(bs->kc->key, node, key)) {
+ enum bfd_auth_type type;
+ size_t keylen;
+
+ if (key->string == NULL)
+ continue;
+
+ /* A key whose algorithm has no BFD equivalent is unusable. */
+ type = map_keychain_algo_to_bfd_auth_type(key->hash_algo, bs->auth_meticulous);
+ if (type == BFD_AUTH_TYPE_RESERVED)
+ continue;
+
+ keylen = strlen(key->string);
+ if (keylen == 0 || keylen > BFDDP_AUTH_KEY_MAX)
+ continue;
+
+ /*
+ * RFC 5880 gives the Auth Key ID eight bits, so a key chain
+ * index above that cannot be put on the wire. Skipping it
+ * keeps the data plane's view of the key chain honest;
+ * truncating would give two keys the same identifier.
+ */
+ if (key->index > UINT8_MAX)
+ continue;
+
+ if (count == BFDDP_AUTH_KEY_COUNT_MAX) {
+ zlog_warn("%s: key chain %s has more than %u usable keys, the rest are not offloaded",
+ __func__, bs->kc->name, BFDDP_AUTH_KEY_COUNT_MAX);
+ break;
+ }
+
+ keys[count].type = type;
+ keys[count].key_id = (uint8_t)key->index;
+ keys[count].key_len = (uint8_t)keylen;
+ keys[count].send.start = htobe64((uint64_t)key->send.start);
+ keys[count].send.end = htobe64((uint64_t)bfd_dplane_key_end(&key->send));
+ keys[count].accept.start = htobe64((uint64_t)key->accept.start);
+ keys[count].accept.end = htobe64((uint64_t)bfd_dplane_key_end(&key->accept));
+ memcpy(keys[count].key, key->string, keylen);
+ count++;
+ }
+
+ /*
+ * Only the keys that are present go on the wire, so the message is
+ * shorter than the structure it was built in.
+ */
+ msglen = sizeof(msg.header) + offsetof(struct bfddp_session_auth, keys) +
+ (uint16_t)(count * sizeof(*keys));
+
+ msg.header.version = BFD_DP_VERSION;
+ msg.header.length = htons(msglen);
+ msg.header.type = htons(DP_SESSION_AUTH);
+
+ msg.data.session_auth.lid = htonl(bs->discrs.my_discr);
+ msg.data.session_auth.key_count = htons(count);
+
+ return bfd_dplane_enqueue(bs->bdc, &msg, msglen);
+}
+
int bfd_dplane_update_session(const struct bfd_session *bs)
{
struct bfddp_message msg = {};
+ int rv;
if (bs->bdc == NULL)
return 0;
@@ -1280,7 +1388,15 @@ int bfd_dplane_update_session(const struct bfd_session *bs)
ntohl(msg.data.session.flags), msg.data.session.detect_mult, msg.data.session.ttl);
/* Enqueue message to data plane client. */
- return bfd_dplane_enqueue(bs->bdc, &msg, ntohs(msg.header.length));
+ rv = bfd_dplane_enqueue(bs->bdc, &msg, ntohs(msg.header.length));
+ if (rv != 0)
+ return rv;
+
+ /* The keys follow the session they belong to. */
+ if (bs->kc)
+ rv = bfd_dplane_send_session_auth(bs);
+
+ return rv;
}
int bfd_dplane_delete_session(struct bfd_session *bs)