Hello Maria,

Thank you for this initial feedback, it is appreciated.

I have already read the contribution policy, and to be honest based on what I 
read I did not expect a comment about tests ("bird-tools.git [...] This 
repository is quite messy and you may need some help with it. We're planning to 
move the Netlab suite into the main git repository; after we do that, we'll 
require every contribution to add tests").
I haven't looked into BIRD 3 yet and I don't know much about it, just that is 
has threads :)
Both of those (adding tests and supporting BIRD 3) are probably a lot of work 
and unfortunately I can't promise I will be able to work on both...

I addressed some of your comments, please see attached patches (only netlink 
and bgp have updates):
- Use struct for SRH in netlink (from Linux kernel header).
- Renamed variables with double-underscore prefix.
- Moved SID transposition in its own function and added comments and 
intermediate variables.

As for the other comments:

> This is unacceptable at all, it adds an awful lot of bytes into a
> memory-constrained data structure, and you run into massive merging
> problems with BIRD 3. You need an EA for this, for sure.

Maybe I missed something or misunderstood what you mean, but I think EA are per 
routes, not per nexthop, so that would not work.


> All the changes in the hostentry logic are going to make a massive merge
> conflict not only with BIRD 3 but also with the (in-progress) igp filter
> feature expected for BIRD 3.3, and this is exactly why we ask
> contributors to check first with the core team before doing something
> big.

This patch set originates in 2019, I implemented SRv6 L3VPN support to BIRD 
because we needed it for our networks (and we still run it). At the time BIRD 
did not support the l3vpn protocol, so as you can imagine there were a number 
of kludges. Also BIRD 3 did not exists yet. End of last year I started to 
rebase and clean the implementation to remove the kludges and integrate with 
the l3vpn protocol. It was a great opportunity to share the patch set with the 
BIRD community, so I asked for feedback on the initial nest and static patches 
on the mailing list but I got no comment then 
(https://bird.network.cz/pipermail/bird-users/2025-September/018385.html and 
https://bird.network.cz/pipermail/bird-users/2025-October/018446.html).


> This _may_ need an autoconf guard but I have no idea how old this
> feature actually is.

SEG6 support was added into Linux 4.10 (February 19 2017).


Thank you for your hard work on the BIRD project.

--
Sébastien

________________________________________
De : Maria Matejka <[email protected]>
Envoyé : jeudi 12 février 2026 18:32
À : Sébastien PARISOT
Cc : BIRD Users
Objet : Re: [PATCH] SRv6 support for BIRD

Hello Sébastien,

thank you for contributing to BIRD. While we deeply appreciate your work, we’ll 
need you to do a little bit more to get into our review queue:

  *   please add also proper test setups to both demonstrate that the thing 
indeed works, but also to demonstrate how the thing is expected to be used.
  *   please check and prepare merging into BIRD 3
  *   just by the stats, the docs update looks a little bit sparse but we may 
change our mind after reading your patches

Having all these three will massively help not only accepting your patches, but 
also maintaining the feature long-term.

For more information, please check our Contribution policy: 
https://gitlab.nic.cz/labs/bird/-/blob/master/CONTRIBUTING.md

Some of these things may look hypocritical because e.g. we don’t have autotests 
for some already existing features and parts of BIRD, but we consider it a good 
practice to have tests, and we don’t want our technological debt to increase.

Also, please see following short notes on some things which caught my eye on a 
fast skim. No note means simply that it didn’t look suspicious on first glance. 
I’m pointing out things which are almost certainly obvious problems.

This patch (for master branch / 2.18) adds SRv6 L3VPN support to BIRD. It 
applies on top of the Multiple Labels capability (RFC 8277) patch I sent 
earlier.

Our requirements above (mostly with the test setup) apply for that as well.

  *   Patch 0001 is preparatory work that adds a new internal attribute 
BA_RAW_MPLS_LABEL_STACK (0xfd) that stores the full 24-bit wire values from 
MPLS label fields in BGP NLRI, alongside the existing BA_MPLS_LABEL_STACK 
(0xfe) which stores decoded 20-bit label values.

This needs a specific update for BIRD 3, as the BGP attributes are specified a 
little bit differently there.

net_addr_mpls n = NET_ADDR_MPLS(fec->label); diff –git a/nest/route.h 
b/nest/route.h index 5a9e7fa..3addc17 100644 — a/nest/route.h +++ 
b/nest/route.h @@ -434,6 +434,9 @@ struct nexthop { struct nexthop next; byte 
flags; byte weight; + byte sid6s_orig; / Number of SRv6 SIDs before hostentry 
was applied / + byte sid6s; / Number of all SRv6 SIDs / + ip6_addr 
sid6[SRV6_MAX_SID_STACK]; byte labels_orig; / Number of labels before hostentry 
was applied / byte labels; / Number of all labels */ u32 label[0];

This is unacceptable at all, it adds an awful lot of bytes into a 
memory-constrained data structure, and you run into massive merging problems 
with BIRD 3. You need an EA for this, for sure.

diff –git a/nest/rt-table.c b/nest/rt-table.c index ed364d3..cdcebd5 100644 — 
a/nest/rt-table.c +++ b/nest/rt-table.c @@ -2391,7 +2391,7 @@ 
rt_postconfig(struct config c) /

void -rta_apply_hostentry(rta a, struct hostentry he, mpls_label_stack mls) 
+rta_apply_hostentry(rta a, struct hostentry he, mpls_label_stack mls, 
srv6_sid_stack *sid6)

All the changes in the hostentry logic are going to make a massive merge 
conflict not only with BIRD 3 but also with the (in-progress) igp filter 
feature expected for BIRD 3.3, and this is exactly why we ask contributors to 
check first with the core team before doing something big.

diff –git a/sysdep/linux/netlink-sys.h b/sysdep/linux/netlink-sys.h index 
4c99307..463205f 100644 — a/sysdep/linux/netlink-sys.h +++ 
b/sysdep/linux/netlink-sys.h @@ -18,6 +18,8 @@ #include <linux/lwtunnel.h> 
#endif

+#include <linux/seg6_iptunnel.h> + #ifndef MSG_TRUNC /* Hack: Several versions 
of glibc miss this one :( */ #define MSG_TRUNC 0x20 #endif

This may need an autoconf guard but I have no idea how old this feature 
actually is.

[…] + /* SRH header (8 bytes) / + put_u8(pos, 0); / nexthdr: kernel overwrites 
this / + pos += 1; + put_u8(pos, (srh_len / 8) - 1); / hdrlen in 8-byte units / 
+ pos += 1; + put_u8(pos, 4); / type: SRv6 / + pos += 1; + put_u8(pos, 
sid_count - 1); / segments_left / + pos += 1; + put_u8(pos, sid_count - 1); / 
last_entry/first_segment / + pos += 1; + put_u8(pos, 0); / flags / + pos += 1; 
+ put_u16(pos, 0); / tag */ + pos += 2; […]

I suspect that this would be much easier to read and check if you used a 
structure and assigned to it appropriately. Or, if that is impossible, what 
about the ADVANCE macro from BGP?

  *   ip6_addr __sid = get_ip6(sb + 1);

No double-underscore locals allowed. Looks like, and may collide with, compiler 
internals.

  *

 if (trans_off % 32 + trans_len <= 32) {

  *

   __sid.addr[trans_off / 32] &= ~(((1 << trans_len) - 1) << (32 - trans_len - 
trans_off % 32));

  *

   __sid.addr[trans_off / 32] |= raw_label << (32 - trans_off % 32 - trans_len);

  *

 } else {

  *

   __sid.addr[trans_off / 32] &= ~(((1 << trans_len) - 1) >> (trans_off % 32 + 
trans_len - 32));

  *

   __sid.addr[trans_off / 32] |= raw_label >> (trans_off % 32 + trans_len - 32);

  *
  *

   __sid.addr[trans_off / 32 + 1] &= ~(((1 << trans_len) - 1) << (32 - 
trans_off % 32 - trans_len + 32));

  *

   __sid.addr[trans_off / 32 + 1] |= raw_label << (32 - trans_off % 32 - 
trans_len + 32);

  *

 }

This needs not only a comment but probably a massive comment, and possibly a 
function. All the production builds are with -O2 and -flto anyway, and it’s not 
a shame to name parts of the expression, so that even a junior dev would 
understand what this code is doing.

Thank you for your understanding.
Maria

–
Maria Matejka (she/her) | BIRD Team Leader | CZ.NIC, z.s.p.o.
From 20c5af47299c80c39d111874e0d6b28e4cfefc60 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=A9bastien=20Parisot?= <[email protected]>
Date: Thu, 12 Feb 2026 16:09:45 +0000
Subject: [PATCH 3/6] netlink: Add SEG6 encapsulation support for SRv6

Add SEG6 SRH encoding/decoding for SRv6 SID installation to the Linux
kernel via Netlink. Includes nl_add_attr_seg6_srh(),
nl_add_attr_seg6_encap(), nl_parse_seg6_encap() and encap_seg6_want[]
table.

SRv6 takes precedence over MPLS when both are present. Handle SEG6
in nl_add_nexthop(), nl_parse_multipath() and nl_parse_route().
---
 sysdep/linux/netlink-sys.h |   2 +
 sysdep/linux/netlink.c     | 147 +++++++++++++++++++++++++++++++++----
 2 files changed, 136 insertions(+), 13 deletions(-)

diff --git a/sysdep/linux/netlink-sys.h b/sysdep/linux/netlink-sys.h
index 4c99307..463205f 100644
--- a/sysdep/linux/netlink-sys.h
+++ b/sysdep/linux/netlink-sys.h
@@ -18,6 +18,8 @@
 #include <linux/lwtunnel.h>
 #endif
 
+#include <linux/seg6_iptunnel.h>
+
 #ifndef MSG_TRUNC			/* Hack: Several versions of glibc miss this one :( */
 #define MSG_TRUNC 0x20
 #endif
diff --git a/sysdep/linux/netlink.c b/sysdep/linux/netlink.c
index 299f132..e76a6b6 100644
--- a/sysdep/linux/netlink.c
+++ b/sysdep/linux/netlink.c
@@ -394,6 +394,12 @@ static struct nl_want_attrs nexthop_attr_want_mpls[BIRD_RTA_MAX] = {
 static struct nl_want_attrs encap_mpls_want[BIRD_RTA_MAX] = {
   [RTA_DST]       = { 1, 0, 0 },
 };
+
+#define BIRD_SEG6_IPTUNNEL_MAX (SEG6_IPTUNNEL_SRH+1)
+
+static struct nl_want_attrs encap_seg6_want[BIRD_SEG6_IPTUNNEL_MAX] = {
+  [SEG6_IPTUNNEL_SRH] = { 1, 0, 0 },
+};
 #endif
 
 static struct nl_want_attrs rtm_attr_want4[BIRD_RTA_MAX] = {
@@ -619,6 +625,103 @@ nl_add_attr_mpls_encap(struct nlmsghdr *h, uint bufsize, int len, u32 *stack)
   nl_add_attr_mpls(h, bufsize, RTA_DST, len, stack);
   nl_close_attr(h, nest);
 }
+#endif
+
+static inline void
+nl_add_attr_seg6_srh(struct nlmsghdr *h, uint bufsize, int sid_count, ip6_addr *sids)
+{
+  uint srh_len = sizeof(struct ipv6_sr_hdr) + sid_count * sizeof(struct in6_addr);
+  uint encap_len = sizeof(struct seg6_iptunnel_encap) + srh_len;
+  struct seg6_iptunnel_encap *encap = alloca(encap_len);
+
+  encap->mode = SEG6_IPTUN_MODE_ENCAP;
+
+  struct ipv6_sr_hdr *srh = encap->srh;
+  srh->nexthdr = 0;			/* kernel overwrites this */
+  srh->hdrlen = (srh_len / 8) - 1;	/* in 8-byte units */
+  srh->type = 4;			/* SRv6 */
+  srh->segments_left = sid_count - 1;
+  srh->first_segment = sid_count - 1;	/* last_entry */
+  srh->flags = 0;
+  srh->tag = htons(0);
+
+  for (int i = 0; i < sid_count; i++)
+    put_ip6(&srh->segments[i], sids[i]);
+
+  nl_add_attr(h, bufsize, SEG6_IPTUNNEL_SRH, encap, encap_len);
+}
+
+static inline void
+nl_add_attr_seg6_encap(struct nlmsghdr *h, uint bufsize, int sid_count, ip6_addr *sids)
+{
+  nl_add_attr_u16(h, bufsize, RTA_ENCAP_TYPE, LWTUNNEL_ENCAP_SEG6);
+
+  struct rtattr *nest = nl_open_attr(h, bufsize, RTA_ENCAP);
+  nl_add_attr_seg6_srh(h, bufsize, sid_count, sids);
+  nl_close_attr(h, nest);
+}
+
+/*
+ * Parse SEG6 encapsulation from RTA_ENCAP into SID array.
+ * Returns number of SIDs parsed, or 0 on error.
+ */
+static int
+nl_parse_seg6_encap(struct rtattr *rta_encap, ip6_addr *sids)
+{
+  struct rtattr *enca[BIRD_SEG6_IPTUNNEL_MAX];
+  nl_attr_len = RTA_PAYLOAD(rta_encap);
+  nl_parse_attrs(RTA_DATA(rta_encap), encap_seg6_want, enca, sizeof(enca));
+
+  if (!enca[SEG6_IPTUNNEL_SRH])
+  {
+    log(L_WARN "KRT: Received SEG6 encap with missing SRH attribute");
+    return 0;
+  }
+
+  struct seg6_iptunnel_encap *encap = RTA_DATA(enca[SEG6_IPTUNNEL_SRH]);
+  uint encap_len = RTA_PAYLOAD(enca[SEG6_IPTUNNEL_SRH]);
+
+  if (encap_len < sizeof(struct seg6_iptunnel_encap) + sizeof(struct ipv6_sr_hdr) + sizeof(struct in6_addr))
+  {
+    log(L_WARN "KRT: Received SEG6 encap too short (%d bytes)", encap_len);
+    return 0;
+  }
+
+  struct ipv6_sr_hdr *srh = encap->srh;
+  uint srh_len = encap_len - sizeof(struct seg6_iptunnel_encap);
+
+  /* hdrlen is total SRH length in 8-byte units, minus 1 */
+  uint total_len = (srh->hdrlen + 1) * 8;
+
+  if (total_len > srh_len)
+  {
+    log(L_WARN "KRT: Received SEG6 encap with invalid SRH length (%d)", total_len);
+    return 0;
+  }
+
+  /* first_segment is the index of the last segment, i.e. sid_count - 1 */
+  uint sid_count = srh->first_segment + 1;
+
+  if (sid_count > SRV6_MAX_SID_STACK)
+  {
+    log(L_WARN "KRT: Received SEG6 encap with too many SIDs (%d, max %d supported)",
+	sid_count, SRV6_MAX_SID_STACK);
+    return 0;
+  }
+
+  /* Verify SID list fits within the SRH */
+  if (sizeof(struct ipv6_sr_hdr) + sid_count * sizeof(struct in6_addr) > total_len)
+  {
+    log(L_WARN "KRT: Received SEG6 encap with SID list exceeding SRH (%d SIDs, SRH %d bytes)",
+	sid_count, total_len);
+    return 0;
+  }
+
+  for (uint i = 0; i < sid_count; i++)
+    sids[i] = get_ip6(&srh->segments[i]);
+
+  return sid_count;
+}
 
 static inline void
 nl_add_attr_via(struct nlmsghdr *h, uint bufsize, ip_addr ipa)
@@ -638,7 +741,6 @@ nl_add_attr_via(struct nlmsghdr *h, uint bufsize, ip_addr ipa)
     nl_add_attr(h, bufsize, RTA_VIA, via, sizeof(struct rtvia) + 16);
   }
 }
-#endif
 
 static inline struct rtnexthop *
 nl_open_nexthop(struct nlmsghdr *h, uint bufsize)
@@ -663,12 +765,16 @@ nl_close_nexthop(struct nlmsghdr *h, struct rtnexthop *nh)
 static inline void
 nl_add_nexthop(struct nlmsghdr *h, uint bufsize, struct nexthop *nh, int af UNUSED)
 {
+  if (nh->sid6s > 0)
+    nl_add_attr_seg6_encap(h, bufsize, nh->sid6s, nh->sid6);
+  else if (nh->labels > 0) {
 #ifdef HAVE_MPLS_KERNEL
-  if (nh->labels > 0)
     if (af == AF_MPLS)
       nl_add_attr_mpls(h, bufsize, RTA_NEWDST, nh->labels, nh->label);
     else
       nl_add_attr_mpls_encap(h, bufsize, nh->labels, nh->label);
+#endif
+  }
 
   if (ipa_nonzero(nh->gw))
   {
@@ -677,11 +783,6 @@ nl_add_nexthop(struct nlmsghdr *h, uint bufsize, struct nexthop *nh, int af UNUS
     else
       nl_add_attr_via(h, bufsize, nh->gw);
   }
-#else
-
-  if (ipa_nonzero(nh->gw))
-    nl_add_attr_ipa(h, bufsize, RTA_GATEWAY, nh->gw);
-#endif
 }
 
 static void
@@ -806,17 +907,29 @@ nl_parse_multipath(struct nl_parse_state *s, struct krt_proto *p, const net_addr
 #ifdef HAVE_MPLS_KERNEL
       if (a[RTA_ENCAP] && a[RTA_ENCAP_TYPE])
       {
-	if (rta_get_u16(a[RTA_ENCAP_TYPE]) != LWTUNNEL_ENCAP_MPLS)
+	switch (rta_get_u16(a[RTA_ENCAP_TYPE]))
 	{
+	case LWTUNNEL_ENCAP_MPLS:
+	  {
+	    struct rtattr *enca[BIRD_RTA_MAX];
+	    nl_attr_len = RTA_PAYLOAD(a[RTA_ENCAP]);
+	    nl_parse_attrs(RTA_DATA(a[RTA_ENCAP]), encap_mpls_want, enca, sizeof(enca));
+	    rv->labels = rta_get_mpls(enca[RTA_DST], rv->label);
+	    break;
+	  }
+	case LWTUNNEL_ENCAP_SEG6:
+	  {
+	    int sid_count = nl_parse_seg6_encap(a[RTA_ENCAP], rv->sid6);
+	    if (!sid_count)
+	      return NULL;
+	    rv->sid6s = sid_count;
+	    break;
+	  }
+	default:
 	  log(L_WARN "KRT: Received route %N with unknown encapsulation method %d",
 	      n, rta_get_u16(a[RTA_ENCAP_TYPE]));
 	  return NULL;
 	}
-
-	struct rtattr *enca[BIRD_RTA_MAX];
-	nl_attr_len = RTA_PAYLOAD(a[RTA_ENCAP]);
-	nl_parse_attrs(RTA_DATA(a[RTA_ENCAP]), encap_mpls_want, enca, sizeof(enca));
-	rv->labels = rta_get_mpls(enca[RTA_DST], rv->label);
       }
 #endif
 
@@ -1792,6 +1905,14 @@ nl_parse_route(struct nl_parse_state *s, struct nlmsghdr *h)
 	      ra->nh.labels = rta_get_mpls(enca[RTA_DST], ra->nh.label);
 	      break;
 	    }
+          case LWTUNNEL_ENCAP_SEG6:
+            {
+              int sid_count = nl_parse_seg6_encap(a[RTA_ENCAP], ra->nh.sid6);
+              if (!sid_count)
+                return;
+              ra->nh.sid6s = sid_count;
+              break;
+            }
 	  default:
 	    SKIP("unknown encapsulation method %d\n", rta_get_u16(a[RTA_ENCAP_TYPE]));
 	    break;
-- 
2.39.5

From a704740dee08eca7a28d4da9c4e70403aed3efe8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=A9bastien=20Parisot?= <[email protected]>
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 | 104 ++++++--
 5 files changed, 705 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..c305265 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, ra->nh.sid6[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..1118573 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,30 @@ 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);
+    if (bgp_prefix_sid_get_srv6_l3vpn_sid(a->eattrs, &a->nh.sid6[0])) {
+      a->nh.labels = 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 +1371,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 +1661,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 +1672,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 +1732,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 +1768,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 +1806,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 +1828,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 +1894,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 +1916,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 +1981,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 +2003,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 +2081,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 +2103,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 +2181,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 +2276,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 +2511,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 +2555,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 +2609,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 +2639,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 +2663,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.39.5

Reply via email to