diff --git a/bfdd/bfd.c b/bfdd/bfd.c index 99c96869cb58..19dff55f2509 100644 --- a/bfdd/bfd.c +++ b/bfdd/bfd.c @@ -1175,6 +1175,27 @@ static void _bfd_session_update(struct bfd_session *bs, */ if (bpc->bpc_has_profile) bfd_profile_apply(bpc->bpc_profile, bs); + + /* + * Propagate any SBFD/SRv6 tail fields the caller flagged on this + * re-register. Each field is gated by its `BFD_REGEXT_FLAG_*` bit + * so absent fields don't clobber the session's current state. The + * `bfd_mode` and `bfd_name` (key) of an existing session are + * deliberately NOT mutated here: changing either mid-flight would + * require socket re-open / key re-hash, which `bs_registrate` + * already handles on the initial-create path; callers wanting that + * must deregister + re-register instead. + */ + if (bpc->bfd_regext_flags & BFD_REGEXT_FLAG_SRV6_SOURCE) + memcpy(&bs->out_sip6, &bpc->srv6_source_ipv6, sizeof(bs->out_sip6)); + if (bpc->bfd_regext_flags & BFD_REGEXT_FLAG_SEG_LIST) { + bs->segnum = bpc->seg_num; + if (bpc->seg_num > 0) + memcpy(bs->seg_list, bpc->seg_list, + sizeof(struct in6_addr) * bpc->seg_num); + } + if (bpc->bfd_regext_flags & BFD_REGEXT_FLAG_REMOTE_DISCR) + bs->discrs.remote_discr = bpc->remote_discr; } int bfd_session_update(struct bfd_session *bs, struct bfd_peer_cfg *bpc) @@ -1230,8 +1251,12 @@ struct bfd_session *ptm_bfd_sess_new(struct bfd_peer_cfg *bpc) return NULL; } - /* Get BFD session storage with its defaults. */ - bfd = bfd_session_new(BFD_MODE_TYPE_BFD); + /* + * honour the BFD session mode supplied via the ZAPI + * register payload. Defaulting to classical BFD when the caller + * left the field at zero preserves pre-existing behaviour. + */ + bfd = bfd_session_new(bpc->bfd_mode ? bpc->bfd_mode : BFD_MODE_TYPE_BFD); /* * Store interface/VRF name in case we need to delay session @@ -1284,6 +1309,34 @@ struct bfd_session *ptm_bfd_sess_new(struct bfd_peer_cfg *bpc) bfd->key.mhop = bpc->bpc_mhop; + /* + * propagate the rest of the SBFD/SRv6 tail onto the new + * session *before* `bs_registrate` runs. `bs_registrate` invokes + * `bfd_session_enable`, which depends on `bs->bfd_mode`, + * `bs->segnum`, `bs->out_sip6`, `bs->seg_list[]` and on the key + * hash containing `bfd_name` — so a post-`bs_registrate` copy + * would race past the SBFD socket-open and hash-insertion paths. + * + * Gating is on the wire-format flag bits in `bpc->bfd_regext_flags`, + * so a caller explicitly setting `remote_discr == 0` (legitimate for + * `sbfd_init` before the responder is learned) is honoured rather + * than skipped. + */ + if (bpc->bfd_regext_flags & BFD_REGEXT_FLAG_BFD_NAME) { + strlcpy(bfd->bfd_name, bpc->bfd_name, sizeof(bfd->bfd_name)); + strlcpy(bfd->key.bfdname, bpc->bfd_name, sizeof(bfd->key.bfdname)); + } + if (bpc->bfd_regext_flags & BFD_REGEXT_FLAG_SRV6_SOURCE) + memcpy(&bfd->out_sip6, &bpc->srv6_source_ipv6, sizeof(bfd->out_sip6)); + if (bpc->bfd_regext_flags & BFD_REGEXT_FLAG_SEG_LIST) { + bfd->segnum = bpc->seg_num; + if (bpc->seg_num > 0) + memcpy(bfd->seg_list, bpc->seg_list, + sizeof(struct in6_addr) * bpc->seg_num); + } + if (bpc->bfd_regext_flags & BFD_REGEXT_FLAG_REMOTE_DISCR) + bfd->discrs.remote_discr = bpc->remote_discr; + if (bs_registrate(bfd) == NULL) return NULL; diff --git a/bfdd/bfd.h b/bfdd/bfd.h index faf4fb0085f5..6d0bb20c671f 100644 --- a/bfdd/bfd.h +++ b/bfdd/bfd.h @@ -102,6 +102,21 @@ struct bfd_peer_cfg { char bfd_name[BFD_NAME_SIZE + 1]; uint8_t bfd_name_len; + /* + * SBFD over SRv6 fields carried in the optional tail of + * the ZEBRA_BFD_DEST_REGISTER / DEST_UPDATE / DEST_DEREGISTER + * payload. `bfd_regext_flags` mirrors the wire-format + * `BFD_REGEXT_FLAG_*` word so propagation and update paths can + * distinguish "field absent" from "field set to zero" (notably + * `remote_discr == 0` for `sbfd_init`). + */ + uint16_t bfd_regext_flags; + uint8_t bfd_mode; + uint32_t remote_discr; + struct in6_addr srv6_source_ipv6; + uint8_t seg_num; + struct in6_addr seg_list[SRV6_MAX_SEGS]; + struct { /* Keychain name for authentication */ char key_chain_name[MAXKEYCHAINNAMELEN + 1]; diff --git a/bfdd/ptm_adapter.c b/bfdd/ptm_adapter.c index 9169728cab6e..04b5c8792a56 100644 --- a/bfdd/ptm_adapter.c +++ b/bfdd/ptm_adapter.c @@ -368,6 +368,23 @@ static int _ptm_msg_read(struct stream *msg, int command, vrf_id_t vrf_id, * - c: profile name length. * - X bytes: profile name. * + * Optional tail (modeled on route ZAPI's ZAPI_MESSAGE_* pattern): + * - w: bfd_regext_flags (always, when tail present) + * if FLAG_BFD_MODE: c bfd_mode (`bfd_mode_type` in bfdd/bfd.h) + * if FLAG_REMOTE_DISCR: l remote_discr + * if FLAG_SRV6_SOURCE: 16 srv6_source_ipv6 + * if FLAG_SEG_LIST: c seg_num (0..SRV6_MAX_SEGS) + * 16*seg_num bytes: seg_list[] + * if FLAG_BFD_NAME: c bfd_name length + * X bytes: bfd_name + * + * The encoder emits the tail when at least one BFD_REGEXT_FLAG_* bit + * is set on `bfd_session_arg`, including on deregister frames so a + * named session (whose key includes `bfd_name`) can be located for + * deletion. The decoder detects the no-tail case via STREAM_READABLE + * and leaves all optional fields zero-initialised, so classical-BFD + * senders are accepted unchanged. + * * q(64), l(32), w(16), c(8) */ @@ -461,6 +478,84 @@ static int _ptm_msg_read(struct stream *msg, int command, vrf_id_t vrf_id, bpc->bpc_profile[ifnamelen] = 0; } + /* + * Optional-field tail. Each BFD_REGEXT_FLAG_* bit in the leading + * flags word (see `lib/bfd.h`) explicitly indicates that the + * corresponding field follows. Classical-BFD senders write no tail + * at all and are detected via STREAM_READABLE; in that case every + * optional field stays at its memset-zero default. + * + * The "no tail" check is the only positional probing left; past the + * flags word, every field's presence is signalled explicitly, so a + * future upstream extension that appends bytes here (or that lands + * with no flags word at all) is rejected cleanly via the bounds + * check on the trailing wire bytes rather than silently misparsing + * as an SBFD field. + * + * Forward compatibility: bytes left after the last flagged field + * this decoder knows about are silently ignored. A newer sender + * may append fields by claiming new BFD_REGEXT_FLAG_* bits; older + * decoders parse what they recognise and stop. + */ + if (STREAM_READABLE(msg) > 0) { + uint16_t flags; + uint8_t i; + + STREAM_GETW(msg, flags); + bpc->bfd_regext_flags = flags; + + if (flags & BFD_REGEXT_FLAG_BFD_MODE) { + STREAM_GETC(msg, bpc->bfd_mode); + if (bpc->bfd_mode > BFD_MODE_TYPE_SBFD_INIT) { + zlog_err("ptm-read: unknown bfd_mode %u (max %u)", + bpc->bfd_mode, BFD_MODE_TYPE_SBFD_INIT); + goto stream_failure; + } + } + if (flags & BFD_REGEXT_FLAG_REMOTE_DISCR) + STREAM_GETL(msg, bpc->remote_discr); + if (flags & BFD_REGEXT_FLAG_SRV6_SOURCE) + STREAM_GET(&bpc->srv6_source_ipv6, msg, + sizeof(struct in6_addr)); + if (flags & BFD_REGEXT_FLAG_SEG_LIST) { + STREAM_GETC(msg, bpc->seg_num); + if (bpc->seg_num > SRV6_MAX_SEGS) { + zlog_err("ptm-read: seg_num %u exceeds SRV6_MAX_SEGS %u", + bpc->seg_num, SRV6_MAX_SEGS); + goto stream_failure; + } + for (i = 0; i < bpc->seg_num; i++) + STREAM_GET(&bpc->seg_list[i], msg, + sizeof(struct in6_addr)); + } + if (flags & BFD_REGEXT_FLAG_BFD_NAME) { + /* + * bfd_name_len is uint8_t (max 255) and the destination + * buffer has BFD_NAME_SIZE + 1 == 256 bytes, so any wire + * value fits with room for the NUL terminator. + */ + STREAM_GETC(msg, bpc->bfd_name_len); + if (bpc->bfd_name_len) { + STREAM_GET(bpc->bfd_name, msg, bpc->bfd_name_len); + bpc->bfd_name[bpc->bfd_name_len] = '\0'; + } + } + + /* + * Cross-flag validation: SBFD_INIT identifies the remote end + * by a discriminator (RFC 7881 §3); discriminator 0 is reserved, + * so a session whose mode is SBFD_INIT must arrive with an + * explicit `remote_discr` (FLAG_REMOTE_DISCR set). Reject the + * frame rather than create a session that can never come up. + */ + if ((flags & BFD_REGEXT_FLAG_BFD_MODE) && + bpc->bfd_mode == BFD_MODE_TYPE_SBFD_INIT && + !(flags & BFD_REGEXT_FLAG_REMOTE_DISCR)) { + zlog_err("ptm-read: SBFD_INIT mode without FLAG_REMOTE_DISCR; rejecting"); + goto stream_failure; + } + } + /* Sanity check: peer and local address must match IP types. */ if (bpc->bpc_local.sa_sin.sin_family != AF_UNSPEC && (bpc->bpc_local.sa_sin.sin_family @@ -513,6 +608,22 @@ static void bfdd_dest_register(struct stream *msg, vrf_id_t vrf_id) bfd_profile_apply(bpc.bpc_profile, bs); } + /* + * the SBFD/SRv6 fields decoded in `_ptm_msg_read` are + * propagated onto the freshly-created session inside + * `ptm_bfd_sess_new` — *before* `bs_registrate` runs. That ordering + * matters because `bfd_session_enable` (called from `bs_registrate`) + * dispatches on `bs->bfd_mode` to choose between the SRH and the + * classical UDP socket; setting `bs->bfd_mode` here would be too + * late. + * + * Re-registers of an *existing* session reach the `bs != NULL` + * branch above and never enter `ptm_bfd_sess_new`, so the SBFD + * fields on the live session are intentionally immutable from the + * ZAPI surface. A pathd-side reroute that wants to change the + * segment list must issue a DEREGISTER followed by a REGISTER. + */ + /* Create client peer notification register. */ pcn_new(pc, bs); diff --git a/lib/bfd.c b/lib/bfd.c index 5c0004224b36..4730bcf002ba 100644 --- a/lib/bfd.c +++ b/lib/bfd.c @@ -348,6 +348,90 @@ int zclient_bfd_command(struct zclient *zc, struct bfd_session_arg *args) stream_putc(s, args->profilelen); if (args->profilelen) stream_put(s, args->profile, args->profilelen); + + /* + * Optional-field tail. Modeled on the ZAPI_MESSAGE_* pattern used by + * the route ZAPI messages: each BFD_REGEXT_FLAG_* bit in + * `args->bfd_regext_flags` indicates that the corresponding field + * follows. The tail is emitted iff at least one bit is set — + * including on `ZEBRA_BFD_DEST_DEREGISTER`, because `bfd_name` + * participates in `gen_bfd_key` and a named session cannot be + * located for deletion without it. Classical-BFD callers leave + * `bfd_regext_flags` zero and produce byte-identical wire frames to + * the pre-extension layout. + * + * Tail format: + * w bfd_regext_flags (always, when tail is emitted) + * if FLAG_BFD_MODE: c bfd_mode + * if FLAG_REMOTE_DISCR: l remote_discr + * if FLAG_SRV6_SOURCE: 16 srv6_source_ipv6 + * if FLAG_SEG_LIST: c seg_num + 16*seg_num bytes seg_list + * if FLAG_BFD_NAME: c bfd_name length + X bytes bfd_name + * + * q(64), l(32), w(16), c(8) + */ + if (args->bfd_regext_flags != 0) { + uint16_t flags = args->bfd_regext_flags; + + /* + * Reject cross-flag combinations the wire format admits but + * the session semantics don't: `remote_discr` is only + * meaningful when `bfd_mode` is set to an SBFD mode, and + * `FLAG_BFD_MODE` should carry a non-classical mode value. + * Catch caller bugs at the boundary rather than silently + * shipping garbage on the wire. + */ + if ((flags & BFD_REGEXT_FLAG_REMOTE_DISCR) && + !(flags & BFD_REGEXT_FLAG_BFD_MODE)) { + zlog_err("%s: FLAG_REMOTE_DISCR set without FLAG_BFD_MODE; rejecting", + __func__); + return -1; + } + if ((flags & BFD_REGEXT_FLAG_BFD_MODE) && args->bfd_mode == 0) { + zlog_err("%s: FLAG_BFD_MODE set with classical mode (0); rejecting", + __func__); + return -1; + } + + /* + * Reject out-of-range `seg_num` rather than silently + * truncating: an SBFD/SRv6 session whose SID list is short + * by one entry would route through the wrong path and the + * BFD session would still report Up — the worst possible + * silent failure mode for fast-switchover infrastructure. + */ + if ((flags & BFD_REGEXT_FLAG_SEG_LIST) && + args->seg_num > SRV6_MAX_SEGS) { + zlog_err("%s: seg_num %u exceeds SRV6_MAX_SEGS %u; rejecting registration", + __func__, args->seg_num, SRV6_MAX_SEGS); + return -1; + } + + stream_putw(s, flags); + + if (flags & BFD_REGEXT_FLAG_BFD_MODE) + stream_putc(s, args->bfd_mode); + if (flags & BFD_REGEXT_FLAG_REMOTE_DISCR) + stream_putl(s, args->remote_discr); + if (flags & BFD_REGEXT_FLAG_SRV6_SOURCE) + stream_put(s, &args->srv6_source_ipv6, + sizeof(struct in6_addr)); + if (flags & BFD_REGEXT_FLAG_SEG_LIST) { + stream_putc(s, args->seg_num); + for (uint8_t i = 0; i < args->seg_num; i++) + stream_put(s, &args->seg_list[i], + sizeof(struct in6_addr)); + } + if (flags & BFD_REGEXT_FLAG_BFD_NAME) { + size_t bfd_name_len = strnlen(args->bfd_name, + sizeof(args->bfd_name)); + if (bfd_name_len > UINT8_MAX) + bfd_name_len = UINT8_MAX; + stream_putc(s, (uint8_t)bfd_name_len); + if (bfd_name_len) + stream_put(s, args->bfd_name, bfd_name_len); + } + } #else /* PTM BFD */ /* Encode timers if this is a registration message. */ if (args->command != ZEBRA_BFD_DEST_DEREGISTER) { diff --git a/lib/bfd.h b/lib/bfd.h index 881f263e7d15..06e57567a99f 100644 --- a/lib/bfd.h +++ b/lib/bfd.h @@ -8,6 +8,7 @@ #ifndef _ZEBRA_BFD_H #define _ZEBRA_BFD_H +#include "lib/srv6.h" #include "lib/zclient.h" #ifdef __cplusplus @@ -355,6 +356,32 @@ void bfd_sess_show(struct vty *vty, struct json_object *json, */ void bfd_protocol_integration_init(struct zclient *zc, struct event_loop *tm); +/* + * Optional-field flags for the ZEBRA_BFD_DEST_REGISTER / DEST_UPDATE tail. + * + * Each bit indicates that the corresponding field is present in the wire + * payload and carries a meaningful value (including zero). Modeled on the + * ZAPI_MESSAGE_* pattern used by the route ZAPI messages: presence is + * explicit, so "field absent" and "field set to zero" remain + * distinguishable, and new optional fields can be added by claiming a new + * bit without reshuffling positional probing on either side. + * + * Encoder: if any bit in `bfd_regext_flags` is set, emit the 16-bit + * flags word followed by each flagged field in the order below. The + * tail is emitted on deregister too — `bfd_name` participates in + * `gen_bfd_key`, so a named session cannot be located for deletion + * without it. Classical-BFD callers leave `bfd_regext_flags` zero and + * produce byte-identical wire frames to the pre-extension layout. + * + * Decoder: detect the no-tail case via STREAM_READABLE; on a non-empty + * tail, read the flags word and conditionally read each flagged field. + */ +#define BFD_REGEXT_FLAG_BFD_MODE 0x0001 +#define BFD_REGEXT_FLAG_REMOTE_DISCR 0x0002 +#define BFD_REGEXT_FLAG_SRV6_SOURCE 0x0004 +#define BFD_REGEXT_FLAG_SEG_LIST 0x0008 +#define BFD_REGEXT_FLAG_BFD_NAME 0x0010 + /** * BFD session registration arguments. */ @@ -414,6 +441,26 @@ struct bfd_session_arg { uint32_t detection_multiplier; /* bfd session name*/ char bfd_name[BFD_NAME_SIZE + 1]; + + /* + * Optional-tail fields for the ZEBRA_BFD_DEST_REGISTER / + * DEST_UPDATE payload. Each field is emitted on the wire iff the + * corresponding BFD_REGEXT_FLAG_* bit is set in `bfd_regext_flags` + * (above). Classical-BFD callers leave `bfd_regext_flags` zero and + * none of these fields are touched on the wire. + */ + /** Bitmask of optional-tail fields present (BFD_REGEXT_FLAG_*). */ + uint16_t bfd_regext_flags; + /** BFD session mode (`bfd_mode_type` in bfdd/bfd.h). */ + uint8_t bfd_mode; + /** A-priori remote discriminator (sbfd_init only). */ + uint32_t remote_discr; + /** SRv6 outer-IPv6 source. */ + struct in6_addr srv6_source_ipv6; + /** Number of valid SIDs in `seg_list[]` (0 .. `SRV6_MAX_SEGS`). */ + uint8_t seg_num; + /** SRv6 SID list, transmission order. */ + struct in6_addr seg_list[SRV6_MAX_SEGS]; }; /** diff --git a/tests/topotests/bfd_zapi_sbfd_topo1/__init__.py b/tests/topotests/bfd_zapi_sbfd_topo1/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/tests/topotests/bfd_zapi_sbfd_topo1/r1/frr.conf b/tests/topotests/bfd_zapi_sbfd_topo1/r1/frr.conf new file mode 100644 index 000000000000..52e0f3cae830 --- /dev/null +++ b/tests/topotests/bfd_zapi_sbfd_topo1/r1/frr.conf @@ -0,0 +1,7 @@ +hostname r1 +! +interface r1-eth0 + ipv6 address 2001:db8:1::1/64 +! +bfd +! diff --git a/tests/topotests/bfd_zapi_sbfd_topo1/test_bfd_zapi_sbfd_topo1.py b/tests/topotests/bfd_zapi_sbfd_topo1/test_bfd_zapi_sbfd_topo1.py new file mode 100644 index 000000000000..4d41525f5161 --- /dev/null +++ b/tests/topotests/bfd_zapi_sbfd_topo1/test_bfd_zapi_sbfd_topo1.py @@ -0,0 +1,381 @@ +#!/usr/bin/env python +# +# test_bfd_zapi_sbfd_topo1.py +# +# Verifies the ZAPI wire-payload extension: a ZEBRA_BFD_DEST_REGISTER +# message that carries the appended SBFD/SRv6 tail (bfd_mode, remote_discr, +# srv6_source_ipv6, seg_num, seg_list[], bfd_name) is decoded by bfdd and the +# resulting bfd_session reflects those fields. A second register without the +# SBFD tail acts as a back-compatibility regression check that the previous wire format +# senders are still accepted. +# +# Topology: single router r1 running zebra + bfdd. The test process opens +# zebra's ZAPI socket from inside r1's namespace via r1.popen("python3 -c"), +# performs the ZEBRA_HELLO + ZEBRA_BFD_CLIENT_REGISTER + ZEBRA_BFD_DEST_REGISTER +# handshake, and then asserts the bfdd-side session state via vtysh. + +import json +import os +import sys +import time + +import pytest + +CWD = os.path.dirname(os.path.realpath(__file__)) +sys.path.append(os.path.join(CWD, "../")) + +# pylint: disable=C0413 +from lib.common_config import required_linux_kernel_version +from lib.topogen import Topogen, TopoRouter, get_topogen + +pytestmark = [pytest.mark.bfdd] + + +# ZAPI wire-protocol constants — mirror lib/zclient.h / lib/route_types.txt. +ZSERV_PATH = "/var/run/frr/zserv.api" +ZEBRA_HEADER_MARKER = 254 +ZSERV_VERSION = 6 +ZEBRA_HEADER_SIZE = 10 # length(2)+marker(1)+version(1)+vrf(4)+command(2) + +# Subset of `enum zclient_msg_type` from lib/zclient.h. Counted positionally +# from ZEBRA_INTERFACE_ADD (=0). Verified against FRR base commit +# 25d64c41b8 (origin/frr-10.4.1 at the time of this patch); re-verify on +# each upstream FRR rebase with: +# grep -nE '^[[:space:]]+ZEBRA_(HELLO|BFD_DEST_REGISTER|BFD_CLIENT_REGISTER),' \ +# lib/zclient.h +# A stale value will produce no protocol NAK from zebra — the test fails as +# a `_wait_for_peer` timeout rather than a wire-level error. +ZEBRA_HELLO = 19 +ZEBRA_BFD_DEST_REGISTER = 27 +ZEBRA_BFD_CLIENT_REGISTER = 35 + +# enum bfd_mode_type from bfdd/bfd.h. +BFD_MODE_TYPE_BFD = 0 +BFD_MODE_TYPE_SBFD_INIT = 2 + +# Position of ZEBRA_ROUTE_SHARP in lib/route_types.txt — we identify as +# sharpd, the canonical dev/test ZAPI client. +ZEBRA_ROUTE_SHARP = 23 + +VRF_DEFAULT = 0 + + +def build_topo(tgen): + tgen.add_router("r1") + sw = tgen.add_switch("s1") + sw.add_link(tgen.gears["r1"]) + + +def setup_module(mod): + tgen = Topogen(build_topo, mod.__name__) + tgen.start_topology() + for rname, router in tgen.routers().items(): + router.load_frr_config( + os.path.join(CWD, "{}/frr.conf".format(rname)), + [(TopoRouter.RD_ZEBRA, None), (TopoRouter.RD_BFD, None)], + ) + tgen.start_router() + + +def teardown_module(mod): + get_topogen().stop_topology() + + +# Inline Python script that runs inside r1's namespace, opens the ZAPI +# socket, performs the BFD client handshake, and emits one +# ZEBRA_BFD_DEST_REGISTER. Two forms are exercised, selected by argv[1]: +# "sbfd" — appends the SBFD tail (bfd_mode/seg_num/bfd_name). +# "classical" — omits the tail entirely, mimicking a sender without the SBFD tail. +# Exit code 0 indicates the socket exchange succeeded; non-zero plus a +# diagnostic on stderr indicates a wire-level failure. +ZAPI_CLIENT_SCRIPT = r""" +import os, socket, struct, sys, time + +ZSERV_PATH = "/var/run/frr/zserv.api" +ZEBRA_HEADER_MARKER = 254 +ZSERV_VERSION = 6 +ZEBRA_HEADER_SIZE = 10 + +ZEBRA_HELLO = 19 +ZEBRA_BFD_DEST_REGISTER = 27 +ZEBRA_BFD_CLIENT_REGISTER = 35 + +BFD_MODE_TYPE_SBFD_INIT = 2 +ZEBRA_ROUTE_SHARP = 23 +VRF_DEFAULT = 0 + + +def header(command, vrf_id=VRF_DEFAULT): + return struct.pack( + "!HBBIH", ZEBRA_HEADER_SIZE, ZEBRA_HEADER_MARKER, ZSERV_VERSION, + vrf_id, command, + ) + + +def finalize(buf): + # length prefix at offset 0 covers the whole frame + return struct.pack("!H", len(buf)) + buf[2:] + + +def send(sock, buf): + sock.sendall(finalize(buf)) + + +def hello(sock): + buf = bytearray(header(ZEBRA_HELLO)) + buf += struct.pack("!B", ZEBRA_ROUTE_SHARP) # redist_default + buf += struct.pack("!H", 0) # instance + buf += struct.pack("!I", 0) # session_id + buf += struct.pack("!B", 0) # synchronous=false + send(sock, buf) + + +def bfd_client_register(sock): + buf = bytearray(header(ZEBRA_BFD_CLIENT_REGISTER)) + buf += struct.pack("!I", os.getpid()) + send(sock, buf) + + +def bfd_dest_register(sock, kind, dst_addr): + # Mirrors lib/bfd.c::zclient_bfd_command, HAVE_BFDD path. Encodes a + # multihop IPv6 register so the body length is identical between the + # SBFD and classical variants up to the appended tail. + buf = bytearray(header(ZEBRA_BFD_DEST_REGISTER)) + buf += struct.pack("!I", os.getpid()) + + dst = socket.inet_pton(socket.AF_INET6, dst_addr) + src = socket.inet_pton(socket.AF_INET6, "2001:db8:1::1") + + # Layout per lib/bfd.c::zclient_bfd_command (HAVE_BFDD path). + # min_rx/min_tx are uint32; detection_multiplier, mhop, hops, ifname_len, + # cbit and profile_len are each uint8. The stream_putc/stream_putl mix + # is easy to misencode from Python; keep the sizes lined up below. + buf += struct.pack("!H", socket.AF_INET6) + dst # dst family + addr + buf += struct.pack("!II", 300000, 300000) # min_rx, min_tx + buf += struct.pack("!B", 3) # det_mult + buf += struct.pack("!B", 1) # is_multihop = 1 + buf += struct.pack("!H", socket.AF_INET6) + src # src family + addr + buf += struct.pack("!B", 1) # hops/ttl + buf += struct.pack("!B", 0) # ifname_len = 0 (mhop) + buf += struct.pack("!B", 0) # cbit + buf += struct.pack("!B", 0) # profile_len = 0 + + if kind == "sbfd": + # Optional-field tail with BFD_REGEXT_FLAG_* indicating which fields + # follow. Mirrors the encoder in lib/bfd.c::zclient_bfd_command. + BFD_REGEXT_FLAG_BFD_MODE = 0x0001 + BFD_REGEXT_FLAG_REMOTE_DISCR = 0x0002 + BFD_REGEXT_FLAG_SRV6_SOURCE = 0x0004 + BFD_REGEXT_FLAG_SEG_LIST = 0x0008 + BFD_REGEXT_FLAG_BFD_NAME = 0x0010 + + seg_list = [ + socket.inet_pton(socket.AF_INET6, "2001:db8:a::100"), + socket.inet_pton(socket.AF_INET6, "2001:db8:a::200"), + ] + bfd_name = b"zapi-sbfd-test" + + flags = (BFD_REGEXT_FLAG_BFD_MODE + | BFD_REGEXT_FLAG_REMOTE_DISCR + | BFD_REGEXT_FLAG_SRV6_SOURCE + | BFD_REGEXT_FLAG_SEG_LIST + | BFD_REGEXT_FLAG_BFD_NAME) + buf += struct.pack("!H", flags) # bfd_regext_flags + buf += struct.pack("!B", BFD_MODE_TYPE_SBFD_INIT) # bfd_mode + buf += struct.pack("!I", 0x000186A0) # remote_discr = 100000 + buf += src # srv6_source_ipv6 + buf += struct.pack("!B", len(seg_list)) # seg_num + for sid in seg_list: + buf += sid + buf += struct.pack("!B", len(bfd_name)) + bfd_name + elif kind == "classical": + # A sender without the optional tail: no tail bytes at all. + pass + else: + raise SystemExit("unknown kind {!r}".format(kind)) + + send(sock, buf) + + +def main(): + if len(sys.argv) != 3: + raise SystemExit("usage: {} sbfd|classical ".format(sys.argv[0])) + kind = sys.argv[1] + dst_addr = sys.argv[2] + + deadline = time.time() + 10.0 + while time.time() < deadline: + try: + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + sock.connect(ZSERV_PATH) + break + except (FileNotFoundError, ConnectionRefusedError, OSError) as exc: + last_err = exc + time.sleep(0.2) + else: + raise SystemExit("zserv connect failed: {}".format(last_err)) + + try: + hello(sock) + bfd_client_register(sock) + bfd_dest_register(sock, kind, dst_addr) + # Give zebra a beat to forward the register to bfdd before we close. + time.sleep(0.5) + finally: + sock.close() + + +main() +""" + + +def _run_zapi_client(net_node, kind, dst_addr): + """Run the inline ZAPI client inside `net_node`'s network namespace. + + `net_node` is the mininet Router (tgen.net[name]) — it exposes `cmd` + without the higher-level TopoRouter wrappers. The inline script + raises SystemExit on ZAPI handshake failure, but `cmd()` swallows + the exit code; we append a sentinel echo so the caller can surface + a real diagnostic instead of silently timing out at `_wait_for_peer`. + """ + script = ZAPI_CLIENT_SCRIPT.replace("'", "'\\''") + output = net_node.cmd( + "python3 -c '{}' {} {} 2>&1; echo __ZAPI_EXIT__=$?".format( + script, kind, dst_addr)) + assert "__ZAPI_EXIT__=0" in output, ( + "ZAPI client ({} -> {}) failed; output:\n{}".format( + kind, dst_addr, output)) + return output + + +def _show_bfd_peers(net_node): + """Read bfdd's session table as JSON; return list of peer dicts.""" + raw = net_node.cmd("vtysh -c 'show bfd peers json'") + raw = raw.strip() + if not raw: + return [] + return json.loads(raw) + + +def _wait_for_peer(net_node, predicate, timeout=10.0): + """Poll `show bfd peers json` until predicate matches one peer.""" + deadline = time.time() + timeout + last = [] + while time.time() < deadline: + last = _show_bfd_peers(net_node) + for peer in last: + if predicate(peer): + return peer + time.sleep(0.3) + raise AssertionError( + "no bfd peer matched predicate within {}s; last seen: {}".format( + timeout, last)) + + +SBFD_DST = "2001:db8:2::1" +CLASSICAL_DST = "2001:db8:2::2" + + +def test_zapi_sbfd_register_creates_session(): + """ + Drive the new ZAPI tail end-to-end: a ZEBRA_BFD_DEST_REGISTER with + `bfd_mode=SBFD_INIT` + a non-empty `bfd_name` + a 2-SID seg_list + materialises as a bfdd session whose peer/source addresses match + what was sent. `bfd_name` being preserved through the tail is the + load-bearing assertion — it is the only new field already + surfaced by `show bfd peers json` without bfdd-side debug counters. + """ + if required_linux_kernel_version("4.5") is not True: + pytest.skip("Kernel requirements are not met") + + tgen = get_topogen() + if tgen.routers_have_failure(): + pytest.skip(tgen.errors) + + r1 = tgen.net["r1"] + _run_zapi_client(r1, "sbfd", SBFD_DST) + + peer = _wait_for_peer( + r1, lambda p: p.get("peer") == SBFD_DST, + ) + assert peer.get("bfd-name") == "zapi-sbfd-test", ( + "bfd_name from ZAPI SBFD tail not preserved: {!r}".format(peer)) + + +def test_zapi_classical_register_still_accepted(): + """ + Back-compat: a register frame without the SBFD tail (the previous wire + format, and the extended wire format that classical-BFD callers like + BGP/OSPF continue to produce) must still create a + working bfdd session. The decoder's `STREAM_READABLE > 0` gate is + what enables this; the encoder's matching gate is what keeps + classical-BFD wire bytes byte-for-byte unchanged. + """ + if required_linux_kernel_version("4.5") is not True: + pytest.skip("Kernel requirements are not met") + + tgen = get_topogen() + if tgen.routers_have_failure(): + pytest.skip(tgen.errors) + + r1 = tgen.net["r1"] + _run_zapi_client(r1, "classical", CLASSICAL_DST) + + peer = _wait_for_peer( + r1, lambda p: p.get("peer") == CLASSICAL_DST, + ) + # Classical-BFD register: bfd_name was not carried, so the session + # comes up without one. (Default formatting may omit the key + # entirely or emit an empty string; accept both.) + assert not peer.get("bfd-name"), ( + "classical register unexpectedly carries bfd-name: {!r}".format(peer)) + + +def test_zapi_sbfd_register_deduplicates_by_bfd_name(): + """ + Re-registering the *same* SBFD session — same peer/local/vrf/bfd_name + — must reuse the existing `bfd_session`. The load-bearing piece is + that `bs_peer_find` uses `bpc->bfd_name` in the key; for that lookup + to match the previously-inserted session, `ptm_bfd_sess_new` must + also have populated `bs->key.bfdname` (set by the SBFD register path). Without + that, a re-register would silently create a duplicate session and + bfdd's peer count would grow on every flap. + + Self-contained: uses a distinct destination address so the test + does not depend on other tests' session state and can be reordered + or run in isolation. + """ + if required_linux_kernel_version("4.5") is not True: + pytest.skip("Kernel requirements are not met") + + tgen = get_topogen() + if tgen.routers_have_failure(): + pytest.skip(tgen.errors) + + r1 = tgen.net["r1"] + dedupe_dst = "2001:db8:2::3" + + def count_with_dst(dst): + return sum(1 for p in _show_bfd_peers(r1) if p.get("peer") == dst) + + # Issue two back-to-back identical SBFD registers; expect exactly + # one session afterwards. + _run_zapi_client(r1, "sbfd", dedupe_dst) + _wait_for_peer(r1, lambda p: p.get("peer") == dedupe_dst) + after_first = count_with_dst(dedupe_dst) + assert after_first == 1, ( + "first SBFD register did not produce exactly one session " + "(got {})".format(after_first)) + + _run_zapi_client(r1, "sbfd", dedupe_dst) + # Negative-test timing window: we're asserting that the count + # *did not grow*, so there's no event to poll for. The inline + # client already sleeps 0.5s post-send, and we add another 0.5s + # here; on a loaded CI runner this can be bumped if the dedupe + # assertion ever flakes. + time.sleep(0.5) + after_second = count_with_dst(dedupe_dst) + assert after_second == 1, ( + "duplicate SBFD session created on same-name re-register " + "(after_first={}, after_second={})".format( + after_first, after_second))