git: e7b88fdfefcc - main - net/frr10: Enable pimd, install daemons in lib/frr

From: Olivier Cochard <olivier_at_FreeBSD.org>
Date: Thu, 24 Sep 2026 22:55:05 UTC
The branch main has been updated by olivier:

URL: https://cgit.FreeBSD.org/ports/commit/?id=e7b88fdfefccbd47dfc423d1a394db668e9ae6ad

commit e7b88fdfefccbd47dfc423d1a394db668e9ae6ad
Author:     Olivier Cochard <olivier@FreeBSD.org>
AuthorDate: 2026-09-24 22:42:47 +0000
Commit:     Olivier Cochard <olivier@FreeBSD.org>
CommitDate: 2026-09-24 22:52:01 +0000

    net/frr10: Enable pimd, install daemons in lib/frr
    
    Add four patches to make pimd work on FreeBSD (submitted upstream).
    
    Daemons move from ${PREFIX}/sbin to ${PREFIX}/lib/frr, matching the
    Linux packages. Only vtysh and mtracebis stay in bin/. This avoids filename
    collision with net/openbgpd*, net/pimd, net/quagga, etc.
---
 net/frr10/Makefile                         |   7 +-
 net/frr10/files/frr.in                     |   2 +-
 net/frr10/files/patch-pimd_pim__igmp.c     |  22 +++
 net/frr10/files/patch-pimd_pim__mroute.c   | 306 +++++++++++++++++++++++++++++
 net/frr10/files/patch-pimd_pim__sock.c     |  41 ++++
 net/frr10/files/patch-pimd_pim__upstream.c |  71 +++++++
 net/frr10/pkg-plist                        | 105 +++++-----
 7 files changed, 502 insertions(+), 52 deletions(-)

diff --git a/net/frr10/Makefile b/net/frr10/Makefile
index c4cf09311529..65e16583a9f1 100644
--- a/net/frr10/Makefile
+++ b/net/frr10/Makefile
@@ -1,6 +1,7 @@
 PORTNAME=	frr
 DISTVERSIONPREFIX=	frr-
 DISTVERSION=	10.7.1
+PORTREVISION=	1
 CATEGORIES=	net
 .if defined(PYTHONTOOLS)
 PKGNAMESUFFIX=	10-pythontools
@@ -45,7 +46,7 @@ USES=		autoreconf bison compiler:c++11-lang cpe gmake libtool makeinfo \
 
 BINARY_ALIAS=	python=${PYTHON_VERSION}
 
-CONFLICTS=	frr8 frr9 openbgpd openospfd quagga zebra
+CONFLICTS=	frr9
 
 CPE_PRODUCT=	frrouting
 
@@ -91,18 +92,18 @@ TCMALLOC_DESC=		Use tcmalloc
 USERS=	frr
 GROUPS=	frr frrvty
 
-# PIMD and PBRD compile but doesn't work on FreeBSD
+# PBRD compile but doesn't work on FreeBSD
 CONFIGURE_ARGS=	--enable-user=frr \
 		--enable-group=frr \
 		--enable-vty-group=frrvty \
 		--enable-vtysh \
 		--disable-doc-html \
 		--sysconfdir=${ETCDIR} \
+		--sbindir=${PREFIX}/lib/frr \
 		--localstatedir=/var/run/frr \
 		--disable-nhrpd \
 		--disable-pathd \
 		--disable-ospfclient \
-		--disable-pimd \
 		--disable-pbrd \
 		--with-vtysh-pager=cat
 
diff --git a/net/frr10/files/frr.in b/net/frr10/files/frr.in
index ff443b7d2dc8..c71e4f5bd037 100644
--- a/net/frr10/files/frr.in
+++ b/net/frr10/files/frr.in
@@ -116,7 +116,7 @@ do_cmd() {
 	ret=0
 	dmn=$1
 	if [ -n "${dmn}" ] && [ "${dmn}" != "all" ]; then
-		command=%%PREFIX%%/sbin/${dmn}
+		command=%%PREFIX%%/lib/frr/${dmn}
 		pidfile=/var/run/frr/${dmn}.pid
 		eval flags=\$\{${dmn}_flags:-\"\"\}
 		flags="${flags} ${frr_flags}"
diff --git a/net/frr10/files/patch-pimd_pim__igmp.c b/net/frr10/files/patch-pimd_pim__igmp.c
new file mode 100644
index 000000000000..ce85bf5dc5ce
--- /dev/null
+++ b/net/frr10/files/patch-pimd_pim__igmp.c
@@ -0,0 +1,22 @@
+Zero the sockaddr before binding the IGMP socket.
+
+pim_igmp_sock_add() binds its raw IGMP socket with a struct sockaddr_in it never
+zeroes, so sin_zero carries whatever was on the stack.  Linux looks at sin_addr
+alone and the garbage is harmless there.  FreeBSD resolves the address in
+rip_bind() with ifa_ifwithaddr_fib_check(), which compares every sa_len byte
+through sa_equal() -- sin_zero included -- so the lookup misses a perfectly
+local address and bind() fails with EADDRNOTAVAIL.  pimd is then left without an
+IGMP socket on any interface.
+
+Submitted upstream: https://github.com/FRRouting/frr/pull/23455
+
+--- pimd/pim_igmp.c.orig	2026-09-24 18:59:47 UTC
++++ pimd/pim_igmp.c	2026-09-24 18:59:47 UTC
+@@ -1271,6 +1271,7 @@ struct gm_sock *pim_igmp_sock_add(struct list *igmp_sock_list,
+ 		return NULL;
+ 	}
+ 
++	memset(&sin, 0, sizeof(sin));
+ 	sin.sin_family = AF_INET;
+ 	sin.sin_addr = ifaddr;
+ 	sin.sin_port = 0;
diff --git a/net/frr10/files/patch-pimd_pim__mroute.c b/net/frr10/files/patch-pimd_pim__mroute.c
new file mode 100644
index 000000000000..fa9ce02974f3
--- /dev/null
+++ b/net/frr10/files/patch-pimd_pim__mroute.c
@@ -0,0 +1,306 @@
+Four fixes to the multicast routing plumbing on BSD.
+
+1. The mroute socket asked for the ingress ifindex with IP_PKTINFO only, which
+   is Linux and Solaris.  pim_mroute_msg() dispatches every IPPROTO_IGMP packet
+   read from that socket to process_igmp_packet(buf, ifindex), so on BSD ifindex
+   stayed -1, if_lookup_by_index(-1) returned NULL, and every membership report
+   was dropped without a log line.  Use setsockopt_ifindex(), which picks the
+   right option per platform.
+
+2. pimreg is a pseudo interface with no address, which is fine on Linux where
+   the vif is added by ifindex.  BSD adds a vif by local address and add_vif()
+   rejects INADDR_ANY before it looks at VIFF_REGISTER, so the register vif was
+   never created and no PIM Register was ever built.  Lend it the address of
+   another PIM interface, as net/pimd's update_reg_vif() does.
+
+3. SIOCGETSGCNT is gated on PRIV_NETINET_MROUTE in X_mrt_ioctl() and pimd has
+   dropped to its unprivileged user, so the (S,G) counters stayed at zero -- the
+   counters the keepalive timer and the SPT switchover decision read.
+
+4. MRT_DEL_MFC was not wrapped in frr_with_privs while the MRT_ADD_MFC that
+   installs the entry is, so every mroute removal failed with EPERM and left the
+   entry in the kernel with nothing in the daemon referring to it.
+
+Plus: a last hop router has to build (S,G) state itself here.  Linux resolves an
+(S,G) miss against the (*,G) entry in ipmr_cache_find_any() and forwards the
+packet; BSD's mfc_find() matches the source exactly and has no wildcard mfc, so
+the kernel raises a NOCACHE upcall for every new source instead and
+pim_mroute_nocache_forward_existing() returned early for want of an (S,G)
+upstream -- leaving an mfc entry with an empty outgoing list that silenced
+further upcalls and black-holed the traffic.
+
+Submitted upstream: https://github.com/FRRouting/frr/pull/23455
+
+--- pimd/pim_mroute.c.orig	2026-09-24 18:59:47 UTC
++++ pimd/pim_mroute.c	2026-09-24 18:59:47 UTC
+@@ -113,33 +113,18 @@ int pim_mroute_set(struct pim_instance *pim, int enable)
+ 		}
+ 	}
+ 
+-#if defined(HAVE_IP_PKTINFO)
+-	if (enable) {
+-		/* Linux and Solaris IP_PKTINFO */
+-		data = 1;
+-		if (setsockopt(pim->mroute_socket, PIM_IPPROTO, IP_PKTINFO,
+-			       &data, data_len)) {
+-			zlog_warn(
+-				"Could not set IP_PKTINFO on socket fd=%d: errno=%d: %s",
+-				pim->mroute_socket, errno,
+-				safe_strerror(errno));
+-		}
+-	}
+-#endif
+-
+-#if PIM_IPV == 6
+-	if (enable) {
+-		/* Linux and Solaris IPV6_PKTINFO */
+-		data = 1;
+-		if (setsockopt(pim->mroute_socket, PIM_IPPROTO,
+-			       IPV6_RECVPKTINFO, &data, data_len)) {
+-			zlog_warn(
+-				"Could not set IPV6_RECVPKTINFO on socket fd=%d: errno=%d: %s",
+-				pim->mroute_socket, errno,
+-				safe_strerror(errno));
+-		}
+-	}
+-#endif
++	/*
++	 * pim_mroute_msg() hands every IPPROTO_IGMP packet read from this
++	 * socket to process_igmp_packet(), which needs the ingress ifindex to
++	 * find the interface the report came in on; without it
++	 * if_lookup_by_index() fails and every IGMP report is dropped
++	 * silently.  setsockopt_ifindex() asks for it with whatever this
++	 * platform provides -- IP_PKTINFO on Linux and Solaris, IP_RECVIF on
++	 * BSD, IPV6_RECVPKTINFO for v6.
++	 */
++	if (enable && setsockopt_ifindex(PIM_AF, pim->mroute_socket, 1) < 0)
++		zlog_warn("Could not request the ingress ifindex on socket fd=%d",
++			  pim->mroute_socket);
+ 	setsockopt_so_recvbuf(pim->mroute_socket, 1024 * 1024 * 8);
+ 
+ 	if (set_nonblocking(pim->mroute_socket) < 0) {
+@@ -194,10 +179,51 @@ static void pim_mroute_nocache_forward_existing(struct interface *ifp, pim_sgadd
+ 	struct pim_nexthop rpf_nh;
+ 	struct prefix grp;
+ 	struct pim_rpf old;
++	bool on_rpt = false;
+ 
+ 	up = pim_upstream_find(pim, sg);
+-	if (!up)
+-		return;
++	if (!up) {
++		/*
++		 * No (S,G) state yet.  On Linux this upcall would never have
++		 * been raised: ipmr_cache_find_any() resolves the lookup
++		 * against the (*,G) entry and the kernel forwards the packet
++		 * itself.  BSD's mfc_find() matches the source exactly and has
++		 * no wildcard mfc, so a last hop router whose receivers joined
++		 * (*,G) is asked about every new source and has to build the
++		 * (S,G) itself, or the traffic stops one hop short of the
++		 * receiver.
++		 *
++		 * Only do so when the (*,G) it would inherit from exists --
++		 * otherwise this is a source nobody here asked for.  The
++		 * creation is the same one the WHOLEPKT path uses for an LHR;
++		 * the rest of this function resolves the iif and inherits the
++		 * olist, and returns without installing anything if there turn
++		 * out to be no receivers.
++		 */
++		pim_sgaddr star_g = *sg;
++
++		star_g.src = PIMADDR_ANY;
++		if (!pim_upstream_find(pim, &star_g))
++			return;
++
++		up = pim_upstream_add(pim, sg, ifp, PIM_UPSTREAM_FLAG_MASK_SRC_LHR, __func__, NULL);
++		if (!up)
++			return;
++
++		/*
++		 * The keepalive is this entry's expiry: when the source stops,
++		 * it fires and pim_upstream_keep_alive_timer_proc() releases
++		 * the SRC_LHR reference, taking the mfc with it.  It no longer
++		 * drags the router onto the source tree either -- whether that
++		 * switch may happen is decided by spt-switchover, in
++		 * pim_upstream_evaluate_join_desired().
++		 */
++		pim_upstream_keep_alive_timer_start(up, pim->keep_alive_time);
++
++		if (PIM_DEBUG_MROUTE)
++			zlog_debug("%s: %pSG NOCACHE on %s, creating (S,G) from (*,G) on LHR",
++				   __func__, sg, ifp->name);
++	}
+ 
+ 	memset(&rpf_nh, 0, sizeof(rpf_nh));
+ 	pim_addr_to_prefix(&grp, sg->grp);
+@@ -216,14 +242,31 @@ static void pim_mroute_nocache_forward_existing(struct interface *ifp, pim_sgadd
+ 	}
+ 
+ 	if (!rpf_nh.interface || rpf_nh.interface->ifindex != ifp->ifindex) {
+-		if (PIM_DEBUG_MROUTE_DETAIL)
+-			zlog_debug("%s: %pSG NOCACHE on %s, RPF interface is %s", __func__, sg,
+-				   ifp->name, rpf_nh.interface ? rpf_nh.interface->name : "(none)");
+-		return;
++		/*
++		 * Not the RPF interface towards the source -- which on a
++		 * shared tree is the ordinary case, not an error: the packet
++		 * comes from the RP, and it is the (*,G) this (S,G) inherits
++		 * from that knows the interface it arrives on.  Forward it
++		 * along the RPT instead of dropping it; Linux's wildcard mfc
++		 * does exactly that without involving the daemon at all.
++		 *
++		 * A packet on neither interface is genuinely unexpected, and
++		 * still returns.
++		 */
++		if (!up->parent || !up->parent->rpf.source_nexthop.interface ||
++		    up->parent->rpf.source_nexthop.interface->ifindex != ifp->ifindex) {
++			if (PIM_DEBUG_MROUTE_DETAIL)
++				zlog_debug("%s: %pSG NOCACHE on %s, RPF interface is %s", __func__,
++					   sg, ifp->name,
++					   rpf_nh.interface ? rpf_nh.interface->name : "(none)");
++			return;
++		}
++		on_rpt = true;
+ 	}
+ 
+-	if (up->rpf.source_nexthop.interface != rpf_nh.interface ||
+-	    pim_addr_cmp(up->rpf.source_nexthop.mrib_nexthop_addr, rpf_nh.mrib_nexthop_addr)) {
++	if (!on_rpt &&
++	    (up->rpf.source_nexthop.interface != rpf_nh.interface ||
++	     pim_addr_cmp(up->rpf.source_nexthop.mrib_nexthop_addr, rpf_nh.mrib_nexthop_addr))) {
+ 		enum pim_rpf_result rpf_result;
+ 
+ 		memset(&old, 0, sizeof(old));
+@@ -233,6 +276,16 @@ static void pim_mroute_nocache_forward_existing(struct interface *ifp, pim_sgadd
+ 			pim_zebra_upstream_rpf_changed(pim, up, &old);
+ 	}
+ 
++	/*
++	 * An (S,G) forwarding off the shared tree takes its iif from the
++	 * parent rather than from the route to the source, and USE_RPT is
++	 * what pim_upstream_get_mroute_iif() reads to pick it.  Without this
++	 * the entry would be installed pointing at the interface the traffic
++	 * is not arriving on.
++	 */
++	if (on_rpt)
++		pim_upstream_update_use_rpt(up, false /*update_mroute*/);
++
+ 	pim_upstream_inherited_olist_decide(pim, up);
+ 	if (pim_upstream_empty_inherited_olist(up)) {
+ 		if (PIM_DEBUG_MROUTE_DETAIL)
+@@ -241,10 +294,21 @@ static void pim_mroute_nocache_forward_existing(struct interface *ifp, pim_sgadd
+ 		return;
+ 	}
+ 
+-	if (up->sptbit != PIM_UPSTREAM_SPTBIT_TRUE)
++	/* Only the source tree sets the SPT bit; on the RPT we have not
++	 * switched trees and must not claim to have.
++	 */
++	if (!on_rpt && up->sptbit != PIM_UPSTREAM_SPTBIT_TRUE)
+ 		pim_upstream_set_sptbit(up, ifp);
+ 
+-	PIM_UPSTREAM_FLAG_SET_SRC_STREAM(up->flags);
++	/*
++	 * SRC_STREAM says this entry exists because traffic is flowing on the
++	 * source tree, and the keepalive expiry releases the stream reference
++	 * that goes with it.  An (S,G) forwarding off the shared tree is not
++	 * that: its reference is SRC_LHR, and it has to expire through that
++	 * branch instead.
++	 */
++	if (!on_rpt)
++		PIM_UPSTREAM_FLAG_SET_SRC_STREAM(up->flags);
+ 	up->channel_oil->cc.pktcnt++;
+ 
+ 	pim_upstream_update_join_desired(pim, up);
+@@ -1618,6 +1682,38 @@ int pim_mroute_add_vif(struct interface *ifp, pim_addr ifaddr,
+ #ifdef VIFF_USE_IFINDEX
+ 	vc.vc_lcl_ifindex = ifp->ifindex;
+ #else
++	if (ifaddr.s_addr == INADDR_ANY && (flags & VIFF_REGISTER)) {
++		/*
++		 * pimreg is a pseudo interface with no address of its own
++		 * (pim_if_create_pimreg()), which on Linux is fine: the vif is
++		 * added by ifindex.  BSD adds a vif by local address, and its
++		 * add_vif() rejects INADDR_ANY before it ever looks at
++		 * VIFF_REGISTER -- even though the same function documents
++		 * that a register vif "does not really need a valid local
++		 * interface".  Without an address here the register vif is
++		 * never created, so the first hop router's mfc entry points at
++		 * a vif that does not exist, no PIM Register is ever built,
++		 * and the RP never learns of a source behind it.
++		 *
++		 * Lend it the address of another PIM interface, and fall back
++		 * to a loopback address when none has one yet, since only
++		 * non-nullness is actually required.
++		 */
++		struct interface *tmp_ifp;
++
++		FOR_ALL_INTERFACES (pim_ifp->pim->vrf, tmp_ifp) {
++			struct pim_interface *tmp_pim_ifp = tmp_ifp->info;
++
++			if (!tmp_pim_ifp || tmp_ifp == ifp)
++				continue;
++			if (pim_addr_is_any(tmp_pim_ifp->primary_address))
++				continue;
++			ifaddr = tmp_pim_ifp->primary_address;
++			break;
++		}
++		if (ifaddr.s_addr == INADDR_ANY)
++			ifaddr.s_addr = htonl(INADDR_LOOPBACK + 1);
++	}
+ 	if (ifaddr.s_addr == INADDR_ANY) {
+ 		zlog_warn(
+ 			"%s: unnumbered interfaces are not supported on this platform",
+@@ -1973,8 +2069,18 @@ int pim_mroute_del(struct channel_oil *c_oil, const char *name)
+ 		return -2;
+ 	}
+ 
+-	err = setsockopt(pim->mroute_socket, PIM_IPPROTO, MRT_DEL_MFC,
+-			 &c_oil->oil, sizeof(c_oil->oil));
++	/*
++	 * Privileged, exactly as the MRT_ADD_MFC that put the entry there is:
++	 * a kernel that checks (FreeBSD gates the mroute setsockopts on
++	 * PRIV_NETINET_MROUTE) otherwise answers EPERM, and pimd goes on to
++	 * free its own state, leaving the mfc installed with nothing left to
++	 * remove it.
++	 */
++	frr_with_privs (&pimd_privs) {
++		err = setsockopt(pim->mroute_socket, PIM_IPPROTO, MRT_DEL_MFC, &c_oil->oil,
++				 sizeof(c_oil->oil));
++	}
++
+ 	if (err) {
+ 		if (PIM_DEBUG_MROUTE)
+ 			zlog_warn(
+@@ -2001,6 +2107,7 @@ void pim_mroute_update_counters(struct channel_oil *c_oil)
+ {
+ 	struct pim_instance *pim = c_oil->pim;
+ 	pim_sioc_sg_req sgreq;
++	int err;
+ 
+ 	c_oil->cc.oldpktcnt = c_oil->cc.pktcnt;
+ 	c_oil->cc.oldbytecnt = c_oil->cc.bytecnt;
+@@ -2031,7 +2138,22 @@ void pim_mroute_update_counters(struct channel_oil *c_oil)
+ 	sgreq.src = c_oil->oil.mf6cc_origin;
+ 	sgreq.grp = c_oil->oil.mf6cc_mcastgrp;
+ #endif
+-	if (ioctl(pim->mroute_socket, PIM_SIOCGETSGCNT, &sgreq)) {
++#ifdef GNU_LINUX
++	err = ioctl(pim->mroute_socket, PIM_SIOCGETSGCNT, &sgreq);
++#else
++	/*
++	 * BSD gates this ioctl on PRIV_NETINET_MROUTE (X_mrt_ioctl()) and pimd
++	 * has long since dropped to its unprivileged user, so every call
++	 * returned EPERM and the (S,G) counters stayed at zero -- which is
++	 * what the keepalive timer and the SPT switchover decision are read
++	 * from.
++	 */
++	frr_with_privs (&pimd_privs) {
++		err = ioctl(pim->mroute_socket, PIM_SIOCGETSGCNT, &sgreq);
++	}
++#endif
++
++	if (err) {
+ 		pim_sgaddr sg;
+ 
+ 		sg.src = *oil_origin(c_oil);
diff --git a/net/frr10/files/patch-pimd_pim__sock.c b/net/frr10/files/patch-pimd_pim__sock.c
new file mode 100644
index 000000000000..2cab9150f31e
--- /dev/null
+++ b/net/frr10/files/patch-pimd_pim__sock.c
@@ -0,0 +1,41 @@
+Ask for IP_HDRINCL on the register socket: only Linux implies it.
+
+pim_msg_send() builds the outer IP header for every message it puts on
+pim->reg_sock, which pim_reg_sock() creates with SOCK_RAW/IPPROTO_RAW.  Linux
+implies IP_HDRINCL for IPPROTO_RAW, so that header is what goes on the wire.
+BSD does not: rip_output() prepends a header of its own with ip_p taken from the
+socket's protocol, so every PIM Register left FreeBSD as IP protocol 255 with
+pimd's own header as payload, and the RP answered with an ICMP "protocol 255
+unreachable" rather than learning the source.
+
+Submitted upstream: https://github.com/FRRouting/frr/pull/23455
+
+--- pimd/pim_sock.c.orig	2026-09-24 18:59:47 UTC
++++ pimd/pim_sock.c	2026-09-24 18:59:47 UTC
+@@ -198,6 +198,26 @@ int pim_reg_sock(void)
+ 		return PIM_SOCK_ERR_SOCKET;
+ 	}
+ 
++#if PIM_IPV == 4
++	/*
++	 * pim_msg_send() builds the outer IP header itself for everything it
++	 * puts on this socket.  Linux implies IP_HDRINCL for an IPPROTO_RAW
++	 * socket, so that header goes on the wire as written; BSD does not,
++	 * and without the option the kernel prepends a second header of its
++	 * own carrying ip_p = IPPROTO_RAW.  A Register then leaves as IP
++	 * protocol 255 with FRR's header as payload, and the RP answers
++	 * "protocol 255 unreachable" instead of learning the source.
++	 * Setting it explicitly is a no-op on Linux and the fix on BSD.
++	 */
++	{
++		int hincl = 1;
++
++		if (setsockopt(fd, IPPROTO_IP, IP_HDRINCL, &hincl, sizeof(hincl)))
++			zlog_warn("Could not set IP_HDRINCL on socket fd=%d: errno=%d: %s", fd,
++				  errno, safe_strerror(errno));
++	}
++#endif
++
+ 	if (sockopt_reuseaddr(fd)) {
+ 		close(fd);
+ 		return PIM_SOCK_ERR_REUSE;
diff --git a/net/frr10/files/patch-pimd_pim__upstream.c b/net/frr10/files/patch-pimd_pim__upstream.c
new file mode 100644
index 000000000000..6f3bec32559d
--- /dev/null
+++ b/net/frr10/files/patch-pimd_pim__upstream.c
@@ -0,0 +1,71 @@
+Honour spt-switchover when a source is learned off the shared tree.
+
+spt-switchover infinity-and-beyond is enforced only by withholding the register
+vif from the (*,G).  That is enough while the register punt is the only way a
+last hop router can learn a source, and it is not:
+pim_upstream_evaluate_join_desired() joins an (S,G) as soon as its inherited
+olist is non-empty and a keepalive is running, whichever tree the traffic
+arrived on.  A router that learns a source another way therefore leaves the
+shared tree behind the operator's back -- reproducible here, where the last hop
+router holds (S,G) state so the kernel can forward at all.
+
+Submitted upstream: https://github.com/FRRouting/frr/pull/23455
+
+--- pimd/pim_upstream.c.orig	2026-09-24 18:59:47 UTC
++++ pimd/pim_upstream.c	2026-09-24 18:59:47 UTC
+@@ -1539,6 +1539,33 @@ static inline bool pim_upstream_is_msdp_peer_sa(struct pim_upstream *up)
+  *           AND inherited_olist(S,G) != NULL ) )
+  *   }
+  */
++/*
++ * Is this router allowed to leave the shared tree for this group?
++ *
++ * spt-switchover infinity-and-beyond says never, optionally narrowed to the
++ * groups a prefix list does not deny.  Until now the setting was enforced
++ * only by withholding the register vif from the (*,G), which is enough as
++ * long as that punt is the only way a last hop router can learn a source --
++ * it is not, so check it where the join decision is actually made.
++ */
++static bool pim_upstream_spt_switch_allowed(struct pim_instance *pim, pim_addr grp)
++{
++	struct prefix_list *plist;
++	struct prefix g;
++
++	if (pim->spt.switchover != PIM_SPT_INFINITY)
++		return true;
++
++	if (!pim->spt.plist)
++		return false;
++
++	plist = prefix_list_lookup(PIM_AFI, pim->spt.plist);
++	pim_addr_to_prefix(&g, grp);
++
++	/* A group the list denies is exempt from the never-switch policy. */
++	return prefix_list_apply_ext(plist, NULL, &g, true) == PREFIX_DENY;
++}
++
+ bool pim_upstream_evaluate_join_desired(struct pim_instance *pim,
+ 				       struct pim_upstream *up)
+ {
+@@ -1556,9 +1583,19 @@ bool pim_upstream_evaluate_join_desired(struct pim_instance *pim,
+ 		return true;
+ 	empty_inh_oil = pim_upstream_empty_inherited_olist(up);
+ 	if (!empty_inh_oil &&
+-			(pim_upstream_is_kat_running(up) ||
+-			 pim_upstream_is_msdp_peer_sa(up)))
++	    (pim_upstream_is_kat_running(up) || pim_upstream_is_msdp_peer_sa(up))) {
++		/*
++		 * Joining an (S,G) whose traffic is arriving on the shared
++		 * tree is the last-hop switch to the source tree.  Where the
++		 * operator has turned that off, stay on the shared tree
++		 * however much traffic shows up.
++		 */
++		if (PIM_UPSTREAM_FLAG_TEST_USE_RPT(up->flags) &&
++		    !pim_upstream_spt_switch_allowed(pim, up->sg.grp))
++			return false;
++
+ 		return true;
++	}
+ 
+ 	return false;
+ }
diff --git a/net/frr10/pkg-plist b/net/frr10/pkg-plist
index dde1e10226dc..744eeab9f33f 100644
--- a/net/frr10/pkg-plist
+++ b/net/frr10/pkg-plist
@@ -1,4 +1,6 @@
+bin/mtracebis
 bin/vtysh
+include/frr/bfdd/bfddp_packet.h
 include/frr/eigrpd/eigrp_dump.h
 include/frr/eigrpd/eigrp_topology.h
 include/frr/eigrpd/eigrpd.h
@@ -11,7 +13,6 @@ include/frr/lib/atomlist.h
 include/frr/lib/atomptr.h
 include/frr/lib/base64.h
 include/frr/lib/bfd.h
-include/frr/bfdd/bfddp_packet.h
 include/frr/lib/bitfield.h
 include/frr/lib/buffer.h
 include/frr/lib/checksum.h
@@ -53,9 +54,9 @@ include/frr/lib/jhash.h
 include/frr/lib/json.h
 include/frr/lib/keychain.h
 include/frr/lib/ldp_sync.h
-include/frr/lib/libagentx.h
 include/frr/lib/lib_errors.h
 include/frr/lib/lib_vty.h
+include/frr/lib/libagentx.h
 include/frr/lib/libfrr.h
 include/frr/lib/libfrr_trace.h
 include/frr/lib/libospf.h
@@ -85,18 +86,6 @@ include/frr/lib/northbound_db.h
 include/frr/lib/ns.h
 include/frr/lib/openbsd-queue.h
 include/frr/lib/openbsd-tree.h
-include/frr/lib/vlan.h
-include/frr/lib/workqueue.h
-include/frr/ospfd/ospf_api.h
-include/frr/ospfd/ospf_asbr.h
-include/frr/ospfd/ospf_dump.h
-include/frr/ospfd/ospf_dump_api.h
-include/frr/ospfd/ospf_ism.h
-include/frr/ospfd/ospf_lsa.h
-include/frr/ospfd/ospf_lsdb.h
-include/frr/ospfd/ospf_nsm.h
-include/frr/ospfd/ospf_opaque.h
-include/frr/ospfd/ospfd.h
 include/frr/lib/pbr.h
 include/frr/lib/plist.h
 include/frr/lib/prefix.h
@@ -136,11 +125,13 @@ include/frr/lib/typerb.h
 include/frr/lib/typesafe.h
 include/frr/lib/vector.h
 include/frr/lib/version.h
+include/frr/lib/vlan.h
 include/frr/lib/vrf.h
 include/frr/lib/vrf_int.h
 include/frr/lib/vty.h
 include/frr/lib/vxlan.h
 include/frr/lib/wheel.h
+include/frr/lib/workqueue.h
 include/frr/lib/xref.h
 include/frr/lib/yang.h
 include/frr/lib/yang_translator.h
@@ -152,6 +143,16 @@ include/frr/lib/zlog_5424.h
 include/frr/lib/zlog_live.h
 include/frr/lib/zlog_recirculate.h
 include/frr/lib/zlog_targets.h
+include/frr/ospfd/ospf_api.h
+include/frr/ospfd/ospf_asbr.h
+include/frr/ospfd/ospf_dump.h
+include/frr/ospfd/ospf_dump_api.h
+include/frr/ospfd/ospf_ism.h
+include/frr/ospfd/ospf_lsa.h
+include/frr/ospfd/ospf_lsdb.h
+include/frr/ospfd/ospf_nsm.h
+include/frr/ospfd/ospf_opaque.h
+include/frr/ospfd/ospfd.h
 include/frr/zebra/connected.h
 include/frr/zebra/debug.h
 include/frr/zebra/ge_netlink.h
@@ -216,8 +217,34 @@ include/frr/zebra/zebra_vxlan.h
 include/frr/zebra/zebra_vxlan_if.h
 include/frr/zebra/zebra_vxlan_private.h
 include/frr/zebra/zserv.h
-share/yang/frr-logging.yang
-share/yang/ietf-syslog-types.yang
+lib/frr/babeld
+lib/frr/bfdd
+lib/frr/bgpd
+lib/frr/eigrpd
+lib/frr/fabricd
+lib/frr/fpm_listener
+lib/frr/isisd
+%%LDPD%%lib/frr/ldpd
+lib/frr/mgmtd
+%%SNMP%%lib/frr/modules/bgpd_bmp.so
+%%RPKI%%lib/frr/modules/bgpd_rpki.so
+%%SNMP%%lib/frr/modules/bgpd_snmp.so
+%%SNMP%%lib/frr/modules/isisd_snmp.so
+%%SNMP%%%%LDPD%%lib/frr/modules/ldpd_snmp.so
+%%SNMP%%lib/frr/modules/ospf6d_snmp.so
+%%SNMP%%lib/frr/modules/ospfd_snmp.so
+%%SNMP%%lib/frr/modules/ripd_snmp.so
+%%FPM%%lib/frr/modules/zebra_fpm.so
+%%SNMP%%lib/frr/modules/zebra_snmp.so
+lib/frr/ospf6d
+lib/frr/ospfd
+lib/frr/pimd
+lib/frr/ripd
+lib/frr/ripngd
+%%SHARPD%%lib/frr/sharpd
+lib/frr/staticd
+lib/frr/watchfrr
+lib/frr/zebra
 lib/libfrr.a
 lib/libfrr.so
 lib/libfrr.so.0
@@ -246,49 +273,24 @@ lib/libmlag_pb.a
 lib/libmlag_pb.so
 lib/libmlag_pb.so.0
 lib/libmlag_pb.so.0.0.0
-%%SNMP%%lib/frr/modules/bgpd_bmp.so
-%%RPKI%%lib/frr/modules/bgpd_rpki.so
-%%SNMP%%lib/frr/modules/bgpd_snmp.so
-%%SNMP%%lib/frr/modules/isisd_snmp.so
-%%SNMP%%%%LDPD%%lib/frr/modules/ldpd_snmp.so
-%%SNMP%%lib/frr/modules/ospf6d_snmp.so
-%%SNMP%%lib/frr/modules/ospfd_snmp.so
-%%SNMP%%lib/frr/modules/ripd_snmp.so
-%%FPM%%lib/frr/modules/zebra_fpm.so
-%%SNMP%%lib/frr/modules/zebra_snmp.so
+sbin/frr-reload
 %%MANPAGES%%share/man/man1/frr.1.gz
+%%MANPAGES%%share/man/man1/vtysh.1.gz
 %%MANPAGES%%share/man/man8/frr-bfdd.8.gz
+%%MANPAGES%%share/man/man8/frr-bgpd.8.gz
 %%MANPAGES%%share/man/man8/frr-eigrpd.8.gz
 %%MANPAGES%%share/man/man8/frr-isisd.8.gz
-%%MANPAGES%%share/man/man8/frr-staticd.8.gz
-%%MANPAGES%%share/man/man8/frr-watchfrr.8.gz
-%%MANPAGES%%share/man/man1/vtysh.1.gz
-%%MANPAGES%%share/man/man8/frr-bgpd.8.gz
 %%MANPAGES%%%%LDPD%%share/man/man8/frr-ldpd.8.gz
 %%MANPAGES%%share/man/man8/frr-ospf6d.8.gz
 %%MANPAGES%%share/man/man8/frr-ospfd.8.gz
+%%MANPAGES%%share/man/man8/frr-pimd.8.gz
 %%MANPAGES%%share/man/man8/frr-ripd.8.gz
 %%MANPAGES%%share/man/man8/frr-ripngd.8.gz
 %%MANPAGES%%%%SHARPD%%share/man/man8/frr-sharpd.8.gz
+%%MANPAGES%%share/man/man8/frr-staticd.8.gz
+%%MANPAGES%%share/man/man8/frr-watchfrr.8.gz
 %%MANPAGES%%share/man/man8/frr-zebra.8.gz
-sbin/babeld
-sbin/bfdd
-sbin/bgpd
-sbin/eigrpd
-sbin/fabricd
-sbin/fpm_listener
-sbin/frr-reload
-%%LDPD%%sbin/ldpd
-sbin/isisd
-sbin/mgmtd
-sbin/ospf6d
-sbin/ospfd
-sbin/ripd
-sbin/ripngd
-%%SHARPD%%sbin/sharpd
-sbin/staticd
-sbin/watchfrr
-sbin/zebra
+%%MANPAGES%%share/man/man8/mtracebis.8.gz
 share/yang/frr-affinity-map.yang
 share/yang/frr-backend.yang
 share/yang/frr-bfdd.yang
@@ -307,15 +309,21 @@ share/yang/frr-deviations-bgp-datacenter.yang
 share/yang/frr-deviations-ietf-key-chain.yang
 share/yang/frr-eigrpd.yang
 share/yang/frr-filter.yang
+share/yang/frr-gmp.yang
 share/yang/frr-host.yang
 share/yang/frr-if-rmap.yang
 share/yang/frr-interface.yang
 share/yang/frr-isisd.yang
+share/yang/frr-logging.yang
 share/yang/frr-module-translator.yang
 share/yang/frr-nexthop.yang
 share/yang/frr-ospf-route-map.yang
 share/yang/frr-ospf6-route-map.yang
 share/yang/frr-ospfd.yang
+share/yang/frr-pim-candidate.yang
+share/yang/frr-pim-route-map.yang
+share/yang/frr-pim-rp.yang
+share/yang/frr-pim.yang
 share/yang/frr-ripd.yang
 share/yang/frr-ripngd.yang
 share/yang/frr-route-map.yang
@@ -335,4 +343,5 @@ share/yang/ietf-netconf-with-defaults.yang
 share/yang/ietf-netconf.yang
 share/yang/ietf-routing-types.yang
 share/yang/ietf-srv6-types.yang
+share/yang/ietf-syslog-types.yang
 @dir(frr,frr,755) /var/run/frr