Commit b84a17ebdd for frr

commit b84a17ebdd82e17010d944839655b76165d96f39
Author: Yoann Lopez <yoann.lopez@gmail.com>
Date:   Mon Nov 2 14:12:29 2026 +0100

    isisd: add an option to disable periodic CSNPs on p2p circuits

    A point-to-point circuit sends a complete set of CSNPs every
    csnp-interval, and this cannot be turned off. Each CSNP lists every
    LSP, so it is a steady cost even when nothing changes.

    Add "no isis periodic-csnp": on a point-to-point circuit, only the
    CSNP sent when an adjacency comes up is kept (RFC 5303 section 1);
    flooding stays reliable through PSNP acknowledgements. Broadcast
    circuits are not affected, and the default is unchanged. Warn when a
    setting has no effect: csnp-interval on a point-to-point circuit with
    periodic CSNPs disabled, or no periodic-csnp on a broadcast circuit.

    Assisted-by: Claude <noreply@anthropic.com>
    Signed-off-by: Yoann Lopez <yoann.lopez@gmail.com>

diff --git a/isisd/isis_circuit.h b/isisd/isis_circuit.h
index dc396666ef..2f82ef28a2 100644
--- a/isisd/isis_circuit.h
+++ b/isisd/isis_circuit.h
@@ -148,6 +148,8 @@ struct isis_circuit {
 #define ISIS_CIRCUIT_IF_DOWN_FROM_Z    0x02
 	uint8_t flags;
 	bool disable_threeway_adj;
+	/* p2p: CSNPs only when an adjacency comes up */
+	bool disable_p2p_periodic_csnp;
 	/* route IPv4 over IPv6 link-local nexthops where IPv4 has none? */
 	bool ipv4_over_ipv6_nexthop;
 	struct {
diff --git a/isisd/isis_cli.c b/isisd/isis_cli.c
index 038d82acf2..965f03873d 100644
--- a/isisd/isis_cli.c
+++ b/isisd/isis_cli.c
@@ -107,6 +107,45 @@ static const struct lyd_node *isis_iface_cfg_dnode(struct vty *vty)
 	return yang_dnode_getf(vty->candidate_config->dnode, "%s/frr-isisd:isis", VTY_CURR_XPATH);
 }

+/* Say that the CSNP interval is unused on p2p while periodic CSNPs are off. */
+static void isis_warn_csnp_interval_unused(struct vty *vty, bool disabled, bool interval_set)
+{
+	if (disabled && interval_set)
+		vty_out(vty,
+			"%% isis csnp-interval is not used on point-to-point circuits while periodic CSNPs are disabled\n");
+}
+
+/*
+ * Would this interface make a broadcast circuit by default? From its flags,
+ * as isis_circuit_if_add() decides: the circuit may not exist yet. Callers
+ * check the configured network type.
+ */
+static bool isis_iface_broadcast_capable(struct vty *vty)
+{
+	struct interface *ifp = VTY_GET_CONTEXT(interface);
+
+	if (!ifp || if_is_loopback(ifp))
+		return false;
+	if (isis_option_check(ISIS_OPT_DUMMY_AS_LOOPBACK) &&
+	    CHECK_FLAG(ifp->status, ZEBRA_INTERFACE_DUMMY))
+		return false;
+	return !if_is_pointopoint(ifp);
+}
+
+/* Is this interface a broadcast circuit, given its configured network type? */
+static bool isis_iface_broadcast(struct vty *vty, const struct lyd_node *dnode)
+{
+	return isis_iface_broadcast_capable(vty) &&
+	       yang_dnode_get_enum(dnode, "network-type") != CIRCUIT_T_P2P;
+}
+
+/* Say that periodic CSNPs cannot be disabled on a broadcast circuit. */
+static void isis_warn_periodic_csnp_broadcast(struct vty *vty)
+{
+	vty_out(vty,
+		"%% periodic CSNPs cannot be disabled on broadcast circuits; no isis periodic-csnp only applies to point-to-point\n");
+}
+
 /*
  * XPath: /frr-interface:lib/interface/frr-isisd:isis/
  * XPath: /frr-interface:lib/interface/frr-isisd:isis/ipv4-routing
@@ -2556,6 +2595,44 @@ void cli_show_ip_isis_threeway_shake(struct vty *vty, const struct lyd_node *dno
 	vty_out(vty, " isis three-way-handshake\n");
 }

+/*
+ * XPath:
+ * /frr-interface:lib/interface/frr-isisd:isis/disable-p2p-periodic-csnp
+ */
+DEFPY_YANG(isis_periodic_csnp, isis_periodic_csnp_cmd, "[no] isis periodic-csnp",
+	   NO_STR
+	   "IS-IS commands\n"
+	   "Enable/Disable periodic CSNPs on point-to-point circuits\n")
+{
+	const struct lyd_node *dnode = isis_iface_cfg_dnode(vty);
+	bool interval_set;
+
+	if (!dnode) {
+		vty_out(vty, "ISIS is not enabled on this circuit\n");
+		return CMD_SUCCESS;
+	}
+
+	interval_set = !yang_dnode_is_default(dnode, "csnp-interval/level-1") ||
+		       !yang_dnode_is_default(dnode, "csnp-interval/level-2");
+	if (no && isis_iface_broadcast(vty, dnode))
+		isis_warn_periodic_csnp_broadcast(vty);
+	else
+		isis_warn_csnp_interval_unused(vty, !!no, interval_set);
+
+	nb_cli_enqueue_change(vty, "./frr-isisd:isis/disable-p2p-periodic-csnp", NB_OP_MODIFY,
+			      no ? "true" : "false");
+
+	return nb_cli_apply_changes(vty, NULL);
+}
+
+void cli_show_ip_isis_periodic_csnp(struct vty *vty, const struct lyd_node *dnode,
+				    bool show_defaults)
+{
+	if (yang_dnode_get_bool(dnode, NULL))
+		vty_out(vty, " no");
+	vty_out(vty, " isis periodic-csnp\n");
+}
+
 /*
  * XPath: /frr-interface:lib/interface/frr-isisd:isis/ipv4-over-ipv6-nexthop
  */
@@ -2634,6 +2711,15 @@ DEFPY_YANG(csnp_interval, csnp_interval_cmd,
       "Specify interval for level-1 CSNPs\n"
       "Specify interval for level-2 CSNPs\n")
 {
+	const struct lyd_node *dnode = isis_iface_cfg_dnode(vty);
+	bool disabled;
+
+	/* On a broadcast circuit the DIS uses the interval: nothing to warn. */
+	if (dnode && !isis_iface_broadcast(vty, dnode)) {
+		disabled = yang_dnode_get_bool(dnode, "disable-p2p-periodic-csnp");
+		isis_warn_csnp_interval_unused(vty, disabled, intv != DEFAULT_CSNP_INTERVAL);
+	}
+
 	if (!level || strmatch(level, "level-1"))
 		nb_cli_enqueue_change(vty, "./frr-isisd:isis/csnp-interval/level-1", NB_OP_MODIFY,
 				      intv_str);
@@ -2897,9 +2983,14 @@ DEFPY_YANG(isis_network, isis_network_cmd, "[no] isis network point-to-point",
       "Set network type\n"
       "point-to-point network type\n")
 {
-	if (no)
+	const struct lyd_node *dnode = isis_iface_cfg_dnode(vty);
+
+	if (no) {
+		if (dnode && yang_dnode_get_bool(dnode, "disable-p2p-periodic-csnp") &&
+		    isis_iface_broadcast_capable(vty))
+			isis_warn_periodic_csnp_broadcast(vty);
 		nb_cli_enqueue_change(vty, "./frr-isisd:isis/network-type", NB_OP_DESTROY, NULL);
-	else
+	} else
 		nb_cli_enqueue_change(vty, "./frr-isisd:isis/network-type", NB_OP_MODIFY,
 				      "point-to-point");

@@ -3789,6 +3880,7 @@ void isis_cli_init(void)
 	install_element(INTERFACE_NODE, &no_isis_hello_multiplier_cmd);

 	install_element(INTERFACE_NODE, &isis_threeway_adj_cmd);
+	install_element(INTERFACE_NODE, &isis_periodic_csnp_cmd);
 	install_element(INTERFACE_NODE, &isis_ipv4_over_ipv6_nexthop_cmd);

 	install_element(INTERFACE_NODE, &isis_hello_padding_cmd);
diff --git a/isisd/isis_nb.c b/isisd/isis_nb.c
index 332a9152a3..4fd1deeca4 100644
--- a/isisd/isis_nb.c
+++ b/isisd/isis_nb.c
@@ -1139,6 +1139,13 @@ const struct frr_yang_module_info frr_isisd_info = {
 				.modify = lib_interface_isis_disable_three_way_handshake_modify,
 			},
 		},
+		{
+			.xpath = "/frr-interface:lib/interface/frr-isisd:isis/disable-p2p-periodic-csnp",
+			.cbs = {
+				.cli_show = cli_show_ip_isis_periodic_csnp,
+				.modify = lib_interface_isis_disable_p2p_periodic_csnp_modify,
+			},
+		},
 		{
 			.xpath = "/frr-interface:lib/interface/frr-isisd:isis/ipv4-over-ipv6-nexthop",
 			.cbs = {
diff --git a/isisd/isis_nb.h b/isisd/isis_nb.h
index 71ef26453f..e97cd3d28e 100644
--- a/isisd/isis_nb.h
+++ b/isisd/isis_nb.h
@@ -257,6 +257,7 @@ int lib_interface_isis_password_destroy(struct nb_cb_destroy_args *args);
 int lib_interface_isis_password_password_modify(struct nb_cb_modify_args *args);
 int lib_interface_isis_password_password_type_modify(struct nb_cb_modify_args *args);
 int lib_interface_isis_disable_three_way_handshake_modify(struct nb_cb_modify_args *args);
+int lib_interface_isis_disable_p2p_periodic_csnp_modify(struct nb_cb_modify_args *args);
 int lib_interface_isis_ipv4_over_ipv6_nexthop_modify(struct nb_cb_modify_args *args);
 int lib_interface_isis_multi_topology_standard_modify(struct nb_cb_modify_args *args);
 int lib_interface_isis_multi_topology_ipv4_multicast_modify(struct nb_cb_modify_args *args);
@@ -473,6 +474,8 @@ void cli_show_ip_isis_hello_multi(struct vty *vty, const struct lyd_node *dnode,
 				  bool show_defaults);
 void cli_show_ip_isis_threeway_shake(struct vty *vty, const struct lyd_node *dnode,
 				     bool show_defaults);
+void cli_show_ip_isis_periodic_csnp(struct vty *vty, const struct lyd_node *dnode,
+				    bool show_defaults);
 void cli_show_ip_isis_ipv4_over_ipv6_nexthop(struct vty *vty, const struct lyd_node *dnode,
 					     bool show_defaults);
 void cli_show_ip_isis_hello_padding(struct vty *vty, const struct lyd_node *dnode,
diff --git a/isisd/isis_nb_config.c b/isisd/isis_nb_config.c
index 97acaa656f..061f13105f 100644
--- a/isisd/isis_nb_config.c
+++ b/isisd/isis_nb_config.c
@@ -33,6 +33,7 @@
 #include "isisd/isis_misc.h"
 #include "isisd/isis_csm.h"
 #include "isisd/isis_adjacency.h"
+#include "isisd/isis_pdu.h"
 #include "isisd/isis_spf.h"
 #include "isisd/isis_spf_private.h"
 #include "isisd/isis_srv6.h"
@@ -4024,6 +4025,42 @@ int lib_interface_isis_disable_three_way_handshake_modify(struct nb_cb_modify_ar
 	return NB_OK;
 }

+/*
+ * XPath:
+ * /frr-interface:lib/interface/frr-isisd:isis/disable-p2p-periodic-csnp
+ */
+int lib_interface_isis_disable_p2p_periodic_csnp_modify(struct nb_cb_modify_args *args)
+{
+	struct isis_circuit *circuit;
+
+	if (args->event != NB_EV_APPLY)
+		return NB_OK;
+
+	circuit = nb_running_get_entry(args->dnode, NULL, true);
+	circuit->disable_p2p_periodic_csnp = yang_dnode_get_bool(args->dnode, NULL);
+
+	if (circuit->circ_type != CIRCUIT_T_P2P)
+		return NB_OK;
+
+	/* Turned off: drop the pending periodic CSNPs. */
+	if (circuit->disable_p2p_periodic_csnp) {
+		event_cancel(&circuit->t_send_csnp[0]);
+		event_cancel(&circuit->t_send_csnp[1]);
+		return NB_OK;
+	}
+
+	/* Turned back on: re-arm the timer on the circuit's levels with an adjacency up. */
+	for (int level = ISIS_LEVEL1; level <= ISIS_LEVEL2; level++) {
+		if (!(circuit->is_type & level) || !circuit->upadjcount[level - 1])
+			continue;
+		event_add_timer(master, level == IS_LEVEL_1 ? send_l1_csnp : send_l2_csnp, circuit,
+				isis_jitter(circuit->csnp_interval[level - 1], CSNP_JITTER),
+				&circuit->t_send_csnp[level - 1]);
+	}
+
+	return NB_OK;
+}
+
 /*
  * XPath:
  * /frr-interface:lib/interface/frr-isisd:isis/ipv4-over-ipv6-nexthop
diff --git a/isisd/isis_pdu.c b/isisd/isis_pdu.c
index 23b0d5f49b..a6b0e813c7 100644
--- a/isisd/isis_pdu.c
+++ b/isisd/isis_pdu.c
@@ -2311,6 +2311,9 @@ void send_l1_csnp(struct event *event)
 	     || circuit->circ_type == CIRCUIT_T_P2P) {
 		send_csnp(circuit, 1);
 	}
+	/* p2p, periodic CSNPs disabled: no re-arm, next CSNP on adjacency up */
+	if (circuit->circ_type == CIRCUIT_T_P2P && circuit->disable_p2p_periodic_csnp)
+		return;
 	/* set next timer event */
 	event_add_timer(master, send_l1_csnp, circuit,
 			isis_jitter(circuit->csnp_interval[0], CSNP_JITTER),
@@ -2331,6 +2334,9 @@ void send_l2_csnp(struct event *event)
              || circuit->circ_type == CIRCUIT_T_P2P) {
 		send_csnp(circuit, 2);
 	}
+	/* p2p, periodic CSNPs disabled: no re-arm, next CSNP on adjacency up */
+	if (circuit->circ_type == CIRCUIT_T_P2P && circuit->disable_p2p_periodic_csnp)
+		return;
 	/* set next timer event */
 	event_add_timer(master, send_l2_csnp, circuit,
 			isis_jitter(circuit->csnp_interval[1], CSNP_JITTER),
diff --git a/yang/frr-isisd.yang b/yang/frr-isisd.yang
index 7d77997a7a..39152cb2e2 100644
--- a/yang/frr-isisd.yang
+++ b/yang/frr-isisd.yang
@@ -793,6 +793,18 @@ module frr-isisd {
         "Disables three-way handshake when creating new adjacencies.";
     }

+    leaf disable-p2p-periodic-csnp {
+      type boolean;
+      default "false";
+      description
+        "On a point-to-point circuit, send a complete set of CSNPs only
+         when an adjacency comes up (RFC 5303 section 1), not every
+         CSNP interval.";
+      reference
+        "RFC 5303: Three-Way Handshake for IS-IS Point-to-Point
+         Adjacencies, section 1";
+    }
+
     leaf ipv4-over-ipv6-nexthop {
       type boolean;
       default "false";