From 601926436341b43ed309f53ddd52a01c99d17efb Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=A9bastien=20Parisot?= <sparisot@free-mobile.fr>
Date: Thu, 12 Feb 2026 16:10:19 +0000
Subject: [PATCH 6/6] bgp: Add SRv6 Prefix SID attribute and service TLV
 support

Add BA_PREFIX_SID (0x28) attribute with full Prefix SID TLV handling:
validation, get/set for SRv6 VPN SID (Type 4) and SRv6 L3 Service
(Type 5, RFC 9252) TLVs, including SID transposition via Data SID
Structure sub-TLV.

Extend bgp_apply_mpls_labels() for SRv6 SID extraction via Prefix SID.
Extend bgp_encode_mpls_labels() with SRv6 L3/L2 Service TLV detection
to activate the raw label path. Extend bgp_decode_mpls_labels() for
SRv6 single-label exception.

Update all bgp_encode_nlri_*() signatures and bgp_encode_nlri() wrapper
to pass eattrs. Generate SRv6 Prefix SID in bgp_update_attrs().

SRv6 routes on labeled BGP channels (SAFI 128) carry SIDs in the
Prefix SID attribute instead of MPLS labels, so the NLRI label field
is set to implicit null per RFC 9252 Section 5. Non-SRv6 routes without
labels are still rejected.
---
 doc/bird.sgml       |  16 +-
 proto/bgp/attrs.c   | 594 ++++++++++++++++++++++++++++++++++++++++++++
 proto/bgp/bgp.c     |   2 +
 proto/bgp/bgp.h     |  22 +-
 proto/bgp/packets.c | 105 ++++++--
 5 files changed, 706 insertions(+), 33 deletions(-)

diff --git a/doc/bird.sgml b/doc/bird.sgml
index 32c770b..cc1b425 100644
--- a/doc/bird.sgml
+++ b/doc/bird.sgml
@@ -2868,14 +2868,16 @@ 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="8277"> &ndash; Using BGP to Bind MPLS Labels to Address Prefixes
 <item> <rfc id="8654"> &ndash; Extended Message Support for BGP
+<item> <rfc id="8669"> &ndash; Segment Routing Prefix Segment Identifier Extensions for BGP [partial]
 <item> <rfc id="8950"> &ndash; Advertising IPv4 NLRI with an IPv6 Next Hop
 <item> <rfc id="9003"> &ndash; Extended BGP Administrative Shutdown Communication
 <item> <rfc id="9072"> &ndash; Extended Optional Parameters Length for BGP OPEN Message
 <item> <rfc id="9117"> &ndash; Revised Validation Procedure for BGP Flow Specifications
 <item> <rfc id="9234"> &ndash; Route Leak Prevention and Detection Using Roles
+<item> <rfc id="9252"> &ndash; BGP Overlay Services Based on Segment Routing over IPv6 (SRv6) [partial]
 <item> <rfc id="9494"> &ndash; Long-Lived Graceful Restart for BGP
 <item> <rfc id="9687"> &ndash; Send Hold Timer
 </itemize>
@@ -3923,12 +3925,16 @@ be used in explicit configuration.
 	When set to <cf/no/ (or <cf/disabled/), the capability is not
 	advertised and only single labels are used.
 
-	Default: always.
+	Note: for routes carrying SRv6 transposition data (indicated by an
+	SRv6 L3/L2 Service TLV in the Prefix SID attribute), only a single
+	label entry is read regardless of this setting, because the BOS bit is
+	not meaningful for SRv6 transposition (<rfc id="9252">). 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.
+	If enabled, the BGP multiple labels capability (<rfc id="8277">) must
+	be announced by the BGP neighbor for the given address family,
+	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
diff --git a/proto/bgp/attrs.c b/proto/bgp/attrs.c
index acd3eb9..704903c 100644
--- a/proto/bgp/attrs.c
+++ b/proto/bgp/attrs.c
@@ -203,6 +203,449 @@ bgp_encode_raw(struct bgp_write_state *s UNUSED, eattr *a, byte *buf, uint size)
 }
 
 
+/*
+ * 	Prefix SID handling
+ */
+
+static int
+bgp_prefix_sid_valid(byte *data, uint len, char *err, uint elen)
+{
+  byte *pos = data;
+  char *err_dsc = NULL;
+  uint err_val = 0;
+
+#define BAD(DSC,VAL) ({ err_dsc = DSC; err_val = VAL; goto bad; })
+  while (len)
+  {
+    if (len < 1 + 2)
+      BAD("TLV framing error", len);
+
+    /* Process one TLV */
+    uint ptype = get_u8(pos);
+    ADVANCE(pos, len, 1);
+    uint plen = get_u16(pos);
+    ADVANCE(pos, len, 2);
+
+    if (len < plen)
+      BAD("TLV framing error", plen);
+
+    if (0) {
+    } else if (ptype == BGP_PREFIX_SID_SRV6_VPN_SID) {
+      if ((plen - 1) % (1 + 1 + 16) != 0)
+        BAD("Incorrect SRv6-VPN SID TLV length", plen);
+    } else if (ptype == BGP_PREFIX_SID_SRV6_L3_SERVICE) {
+      if (plen < 1)
+        BAD("Incorrect SRv6 L3 Service TLV length", plen);
+
+      byte *spos = pos;
+      uint slen = plen;
+
+      /* skip fixed data */
+      ADVANCE(spos, slen, 1);
+
+      while (slen)
+      {
+        if (slen < 1 + 2)
+          BAD("Sub-TLV framing error", slen);
+
+        /* Process one Sub-TLV */
+        uint pstype = get_u8(spos);
+        ADVANCE(spos, slen, 1);
+        uint pslen = get_u16(spos);
+        ADVANCE(spos, slen, 2);
+
+        if (slen < pslen)
+          BAD("Sub-TLV framing error", slen);
+
+        if (0) {
+        } else if (pstype == BGP_PREFIX_SID_SRV6_SERVICE_SID_INFORMATION) {
+          if (pslen < (1 + 16 + 1 + 2 + 1))
+            BAD("Incorrect SID Information Sub-TLV length", pslen);
+
+          byte *sspos = spos;
+          uint sslen = pslen;
+
+          /* skip fixed data */
+          ADVANCE(sspos, sslen, 1 + 16 + 1 + 2 + 1);
+
+          while (sslen)
+          {
+            if (sslen < 1 + 2)
+              BAD("Sub-Sub-TLV framing error", sslen);
+
+            /* Process one Sub-Sub-TLV */
+            uint psstype = get_u8(sspos);
+            ADVANCE(sspos, sslen, 1);
+            uint psslen = get_u16(sspos);
+            ADVANCE(sspos, sslen, 2);
+
+            if (sslen < psslen)
+              BAD("Sub-Sub-TLV framing error", sslen);
+
+            if (0) {
+            } else if (psstype == BGP_PREFIX_SID_SRV6_SERVICE_DATA_SID_STRUCTURE) {
+              if (psslen != 1 + 1 + 1 + 1 + 1 + 1)
+                BAD("Incorrect SID Structure Sub-Sub-TLV length", psslen);
+            }
+
+            ADVANCE(sspos, sslen, psslen);
+          }
+        }
+
+        ADVANCE(spos, slen, pslen);
+      }
+    }
+
+    ADVANCE(pos, len, plen);
+  }
+#undef BAD
+
+  return 1;
+
+bad:
+  if (err)
+    if (bsnprintf(err, elen, "%s (%u) at %d", err_dsc, err_val, (int) (pos - data)) < 0)
+      err[0] = 0;
+
+  return 0;
+}
+
+/* Get TLV for BA_PREFIX_SID */
+const byte *
+bgp_prefix_sid_get_tlv(const struct adata *ad, uint type)
+{
+  if (!ad)
+    return NULL;
+
+  uint len = ad->length;
+  const byte *pos = ad->data;
+
+  while (len)
+  {
+    uint ptype = get_u8(pos);
+    uint plen = get_u16(pos + 1);
+
+    if (ptype == type)
+      return pos;
+
+    ADVANCE(pos, len, 1 + 2 + plen);
+  }
+
+  return NULL;
+}
+
+/* Get Sub-TLV for BA_PREFIX_SID -> BGP_PREFIX_SID_SRV6_L3_SERVICE; BGP_PREFIX_SID_SRV6_L2_SERVICE */
+static const byte *
+bgp_prefix_sid_srv6_service_get_subtlv(const byte *b, uint len, uint type)
+{
+  if (!b)
+    return NULL;
+
+  while (len)
+  {
+    uint stype = get_u8(b);
+    uint slen = get_u16(b + 1);
+
+    if (stype == type)
+      return b;
+
+    ADVANCE(b, len, 1 + 2 + slen);
+  }
+
+  return NULL;
+}
+
+/* Get SRv6 SID from BA_PREFIX_SID -> BGP_PREFIX_SID_SRV6_VPN_SID TLV */
+static int
+bgp_prefix_sid_get_srv6_vpn_sid(const struct adata *ad, u8 *type, u8 *flag, ip6_addr *sid)
+{
+  const byte *b;
+  uint len;
+
+  if (!(b = bgp_prefix_sid_get_tlv(ad, BGP_PREFIX_SID_SRV6_VPN_SID)))
+    return 0;
+
+  len = get_u16(b + 1);
+
+  b += 1 + 2;
+
+  if (type)
+    *type = b[1 + (1 + 1 + 16) * 0 + 0];
+  if (flag)
+    *flag = b[1 + (1 + 1 + 16) * 0 + 1];
+  if (sid)
+    *sid = get_ip6(&b[1 + (1 + 1 + 16) * 0 + 2]);
+
+  return 1;
+}
+
+/*
+ * RFC 9252 SID transposition: splice the top @len bits from the 24-bit raw
+ * MPLS label @raw_label into @sid at bit offset @off (0 = MSB of the 128-bit
+ * SID).
+ *
+ * The SID advertised in the SID Information sub-TLV carries zeros in the
+ * transposition window.  The actual bits come from the MPLS label field
+ * of the NLRI and are inserted here to reconstruct the full SID.
+ */
+static inline void
+bgp_prefix_sid_transpose(ip6_addr *sid, uint off, uint len, u32 raw_label)
+{
+  u32  bits = (raw_label >> (24 - len)) & ((1u << len) - 1);    /* top @len bits extracted from 24-bit raw label */
+  uint word = off / 32;                                         /* index into sid->addr[] (32-bit words) */
+  uint bit  = off % 32;                                         /* bit position within that word */
+  u32  mask = (1u << len) - 1;                                  /* @len-bit mask for clearing and inserting */
+
+  if (bit + len <= 32)
+  {
+    /* Transposed bits fit within a single 32-bit word */
+    uint shift = 32 - bit - len;
+    sid->addr[word] = (sid->addr[word] & ~(mask << shift)) | (bits << shift);
+  }
+  else
+  {
+    /* Transposed bits span two consecutive 32-bit words */
+    uint lo_len = bit + len - 32;
+    sid->addr[word]     = (sid->addr[word]     & ~(mask >> lo_len))        | (bits >> lo_len);
+    sid->addr[word + 1] = (sid->addr[word + 1] & ~(mask << (32 - lo_len))) | (bits << (32 - lo_len));
+  }
+}
+
+/* Get SRv6 SID from BA_PREFIX_SID -> BGP_PREFIX_SID_SRV6_L3_SERVICE TLV
+ * Returns:
+ *   1: Success
+ *   0: Type 5 TLV or SID Information sub-TLV not present (caller should try fallback)
+ *  -1: Type 5 present but invalid (error, no fallback)
+ */
+static int
+bgp_prefix_sid_get_srv6_l3_service_sid(const struct adata *ad, struct ea_list *attrs, ip6_addr *sid)
+{
+  const byte *b;
+
+  if (!(b = bgp_prefix_sid_get_tlv(ad, BGP_PREFIX_SID_SRV6_L3_SERVICE)))
+    return 0;
+
+  uint len = get_u16(b + 1);
+
+  b += 1 + 2;
+
+  /* Reserved */
+  b += 1;
+  len -= 1;
+
+  const byte *sb;
+
+  if (!(sb = bgp_prefix_sid_srv6_service_get_subtlv(b, len, BGP_PREFIX_SID_SRV6_SERVICE_SID_INFORMATION)))
+    return 0;
+
+  uint slen = get_u16(sb + 1);
+
+  sb += 1 + 2;
+
+  ip6_addr tmp_sid = get_ip6(sb + 1);
+
+  sb += 1 + 16 + 1 + 2 + 1;
+  slen -= 1 + 16 + 1 + 2 + 1;
+
+  const byte *ssb;
+
+  if ((ssb = bgp_prefix_sid_srv6_service_get_subtlv(sb, slen, BGP_PREFIX_SID_SRV6_SERVICE_DATA_SID_STRUCTURE))) {
+    uint sslen = get_u16(ssb + 1);
+
+    ssb += 1 + 2;
+
+    uint trans_len = get_u8(ssb + 4);
+    uint trans_off = get_u8(ssb + 5);
+
+    if (trans_len > 24 || trans_off + trans_len > 128)
+      return -1;
+
+    if (trans_len) {
+      eattr *mea;
+
+      if (!(mea = bgp_find_attr(attrs, BA_RAW_MPLS_LABEL_STACK)))
+        return -1;
+
+      const struct adata *mad = mea->u.ptr;
+
+      if (mad->length < 4)
+        return -1;
+
+      /* First raw label stack entry is a 24-bit MPLS wire value in a u32 */
+      u32 raw_label;
+      memcpy(&raw_label, mad->data, 4);
+
+      bgp_prefix_sid_transpose(&tmp_sid, trans_off, trans_len, raw_label);
+    }
+  }
+
+  *sid = tmp_sid;
+
+  return 1;
+}
+
+/* Set TLV to BA_PREFIX_SID */
+static const struct adata *
+bgp_prefix_sid_set_tlv(struct linpool *pool, const struct adata *ad, uint type, byte *data, uint dlen)
+{
+  uint len = ad ? ad->length : 0;
+  const byte *pos = ad ? ad->data : NULL;
+  struct adata *res = lp_alloc_adata(pool, len + 3 + dlen);
+  byte *dst = res->data;
+  byte *tlv = NULL;
+  int del = 0;
+
+  while (len)
+  {
+    uint ptype = get_u8(pos);
+    uint plen = get_u16(pos + 1);
+
+    /* Find position for new TLV */
+    if ((ptype >= type) && !tlv)
+    {
+      tlv = dst;
+      dst += 1 + 2 + dlen;
+    }
+
+    /* Skip first matching TLV, copy others */
+    if ((ptype == type) && !del)
+      del = 1;
+    else
+    {
+      memcpy(dst, pos, 1 + 2 + plen);
+      dst += 1 + 2 + plen;
+    }
+
+    ADVANCE(pos, len, 1 + 2 + plen);
+  }
+
+  if (!tlv)
+  {
+    tlv = dst;
+    dst += 1 + 2 + dlen;
+  }
+
+  /* Store the TLV */
+  put_u8(tlv + 0, type);
+  put_u16(tlv + 1, dlen);
+  memcpy(tlv + 1 + 2, data, dlen);
+
+  /* Update length */
+  res->length = dst - res->data;
+
+  return res;
+}
+
+/* Set Sub-TLV to BA_PREFIX_SID -> BGP_PREFIX_SID_SRV6_L3_SERVICE; BGP_PREFIX_SID_SRV6_L2_SERVICE */
+static uint
+bgp_prefix_sid_srv6_service_set_subtlv(byte *pos, uint type, const byte *data, uint dlen)
+{
+  put_u8(pos, type);
+  put_u16(pos + 1, dlen);
+  memcpy(pos + 1 + 2, data, dlen);
+  return 1 + 2 + dlen;
+}
+
+/* Set SRv6 SID to BA_PREFIX_SID -> BGP_PREFIX_SID_SRV6_VPN_SID TLV */
+static const struct adata *
+bgp_prefix_sid_set_srv6_vpn_sid(struct linpool *pool, const struct adata *ad, u8 type, u8 flag, ip6_addr sid)
+{
+  byte data[1 + 1 + 1 + 16];
+
+  put_u8(data + 0, 0);
+
+  put_u8(data + 1, type);
+  put_u8(data + 2, flag);
+  put_ip6(data + 3, sid);
+
+  return bgp_prefix_sid_set_tlv(pool, ad, BGP_PREFIX_SID_SRV6_VPN_SID, data, sizeof(data));
+}
+
+/* Set SRv6 SID to BA_PREFIX_SID -> BGP_PREFIX_SID_SRV6_L3_SERVICE TLV */
+static const struct adata *
+bgp_prefix_sid_set_srv6_l3_service_sid(struct linpool *pool, const struct adata *ad, ip6_addr sid)
+{
+  /* RFC 9252 Type 5 TLV: SRv6 L3 Service */
+  byte data[1 + (1 + 2 + (1 + 16 + 1 + 2 + 1))];
+  byte *pos = data;
+
+  /* Outer TLV RESERVED byte (RFC 9252 Section 2) */
+  put_u8(pos, 0);
+  pos += 1;
+
+  /* Prepare SRv6 SID Information Sub-TLV data */
+  byte sid_info_data[1 + 16 + 1 + 2 + 1];
+  byte *spos = sid_info_data;
+
+  /* Sub-TLV RESERVED1 */
+  put_u8(spos, 0);
+  spos += 1;
+
+  /* SRv6 SID Value (16 bytes) */
+  put_ip6(spos, sid);
+  spos += 16;
+
+  /* SID Flags (no flags set) */
+  put_u8(spos, 0);
+  spos += 1;
+
+  /* Endpoint Behavior (0xFFFF = opaque/abstract) */
+  put_u16(spos, 0xFFFF);
+  spos += 2;
+
+  /* RESERVED2 */
+  put_u8(spos, 0);
+  spos += 1;
+
+  /* Write Sub-TLV: SRv6 SID Information (Type 1) */
+  pos += bgp_prefix_sid_srv6_service_set_subtlv(pos, BGP_PREFIX_SID_SRV6_SERVICE_SID_INFORMATION, sid_info_data, sizeof(sid_info_data));
+
+  /* No Sub-Sub-TLV (SID Structure) in this implementation */
+
+  return bgp_prefix_sid_set_tlv(pool, ad, BGP_PREFIX_SID_SRV6_L3_SERVICE, data, sizeof(data));
+}
+
+/* Get SRv6 SID for L3VPN from BA_PREFIX_SID (either BGP_PREFIX_SID_SRV6_L3_SERVICE or BGP_PREFIX_SID_SRV6_VPN_SID TLVs) */
+int
+bgp_prefix_sid_get_srv6_l3vpn_sid(struct ea_list *attrs, ip6_addr *sid)
+{
+  eattr *ea;
+
+  if (!(ea = bgp_find_attr(attrs, BA_PREFIX_SID)))
+    return 0;
+
+  /* Try BGP_PREFIX_SID_SRV6_L3_SERVICE */
+  int result = bgp_prefix_sid_get_srv6_l3_service_sid(ea->u.ptr, attrs, sid);
+  if (result != 0)
+    return result > 0 ? 1 : 0;
+
+  /* Try BGP_PREFIX_SID_SRV6_VPN_L3 */
+  u8 srv6_vpn_sid_type;
+  ip6_addr vpn_sid;
+  if (bgp_prefix_sid_get_srv6_vpn_sid(ea->u.ptr, &srv6_vpn_sid_type, NULL, &vpn_sid)) {
+    if (srv6_vpn_sid_type == BGP_PREFIX_SID_SRV6_VPN_L3) {
+      *sid = vpn_sid;
+
+      return 1;
+    }
+  }
+
+  return 0;
+}
+
+/* Set SRv6 SID for L3VPN to BA_PREFIX_SID (either BGP_PREFIX_SID_SRV6_L3_SERVICE or BGP_PREFIX_SID_SRV6_VPN_SID TLVs) */
+static const struct adata *
+bgp_prefix_sid_set_srv6_l3vpn_sid(struct linpool *pool, const struct adata *ad, ip6_addr sid)
+{
+#if 1
+  /* RFC 9252 Type 5 TLV */
+  return bgp_prefix_sid_set_srv6_l3_service_sid(pool, ad, sid);
+#else
+  /* Old Type 4 TLV */
+  return bgp_prefix_sid_set_srv6_vpn_sid(pool, ad, BGP_PREFIX_SID_SRV6_VPN_L3, 0x00, sid);
+#endif
+}
+
 /*
  *	AIGP handling
  */
@@ -916,6 +1359,139 @@ bgp_decode_otc(struct bgp_parse_state *s, uint code UNUSED, uint flags, byte *da
 }
 
 
+static int
+bgp_encode_prefix_sid(struct bgp_write_state *s, eattr *a, byte *buf, uint size)
+{
+  return bgp_encode_raw(s, a, buf, size);
+  bgp_put_attr(buf, size, BA_PREFIX_SID, a->flags, a->u.ptr->data, a->u.ptr->length);
+}
+
+static void
+bgp_decode_prefix_sid(struct bgp_parse_state *s, uint code UNUSED, uint flags, byte *data, uint len, ea_list **to)
+{
+  char err[128];
+
+  if (!bgp_prefix_sid_valid(data, len, err, sizeof(err)))
+    DISCARD("Malformed Prefix SID attribute - %s", err);
+
+  bgp_set_attr_data(to, s->pool, BA_PREFIX_SID, flags, data, len);
+}
+
+static void
+bgp_format_prefix_sid(const eattr *a, byte *buf, uint size UNUSED)
+{
+  const byte *b;
+
+  *buf = '\0';
+
+  if ((b = bgp_prefix_sid_get_tlv(a->u.ptr, BGP_PREFIX_SID_SRV6_VPN_SID))) {
+    uint tlen = get_u16(b + 1);
+
+    b += 3;
+
+    if (*buf)
+      buf += bsprintf(buf, ", ");
+    buf += bsprintf(buf, "SRv6-VPN SID ");
+    if (tlen == 0 || tlen == 1) {
+      buf += bsprintf(buf, "<>");
+    } else {
+      buf += bsprintf(buf, "<");
+      for (uint i = 0; i < (tlen - 1) / (1 + 1 + 16); ++i) {
+        u8 t = get_u8(&b[1 + (1 + 1 + 16) * i + 0]);
+        u8 f = get_u8(&b[1 + (1 + 1 + 16) * i + 1]);
+        ip6_addr ip6 = get_ip6(&b[1 + (1 + 1 + 16) * i + 2]);
+
+        if (i > 0)
+          buf += bsprintf(buf, ", ");
+
+        switch (t) {
+          case BGP_PREFIX_SID_SRV6_VPN_L3:
+            buf += bsprintf(buf, "L3 ");
+            break;
+          case BGP_PREFIX_SID_SRV6_VPN_L2:
+            buf += bsprintf(buf, "L2 ");
+            break;
+          default:
+            buf += bsprintf(buf, "T%u ", (uint)t);
+            break;
+        }
+        buf += bsprintf(buf, "0x%02x %I6",
+          (uint)f, ip6);
+      }
+      buf += bsprintf(buf, ">");
+    }
+  }
+
+  if ((b = bgp_prefix_sid_get_tlv(a->u.ptr, BGP_PREFIX_SID_SRV6_L3_SERVICE))) {
+    uint tlen = get_u16(b + 1);
+
+    b += 3;
+
+    if (*buf)
+      buf += bsprintf(buf, ", ");
+    buf += bsprintf(buf, "SRv6 L3 Service ");
+
+    const byte *spos = b;
+    uint slen = tlen;
+
+    /* skip fixed data */
+    ADVANCE(spos, slen, 1);
+
+    while (slen)
+    {
+      /* Process one Sub-TLV */
+      uint pstype = get_u8(spos);
+      ADVANCE(spos, slen, 1);
+      uint pslen = get_u16(spos);
+      ADVANCE(spos, slen, 2);
+
+      if (0) {
+      } else if (pstype == BGP_PREFIX_SID_SRV6_SERVICE_SID_INFORMATION) {
+        const byte *sspos = spos;
+        uint sslen = pslen;
+
+        ip6_addr ip6 = get_ip6(&spos[1]);
+        u8 flags = get_u8(&spos[1 + 16]);
+        u16 behavior = get_u16(&spos[1 + 16 + 1]);
+
+        buf += bsprintf(buf, "SID=%I6, Flags=0x%02x, Behavior=0x%04x",
+          ip6, (uint)flags, (uint)behavior);
+
+        /* skip fixed data */
+        ADVANCE(sspos, sslen, 1 + 16 + 1 + 2 + 1);
+
+        while (sslen)
+        {
+          /* Process one Sub-Sub-TLV */
+          uint psstype = get_u8(sspos);
+          ADVANCE(sspos, sslen, 1);
+          uint psslen = get_u16(sspos);
+          ADVANCE(sspos, sslen, 2);
+
+          if (0) {
+          } else if (psstype == BGP_PREFIX_SID_SRV6_SERVICE_DATA_SID_STRUCTURE) {
+            uint loc_block_len = get_u8(&sspos[0]);
+            uint loc_node_len  = get_u8(&sspos[1]);
+            uint func_len      = get_u8(&sspos[2]);
+            uint arg_len       = get_u8(&sspos[3]);
+            uint trans_len     = get_u8(&sspos[4]);
+            uint trans_off     = get_u8(&sspos[5]);
+
+            buf += bsprintf(buf, ", Structure=block:%u,node:%u,func:%u,args:%u, Transposition=off:%u,len:%u",
+              loc_block_len, loc_node_len, func_len, arg_len,
+              trans_off, trans_len);
+          }
+
+          ADVANCE(sspos, sslen, psslen);
+        }
+      }
+
+      ADVANCE(spos, slen, pslen);
+    }
+  }
+}
+
+
 static void
 bgp_export_mpls_label_stack(struct bgp_export_state *s, eattr *a)
 {
@@ -1210,6 +1786,14 @@ static const struct bgp_attr_desc bgp_attr_table[] = {
     .encode = bgp_encode_u32,
     .decode = bgp_decode_otc,
   },
+  [BA_PREFIX_SID] = {
+    .name = "prefix_sid",
+    .type = EAF_TYPE_OPAQUE,
+    .flags = BAF_OPTIONAL | BAF_TRANSITIVE,
+    .encode = bgp_encode_prefix_sid,
+    .decode = bgp_decode_prefix_sid,
+    .format = bgp_format_prefix_sid,
+  },
   [BA_RAW_MPLS_LABEL_STACK] = {
     .name = "raw_mpls_label_stack",
     .type = EAF_TYPE_INT_SET,
@@ -1943,6 +2527,16 @@ bgp_update_attrs(struct bgp_proto *p, struct bgp_channel *c, rte *e, ea_list *at
   a = bgp_find_attr(attrs0, BA_NEXT_HOP);
   bgp_update_next_hop(&s, a, &attrs);
 
+  /* PREFIX_SID attribute */
+  if (! bgp_find_attr(attrs0, BA_PREFIX_SID)) {
+    rta *ra = e->attrs;
+    if (ra->nh.sid6s)
+    {
+      ad = bgp_prefix_sid_set_srv6_l3vpn_sid(pool, NULL, nexthop_sid6(&ra->nh)[0]);
+      bgp_set_attr_ptr(&attrs, pool, BA_PREFIX_SID, 0, ad);
+    }
+  }
+
   /* LOCAL_PREF attribute - required for IBGP, attach if missing */
   if (p->is_interior && ! bgp_find_attr(attrs0, BA_LOCAL_PREF))
     bgp_set_attr_u32(&attrs, pool, BA_LOCAL_PREF, 0, p->cf->default_local_pref);
diff --git a/proto/bgp/bgp.c b/proto/bgp/bgp.c
index eb60cb6..7225681 100644
--- a/proto/bgp/bgp.c
+++ b/proto/bgp/bgp.c
@@ -100,6 +100,7 @@
  * 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 8669 - Segment Routing Prefix Segment Identifier Extensions for BGP (partial)
  * RFC 8950 - Advertising IPv4 NLRI with an IPv6 Next Hop
  * RFC 8955 - Dissemination of Flow Specification Rules
  * RFC 8956 - Dissemination of Flow Specification Rules for IPv6
@@ -107,6 +108,7 @@
  * RFC 9072 - Extended Optional Parameters Length for BGP OPEN Message
  * RFC 9117 - Revised Validation Procedure for BGP Flow Specifications
  * RFC 9234 - Route Leak Prevention and Detection Using Roles
+ * RFC 9252 - BGP Overlay Services Based on Segment Routing over IPv6 (partial)
  * RFC 9494 - Long-Lived Graceful Restart for BGP
  * RFC 9687 - Send Hold Timer
  * draft-walton-bgp-hostname-capability-02
diff --git a/proto/bgp/bgp.h b/proto/bgp/bgp.h
index 96286bc..6551589 100644
--- a/proto/bgp/bgp.h
+++ b/proto/bgp/bgp.h
@@ -66,7 +66,7 @@ struct bgp_af_desc {
   u8 mpls;
   u8 no_igp;
   const char *name;
-  uint (*encode_nlri)(struct bgp_write_state *s, struct bgp_bucket *buck, byte *buf, uint size);
+  uint (*encode_nlri)(struct bgp_write_state *s, struct bgp_bucket *buck, ea_list *eattrs, byte *buf, uint size);
   void (*decode_nlri)(struct bgp_parse_state *s, byte *pos, uint len, rta *a);
   void (*update_next_hop)(struct bgp_export_state *s, eattr *nh, ea_list **to);
   uint (*encode_next_hop)(struct bgp_write_state *s, eattr *nh, byte *buf, uint size);
@@ -732,6 +732,25 @@ bgp_total_aigp_metric(rte *r)
   return metric;
 }
 
+/* TLVs for BA_PREFIX_SID */
+#define BGP_PREFIX_SID_LABEL_INDEX         1
+#define BGP_PREFIX_SID_ORIGINATOR_SRGB     3
+#define BGP_PREFIX_SID_SRV6_VPN_SID        4 /* DEPRECATED */
+#define BGP_PREFIX_SID_SRV6_L3_SERVICE     5
+#define BGP_PREFIX_SID_SRV6_L2_SERVICE     6
+
+/* Type for BGP_PREFIX_SID_SRV6_VPN_SID */
+#define BGP_PREFIX_SID_SRV6_VPN_L3     1
+#define BGP_PREFIX_SID_SRV6_VPN_L2     2
+
+/* Sub-TLVs for BGP_PREFIX_SID_SRV6_L3_SERVICE; BGP_PREFIX_SID_SRV6_L2_SERVICE */
+#define BGP_PREFIX_SID_SRV6_SERVICE_SID_INFORMATION     1
+
+/* Sub-Sub-TLVs for BGP_PREFIX_SID_SRV6_L3_SERVICE; BGP_PREFIX_SID_SRV6_L2_SERVICE */
+#define BGP_PREFIX_SID_SRV6_SERVICE_DATA_SID_STRUCTURE     1
+
+const byte * bgp_prefix_sid_get_tlv(const struct adata *ad, uint type);
+int bgp_prefix_sid_get_srv6_l3vpn_sid(struct ea_list *attrs, ip6_addr *sid);
 
 /* packets.c */
 
@@ -788,6 +807,7 @@ byte *bgp_create_end_mark_(struct bgp_channel *c, byte *buf);
 #define BA_AIGP			0x1a	/* RFC 7311 */
 #define BA_LARGE_COMMUNITY	0x20	/* RFC 8092 */
 #define BA_ONLY_TO_CUSTOMER	0x23	/* RFC 9234 */
+#define BA_PREFIX_SID           0x28	/* RFC 8669 */
 
 /* Bird's private internal BGP attributes */
 #define BA_RAW_MPLS_LABEL_STACK	0xfd	/* raw MPLS label stack transfer attribute */
diff --git a/proto/bgp/packets.c b/proto/bgp/packets.c
index 2329bb3..bcf10bb 100644
--- a/proto/bgp/packets.c
+++ b/proto/bgp/packets.c
@@ -1161,10 +1161,10 @@ bgp_apply_next_hop(struct bgp_parse_state *s, rta *a, ip_addr gw, ip_addr ll)
     ip_addr lla = (c->cf->next_hop_prefer == NHP_LOCAL) ? ll : IPA_NONE;
     s->hostentry = rt_get_hostentry(tab, gw, lla, c->c.table);
 
-    if (!s->mpls)
-      rta_apply_hostentry(a, s->hostentry, NULL);
+    if (!s->mpls) /* !mpls && !sid6 */
+      rta_apply_hostentry(a, s->hostentry, NULL, NULL);
 
-    /* With MPLS, hostentry is applied later in bgp_apply_mpls_labels() */
+    /* With MPLS and SRv6, hostentry is applied later in bgp_apply_mpls_labels() */
   }
 }
 
@@ -1187,16 +1187,31 @@ bgp_apply_mpls_labels(struct bgp_parse_state *s, rta *a, u32 *labels, uint lnum)
 
   if (s->channel->cf->gw_mode == GW_DIRECT)
   {
-    a->nh.labels = lnum;
-    memcpy(a->nh.label, labels, 4*lnum);
+    a->nh.labels = 0;
+    a->nh.sid6s = 0;
+    if (bgp_prefix_sid_get_srv6_l3vpn_sid(a->eattrs, &nexthop_sid6(&a->nh)[0])) {
+      a->nh.sid6s = 1;
+    } else {
+      a->nh.labels = lnum;
+      memcpy(a->nh.label, labels, 4*lnum);
+      a->nh.sid6s = 0;
+    }
   }
   else /* GW_RECURSIVE */
   {
     mpls_label_stack ms;
+    srv6_sid_stack srv6s;
+
+    if (bgp_prefix_sid_get_srv6_l3vpn_sid(a->eattrs, &srv6s.sid[0])) {
+      ms.len = 0;
+      srv6s.len = 1;
+    } else {
+      ms.len = lnum;
+      memcpy(ms.stack, labels, 4*lnum);
+      srv6s.len = 0;
+    }
 
-    ms.len = lnum;
-    memcpy(ms.stack, labels, 4*lnum);
-    rta_apply_hostentry(a, s->hostentry, &ms);
+    rta_apply_hostentry(a, s->hostentry, &ms, srv6s.len > 0 ? &srv6s : NULL);
   }
 }
 
@@ -1357,7 +1372,17 @@ bgp_update_next_hop_ip(struct bgp_export_state *s, eattr *a, ea_list **to)
 
   /* Just check if MPLS stack */
   if (s->mpls && !bgp_find_attr(*to, BA_MPLS_LABEL_STACK))
-    REJECT(NO_LABEL_STACK);
+  {
+    /* SRv6 routes carry SIDs instead of MPLS labels; insert implicit null
+     * as placeholder for the NLRI label field (RFC 9252 Section 5) */
+    if (s->route->attrs->nh.sid6s || bgp_find_attr(*to, BA_PREFIX_SID))
+    {
+      u32 implicit_null = BGP_MPLS_NULL;
+      bgp_set_attr_data(to, s->pool, BA_MPLS_LABEL_STACK, 0, &implicit_null, 4);
+    }
+    else
+      REJECT(NO_LABEL_STACK);
+  }
 }
 
 static uint
@@ -1637,7 +1662,7 @@ 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, const adata *mpls, const adata *raw_mpls, byte **pos, uint *size, byte *pxlen)
+bgp_encode_mpls_labels(struct bgp_write_state *s, ea_list *eattrs, const adata *mpls, const adata *raw_mpls, byte **pos, uint *size, byte *pxlen)
 {
   struct bgp_channel *c = s->channel;
   const u32 dummy = 0;
@@ -1648,6 +1673,15 @@ bgp_encode_mpls_labels(struct bgp_write_state *s, const adata *mpls, const adata
   uint num;
   int raw = 0;
 
+  /* if TLV SRV6 L3/L2 Service exist, we need to send raw labels */
+  eattr *psid_ea = eattrs ? bgp_find_attr(eattrs, BA_PREFIX_SID) : NULL;
+  if (psid_ea)
+  {
+    if (bgp_prefix_sid_get_tlv(psid_ea->u.ptr, BGP_PREFIX_SID_SRV6_L3_SERVICE) != NULL ||
+        bgp_prefix_sid_get_tlv(psid_ea->u.ptr, BGP_PREFIX_SID_SRV6_L2_SERVICE) != NULL)
+      raw = 1;
+  }
+
   if (raw)
   {
     /* raw labels */
@@ -1699,6 +1733,23 @@ bgp_decode_mpls_labels(struct bgp_parse_state *s, byte **pos, uint *len, uint *p
   struct bgp_channel *c = s->channel;
   u32 labels[BGP_MPLS_MAX], raw_labels[BGP_MPLS_MAX], label;
   uint lnum = 0, rlnum = 0;
+  int srv6 = 0;
+
+  /* Read multiple labels (BOS-delimited) when:
+   *  - Multiple labels capability is negotiated (strict RFC 8277), or
+   *  - Config is ALWAYS (RFC 3107 compatible, reads until BOS regardless)
+   *
+   * Exception: with SRv6 transposition (RFC 9252), the NLRI label field
+   * carries raw SID bits where the BOS bit is meaningless and may not be set.
+   * In that case we read exactly one entry. */
+  if (a)
+  {
+    eattr *psid_ea = bgp_find_attr(a->eattrs, BA_PREFIX_SID);
+    if (psid_ea &&
+	(bgp_prefix_sid_get_tlv(psid_ea->u.ptr, BGP_PREFIX_SID_SRV6_L3_SERVICE) ||
+	 bgp_prefix_sid_get_tlv(psid_ea->u.ptr, BGP_PREFIX_SID_SRV6_L2_SERVICE)))
+      srv6 = 1;
+  }
 
   do {
     if (*pxlen < 24)
@@ -1718,7 +1769,7 @@ bgp_decode_mpls_labels(struct bgp_parse_state *s, byte **pos, uint *len, uint *p
     if (!s->reach_nlri_step)
       return;
   }
-  while ((c->multiple_labels || c->cf->multiple_labels >= BGP_MPLS_ML_ADVERTISE) && !(label & BGP_MPLS_BOS));
+  while ((c->multiple_labels || c->cf->multiple_labels >= BGP_MPLS_ML_ADVERTISE) && !srv6 && !(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)
@@ -1756,7 +1807,7 @@ bgp_decode_mpls_labels(struct bgp_parse_state *s, byte **pos, uint *len, uint *p
 }
 
 static uint
-bgp_encode_nlri_ip4(struct bgp_write_state *s, struct bgp_bucket *buck, byte *buf, uint size)
+bgp_encode_nlri_ip4(struct bgp_write_state *s, struct bgp_bucket *buck, ea_list *eattrs, byte *buf, uint size)
 {
   byte *pos = buf;
 
@@ -1778,7 +1829,7 @@ bgp_encode_nlri_ip4(struct bgp_write_state *s, struct bgp_bucket *buck, byte *bu
 
     /* Encode MPLS labels */
     if (s->mpls)
-      bgp_encode_mpls_labels(s, s->mpls_labels, s->raw_mpls_labels, &pos, &size, pos - 1);
+      bgp_encode_mpls_labels(s, eattrs, s->mpls_labels, s->raw_mpls_labels, &pos, &size, pos - 1);
 
     /* Encode prefix body */
     ip4_addr a = ip4_hton(net->prefix);
@@ -1844,7 +1895,7 @@ bgp_decode_nlri_ip4(struct bgp_parse_state *s, byte *pos, uint len, rta *a)
 
 
 static uint
-bgp_encode_nlri_ip6(struct bgp_write_state *s, struct bgp_bucket *buck, byte *buf, uint size)
+bgp_encode_nlri_ip6(struct bgp_write_state *s, struct bgp_bucket *buck, ea_list *eattrs, byte *buf, uint size)
 {
   byte *pos = buf;
 
@@ -1866,7 +1917,7 @@ bgp_encode_nlri_ip6(struct bgp_write_state *s, struct bgp_bucket *buck, byte *bu
 
     /* Encode MPLS labels */
     if (s->mpls)
-      bgp_encode_mpls_labels(s, s->mpls_labels, s->raw_mpls_labels, &pos, &size, pos - 1);
+      bgp_encode_mpls_labels(s, eattrs, s->mpls_labels, s->raw_mpls_labels, &pos, &size, pos - 1);
 
     /* Encode prefix body */
     ip6_addr a = ip6_hton(net->prefix);
@@ -1931,7 +1982,7 @@ bgp_decode_nlri_ip6(struct bgp_parse_state *s, byte *pos, uint len, rta *a)
 }
 
 static uint
-bgp_encode_nlri_vpn4(struct bgp_write_state *s, struct bgp_bucket *buck, byte *buf, uint size)
+bgp_encode_nlri_vpn4(struct bgp_write_state *s, struct bgp_bucket *buck, ea_list *eattrs, byte *buf, uint size)
 {
   byte *pos = buf;
 
@@ -1953,7 +2004,7 @@ bgp_encode_nlri_vpn4(struct bgp_write_state *s, struct bgp_bucket *buck, byte *b
 
     /* Encode MPLS labels */
     if (s->mpls)
-      bgp_encode_mpls_labels(s, s->mpls_labels, s->raw_mpls_labels, &pos, &size, pos - 1);
+      bgp_encode_mpls_labels(s, eattrs, s->mpls_labels, s->raw_mpls_labels, &pos, &size, pos - 1);
 
     /* Encode route distinguisher */
     put_rd(pos, net->rd);
@@ -2031,7 +2082,7 @@ bgp_decode_nlri_vpn4(struct bgp_parse_state *s, byte *pos, uint len, rta *a)
 
 
 static uint
-bgp_encode_nlri_vpn6(struct bgp_write_state *s, struct bgp_bucket *buck, byte *buf, uint size)
+bgp_encode_nlri_vpn6(struct bgp_write_state *s, struct bgp_bucket *buck, ea_list *eattrs, byte *buf, uint size)
 {
   byte *pos = buf;
 
@@ -2053,7 +2104,7 @@ bgp_encode_nlri_vpn6(struct bgp_write_state *s, struct bgp_bucket *buck, byte *b
 
     /* Encode MPLS labels */
     if (s->mpls)
-      bgp_encode_mpls_labels(s, s->mpls_labels, s->raw_mpls_labels, &pos, &size, pos - 1);
+      bgp_encode_mpls_labels(s, eattrs, s->mpls_labels, s->raw_mpls_labels, &pos, &size, pos - 1);
 
     /* Encode route distinguisher */
     put_rd(pos, net->rd);
@@ -2131,7 +2182,7 @@ bgp_decode_nlri_vpn6(struct bgp_parse_state *s, byte *pos, uint len, rta *a)
 
 
 static uint
-bgp_encode_nlri_flow4(struct bgp_write_state *s, struct bgp_bucket *buck, byte *buf, uint size)
+bgp_encode_nlri_flow4(struct bgp_write_state *s, struct bgp_bucket *buck, ea_list *eattrs, byte *buf, uint size)
 {
   byte *pos = buf;
 
@@ -2226,7 +2277,7 @@ bgp_decode_nlri_flow4(struct bgp_parse_state *s, byte *pos, uint len, rta *a)
 
 
 static uint
-bgp_encode_nlri_flow6(struct bgp_write_state *s, struct bgp_bucket *buck, byte *buf, uint size)
+bgp_encode_nlri_flow6(struct bgp_write_state *s, struct bgp_bucket *buck, ea_list *eattrs, byte *buf, uint size)
 {
   byte *pos = buf;
 
@@ -2461,9 +2512,9 @@ bgp_get_af_desc(u32 afi)
 }
 
 static inline uint
-bgp_encode_nlri(struct bgp_write_state *s, struct bgp_bucket *buck, byte *buf, byte *end)
+bgp_encode_nlri(struct bgp_write_state *s, struct bgp_bucket *buck, ea_list *eattrs, byte *buf, byte *end)
 {
-  return s->channel->desc->encode_nlri(s, buck, buf, end - buf);
+  return s->channel->desc->encode_nlri(s, buck, eattrs, buf, end - buf);
 }
 
 static inline uint
@@ -2505,7 +2556,7 @@ bgp_create_ip_reach(struct bgp_write_state *s, struct bgp_bucket *buck, byte *bu
   put_u16(buf+0, 0);
   put_u16(buf+2, la);
 
-  lr = bgp_encode_nlri(s, buck, buf+4+la, end);
+  lr = bgp_encode_nlri(s, buck, buck->eattrs, buf+4+la, end);
 
   return buf+4+la+lr;
 }
@@ -2559,7 +2610,7 @@ bgp_create_mp_reach(struct bgp_write_state *s, struct bgp_bucket *buck, byte *bu
   *pos++ = 0;
 
   /* Encode the NLRI */
-  lr = bgp_encode_nlri(s, buck, pos, end - la);
+  lr = bgp_encode_nlri(s, buck, buck->eattrs, pos, end - la);
   pos += lr;
 
   /* End of MP_REACH_NLRI atribute, update data length */
@@ -2589,7 +2640,7 @@ bgp_create_ip_unreach(struct bgp_write_state *s, struct bgp_bucket *buck, byte *
    *	---	IPv4 Network Layer Reachability Information (unused)
    */
 
-  uint len = bgp_encode_nlri(s, buck, buf+2, end);
+  uint len = bgp_encode_nlri(s, buck, NULL, buf+2, end);
 
   put_u16(buf+0, len);
   put_u16(buf+2+len, 0);
@@ -2613,7 +2664,7 @@ bgp_create_mp_unreach(struct bgp_write_state *s, struct bgp_bucket *buck, byte *
    *	---	IPv4 Network Layer Reachability Information (unused)
    */
 
-  uint len = bgp_encode_nlri(s, buck, buf+11, end);
+  uint len = bgp_encode_nlri(s, buck, NULL, buf+11, end);
 
   put_u16(buf+0, 0);
   put_u16(buf+2, 7+len);
-- 
2.47.3

