Hello BIRD team,

This patch (for master branch / 2.18) adds negotiation for the Multiple Labels 
capability (Capability Code 8, RFC 8277) to control MPLS label stack encoding 
in BGP NLRI updates.

BIRD currently reads labels until BOS unconditionally, as specified by RFC 
3107. This works fine with peers that do the same (including previous BIRD 
versions) but does not conform to RFC 8277 when interoperating with peers that 
only expect a single label.

Note that this also matters for SRv6: when an SRv6 service TLV is present (RFC 
9252), the NLRI label field carries raw 24-bit values used for SID 
transposition, and the BOS bit is not set, so it cannot be used to delimit the 
label stack.

The patch adds a new "multiple labels" channel option with 4 modes:

- always (default): always read/write label stacks until BOS. The capability is 
not advertised. This preserves compatibility with existing BIRD peers and RFC 
3107 implementations that send multiple labels.
- advertise: same as always, but also advertises the capability to the peer.
- yes / negotiated: advertise the capability and only use BOS-delimited stacks 
when the peer also advertises it (strict RFC 8277 behavior). If the peer does 
not support the capability, only a single label is read and sent.
- no / disabled: no capability advertised, only single labels used.

The default "always" preserves the existing BIRD behavior, so this is a no-op 
change for existing configurations.

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

Additional RFC 8277 compliance:
- Duplicate AFI/SAFI triples in the capability are handled by keeping the first 
value and ignoring duplicates (Section 2.1)
- Excess labels on receive trigger treat-as-withdraw per RFC 7606 (Section 2.1)
- Routes with more labels than the peer's advertised count are rejected on 
export (Section 3.2.1/3.2.2)

Thanks!
--
Sébastien
From 6a16e6e4703efc391e681e47cc1dbc5c8ac299c0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=A9bastien=20Parisot?= <[email protected]>
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

Reply via email to