From 6a16e6e4703efc391e681e47cc1dbc5c8ac299c0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=A9bastien=20Parisot?= <sparisot@free-mobile.fr>
Date: Mon, 9 Feb 2026 19:49:05 +0000
Subject: [PATCH] bgp: Implement Multiple Labels capability (RFC 8277)

Add negotiation for the Multiple Labels capability (Capability Code 8,
RFC 8277) to control BOS-delimited MPLS label stack reading in BGP.

The new 'multiple labels' channel option has four modes:
- 'always' (default): always read/write label stacks until BOS,
  preserving compatibility with RFC 3107 peers. No capability advertised.
- 'advertise': same as always, but also advertises the capability
- 'yes' / 'negotiated': advertise the RFC 8277 capability and only use
  BOS-delimited stacks when the peer also advertises it
- 'no' / 'disabled': read and send only a single label, no capability

'require multiple labels yes' rejects sessions where the peer does not
advertise the capability.

RFC 8277 compliance:
- Duplicate AFI/SAFI triples: first value kept, rest ignored (Sec 2.1)
- Excess labels on receive: treat-as-withdraw per RFC 7606 (Sec 2.1)
- Excess labels on export: route rejected if exceeding peer count
  (Sec 3.2.1/3.2.2)

The default 'always' preserves the existing BIRD behavior (unconditional
BOS-delimited reading), so this is a no-op change for existing configs.
---
 doc/bird.sgml       | 37 +++++++++++++++++++++++
 proto/bgp/attrs.c   |  5 ++++
 proto/bgp/bgp.c     | 25 ++++++++++++++++
 proto/bgp/bgp.h     | 12 ++++++++
 proto/bgp/config.Y  | 10 ++++++-
 proto/bgp/packets.c | 73 +++++++++++++++++++++++++++++++++++++++++----
 6 files changed, 156 insertions(+), 6 deletions(-)

diff --git a/doc/bird.sgml b/doc/bird.sgml
index 8b68c87..5761460 100644
--- a/doc/bird.sgml
+++ b/doc/bird.sgml
@@ -2868,6 +2868,7 @@ avoid routing loops.
 <item> <rfc id="7911"> &ndash; Advertisement of Multiple Paths in BGP
 <item> <rfc id="7947"> &ndash; Internet Exchange BGP Route Server
 <item> <rfc id="8092"> &ndash; BGP Large Communities Attribute
+<item> <rfc id="8277"> &ndash; Using BGP to Bind MPLS Labels to Address Prefixes
 <item> <rfc id="8212"> &ndash; Default EBGP Route Propagation Behavior without Policies
 <item> <rfc id="8654"> &ndash; Extended Message Support for BGP
 <item> <rfc id="8950"> &ndash; Advertising IPv4 NLRI with an IPv6 Next Hop
@@ -2938,6 +2939,8 @@ protocol bgp [<name>] {
 		require extended next hop <switch>;
 		add paths <switch>|rx|tx;
 		require add paths <switch>;
+		multiple labels <switch>|negotiated|disabled|advertise|always;
+		require multiple labels <switch>;
 		aigp <switch>|originate;
 		cost <number>;
 		graceful restart <switch>;
@@ -3894,6 +3897,39 @@ be used in explicit configuration.
 	configured locally, then the neighbor capability must announce RX.
 	Default: off.
 
+	<tag><label id="bgp-multiple-labels">multiple labels <m/switch/|negotiated|disabled|advertise|always</tag>
+	Controls the MPLS multiple labels capability (<rfc id="8277">) for
+	MPLS-enabled channels (SAFI 4 and SAFI 128). This determines how MPLS
+	label stacks in BGP NLRI are encoded and decoded. Four modes are
+	available:
+
+	When set to <cf/always/ (the default), BIRD always reads label stacks
+	until the bottom-of-stack (BOS) bit on decode, regardless of whether
+	the peer advertises the capability. On encode, all labels are sent with
+	BOS on the last one. The capability is not advertised. This preserves
+	compatibility with RFC 3107 peers that send multiple labels without the
+	RFC 8277 capability negotiation.
+
+	When set to <cf/advertise/, behavior is the same as <cf/always/
+	(unconditional BOS-delimited reading and writing) but the Multiple
+	Labels capability is also advertised to the peer.
+
+	When set to <cf/yes/ (or <cf/negotiated/), BIRD advertises the
+	capability but only uses BOS-delimited label stacks when the peer also
+	advertises the capability (strict RFC 8277 behavior). If the peer does
+	not support the capability, only a single label is read on decode and
+	sent on encode.
+
+	When set to <cf/no/ (or <cf/disabled/), the capability is not
+	advertised and only single labels are used.
+
+	Default: always.
+
+	<tag><label id="bgp-require-multiple-labels">require multiple labels <m/switch/</tag>
+	If enabled, the Multiple Labels capability (<rfc id="8277">) must be
+	announced by the BGP neighbor, otherwise the BGP session will not be
+	established. Default: off.
+
 	<tag><label id="bgp-aigp">aigp <m/switch/|originate</tag>
 	The BGP protocol does not use a common metric like other routing
 	protocols, instead it uses a set of criteria for route selection
@@ -4037,6 +4073,7 @@ direction (re-export of routes to the BGP neighbor):
 	<item><cf/next hop address/
 	<item><cf/extended next hop/
 	<item><cf/add paths/
+	<item><cf/multiple labels/
 	<item><cf/import table/
 	<item><cf/export table/
 	<item><cf/igp table/
diff --git a/proto/bgp/attrs.c b/proto/bgp/attrs.c
index e853624..00a7788 100644
--- a/proto/bgp/attrs.c
+++ b/proto/bgp/attrs.c
@@ -919,6 +919,7 @@ bgp_decode_otc(struct bgp_parse_state *s, uint code UNUSED, uint flags, byte *da
 static void
 bgp_export_mpls_label_stack(struct bgp_export_state *s, eattr *a)
 {
+  struct bgp_channel *bc = (struct bgp_channel *) s->channel;
   net_addr *n = s->route->net->n.addr;
   u32 *labels = (u32 *) a->u.ptr->data;
   uint lnum = a->u.ptr->length / 4;
@@ -935,6 +936,10 @@ bgp_export_mpls_label_stack(struct bgp_export_state *s, eattr *a)
   if ((24*lnum + (net_is_vpn(n) ? 64 : 0) + net_pxlen(n)) > 255)
     REJECT("Malformed MPLS stack - too many labels (%u)", lnum);
 
+  /* RFC 8277 3.2.1/3.2.2: MUST NOT send more labels than peer can handle */
+  if (bc->multiple_labels_count && lnum > bc->multiple_labels_count)
+    REJECT("MPLS stack too deep for peer (%u labels, peer max %u)", lnum, bc->multiple_labels_count);
+
   for (uint i = 0; i < lnum; i++)
   {
     if (labels[i] > 0xfffff)
diff --git a/proto/bgp/bgp.c b/proto/bgp/bgp.c
index 0a68acb..eb60cb6 100644
--- a/proto/bgp/bgp.c
+++ b/proto/bgp/bgp.c
@@ -98,6 +98,7 @@
  * RFC 7947 - Internet Exchange BGP Route Server
  * RFC 8092 - BGP Large Communities Attribute
  * RFC 8212 - Default EBGP Route Propagation Behavior without Policies
+ * RFC 8277 - Using BGP to Bind MPLS Labels to Address Prefixes
  * RFC 8654 - Extended Message Support for BGP
  * RFC 8950 - Advertising IPv4 NLRI with an IPv6 Next Hop
  * RFC 8955 - Dissemination of Flow Specification Rules
@@ -1236,6 +1237,8 @@ bgp_conn_enter_established_state(struct bgp_conn *conn)
     c->ext_next_hop = c->cf->ext_next_hop && (bgp_channel_is_ipv6(c) || rem->ext_next_hop);
     c->add_path_rx = (loc->add_path & BGP_ADD_PATH_RX) && (rem->add_path & BGP_ADD_PATH_TX);
     c->add_path_tx = (loc->add_path & BGP_ADD_PATH_TX) && (rem->add_path & BGP_ADD_PATH_RX);
+    c->multiple_labels = c->cf->multiple_labels && rem->multiple_labels;
+    c->multiple_labels_count = c->multiple_labels ? rem->multiple_labels : 0;
 
     if (active)
       summary_add_path_rx |= !c->add_path_rx ? 1 : 2;
@@ -3122,6 +3125,7 @@ bgp_channel_reconfigure(struct channel *C, struct channel_config *CC, int *impor
       (new->llgr_time != old->llgr_time) ||
       (new->ext_next_hop != old->ext_next_hop) ||
       (new->add_path != old->add_path) ||
+      (new->multiple_labels != old->multiple_labels) ||
       (new->import_table != old->import_table) ||
       (new->export_table != old->export_table) ||
       (TABLE(new, igp_table_ip4) != TABLE(old, igp_table_ip4)) ||
@@ -3348,6 +3352,7 @@ bgp_show_capabilities(struct bgp_proto *p UNUSED, struct bgp_caps *caps)
   uint any_gr_able = 0;
   uint any_add_path = 0;
   uint any_ext_next_hop = 0;
+  uint any_multiple_labels = 0;
   uint any_llgr_able = 0;
   u32 *afl1 = alloca(caps->af_count * sizeof(u32));
   u32 *afl2 = alloca(caps->af_count * sizeof(u32));
@@ -3359,6 +3364,7 @@ bgp_show_capabilities(struct bgp_proto *p UNUSED, struct bgp_caps *caps)
     any_gr_able |= ac->gr_able;
     any_add_path |= ac->add_path;
     any_ext_next_hop |= ac->ext_next_hop;
+    any_multiple_labels |= ac->multiple_labels;
     any_llgr_able |= ac->llgr_able;
   }
 
@@ -3437,6 +3443,25 @@ bgp_show_capabilities(struct bgp_proto *p UNUSED, struct bgp_caps *caps)
     bgp_show_afis(-1006, "        TX:", afl2, afn2);
   }
 
+  if (any_multiple_labels)
+  {
+    cli_msg(-1006, "      Multiple labels");
+
+    buffer b;
+    LOG_BUFFER_INIT(b);
+    buffer_puts(&b, "        AF supported:");
+    WALK_AF_CAPS(caps, ac)
+      if (ac->multiple_labels)
+      {
+	const struct bgp_af_desc *desc = bgp_get_af_desc(ac->afi);
+	if (desc)
+	  buffer_print(&b, " %s (%u)", desc->name, ac->multiple_labels);
+	else
+	  buffer_print(&b, " <%u/%u> (%u)", BGP_AFI(ac->afi), BGP_SAFI(ac->afi), ac->multiple_labels);
+      }
+    cli_msg(-1006, "%s", b.start);
+  }
+
   if (caps->enhanced_refresh)
     cli_msg(-1006, "      Enhanced refresh");
 
diff --git a/proto/bgp/bgp.h b/proto/bgp/bgp.h
index 9a206cf..ea4de2a 100644
--- a/proto/bgp/bgp.h
+++ b/proto/bgp/bgp.h
@@ -186,6 +186,8 @@ struct bgp_channel_config {
   u8 require_ext_next_hop;		/* Require remote support of IPv4 NLRI with IPv6 next hops [RFC 8950] */
   u8 add_path;				/* Use ADD-PATH extension [RFC 7911] */
   u8 require_add_path;			/* Require remote support of ADD-PATH extension [RFC 7911] */
+  u8 multiple_labels;			/* Use multiple labels extension [RFC 8277] (BGP_MPLS_ML_*) */
+  u8 require_multiple_labels;		/* Require remote support of multiple labels [RFC 8277] */
   u8 aigp;				/* AIGP is allowed on this session */
   u8 aigp_originate;			/* AIGP is originated automatically */
   u32 cost;				/* IGP cost for direct next hops */
@@ -234,6 +236,11 @@ struct bgp_channel_config {
 #define BGP_ADD_PATH_TX		2
 #define BGP_ADD_PATH_FULL	3
 
+#define BGP_MPLS_ML_DISABLED	0	/* No multiple labels */
+#define BGP_MPLS_ML_NEGOTIATED	1	/* Advertise and use when negotiated (strict RFC 8277) */
+#define BGP_MPLS_ML_ADVERTISE	3	/* Like ALWAYS but also advertise the capability */
+#define BGP_MPLS_ML_ALWAYS	4	/* Always read until BOS on decode (RFC 3107 compatible) */
+
 #define BGP_GR_ABLE		1
 #define BGP_GR_AWARE		2
 
@@ -269,6 +276,7 @@ struct bgp_af_caps {
   u8 llgr_flags;			/* Long-lived GR per-AF flags */
   u8 ext_next_hop;			/* Extended IPv6 next hop,   RFC 8950 */
   u8 add_path;				/* Multiple paths support,   RFC 7911 */
+  u8 multiple_labels;			/* Multiple labels support,  RFC 8277 */
 };
 
 struct bgp_caps {
@@ -287,6 +295,7 @@ struct bgp_caps {
   u8 llgr_aware;			/* Long-lived GR capability, RFC 9494 */
   u8 any_ext_next_hop;			/* Bitwise OR of per-AF ext_next_hop */
   u8 any_add_path;			/* Bitwise OR of per-AF add_path */
+  u8 any_multiple_labels;		/* Bitwise OR of per-AF multiple_labels */
 
   const char *hostname;			/* Hostname, RFC draft */
 
@@ -460,6 +469,9 @@ struct bgp_channel {
   u8 add_path_rx;			/* Session expects receive of ADD-PATH extended NLRI */
   u8 add_path_tx;			/* Session expects transmit of ADD-PATH extended NLRI */
 
+  u8 multiple_labels;			/* Session uses NLRI encoding with multiple labels */
+  u8 multiple_labels_count;		/* Peer's advertised max label count (RFC 8277) */
+
   u8 feed_state;			/* Feed state (TX) for EoR, RR packets, see BFS_* */
   u8 load_state;			/* Load state (RX) for EoR, RR packets, see BFS_* */
 };
diff --git a/proto/bgp/config.Y b/proto/bgp/config.Y
index 4ffc49f..d434b16 100644
--- a/proto/bgp/config.Y
+++ b/proto/bgp/config.Y
@@ -36,7 +36,8 @@ CF_KEYWORDS(BGP, LOCAL, NEIGHBOR, AS, HOLD, TIME, CONNECT, RETRY, KEEPALIVE,
 	DYNAMIC, RANGE, NAME, DIGITS, BGP_AIGP, AIGP, ORIGINATE, COST, ENFORCE,
 	FIRST, FREE, VALIDATE, BASE, ROLE, ROLES, PEER, PROVIDER, CUSTOMER,
 	RS_SERVER, RS_CLIENT, REQUIRE, BGP_OTC, GLOBAL, SEND, RECV, MIN, MAX,
-	AUTHENTICATION, NONE, MD5, AO, FORMAT, NATIVE, SINGLE, DOUBLE)
+	AUTHENTICATION, NONE, MD5, AO, FORMAT, NATIVE, SINGLE, DOUBLE,
+	MULTIPLE, LABELS, ALWAYS, NEGOTIATED, DISABLED)
 
 CF_KEYWORDS(KEY, KEYS, SECRET, DEPRECATED, PREFERRED, ALGORITHM, CMAC, AES128)
 
@@ -403,6 +404,7 @@ bgp_channel_start: bgp_afi
     BGP_CC->min_llgr_time = ~0U; /* undefined */
     BGP_CC->max_llgr_time = ~0U; /* undefined */
     BGP_CC->aigp = 0xff;	/* undefined */
+    BGP_CC->multiple_labels = BGP_MPLS_ML_ALWAYS;	/* default: always read until BOS (RFC 3107 compatible) */
   }
 };
 
@@ -449,6 +451,12 @@ bgp_channel_item:
  | ADD PATHS TX { BGP_CC->add_path = BGP_ADD_PATH_TX; }
  | ADD PATHS bool { BGP_CC->add_path = $3 ? BGP_ADD_PATH_FULL : 0; }
  | REQUIRE ADD PATHS bool { BGP_CC->require_add_path = $4; }
+ | MULTIPLE LABELS bool { BGP_CC->multiple_labels = $3 ? BGP_MPLS_ML_NEGOTIATED : BGP_MPLS_ML_DISABLED; }
+ | MULTIPLE LABELS NEGOTIATED { BGP_CC->multiple_labels = BGP_MPLS_ML_NEGOTIATED; }
+ | MULTIPLE LABELS DISABLED { BGP_CC->multiple_labels = BGP_MPLS_ML_DISABLED; }
+ | MULTIPLE LABELS ADVERTISE { BGP_CC->multiple_labels = BGP_MPLS_ML_ADVERTISE; }
+ | MULTIPLE LABELS ALWAYS { BGP_CC->multiple_labels = BGP_MPLS_ML_ALWAYS; }
+ | REQUIRE MULTIPLE LABELS bool { BGP_CC->require_multiple_labels = $4; }
  | IMPORT TABLE bool { BGP_CC->import_table = $3; }
  | EXPORT TABLE bool { BGP_CC->export_table = $3; }
  | AIGP bool { BGP_CC->aigp = $2; BGP_CC->aigp_originate = 0; }
diff --git a/proto/bgp/packets.c b/proto/bgp/packets.c
index fcaff74..eda38ef 100644
--- a/proto/bgp/packets.c
+++ b/proto/bgp/packets.c
@@ -303,6 +303,10 @@ bgp_prepare_capabilities(struct bgp_conn *conn)
     ac->add_path = c->cf->add_path;
     caps->any_add_path |= ac->add_path;
 
+    /* Only advertise Multiple Labels capability in NEGOTIATED and ADVERTISE modes */
+    ac->multiple_labels = (c->desc->mpls && c->cf->multiple_labels && c->cf->multiple_labels < BGP_MPLS_ML_ALWAYS) ? MPLS_MAX_LABEL_STACK : 0;
+    caps->any_multiple_labels |= ac->multiple_labels;
+
     if (c->cf->gr_able)
     {
       ac->gr_able = 1;
@@ -431,6 +435,23 @@ bgp_write_capabilities(struct bgp_caps *caps, byte *buf)
     data[-1] = buf - data;
   }
 
+  if (caps->any_multiple_labels)
+  {
+    *buf++ = 8;			/* Capability 8: Multiple labels, RFC 8277 */
+    *buf++ = 0;			/* Capability data length, will be fixed later */
+    data = buf;
+
+    WALK_AF_CAPS(caps, ac)
+      if (ac->multiple_labels)
+      {
+	put_af3(buf, ac->afi);
+	buf[3] = MPLS_MAX_LABEL_STACK;	/* Max labels we can process (less than BGP_MPLS_MAX) */
+	buf += 4;
+      }
+
+    data[-1] = buf - data;
+  }
+
   if (caps->enhanced_refresh)
   {
     *buf++ = 70;		/* Capability 70: Support for enhanced route refresh */
@@ -549,6 +570,25 @@ bgp_read_capabilities(struct bgp_conn *conn, byte *pos, int len)
       caps->ext_messages = 1;
       break;
 
+    case  8: /* Multiple labels capability, RFC 8277 */
+      if (cl % 4)
+	goto err;
+
+      for (i = 0; i < cl; i += 4)
+      {
+	u8 count = pos[2+i+3];
+
+	/* RFC 8277: triples with count 0 or 1 MUST be ignored */
+	if (count <= 1)
+	  continue;
+
+	af = get_af3(pos+2+i);
+	ac = bgp_get_af_caps(&caps, af);
+	if (!ac->multiple_labels)	/* RFC 8277: keep first, ignore duplicates */
+	  ac->multiple_labels = count;
+      }
+      break;
+
     case  9: /* BGP role capability, RFC 9234 */
       if (cl != 1)
         goto err;
@@ -751,6 +791,9 @@ bgp_check_capabilities(struct bgp_conn *conn)
       if (c->cf->require_add_path && (loc->add_path & BGP_ADD_PATH_TX) && !(rem->add_path & BGP_ADD_PATH_RX))
 	return 0;
 
+      if (c->cf->require_multiple_labels && !rem->multiple_labels)
+	return 0;
+
       count++;
     }
   }
@@ -1594,27 +1637,40 @@ bgp_rte_update(struct bgp_parse_state *s, const net_addr *n, u32 path_id, rta *a
 }
 
 static void
-bgp_encode_mpls_labels(struct bgp_write_state *s UNUSED, const adata *mpls, byte **pos, uint *size, byte *pxlen)
+bgp_encode_mpls_labels(struct bgp_write_state *s, const adata *mpls, byte **pos, uint *size, byte *pxlen)
 {
+  struct bgp_channel *c = s->channel;
   const u32 dummy = 0;
   const u32 *labels = mpls ? (const u32 *) mpls->data : &dummy;
   uint lnum = mpls ? (mpls->length / 4) : 1;
+  uint num;
 
-  for (uint i = 0; i < lnum; i++)
+  if (lnum == 1 || c->multiple_labels || c->cf->multiple_labels >= BGP_MPLS_ML_ADVERTISE)
   {
-    put_u24(*pos, labels[i] << 4);
+    num = lnum;
+    for (uint i = 0; i < lnum; i++)
+    {
+      put_u24(*pos, labels[i] << 4);
+      ADVANCE(*pos, *size, 3);
+    }
+  }
+  else
+  {
+    num = 1;
+    put_u24(*pos, BGP_MPLS_NULL << 4);
     ADVANCE(*pos, *size, 3);
   }
 
   /* Add bottom-of-stack flag */
   (*pos)[-1] |= BGP_MPLS_BOS;
 
-  *pxlen += 24 * lnum;
+  *pxlen += 24 * num;
 }
 
 static void
 bgp_decode_mpls_labels(struct bgp_parse_state *s, byte **pos, uint *len, uint *pxlen, rta *a)
 {
+  struct bgp_channel *c = s->channel;
   u32 labels[BGP_MPLS_MAX], label;
   uint lnum = 0;
 
@@ -1622,6 +1678,9 @@ bgp_decode_mpls_labels(struct bgp_parse_state *s, byte **pos, uint *len, uint *p
     if (*pxlen < 24)
       bgp_parse_error(s, 1);
 
+    if (lnum >= BGP_MPLS_MAX)
+      bgp_parse_error(s, 1);
+
     label = get_u24(*pos);
     labels[lnum++] = label >> 4;
     ADVANCE(*pos, *len, 3);
@@ -1632,7 +1691,11 @@ bgp_decode_mpls_labels(struct bgp_parse_state *s, byte **pos, uint *len, uint *p
     if (!s->reach_nlri_step)
       return;
   }
-  while (!(label & BGP_MPLS_BOS));
+  while ((c->multiple_labels || c->cf->multiple_labels >= BGP_MPLS_ML_ADVERTISE) && !(label & BGP_MPLS_BOS));
+
+  /* RFC 8277 2.1: treat-as-withdraw if more labels than our advertised count */
+  if (c->multiple_labels && lnum > MPLS_MAX_LABEL_STACK)
+    bgp_parse_error(s, 1);
 
   if (!a)
     return;
-- 
2.47.3

