Hello,

I'm sending a new version of the SRv6 patch set that addresses the memory waste 
in the struct nexthop for non-SRv6 routes. Now the SIDs are stored at the end 
of struct, after the labels flex array, using an inline accessor function.

I think it is the best compromise, it has the best memory efficiency and it is 
the least invasive to the existing BIRD code. The only downside is that MPLS 
labels must always be filled before SRv6 SIDs. I also added a new slab index 
for single-SID SRv6 routes.

FYI I had a couple more ideas to solve the memory waste issue, but they bring 
bigger problems:
- Union with the label stack. While in practice VPN routes do not have both 
MPLS labels and SRv6 SIDs, I don't think enforcing mutual exclusion is a good 
idea, many things in the code would need to be changed and it adds a risk for 
the future.
- Pointer to SID stack in struct nexthop. Very invasive because currently the 
struct only contains flat data that can be copied or allocated (for example on 
the stack or at the end of struct rta) without extra allocation.

Thanks.

--
Sébastien

________________________________________
De : Sébastien PARISOT <[email protected]>
Envoyé : mardi 17 février 2026 11:19
À : Maria Matejka
Cc : BIRD Users
Objet : RE: [PATCH] SRv6 support for BIRD

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 58144c9b975e5848ff2082194f0085fc66dc85f2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=A9bastien=20Parisot?= <[email protected]>
Date: Mon, 9 Feb 2026 20:02:34 +0000
Subject: [PATCH 1/6] bgp: Add raw MPLS label stack attribute
 (BA_RAW_MPLS_LABEL_STACK)

Store the full 24-bit wire values from MPLS label stacks in BGP NLRI as
a new internal attribute (BA_RAW_MPLS_LABEL_STACK, 0xfd), alongside the
existing decoded 20-bit labels in BA_MPLS_LABEL_STACK (0xfe).

The raw attribute preserves the exact wire encoding including traffic
class and bottom-of-stack fields. It is populated on decode and visible
via 'show route all' (formatted as hex values). The encode function is
prepared to use raw labels when activated, but the raw path is not
enabled in this patch (it is activated by the SRv6 Prefix SID patch).

This patch does not change existing routing behavior, BGP wire protocol
encoding, or forwarding. The only visible change is the additional
raw_mpls_label_stack attribute in 'show route all' output.
---
 proto/bgp/attrs.c   | 77 +++++++++++++++++++++++++++++++++++++++++++++
 proto/bgp/bgp.h     |  3 ++
 proto/bgp/packets.c | 70 ++++++++++++++++++++++++++++++-----------
 3 files changed, 132 insertions(+), 18 deletions(-)

diff --git a/proto/bgp/attrs.c b/proto/bgp/attrs.c
index 00a7788..acd3eb9 100644
--- a/proto/bgp/attrs.c
+++ b/proto/bgp/attrs.c
@@ -990,6 +990,75 @@ bgp_format_mpls_label_stack(const eattr *a, byte *buf, uint size)
   pos[lnum ? -1 : 0] = 0;
 }
 
+
+static void
+bgp_export_raw_mpls_label_stack(struct bgp_export_state *s, eattr *a)
+{
+  net_addr *n = s->route->net->n.addr;
+  u32 *raw_labels = (u32 *) a->u.ptr->data;
+  uint rlnum = a->u.ptr->length / 4;
+
+  /* Perhaps we should just ignore it? */
+  if (!s->mpls)
+    REJECT("Unexpected raw MPLS stack");
+
+  /* Empty MPLS stack is not allowed */
+  if (!rlnum)
+    REJECT("Malformed raw MPLS stack - empty");
+
+  /* This is ugly, but we must ensure that labels fit into NLRI field */
+  if ((24*rlnum + (net_is_vpn(n) ? 64 : 0) + net_pxlen(n)) > 255)
+    REJECT("Malformed raw MPLS stack - too many labels (%u)", rlnum);
+
+  for (uint i = 0; i < rlnum; i++)
+  {
+    if (raw_labels[i] > 0xffffff)
+      REJECT("Malformed raw MPLS stack - invalid label (0x%x)", raw_labels[i]);
+  }
+}
+
+static int
+bgp_encode_raw_mpls_label_stack(struct bgp_write_state *s, eattr *a, byte *buf UNUSED, uint size UNUSED)
+{
+  /*
+   * Raw MPLS labels are encoded as a part of the NLRI in MP_REACH_NLRI attribute,
+   * so we store RAW_MPLS_LABEL_STACK and encode it later by AFI-specific hooks.
+   */
+
+  s->raw_mpls_labels = a->u.ptr;
+  return 0;
+}
+
+static void
+bgp_decode_raw_mpls_label_stack(struct bgp_parse_state *s, uint code UNUSED, uint flags UNUSED, byte *data UNUSED, uint len UNUSED, ea_list **to UNUSED)
+{
+  DISCARD("Discarding received attribute #0");
+}
+
+static void
+bgp_format_raw_mpls_label_stack(const eattr *a, byte *buf, uint size)
+{
+  u32 *raw_labels = (u32 *) a->u.ptr->data;
+  uint rlnum = a->u.ptr->length / 4;
+  char *pos = buf;
+
+  for (uint i = 0; i < rlnum; i++)
+  {
+    if (size < 20)
+    {
+      bsprintf(pos, "...");
+      return;
+    }
+
+    uint l = bsprintf(pos, "0x%06x/", raw_labels[i]);
+    ADVANCE(pos, size, l);
+  }
+
+  /* Clear last slash or terminate empty string */
+  pos[rlnum ? -1 : 0] = 0;
+}
+
+
 static inline void
 bgp_decode_unknown(struct bgp_parse_state *s, uint code, uint flags, byte *data, uint len, ea_list **to)
 {
@@ -1141,6 +1210,14 @@ static const struct bgp_attr_desc bgp_attr_table[] = {
     .encode = bgp_encode_u32,
     .decode = bgp_decode_otc,
   },
+  [BA_RAW_MPLS_LABEL_STACK] = {
+    .name = "raw_mpls_label_stack",
+    .type = EAF_TYPE_INT_SET,
+    .export = bgp_export_raw_mpls_label_stack,
+    .encode = bgp_encode_raw_mpls_label_stack,
+    .decode = bgp_decode_raw_mpls_label_stack,
+    .format = bgp_format_raw_mpls_label_stack,
+  },
   [BA_MPLS_LABEL_STACK] = {
     .name = "mpls_label_stack",
     .type = EAF_TYPE_INT_SET,
diff --git a/proto/bgp/bgp.h b/proto/bgp/bgp.h
index ea4de2a..96286bc 100644
--- a/proto/bgp/bgp.h
+++ b/proto/bgp/bgp.h
@@ -521,6 +521,7 @@ struct bgp_write_state {
 
   eattr *mp_next_hop;
   const adata *mpls_labels;
+  const adata *raw_mpls_labels;
 };
 
 struct bgp_parse_state {
@@ -560,6 +561,7 @@ struct bgp_parse_state {
 
   struct hostentry *hostentry;
   adata *mpls_labels;
+  adata *raw_mpls_labels;
 
   /* Cached state for bgp_rte_update() */
   u32 last_id;
@@ -788,6 +790,7 @@ byte *bgp_create_end_mark_(struct bgp_channel *c, byte *buf);
 #define BA_ONLY_TO_CUSTOMER	0x23	/* RFC 9234 */
 
 /* Bird's private internal BGP attributes */
+#define BA_RAW_MPLS_LABEL_STACK	0xfd	/* raw MPLS label stack transfer attribute */
 #define BA_MPLS_LABEL_STACK	0xfe	/* MPLS label stack transfer attribute */
 
 /* BGP connection states */
diff --git a/proto/bgp/packets.c b/proto/bgp/packets.c
index eda38ef..2329bb3 100644
--- a/proto/bgp/packets.c
+++ b/proto/bgp/packets.c
@@ -1637,32 +1637,58 @@ 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, byte **pos, uint *size, byte *pxlen)
+bgp_encode_mpls_labels(struct bgp_write_state *s, const adata *mpls, const adata *raw_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;
+  const u32 *raw_labels = raw_mpls ? (const u32 *) raw_mpls->data : &dummy;
   uint lnum = mpls ? (mpls->length / 4) : 1;
+  uint rlnum = raw_mpls ? (raw_mpls->length / 4) : 1;
   uint num;
+  int raw = 0;
 
-  if (lnum == 1 || c->multiple_labels || c->cf->multiple_labels >= BGP_MPLS_ML_ADVERTISE)
+  if (raw)
   {
-    num = lnum;
-    for (uint i = 0; i < lnum; i++)
+    /* raw labels */
+    if (rlnum == 1 || c->multiple_labels || c->cf->multiple_labels >= BGP_MPLS_ML_ADVERTISE)
     {
-      put_u24(*pos, labels[i] << 4);
+      num = rlnum;
+      for (uint i = 0; i < rlnum; i++)
+      {
+        put_u24(*pos, raw_labels[i]);
+        ADVANCE(*pos, *size, 3);
+      }
+    }
+    else
+    {
+      num = 1;
+      put_u24(*pos, BGP_MPLS_NULL << 4);
       ADVANCE(*pos, *size, 3);
     }
   }
   else
   {
-    num = 1;
-    put_u24(*pos, BGP_MPLS_NULL << 4);
-    ADVANCE(*pos, *size, 3);
-  }
+    /* labels */
+    if (lnum == 1 || c->multiple_labels || c->cf->multiple_labels >= BGP_MPLS_ML_ADVERTISE)
+    {
+      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;
+    /* Add bottom-of-stack flag */
+    (*pos)[-1] |= BGP_MPLS_BOS;
+  }
 
   *pxlen += 24 * num;
 }
@@ -1671,8 +1697,8 @@ 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;
+  u32 labels[BGP_MPLS_MAX], raw_labels[BGP_MPLS_MAX], label;
+  uint lnum = 0, rlnum = 0;
 
   do {
     if (*pxlen < 24)
@@ -1683,6 +1709,7 @@ bgp_decode_mpls_labels(struct bgp_parse_state *s, byte **pos, uint *len, uint *p
 
     label = get_u24(*pos);
     labels[lnum++] = label >> 4;
+    raw_labels[rlnum++] = label;
     ADVANCE(*pos, *len, 3);
     *pxlen -= 24;
 
@@ -1706,10 +1733,17 @@ bgp_decode_mpls_labels(struct bgp_parse_state *s, byte **pos, uint *len, uint *p
     s->mpls_labels = lp_alloc_adata(s->pool, 4*BGP_MPLS_MAX);
     bgp_set_attr_ptr(&(a->eattrs), s->pool, BA_MPLS_LABEL_STACK, 0, s->mpls_labels);
   }
+  if (!s->raw_mpls_labels)
+  {
+    s->raw_mpls_labels = lp_alloc_adata(s->pool, 4*BGP_MPLS_MAX);
+    bgp_set_attr_ptr(&(a->eattrs), s->pool, BA_RAW_MPLS_LABEL_STACK, 0, s->raw_mpls_labels);
+  }
 
-  /* Overwrite data in the attribute */
+  /* Overwrite data in the attributes */
   s->mpls_labels->length = 4*lnum;
   memcpy(s->mpls_labels->data, labels, 4*lnum);
+  s->raw_mpls_labels->length = 4*rlnum;
+  memcpy(s->raw_mpls_labels->data, raw_labels, 4*rlnum);
 
   /* Update next hop entry in rta */
   bgp_apply_mpls_labels(s, a, labels, lnum);
@@ -1744,7 +1778,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, &pos, &size, pos - 1);
+      bgp_encode_mpls_labels(s, s->mpls_labels, s->raw_mpls_labels, &pos, &size, pos - 1);
 
     /* Encode prefix body */
     ip4_addr a = ip4_hton(net->prefix);
@@ -1832,7 +1866,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, &pos, &size, pos - 1);
+      bgp_encode_mpls_labels(s, s->mpls_labels, s->raw_mpls_labels, &pos, &size, pos - 1);
 
     /* Encode prefix body */
     ip6_addr a = ip6_hton(net->prefix);
@@ -1919,7 +1953,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, &pos, &size, pos - 1);
+      bgp_encode_mpls_labels(s, s->mpls_labels, s->raw_mpls_labels, &pos, &size, pos - 1);
 
     /* Encode route distinguisher */
     put_rd(pos, net->rd);
@@ -2019,7 +2053,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, &pos, &size, pos - 1);
+      bgp_encode_mpls_labels(s, s->mpls_labels, s->raw_mpls_labels, &pos, &size, pos - 1);
 
     /* Encode route distinguisher */
     put_rd(pos, net->rd);
-- 
2.47.3

From 5a8826a020862ced88d2e4a301978c37bc96572f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=A9bastien=20Parisot?= <[email protected]>
Date: Thu, 12 Feb 2026 16:09:31 +0000
Subject: [PATCH 2/6] nest: Add SRv6 SID stack support to nexthop

Add SRv6 SID stack type and constants (SRV6_MAX_SID_STACK,
SRV6_MAX_SID_STRING), srv6_sid_stack struct, and nexthop fields
(sid6s_orig, sid6s, sid6[]).

Extend nexthop_hash(), nexthop_equal_1(), nexthop__same(),
nexthop_compare_node(), nexthop_copy() and rta_dump() for SRv6 SID
handling. Update rta_apply_hostentry() and rta_set_recursive_next_hop()
signatures with SRv6 parameter.

Add SRv6 SID display in route show output and propagate SRv6 SIDs
through recursive next hop resolution.
---
 conf/confbase.Y | 16 ++++++++++++++++
 lib/ip.h        |  7 +++++++
 nest/mpls.c     |  2 +-
 nest/route.h    | 21 ++++++++++++++-------
 nest/rt-attr.c  | 48 ++++++++++++++++++++++++++++++++++++++++--------
 nest/rt-show.c  | 18 ++++++++++++++----
 nest/rt-table.c | 38 +++++++++++++++++++++++++++++++++++---
 7 files changed, 127 insertions(+), 23 deletions(-)

diff --git a/conf/confbase.Y b/conf/confbase.Y
index 27c422e..3621f37 100644
--- a/conf/confbase.Y
+++ b/conf/confbase.Y
@@ -125,6 +125,7 @@ CF_DECLS
   struct timeformat tf;
   struct timeformat *tfp;
   mpls_label_stack *mls;
+  srv6_sid_stack *sid6;
   const struct adata *bs;
   struct aggr_item_node *ai;
 }
@@ -148,6 +149,7 @@ CF_DECLS
 %type <net> net_ip4_ net_ip4 net_ip6_ net_ip6 net_ip_ net_ip net_or_ipa
 %type <net_ptr> net_ net_any net_vpn4_ net_vpn6_ net_vpn_ net_roa4_ net_roa6_ net_roa_ net_ip6_sadr_ net_mpls_ net_aspa_
 %type <mls> label_stack_start label_stack
+%type <sid6> srv6_sid_stack
 
 %type <t> text opttext
 %type <bs> bytestring
@@ -442,6 +444,20 @@ label_stack:
   }
 ;
 
+srv6_sid_stack: IP6
+{
+  $$ = cfg_allocz(sizeof(srv6_sid_stack));
+  $$->len = 1;
+  $$->sid[0] = ipa_from_ip6($1);
+}
+ | srv6_sid_stack ',' IP6
+{
+  $$ = $1;
+  if ($$->len >= SRV6_MAX_SID_STACK)
+    cf_error("Too many SRv6 SIDs (max %d)", SRV6_MAX_SID_STACK);
+  $$->sid[$$->len++] = ipa_from_ip6($3);
+}
+;
 
 /* Strings */
 
diff --git a/lib/ip.h b/lib/ip.h
index bae0526..7f3c8cc 100644
--- a/lib/ip.h
+++ b/lib/ip.h
@@ -393,6 +393,13 @@ static inline ip6_addr ip6_ntoh(ip6_addr a)
 { return _MI6(ntohl(_I0(a)), ntohl(_I1(a)), ntohl(_I2(a)), ntohl(_I3(a))); }
 
 
+#define SRV6_MAX_SID_STACK 1
+#define SRV6_MAX_SID_STRING (8 /* " srv6 <" */ + IP6_MAX_TEXT_LENGTH + (1 /* "," */ + IP6_MAX_TEXT_LENGTH) * (SRV6_MAX_SID_STACK - 1) + 1 /* ">" */ + 1 /* '\0' */)
+typedef struct srv6_sid_stack {
+  uint len;
+  ip6_addr sid[SRV6_MAX_SID_STACK];
+} srv6_sid_stack;
+
 /*
  *	Unaligned data access (in network order)
  */
diff --git a/nest/mpls.c b/nest/mpls.c
index 9cdcd57..20aa09e 100644
--- a/nest/mpls.c
+++ b/nest/mpls.c
@@ -1065,7 +1065,7 @@ mpls_announce_fec(struct mpls_fec_map *m, struct mpls_fec *fec, const rta *src)
 
     /* The same hostentry, but different dependent table */
     struct hostentry *s = src->hostentry;
-    rta_set_recursive_next_hop(m->channel->table, a, s->owner, s->addr, s->link, &ms);
+    rta_set_recursive_next_hop(m->channel->table, a, s->owner, s->addr, s->link, &ms, NULL);
   }
 
   net_addr_mpls n = NET_ADDR_MPLS(fec->label);
diff --git a/nest/route.h b/nest/route.h
index 5a9e7fa..0450d6b 100644
--- a/nest/route.h
+++ b/nest/route.h
@@ -434,11 +434,18 @@ 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 */
   byte labels_orig;			/* Number of labels before hostentry was applied */
   byte labels;				/* Number of all labels */
   u32 label[0];
+  /* ip6_addr sid6[0] -- after label[], access via nexthop_sid6() */
 };
 
+/* Access SRv6 SIDs stored after MPLS labels in the flexible array */
+static inline ip6_addr *nexthop_sid6(const struct nexthop *nh)
+{ return (ip6_addr *)(&nh->label[nh->labels]); }
+
 #define RNF_ONLINK		0x1	/* Gateway is onlink regardless of IP ranges */
 
 
@@ -691,10 +698,10 @@ ea_set_attr_data(ea_list **to, struct linpool *pool, uint id, uint flags, uint t
 }
 
 
-#define NEXTHOP_MAX_SIZE (sizeof(struct nexthop) + sizeof(u32)*MPLS_MAX_LABEL_STACK)
+#define NEXTHOP_MAX_SIZE (sizeof(struct nexthop) + sizeof(u32)*MPLS_MAX_LABEL_STACK + sizeof(ip6_addr)*SRV6_MAX_SID_STACK)
 
 static inline size_t nexthop_size(const struct nexthop *nh)
-{ return sizeof(struct nexthop) + sizeof(u32)*nh->labels; }
+{ return sizeof(struct nexthop) + sizeof(u32)*nh->labels + sizeof(ip6_addr)*nh->sid6s; }
 int nexthop__same(struct nexthop *x, struct nexthop *y); /* Compare multipath nexthops */
 static inline int nexthop_same(struct nexthop *x, struct nexthop *y)
 { return (x == y) || nexthop__same(x, y); }
@@ -709,8 +716,8 @@ void nexthop_insert(struct nexthop **n, struct nexthop *y);
 int nexthop_is_sorted(struct nexthop *x);
 
 void rta_init(void);
-static inline size_t rta_size(const rta *a) { return sizeof(rta) + sizeof(u32)*a->nh.labels; }
-#define RTA_MAX_SIZE (sizeof(rta) + sizeof(u32)*MPLS_MAX_LABEL_STACK)
+static inline size_t rta_size(const rta *a) { return sizeof(rta) + sizeof(u32)*a->nh.labels + sizeof(ip6_addr)*a->nh.sid6s; }
+#define RTA_MAX_SIZE (sizeof(rta) + sizeof(u32)*MPLS_MAX_LABEL_STACK + sizeof(ip6_addr)*SRV6_MAX_SID_STACK)
 rta *rta_lookup(rta *);			/* Get rta equivalent to this one, uc++ */
 static inline int rta_is_cached(rta *r) { return r->cached; }
 static inline rta *rta_clone(rta *r) { r->uc++; return r; }
@@ -724,12 +731,12 @@ void rta_show(struct cli *, rta *);
 
 u32 rt_get_igp_metric(rte *rt);
 struct hostentry * rt_get_hostentry(rtable *tab, ip_addr a, ip_addr ll, rtable *dep);
-void rta_apply_hostentry(rta *a, struct hostentry *he, mpls_label_stack *mls);
+void rta_apply_hostentry(rta *a, struct hostentry *he, mpls_label_stack *mls, srv6_sid_stack *sid6);
 
 static inline void
-rta_set_recursive_next_hop(rtable *dep, rta *a, rtable *tab, ip_addr gw, ip_addr ll, mpls_label_stack *mls)
+rta_set_recursive_next_hop(rtable *dep, rta *a, rtable *tab, ip_addr gw, ip_addr ll, mpls_label_stack *mls, srv6_sid_stack *sid6)
 {
-  rta_apply_hostentry(a, rt_get_hostentry(tab, gw, ll, dep), mls);
+  rta_apply_hostentry(a, rt_get_hostentry(tab, gw, ll, dep), mls, sid6);
 }
 
 /*
diff --git a/nest/rt-attr.c b/nest/rt-attr.c
index e10e1ec..b3c5930 100644
--- a/nest/rt-attr.c
+++ b/nest/rt-attr.c
@@ -90,8 +90,8 @@ const char * rta_dest_names[RTD_MAX] = {
 
 pool *rta_pool;
 
-static slab *rta_slab_[4];
-static slab *nexthop_slab_[4];
+static slab *rta_slab_[5];
+static slab *nexthop_slab_[5];
 static slab *rte_src_slab;
 
 static struct idm src_ids;
@@ -180,6 +180,9 @@ nexthop_hash(struct nexthop *x)
 
     for (int i = 0; i < x->labels; i++)
       h ^= x->label[i] ^ (h << 6) ^ (h >> 7);
+
+    for (int i = 0; i < x->sid6s; i++)
+      h ^= ip6_hash(nexthop_sid6(x)[i]) ^ (h << 7) ^ (h >> 8);
   }
 
   return h;
@@ -190,13 +193,18 @@ nexthop_equal_1(struct nexthop *x, struct nexthop *y)
 {
   if (!ipa_equal(x->gw, y->gw) || (x->iface != y->iface) ||
       (x->flags != y->flags) || (x->weight != y->weight) ||
-      (x->labels != y->labels))
+      (x->labels != y->labels) ||
+      (x->sid6s != y->sid6s))
     return 0;
 
   for (int i = 0; i < x->labels; i++)
     if (x->label[i] != y->label[i])
       return 0;
 
+  for (int i = 0; i < x->sid6s; i++)
+    if (memcmp(&nexthop_sid6(x)[i], &nexthop_sid6(y)[i], sizeof(ip6_addr)) != 0)
+      return 0;
+
   return 1;
 }
 
@@ -217,7 +225,8 @@ nexthop__same(struct nexthop *x, struct nexthop *y)
 {
   for (; x && y; x = x->next, y = y->next)
     if (!nexthop_equal_1(x, y) ||
-	(x->labels_orig != y->labels_orig))
+	(x->labels_orig != y->labels_orig) ||
+        (x->sid6s_orig != y->sid6s_orig))
       return 0;
 
   return x == y;
@@ -255,6 +264,17 @@ nexthop_compare_node(const struct nexthop *x, const struct nexthop *y)
       return r;
   }
 
+  r = ((int) y->sid6s) - ((int) x->sid6s);
+  if (r)
+    return r;
+
+  for (int i = 0; i < y->sid6s; i++)
+  {
+    r = ip6_compare(nexthop_sid6(x)[i], nexthop_sid6(y)[i]);
+    if (r)
+      return r;
+  }
+
   return ((int) x->iface->index) - ((int) y->iface->index);
 }
 
@@ -377,7 +397,8 @@ nexthop_is_sorted(struct nexthop *x)
 static inline slab *
 nexthop_slab(struct nexthop *nh)
 {
-  return nexthop_slab_[MIN(nh->labels, 3)];
+  uint slots = nh->labels + nh->sid6s * 4;
+  return nexthop_slab_[slots <= 2 ? slots : slots <= 4 ? 3 : 4];
 }
 
 static struct nexthop *
@@ -398,6 +419,10 @@ nexthop_copy(struct nexthop *o)
       n->labels = o->labels;
       for (int i=0; i<o->labels; i++)
 	n->label[i] = o->label[i];
+      n->sid6s_orig = o->sid6s_orig;
+      n->sid6s = o->sid6s;
+      for (int i=0; i<o->sid6s; i++)
+        memcpy(&nexthop_sid6(n)[i], &nexthop_sid6(o)[i], sizeof(ip6_addr));
 
       *last = n;
       last = &(n->next);
@@ -1193,7 +1218,8 @@ rta_same(rta *x, rta *y)
 static inline slab *
 rta_slab(rta *a)
 {
-  return rta_slab_[a->nh.labels > 2 ? 3 : a->nh.labels];
+  uint slots = a->nh.labels + a->nh.sid6s * 4;
+  return rta_slab_[slots <= 2 ? slots : slots <= 4 ? 3 : 4];
 }
 
 static rta *
@@ -1340,6 +1366,10 @@ rta_dump(struct dump_request *dreq, rta *a)
 	if (nh->labels) RDUMP(" L %d", nh->label[0]);
 	for (int i=1; i<nh->labels; i++)
 	  RDUMP("/%d", nh->label[i]);
+	if (nh->sid6s) RDUMP(" SRv6 <%I6", nexthop_sid6(nh)[0]);
+	for (int i=1; i<nh->sid6s; i++)
+	  RDUMP(",%I6", nexthop_sid6(nh)[i]);
+	if (nh->sid6s) RDUMP(">");
 	RDUMP(" [%s]", nh->iface ? nh->iface->name : "???");
       }
   if (a->eattrs)
@@ -1396,12 +1426,14 @@ rta_init(void)
   rta_slab_[0] = sl_new(rta_pool, sizeof(rta));
   rta_slab_[1] = sl_new(rta_pool, sizeof(rta) + sizeof(u32));
   rta_slab_[2] = sl_new(rta_pool, sizeof(rta) + sizeof(u32)*2);
-  rta_slab_[3] = sl_new(rta_pool, sizeof(rta) + sizeof(u32)*MPLS_MAX_LABEL_STACK);
+  rta_slab_[3] = sl_new(rta_pool, sizeof(rta) + sizeof(ip6_addr));
+  rta_slab_[4] = sl_new(rta_pool, RTA_MAX_SIZE);
 
   nexthop_slab_[0] = sl_new(rta_pool, sizeof(struct nexthop));
   nexthop_slab_[1] = sl_new(rta_pool, sizeof(struct nexthop) + sizeof(u32));
   nexthop_slab_[2] = sl_new(rta_pool, sizeof(struct nexthop) + sizeof(u32)*2);
-  nexthop_slab_[3] = sl_new(rta_pool, sizeof(struct nexthop) + sizeof(u32)*MPLS_MAX_LABEL_STACK);
+  nexthop_slab_[3] = sl_new(rta_pool, sizeof(struct nexthop) + sizeof(ip6_addr));
+  nexthop_slab_[4] = sl_new(rta_pool, NEXTHOP_MAX_SIZE);
 
   rta_alloc_hash();
   rte_src_init();
diff --git a/nest/rt-show.c b/nest/rt-show.c
index 0ecc863..4c810e1 100644
--- a/nest/rt-show.c
+++ b/nest/rt-show.c
@@ -73,6 +73,7 @@ rt_show_rte(struct cli *c, byte *ia, rte *e, struct rt_show_data *d, int primary
     for (nh = &(a->nh); nh; nh = nh->next)
     {
       char mpls[MPLS_MAX_LABEL_STRING], *lsp = mpls;
+      char srv6[SRV6_MAX_SID_STRING], *sid6 = srv6;
       char *onlink = (nh->flags & RNF_ONLINK) ? " onlink" : "";
       char weight[16] = "";
 
@@ -84,15 +85,24 @@ rt_show_rte(struct cli *c, byte *ia, rte *e, struct rt_show_data *d, int primary
 	}
       *lsp = '\0';
 
+      if (nh->sid6s)
+	{
+	  sid6 += bsprintf(sid6, " srv6 <%I6", nexthop_sid6(nh)[0]);
+	  for (int i = 1; i < nh->sid6s; ++i)
+	    sid6 += bsprintf(sid6, ",%I6", nexthop_sid6(nh)[i]);
+	  sid6 += bsprintf(sid6, ">");
+	}
+      *sid6 = '\0';
+
       if (a->nh.next)
 	bsprintf(weight, " weight %d", nh->weight + 1);
 
       if (ipa_nonzero(nh->gw))
-	cli_printf(c, -1007, "\tvia %I on %s%s%s%s",
-		   nh->gw, nh->iface->name, mpls, onlink, weight);
+	cli_printf(c, -1007, "\tvia %I on %s%s%s%s%s",
+		   nh->gw, nh->iface->name, mpls, srv6, onlink, weight);
       else
-	cli_printf(c, -1007, "\tdev %s%s%s",
-		   nh->iface->name, mpls,  onlink, weight);
+	cli_printf(c, -1007, "\tdev %s%s%s%s",
+		   nh->iface->name, mpls, srv6, onlink, weight);
     }
 
   if (d->verbose)
diff --git a/nest/rt-table.c b/nest/rt-table.c
index ed364d3..f720265 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)
 {
   a->hostentry = he;
   a->dest = he->dest;
@@ -2407,10 +2407,15 @@ no_nexthop:
       a->nh.labels_orig = a->nh.labels = mls->len;
       memcpy(a->nh.label, mls->stack, mls->len * sizeof(u32));
     }
+    if (sid6)
+    {
+      a->nh.sid6s_orig = a->nh.sid6s = sid6->len;
+      memcpy(nexthop_sid6(&a->nh), sid6->sid, sid6->len * sizeof(sid6->sid[0]));
+    }
     return;
   }
 
-  if (((!mls) || (!mls->len)) && he->nexthop_linkable)
+  if ((((!mls) || (!mls->len)) && (!(sid6) || !(sid6->len))) && he->nexthop_linkable)
   { /* Just link the nexthop chain, no label append happens. */
     memcpy(&(a->nh), &(he->src->nh), nexthop_size(&(he->src->nh)));
     return;
@@ -2457,6 +2462,30 @@ no_nexthop:
       memcpy(nhp->label, nh->label, nh->labels * sizeof(u32));
     }
 
+    if (sid6)
+    {
+      nhp->sid6s = nh->sid6s + sid6->len;
+      nhp->sid6s_orig = sid6->len;
+      if (nhp->sid6s <= SRV6_MAX_SID_STACK)
+      {
+        memcpy(nexthop_sid6(nhp), nexthop_sid6(nh), nh->sid6s * sizeof(ip6_addr)); /* First the hostentry SIDs */
+        memcpy(&(nexthop_sid6(nhp)[nh->sid6s]), sid6->sid, sid6->len * sizeof(ip6_addr)); /* Then the bottom SIDs */
+      }
+      else
+      {
+        log(L_WARN "Sum of SID stack sizes %d + %d = %d exceedes allowed maximum (%d)",
+            nh->sid6s, sid6->len, nhp->sid6s, SRV6_MAX_SID_STACK);
+        skip_nexthop++;
+        continue;
+      }
+    }
+    else if (nh->sid6s)
+    {
+      nhp->sid6s = nh->sid6s;
+      nhp->sid6s_orig = 0;
+      memcpy(nexthop_sid6(nhp), nexthop_sid6(nh), nhp->sid6s * sizeof(ip6_addr));
+    }
+
     if (ipa_nonzero(nh->gw))
     {
       nhp->gw = nh->gw;			/* Router nexthop */
@@ -2508,7 +2537,10 @@ rt_next_hop_update_rte(rtable *tab UNUSED, rte *old)
   mpls_label_stack mls = { .len = a->nh.labels_orig };
   memcpy(mls.stack, &a->nh.label[a->nh.labels - mls.len], mls.len * sizeof(u32));
 
-  rta_apply_hostentry(a, old->attrs->hostentry, &mls);
+  srv6_sid_stack sid6 = { .len = a->nh.sid6s_orig };
+  memcpy(sid6.sid, &nexthop_sid6(&a->nh)[a->nh.sid6s - sid6.len], sid6.len * sizeof(sid6.sid[0]));
+
+  rta_apply_hostentry(a, old->attrs->hostentry, &mls, &sid6);
   a->cached = 0;
 
   rte *e = sl_alloc(rte_slab);
-- 
2.47.3

From fe3bb31bf240ab9cf6e1b85a56af7e6977a11c40 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..0a344b8 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, nexthop_sid6(nh));
+  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], nexthop_sid6(rv));
+	    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], nexthop_sid6(&ra->nh));
+              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.47.3

From 3d99b922ecf8e9f682dcccc9ccc39e90ad254f31 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=A9bastien=20Parisot?= <[email protected]>
Date: Thu, 12 Feb 2026 16:12:46 +0000
Subject: [PATCH 4/6] static: Add SRv6 SID support for static routes

Allow configuring SRv6 SIDs on static routes, both for direct nexthops
(via ... srv6 <sid>) and recursive routes (recursive ... srv6 <sid>).
SRv6 SIDs are propagated to the nexthop and through recursive next hop
resolution, and are compared when detecting configuration changes.
---
 doc/bird.sgml         |  2 +-
 proto/static/config.Y | 10 +++++++++-
 proto/static/static.c | 41 ++++++++++++++++++++++++++++-------------
 proto/static/static.h |  5 ++++-
 4 files changed, 42 insertions(+), 16 deletions(-)

diff --git a/doc/bird.sgml b/doc/bird.sgml
index 5761460..32c770b 100644
--- a/doc/bird.sgml
+++ b/doc/bird.sgml
@@ -6526,7 +6526,7 @@ each labeled route.
 <ref id="type-prefix" name="dependent on network type">.
 
 <descrip>
-	<tag><label id="static-route-regular">route <m/prefix/ [mpls <m/number/] via <m/ip/|<m/"interface"/ [<m/per-nexthop options/] [via ...]</tag>
+	<tag><label id="static-route-regular">route <m/prefix/ [mpls <m/number/] via <m/ip/|<m/"interface"/ [srv6 <m/srv6-sid-stack/] [<m/per-nexthop options/] [via ...]</tag>
 	Regular routes may bear one or more <ref id="route-next-hop" name="next
 	hops">. Every next hop is preceded by <cf/via/ and configured as shown.
 
diff --git a/proto/static/config.Y b/proto/static/config.Y
index f840947..1f0d1f6 100644
--- a/proto/static/config.Y
+++ b/proto/static/config.Y
@@ -47,7 +47,7 @@ static_route_finish(void)
 CF_DECLS
 
 CF_KEYWORDS(STATIC, ROUTE, VIA, DROP, REJECT, PROHIBIT, PREFERENCE, CHECK, LINK, DEV)
-CF_KEYWORDS(ONLINK, WEIGHT, RECURSIVE, IGP, TABLE, BLACKHOLE, UNREACHABLE, BFD, MPLS)
+CF_KEYWORDS(ONLINK, WEIGHT, RECURSIVE, IGP, TABLE, BLACKHOLE, UNREACHABLE, BFD, MPLS, SRV6)
 CF_KEYWORDS(TRANSIT, PROVIDERS)
 
 
@@ -97,6 +97,9 @@ stat_nexthop:
   | stat_nexthop MPLS label_stack {
     this_snh->mls = $3;
   }
+  | stat_nexthop SRV6 srv6_sid_stack {
+    this_snh->sid6 = $3;
+  }
   | stat_nexthop ONLINK bool {
     this_snh->onlink = $3;
     if (this_snh->use_bfd && this_snh->onlink)
@@ -146,6 +149,11 @@ stat_route:
       this_srt->via = $3;
       this_srt->mls = $5;
    }
+ | stat_route0 RECURSIVE ipa SRV6 srv6_sid_stack {
+      this_srt->dest = RTDX_RECURSIVE;
+      this_srt->via = $3;
+      this_srt->sid6 = $5;
+   }
  | stat_route0			{ this_srt->dest = RTD_NONE; }
  | stat_route0 DROP		{ this_srt->dest = RTD_BLACKHOLE; }
  | stat_route0 REJECT		{ this_srt->dest = RTD_UNREACHABLE; }
diff --git a/proto/static/static.c b/proto/static/static.c
index 65fdb70..b68b096 100644
--- a/proto/static/static.c
+++ b/proto/static/static.c
@@ -83,6 +83,11 @@ static_announce_rte(struct static_proto *p, struct static_route *r)
 	nh->labels = r2->mls->len;
 	memcpy(nh->label, r2->mls->stack, r2->mls->len * sizeof(u32));
       }
+      if (r2->sid6)
+      {
+        nh->sid6s = r2->sid6->len;
+        memcpy(nexthop_sid6(nh), r2->sid6->sid, r2->sid6->len * sizeof(ip6_addr));
+      }
 
       nexthop_insert(&nhs, nh);
     }
@@ -96,7 +101,7 @@ static_announce_rte(struct static_proto *p, struct static_route *r)
   if (r->dest == RTDX_RECURSIVE)
   {
     rtable *tab = ipa_is_ip4(r->via) ? p->igp_table_ip4 : p->igp_table_ip6;
-    rta_set_recursive_next_hop(p->p.main_channel->table, a, tab, r->via, IPA_NONE, r->mls);
+    rta_set_recursive_next_hop(p->p.main_channel->table, a, tab, r->via, IPA_NONE, r->mls, r->sid6);
   }
 
   if (r->net->type == NET_ASPA)
@@ -387,30 +392,40 @@ static_same_dest(struct static_route *x, struct static_route *y)
 	  (x->weight != y->weight) ||
 	  (x->use_bfd != y->use_bfd) ||
 	  (!x->mls != !y->mls) ||
-	  ((x->mls) && (y->mls) && (x->mls->len != y->mls->len)))
+	  (!x->sid6 != !y->sid6) ||
+	  ((x->mls) && (y->mls) && (x->mls->len != y->mls->len)) ||
+	  ((x->sid6) && (y->sid6) && (x->sid6->len != y->sid6->len)))
 	return 0;
 
-      if (!x->mls)
-	continue;
+      if (x->mls)
+        for (uint i = 0; i < x->mls->len; i++)
+	  if (x->mls->stack[i] != y->mls->stack[i])
+	    return 0;
 
-      for (uint i = 0; i < x->mls->len; i++)
-	if (x->mls->stack[i] != y->mls->stack[i])
-	  return 0;
+      if (x->sid6)
+        for (uint i = 0; i < x->sid6->len; i++)
+	  if (ip6_compare(x->sid6->sid[i], y->sid6->sid[i]) != 0)
+	    return 0;
     }
     return !x && !y;
 
   case RTDX_RECURSIVE:
     if (!ipa_equal(x->via, y->via) ||
 	(!x->mls != !y->mls) ||
-	((x->mls) && (y->mls) && (x->mls->len != y->mls->len)))
+	(!x->sid6 != !y->sid6) ||
+	((x->mls) && (y->mls) && (x->mls->len != y->mls->len)) ||
+	((x->sid6) && (y->sid6) && (x->sid6->len != y->sid6->len)))
       return 0;
 
-    if (!x->mls)
-      return 1;
+    if (x->mls)
+      for (uint i = 0; i < x->mls->len; i++)
+        if (x->mls->stack[i] != y->mls->stack[i])
+	  return 0;
 
-    for (uint i = 0; i < x->mls->len; i++)
-      if (x->mls->stack[i] != y->mls->stack[i])
-	return 0;
+    if (x->sid6)
+      for (uint i = 0; i < x->sid6->len; i++)
+        if (ip6_compare(x->sid6->sid[i], y->sid6->sid[i]) != 0)
+	  return 0;
 
     return 1;
 
diff --git a/proto/static/static.h b/proto/static/static.h
index edd2dc2..d7dd80a 100644
--- a/proto/static/static.h
+++ b/proto/static/static.h
@@ -51,7 +51,10 @@ struct static_route {
   uint mpls_label;			/* Local MPLS label, -1 if unused */
   struct bfd_request *bfd_req;		/* BFD request, if BFD is used */
   union {
-    mpls_label_stack *mls;		/* MPLS label stack; may be NULL */
+    struct {
+      mpls_label_stack *mls;		/* MPLS label stack; may be NULL */
+      srv6_sid_stack *sid6;		/* SRv6 SID; may be NULL */
+    };
     adata *aspa;			/* ASPA provider list; may be NULL */
   };
 };
-- 
2.47.3

From eefa51f26b7a3f9d1c5befae862eb059a06eed8b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=A9bastien=20Parisot?= <[email protected]>
Date: Mon, 9 Feb 2026 20:15:59 +0000
Subject: [PATCH 5/6] l3vpn: Make MPLS channel optional for SRv6 deployments

Guard all P->mpls_channel operations with NULL checks and remove
mandatory MPLS channel check in l3vpn_postconfig(). This allows
L3VPN to operate without an MPLS channel when using SRv6.
---
 proto/l3vpn/l3vpn.c | 25 +++++++++++++++----------
 1 file changed, 15 insertions(+), 10 deletions(-)

diff --git a/proto/l3vpn/l3vpn.c b/proto/l3vpn/l3vpn.c
index ece79d5..dbb1b80 100644
--- a/proto/l3vpn/l3vpn.c
+++ b/proto/l3vpn/l3vpn.c
@@ -227,8 +227,11 @@ l3vpn_rt_notify(struct proto *P, struct channel *c0, net *net, rte *new, rte *ol
 
     if (export)
     {
-      struct mpls_channel *mc = (void *) p->p.mpls_channel;
-      ea_set_attr_u32(&a->eattrs, tmp_linpool, EA_MPLS_POLICY, 0, EAF_TYPE_INT, mc->label_policy);
+      if (P->mpls_channel)
+      {
+        struct mpls_channel *mc = (void *) p->p.mpls_channel;
+        ea_set_attr_u32(&a->eattrs, tmp_linpool, EA_MPLS_POLICY, 0, EAF_TYPE_INT, mc->label_policy);
+      }
 
       struct adata *ad = l3vpn_export_targets(p, ea_get_adata(a0->eattrs, EA_BGP_EXT_COMMUNITY));
       ea_set_attr_ptr(&a->eattrs, tmp_linpool, EA_BGP_EXT_COMMUNITY, BAF_OPTIONAL | BAF_TRANSITIVE, EAF_TYPE_EC_SET, ad);
@@ -338,9 +341,6 @@ l3vpn_postconfig(struct proto_config *CF)
   if (!!proto_cf_find_channel(CF, NET_IP6) != !!proto_cf_find_channel(CF, NET_VPN6))
     cf_error("For IPv6 L3VPN, both IPv6 and VPNv6 channels must be specified");
 
-  if (!proto_cf_find_channel(CF, NET_MPLS))
-    cf_error("MPLS channel not specified");
-
   if (rd_zero(cf->rd))
     cf_error("Route distinguisher not specified");
 
@@ -389,10 +389,13 @@ l3vpn_start(struct proto *P)
   l3vpn_prepare_import_targets(p);
   l3vpn_prepare_export_targets(p);
 
-  proto_setup_mpls_map(P, RTS_L3VPN, 1);
+  if (P->mpls_channel)
+  {
+    proto_setup_mpls_map(P, RTS_L3VPN, 1);
 
-  if (P->vrf_set)
-    P->mpls_map->vrf_iface = P->vrf;
+    if (P->vrf_set)
+      P->mpls_map->vrf_iface = P->vrf;
+  }
 
   return PS_UP;
 }
@@ -402,7 +405,8 @@ l3vpn_shutdown(struct proto *P)
 {
   // struct l3vpn_proto *p = (void *) P;
 
-  proto_shutdown_mpls_map(P, 1);
+  if (P->mpls_channel)
+    proto_shutdown_mpls_map(P, 1);
 
   return PS_DOWN;
 }
@@ -430,7 +434,8 @@ l3vpn_reconfigure(struct proto *P, struct proto_config *CF)
   p->import_target = cf->import_target;
   p->export_target = cf->export_target;
 
-  proto_setup_mpls_map(P, RTS_L3VPN, 1);
+  if (P->mpls_channel)
+    proto_setup_mpls_map(P, RTS_L3VPN, 1);
 
   if (import_changed)
   {
-- 
2.47.3

From 601926436341b43ed309f53ddd52a01c99d17efb 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 | 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

Reply via email to